import { Button, Container, FormControl, FormControlLabel, FormGroup, Grid, InputAdornment, InputLabel, MenuItem, Paper, Select, SelectChangeEvent, Stack, Switch, TextField, Typography, capitalize } from '@mui/material' import { useEffect, useMemo, useState } from 'react' import { useRecoilState } from 'recoil' import { Subject, debounceTime, distinctUntilChanged, map, takeWhile } from 'rxjs' import { Language, Theme, enableCustomArgsState, fileRenamingState, formatSelectionState, languageState, languages, latestCliArgumentsState, pathOverridingState, serverAddressState, serverPortState, themeState } from '../atoms/settings' import { useToast } from '../hooks/toast' import { useI18n } from '../hooks/useI18n' import { useRPC } from '../hooks/useRPC' import { CliArguments } from '../lib/argsParser' import { validateDomain, validateIP } from '../utils' // NEED ABSOLUTELY TO BE SPLIT IN MULTIPLE COMPONENTS export default function Settings() { 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 [language, setLanguage] = useRecoilState(languageState) const [cliArgs, setCliArgs] = useRecoilState(latestCliArgumentsState) const [theme, setTheme] = useRecoilState(themeState) const [invalidIP, setInvalidIP] = useState(false) const { i18n } = useI18n() const { client } = useRPC() const { pushMessage } = useToast() const argsBuilder = useMemo(() => new CliArguments().fromString(cliArgs), []) const serverAddr$ = useMemo(() => new Subject(), []) const serverPort$ = useMemo(() => new Subject(), []) useEffect(() => { const sub = serverAddr$ .pipe( debounceTime(500), distinctUntilChanged() ) .subscribe(addr => { if (validateIP(addr)) { setInvalidIP(false) setServerAddr(addr) } else if (validateDomain(addr)) { setInvalidIP(false) setServerAddr(addr) } 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) }) 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'))) } return ( {i18n.t('settingsAnchor')} serverAddr$.next(e.currentTarget.value)} InputProps={{ startAdornment: ws://, }} sx={{ mb: 2 }} /> serverPort$.next(e.currentTarget.value)} error={isNaN(Number(serverPort)) || Number(serverPort) > 65535} sx={{ mb: 2 }} /> {i18n.t('languageSelect')} {i18n.t('themeSelect')} setCliArgs(argsBuilder.toggleNoMTime().toString())} /> } label={i18n.t('noMTimeCheckbox')} sx={{ mt: 3 }} /> setCliArgs(argsBuilder.toggleExtractAudio().toString())} disabled={formatSelection} /> } label={i18n.t('extractAudioCheckbox')} /> { setCliArgs(argsBuilder.disableExtractAudio().toString()) 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')} /> ) }