detect system theme, toast performance opt.

This commit is contained in:
2023-08-01 11:52:50 +02:00
parent 13dd9526e2
commit 4bc5e5e1c7
8 changed files with 71 additions and 49 deletions

View File

@@ -33,6 +33,7 @@ languages:
archiveTitle: Archive archiveTitle: Archive
clipboardAction: Copied URL to clipboard clipboardAction: Copied URL to clipboard
playlistCheckbox: Download playlist (it will take time, after submitting you may close this window) playlistCheckbox: Download playlist (it will take time, after submitting you may close this window)
restartAppMessage: Needs a page reload to take effect
italian: italian:
urlInput: URL di YouTube o di qualsiasi altro servizio supportato urlInput: URL di YouTube o di qualsiasi altro servizio supportato
statusTitle: Stato statusTitle: Stato
@@ -65,6 +66,7 @@ languages:
archiveTitle: Archivio archiveTitle: Archivio
clipboardAction: URL copiato negli appunti clipboardAction: URL copiato negli appunti
playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro) playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro)
restartAppMessage: La finestra deve essere ricaricata perché abbia effetto
chinese: chinese:
urlInput: YouTube 或其他受支持服务的视频网址 urlInput: YouTube 或其他受支持服务的视频网址
statusTitle: 状态 statusTitle: 状态

View File

