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

@@ -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>
)
}

View File

@@ -23,7 +23,7 @@ export function DownloadsListView({ downloads, abortFunction }: Props) {
return ( return (
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}> <Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
<Grid item xs={12}> <Grid item xs={12}>
<TableContainer component={Paper} sx={{ minHeight: '65vh' }} elevation={2}> <TableContainer component={Paper} sx={{ minHeight: '80vh' }} elevation={2}>
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>

View File

@@ -18,7 +18,7 @@ export default function Logout() {
<ListItemIcon> <ListItemIcon>
<LogoutIcon /> <LogoutIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Authentication" /> <ListItemText primary="RPC authentication" />
</ListItemButton> </ListItemButton>
) )
} }

View 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>
)
}

View File

@@ -0,0 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export interface FormatSelectionState {
bestFormat: string
audioFormat: string
videoFormat: string
}

View File

@@ -21,23 +21,28 @@ export type RPCResponse<T> = {
id?: string 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 = { export type RPCResult = {
id: string id: string
progress: { progress: DownloadProgress
speed: number info: DownloadInfo
eta: number
percentage: string
}
info: {
url: string
filesize_approx?: number
resolution?: string
thumbnail: string
title: string
vcodec?: string
acodec?: string
ext?: string
}
} }
export type RPCParams = { export type RPCParams = {

View File

@@ -92,3 +92,5 @@ export function formatGiB(bytes: number) {
export const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB` 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()

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 FormatListBulleted from '@mui/icons-material/FormatListBulleted'
import { import {
Alert, Alert,
Backdrop, Backdrop,
Button,
CircularProgress, CircularProgress,
Container, Container,
FormControl,
Grid,
IconButton,
InputAdornment,
InputLabel,
MenuItem,
Paper,
Select,
Snackbar, Snackbar,
SpeedDial, SpeedDial,
SpeedDialAction, SpeedDialAction,
SpeedDialIcon, SpeedDialIcon,
styled, styled
TextField
} from '@mui/material' } from '@mui/material'
import { Buffer } from 'buffer' import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import DownloadDialog from '../components/DownloadDialog'
import { DownloadsCardView } from '../components/DownloadsCardView' import { DownloadsCardView } from '../components/DownloadsCardView'
import { DownloadsListView } from '../components/DownloadsListView' import { DownloadsListView } from '../components/DownloadsListView'
import FormatsGrid from '../components/FormatsGrid' import Splash from '../components/Splash'
import { CliArguments } from '../lib/argsParser'
import I18nBuilder from '../lib/intl'
import { RPCClient, socket$ } from '../lib/rpcClient'
import { toggleListView } from '../features/settings/settingsSlice' import { toggleListView } from '../features/settings/settingsSlice'
import { connected, setFreeSpace } from '../features/status/statusSlice' import { connected, setFreeSpace } from '../features/status/statusSlice'
import I18nBuilder from '../lib/intl'
import { RPCClient, socket$ } from '../lib/rpcClient'
import { RootState } from '../stores/store' import { RootState } from '../stores/store'
import type { DLMetadata, RPCResponse, RPCResult } from '../types' import type { RPCResponse, RPCResult } from '../types'
import { isValidURL, toFormatArgs } from '../utils' import { dateTimeComparatorFunc } from '../utils'
export default function Home() { export default function Home() {
// redux state // redux state
@@ -43,34 +33,17 @@ export default function Home() {
const dispatch = useDispatch() const dispatch = useDispatch()
// ephemeral state // ephemeral state
const [activeDownloads, setActiveDownloads] = useState<Array<RPCResult>>() const [activeDownloads, setActiveDownloads] = useState<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 [showBackdrop, setShowBackdrop] = useState(true) const [showBackdrop, setShowBackdrop] = useState(true)
const [showToast, setShowToast] = useState(true) const [showToast, setShowToast] = useState(true)
const [openDialog, setOpenDialog] = useState(false)
const [socketHasError, setSocketHasError] = useState(false) const [socketHasError, setSocketHasError] = useState(false)
// memos // memos
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
const client = useMemo(() => new RPCClient(), [settings.serverAddr, settings.serverPort]) 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 -------------------- */ /* -------------------- Effects -------------------- */
@@ -81,8 +54,6 @@ export default function Home() {
const sub = socket$.subscribe({ const sub = socket$.subscribe({
next: () => { next: () => {
dispatch(connected()) dispatch(connected())
setCustomArgs(localStorage.getItem('last-input-args') ?? '')
setFilenameOverride(localStorage.getItem('last-filename-override') ?? '')
}, },
error: () => { error: () => {
setSocketHasError(true) setSocketHasError(true)
@@ -117,7 +88,10 @@ export default function Home() {
setActiveDownloads( setActiveDownloads(
(event.result ?? []) (event.result ?? [])
.filter((r) => !!r.info.url) .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 break
default: default:
@@ -133,88 +107,6 @@ export default function Home() {
} }
}, [activeDownloads?.length]) }, [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. * Abort a specific download if id's provided, other wise abort all running ones.
* @param id The download id / pid * @param id The download id / pid
@@ -228,27 +120,6 @@ export default function Home() {
client.killAll() 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 -------------------- */ /* -------------------- styled components -------------------- */
@@ -264,133 +135,9 @@ export default function Home() {
> >
<CircularProgress color="primary" /> <CircularProgress color="primary" />
</Backdrop> </Backdrop>
<Grid container spacing={2}> {activeDownloads?.length === 0 &&
<Grid item xs={12}> <Splash />
<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}
/>}
{ {
settings.listView ? settings.listView ?
<DownloadsListView downloads={activeDownloads ?? []} abortFunction={abort} /> : <DownloadsListView downloads={activeDownloads ?? []} abortFunction={abort} /> :
@@ -418,10 +165,25 @@ export default function Home() {
<SpeedDialAction <SpeedDialAction
icon={<FormatListBulleted />} icon={<FormatListBulleted />}
tooltipTitle={`Table view`} tooltipTitle={`Table view`}
tooltipOpen
onClick={() => dispatch(toggleListView())} onClick={() => dispatch(toggleListView())}
/> />
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={i18n.t('abortAllButton')}
onClick={() => abort()}
/>
<SpeedDialAction
icon={<AddCircleIcon />}
tooltipTitle={`New download`}
onClick={() => setOpenDialog(true)}
/>
</SpeedDial> </SpeedDial>
<DownloadDialog open={openDialog} onClose={() => {
setOpenDialog(false)
activeDownloads?.length === 0
? setShowBackdrop(false)
: setShowBackdrop(true)
}} />
</Container> </Container>
) )
} }

View File

@@ -116,7 +116,10 @@ func (p *Process) Start(path, filename string) {
if err != nil { if err != nil {
log.Println("Cannot retrieve info for", p.url) log.Println("Cannot retrieve info for", p.url)
} }
info := DownloadInfo{URL: p.url} info := DownloadInfo{
URL: p.url,
CreatedAt: time.Now(),
}
json.Unmarshal(stdout, &info) json.Unmarshal(stdout, &info)
p.mem.UpdateInfo(p.id, info) p.mem.UpdateInfo(p.id, info)
}() }()

View File

@@ -1,5 +1,7 @@
package server package server
import "time"
// Progress for the Running call // Progress for the Running call
type DownloadProgress struct { type DownloadProgress struct {
Percentage string `json:"percentage"` Percentage string `json:"percentage"`
@@ -9,14 +11,15 @@ type DownloadProgress struct {
// Used to deser the yt-dlp -J output // Used to deser the yt-dlp -J output
type DownloadInfo struct { type DownloadInfo struct {
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
Thumbnail string `json:"thumbnail"` Thumbnail string `json:"thumbnail"`
Resolution string `json:"resolution"` Resolution string `json:"resolution"`
Size int32 `json:"filesize_approx"` Size int32 `json:"filesize_approx"`
VCodec string `json:"vcodec"` VCodec string `json:"vcodec"`
ACodec string `json:"acodec"` ACodec string `json:"acodec"`
Extension string `json:"ext"` Extension string `json:"ext"`
CreatedAt time.Time `json:"created_at"`
} }
// Used to deser the formats in the -J output // Used to deser the formats in the -J output