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