import { Button, Checkbox, Container, FormControl, FormControlLabel, Grid, InputAdornment, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, Slider, Stack, Switch, TextField, Typography, capitalize } from '@mui/material' import { Suspense, useEffect, useMemo, useState } from 'react' import { useRecoilState } from 'recoil' import { Subject, debounceTime, distinctUntilChanged, map, takeWhile } from 'rxjs' import { rpcPollingTimeState } from '../atoms/rpc' import { Language, Theme, appTitleState, enableCustomArgsState, fileRenamingState, formatSelectionState, languageState, languages, pathOverridingState, servedFromReverseProxyState, servedFromReverseProxySubDirState, serverAddressState, serverPortState, themeState } from '../atoms/settings' import CookiesTextField from '../components/CookiesTextField' import { useToast } from '../hooks/toast' import { useI18n } from '../hooks/useI18n' import { useRPC } from '../hooks/useRPC' import { validateDomain, validateIP } from '../utils' // NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS export default function Settings() { const [reverseProxy, setReverseProxy] = useRecoilState(servedFromReverseProxyState) const [baseURL, setBaseURL] = useRecoilState(servedFromReverseProxySubDirState) const [formatSelection, setFormatSelection] = useRecoilState(formatSelectionState) const [pathOverriding, setPathOverriding] = useRecoilState(pathOverridingState) const [fileRenaming, setFileRenaming] = useRecoilState(fileRenamingState) const [enableArgs, setEnableArgs] = useRecoilState(enableCustomArgsState) const [serverAddr, setServerAddr] = useRecoilState(serverAddressState) const [serverPort, setServerPort] = useRecoilState(serverPortState) const [pollingTime, setPollingTime] = useRecoilState(rpcPollingTimeState) const [language, setLanguage] = useRecoilState(languageState) const [appTitle, setApptitle] = useRecoilState(appTitleState) const [theme, setTheme] = useRecoilState(themeState) const [invalidIP, setInvalidIP] = useState(false) const { i18n } = useI18n() const { client } = useRPC() const { pushMessage } = useToast() const baseURL$ = useMemo(() => new Subject(), []) const serverAddr$ = useMemo(() => new Subject(), []) const serverPort$ = useMemo(() => new Subject(), []) 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) } /** * Theme toggler handler */ const handleThemeChange = (event: SelectChangeEvent) => { setTheme(event.target.value as Theme) } /** * Updates yt-dlp binary via RPC */ const updateBinary = () => { client.updateExecutable().then(() => pushMessage(i18n.t('toastUpdated'), 'success')) } 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')} General download settings { setFormatSelection(!formatSelection) }} /> } label={i18n.t('formatSelectionEnabler')} /> {i18n.t('overridesAnchor')} { setPathOverriding(state => !state) }} /> } label={i18n.t('pathOverrideOption')} /> { setFileRenaming(state => !state) }} /> } label={i18n.t('filenameOverrideOption')} /> { setEnableArgs(state => !state) }} /> } label={i18n.t('customArgs')} /> Cookies ) }