From 4bc5e5e1c76b3581968411f06895f7b3154424da Mon Sep 17 00:00:00 2001 From: marcobaobao Date: Tue, 1 Aug 2023 11:52:50 +0200 Subject: [PATCH] detect system theme, toast performance opt. --- frontend/src/assets/i18n.yaml | 2 + frontend/src/atoms/settings.ts | 21 ++++++++-- frontend/src/components/DownloadsCardView.tsx | 2 +- frontend/src/components/ThemeToggler.tsx | 31 +++++++++------ frontend/src/hooks/toast.ts | 6 +-- frontend/src/providers/ToasterProvider.tsx | 16 ++++++-- frontend/src/utils.ts | 38 +++++++------------ frontend/src/views/Settings.tsx | 4 ++ 8 files changed, 71 insertions(+), 49 deletions(-) diff --git a/frontend/src/assets/i18n.yaml b/frontend/src/assets/i18n.yaml index 11b3a49..7699263 100644 --- a/frontend/src/assets/i18n.yaml +++ b/frontend/src/assets/i18n.yaml @@ -33,6 +33,7 @@ languages: archiveTitle: Archive clipboardAction: Copied URL to clipboard playlistCheckbox: Download playlist (it will take time, after submitting you may close this window) + restartAppMessage: Needs a page reload to take effect italian: urlInput: URL di YouTube o di qualsiasi altro servizio supportato statusTitle: Stato @@ -65,6 +66,7 @@ languages: archiveTitle: Archivio clipboardAction: URL copiato negli appunti playlistCheckbox: Download playlist (richiederà tempo, puoi chiudere la finestra dopo l'inoltro) + restartAppMessage: La finestra deve essere ricaricata perché abbia effetto chinese: urlInput: YouTube 或其他受支持服务的视频网址 statusTitle: 状态 diff --git a/frontend/src/atoms/settings.ts b/frontend/src/atoms/settings.ts index c46c8d1..f63a6cb 100644 --- a/frontend/src/atoms/settings.ts +++ b/frontend/src/atoms/settings.ts @@ -1,4 +1,5 @@ import { atom, selector } from 'recoil' +import { prefersDarkMode } from '../utils' export type Language = | 'english' @@ -25,13 +26,14 @@ export const languages = [ 'polish', ] as const -export type Theme = 'light' | 'dark' +export type Theme = 'light' | 'dark' | 'system' +export type ThemeNarrowed = 'light' | 'dark' export interface SettingsState { serverAddr: string serverPort: number language: Language - theme: Theme + theme: ThemeNarrowed cliArgs: string formatSelection: boolean fileRenaming: boolean @@ -51,7 +53,7 @@ export const languageState = atom({ export const themeState = atom({ key: 'themeStateState', - default: localStorage.getItem('theme') as Theme || 'light', + default: localStorage.getItem('theme') as Theme || 'system', effects: [ ({ onSet }) => onSet(l => localStorage.setItem('theme', l.toString())) @@ -158,13 +160,24 @@ export const rpcHTTPEndpoint = selector({ } }) +export const themeSelector = selector({ + key: 'themeSelector', + get: ({ get }) => { + const theme = get(themeState) + if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') { + return 'dark' + } + return 'light' + } +}) + export const settingsState = selector({ key: 'settingsState', get: ({ get }) => ({ serverAddr: get(serverAddressState), serverPort: get(serverPortState), language: get(languageState), - theme: get(themeState), + theme: get(themeSelector), cliArgs: get(latestCliArgumentsState), formatSelection: get(formatSelectionState), fileRenaming: get(fileRenamingState), diff --git a/frontend/src/components/DownloadsCardView.tsx b/frontend/src/components/DownloadsCardView.tsx index 3befbc2..f843711 100644 --- a/frontend/src/components/DownloadsCardView.tsx +++ b/frontend/src/components/DownloadsCardView.tsx @@ -28,7 +28,7 @@ const DownloadsCardView: React.FC = () => { thumbnail={download.info.thumbnail} percentage={download.progress.percentage} onStop={() => abort(download.id)} - onCopy={() => pushMessage(i18n.t('clipboardAction'))} + onCopy={() => pushMessage(i18n.t('clipboardAction'), 'info')} resolution={download.info.resolution ?? ''} speed={download.progress.speed} size={download.info.filesize_approx ?? 0} diff --git a/frontend/src/components/ThemeToggler.tsx b/frontend/src/components/ThemeToggler.tsx index c7a3932..919c24b 100644 --- a/frontend/src/components/ThemeToggler.tsx +++ b/frontend/src/components/ThemeToggler.tsx @@ -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 { 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 actions: Record = { + system: , + light: , + dark: , + } + + const themes: Theme[] = ['system', 'light', 'dark'] + const currentTheme = themes.indexOf(theme) + return ( { - theme === 'light' - ? setTheme('dark') - : setTheme('light') + setTheme(themes[(currentTheme + 1) % themes.length]) }}> - { - theme === 'light' - ? - : - } + {actions[theme]} ) -} \ No newline at end of file +} + +export default ThemeToggler \ No newline at end of file diff --git a/frontend/src/hooks/toast.ts b/frontend/src/hooks/toast.ts index 3e589ee..d42b601 100644 --- a/frontend/src/hooks/toast.ts +++ b/frontend/src/hooks/toast.ts @@ -3,17 +3,17 @@ import { useRecoilState } from 'recoil' import { toastListState } from '../atoms/toast' export const useToast = () => { - const [toasts, setToasts] = useRecoilState(toastListState) + const [, setToasts] = useRecoilState(toastListState) return { pushMessage: (message: string, severity?: AlertColor) => { - setToasts([{ + setToasts(state => [...state, { open: true, message: message, severity: severity, autoClose: true, createdAt: Date.now() - }, ...toasts]) + }]) } } } \ No newline at end of file diff --git a/frontend/src/providers/ToasterProvider.tsx b/frontend/src/providers/ToasterProvider.tsx index 0f4e158..8f3ba91 100644 --- a/frontend/src/providers/ToasterProvider.tsx +++ b/frontend/src/providers/ToasterProvider.tsx @@ -8,13 +8,20 @@ const Toaster: React.FC = () => { useEffect(() => { if (toasts.length > 0) { - const interval = setInterval(() => { - setToasts(t => t.filter((x) => (Date.now() - x.createdAt) < 1500)) + const closer = setInterval(() => { + setToasts(t => t.map(t => ({ ...t, open: false }))) }, 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 ( <> @@ -22,6 +29,7 @@ const Toaster: React.FC = () => { 0 ? { marginBottom: index * 6.5 } : {}} > {toast.message} diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index c798946..fc93bbd 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -47,24 +47,6 @@ export function ellipsis(str: string, lim: number): string { 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 { if (codes.length > 1) { return codes.reduce((v, a) => ` -f ${v}+${a}`) @@ -75,14 +57,17 @@ export function toFormatArgs(codes: string[]): string { return '' } -export function formatGiB(bytes: number) { - return `${(bytes / 1_000_000_000).toFixed(0)}GiB` -} +export const formatGiB = (bytes: number) => + `${(bytes / 1_000_000_000).toFixed(0)}GiB` -export const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB` -export const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s` +export const roundMiB = (bytes: number) => + `${(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 { return 'result' in object && 'id' in object @@ -101,4 +86,7 @@ export function mapProcessStatus(status: number) { default: return 'Pending' } -} \ No newline at end of file +} + +export const prefersDarkMode = () => + window.matchMedia('(prefers-color-scheme: dark)').matches \ No newline at end of file diff --git a/frontend/src/views/Settings.tsx b/frontend/src/views/Settings.tsx index fdf074f..b04533e 100644 --- a/frontend/src/views/Settings.tsx +++ b/frontend/src/views/Settings.tsx @@ -80,9 +80,11 @@ export default function Settings() { if (validateIP(addr)) { setInvalidIP(false) setServerAddr(addr) + pushMessage(i18n.t('restartAppMessage'), 'info') } else if (validateDomain(addr)) { setInvalidIP(false) setServerAddr(addr) + pushMessage(i18n.t('restartAppMessage'), 'info') } else { setInvalidIP(true) } @@ -99,6 +101,7 @@ export default function Settings() { ) .subscribe(port => { setServerPort(port) + pushMessage(i18n.t('restartAppMessage'), 'info') }) return () => sub.unsubscribe() }, []) @@ -192,6 +195,7 @@ export default function Settings() { > Light Dark + System