New home view layout (#58)
* Home layout refactor, moved new download to dialog * sort downloads by date
This commit is contained in:
335
frontend/src/components/DownloadDialog.tsx
Normal file
335
frontend/src/components/DownloadDialog.tsx
Normal file
@@ -0,0 +1,335 @@
|
||||
import { FileUpload } from '@mui/icons-material'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
FormControl,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
styled,
|
||||
TextField
|
||||
} from '@mui/material'
|
||||
import AppBar from '@mui/material/AppBar'
|
||||
import Dialog from '@mui/material/Dialog'
|
||||
import Slide from '@mui/material/Slide'
|
||||
import Toolbar from '@mui/material/Toolbar'
|
||||
import { TransitionProps } from '@mui/material/transitions'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import { Buffer } from 'buffer'
|
||||
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import FormatsGrid from '../components/FormatsGrid'
|
||||
import { CliArguments } from '../lib/argsParser'
|
||||
import I18nBuilder from '../lib/intl'
|
||||
import { RPCClient } from '../lib/rpcClient'
|
||||
import { RootState } from '../stores/store'
|
||||
import type { DLMetadata } from '../types'
|
||||
import { isValidURL, toFormatArgs } from '../utils'
|
||||
|
||||
const Transition = forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />
|
||||
})
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function DownloadDialog({ open, onClose }: Props) {
|
||||
// redux state
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const status = useSelector((state: RootState) => state.status)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ephemeral state
|
||||
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('')
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
client.directoryTree()
|
||||
.then(data => {
|
||||
setAvailableDownloadPaths(data.result)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setCustomArgs(localStorage.getItem('last-input-args') ?? '')
|
||||
setFilenameOverride(localStorage.getItem('last-filename-override') ?? '')
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 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('')
|
||||
|
||||
setTimeout(() => {
|
||||
resetInput()
|
||||
setDownloadFormats(undefined)
|
||||
onClose()
|
||||
}, 250)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive url from input and display the formats selection view
|
||||
*/
|
||||
const sendUrlFormatSelection = () => {
|
||||
setWorkingUrl(url)
|
||||
setUrl('')
|
||||
setPickedAudioFormat('')
|
||||
setPickedVideoFormat('')
|
||||
setPickedBestFormat('')
|
||||
|
||||
client.formats(url)
|
||||
?.then(formats => {
|
||||
setDownloadFormats(formats.result)
|
||||
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)
|
||||
}
|
||||
|
||||
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 -------------------- */
|
||||
|
||||
const Input = styled('input')({
|
||||
display: 'none',
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
fullScreen
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<AppBar sx={{ position: 'relative' }}>
|
||||
<Toolbar>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
onClick={onClose}
|
||||
aria-label="close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography sx={{ ml: 2, flex: 1 }} variant="h6" component="div">
|
||||
Download
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Container sx={{ mt: 4 }}>
|
||||
<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>
|
||||
</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}
|
||||
/>}
|
||||
</Container>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export function DownloadsListView({ downloads, abortFunction }: Props) {
|
||||
return (
|
||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||
<Grid item xs={12}>
|
||||
<TableContainer component={Paper} sx={{ minHeight: '65vh' }} elevation={2}>
|
||||
<TableContainer component={Paper} sx={{ minHeight: '80vh' }} elevation={2}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function Logout() {
|
||||
<ListItemIcon>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Authentication" />
|
||||
<ListItemText primary="RPC authentication" />
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
||||
34
frontend/src/components/Splash.tsx
Normal file
34
frontend/src/components/Splash.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
||||
import { Container, SvgIcon, Typography, styled } from '@mui/material'
|
||||
|
||||
const FlexContainer = styled(Container)({
|
||||
display: 'flex',
|
||||
minWidth: '100%',
|
||||
minHeight: '80vh',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column'
|
||||
})
|
||||
|
||||
const Title = styled(Typography)({
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingBottom: '0.5rem'
|
||||
})
|
||||
|
||||
export default function Splash() {
|
||||
return (
|
||||
<FlexContainer>
|
||||
<Title fontWeight={'500'} fontSize={72} color={'gray'}>
|
||||
<SvgIcon sx={{ fontSize: '200px' }}>
|
||||
<CloudDownloadIcon />
|
||||
</SvgIcon>
|
||||
</Title>
|
||||
<Title fontWeight={'500'} fontSize={36} color={'gray'}>
|
||||
No active downloads
|
||||
</Title>
|
||||
</FlexContainer>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export interface FormatSelectionState {
|
||||
bestFormat: string
|
||||
audioFormat: string
|
||||
videoFormat: string
|
||||
}
|
||||
35
frontend/src/types/index.d.ts
vendored
35
frontend/src/types/index.d.ts
vendored
@@ -21,23 +21,28 @@ export type RPCResponse<T> = {
|
||||
id?: string
|
||||
}
|
||||
|
||||
type DownloadInfo = {
|
||||
url: string
|
||||
filesize_approx?: number
|
||||
resolution?: string
|
||||
thumbnail: string
|
||||
title: string
|
||||
vcodec?: string
|
||||
acodec?: string
|
||||
ext?: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
type DownloadProgress = {
|
||||
speed: number
|
||||
eta: number
|
||||
percentage: string
|
||||
}
|
||||
|
||||
export type RPCResult = {
|
||||
id: string
|
||||
progress: {
|
||||
speed: number
|
||||
eta: number
|
||||
percentage: string
|
||||
}
|
||||
info: {
|
||||
url: string
|
||||
filesize_approx?: number
|
||||
resolution?: string
|
||||
thumbnail: string
|
||||
title: string
|
||||
vcodec?: string
|
||||
acodec?: string
|
||||
ext?: string
|
||||
}
|
||||
progress: DownloadProgress
|
||||
info: DownloadInfo
|
||||
}
|
||||
|
||||
export type RPCParams = {
|
||||
|
||||
@@ -91,4 +91,6 @@ export function formatGiB(bytes: number) {
|
||||
}
|
||||
|
||||
export const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB`
|
||||
export const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s`
|
||||
export const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s`
|
||||
|
||||
export const dateTimeComparatorFunc = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime()
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user