migrated from redux to recoil
This commit is contained in:
@@ -27,28 +27,25 @@ import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile'
|
||||
import VideoFileIcon from '@mui/icons-material/VideoFile'
|
||||
|
||||
import { Buffer } from 'buffer'
|
||||
import { useContext, useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useEffect, useMemo, useState, useTransition } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import { BehaviorSubject, Subject, combineLatestWith, map, share } from 'rxjs'
|
||||
import { serverURL } from '../atoms/settings'
|
||||
import { useObservable } from '../hooks/observable'
|
||||
import { RootState } from '../stores/store'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { ffetch } from '../lib/httpClient'
|
||||
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 serverAddr = useRecoilValue(serverURL)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { i18n } = useI18n()
|
||||
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const serverAddr =
|
||||
`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}`
|
||||
|
||||
const files$ = useMemo(() => new Subject<DirectoryEntry[]>(), [])
|
||||
const selected$ = useMemo(() => new BehaviorSubject<string[]>([]), [])
|
||||
|
||||
@@ -138,8 +135,7 @@ export default function Downloaded() {
|
||||
|
||||
useEffect(() => {
|
||||
fetcher()
|
||||
}, [settings.serverAddr, settings.serverPort])
|
||||
|
||||
}, [serverAddr])
|
||||
|
||||
const onFileClick = (path: string) => startTransition(() => {
|
||||
window.open(`${serverAddr}/archive/d/${Buffer.from(path).toString('hex')}`)
|
||||
|
||||
@@ -1,168 +1,77 @@
|
||||
import AddCircleIcon from '@mui/icons-material/AddCircle'
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever'
|
||||
import FormatListBulleted from '@mui/icons-material/FormatListBulleted'
|
||||
import {
|
||||
Backdrop,
|
||||
CircularProgress,
|
||||
Container,
|
||||
SpeedDial,
|
||||
SpeedDialAction,
|
||||
SpeedDialIcon
|
||||
Container
|
||||
} from '@mui/material'
|
||||
import { useContext, useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
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 { DownloadsCardView } from '../components/DownloadsCardView'
|
||||
import { DownloadsListView } from '../components/DownloadsListView'
|
||||
import Downloads from '../components/Downloads'
|
||||
import HomeSpeedDial from '../components/HomeSpeedDial'
|
||||
import Splash from '../components/Splash'
|
||||
import { toggleListView } from '../features/settings/settingsSlice'
|
||||
import { connected, setFreeSpace } from '../features/status/statusSlice'
|
||||
import { useToast } from '../hooks/toast'
|
||||
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 { datetimeCompareFunc, isRPCResponse } from '../utils'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
|
||||
export default function Home() {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const status = useSelector((state: RootState) => state.status)
|
||||
const dispatch = useDispatch()
|
||||
const isDownloading = useRecoilValue(isDownloadingState)
|
||||
const serverAddressAndPort = useRecoilValue(serverAddressAndPortState)
|
||||
|
||||
const [, setFreeSpace] = useRecoilState(freeSpaceBytesState)
|
||||
const [isConnected, setIsDownloading] = useRecoilState(connectedState)
|
||||
|
||||
const [activeDownloads, setActiveDownloads] = useState<RPCResult[]>()
|
||||
const [showBackdrop, setShowBackdrop] = useState(true)
|
||||
const [openDialog, setOpenDialog] = useState(false)
|
||||
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { client } = useContext(RPCClientContext)
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
/* -------------------- Effects -------------------- */
|
||||
|
||||
/* WebSocket connect event handler*/
|
||||
useEffect(() => {
|
||||
if (status.connected) { return }
|
||||
|
||||
const sub = socket$.subscribe({
|
||||
next: () => {
|
||||
dispatch(connected())
|
||||
},
|
||||
error: () => {
|
||||
pushMessage(
|
||||
`${i18n.t('rpcConnErr')} (${settings.serverAddr}:${settings.serverPort})`,
|
||||
"error"
|
||||
)
|
||||
setShowBackdrop(false)
|
||||
}
|
||||
})
|
||||
return () => sub.unsubscribe()
|
||||
}, [socket$, status.connected])
|
||||
|
||||
useEffect(() => {
|
||||
if (status.connected) {
|
||||
if (isConnected) {
|
||||
client.running()
|
||||
const interval = setInterval(() => client.running(), 1000)
|
||||
return () => clearInterval(interval)
|
||||
}
|
||||
}, [status.connected])
|
||||
}, [isConnected])
|
||||
|
||||
useEffect(() => {
|
||||
client
|
||||
.freeSpace()
|
||||
.then(bytes => dispatch(setFreeSpace(bytes.result)))
|
||||
.then(bytes => setFreeSpace(bytes.result))
|
||||
.catch(() => {
|
||||
pushMessage(
|
||||
`${i18n.t('rpcConnErr')} (${settings.serverAddr}:${settings.serverPort})`,
|
||||
`${i18n.t('rpcConnErr')} (${serverAddressAndPort})`,
|
||||
"error"
|
||||
)
|
||||
setShowBackdrop(false)
|
||||
setIsDownloading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!status.connected) { return }
|
||||
|
||||
const sub = socket$.subscribe((event: RPCResponse<RPCResult[]>) => {
|
||||
if (!isRPCResponse(event)) { return }
|
||||
|
||||
setActiveDownloads((event.result ?? [])
|
||||
.filter(f => !!f.info.url)
|
||||
.sort((a, b) => datetimeCompareFunc(
|
||||
b.info.created_at,
|
||||
a.info.created_at,
|
||||
)))
|
||||
})
|
||||
|
||||
pushMessage(
|
||||
`Connected to (${settings.serverAddr}:${settings.serverPort})`,
|
||||
"success"
|
||||
)
|
||||
|
||||
return () => sub.unsubscribe()
|
||||
}, [socket$, status.connected])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeDownloads && activeDownloads.length >= 0) {
|
||||
setShowBackdrop(false)
|
||||
}
|
||||
}, [activeDownloads?.length])
|
||||
|
||||
const abort = (id?: string) => {
|
||||
if (id) {
|
||||
client.kill(id)
|
||||
return
|
||||
}
|
||||
client.killAll()
|
||||
}
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={showBackdrop}
|
||||
open={!isDownloading}
|
||||
>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
{activeDownloads?.length === 0 &&
|
||||
<Splash />
|
||||
}
|
||||
{
|
||||
settings.listView ?
|
||||
<DownloadsListView downloads={activeDownloads ?? []} onStop={abort} /> :
|
||||
<DownloadsCardView downloads={activeDownloads ?? []} onStop={abort} />
|
||||
}
|
||||
<SpeedDial
|
||||
ariaLabel="SpeedDial basic example"
|
||||
sx={{ position: 'absolute', bottom: 32, right: 32 }}
|
||||
icon={<SpeedDialIcon />}
|
||||
>
|
||||
<SpeedDialAction
|
||||
icon={<FormatListBulleted />}
|
||||
tooltipTitle={`Table view`}
|
||||
onClick={() => dispatch(toggleListView())}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<DeleteForeverIcon />}
|
||||
tooltipTitle={i18n.t('abortAllButton')}
|
||||
onClick={() => abort()}
|
||||
/>
|
||||
<SpeedDialAction
|
||||
icon={<AddCircleIcon />}
|
||||
tooltipTitle={`New download`}
|
||||
onClick={() => setOpenDialog(true)}
|
||||
/>
|
||||
</SpeedDial>
|
||||
<Splash />
|
||||
<Downloads />
|
||||
<HomeSpeedDial
|
||||
onOpen={() => setOpenDialog(true)}
|
||||
/>
|
||||
<DownloadDialog
|
||||
open={openDialog}
|
||||
onClose={() => {
|
||||
setOpenDialog(false)
|
||||
setShowBackdrop(false)
|
||||
setIsDownloading(false)
|
||||
}}
|
||||
onDownloadStart={() => {
|
||||
setOpenDialog(false)
|
||||
setShowBackdrop(true)
|
||||
setIsDownloading(false)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
TextField,
|
||||
Typography
|
||||
} from '@mui/material'
|
||||
import { getHttpEndpoint } from '../utils'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { getHttpEndpoint } from '../utils'
|
||||
|
||||
const LoginContainer = styled(Container)({
|
||||
display: 'flex',
|
||||
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
Stack,
|
||||
Switch,
|
||||
TextField,
|
||||
Typography
|
||||
Typography,
|
||||
capitalize
|
||||
} from '@mui/material'
|
||||
import { useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRecoilState } from 'recoil'
|
||||
import {
|
||||
Subject,
|
||||
debounceTime,
|
||||
@@ -26,38 +27,45 @@ import {
|
||||
takeWhile
|
||||
} from 'rxjs'
|
||||
import {
|
||||
LanguageUnion,
|
||||
ThemeUnion,
|
||||
setCliArgs,
|
||||
setEnableCustomArgs,
|
||||
setFileRenaming,
|
||||
setFormatSelection,
|
||||
setLanguage,
|
||||
setPathOverriding,
|
||||
setServerAddr,
|
||||
setServerPort,
|
||||
setTheme
|
||||
} from '../features/settings/settingsSlice'
|
||||
Language,
|
||||
Theme,
|
||||
enableCustomArgsState,
|
||||
fileRenamingState,
|
||||
formatSelectionState,
|
||||
languageState,
|
||||
languages,
|
||||
latestCliArgumentsState,
|
||||
pathOverridingState,
|
||||
serverAddressState,
|
||||
serverPortState,
|
||||
themeState
|
||||
} from '../atoms/settings'
|
||||
import { useToast } from '../hooks/toast'
|
||||
import { useI18n } from '../hooks/useI18n'
|
||||
import { useRPC } from '../hooks/useRPC'
|
||||
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'
|
||||
|
||||
// NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS
|
||||
export default function Settings() {
|
||||
const dispatch = useDispatch()
|
||||
const [formatSelection, setFormatSelection] = useRecoilState(formatSelectionState)
|
||||
const [pathOverriding, setPathOverriding] = useRecoilState(pathOverridingState)
|
||||
const [fileRenaming, setFileRenaming] = useRecoilState(fileRenamingState)
|
||||
const [enableArgs, setEnableArgs] = useRecoilState(enableCustomArgsState)
|
||||
const [serverAddr, setServerAddr] = useRecoilState(serverAddressState)
|
||||
const [serverPort, setServerPort] = useRecoilState(serverPortState)
|
||||
const [language, setLanguage] = useRecoilState(languageState)
|
||||
const [cliArgs, setCliArgs] = useRecoilState(latestCliArgumentsState)
|
||||
const [theme, setTheme] = useRecoilState(themeState)
|
||||
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const [invalidIP, setInvalidIP] = useState(false)
|
||||
|
||||
const [invalidIP, setInvalidIP] = useState(false);
|
||||
|
||||
const { i18n } = useContext(I18nContext)
|
||||
const { client } = useContext(RPCClientContext)
|
||||
const { i18n } = useI18n()
|
||||
const { client } = useRPC()
|
||||
|
||||
const { pushMessage } = useToast()
|
||||
|
||||
const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [])
|
||||
const argsBuilder = useMemo(() => new CliArguments().fromString(cliArgs), [])
|
||||
|
||||
const serverAddr$ = useMemo(() => new Subject<string>(), [])
|
||||
const serverPort$ = useMemo(() => new Subject<string>(), [])
|
||||
@@ -71,10 +79,10 @@ export default function Settings() {
|
||||
.subscribe(addr => {
|
||||
if (validateIP(addr)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(addr))
|
||||
setServerAddr(addr)
|
||||
} else if (validateDomain(addr)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(addr))
|
||||
setServerAddr(addr)
|
||||
} else {
|
||||
setInvalidIP(true)
|
||||
}
|
||||
@@ -90,7 +98,7 @@ export default function Settings() {
|
||||
takeWhile(val => isFinite(val) && val <= 65535),
|
||||
)
|
||||
.subscribe(port => {
|
||||
dispatch(setServerPort(port.toString()))
|
||||
setServerPort(port)
|
||||
})
|
||||
return () => sub.unsubscribe()
|
||||
}, [])
|
||||
@@ -98,15 +106,15 @@ export default function Settings() {
|
||||
/**
|
||||
* Language toggler handler
|
||||
*/
|
||||
const handleLanguageChange = (event: SelectChangeEvent<LanguageUnion>) => {
|
||||
dispatch(setLanguage(event.target.value as LanguageUnion));
|
||||
const handleLanguageChange = (event: SelectChangeEvent<Language>) => {
|
||||
setLanguage(event.target.value as Language)
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme toggler handler
|
||||
*/
|
||||
const handleThemeChange = (event: SelectChangeEvent<ThemeUnion>) => {
|
||||
dispatch(setTheme(event.target.value as ThemeUnion));
|
||||
const handleThemeChange = (event: SelectChangeEvent<Theme>) => {
|
||||
setTheme(event.target.value as Theme)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,7 +145,7 @@ export default function Settings() {
|
||||
<TextField
|
||||
fullWidth
|
||||
label={i18n.t('serverAddressTitle')}
|
||||
defaultValue={settings.serverAddr}
|
||||
defaultValue={serverAddr}
|
||||
error={invalidIP}
|
||||
onChange={(e) => serverAddr$.next(e.currentTarget.value)}
|
||||
InputProps={{
|
||||
@@ -150,9 +158,9 @@ export default function Settings() {
|
||||
<TextField
|
||||
fullWidth
|
||||
label={i18n.t('serverPortTitle')}
|
||||
defaultValue={settings.serverPort}
|
||||
defaultValue={serverPort}
|
||||
onChange={(e) => serverPort$.next(e.currentTarget.value)}
|
||||
error={isNaN(Number(settings.serverPort)) || Number(settings.serverPort) > 65535}
|
||||
error={isNaN(Number(serverPort)) || Number(serverPort) > 65535}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -162,20 +170,15 @@ export default function Settings() {
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{i18n.t('languageSelect')}</InputLabel>
|
||||
<Select
|
||||
defaultValue={settings.language}
|
||||
defaultValue={language}
|
||||
label={i18n.t('languageSelect')}
|
||||
onChange={handleLanguageChange}
|
||||
>
|
||||
<MenuItem value="english">English</MenuItem>
|
||||
<MenuItem value="spanish">Spanish</MenuItem>
|
||||
<MenuItem value="italian">Italian</MenuItem>
|
||||
<MenuItem value="chinese">Chinese</MenuItem>
|
||||
<MenuItem value="russian">Russian</MenuItem>
|
||||
<MenuItem value="korean">Korean</MenuItem>
|
||||
<MenuItem value="japanese">Japanese</MenuItem>
|
||||
<MenuItem value="catalan">Catalan</MenuItem>
|
||||
<MenuItem value="ukrainian">Ukrainian</MenuItem>
|
||||
<MenuItem value="polish">Polish</MenuItem>
|
||||
{languages.map(l => (
|
||||
<MenuItem value={l} key={l}>
|
||||
{capitalize(l)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
@@ -183,7 +186,7 @@ export default function Settings() {
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>{i18n.t('themeSelect')}</InputLabel>
|
||||
<Select
|
||||
defaultValue={settings.theme}
|
||||
defaultValue={theme}
|
||||
label={i18n.t('themeSelect')}
|
||||
onChange={handleThemeChange}
|
||||
>
|
||||
@@ -196,8 +199,8 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={cliArgs.noMTime}
|
||||
onChange={() => dispatch(setCliArgs(cliArgs.toggleNoMTime().toString()))}
|
||||
defaultChecked={argsBuilder.noMTime}
|
||||
onChange={() => setCliArgs(argsBuilder.toggleNoMTime().toString())}
|
||||
/>
|
||||
}
|
||||
label={i18n.t('noMTimeCheckbox')}
|
||||
@@ -206,9 +209,9 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={cliArgs.extractAudio}
|
||||
onChange={() => dispatch(setCliArgs(cliArgs.toggleExtractAudio().toString()))}
|
||||
disabled={settings.formatSelection}
|
||||
defaultChecked={argsBuilder.extractAudio}
|
||||
onChange={() => setCliArgs(argsBuilder.toggleExtractAudio().toString())}
|
||||
disabled={formatSelection}
|
||||
/>
|
||||
}
|
||||
label={i18n.t('extractAudioCheckbox')}
|
||||
@@ -216,10 +219,10 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={settings.formatSelection}
|
||||
defaultChecked={formatSelection}
|
||||
onChange={() => {
|
||||
dispatch(setCliArgs(cliArgs.disableExtractAudio().toString()))
|
||||
dispatch(setFormatSelection(!settings.formatSelection))
|
||||
setCliArgs(argsBuilder.disableExtractAudio().toString())
|
||||
setFormatSelection(!formatSelection)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -233,9 +236,9 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={settings.pathOverriding}
|
||||
defaultChecked={!!pathOverriding}
|
||||
onChange={() => {
|
||||
dispatch(setPathOverriding(!settings.pathOverriding))
|
||||
setPathOverriding(state => !state)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -244,9 +247,9 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={settings.fileRenaming}
|
||||
defaultChecked={fileRenaming}
|
||||
onChange={() => {
|
||||
dispatch(setFileRenaming(!settings.fileRenaming))
|
||||
setFileRenaming(state => !state)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -255,9 +258,9 @@ export default function Settings() {
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
defaultChecked={settings.enableCustomArgs}
|
||||
defaultChecked={enableArgs}
|
||||
onChange={() => {
|
||||
dispatch(setEnableCustomArgs(!settings.enableCustomArgs))
|
||||
setEnableArgs(state => !state)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -281,5 +284,5 @@ export default function Settings() {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user