import { FileUpload } from "@mui/icons-material"; import { Backdrop, Button, ButtonGroup, CircularProgress, Container, FormControl, Grid, IconButton, InputAdornment, InputLabel, MenuItem, Paper, Select, Snackbar, styled, TextField, Typography } from "@mui/material"; import { Buffer } from 'buffer'; import { Fragment, useEffect, useMemo, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { StackableResult } from "./components/StackableResult"; import { CliArguments } from "./features/core/argsParser"; import I18nBuilder from "./features/core/intl"; import { RPCClient } from "./features/core/rpcClient"; import { connected, setFreeSpace } from "./features/status/statusSlice"; import { RootState } from "./stores/store"; import { IDLMetadata, RPCResult } from "./types"; import { isValidURL, toFormatArgs } from "./utils"; type Props = { socket: WebSocket } export default function Home({ socket }: Props) { // redux state const settings = useSelector((state: RootState) => state.settings) const status = useSelector((state: RootState) => state.status) const dispatch = useDispatch() // ephemeral state const [activeDownloads, setActiveDownloads] = useState(new Array()); const [downloadFormats, setDownloadFormats] = useState(); const [pickedVideoFormat, setPickedVideoFormat] = useState(''); const [pickedAudioFormat, setPickedAudioFormat] = useState(''); const [pickedBestFormat, setPickedBestFormat] = useState(''); const [customArgs, setCustomArgs] = useState(''); const [downloadPath, setDownloadPath] = useState(0); const [availableDownloadPaths, setAvailableDownloadPaths] = useState([]); const [fileNameOverride, setFilenameOverride] = useState(''); const [url, setUrl] = useState(''); const [workingUrl, setWorkingUrl] = useState(''); const [showBackdrop, setShowBackdrop] = useState(false); const [showToast, setShowToast] = useState(true); // memos const i18n = useMemo(() => new I18nBuilder(settings.language), [settings.language]) const client = useMemo(() => new RPCClient(socket), [settings.serverAddr, settings.serverPort]) const cliArgs = useMemo(() => new CliArguments().fromString(settings.cliArgs), [settings.cliArgs]) /* -------------------- Effects -------------------- */ /* WebSocket connect event handler*/ useEffect(() => { socket.onopen = () => { dispatch(connected()) setCustomArgs(localStorage.getItem('last-input-args') ?? '') } }, []) useEffect(() => { const interval = setInterval(() => client.running(), 1000) return () => clearInterval(interval) }, []) useEffect(() => { client.freeSpace() .then(bytes => dispatch(setFreeSpace(bytes.result))) }, []) useEffect(() => { socket.onmessage = (event) => { const res = client.decode(event.data) switch (typeof res.result) { case 'object': setActiveDownloads( res.result .filter((r: RPCResult) => !!r.info.url) .sort((a: RPCResult, b: RPCResult) => a.info.title.localeCompare(b.info.title)) ) break default: break } } }, []) useEffect(() => { if (activeDownloads.length > 0 && showBackdrop) { setShowBackdrop(false) } }, [activeDownloads, showBackdrop]) useEffect(() => { client.directoryTree() .then(data => { setAvailableDownloadPaths(data.result) }) }, []) /* -------------------- component functions -------------------- */ /** * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket */ const sendUrl = (immediate?: string) => { const codes = new Array(); if (pickedVideoFormat !== '') codes.push(pickedVideoFormat); if (pickedAudioFormat !== '') codes.push(pickedAudioFormat); if (pickedBestFormat !== '') codes.push(pickedBestFormat); client.download( immediate || url || workingUrl, `${cliArgs.toString()} ${toFormatArgs(codes)} ${customArgs}`, availableDownloadPaths[downloadPath] ?? '', fileNameOverride ) setUrl('') setWorkingUrl('') setFilenameOverride('') setTimeout(() => { resetInput() setShowBackdrop(true) setDownloadFormats(undefined) }, 250); } /** * Retrive url from input and display the formats selection view */ const sendUrlFormatSelection = () => { setWorkingUrl(url) setUrl('') setPickedAudioFormat('') setPickedVideoFormat('') setPickedBestFormat('') setShowBackdrop(true) client.formats(url) ?.then(formats => { setDownloadFormats(formats.result) setShowBackdrop(false) resetInput() }) } /** * Update the url state whenever the input value changes * @param e Input change event */ const handleUrlChange = (e: React.ChangeEvent) => { setUrl(e.target.value) } /** * Update the filename override state whenever the input value changes * @param e Input change event */ const handleFilenameOverrideChange = (e: React.ChangeEvent) => { setFilenameOverride(e.target.value) } /** * Update the custom args state whenever the input value changes * @param e Input change event */ const handleCustomArgsChange = (e: React.ChangeEvent) => { setCustomArgs(e.target.value) localStorage.setItem("last-input-args", e.target.value) } /** * Abort a specific download if id's provided, other wise abort all running ones. * @param id The download id / pid * @returns void */ const abort = (id?: string) => { if (id) { client.kill(id) return } client.killAll() } const parseUrlListFile = (event: any) => { const urlList = event.target.files const reader = new FileReader() reader.addEventListener('load', $event => { const base64 = $event.target?.result!.toString().split(',')[1] Buffer.from(base64!, 'base64') .toString() .trimEnd() .split('\n') .filter(_url => isValidURL(_url)) .forEach(_url => sendUrl(_url)) }) reader.readAsDataURL(urlList[0]) } const resetInput = () => { const input = document.getElementById('urlInput') as HTMLInputElement; input.value = ''; const filename = document.getElementById('customFilenameInput') as HTMLInputElement; if (filename) { filename.value = ''; } } /* -------------------- styled components -------------------- */ const Input = styled('input')({ display: 'none', }); return ( theme.zIndex.drawer + 1 }} open={showBackdrop} > ), }} /> { settings.enableCustomArgs ? : null } { settings.fileRenaming ? : null } { settings.pathOverriding ? {i18n.t('customPath')} : null } {/* Format Selection grid */} { downloadFormats ? {downloadFormats.title} {/* */} {/* video only */} Best quality {/* video only */} {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? Video data {downloadFormats.formats[1].acodec} : null } {downloadFormats.formats .filter(format => format.acodec === 'none' && format.vcodec !== 'none') .map((format, idx) => ( )) } {downloadFormats.formats.filter(format => format.acodec === 'none' && format.vcodec !== 'none').length ? Audio data : null } {downloadFormats.formats .filter(format => format.acodec !== 'none' && format.vcodec === 'none') .map((format, idx) => ( )) } : null } { activeDownloads.map(download => ( abort(download.id)} resolution={download.info.resolution ?? ''} speed={download.progress.speed} size={download.info.filesize_approx ?? 0} /> )) } setShowToast(false)} /> ); }