import { Checkbox, Container, FormControl, FormControlLabel, Grid, InputAdornment, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, Slider, Stack, Switch, TextField, Typography, capitalize } from '@mui/material' import { useAtom } from 'jotai' import { Suspense, useCallback, useEffect, useMemo, useState } from 'react' import { Subject, debounceTime, distinctUntilChanged, map, takeWhile } from 'rxjs' import { rpcPollingTimeState } from '../atoms/rpc' import { Accent, Language, Theme, accentState, accents, appTitleState, enableCustomArgsState, fileRenamingState, autoFileExtensionState, formatSelectionState, languageState, languages, pathOverridingState, servedFromReverseProxyState, servedFromReverseProxySubDirState, serverAddressState, serverPortState, themeState } from '../atoms/settings' import CookiesTextField from '../components/CookiesTextField' import UpdateBinaryButton from '../components/UpdateBinaryButton' import { useToast } from '../hooks/toast' import { useI18n } from '../hooks/useI18n' import Translator from '../lib/i18n' import { validateDomain, validateIP } from '../utils' // NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS export default function Settings() { const [reverseProxy, setReverseProxy] = useAtom(servedFromReverseProxyState) const [baseURL, setBaseURL] = useAtom(servedFromReverseProxySubDirState) const [formatSelection, setFormatSelection] = useAtom(formatSelectionState) const [pathOverriding, setPathOverriding] = useAtom(pathOverridingState) const [fileRenaming, setFileRenaming] = useAtom(fileRenamingState) const [autoFileExtension, setAutoFileExtension] = useAtom(autoFileExtensionState) const [enableArgs, setEnableArgs] = useAtom(enableCustomArgsState) const [serverAddr, setServerAddr] = useAtom(serverAddressState) const [serverPort, setServerPort] = useAtom(serverPortState) const [pollingTime, setPollingTime] = useAtom(rpcPollingTimeState) const [language, setLanguage] = useAtom(languageState) const [appTitle, setApptitle] = useAtom(appTitleState) const [accent, setAccent] = useAtom(accentState) const [theme, setTheme] = useAtom(themeState) const [invalidIP, setInvalidIP] = useState(false) const { i18n } = useI18n() const { pushMessage } = useToast() const baseURL$ = useMemo(() => new Subject(), []) const serverAddr$ = useMemo(() => new Subject(), []) const serverPort$ = useMemo(() => new Subject(), []) const [, updateState] = useState({}) const forceUpdate = useCallback(() => updateState({}), []) useEffect(() => { const sub = baseURL$ .pipe(debounceTime(500)) .subscribe(baseURL => { setBaseURL(baseURL) pushMessage(i18n.t('restartAppMessage'), 'info') }) return () => sub.unsubscribe() }, []) useEffect(() => { const sub = serverAddr$ .pipe( debounceTime(500), distinctUntilChanged() ) .subscribe(addr => { 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) } }) return () => sub.unsubscribe() }, [serverAddr$]) useEffect(() => { const sub = serverPort$ .pipe( debounceTime(500), map(val => Number(val)), takeWhile(val => isFinite(val) && val <= 65535), ) .subscribe(port => { setServerPort(port) pushMessage(i18n.t('restartAppMessage'), 'info') }) return () => sub.unsubscribe() }, []) /** * Language toggler handler */ const handleLanguageChange = (event: SelectChangeEvent) => { setLanguage(event.target.value as Language) Translator.instance.setLanguage(event.target.value) setTimeout(() => { forceUpdate() }, 100) } /** * Theme toggler handler */ const handleThemeChange = (event: SelectChangeEvent) => { setTheme(event.target.value as Theme) } return ( {i18n.t('settingsAnchor')} serverAddr$.next(e.currentTarget.value)} InputProps={{ startAdornment: ws://, }} /> serverPort$.next(e.currentTarget.value)} error={isNaN(Number(serverPort)) || Number(serverPort) > 65535} /> setApptitle(e.currentTarget.value)} error={appTitle === ''} /> {i18n.t('rpcPollingTimeTitle')} {i18n.t('rpcPollingTimeDescription')} `${v} ms`} step={null} valueLabelDisplay="off" marks={[ { value: 100, label: '100 ms' }, { value: 250, label: '250 ms' }, { value: 500, label: '500 ms' }, { value: 750, label: '750 ms' }, { value: 1000, label: '1000 ms' }, { value: 2000, label: '2000 ms' }, ]} onChange={(_, value) => typeof value === 'number' ? setPollingTime(value) : setPollingTime(1000) } /> Reverse Proxy setReverseProxy(state => !state)} /> } label={i18n.t('servedFromReverseProxyCheckbox')} sx={{ mb: 1 }} /> { let value = e.currentTarget.value if (value.startsWith('/')) { value = value.substring(1) } if (value.endsWith('/')) { value = value.substring(0, value.length - 1) } baseURL$.next(value) }} sx={{ mb: 2 }} /> Appearance {i18n.t('languageSelect')} {i18n.t('themeSelect')} {i18n.t('accentSelect')} {i18n.t('generalDownloadSettings')} { setFormatSelection(!formatSelection) }} /> } label={i18n.t('formatSelectionEnabler')} /> {i18n.t('overridesAnchor')} { setPathOverriding(state => !state) }} /> } label={i18n.t('pathOverrideOption')} /> { if (fileRenaming) { setAutoFileExtension(false) } setFileRenaming(state => !state) }} /> } label={i18n.t('filenameOverrideOption')} /> { { setAutoFileExtension(state => !state) }} /> } label={i18n.t('autoFileExtensionOption')} /> } { setEnableArgs(state => !state) }} /> } label={i18n.t('customArgs')} /> Cookies ) }