migrated from redux to recoil

This commit is contained in:
2023-07-31 12:27:36 +02:00
parent 8327d1e94c
commit b5731759b0
36 changed files with 810 additions and 741 deletions

View File

@@ -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 (
<Card>
<CardActionArea onClick={() => window.open(url)}>
{thumbnail ?
<CardMedia
component="img"
height={180}
image={thumbnail}
/> :
<Skeleton variant="rectangular" height={180} />
}
<CardContent>
<Typography gutterBottom variant="body2" component="div">
{ellipsis(title, 72)}
</Typography>
</CardContent>
</CardActionArea>
</Card>
)
}

View File

@@ -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<DLMetadata>()
@@ -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<HTMLInputElement>(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)
}
/>
</Grid>
}
@@ -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)
}
/>
</Grid>
}
@@ -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')
}
</Button>
</Grid>
<Grid item>

View File

@@ -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<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(() => {
if (active) {
setIsDownloading(true)
}
}, [active?.length])
if (listView) {
return (
<DownloadsListView
downloads={active ?? []}
onStop={abort}
/>
)
}
return (
<DownloadsCardView
downloads={active ?? []}
onStop={abort}
/>
)
}
export default Downloads

View File

@@ -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 (

View File

@@ -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<Props> = ({ onOpen }) => {
const [, setListView] = useRecoilState(listViewState)
const { i18n } = useI18n()
const { client } = useRPC()
const abort = () => client.killAll()
return (
<SpeedDial
ariaLabel="Home speed dial"
sx={{ position: 'absolute', bottom: 32, right: 32 }}
icon={<SpeedDialIcon />}
>
<SpeedDialAction
icon={<FormatListBulleted />}
tooltipTitle={`Table view`}
onClick={() => setListView(state => !state)}
/>
<SpeedDialAction
icon={<DeleteForeverIcon />}
tooltipTitle={i18n.t('abortAllButton')}
onClick={abort}
/>
<SpeedDialAction
icon={<AddCircleIcon />}
tooltipTitle={`New download`}
onClick={onOpen}
/>
</SpeedDial>
)
}
export default HomeSpeedDial

View File

@@ -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<HTMLBaseElement> { }
const SocketSubscriber: React.FC<Props> = ({ 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

View File

@@ -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 (
<FlexContainer>

View File

@@ -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 (
<ListItemButton onClick={() => {
settings.theme === 'light'
? dispatch(setTheme('dark'))
: dispatch(setTheme('light'))
theme === 'light'
? setTheme('dark')
: setTheme('light')
}}>
<ListItemIcon>
{
settings.theme === 'light'
theme === 'light'
? <Brightness4 />
: <Brightness5 />
}