code refactoring
This commit is contained in:
@@ -32,8 +32,10 @@ import AppBar from './components/AppBar'
|
||||
import Drawer from './components/Drawer'
|
||||
|
||||
import Logout from './components/Logout'
|
||||
import { formatGiB } from './utils'
|
||||
import ThemeToggler from './components/ThemeToggler'
|
||||
import I18nProvider from './providers/i18nProvider'
|
||||
import RPCCLientProvider from './providers/rpcClientProvider'
|
||||
import { formatGiB } from './utils'
|
||||
|
||||
export default function Layout() {
|
||||
const [open, setOpen] = useState(false)
|
||||
@@ -59,123 +61,131 @@ export default function Layout() {
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<AppBar position="absolute" open={open}>
|
||||
<Toolbar sx={{ pr: '24px' }}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={toggleDrawer}
|
||||
sx={{
|
||||
marginRight: '36px',
|
||||
...(open && { display: 'none' }),
|
||||
}}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
yt-dlp WebUI
|
||||
</Typography>
|
||||
{
|
||||
status.freeSpace ?
|
||||
<I18nProvider>
|
||||
<RPCCLientProvider>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
<AppBar position="absolute" open={open}>
|
||||
<Toolbar sx={{ pr: '24px' }}>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
onClick={toggleDrawer}
|
||||
sx={{
|
||||
marginRight: '36px',
|
||||
...(open && { display: 'none' }),
|
||||
}}
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1 }}
|
||||
>
|
||||
yt-dlp WebUI
|
||||
</Typography>
|
||||
{
|
||||
status.freeSpace ?
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Storage />
|
||||
<span>
|
||||
{formatGiB(status.freeSpace)}
|
||||
</span>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Storage />
|
||||
<span> {formatGiB(status.freeSpace)} </span>
|
||||
<SettingsEthernet />
|
||||
<span>
|
||||
{status.connected ? settings.serverAddr : 'not connected'}
|
||||
</span>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<SettingsEthernet />
|
||||
<span> {status.connected ? settings.serverAddr : 'not connected'}</span>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer variant="permanent" open={open}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
px: [1],
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer}>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List component="nav">
|
||||
<Link to={'/'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link to={'/archive'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<DownloadIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Archive" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link to={'/settings'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<ThemeToggler />
|
||||
<Logout />
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer variant="permanent" open={open}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
px: [1],
|
||||
}}
|
||||
>
|
||||
<IconButton onClick={toggleDrawer}>
|
||||
<ChevronLeft />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
<Divider />
|
||||
<List component="nav">
|
||||
<Link to={'/'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<Dashboard />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link to={'/archive'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<DownloadIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Archive" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<Link to={'/settings'} style={
|
||||
{
|
||||
textDecoration: 'none',
|
||||
color: mode === 'dark' ? '#ffffff' : '#000000DE'
|
||||
}
|
||||
}>
|
||||
<ListItemButton disabled={status.downloading}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</ListItemButton>
|
||||
</Link>
|
||||
<ThemeToggler />
|
||||
<Logout />
|
||||
</List>
|
||||
</Drawer>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
height: '100vh',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
<Toolbar />
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
</RPCCLientProvider>
|
||||
</I18nProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,8 @@ languages:
|
||||
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
|
||||
customArgsInput: Custom yt-dlp arguments
|
||||
rpcConnErr: Error while conencting to RPC server
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
italian:
|
||||
urlInput: URL di YouTube o di qualsiasi altro servizio supportato
|
||||
statusTitle: Stato
|
||||
@@ -57,6 +59,8 @@ languages:
|
||||
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
|
||||
customArgsInput: Custom yt-dlp arguments
|
||||
rpcConnErr: Error nella connessione al server RPC
|
||||
splashText: Nessun download attivo
|
||||
archiveTitle: Archivio
|
||||
chinese:
|
||||
urlInput: YouTube 或其他受支持服务的视频网址
|
||||
statusTitle: 状态
|
||||
@@ -85,6 +89,8 @@ languages:
|
||||
customArgs: 启用自定义 yt-dlp 参数(能力越大 = 责任越大)
|
||||
customArgsInput: 自定义 yt-dlp 参数
|
||||
rpcConnErr: Error while conencting to RPC server
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
spanish:
|
||||
urlInput: URL de YouTube u otro servicio compatible
|
||||
statusTitle: Estado
|
||||
@@ -113,6 +119,8 @@ languages:
|
||||
customArgs: Habilitar los argumentos yt-dlp personalizados (un gran poder conlleva una gran responsabilidad)
|
||||
customArgsInput: Argumentos yt-dlp personalizados
|
||||
rpcConnErr: Error al conectarse al servidor RPC
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
russian:
|
||||
urlInput: YouTube or other supported service video url
|
||||
statusTitle: Status
|
||||
@@ -141,6 +149,8 @@ languages:
|
||||
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
|
||||
customArgsInput: Custom yt-dlp arguments
|
||||
rpcConnErr: Error while conencting to RPC server
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
korean:
|
||||
urlInput: YouTube나 다른 지원되는 사이트의 URL
|
||||
statusTitle: 상태
|
||||
@@ -169,6 +179,8 @@ languages:
|
||||
customArgs: Enable custom yt-dlp args (great power = great responsabilities)
|
||||
customArgsInput: Custom yt-dlp arguments
|
||||
rpcConnErr: Error while conencting to RPC server
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
japanese:
|
||||
urlInput: YouTubeまたはサポート済み動画のURL
|
||||
statusTitle: 状態
|
||||
@@ -198,6 +210,8 @@ languages:
|
||||
customArgs: yt-dlpのオプションの有効化 (最適設定にする場合)
|
||||
customArgsInput: yt-dlpのオプション
|
||||
rpcConnErr: Error while conencting to RPC server
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
catalan:
|
||||
urlInput: URL de YouTube o d'un altre servei compatible
|
||||
statusTitle: Estat
|
||||
@@ -226,3 +240,5 @@ languages:
|
||||
customArgs: Habilitar els arguments yt-dlp personalitzats (un gran poder comporta una gran responsabilitat)
|
||||
customArgsInput: Arguments yt-dlp personalitzats
|
||||
rpcConnErr: Error en connectar-se al servidor RPC
|
||||
splashText: No active downloads
|
||||
archiveTitle: Archive
|
||||
|
||||
@@ -21,12 +21,12 @@ 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 { forwardRef, useContext, 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 { I18nContext } from '../providers/i18nProvider'
|
||||
import { RPCClientContext } from '../providers/rpcClientProvider'
|
||||
import { RootState } from '../stores/store'
|
||||
import type { DLMetadata } from '../types'
|
||||
import { isValidURL, toFormatArgs } from '../utils'
|
||||
@@ -67,10 +67,12 @@ export default function DownloadDialog({ open, onClose }: Props) {
|
||||
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])
|
||||
|
||||
// context
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { client } = useContext(RPCClientContext)
|
||||
|
||||
// refs
|
||||
const urlInputRef = useRef<HTMLInputElement>(null)
|
||||
const customFilenameInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload'
|
||||
import { Container, SvgIcon, Typography, styled } from '@mui/material'
|
||||
import { useContext } from 'react'
|
||||
import { I18nContext } from '../providers/i18nProvider'
|
||||
|
||||
const FlexContainer = styled(Container)({
|
||||
display: 'flex',
|
||||
@@ -19,6 +21,8 @@ const Title = styled(Typography)({
|
||||
})
|
||||
|
||||
export default function Splash() {
|
||||
const { i18n } = useContext(I18nContext)
|
||||
|
||||
return (
|
||||
<FlexContainer>
|
||||
<Title fontWeight={'500'} fontSize={72} color={'gray'}>
|
||||
@@ -27,7 +31,7 @@ export default function Splash() {
|
||||
</SvgIcon>
|
||||
</Title>
|
||||
<Title fontWeight={'500'} fontSize={36} color={'gray'}>
|
||||
No active downloads
|
||||
{i18n.t('splashText')}
|
||||
</Title>
|
||||
</FlexContainer>
|
||||
)
|
||||
|
||||
30
frontend/src/providers/i18nProvider.tsx
Normal file
30
frontend/src/providers/i18nProvider.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { createContext, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import I18nBuilder from '../lib/intl'
|
||||
import { RootState, store } from '../stores/store'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
interface Context {
|
||||
i18n: I18nBuilder
|
||||
}
|
||||
|
||||
export const I18nContext = createContext<Context>({
|
||||
i18n: new I18nBuilder(store.getState().settings.language)
|
||||
})
|
||||
|
||||
export default function I18nProvider({ children }: Props) {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
|
||||
const i18n = useMemo(() => new I18nBuilder(
|
||||
settings.language
|
||||
), [settings.language])
|
||||
|
||||
return (
|
||||
<I18nContext.Provider value={{ i18n }}>
|
||||
{children}
|
||||
</I18nContext.Provider>
|
||||
)
|
||||
}
|
||||
31
frontend/src/providers/rpcClientProvider.tsx
Normal file
31
frontend/src/providers/rpcClientProvider.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createContext, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RPCClient } from '../lib/rpcClient'
|
||||
import { RootState } from '../stores/store'
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
interface Context {
|
||||
client: RPCClient
|
||||
}
|
||||
|
||||
export const RPCClientContext = createContext<Context>({
|
||||
client: new RPCClient()
|
||||
})
|
||||
|
||||
export default function RPCCLientProvider({ children }: Props) {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
|
||||
const client = useMemo(() => new RPCClient(), [
|
||||
settings.serverAddr,
|
||||
settings.serverPort,
|
||||
])
|
||||
|
||||
return (
|
||||
<RPCClientContext.Provider value={{ client }}>
|
||||
{children}
|
||||
</RPCClientContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
||||
import VideoFileIcon from '@mui/icons-material/VideoFile'
|
||||
|
||||
import { Buffer } from 'buffer'
|
||||
import { useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useContext, useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
||||
import { useObservable } from '../hooks/observable'
|
||||
@@ -36,11 +36,14 @@ import { DeleteRequest, DirectoryEntry } from '../types'
|
||||
import { roundMiB } from '../utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
import { I18nContext } from '../providers/i18nProvider'
|
||||
|
||||
export default function Downloaded() {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { i18n } = useContext(I18nContext)
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const serverAddr =
|
||||
@@ -160,7 +163,7 @@ export default function Downloaded() {
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<Typography py={1} variant="h5" color="primary">
|
||||
{'Archive'}
|
||||
{i18n.t('archiveTitle')}
|
||||
</Typography>
|
||||
<List sx={{ width: '100%', bgcolor: 'background.paper' }}>
|
||||
{selectable.length === 0 && 'No files found'}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
SpeedDialIcon,
|
||||
styled
|
||||
} from '@mui/material'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import DownloadDialog from '../components/DownloadDialog'
|
||||
import { DownloadsCardView } from '../components/DownloadsCardView'
|
||||
@@ -20,8 +20,9 @@ import { DownloadsListView } from '../components/DownloadsListView'
|
||||
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 { socket$ } from '../lib/rpcClient'
|
||||
import { I18nContext } from '../providers/i18nProvider'
|
||||
import { RPCClientContext } from '../providers/rpcClientProvider'
|
||||
import { RootState } from '../stores/store'
|
||||
import type { RPCResponse, RPCResult } from '../types'
|
||||
import { dateTimeComparatorFunc } from '../utils'
|
||||
@@ -41,9 +42,9 @@ export default function Home() {
|
||||
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])
|
||||
// context
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { client } = useContext(RPCClientContext)
|
||||
|
||||
/* -------------------- Effects -------------------- */
|
||||
|
||||
@@ -120,7 +121,6 @@ export default function Home() {
|
||||
client.killAll()
|
||||
}
|
||||
|
||||
|
||||
/* -------------------- styled components -------------------- */
|
||||
|
||||
const Input = styled('input')({
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import {
|
||||
Subject,
|
||||
@@ -26,9 +26,6 @@ import {
|
||||
map,
|
||||
takeWhile
|
||||
} from 'rxjs'
|
||||
import { CliArguments } from '../lib/argsParser'
|
||||
import I18nBuilder from '../lib/intl'
|
||||
import { RPCClient } from '../lib/rpcClient'
|
||||
import {
|
||||
LanguageUnion,
|
||||
ThemeUnion,
|
||||
@@ -43,6 +40,9 @@ import {
|
||||
setTheme
|
||||
} from '../features/settings/settingsSlice'
|
||||
import { updated } from '../features/status/statusSlice'
|
||||
import { CliArguments } from '../lib/argsParser'
|
||||
import { I18nContext } from '../providers/i18nProvider'
|
||||
import { RPCClientContext } from '../providers/rpcClientProvider'
|
||||
import { RootState } from '../stores/store'
|
||||
import { validateDomain, validateIP } from '../utils'
|
||||
|
||||
@@ -54,9 +54,9 @@ export default function Settings() {
|
||||
|
||||
const [invalidIP, setInvalidIP] = useState(false);
|
||||
|
||||
const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language])
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { client } = useContext(RPCClientContext)
|
||||
|
||||
const client = useMemo(() => new RPCClient(), [])
|
||||
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [])
|
||||
|
||||
const serverAddr$ = useMemo(() => new Subject<string>(), [])
|
||||
|
||||
Reference in New Issue
Block a user