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