File upload and code refactoring
This commit is contained in:
@@ -6,8 +6,11 @@ import {
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Paper,
|
||||
Snackbar,
|
||||
styled,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
@@ -17,7 +20,9 @@ import { StackableResult } from "./components/StackableResult";
|
||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
||||
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
|
||||
import { RootState } from "./stores/store";
|
||||
import { toFormatArgs, updateInStateMap, } from "./utils";
|
||||
import { isValidURL, toFormatArgs, updateInStateMap, } from "./utils";
|
||||
import { FileUpload } from "@mui/icons-material";
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
type Props = {
|
||||
socket: Socket
|
||||
@@ -74,7 +79,7 @@ export default function Home({ socket }: Props) {
|
||||
socket.on('info', (data: IDLInfo) => {
|
||||
setShowBackdrop(false)
|
||||
dispatch(downloading())
|
||||
updateInStateMap(data.pid, data.info, downloadInfoMap, setDownloadInfoMap);
|
||||
updateInStateMap<number, IDLInfoBase>(data.pid, data.info, downloadInfoMap, setDownloadInfoMap);
|
||||
})
|
||||
}, [])
|
||||
|
||||
@@ -83,15 +88,19 @@ export default function Home({ socket }: Props) {
|
||||
socket.on('progress', (data: IMessage) => {
|
||||
if (data.status === 'Done!' || data.status === 'Aborted') {
|
||||
setShowBackdrop(false)
|
||||
updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap);
|
||||
updateInStateMap(data.pid, 0, progressMap, setProgressMap);
|
||||
updateInStateMap<number, IMessage>(data.pid, 'Done!', messageMap, setMessageMap);
|
||||
updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
|
||||
socket.emit('disk-space')
|
||||
dispatch(finished())
|
||||
return;
|
||||
}
|
||||
updateInStateMap(data.pid, data, messageMap, setMessageMap);
|
||||
updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
|
||||
if (data.progress) {
|
||||
updateInStateMap(data.pid, Math.ceil(Number(data.progress.replace('%', ''))), progressMap, setProgressMap)
|
||||
updateInStateMap<number, number>(data.pid,
|
||||
Math.ceil(Number(data.progress.replace('%', ''))),
|
||||
progressMap,
|
||||
setProgressMap
|
||||
);
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
@@ -101,14 +110,14 @@ export default function Home({ socket }: Props) {
|
||||
/**
|
||||
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
|
||||
*/
|
||||
const sendUrl = () => {
|
||||
const sendUrl = (immediate?: string) => {
|
||||
const codes = new Array<string>();
|
||||
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
|
||||
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
||||
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
||||
|
||||
socket.emit('send-url', {
|
||||
url: url || workingUrl,
|
||||
url: immediate || url || workingUrl,
|
||||
params: settings.cliArgs.toString() + toFormatArgs(codes),
|
||||
})
|
||||
setUrl('')
|
||||
@@ -163,6 +172,27 @@ export default function Home({ socket }: Props) {
|
||||
socket.emit('abort-all')
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
/* -------------------- styled components -------------------- */
|
||||
|
||||
const Input = styled('input')({
|
||||
display: 'none',
|
||||
});
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Backdrop
|
||||
@@ -185,7 +215,19 @@ export default function Home({ socket }: Props) {
|
||||
label={settings.i18n.t('urlInput')}
|
||||
variant="outlined"
|
||||
onChange={handleUrlChange}
|
||||
disabled={settings.formatSelection && downloadFormats != null}
|
||||
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<label htmlFor="icon-button-file">
|
||||
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
|
||||
<IconButton color="primary" aria-label="upload file" component="span">
|
||||
<FileUpload />
|
||||
</IconButton>
|
||||
</label>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Grid container spacing={1} pt={2}>
|
||||
<Grid item>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { debounceTime, distinctUntilChanged, map, of } from "rxjs";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice";
|
||||
import { alreadyUpdated, updated } from "./features/status/statusSlice";
|
||||
@@ -38,20 +39,28 @@ export default function Settings({ socket }: Props) {
|
||||
|
||||
/**
|
||||
* Update the server ip address state and localstorage whenever the input value changes.
|
||||
* Validate the ip-addr then set.
|
||||
* Validate the ip-addr then set.s
|
||||
* @param e Input change event
|
||||
*/
|
||||
const handleAddrChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const input = e.target.value;
|
||||
if (validateIP(input)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(input))
|
||||
} else if (validateDomain(input)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(input))
|
||||
} else {
|
||||
setInvalidIP(true)
|
||||
}
|
||||
const handleAddrChange = (event: any) => {
|
||||
const $serverAddr = of(event)
|
||||
.pipe(
|
||||
map(event => event.target.value),
|
||||
debounceTime(500),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
.subscribe(addr => {
|
||||
if (validateIP(addr)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(addr))
|
||||
} else if (validateDomain(addr)) {
|
||||
setInvalidIP(false)
|
||||
dispatch(setServerAddr(addr))
|
||||
} else {
|
||||
setInvalidIP(true)
|
||||
}
|
||||
})
|
||||
return $serverAddr.unsubscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -79,7 +88,6 @@ export default function Settings({ socket }: Props) {
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Grid container spacing={3}>
|
||||
{/* Chart */}
|
||||
<Grid item xs={12} md={12} lg={12}>
|
||||
<Paper
|
||||
sx={{
|
||||
@@ -99,8 +107,8 @@ export default function Settings({ socket }: Props) {
|
||||
fullWidth
|
||||
label={settings.i18n.t('serverAddressTitle')}
|
||||
defaultValue={settings.serverAddr}
|
||||
onChange={handleAddrChange}
|
||||
error={invalidIP}
|
||||
onChange={handleAddrChange}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start">ws://</InputAdornment>,
|
||||
}}
|
||||
@@ -165,6 +173,7 @@ export default function Settings({ socket }: Props) {
|
||||
<Switch
|
||||
defaultChecked={settings.cliArgs.extractAudio}
|
||||
onChange={() => dispatch(setCliArgs(settings.cliArgs.toggleExtractAudio()))}
|
||||
disabled={settings.formatSelection}
|
||||
/>
|
||||
}
|
||||
label={settings.i18n.t('extractAudioCheckbox')}
|
||||
|
||||
@@ -16,6 +16,11 @@ export class CliArguments {
|
||||
return this;
|
||||
}
|
||||
|
||||
public disableExtractAudio() {
|
||||
this._extractAudio = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get noMTime(): boolean {
|
||||
return this._noMTime;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,10 @@ export interface SettingsState {
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
serverAddr: localStorage.getItem("server-addr") || "localhost",
|
||||
serverAddr: localStorage.getItem("server-addr") || window.location.hostname,
|
||||
language: (localStorage.getItem("language") || "english") as LanguageUnion,
|
||||
theme: (localStorage.getItem("theme") || "light") as ThemeUnion,
|
||||
cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args")) : new CliArguments(false, true),
|
||||
cliArgs: localStorage.getItem("cli-args") ? new CliArguments().fromString(localStorage.getItem("cli-args") ?? "") : new CliArguments(false, true),
|
||||
i18n: new I18nBuilder((localStorage.getItem("language") || "english")),
|
||||
formatSelection: localStorage.getItem("format-selection") === "true",
|
||||
}
|
||||
|
||||
@@ -24,6 +24,22 @@ export function validateDomain(domainName: string): boolean {
|
||||
return domainRegex.test(domainName) || domainName === 'localhost'
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a domain via regex.
|
||||
* Exapmples
|
||||
* - http://example.com
|
||||
* - https://example.com
|
||||
* - http://www.example.com
|
||||
* - https://www.example.com
|
||||
* - http://10.0.0.1/[something]/[something-else]
|
||||
* @param url
|
||||
* @returns url validity test
|
||||
*/
|
||||
export function isValidURL(url: string): boolean {
|
||||
let urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)/
|
||||
return urlRegex.test(url)
|
||||
}
|
||||
|
||||
export function ellipsis(str: string, lim: number): string {
|
||||
if (str) {
|
||||
return str.length > lim ? `${str.substring(0, lim)}...` : str
|
||||
@@ -37,7 +53,7 @@ export function ellipsis(str: string, lim: number): string {
|
||||
* @returns download speed in KiB/s
|
||||
*/
|
||||
export function detectSpeed(str: string): number {
|
||||
let effective = str.match(/[\d,]+(\.\d+)?/)[0]
|
||||
let effective = str.match(/[\d,]+(\.\d+)?/)![0]
|
||||
const unit = str.replace(effective, '')
|
||||
switch (unit) {
|
||||
case 'MiB/s':
|
||||
@@ -57,7 +73,7 @@ export function detectSpeed(str: string): number {
|
||||
* @param callback calls React's StateAction function with the newly created Map
|
||||
* @param remove -optional- is it an update or a deletion operation?
|
||||
*/
|
||||
export const updateInStateMap = (k: number, v: any, target: Map<number, any>, callback: Function, remove: boolean = false) => {
|
||||
export function updateInStateMap<K, V>(k: K, v: any, target: Map<K, V>, callback: Function, remove: boolean = false) {
|
||||
if (remove) {
|
||||
const _target = target
|
||||
_target.delete(k)
|
||||
|
||||
Reference in New Issue
Block a user