@@ -1,4 +1,5 @@
import { atom, selector } from 'recoil' import { atom, selector } from 'recoil'
import { prefersDarkMode } from '../utils'
export type Language = export type Language =
| 'english' | 'english'
@@ -25,13 +26,14 @@ export const languages = [
'polish', 'polish',
] as const ] as const
export type Theme = 'light' | 'dark' export type Theme = 'light' | 'dark' | 'system'
export type ThemeNarrowed = 'light' | 'dark'
export interface SettingsState { export interface SettingsState {
serverAddr: string serverAddr: string
serverPort: number serverPort: number
language: Language language: Language
theme: Theme theme: ThemeNarrowed
cliArgs: string cliArgs: string
formatSelection: boolean formatSelection: boolean
fileRenaming: boolean fileRenaming: boolean
@@ -51,7 +53,7 @@ export const languageState = atom<Language>({
export const themeState = atom<Theme>({ export const themeState = atom<Theme>({
key: 'themeStateState', key: 'themeStateState',
default: localStorage.getItem('theme') as Theme || 'light', default: localStorage.getItem('theme') as Theme || 'system',
effects: [ effects: [
({ onSet }) => ({ onSet }) =>
onSet(l => localStorage.setItem('theme', l.toString())) onSet(l => localStorage.setItem('theme', l.toString()))
@@ -158,13 +160,24 @@ export const rpcHTTPEndpoint = selector({
} }
}) })
export const themeSelector = selector<ThemeNarrowed>({
key: 'themeSelector',
get: ({ get }) => {
const theme = get(themeState)
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
return 'dark'
}
return 'light'
}
})
export const settingsState = selector<SettingsState>({ export const settingsState = selector<SettingsState>({
key: 'settingsState', key: 'settingsState',
get: ({ get }) => ({ get: ({ get }) => ({
serverAddr: get(serverAddressState), serverAddr: get(serverAddressState),
serverPort: get(serverPortState), serverPort: get(serverPortState),
language: get(languageState), language: get(languageState),
theme: get(themeState), theme: get(themeSelector),
cliArgs: get(latestCliArgumentsState), cliArgs: get(latestCliArgumentsState),
formatSelection: get(formatSelectionState), formatSelection: get(formatSelectionState),
fileRenaming: get(fileRenamingState), fileRenaming: get(fileRenamingState),

View File

@@ -28,7 +28,7 @@ const DownloadsCardView: React.FC = () => {
thumbnail={download.info.thumbnail} thumbnail={download.info.thumbnail}
percentage={download.progress.percentage} percentage={download.progress.percentage}
onStop={() => abort(download.id)} onStop={() => abort(download.id)}
onCopy={() => pushMessage(i18n.t('clipboardAction'))} onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')}
resolution={download.info.resolution ?? ''} resolution={download.info.resolution ?? ''}
speed={download.progress.speed} speed={download.progress.speed}
size={download.info.filesize_approx ?? 0} size={download.info.filesize_approx ?? 0}

View File

@@ -1,25 +1,32 @@
import { Brightness4, Brightness5 } from '@mui/icons-material' import Brightness4 from '@mui/icons-material/Brightness4'
import Brightness5 from '@mui/icons-material/Brightness5'
import BrightnessAuto from '@mui/icons-material/BrightnessAuto'
import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material' import { ListItemButton, ListItemIcon, ListItemText } from '@mui/material'
import { useRecoilState } from 'recoil' import { useRecoilState } from 'recoil'
import { themeState } from '../atoms/settings' import { Theme, themeState } from '../atoms/settings'
export default function ThemeToggler() { const ThemeToggler: React.FC = () => {
const [theme, setTheme] = useRecoilState(themeState) const [theme, setTheme] = useRecoilState(themeState)
const actions: Record<Theme, React.ReactNode> = {
system: <BrightnessAuto />,
light: <Brightness4 />,
dark: <Brightness5 />,
}
const themes: Theme[] = ['system', 'light', 'dark']
const currentTheme = themes.indexOf(theme)
return ( return (
<ListItemButton onClick={() => { <ListItemButton onClick={() => {
theme === 'light' setTheme(themes[(currentTheme + 1) % themes.length])
? setTheme('dark')
: setTheme('light')
}}> }}>
<ListItemIcon> <ListItemIcon>
{ {actions[theme]}
theme === 'light'
? <Brightness4 />
: <Brightness5 />
}
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Toggle theme" /> <ListItemText primary="Toggle theme" />
</ListItemButton> </ListItemButton>
) )
} }
export default ThemeToggler

View File

@@ -3,17 +3,17 @@ import { useRecoilState } from 'recoil'
import { toastListState } from '../atoms/toast' import { toastListState } from '../atoms/toast'
export const useToast = () => { export const useToast = () => {
const [toasts, setToasts] = useRecoilState(toastListState) const [, setToasts] = useRecoilState(toastListState)
return { return {
pushMessage: (message: string, severity?: AlertColor) => { pushMessage: (message: string, severity?: AlertColor) => {
setToasts([{ setToasts(state => [...state, {
open: true, open: true,
message: message, message: message,
severity: severity, severity: severity,
autoClose: true, autoClose: true,
createdAt: Date.now() createdAt: Date.now()
}, ...toasts]) }])
} }
} }
} }

View File

@@ -8,13 +8,20 @@ const Toaster: React.FC = () => {
useEffect(() => { useEffect(() => {
if (toasts.length > 0) { if (toasts.length > 0) {
const interval = setInterval(() => { const closer = setInterval(() => {
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500)) setToasts(t => t.map(t => ({ ...t, open: false })))
}, 1500) }, 1500)
return () => clearInterval(interval) const cleaner = setInterval(() => {
setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500))
}, 1750)
return () => {
clearInterval(closer)
clearInterval(cleaner)
} }
}, [setToasts, toasts]) }
}, [setToasts, toasts.length])
return ( return (
<> <>
@@ -22,6 +29,7 @@ const Toaster: React.FC = () => {
<Snackbar <Snackbar
key={index} key={index}
open={toast.open} open={toast.open}
sx={index > 0 ? { marginBottom: index * 6.5 } : {}}
> >
<Alert variant="filled" severity={toast.severity}> <Alert variant="filled" severity={toast.severity}>
{toast.message} {toast.message}

View File

@@ -47,24 +47,6 @@ export function ellipsis(str: string, lim: number): string {
return '' return ''
} }
/**
* Parse the downlaod speed sent by server and converts it to KiB/s
* @param str the downlaod speed, ex. format: 5 MiB/s => 5000 | 50 KiB/s => 50
* @returns download speed in KiB/s
*/
export function detectSpeed(str: string): number {
let effective = str.match(/[\d,]+(\.\d+)?/)![0]
const unit = str.replace(effective, '')
switch (unit) {
case 'MiB/s':
return Number(effective) * 1000
case 'KiB/s':
return Number(effective)
default:
return 0
}
}
export function toFormatArgs(codes: string[]): string { export function toFormatArgs(codes: string[]): string {
if (codes.length > 1) { if (codes.length > 1) {
return codes.reduce((v, a) => ` -f ${v}+${a}`) return codes.reduce((v, a) => ` -f ${v}+${a}`)
@@ -75,14 +57,17 @@ export function toFormatArgs(codes: string[]): string {
return '' return ''
} }
export function formatGiB(bytes: number) { export const formatGiB = (bytes: number) =>
return `${(bytes / 1_000_000_000).toFixed(0)}GiB` `${(bytes / 1_000_000_000).toFixed(0)}GiB`
}
export const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB` export const roundMiB = (bytes: number) =>
export const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s` `${(bytes / 1_000_000).toFixed(2)} MiB`
export const datetimeCompareFunc = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime() export const formatSpeedMiB = (val: number) =>
`${roundMiB(val)}/s`
export const datetimeCompareFunc = (a: string, b: string) =>
new Date(a).getTime() - new Date(b).getTime()
export function isRPCResponse(object: any): object is RPCResponse<any> { export function isRPCResponse(object: any): object is RPCResponse<any> {
return 'result' in object && 'id' in object return 'result' in object && 'id' in object
@@ -102,3 +87,6 @@ export function mapProcessStatus(status: number) {
return 'Pending' return 'Pending'
} }
} }
export const prefersDarkMode = () =>
window.matchMedia('(prefers-color-scheme: dark)').matches

View File

@@ -80,9 +80,11 @@ export default function Settings() {
if (validateIP(addr)) { if (validateIP(addr)) {
setInvalidIP(false) setInvalidIP(false)
setServerAddr(addr) setServerAddr(addr)
pushMessage(i18n.t('restartAppMessage'), 'info')
} else if (validateDomain(addr)) { } else if (validateDomain(addr)) {
setInvalidIP(false) setInvalidIP(false)
setServerAddr(addr) setServerAddr(addr)
pushMessage(i18n.t('restartAppMessage'), 'info')
} else { } else {
setInvalidIP(true) setInvalidIP(true)
} }
@@ -99,6 +101,7 @@ export default function Settings() {
) )
.subscribe(port => { .subscribe(port => {
setServerPort(port) setServerPort(port)
pushMessage(i18n.t('restartAppMessage'), 'info')
}) })
return () => sub.unsubscribe() return () => sub.unsubscribe()
}, []) }, [])
@@ -192,6 +195,7 @@ export default function Settings() {
> >
<MenuItem value="light">Light</MenuItem> <MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem> <MenuItem value="dark">Dark</MenuItem>
<MenuItem value="system">System</MenuItem>
</Select> </Select>
</FormControl> </FormControl>
</Grid> </Grid>