From b5731759b050c19a736b4e63a3d8dad529c8e62a Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Mon, 31 Jul 2023 12:27:36 +0200 Subject: [PATCH] migrated from redux to recoil --- frontend/package.json | 7 +- frontend/src/App.tsx | 7 +- frontend/src/Layout.tsx | 250 +++++++++--------- frontend/src/atoms/downloadTemplate.ts | 9 + frontend/src/atoms/downloads.ts | 7 + frontend/src/atoms/format.ts | 7 + frontend/src/atoms/i18n.ts | 9 + frontend/src/atoms/rpc.ts | 12 + frontend/src/atoms/settings.ts | 175 ++++++++++++ frontend/src/atoms/status.ts | 39 +++ frontend/src/atoms/toast.ts | 15 ++ frontend/src/components/ArchiveResult.tsx | 37 --- frontend/src/components/DownloadDialog.tsx | 41 +-- frontend/src/components/Downloads.tsx | 72 +++++ frontend/src/components/DownloadsCardView.tsx | 6 +- frontend/src/components/HomeSpeedDial.tsx | 51 ++++ frontend/src/components/SocketSubscriber.tsx | 46 ++++ frontend/src/components/Splash.tsx | 12 +- frontend/src/components/ThemeToggler.tsx | 18 +- .../formatSelection/formatSelectionSlice.ts | 7 - .../src/features/settings/settingsSlice.ts | 110 -------- frontend/src/features/status/statusSlice.ts | 55 ---- frontend/src/features/ui/toastSlice.ts | 46 ---- frontend/src/hooks/toast.ts | 17 +- frontend/src/hooks/useI18n.ts | 10 + frontend/src/hooks/useRPC.ts | 11 + frontend/src/lib/intl.ts | 38 +-- frontend/src/lib/rpcClient.ts | 19 +- frontend/src/providers/ToasterProvider.tsx | 40 ++- frontend/src/providers/i18nProvider.tsx | 30 --- frontend/src/providers/rpcClientProvider.tsx | 31 --- frontend/src/stores/store.ts | 17 -- frontend/src/views/Archive.tsx | 22 +- frontend/src/views/Home.tsx | 149 ++--------- frontend/src/views/Login.tsx | 2 +- frontend/src/views/Settings.tsx | 127 ++++----- 36 files changed, 810 insertions(+), 741 deletions(-) create mode 100644 frontend/src/atoms/downloadTemplate.ts create mode 100644 frontend/src/atoms/downloads.ts create mode 100644 frontend/src/atoms/format.ts create mode 100644 frontend/src/atoms/i18n.ts create mode 100644 frontend/src/atoms/rpc.ts create mode 100644 frontend/src/atoms/settings.ts create mode 100644 frontend/src/atoms/status.ts create mode 100644 frontend/src/atoms/toast.ts delete mode 100644 frontend/src/components/ArchiveResult.tsx create mode 100644 frontend/src/components/Downloads.tsx create mode 100644 frontend/src/components/HomeSpeedDial.tsx create mode 100644 frontend/src/components/SocketSubscriber.tsx delete mode 100644 frontend/src/features/formatSelection/formatSelectionSlice.ts delete mode 100644 frontend/src/features/settings/settingsSlice.ts delete mode 100644 frontend/src/features/status/statusSlice.ts delete mode 100644 frontend/src/features/ui/toastSlice.ts create mode 100644 frontend/src/hooks/useI18n.ts create mode 100644 frontend/src/hooks/useRPC.ts delete mode 100644 frontend/src/providers/i18nProvider.tsx delete mode 100644 frontend/src/providers/rpcClientProvider.tsx delete mode 100644 frontend/src/stores/store.ts diff --git a/frontend/package.json b/frontend/package.json index 7850656..3200085 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,11 +13,10 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.13.5", - "@reduxjs/toolkit": "^1.9.5", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-redux": "^8.1.1", "react-router-dom": "^6.13.0", + "recoil": "^0.7.7", "rxjs": "^7.8.1", "uuid": "^9.0.0" }, @@ -28,9 +27,9 @@ "@types/react-dom": "^18.2.6", "@types/react-router-dom": "^5.3.3", "@types/uuid": "^9.0.2", - "@vitejs/plugin-react": "^4.0.1", + "@vitejs/plugin-react": "^4.0.3", "buffer": "^6.0.3", "typescript": "^5.1.3", - "vite": "^4.3.9" + "vite": "^4.4.7" } } \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 234862d..8ba1188 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,12 +1,11 @@ -import { Provider } from 'react-redux' import { RouterProvider } from 'react-router-dom' +import { RecoilRoot } from 'recoil' import { router } from './router' -import { store } from './stores/store' export function App() { return ( - + - + ) } \ No newline at end of file diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index bc23444..cbd3245 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -1,12 +1,12 @@ import { ThemeProvider } from '@emotion/react' import ChevronLeft from '@mui/icons-material/ChevronLeft' import Dashboard from '@mui/icons-material/Dashboard' +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 DownloadIcon from '@mui/icons-material/Download' import CssBaseline from '@mui/material/CssBaseline' import Divider from '@mui/material/Divider' import IconButton from '@mui/material/IconButton' @@ -18,23 +18,23 @@ import Toolbar from '@mui/material/Toolbar' import Typography from '@mui/material/Typography' import { grey } from '@mui/material/colors' import { useMemo, useState } from 'react' -import { useSelector } from 'react-redux' import { Link, Outlet } from 'react-router-dom' -import { RootState } from './stores/store' +import { useRecoilValue } from 'recoil' +import { settingsState } from './atoms/settings' +import { statusState } from './atoms/status' import AppBar from './components/AppBar' import Drawer from './components/Drawer' import Logout from './components/Logout' import ThemeToggler from './components/ThemeToggler' import Toaster from './providers/ToasterProvider' -import I18nProvider from './providers/i18nProvider' -import RPCClientProvider from './providers/rpcClientProvider' import { formatGiB } from './utils' +import SocketSubscriber from './components/SocketSubscriber' export default function Layout() { const [open, setOpen] = useState(false) - const settings = useSelector((state: RootState) => state.settings) - const status = useSelector((state: RootState) => state.status) + const settings = useRecoilValue(settingsState) + const status = useRecoilValue(statusState) const mode = settings.theme const theme = useMemo(() => @@ -54,132 +54,130 @@ export default function Layout() { return ( - - - - - - - - - - - yt-dlp WebUI - - { - status.freeSpace ? -
- - -  {formatGiB(status.freeSpace)}  - -
- : null - } -
- - -  {status.connected ? settings.serverAddr : 'not connected'} - -
- - - - + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + yt-dlp WebUI + + { + status.freeSpace ? +
+ + +  {formatGiB(status.freeSpace)}  + +
+ : null + } +
+ + +  {status.connected ? settings.serverAddr : 'not connected'} + +
+ + + + - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + ) } \ No newline at end of file diff --git a/frontend/src/atoms/downloadTemplate.ts b/frontend/src/atoms/downloadTemplate.ts new file mode 100644 index 0000000..a3e6153 --- /dev/null +++ b/frontend/src/atoms/downloadTemplate.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil' + +export const downloadTemplateState = atom({ + key: 'downloadTemplateState', + default: localStorage.getItem('lastDownloadTemplate') ?? '', + effects: [ + ({ onSet }) => onSet(e => localStorage.setItem('lastDownloadTemplate', e)) + ] +}) \ No newline at end of file diff --git a/frontend/src/atoms/downloads.ts b/frontend/src/atoms/downloads.ts new file mode 100644 index 0000000..c3ca2e0 --- /dev/null +++ b/frontend/src/atoms/downloads.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil' +import { RPCResult } from '../types' + +export const activeDownloadsState = atom({ + key: 'activeDownloadsState', + default: undefined +}) \ No newline at end of file diff --git a/frontend/src/atoms/format.ts b/frontend/src/atoms/format.ts new file mode 100644 index 0000000..632d707 --- /dev/null +++ b/frontend/src/atoms/format.ts @@ -0,0 +1,7 @@ +import { atom } from 'recoil' +import { DLMetadata } from '../types' + +export const selectedFormatState = atom>({ + key: 'selectedFormatState', + default: {}, +}) \ No newline at end of file diff --git a/frontend/src/atoms/i18n.ts b/frontend/src/atoms/i18n.ts new file mode 100644 index 0000000..aacccff --- /dev/null +++ b/frontend/src/atoms/i18n.ts @@ -0,0 +1,9 @@ +import { selector } from 'recoil' +import I18nBuilder from '../lib/intl' +import { languageState } from './settings' + +export const i18nBuilderState = selector({ + key: 'i18nBuilderState', + get: ({ get }) => new I18nBuilder(get(languageState)), + dangerouslyAllowMutability: true, +}) diff --git a/frontend/src/atoms/rpc.ts b/frontend/src/atoms/rpc.ts new file mode 100644 index 0000000..079c060 --- /dev/null +++ b/frontend/src/atoms/rpc.ts @@ -0,0 +1,12 @@ +import { selector } from 'recoil' +import { RPCClient } from '../lib/rpcClient' +import { rpcHTTPEndpoint, rpcWebSocketEndpoint } from './settings' + +export const rpcClientState = selector({ + key: 'rpcClientState', + get: ({ get }) => + new RPCClient(get(rpcHTTPEndpoint), get(rpcWebSocketEndpoint)), + set: ({ get }) => + new RPCClient(get(rpcHTTPEndpoint), get(rpcWebSocketEndpoint)), + dangerouslyAllowMutability: true, +}) \ No newline at end of file diff --git a/frontend/src/atoms/settings.ts b/frontend/src/atoms/settings.ts new file mode 100644 index 0000000..abdcc22 --- /dev/null +++ b/frontend/src/atoms/settings.ts @@ -0,0 +1,175 @@ +import { atom, selector } from 'recoil' + +export type Language = + | 'english' + | 'chinese' + | 'russian' + | 'italian' + | 'spanish' + | 'korean' + | 'japanese' + | 'catalan' + | 'ukrainian' + | 'polish' + +export const languages = [ + 'english', + 'chinese', + 'russian', + 'italian', + 'spanish', + 'korean', + 'japanese', + 'catalan', + 'ukrainian', + 'polish', +] as const + +export type Theme = 'light' | 'dark' + +export interface SettingsState { + serverAddr: string + serverPort: number + language: Language + theme: Theme + cliArgs: string + formatSelection: boolean + fileRenaming: boolean + pathOverriding: boolean + enableCustomArgs: boolean + listView: boolean +} + +export const languageState = atom({ + key: 'languageState', + default: localStorage.getItem('language') as Language ?? 'english', + effects: [ + ({ onSet }) => + onSet(l => localStorage.setItem('language', l.toString())) + ] +}) + +export const themeState = atom({ + key: 'themeStateState', + default: localStorage.getItem('theme') as Theme ?? 'system', + effects: [ + ({ onSet }) => + onSet(l => localStorage.setItem('theme', l.toString())) + ] +}) + +export const serverAddressState = atom({ + key: 'serverAddressState', + default: localStorage.getItem('server-addr') ?? window.location.hostname, + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('server-addr', a.toString())) + ] +}) + +export const serverPortState = atom({ + key: 'serverPortState', + default: Number(localStorage.getItem('server-port')) ?? + Number(window.location.port), + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('server-port', a.toString())) + ] +}) + +export const latestCliArgumentsState = atom({ + key: 'latestCliArgumentsState', + default: localStorage.getItem('cli-args') ?? '', + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('cli-args', a.toString())) + ] +}) + +export const formatSelectionState = atom({ + key: 'formatSelectionState', + default: localStorage.getItem('format-selection') === "true", + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('format-selection', a.toString())) + ] +}) + +export const fileRenamingState = atom({ + key: 'fileRenamingState', + default: localStorage.getItem('file-renaming') === "true", + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('file-renaming', a.toString())) + ] +}) + +export const pathOverridingState = atom({ + key: 'pathOverridingState', + default: localStorage.getItem('path-overriding') === "true", + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('path-overriding', a.toString())) + ] +}) + +export const enableCustomArgsState = atom({ + key: 'enableCustomArgsState', + default: localStorage.getItem('enable-custom-args') === "true", + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('enable-custom-args', a.toString())) + ] +}) + +export const listViewState = atom({ + key: 'listViewState', + default: localStorage.getItem('listview') === "true", + effects: [ + ({ onSet }) => + onSet(a => localStorage.setItem('listview', a.toString())) + ] +}) + +export const serverAddressAndPortState = selector({ + key: 'serverAddressAndPortState', + get: ({ get }) => `${get(serverAddressState)}:${get(serverPortState)}` +}) + +export const serverURL = selector({ + key: 'serverURL', + get: ({ get }) => + `${window.location.protocol}//${get(serverAddressState)}:${get(serverPortState)}` +}) + +export const rpcWebSocketEndpoint = selector({ + key: 'rpcWebSocketEndpoint', + get: ({ get }) => { + const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + return `${proto}//${get(serverAddressAndPortState)}/rpc/ws` + } +}) + +export const rpcHTTPEndpoint = selector({ + key: 'rpcHTTPEndpoint', + get: ({ get }) => { + const proto = window.location.protocol + return `${proto}//${get(serverAddressAndPortState)}/rpc/http` + } +}) + +export const settingsState = selector({ + key: 'settingsState', + get: ({ get }) => ({ + serverAddr: get(serverAddressState), + serverPort: get(serverPortState), + language: get(languageState), + theme: get(themeState), + cliArgs: get(latestCliArgumentsState), + formatSelection: get(formatSelectionState), + fileRenaming: get(fileRenamingState), + pathOverriding: get(pathOverridingState), + enableCustomArgs: get(enableCustomArgsState), + listView: get(listViewState), + }) +}) \ No newline at end of file diff --git a/frontend/src/atoms/status.ts b/frontend/src/atoms/status.ts new file mode 100644 index 0000000..99abde8 --- /dev/null +++ b/frontend/src/atoms/status.ts @@ -0,0 +1,39 @@ +import { atom, selector } from 'recoil' + +type StatusState = { + connected: boolean, + updated: boolean, + downloading: boolean, + freeSpace: number, +} + + +export const connectedState = atom({ + key: 'connectedState', + default: false +}) + +export const updatedBinaryState = atom({ + key: 'updatedBinaryState', + default: false +}) + +export const isDownloadingState = atom({ + key: 'isDownloadingState', + default: false +}) + +export const freeSpaceBytesState = atom({ + key: 'freeSpaceBytesState', + default: 0 +}) + +export const statusState = selector({ + key: 'statusState', + get: ({ get }) => ({ + connected: get(connectedState), + updated: get(updatedBinaryState), + downloading: get(isDownloadingState), + freeSpace: get(freeSpaceBytesState), + }) +}) \ No newline at end of file diff --git a/frontend/src/atoms/toast.ts b/frontend/src/atoms/toast.ts new file mode 100644 index 0000000..f44d339 --- /dev/null +++ b/frontend/src/atoms/toast.ts @@ -0,0 +1,15 @@ +import { AlertColor } from '@mui/material' +import { atom } from 'recoil' + +type Toast = { + open: boolean, + message: string + autoClose: boolean + createdAt: number, + severity?: AlertColor +} + +export const toastListState = atom({ + key: 'toastListState', + default: [], +}) \ No newline at end of file diff --git a/frontend/src/components/ArchiveResult.tsx b/frontend/src/components/ArchiveResult.tsx deleted file mode 100644 index a71a65a..0000000 --- a/frontend/src/components/ArchiveResult.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { - Card, - CardActionArea, - CardContent, - CardMedia, - Skeleton, - Typography -} from '@mui/material' -import { ellipsis } from '../utils' - -type Props = { - title: string, - thumbnail: string, - url: string, -} - -export function ArchiveResult({ title, thumbnail, url }: Props) { - return ( - - window.open(url)}> - {thumbnail ? - : - - } - - - {ellipsis(title, 72)} - - - - - ) -} \ No newline at end of file diff --git a/frontend/src/components/DownloadDialog.tsx b/frontend/src/components/DownloadDialog.tsx index 598d7db..9f3a29a 100644 --- a/frontend/src/components/DownloadDialog.tsx +++ b/frontend/src/components/DownloadDialog.tsx @@ -26,19 +26,19 @@ import { TransitionProps } from '@mui/material/transitions' import { Buffer } from 'buffer' import { forwardRef, - useContext, useEffect, useMemo, useRef, useState, useTransition } from 'react' -import { useSelector } from 'react-redux' +import { useRecoilState, useRecoilValue } from 'recoil' +import { settingsState } from '../atoms/settings' +import { connectedState } from '../atoms/status' import FormatsGrid from '../components/FormatsGrid' +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 type { DLMetadata } from '../types' import { isValidURL, toFormatArgs } from '../utils' @@ -62,9 +62,9 @@ export default function DownloadDialog({ onClose, onDownloadStart }: Props) { - // redux state - const settings = useSelector((state: RootState) => state.settings) - const status = useSelector((state: RootState) => state.status) + // recoil state + const settings = useRecoilValue(settingsState) + const [isConnected] = useRecoilState(connectedState) // ephemeral state const [downloadFormats, setDownloadFormats] = useState() @@ -85,11 +85,12 @@ export default function DownloadDialog({ // memos const cliArgs = useMemo(() => - new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) + new CliArguments().fromString(settings.cliArgs), [settings.cliArgs] + ) // context - const { i18n } = useContext(I18nContext) - const { client } = useContext(RPCClientContext) + const { i18n } = useI18n() + const { client } = useRPC() // refs const urlInputRef = useRef(null) @@ -254,7 +255,7 @@ export default function DownloadDialog({ variant="outlined" onChange={handleUrlChange} disabled={ - !status.connected + !isConnected || (settings.formatSelection && downloadFormats != null) } InputProps={{ @@ -290,7 +291,10 @@ export default function DownloadDialog({ variant="outlined" onChange={handleCustomArgsChange} value={customArgs} - disabled={!status.connected || (settings.formatSelection && downloadFormats != null)} + disabled={ + !isConnected || + (settings.formatSelection && downloadFormats != null) + } /> } @@ -304,7 +308,10 @@ export default function DownloadDialog({ variant="outlined" value={fileNameOverride} onChange={handleFilenameOverrideChange} - disabled={!status.connected || (settings.formatSelection && downloadFormats != null)} + disabled={ + !isConnected || + (settings.formatSelection && downloadFormats != null) + } /> } @@ -338,7 +345,11 @@ export default function DownloadDialog({ : sendUrl() } > - {settings.formatSelection ? i18n.t('selectFormatButton') : i18n.t('startButton')} + { + settings.formatSelection + ? i18n.t('selectFormatButton') + : i18n.t('startButton') + } diff --git a/frontend/src/components/Downloads.tsx b/frontend/src/components/Downloads.tsx new file mode 100644 index 0000000..17761be --- /dev/null +++ b/frontend/src/components/Downloads.tsx @@ -0,0 +1,72 @@ +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' + +const Downloads: React.FC = () => { + const [active, setActive] = useRecoilState(activeDownloadsState) + const isConnected = useRecoilValue(connectedState) + const listView = useRecoilValue(listViewState) + + 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) => { + 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(() => { + if (active) { + setIsDownloading(true) + } + }, [active?.length]) + + if (listView) { + return ( + + ) + } + + return ( + + ) +} + +export default Downloads \ No newline at end of file diff --git a/frontend/src/components/DownloadsCardView.tsx b/frontend/src/components/DownloadsCardView.tsx index 741a09d..6e1a4c8 100644 --- a/frontend/src/components/DownloadsCardView.tsx +++ b/frontend/src/components/DownloadsCardView.tsx @@ -1,7 +1,7 @@ import { Grid } from "@mui/material" -import { Fragment, useContext } from "react" +import { Fragment } from "react" import { useToast } from "../hooks/toast" -import { I18nContext } from "../providers/i18nProvider" +import { useI18n } from '../hooks/useI18n' import type { RPCResult } from "../types" import { StackableResult } from "./StackableResult" @@ -11,7 +11,7 @@ type Props = { } export function DownloadsCardView({ downloads, onStop }: Props) { - const { i18n } = useContext(I18nContext) + const { i18n } = useI18n() const { pushMessage } = useToast() return ( diff --git a/frontend/src/components/HomeSpeedDial.tsx b/frontend/src/components/HomeSpeedDial.tsx new file mode 100644 index 0000000..8e44337 --- /dev/null +++ b/frontend/src/components/HomeSpeedDial.tsx @@ -0,0 +1,51 @@ +import AddCircleIcon from '@mui/icons-material/AddCircle' +import DeleteForeverIcon from '@mui/icons-material/DeleteForever' +import FormatListBulleted from '@mui/icons-material/FormatListBulleted' +import { + SpeedDial, + SpeedDialAction, + SpeedDialIcon +} from '@mui/material' +import { useRecoilState } from 'recoil' +import { listViewState } from '../atoms/settings' +import { useI18n } from '../hooks/useI18n' +import { useRPC } from '../hooks/useRPC' + +type Props = { + onOpen: () => void +} + +const HomeSpeedDial: React.FC = ({ onOpen }) => { + const [, setListView] = useRecoilState(listViewState) + + const { i18n } = useI18n() + const { client } = useRPC() + + const abort = () => client.killAll() + + return ( + } + > + } + tooltipTitle={`Table view`} + onClick={() => setListView(state => !state)} + /> + } + tooltipTitle={i18n.t('abortAllButton')} + onClick={abort} + /> + } + tooltipTitle={`New download`} + onClick={onOpen} + /> + + ) +} + +export default HomeSpeedDial \ No newline at end of file diff --git a/frontend/src/components/SocketSubscriber.tsx b/frontend/src/components/SocketSubscriber.tsx new file mode 100644 index 0000000..01d3999 --- /dev/null +++ b/frontend/src/components/SocketSubscriber.tsx @@ -0,0 +1,46 @@ +import { useEffect } from 'react' +import { useRecoilState, useRecoilValue } from 'recoil' +import { connectedState } from '../atoms/status' +import { useRPC } from '../hooks/useRPC' +import { useToast } from '../hooks/toast' +import { serverAddressAndPortState } from '../atoms/settings' +import { useI18n } from '../hooks/useI18n' + +interface Props extends React.HTMLAttributes { } + +const SocketSubscriber: React.FC = ({ children }) => { + const [isConnected, setIsConnected] = useRecoilState(connectedState) + const serverAddressAndPort = useRecoilValue(serverAddressAndPortState) + + const { i18n } = useI18n() + const { socket$ } = useRPC() + const { pushMessage } = useToast() + + useEffect(() => { + if (isConnected) { return } + + 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]) + + return ( + <>{children} + ) +} + +export default SocketSubscriber \ No newline at end of file diff --git a/frontend/src/components/Splash.tsx b/frontend/src/components/Splash.tsx index c6cc1d3..2b303a4 100644 --- a/frontend/src/components/Splash.tsx +++ b/frontend/src/components/Splash.tsx @@ -1,7 +1,8 @@ import CloudDownloadIcon from '@mui/icons-material/CloudDownload' import { Container, SvgIcon, Typography, styled } from '@mui/material' -import { useContext } from 'react' -import { I18nContext } from '../providers/i18nProvider' +import { useRecoilValue } from 'recoil' +import { activeDownloadsState } from '../atoms/downloads' +import { useI18n } from '../hooks/useI18n' const FlexContainer = styled(Container)({ display: 'flex', @@ -21,7 +22,12 @@ const Title = styled(Typography)({ }) export default function Splash() { - const { i18n } = useContext(I18nContext) + const { i18n } = useI18n() + const activeDownloads = useRecoilValue(activeDownloadsState) + + if (!activeDownloads || activeDownloads.length !== 0) { + return null + } return ( diff --git a/frontend/src/components/ThemeToggler.tsx b/frontend/src/components/ThemeToggler.tsx index 229b00a..c7a3932 100644 --- a/frontend/src/components/ThemeToggler.tsx +++ b/frontend/src/components/ThemeToggler.tsx @@ -1,22 +1,20 @@ -import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material' -import { useDispatch, useSelector } from 'react-redux' -import { setTheme } from '../features/settings/settingsSlice' -import { RootState } from '../stores/store' import { Brightness4, Brightness5 } from '@mui/icons-material' +import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material' +import { useRecoilState } from 'recoil' +import { themeState } from '../atoms/settings' export default function ThemeToggler() { - const settings = useSelector((state: RootState) => state.settings) - const dispatch = useDispatch() + const [theme, setTheme] = useRecoilState(themeState) return ( { - settings.theme === 'light' - ? dispatch(setTheme('dark')) - : dispatch(setTheme('light')) + theme === 'light' + ? setTheme('dark') + : setTheme('light') }}> { - settings.theme === 'light' + theme === 'light' ? : } diff --git a/frontend/src/features/formatSelection/formatSelectionSlice.ts b/frontend/src/features/formatSelection/formatSelectionSlice.ts deleted file mode 100644 index 1398bfc..0000000 --- a/frontend/src/features/formatSelection/formatSelectionSlice.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -export interface FormatSelectionState { - bestFormat: string - audioFormat: string - videoFormat: string -} \ No newline at end of file diff --git a/frontend/src/features/settings/settingsSlice.ts b/frontend/src/features/settings/settingsSlice.ts deleted file mode 100644 index bfa42c7..0000000 --- a/frontend/src/features/settings/settingsSlice.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" - -export type LanguageUnion = - | "english" - | "chinese" - | "russian" - | "italian" - | "spanish" - | "korean" - | "japanese" - | "catalan" - | "ukrainian" - | "polish" - -export type ThemeUnion = "light" | "dark" - -export interface SettingsState { - serverAddr: string - serverPort: string - language: LanguageUnion - theme: ThemeUnion - cliArgs: string - formatSelection: boolean - ratelimit: string - fileRenaming: boolean - pathOverriding: boolean - enableCustomArgs: boolean - listView: boolean -} - -const initialState: SettingsState = { - serverAddr: localStorage.getItem("server-addr") || window.location.hostname, - serverPort: localStorage.getItem("server-port") || window.location.port, - language: (localStorage.getItem("language") || "english") as LanguageUnion, - theme: (localStorage.getItem("theme") || "light") as ThemeUnion, - cliArgs: localStorage.getItem("cli-args") ?? "", - formatSelection: localStorage.getItem("format-selection") === "true", - ratelimit: localStorage.getItem("rate-limit") ?? "", - fileRenaming: localStorage.getItem("file-renaming") === "true", - pathOverriding: localStorage.getItem("path-overriding") === "true", - enableCustomArgs: localStorage.getItem("enable-custom-args") === "true", - listView: localStorage.getItem("listview") === "true", -} - -export const settingsSlice = createSlice({ - name: "settings", - initialState, - reducers: { - setServerAddr: (state, action: PayloadAction) => { - state.serverAddr = action.payload - localStorage.setItem("server-addr", action.payload) - }, - setServerPort: (state, action: PayloadAction) => { - state.serverPort = action.payload - localStorage.setItem("server-port", action.payload) - }, - setLanguage: (state, action: PayloadAction) => { - state.language = action.payload - localStorage.setItem("language", action.payload) - }, - setCliArgs: (state, action: PayloadAction) => { - state.cliArgs = action.payload - localStorage.setItem("cli-args", action.payload) - }, - setTheme: (state, action: PayloadAction) => { - state.theme = action.payload - localStorage.setItem("theme", action.payload) - }, - setFormatSelection: (state, action: PayloadAction) => { - state.formatSelection = action.payload - localStorage.setItem("format-selection", action.payload.toString()) - }, - setRateLimit: (state, action: PayloadAction) => { - state.ratelimit = action.payload - localStorage.setItem("rate-limit", action.payload) - }, - setPathOverriding: (state, action: PayloadAction) => { - state.pathOverriding = action.payload - localStorage.setItem("path-overriding", action.payload.toString()) - }, - setFileRenaming: (state, action: PayloadAction) => { - state.fileRenaming = action.payload - localStorage.setItem("file-renaming", action.payload.toString()) - }, - setEnableCustomArgs: (state, action: PayloadAction) => { - state.enableCustomArgs = action.payload - localStorage.setItem("enable-custom-args", action.payload.toString()) - }, - toggleListView: (state) => { - state.listView = !state.listView - localStorage.setItem("listview", state.listView.toString()) - }, - } -}) - -export const { - setLanguage, - setCliArgs, - setTheme, - setServerAddr, - setServerPort, - setFormatSelection, - setRateLimit, - setFileRenaming, - setPathOverriding, - setEnableCustomArgs, - toggleListView -} = settingsSlice.actions - -export default settingsSlice.reducer diff --git a/frontend/src/features/status/statusSlice.ts b/frontend/src/features/status/statusSlice.ts deleted file mode 100644 index 209bc53..0000000 --- a/frontend/src/features/status/statusSlice.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { createSlice, PayloadAction } from "@reduxjs/toolkit" - -export interface StatusState { - connected: boolean, - updated: boolean, - downloading: boolean, - freeSpace: number, -} - -const initialState: StatusState = { - connected: false, - updated: false, - downloading: false, - freeSpace: 0, -} - -export const statusSlice = createSlice({ - name: 'status', - initialState, - reducers: { - connected: (state) => { - state.connected = true - }, - disconnected: (state) => { - state.connected = false - }, - updated: (state) => { - state.updated = true - }, - alreadyUpdated: (state) => { - state.updated = false - }, - downloading: (state) => { - state.downloading = true - }, - finished: (state) => { - state.downloading = false - }, - setFreeSpace: (state, action: PayloadAction) => { - state.freeSpace = action.payload - } - } -}) - -export const { - connected, - disconnected, - updated, - alreadyUpdated, - downloading, - finished, - setFreeSpace -} = statusSlice.actions - -export default statusSlice.reducer \ No newline at end of file diff --git a/frontend/src/features/ui/toastSlice.ts b/frontend/src/features/ui/toastSlice.ts deleted file mode 100644 index 10f642e..0000000 --- a/frontend/src/features/ui/toastSlice.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { AlertColor } from '@mui/material' -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -export interface ToastState { - message: string - open: boolean - autoClose: boolean - severity?: AlertColor -} - -type MessageAction = { - message: string, - severity?: AlertColor -} - -const initialState: ToastState = { - message: '', - open: false, - autoClose: true, -} - -export const toastSlice = createSlice({ - name: 'toast', - initialState, - reducers: { - setMessage: (state, action: PayloadAction) => { - state.message = action.payload.message - state.severity = action.payload.severity - state.open = true - }, - setOpen: (state) => { - state.open = true - }, - setClose: (state) => { - state.open = false - }, - } -}) - -export const { - setMessage, - setClose, - setOpen, -} = toastSlice.actions - -export default toastSlice.reducer \ No newline at end of file diff --git a/frontend/src/hooks/toast.ts b/frontend/src/hooks/toast.ts index 32f1f93..3e589ee 100644 --- a/frontend/src/hooks/toast.ts +++ b/frontend/src/hooks/toast.ts @@ -1,16 +1,19 @@ -import { useDispatch } from "react-redux" -import { setMessage } from "../features/ui/toastSlice" -import { AlertColor } from "@mui/material" +import { AlertColor } from '@mui/material' +import { useRecoilState } from 'recoil' +import { toastListState } from '../atoms/toast' export const useToast = () => { - const dispatch = useDispatch() + const [toasts, setToasts] = useRecoilState(toastListState) return { pushMessage: (message: string, severity?: AlertColor) => { - dispatch(setMessage({ + setToasts([{ + open: true, message: message, - severity: severity - })) + severity: severity, + autoClose: true, + createdAt: Date.now() + }, ...toasts]) } } } \ No newline at end of file diff --git a/frontend/src/hooks/useI18n.ts b/frontend/src/hooks/useI18n.ts new file mode 100644 index 0000000..bba9802 --- /dev/null +++ b/frontend/src/hooks/useI18n.ts @@ -0,0 +1,10 @@ +import { useRecoilValue } from 'recoil' +import { i18nBuilderState } from '../atoms/i18n' + +export const useI18n = () => { + const instance = useRecoilValue(i18nBuilderState) + + return { + i18n: instance + } +} \ No newline at end of file diff --git a/frontend/src/hooks/useRPC.ts b/frontend/src/hooks/useRPC.ts new file mode 100644 index 0000000..5de6414 --- /dev/null +++ b/frontend/src/hooks/useRPC.ts @@ -0,0 +1,11 @@ +import { useRecoilValue } from 'recoil' +import { rpcClientState } from '../atoms/rpc' + +export const useRPC = () => { + const client = useRecoilValue(rpcClientState) + + return { + client, + socket$: client.socket$ + } +} \ No newline at end of file diff --git a/frontend/src/lib/intl.ts b/frontend/src/lib/intl.ts index 760a86c..629c741 100644 --- a/frontend/src/lib/intl.ts +++ b/frontend/src/lib/intl.ts @@ -2,27 +2,27 @@ import i18n from "../assets/i18n.yaml" export default class I18nBuilder { - private language: string - private textMap = i18n.languages + private language: string + private textMap = i18n.languages - constructor(language: string) { - this.language = language - } + constructor(language: string) { + this.language = language + } - getLanguage(): string { - return this.language - } + getLanguage(): string { + return this.language + } - setLanguage(language: string): void { - this.language = language - } + setLanguage(language: string): void { + this.language = language + } - t(key: string): string { - const map = this.textMap[this.language] - if (map) { - const translation = map[key] - return translation ?? 'caption not defined' - } - return 'caption not defined' + t(key: string): string { + const map = this.textMap[this.language] + if (map) { + const translation = map[key] + return translation ?? 'caption not defined' } -} \ No newline at end of file + return 'caption not defined' + } +} diff --git a/frontend/src/lib/rpcClient.ts b/frontend/src/lib/rpcClient.ts index be6ec86..8dfd1a0 100644 --- a/frontend/src/lib/rpcClient.ts +++ b/frontend/src/lib/rpcClient.ts @@ -1,15 +1,20 @@ import type { DLMetadata, RPCRequest, RPCResponse } from '../types' -import { webSocket } from 'rxjs/webSocket' -import { getHttpRPCEndpoint, getWebSocketEndpoint } from '../utils' - -export const socket$ = webSocket(getWebSocketEndpoint()) +import { WebSocketSubject, webSocket } from 'rxjs/webSocket' export class RPCClient { private seq: number + private httpEndpoint: string + private _socket$: WebSocketSubject - constructor() { + constructor(httpEndpoint: string, webSocketEndpoint: string) { this.seq = 0 + this.httpEndpoint = httpEndpoint + this._socket$ = webSocket(webSocketEndpoint) + } + + public get socket$() { + return this._socket$ } private incrementSeq() { @@ -17,14 +22,14 @@ export class RPCClient { } private send(req: RPCRequest) { - socket$.next({ + this._socket$.next({ ...req, id: this.incrementSeq(), }) } private async sendHTTP(req: RPCRequest) { - const res = await fetch(getHttpRPCEndpoint(), { + const res = await fetch(this.httpEndpoint, { method: 'POST', body: JSON.stringify({ ...req, diff --git a/frontend/src/providers/ToasterProvider.tsx b/frontend/src/providers/ToasterProvider.tsx index dc7e5f8..0f4e158 100644 --- a/frontend/src/providers/ToasterProvider.tsx +++ b/frontend/src/providers/ToasterProvider.tsx @@ -1,22 +1,34 @@ import { Alert, Snackbar } from "@mui/material" -import { useDispatch, useSelector } from "react-redux" -import { setClose } from "../features/ui/toastSlice" -import { RootState } from "../stores/store" +import { useRecoilState } from 'recoil' +import { toastListState } from '../atoms/toast' +import { useEffect } from 'react' const Toaster: React.FC = () => { - const toast = useSelector((state: RootState) => state.toast) - const dispatch = useDispatch() + const [toasts, setToasts] = useRecoilState(toastListState) + + useEffect(() => { + if (toasts.length > 0) { + const interval = setInterval(() => { + setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500)) + }, 1500) + + return () => clearInterval(interval) + } + }, [setToasts, toasts]) return ( - dispatch(setClose())} - > - - {toast.message} - - + <> + {toasts.map((toast, index) => ( + + + {toast.message} + + + ))} + ) } diff --git a/frontend/src/providers/i18nProvider.tsx b/frontend/src/providers/i18nProvider.tsx deleted file mode 100644 index 000c0a2..0000000 --- a/frontend/src/providers/i18nProvider.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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({ - 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 ( - - {children} - - ) -} \ No newline at end of file diff --git a/frontend/src/providers/rpcClientProvider.tsx b/frontend/src/providers/rpcClientProvider.tsx deleted file mode 100644 index 5979471..0000000 --- a/frontend/src/providers/rpcClientProvider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { createContext, useMemo } from 'react' -import { useSelector } from 'react-redux' -import { RPCClient } from '../lib/rpcClient' -import type { RootState } from '../stores/store' - -type Props = { - children: React.ReactNode -} - -interface Context { - client: RPCClient -} - -export const RPCClientContext = createContext({ - 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 ( - - {children} - - ) -} \ No newline at end of file diff --git a/frontend/src/stores/store.ts b/frontend/src/stores/store.ts deleted file mode 100644 index b4bf102..0000000 --- a/frontend/src/stores/store.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { configureStore } from '@reduxjs/toolkit' -import settingsReducer from '../features/settings/settingsSlice' -import statussReducer from '../features/status/statusSlice' -import toastReducer from '../features/ui/toastSlice' - -export const store = configureStore({ - reducer: { - settings: settingsReducer, - status: statussReducer, - toast: toastReducer, - }, -}) - -export type RootState = ReturnType - -export type AppDispatch = typeof store.dispatch - diff --git a/frontend/src/views/Archive.tsx b/frontend/src/views/Archive.tsx index 7f62870..4efa503 100644 --- a/frontend/src/views/Archive.tsx +++ b/frontend/src/views/Archive.tsx @@ -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(), []) const selected$ = useMemo(() => new BehaviorSubject([]), []) @@ -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')}`) diff --git a/frontend/src/views/Home.tsx b/frontend/src/views/Home.tsx index b3e517b..baef968 100644 --- a/frontend/src/views/Home.tsx +++ b/frontend/src/views/Home.tsx @@ -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() - 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) => { - 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 ( theme.zIndex.drawer + 1 }} - open={showBackdrop} + open={!isDownloading} > - {activeDownloads?.length === 0 && - - } - { - settings.listView ? - : - - } - } - > - } - tooltipTitle={`Table view`} - onClick={() => dispatch(toggleListView())} - /> - } - tooltipTitle={i18n.t('abortAllButton')} - onClick={() => abort()} - /> - } - tooltipTitle={`New download`} - onClick={() => setOpenDialog(true)} - /> - + + + setOpenDialog(true)} + /> { setOpenDialog(false) - setShowBackdrop(false) + setIsDownloading(false) }} onDownloadStart={() => { setOpenDialog(false) - setShowBackdrop(true) + setIsDownloading(false) }} /> diff --git a/frontend/src/views/Login.tsx b/frontend/src/views/Login.tsx index 15fe89b..f068f92 100644 --- a/frontend/src/views/Login.tsx +++ b/frontend/src/views/Login.tsx @@ -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', diff --git a/frontend/src/views/Settings.tsx b/frontend/src/views/Settings.tsx index fb3f2c0..fdf074f 100644 --- a/frontend/src/views/Settings.tsx +++ b/frontend/src/views/Settings.tsx @@ -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(), []) const serverPort$ = useMemo(() => new Subject(), []) @@ -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) => { - dispatch(setLanguage(event.target.value as LanguageUnion)); + const handleLanguageChange = (event: SelectChangeEvent) => { + setLanguage(event.target.value as Language) } /** * Theme toggler handler */ - const handleThemeChange = (event: SelectChangeEvent) => { - dispatch(setTheme(event.target.value as ThemeUnion)); + const handleThemeChange = (event: SelectChangeEvent) => { + setTheme(event.target.value as Theme) } /** @@ -137,7 +145,7 @@ export default function Settings() { serverAddr$.next(e.currentTarget.value)} InputProps={{ @@ -150,9 +158,9 @@ export default function Settings() { serverPort$.next(e.currentTarget.value)} - error={isNaN(Number(settings.serverPort)) || Number(settings.serverPort) > 65535} + error={isNaN(Number(serverPort)) || Number(serverPort) > 65535} sx={{ mb: 2 }} /> @@ -162,20 +170,15 @@ export default function Settings() { {i18n.t('languageSelect')} @@ -183,7 +186,7 @@ export default function Settings() { {i18n.t('themeSelect')}