File upload and code refactoring
This commit is contained in:
@@ -6,8 +6,11 @@ import {
|
|||||||
CircularProgress,
|
CircularProgress,
|
||||||
Container,
|
Container,
|
||||||
Grid,
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
Paper,
|
Paper,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@@ -17,7 +20,9 @@ import { StackableResult } from "./components/StackableResult";
|
|||||||
import { connected, downloading, finished } from "./features/status/statusSlice";
|
import { connected, downloading, finished } from "./features/status/statusSlice";
|
||||||
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
|
import { IDLInfo, IDLInfoBase, IDownloadInfo, IMessage } from "./interfaces";
|
||||||
import { RootState } from "./stores/store";
|
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 = {
|
type Props = {
|
||||||
socket: Socket
|
socket: Socket
|
||||||
@@ -74,7 +79,7 @@ export default function Home({ socket }: Props) {
|
|||||||
socket.on('info', (data: IDLInfo) => {
|
socket.on('info', (data: IDLInfo) => {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
dispatch(downloading())
|
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) => {
|
socket.on('progress', (data: IMessage) => {
|
||||||
if (data.status === 'Done!' || data.status === 'Aborted') {
|
if (data.status === 'Done!' || data.status === 'Aborted') {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
updateInStateMap(data.pid, 'Done!', messageMap, setMessageMap);
|
updateInStateMap<number, IMessage>(data.pid, 'Done!', messageMap, setMessageMap);
|
||||||
updateInStateMap(data.pid, 0, progressMap, setProgressMap);
|
updateInStateMap<number, number>(data.pid, 0, progressMap, setProgressMap);
|
||||||
socket.emit('disk-space')
|
socket.emit('disk-space')
|
||||||
dispatch(finished())
|
dispatch(finished())
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateInStateMap(data.pid, data, messageMap, setMessageMap);
|
updateInStateMap<number, IMessage>(data.pid, data, messageMap, setMessageMap);
|
||||||
if (data.progress) {
|
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
|
* Retrive url from input, cli-arguments from checkboxes and emits via WebSocket
|
||||||
*/
|
*/
|
||||||
const sendUrl = () => {
|
const sendUrl = (immediate?: string) => {
|
||||||
const codes = new Array<string>();
|
const codes = new Array<string>();
|
||||||
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
|
if (pickedVideoFormat !== '') codes.push(pickedVideoFormat);
|
||||||
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
if (pickedAudioFormat !== '') codes.push(pickedAudioFormat);
|
||||||
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
if (pickedBestFormat !== '') codes.push(pickedBestFormat);
|
||||||
|
|
||||||
socket.emit('send-url', {
|
socket.emit('send-url', {
|
||||||
url: url || workingUrl,
|
url: immediate || url || workingUrl,
|
||||||
params: settings.cliArgs.toString() + toFormatArgs(codes),
|
params: settings.cliArgs.toString() + toFormatArgs(codes),
|
||||||
})
|
})
|
||||||
setUrl('')
|
setUrl('')
|
||||||
@@ -163,6 +172,27 @@ export default function Home({ socket }: Props) {
|
|||||||
socket.emit('abort-all')
|
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 (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||||
<Backdrop
|
<Backdrop
|
||||||
@@ -185,7 +215,19 @@ export default function Home({ socket }: Props) {
|
|||||||
label={settings.i18n.t('urlInput')}
|
label={settings.i18n.t('urlInput')}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onChange={handleUrlChange}
|
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 container spacing={1} pt={2}>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { debounceTime, distinctUntilChanged, map, of } from "rxjs";
|
||||||
import { Socket } from "socket.io-client";
|
import { Socket } from "socket.io-client";
|
||||||
import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice";
|
import { LanguageUnion, setCliArgs, setFormatSelection, setLanguage, setServerAddr, setTheme, ThemeUnion } from "./features/settings/settingsSlice";
|
||||||
import { alreadyUpdated, updated } from "./features/status/statusSlice";
|
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.
|
* 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
|
* @param e Input change event
|
||||||
*/
|
*/
|
||||||
const handleAddrChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleAddrChange = (event: any) => {
|
||||||
const input = e.target.value;
|
const $serverAddr = of(event)
|
||||||
if (validateIP(input)) {
|
.pipe(
|
||||||
setInvalidIP(false)
|
map(event => event.target.value),
|
||||||
dispatch(setServerAddr(input))
|
debounceTime(500),
|
||||||
} else if (validateDomain(input)) {
|
distinctUntilChanged()
|
||||||
setInvalidIP(false)
|
)
|
||||||
dispatch(setServerAddr(input))
|
.subscribe(addr => {
|
||||||
} else {
|
if (validateIP(addr)) {
|
||||||
setInvalidIP(true)
|
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 (
|
return (
|
||||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||||
<Grid container spacing={3}>
|
<Grid container spacing={3}>
|
||||||
{/* Chart */}
|
|
||||||
<Grid item xs={12} md={12} lg={12}>
|
<Grid item xs={12} md={12} lg={12}>
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
@@ -99,8 +107,8 @@ export default function Settings({ socket }: Props) {
|
|||||||
fullWidth
|
fullWidth
|
||||||
label={settings.i18n.t('serverAddressTitle')}
|
label={settings.i18n.t('serverAddressTitle')}
|
||||||
defaultValue={settings.serverAddr}
|
defaultValue={settings.serverAddr}
|
||||||
onChange={handleAddrChange}
|
|
||||||
error={invalidIP}
|
error={invalidIP}
|
||||||
|
onChange={handleAddrChange}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: <InputAdornment position="start">ws://</InputAdornment>,
|
startAdornment: <InputAdornment position="start">ws://</InputAdornment>,
|
||||||
}}
|
}}
|
||||||
@@ -165,6 +173,7 @@ export default function Settings({ socket }: Props) {
|
|||||||
<Switch
|
<Switch
|
||||||
defaultChecked={settings.cliArgs.extractAudio}
|
defaultChecked={settings.cliArgs.extractAudio}
|
||||||
onChange={() => dispatch(setCliArgs(settings.cliArgs.toggleExtractAudio()))}
|
onChange={() => dispatch(setCliArgs(settings.cliArgs.toggleExtractAudio()))}
|
||||||
|
disabled={settings.formatSelection}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label={settings.i18n.t('extractAudioCheckbox')}
|
label={settings.i18n.t('extractAudioCheckbox')}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export class CliArguments {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public disableExtractAudio() {
|
||||||
|
this._extractAudio = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public get noMTime(): boolean {
|
public get noMTime(): boolean {
|
||||||
return this._noMTime;
|
return this._noMTime;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ export interface SettingsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const initialState: 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,
|
language: (localStorage.getItem("language") || "english") as LanguageUnion,
|
||||||
theme: (localStorage.getItem("theme") || "light") as ThemeUnion,
|
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")),
|
i18n: new I18nBuilder((localStorage.getItem("language") || "english")),
|
||||||
formatSelection: localStorage.getItem("format-selection") === "true",
|
formatSelection: localStorage.getItem("format-selection") === "true",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,22 @@ export function validateDomain(domainName: string): boolean {
|
|||||||
return domainRegex.test(domainName) || domainName === 'localhost'
|
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 {
|
export function ellipsis(str: string, lim: number): string {
|
||||||
if (str) {
|
if (str) {
|
||||||
return str.length > lim ? `${str.substring(0, lim)}...` : 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
|
* @returns download speed in KiB/s
|
||||||
*/
|
*/
|
||||||
export function detectSpeed(str: string): number {
|
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, '')
|
const unit = str.replace(effective, '')
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'MiB/s':
|
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 callback calls React's StateAction function with the newly created Map
|
||||||
* @param remove -optional- is it an update or a deletion operation?
|
* @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) {
|
if (remove) {
|
||||||
const _target = target
|
const _target = target
|
||||||
_target.delete(k)
|
_target.delete(k)
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class Process {
|
|||||||
*/
|
*/
|
||||||
async kill() {
|
async kill() {
|
||||||
spawn('kill', [String(this.pid)]).on('exit', () => {
|
spawn('kill', [String(this.pid)]).on('exit', () => {
|
||||||
log.info('db', `Deleted ${this.pid} because SIGKILL`)
|
log.info('proc', `Stopped ${this.pid} because SIGKILL`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user