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
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: 状态

View File

@@ -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<Language>({
export const themeState = atom<Theme>({
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<ThemeNarrowed>({
key: 'themeSelector',
get: ({ get }) => {
const theme = get(themeState)
if ((theme === 'system' && prefersDarkMode()) || theme === 'dark') {
return 'dark'
}
return 'light'
}
})
export const settingsState = selector<SettingsState>({
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),

View File

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

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 { 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<Theme, React.ReactNode> = {
system: <BrightnessAuto />,
light: <Brightness4 />,
dark: <Brightness5 />,
}
const themes: Theme[] = ['system', 'light', 'dark']
const currentTheme = themes.indexOf(theme)
return (
<ListItemButton onClick={() => {
theme === 'light'
? setTheme('dark')
: setTheme('light')
setTheme(themes[(currentTheme + 1) % themes.length])
}}>
<ListItemIcon>
{
theme === 'light'
? <Brightness4 />
: <Brightness5 />
}
{actions[theme]}
</ListItemIcon>
<ListItemText primary="Toggle theme" />
</ListItemButton>
)
}
}
export default ThemeToggler

View File

@@ -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])
}])
}
}
}

View File

@@ -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 = () => {
<Snackbar
key={index}
open={toast.open}
sx={index > 0 ? { marginBottom: index * 6.5 } : {}}
>
<Alert variant="filled" severity={toast.severity}>
{toast.message}

View File

@@ -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<any> {
return 'result' in object && 'id' in object
@@ -101,4 +86,7 @@ export function mapProcessStatus(status: number) {
default:
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)) {
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() {
>
<MenuItem value="light">Light</MenuItem>
<MenuItem value="dark">Dark</MenuItem>
<MenuItem value="system">System</MenuItem>
</Select>
</FormControl>
</Grid>