diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 665c4d9..e90dbdb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,9 +11,10 @@ import { ButtonGroup, } from "react-bootstrap"; import { X } from "react-bootstrap-icons"; -import { updateInStateMap, validateDomain, validateIP } from "./utils"; +import { buildMessage, updateInStateMap, validateDomain, validateIP } from "./utils"; import { IDLInfo, IDLInfoBase, IMessage } from "./interfaces"; import { MessageToast } from "./components/MessageToast"; +import { StackableResult } from "./components/StackableResult"; import './App.css'; const socket = io(`http://${localStorage.getItem('server-addr') || 'localhost'}:3022`) @@ -33,6 +34,9 @@ export function App() { const [darkMode, setDarkMode] = useState(localStorage.getItem('theme') === 'dark'); const [extractAudio, setExtractAudio] = useState(localStorage.getItem('-x') === 'true'); + /* -------------------- Effects -------------------- */ + + /* WebSocket connect event handler*/ useEffect(() => { socket.on('connect', () => { setShowToast(true) @@ -43,49 +47,37 @@ export function App() { } }, []) + /* Ask server for pending jobs / background jobs */ useEffect(() => { socket.on('pending-jobs', () => { socket.emit('retrieve-jobs') }) }, []) - useEffect(() => { - darkMode ? - document.body.classList.add('dark') : - document.body.classList.remove('dark') - }, [darkMode]) - + /* Handle download information sent by server */ useEffect(() => { socket.on('info', (data: IDLInfo) => { - updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap) + updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap); }) }, []) + /* Handle per-download progress */ useEffect(() => { socket.on('progress', (data: IMessage) => { if (data.status === 'Done!' || data.status === 'Aborted') { - setHalt(false) - updateInStateMap( - data.pid, - 'Done!', - messageMap, - setMessageMap - ) - updateInStateMap(data.pid, 0, progressMap, setProgressMap) - return + setHalt(false); + updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap); + updateInStateMap(data.pid, 0, progressMap, setProgressMap); + return; } - updateInStateMap( - data.pid, - `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`, - messageMap, - setMessageMap - ) + updateInStateMap(data.pid, buildMessage(data), messageMap, setMessageMap); if (data.progress) { updateInStateMap(data.pid, Math.ceil(Number(data.progress.replace('%', ''))), progressMap, setProgressMap) } }) }, []) + /* Handle yt-dlp update success */ useEffect(() => { socket.on('updated', () => { setUpdatedBin(true) @@ -93,6 +85,18 @@ export function App() { }) }, []) + /* Theme changer */ + useEffect(() => { + darkMode ? + document.body.classList.add('dark') : + document.body.classList.remove('dark'); + }, [darkMode]) + + /* -------------------- component functions -------------------- */ + + /** + * Retrive url from input, cli-arguments from checkboxes and emits via WebSocket + */ const sendUrl = () => { setHalt(true) socket.emit('send-url', { @@ -106,10 +110,19 @@ export function App() { input.value = ''; } + /** + * Update the url state whenever the input value changes + * @param {React.ChangeEvent} e Input change event + */ const handleUrlChange = (e: React.ChangeEvent) => { setUrl(e.target.value) } + /** + * Update the server ip address state and localstorage whenever the input value changes. + * Validate the ip-addr then set. + * @param {React.ChangeEvent} e Input change event + */ const handleAddrChange = (e: React.ChangeEvent) => { const input = e.target.value; if (validateIP(input)) { @@ -123,6 +136,11 @@ export function App() { } } + /** + * Abort a specific download if id's provided, other wise abort all running ones. + * @param {number} id The download id / pid + * @returns void + */ const abort = (id?: number) => { if (id) { updateInStateMap(id, null, downloadInfoMap, setDownloadInfoMap, true) @@ -133,11 +151,17 @@ export function App() { setHalt(false) } + /** + * Send via WebSocket a message in order to update the yt-dlp binary from server + */ const updateBinary = () => { setHalt(true) socket.emit('update-bin') } + /** + * Theme toggler handler + */ const toggleTheme = () => { if (darkMode) { localStorage.setItem('theme', 'light') @@ -148,6 +172,9 @@ export function App() { } } + /** + * Handle extract audio checkbox + */ const toggleExtractAudio = () => { if (extractAudio) { localStorage.setItem('-x', 'false') @@ -192,51 +219,25 @@ export function App() { { /* - Message[0] => key, the pid which is shared with the progress Map + Message[0] => key, the pid which is shared with the progress and download Maps Message[1] => value, the actual formatted message sent from server */ } {message[0] && message[1] && message[1] !== 'Done!' ? -
+ + - { - downloadInfoMap.get(message[0]) ? -

{downloadInfoMap.get(message[0]).title}

: - null - } - -
Status
- {!message[1] ?
Ready
: null} -
{message[1]}
- - -
- + +
-
: null - } - { - /* - Gets the progress by indexing the map with the pid - */ - } - {progressMap.get(message[0]) ? - : - null - } - {message[0] && message[1] && message[1] !== 'Done!' ? - - - - - : null +
: null } )) diff --git a/frontend/src/components/StackableInput.tsx b/frontend/src/components/StackableInput.tsx deleted file mode 100644 index 2c53443..0000000 --- a/frontend/src/components/StackableInput.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react"; -import { - InputGroup, - FormControl, - Button, - ProgressBar -} from "react-bootstrap"; - -export function StackableInput(props: any) { - return ( - - - - - -
-
Status
-
{props.message}
-
- - {props.progress ? - : - null - } - - {/* {' '} */} - {/* {' '} */} -
- ) -} \ No newline at end of file diff --git a/frontend/src/components/StackableResult.tsx b/frontend/src/components/StackableResult.tsx new file mode 100644 index 0000000..027a12f --- /dev/null +++ b/frontend/src/components/StackableResult.tsx @@ -0,0 +1,38 @@ +import { Fragment } from "react"; +import { + Row, + Col, + ProgressBar +} from "react-bootstrap"; + +type Props = { + formattedLog: string, + title: string, + thumbnail: string, + progress: number, +} + +export function StackableResult({ formattedLog, title, thumbnail, progress }: Props) { + return ( + +
+ + {title ?

{title}

: null} + +
Status
+ {!formattedLog ?
Ready
: null} +
{formattedLog}
+ + +
+ + +
+
+ {progress ? + : + null + } +
+ ) +} \ No newline at end of file diff --git a/frontend/src/utils.ts b/frontend/src/utils.ts index 2e51d8a..6b1adc3 100644 --- a/frontend/src/utils.ts +++ b/frontend/src/utils.ts @@ -1,8 +1,24 @@ +import { IMessage } from "./interfaces" + +/** + * Validate an ip v4 via regex + * @param {string} ipAddr + * @returns ip validity test + */ export function validateIP(ipAddr: string): boolean { let ipRegex = /^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/gm return ipRegex.test(ipAddr) } +/** + * Validate a domain via regex. + * The validation pass if the domain respects the following formats: + * - localhost + * - domain.tld + * - dir.domain.tld + * @param domainName + * @returns domain validity test + */ export function validateDomain(domainName: string): boolean { let domainRegex = /[^@ \t\r\n]+.[^@ \t\r\n]+\.[^@ \t\r\n]+/ return domainRegex.test(domainName) || domainName === 'localhost' @@ -15,6 +31,11 @@ export function ellipsis(str: string, lim: number): string { 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, '') @@ -28,6 +49,14 @@ export function detectSpeed(str: string): number { } } +/** + * Update a map stored in React State, in this specific impl. all maps have integer keys + * @param {num} k Map key + * @param {*} v Map value + * @param {Map} target The target map saved in-state + * @param {Function} callback calls React's StateAction function with the newly created Map + * @param {boolean} remove -optional- is it an update or a deletion operation? + */ export const updateInStateMap = (k: number, v: any, target: Map, callback: Function, remove: boolean = false) => { if (remove) { target.delete(k) @@ -36,3 +65,12 @@ export const updateInStateMap = (k: number, v: any, target: Map, ca } callback(new Map(target.set(k, v))); } + +/** + * Pre like function + * @param data + * @returns + */ +export function buildMessage(data: IMessage) { + return `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`; +}