code refactoring
This commit is contained in:
@@ -5,7 +5,6 @@ import DownloadIcon from '@mui/icons-material/Download'
|
||||
import Menu from '@mui/icons-material/Menu'
|
||||
import SettingsIcon from '@mui/icons-material/Settings'
|
||||
import SettingsEthernet from '@mui/icons-material/SettingsEthernet'
|
||||
import Storage from '@mui/icons-material/Storage'
|
||||
import { Box, createTheme } from '@mui/material'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import Divider from '@mui/material/Divider'
|
||||
@@ -21,20 +20,20 @@ import { useMemo, useState } from 'react'
|
||||
import { Link, Outlet } from 'react-router-dom'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { settingsState } from './atoms/settings'
|
||||
import { statusState } from './atoms/status'
|
||||
import { connectedState } from './atoms/status'
|
||||
import AppBar from './components/AppBar'
|
||||
import Drawer from './components/Drawer'
|
||||
import FreeSpaceIndicator from './components/FreeSpaceIndicator'
|
||||
import Logout from './components/Logout'
|
||||
import SocketSubscriber from './components/SocketSubscriber'
|
||||
import ThemeToggler from './components/ThemeToggler'
|
||||
import Toaster from './providers/ToasterProvider'
|
||||
import { formatGiB } from './utils'
|
||||
import SocketSubscriber from './components/SocketSubscriber'
|
||||
|
||||
export default function Layout() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const settings = useRecoilValue(settingsState)
|
||||
const status = useRecoilValue(statusState)
|
||||
const isConnected = useRecoilValue(connectedState)
|
||||
|
||||
const mode = settings.theme
|
||||
const theme = useMemo(() =>
|
||||
@@ -48,9 +47,7 @@ export default function Layout() {
|
||||
}), [settings.theme]
|
||||
)
|
||||
|
||||
const toggleDrawer = () => {
|
||||
setOpen(state => !state)
|
||||
}
|
||||
const toggleDrawer = () => setOpen(state => !state)
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
@@ -80,20 +77,7 @@ export default function Layout() {
|
||||
>
|
||||
yt-dlp WebUI
|
||||
</Typography>
|
||||
{
|
||||
status.freeSpace ?
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<Storage />
|
||||
<span>
|
||||
{formatGiB(status.freeSpace)}
|
||||
</span>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<FreeSpaceIndicator />
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -101,7 +85,7 @@ export default function Layout() {
|
||||
}}>
|
||||
<SettingsEthernet />
|
||||
<span>
|
||||
{status.connected ? settings.serverAddr : 'not connected'}
|
||||
{isConnected ? settings.serverAddr : 'not connected'}
|
||||
</span>
|
||||
</div>
|
||||
</Toolbar>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { atom, selector } from 'recoil'
|
||||
import { rpcClientState } from './rpc'
|
||||
|
||||
type StatusState = {
|
||||
connected: boolean,
|
||||
updated: boolean,
|
||||
downloading: boolean,
|
||||
freeSpace: number,
|
||||
}
|
||||
|
||||
|
||||
@@ -23,17 +23,18 @@ export const isDownloadingState = atom({
|
||||
default: false
|
||||
})
|
||||
|
||||
export const freeSpaceBytesState = atom({
|
||||
key: 'freeSpaceBytesState',
|
||||
default: 0
|
||||
})
|
||||
// export const freeSpaceBytesState = selector({
|
||||
// key: 'freeSpaceBytesState',
|
||||
// get: async ({ get }) => {
|
||||
// const res = await get(rpcClientState).freeSpace()
|
||||
// return res.result
|
||||
// }
|
||||
// })
|
||||
|
||||
export const statusState = selector<StatusState>({
|
||||
key: 'statusState',
|
||||
get: ({ get }) => ({
|
||||
connected: get(connectedState),
|
||||
updated: get(updatedBinaryState),
|
||||
downloading: get(isDownloadingState),
|
||||
freeSpace: get(freeSpaceBytesState),
|
||||
})
|
||||
export const availableDownloadPathsState = selector({
|
||||
key: 'availableDownloadPathsState',
|
||||
get: async ({ get }) => {
|
||||
const res = await get(rpcClientState).directoryTree()
|
||||
return res.result
|
||||
}
|
||||
})
|
||||
6
frontend/src/atoms/ui.ts
Normal file
6
frontend/src/atoms/ui.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { atom } from 'recoil'
|
||||
|
||||
export const loadingAtom = atom({
|
||||
key: 'loadingAtom',
|
||||
default: false
|
||||
})
|
||||
@@ -26,15 +26,14 @@ import { TransitionProps } from '@mui/material/transitions'
|
||||
import { Buffer } from 'buffer'
|
||||
import {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition
|
||||
} from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { settingsState } from '../atoms/settings'
|
||||
import { connectedState } from '../atoms/status'
|
||||
import { availableDownloadPathsState, connectedState } from '../atoms/status'
|
||||
import FormatsGrid from '../components/FormatsGrid'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
@@ -64,7 +63,8 @@ export default function DownloadDialog({
|
||||
}: Props) {
|
||||
// recoil state
|
||||
const settings = useRecoilValue(settingsState)
|
||||
const [isConnected] = useRecoilState(connectedState)
|
||||
const isConnected = useRecoilValue(connectedState)
|
||||
const availableDownloadPaths = useRecoilValue(availableDownloadPathsState)
|
||||
|
||||
// ephemeral state
|
||||
const [downloadFormats, setDownloadFormats] = useState<DLMetadata>()
|
||||
@@ -74,7 +74,6 @@ export default function DownloadDialog({
|
||||
|
||||
const [customArgs, setCustomArgs] = useState('')
|
||||
const [downloadPath, setDownloadPath] = useState(0)
|
||||
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([])
|
||||
|
||||
const [fileNameOverride, setFilenameOverride] = useState('')
|
||||
|
||||
@@ -96,19 +95,6 @@ export default function DownloadDialog({
|
||||
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') ?? '')
|
||||
}, [])
|
||||
|
||||
// transitions
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
|
||||
@@ -1,71 +1,31 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { activeDownloadsState } from '../atoms/downloads'
|
||||
import { listViewState } from '../atoms/settings'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { DownloadsCardView } from './DownloadsCardView'
|
||||
import { DownloadsListView } from './DownloadsListView'
|
||||
import { useEffect } from 'react'
|
||||
import { connectedState, isDownloadingState } from '../atoms/status'
|
||||
import { datetimeCompareFunc, isRPCResponse } from '../utils'
|
||||
import { RPCResponse, RPCResult } from '../types'
|
||||
import { loadingAtom } from '../atoms/ui'
|
||||
import DownloadsCardView from './DownloadsCardView'
|
||||
import DownloadsListView from './DownloadsListView'
|
||||
|
||||
const Downloads: React.FC = () => {
|
||||
const [active, setActive] = useRecoilState(activeDownloadsState)
|
||||
const isConnected = useRecoilValue(connectedState)
|
||||
const listView = useRecoilValue(listViewState)
|
||||
const active = useRecoilValue(activeDownloadsState)
|
||||
|
||||
const { client, socket$ } = useRPC()
|
||||
|
||||
const abort = (id?: string) => {
|
||||
if (id) {
|
||||
client.kill(id)
|
||||
return
|
||||
}
|
||||
client.killAll()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) { return }
|
||||
|
||||
const sub = socket$.subscribe((event: RPCResponse<RPCResult[]>) => {
|
||||
if (!isRPCResponse(event)) { return }
|
||||
if (!Array.isArray(event.result)) { return }
|
||||
|
||||
setActive(
|
||||
(event.result || [])
|
||||
.filter(f => !!f.info.url)
|
||||
.sort((a, b) => datetimeCompareFunc(
|
||||
b.info.created_at,
|
||||
a.info.created_at,
|
||||
))
|
||||
)
|
||||
})
|
||||
|
||||
return () => sub.unsubscribe()
|
||||
}, [socket$, isConnected])
|
||||
|
||||
const [, setIsDownloading] = useRecoilState(isDownloadingState)
|
||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
setIsDownloading(true)
|
||||
setIsLoading(true)
|
||||
}
|
||||
}, [active?.length])
|
||||
|
||||
if (listView) {
|
||||
return (
|
||||
<DownloadsListView
|
||||
downloads={active ?? []}
|
||||
onStop={abort}
|
||||
/>
|
||||
<DownloadsListView />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<DownloadsCardView
|
||||
downloads={active ?? []}
|
||||
onStop={abort}
|
||||
/>
|
||||
<DownloadsCardView />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import { Grid } from "@mui/material"
|
||||
import { Fragment } from "react"
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { activeDownloadsState } from '../atoms/downloads'
|
||||
import { useToast } from "../hooks/toast"
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import type { RPCResult } from "../types"
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { StackableResult } from "./StackableResult"
|
||||
|
||||
type Props = {
|
||||
downloads: RPCResult[]
|
||||
onStop: (id: string) => void
|
||||
}
|
||||
const DownloadsCardView: React.FC = () => {
|
||||
const downloads = useRecoilValue(activeDownloadsState) ?? []
|
||||
|
||||
export function DownloadsCardView({ downloads, onStop }: Props) {
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
const abort = (id: string) => client.kill(id)
|
||||
|
||||
return (
|
||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||
{
|
||||
@@ -25,7 +27,7 @@ export function DownloadsCardView({ downloads, onStop }: Props) {
|
||||
title={download.info.title}
|
||||
thumbnail={download.info.thumbnail}
|
||||
percentage={download.progress.percentage}
|
||||
onStop={() => onStop(download.id)}
|
||||
onStop={() => abort(download.id)}
|
||||
onCopy={() => pushMessage(i18n.t('clipboardAction'))}
|
||||
resolution={download.info.resolution ?? ''}
|
||||
speed={download.progress.speed}
|
||||
@@ -38,4 +40,6 @@ export function DownloadsCardView({ downloads, onStop }: Props) {
|
||||
}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DownloadsCardView
|
||||
@@ -11,15 +11,19 @@ import {
|
||||
TableRow,
|
||||
Typography
|
||||
} from "@mui/material"
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { activeDownloadsState } from '../atoms/downloads'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { ellipsis, formatSpeedMiB, roundMiB } from "../utils"
|
||||
import type { RPCResult } from "../types"
|
||||
|
||||
type Props = {
|
||||
downloads: RPCResult[]
|
||||
onStop: (id: string) => void
|
||||
}
|
||||
|
||||
export const DownloadsListView: React.FC<Props> = ({ downloads, onStop }) => {
|
||||
const DownloadsListView: React.FC = () => {
|
||||
const downloads = useRecoilValue(activeDownloadsState) ?? []
|
||||
|
||||
const { client } = useRPC()
|
||||
|
||||
const abort = (id: string) => client.kill(id)
|
||||
|
||||
return (
|
||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||
<Grid item xs={12}>
|
||||
@@ -70,7 +74,7 @@ export const DownloadsListView: React.FC<Props> = ({ downloads, onStop }) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={() => onStop(download.id)}
|
||||
onClick={() => abort(download.id)}
|
||||
>
|
||||
{download.progress.percentage === '-1' ? 'Remove' : 'Stop'}
|
||||
</Button>
|
||||
@@ -84,4 +88,6 @@ export const DownloadsListView: React.FC<Props> = ({ downloads, onStop }) => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default DownloadsListView
|
||||
29
frontend/src/components/FreeSpaceIndicator.tsx
Normal file
29
frontend/src/components/FreeSpaceIndicator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import StorageIcon from '@mui/icons-material/Storage'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatGiB } from '../utils'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
|
||||
const FreeSpaceIndicator = () => {
|
||||
const [freeSpace, setFreeSpace] = useState(0)
|
||||
|
||||
const { client } = useRPC()
|
||||
|
||||
useEffect(() => {
|
||||
client.freeSpace().then(r => setFreeSpace(r.result))
|
||||
}, [client])
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
<StorageIcon />
|
||||
<span>
|
||||
{formatGiB(freeSpace)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FreeSpaceIndicator
|
||||
31
frontend/src/components/HomeActions.tsx
Normal file
31
frontend/src/components/HomeActions.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import { loadingAtom } from '../atoms/ui'
|
||||
import DownloadDialog from './DownloadDialog'
|
||||
import HomeSpeedDial from './HomeSpeedDial'
|
||||
|
||||
const HomeActions: React.FC = () => {
|
||||
const [, setIsLoading] = useRecoilState(loadingAtom)
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<HomeSpeedDial
|
||||
onOpen={() => setOpenDialog(true)}
|
||||
/>
|
||||
<DownloadDialog
|
||||
open={openDialog}
|
||||
onClose={() => {
|
||||
setOpenDialog(false)
|
||||
setIsLoading(true)
|
||||
}}
|
||||
onDownloadStart={() => {
|
||||
setOpenDialog(false)
|
||||
setIsLoading(true)
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HomeActions
|
||||
18
frontend/src/components/LoadingBackdrop.tsx
Normal file
18
frontend/src/components/LoadingBackdrop.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Backdrop, CircularProgress } from '@mui/material'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { loadingAtom } from '../atoms/ui'
|
||||
|
||||
const LoadingBackdrop: React.FC = () => {
|
||||
const isLoading = useRecoilValue(loadingAtom)
|
||||
|
||||
return (
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={!isLoading}
|
||||
>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingBackdrop
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
|
||||
import LogoutIcon from '@mui/icons-material/Logout'
|
||||
import { getHttpEndpoint } from '../utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { serverURL } from '../atoms/settings'
|
||||
|
||||
export default function Logout() {
|
||||
const navigate = useNavigate()
|
||||
const url = useRecoilValue(serverURL)
|
||||
|
||||
const logout = async () => {
|
||||
const res = await fetch(`${getHttpEndpoint()}/auth/logout`)
|
||||
const res = await fetch(`${url}/auth/logout`)
|
||||
if (res.ok) {
|
||||
navigate('/login')
|
||||
}
|
||||
|
||||
@@ -1,42 +1,62 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { connectedState } from '../atoms/status'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { interval, share, take } from 'rxjs'
|
||||
import { activeDownloadsState } from '../atoms/downloads'
|
||||
import { serverAddressAndPortState } from '../atoms/settings'
|
||||
import { connectedState } from '../atoms/status'
|
||||
import { useSubscription } from '../hooks/observable'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
import { datetimeCompareFunc, isRPCResponse } from '../utils'
|
||||
|
||||
interface Props extends React.HTMLAttributes<HTMLBaseElement> { }
|
||||
|
||||
const SocketSubscriber: React.FC<Props> = ({ children }) => {
|
||||
const [isConnected, setIsConnected] = useRecoilState(connectedState)
|
||||
const [, setIsConnected] = useRecoilState(connectedState)
|
||||
const [, setActive] = useRecoilState(activeDownloadsState)
|
||||
|
||||
const serverAddressAndPort = useRecoilValue(serverAddressAndPortState)
|
||||
|
||||
const { i18n } = useI18n()
|
||||
const { socket$ } = useRPC()
|
||||
const { client } = useRPC()
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) { return }
|
||||
const sharedSocket$ = useMemo(() => client.socket$.pipe(share()), [])
|
||||
const socketOnce$ = useMemo(() => sharedSocket$.pipe(take(1)), [])
|
||||
|
||||
const sub = socket$.subscribe({
|
||||
next: () => {
|
||||
setIsConnected(true)
|
||||
pushMessage(
|
||||
`Connected to (${serverAddressAndPort})`,
|
||||
"success"
|
||||
)
|
||||
},
|
||||
error: (e) => {
|
||||
console.error(e)
|
||||
pushMessage(
|
||||
`${i18n.t('rpcConnErr')} (${serverAddressAndPort})`,
|
||||
"error"
|
||||
)
|
||||
}
|
||||
})
|
||||
return () => sub.unsubscribe()
|
||||
}, [isConnected])
|
||||
useSubscription(socketOnce$, () => {
|
||||
setIsConnected(true)
|
||||
pushMessage(
|
||||
`${i18n.t('toastConnected')} (${serverAddressAndPort})`,
|
||||
"success"
|
||||
)
|
||||
})
|
||||
|
||||
useSubscription(sharedSocket$,
|
||||
(event) => {
|
||||
if (!isRPCResponse(event)) { return }
|
||||
if (!Array.isArray(event.result)) { return }
|
||||
|
||||
setActive(
|
||||
(event.result || [])
|
||||
.filter(f => !!f.info.url)
|
||||
.sort((a, b) => datetimeCompareFunc(
|
||||
b.info.created_at,
|
||||
a.info.created_at,
|
||||
))
|
||||
)
|
||||
},
|
||||
(err) => {
|
||||
console.error(err)
|
||||
pushMessage(
|
||||
`${i18n.t('rpcConnErr')} (${serverAddressAndPort})`,
|
||||
"error"
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
useSubscription(interval(1000), () => client.running())
|
||||
|
||||
return (
|
||||
<>{children}</>
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { ellipsis, formatSpeedMiB, mapProcessStatus, roundMiB } from '../utils'
|
||||
|
||||
type Props = {
|
||||
@@ -40,15 +39,11 @@ export function StackableResult({
|
||||
onStop,
|
||||
onCopy,
|
||||
}: Props) {
|
||||
const [isCompleted, setIsCompleted] = useState(false)
|
||||
const isCompleted = () => percentage === '-1'
|
||||
|
||||
useEffect(() => {
|
||||
if (percentage === '-1') {
|
||||
setIsCompleted(true)
|
||||
}
|
||||
}, [percentage])
|
||||
|
||||
const percentageToNumber = () => isCompleted ? 100 : Number(percentage.replace('%', ''))
|
||||
const percentageToNumber = () => isCompleted()
|
||||
? 100
|
||||
: Number(percentage.replace('%', ''))
|
||||
|
||||
const guessResolution = (xByY: string): any => {
|
||||
if (!xByY) return null
|
||||
@@ -82,12 +77,12 @@ export function StackableResult({
|
||||
}
|
||||
<Stack direction="row" spacing={1} py={2}>
|
||||
<Chip
|
||||
label={isCompleted ? 'Completed' : mapProcessStatus(status)}
|
||||
label={isCompleted() ? 'Completed' : mapProcessStatus(status)}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
<Typography>{!isCompleted ? percentage : ''}</Typography>
|
||||
<Typography> {!isCompleted ? formatSpeedMiB(speed) : ''}</Typography>
|
||||
<Typography>{!isCompleted() ? percentage : ''}</Typography>
|
||||
<Typography> {!isCompleted() ? formatSpeedMiB(speed) : ''}</Typography>
|
||||
<Typography>{roundMiB(size ?? 0)}</Typography>
|
||||
{guessResolution(resolution)}
|
||||
</Stack>
|
||||
@@ -95,7 +90,7 @@ export function StackableResult({
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentageToNumber()}
|
||||
color={isCompleted ? "secondary" : "primary"}
|
||||
color={isCompleted() ? "secondary" : "primary"}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
@@ -108,7 +103,7 @@ export function StackableResult({
|
||||
color="primary"
|
||||
onClick={onStop}
|
||||
>
|
||||
{isCompleted ? "Clear" : "Stop"}
|
||||
{isCompleted() ? "Clear" : "Stop"}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
@@ -5,7 +5,6 @@ export const useRPC = () => {
|
||||
const client = useRecoilValue(rpcClientState)
|
||||
|
||||
return {
|
||||
client,
|
||||
socket$: client.socket$
|
||||
client
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { DLMetadata, RPCRequest, RPCResponse } from '../types'
|
||||
import { Observable, share } from 'rxjs'
|
||||
import type { DLMetadata, RPCRequest, RPCResponse, RPCResult } from '../types'
|
||||
|
||||
import { WebSocketSubject, webSocket } from 'rxjs/webSocket'
|
||||
|
||||
export class RPCClient {
|
||||
private seq: number
|
||||
private httpEndpoint: string
|
||||
private _socket$: WebSocketSubject<any>
|
||||
private readonly _socket$: WebSocketSubject<any>
|
||||
|
||||
constructor(httpEndpoint: string, webSocketEndpoint: string) {
|
||||
this.seq = 0
|
||||
@@ -13,8 +14,8 @@ export class RPCClient {
|
||||
this._socket$ = webSocket<any>(webSocketEndpoint)
|
||||
}
|
||||
|
||||
public get socket$() {
|
||||
return this._socket$
|
||||
public get socket$(): Observable<RPCResponse<RPCResult[]>> {
|
||||
return this._socket$.asObservable()
|
||||
}
|
||||
|
||||
private incrementSeq() {
|
||||
@@ -52,7 +53,7 @@ export class RPCClient {
|
||||
return
|
||||
}
|
||||
if (playlist) {
|
||||
return this.send({
|
||||
return this.sendHTTP({
|
||||
method: 'Service.ExecPlaylist',
|
||||
params: [{
|
||||
URL: url,
|
||||
@@ -61,7 +62,7 @@ export class RPCClient {
|
||||
}]
|
||||
})
|
||||
}
|
||||
this.send({
|
||||
this.sendHTTP({
|
||||
method: 'Service.Exec',
|
||||
params: [{
|
||||
URL: url.split("?list").at(0)!,
|
||||
@@ -91,14 +92,14 @@ export class RPCClient {
|
||||
}
|
||||
|
||||
public kill(id: string) {
|
||||
this.send({
|
||||
this.sendHTTP({
|
||||
method: 'Service.Kill',
|
||||
params: [id],
|
||||
})
|
||||
}
|
||||
|
||||
public killAll() {
|
||||
this.send({
|
||||
this.sendHTTP({
|
||||
method: 'Service.KillAll',
|
||||
params: [],
|
||||
})
|
||||
|
||||
@@ -75,19 +75,6 @@ export function toFormatArgs(codes: string[]): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
export function getWebSocketEndpoint() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'
|
||||
return `${protocol}://${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/rpc/ws`
|
||||
}
|
||||
|
||||
export function getHttpRPCEndpoint() {
|
||||
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}/rpc/http`
|
||||
}
|
||||
|
||||
export function getHttpEndpoint() {
|
||||
return `${window.location.protocol}//${localStorage.getItem('server-addr') || window.location.hostname}:${localStorage.getItem('server-port') || window.location.port}`
|
||||
}
|
||||
|
||||
export function formatGiB(bytes: number) {
|
||||
return `${(bytes / 1_000_000_000).toFixed(0)}GiB`
|
||||
}
|
||||
|
||||
@@ -1,79 +1,18 @@
|
||||
import {
|
||||
Backdrop,
|
||||
CircularProgress,
|
||||
Container
|
||||
} from '@mui/material'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRecoilState, useRecoilValue } from 'recoil'
|
||||
import { serverAddressAndPortState } from '../atoms/settings'
|
||||
import { connectedState, freeSpaceBytesState, isDownloadingState } from '../atoms/status'
|
||||
import DownloadDialog from '../components/DownloadDialog'
|
||||
import Downloads from '../components/Downloads'
|
||||
import HomeSpeedDial from '../components/HomeSpeedDial'
|
||||
import HomeActions from '../components/HomeActions'
|
||||
import LoadingBackdrop from '../components/LoadingBackdrop'
|
||||
import Splash from '../components/Splash'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
|
||||
export default function Home() {
|
||||
const isDownloading = useRecoilValue(isDownloadingState)
|
||||
const serverAddressAndPort = useRecoilValue(serverAddressAndPortState)
|
||||
|
||||
const [, setFreeSpace] = useRecoilState(freeSpaceBytesState)
|
||||
const [isConnected, setIsDownloading] = useRecoilState(connectedState)
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
client.running()
|
||||
const interval = setInterval(() => client.running(), 1000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [isConnected])
|
||||
|
||||
useEffect(() => {
|
||||
client
|
||||
.freeSpace()
|
||||
.then(bytes => setFreeSpace(bytes.result))
|
||||
.catch(() => {
|
||||
pushMessage(
|
||||
`${i18n.t('rpcConnErr')} (${serverAddressAndPort})`,
|
||||
"error"
|
||||
)
|
||||
setIsDownloading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={!isDownloading}
|
||||
>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
<LoadingBackdrop />
|
||||
<Splash />
|
||||
<Downloads />
|
||||
<HomeSpeedDial
|
||||
onOpen={() => setOpenDialog(true)}
|
||||
/>
|
||||
<DownloadDialog
|
||||
open={openDialog}
|
||||
onClose={() => {
|
||||
setOpenDialog(false)
|
||||
setIsDownloading(false)
|
||||
}}
|
||||
onDownloadStart={() => {
|
||||
setOpenDialog(false)
|
||||
setIsDownloading(false)
|
||||
}}
|
||||
/>
|
||||
<HomeActions />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
} from '@mui/material'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getHttpEndpoint } from '../utils'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { serverURL } from '../atoms/settings'
|
||||
|
||||
const LoginContainer = styled(Container)({
|
||||
display: 'flex',
|
||||
@@ -35,10 +36,12 @@ export default function Login() {
|
||||
const [secret, setSecret] = useState('')
|
||||
const [formHasError, setFormHasError] = useState(false)
|
||||
|
||||
const url = useRecoilValue(serverURL)
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const login = async () => {
|
||||
const res = await fetch(`${getHttpEndpoint()}/auth/login`, {
|
||||
const res = await fetch(`${url}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
Reference in New Issue
Block a user