New home view layout (#58)

* Home layout refactor, moved new download to dialog

* sort downloads by date
This commit is contained in:
Marco
2023-06-23 10:48:38 +02:00
committed by GitHub
parent 13cc89fe3b
commit 2ae4a5da3d
10 changed files with 451 additions and 300 deletions

View File

@@ -1,40 +1,30 @@
import { FileUpload } from '@mui/icons-material'
import AddCircleIcon from '@mui/icons-material/AddCircle'
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
import {
Alert,
Backdrop,
Button,
CircularProgress,
Container,
FormControl,
Grid,
IconButton,
InputAdornment,
InputLabel,
MenuItem,
Paper,
Select,
Snackbar,
SpeedDial,
SpeedDialAction,
SpeedDialIcon,
styled,
TextField
styled
} from '@mui/material'
import { Buffer } from 'buffer'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import DownloadDialog from '../components/DownloadDialog'
import { DownloadsCardView } from '../components/DownloadsCardView'
import { DownloadsListView } from '../components/DownloadsListView'
import FormatsGrid from '../components/FormatsGrid'
import { CliArguments } from '../lib/argsParser'
import I18nBuilder from '../lib/intl'
import { RPCClient, socket$ } from '../lib/rpcClient'
import Splash from '../components/Splash'
import { toggleListView } from '../features/settings/settingsSlice'
import { connected, setFreeSpace } from '../features/status/statusSlice'
import I18nBuilder from '../lib/intl'
import { RPCClient, socket$ } from '../lib/rpcClient'
import { RootState } from '../stores/store'
import type { DLMetadata, RPCResponse, RPCResult } from '../types'
import { isValidURL, toFormatArgs } from '../utils'
import type { RPCResponse, RPCResult } from '../types'
import { dateTimeComparatorFunc } from '../utils'
export default function Home() {
// redux state
@@ -43,34 +33,17 @@ export default function Home() {
const dispatch = useDispatch()
// ephemeral state
const [activeDownloads, setActiveDownloads] = useState<Array<RPCResult>>()
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
const [pickedVideoFormat, setPickedVideoFormat] = useState('')
const [pickedAudioFormat, setPickedAudioFormat] = useState('')
const [pickedBestFormat, setPickedBestFormat] = useState('')
const [customArgs, setCustomArgs] = useState('')
const [downloadPath, setDownloadPath] = useState(0)
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([])
const [fileNameOverride, setFilenameOverride] = useState('')
const [url, setUrl] = useState('')
const [workingUrl, setWorkingUrl] = useState('')
const [activeDownloads, setActiveDownloads] = useState<RPCResult[]>()
const [showBackdrop, setShowBackdrop] = useState(true)
const [showToast, setShowToast] = useState(true)
const [openDialog, setOpenDialog] = useState(false)
const [socketHasError, setSocketHasError] = useState(false)
// memos
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
const client = useMemo(() => new RPCClient(), [settings.serverAddr, settings.serverPort])
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs])
// refs
const urlInputRef = useRef<HTMLInputElement>(null)
const customFilenameInputRef = useRef<HTMLInputElement>(null)
/* -------------------- Effects -------------------- */
@@ -81,8 +54,6 @@ export default function Home() {
const sub = socket$.subscribe({
next: () => {
dispatch(connected())
setCustomArgs(localStorage.getItem('last-input-args') ?? '')
setFilenameOverride(localStorage.getItem('last-filename-override') ?? '')
},
error: () => {
setSocketHasError(true)
@@ -117,7 +88,10 @@ export default function Home() {
setActiveDownloads(
(event.result ?? [])
.filter((r) => !!r.info.url)
.sort((a, b) => a.info.title.localeCompare(b.info.title))
.sort((a, b) => dateTimeComparatorFunc(
b.info.created_at,
a.info.created_at,
))
)
break
default:
@@ -133,88 +107,6 @@ export default function Home() {
}
}, [activeDownloads?.length])
useEffect(() => {
client.directoryTree()
.then(data => {
setAvailableDownloadPaths(data.result)
})
}, [])
/* -------------------- callbacks-------------------- */
/**
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
*/
const sendUrl = (immediate?: string) => {
const codes = new Array<string>();
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
client.download(
immediate || url || workingUrl,
`${cliArgs.toString()} ${toFormatArgs(codes)} ${customArgs}`,
availableDownloadPaths[downloadPath] ?? '',
fileNameOverride
)
setUrl('')
setWorkingUrl('')
setShowBackdrop(true)
setTimeout(() => {
resetInput()
setShowBackdrop(true)
setDownloadFormats(undefined)
}, 250)
}
/**
* Retrive url from input and display the formats selection view
*/
const sendUrlFormatSelection = () => {
setWorkingUrl(url)
setUrl('')
setPickedAudioFormat('')
setPickedVideoFormat('')
setPickedBestFormat('')
setShowBackdrop(true)
client.formats(url)
?.then(formats => {
setDownloadFormats(formats.result)
setShowBackdrop(false)
resetInput()
})
}
/**
* Update the url state whenever the input value changes
* @param e Input change event
*/
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrl(e.target.value)
}
/**
* Update the filename override state whenever the input value changes
* @param e Input change event
*/
const handleFilenameOverrideChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilenameOverride(e.target.value)
localStorage.setItem('last-filename-override', e.target.value)
}
/**
* Update the custom args state whenever the input value changes
* @param e Input change event
*/
const handleCustomArgsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCustomArgs(e.target.value)
localStorage.setItem("last-input-args", e.target.value)
}
/**
* Abort a specific download if id's provided, other wise abort all running ones.
* @param id The download id / pid
@@ -228,27 +120,6 @@ export default function Home() {
client.killAll()
}
const parseUrlListFile = (event: any) => {
const urlList = event.target.files
const reader = new FileReader()
reader.addEventListener('load', $event => {
const base64 = $event.target?.result!.toString().split(',')[1]
Buffer.from(base64!, 'base64')
.toString()
.trimEnd()
.split('\n')
.filter(_url => isValidURL(_url))
.forEach(_url => sendUrl(_url))
})
reader.readAsDataURL(urlList[0])
}
const resetInput = () => {
urlInputRef.current!.value = '';
if (customFilenameInputRef.current) {
customFilenameInputRef.current!.value = ''
}
}
/* -------------------- styled components -------------------- */
@@ -264,133 +135,9 @@ export default function Home() {
>
<CircularProgress color="primary" />
</Backdrop>
<Grid container spacing={2}>
<Grid item xs={12}>
<Paper
sx={{
p: 2,
display: 'flex',
flexDirection: 'column',
}}
>
<Grid container>
<TextField
fullWidth
ref={urlInputRef}
label={i18n.t('urlInput')}
variant="outlined"
onChange={handleUrlChange}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<label htmlFor="icon-button-file">
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
<IconButton color="primary" aria-label="upload file" component="span">
<FileUpload />
</IconButton>
</label>
</InputAdornment>
),
}}
/>
</Grid>
<Grid container spacing={1} sx={{ mt: 1 }}>
{
settings.enableCustomArgs &&
<Grid item xs={12}>
<TextField
fullWidth
label={i18n.t('customArgsInput')}
variant="outlined"
onChange={handleCustomArgsChange}
value={customArgs}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid>
}
{
settings.fileRenaming &&
<Grid item xs={8}>
<TextField
ref={customFilenameInputRef}
fullWidth
label={i18n.t('customFilename')}
variant="outlined"
value={fileNameOverride}
onChange={handleFilenameOverrideChange}
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
/>
</Grid>
}
{
settings.pathOverriding &&
<Grid item xs={4}>
<FormControl fullWidth>
<InputLabel>{i18n.t('customPath')}</InputLabel>
<Select
label={i18n.t('customPath')}
defaultValue={0}
variant={'outlined'}
value={downloadPath}
onChange={(e) => setDownloadPath(Number(e.target.value))}
>
{availableDownloadPaths.map((val: string, idx: number) => (
<MenuItem key={idx} value={idx}>{val}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
}
</Grid>
<Grid container spacing={1} pt={2}>
<Grid item>
<Button
variant="contained"
disabled={url === ''}
onClick={() => settings.formatSelection ? sendUrlFormatSelection() : sendUrl()}
>
{settings.formatSelection ? i18n.t('selectFormatButton') : i18n.t('startButton')}
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
onClick={() => abort()}
>
{i18n.t('abortAllButton')}
</Button>
</Grid>
</Grid>
</Paper>
</Grid>
</Grid >
{/* Format Selection grid */}
{downloadFormats && <FormatsGrid
downloadFormats={downloadFormats}
onBestQualitySelected={(id) => {
setPickedBestFormat(id)
setPickedVideoFormat('')
setPickedAudioFormat('')
}}
onVideoSelected={(id) => {
setPickedVideoFormat(id)
setPickedBestFormat('')
}}
onAudioSelected={(id) => {
setPickedAudioFormat(id)
setPickedBestFormat('')
}}
onClear={() => {
setPickedAudioFormat('');
setPickedVideoFormat('');
setPickedBestFormat('');
}}
onSubmit={sendUrl}
pickedBestFormat={pickedBestFormat}
pickedVideoFormat={pickedVideoFormat}
pickedAudioFormat={pickedAudioFormat}
/>}
{activeDownloads?.length === 0 &&
<Splash />
}
{
settings.listView ?
<DownloadsListView downloads={activeDownloads ?? []} abortFunction={abort} /> :
@@ -418,10 +165,25 @@ export default function Home() {
<SpeedDialAction
icon={<FormatListBulleted />}
tooltipTitle={`Table view`}
tooltipOpen
onClick={() => dispatch(toggleListView())}
/>
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={i18n.t('abortAllButton')}
onClick={() => abort()}
/>
<SpeedDialAction
icon={<AddCircleIcon />}
tooltipTitle={`New download`}
onClick={() => setOpenDialog(true)}
/>
</SpeedDial>
<DownloadDialog open={openDialog} onClose={() => {
setOpenDialog(false)
activeDownloads?.length === 0
? setShowBackdrop(false)
: setShowBackdrop(true)
}} />
</Container>
)
}