detect system theme, toast performance opt.
This commit is contained in:
@@ -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: 状态
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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])
|
||||
}])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user