it just works
This commit is contained in:
@@ -17,13 +17,12 @@ import {
|
||||
} from "@mui/material";
|
||||
import { grey } from "@mui/material/colors";
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Provider, useSelector } from "react-redux";
|
||||
import {
|
||||
BrowserRouter as Router, Link, Route,
|
||||
Routes
|
||||
} from 'react-router-dom';
|
||||
import ArchivedDownloads from "./Archived";
|
||||
import { AppBar } from "./components/AppBar";
|
||||
import { Drawer } from "./components/Drawer";
|
||||
import Home from "./Home";
|
||||
@@ -163,9 +162,8 @@ function AppContent() {
|
||||
>
|
||||
<Toolbar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home socket={socket}></Home>}></Route>
|
||||
<Route path="/settings" element={<Settings socket={socket}></Settings>}></Route>
|
||||
<Route path="/downloaded" element={<ArchivedDownloads></ArchivedDownloads>}></Route>
|
||||
<Route path="/" element={<Home socket={socket} />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Backdrop, CircularProgress, Container, Grid } from "@mui/material";
|
||||
import { ArchiveResult } from "./components/ArchiveResult";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "./stores/store";
|
||||
|
||||
export default function archivedDownloads() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [archived, setArchived] = useState([]);
|
||||
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`http://${settings.serverAddr}:3022/getAllDownloaded`)
|
||||
.then(res => res.json())
|
||||
.then(data => setArchived(data))
|
||||
.then(() => setLoading(false))
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4, mb: 4 }}>
|
||||
<Backdrop
|
||||
sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }}
|
||||
open={loading}
|
||||
>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
{/*
|
||||
archived.length > 0 ?
|
||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||
{
|
||||
archived.map((el, idx) =>
|
||||
<Grid key={`${idx}-${el.id}`} item xs={4} sm={4} md={4}>
|
||||
<ArchiveResult
|
||||
url={`http://${settings.serverAddr}:3022/stream/${el.id}`}
|
||||
thumbnail={el.img} title={el.title}
|
||||
/>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
</Grid>
|
||||
: null
|
||||
*/}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -21,16 +21,14 @@ import {
|
||||
import { Buffer } from 'buffer';
|
||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { CliArguments } from "./classes";
|
||||
import { StackableResult } from "./components/StackableResult";
|
||||
import { serverStates } from "./events";
|
||||
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 { I18nBuilder } from "./i18n";
|
||||
import { IDLMetadata, IMessage } from "./interfaces";
|
||||
import { RPCClient } from "./rpcClient";
|
||||
import { RootState } from "./stores/store";
|
||||
import { RPCResult } from "./types";
|
||||
import { isValidURL, toFormatArgs, updateInStateMap } from "./utils";
|
||||
import { IDLMetadata, RPCResult } from "./types";
|
||||
import { isValidURL, toFormatArgs } from "./utils";
|
||||
|
||||
type Props = {
|
||||
socket: WebSocket
|
||||
@@ -43,11 +41,7 @@ export default function Home({ socket }: Props) {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
// ephemeral state
|
||||
const [progressMap, setProgressMap] = useState(new Map<number, number>());
|
||||
const [messageMap, setMessageMap] = useState(new Map<number, IMessage>());
|
||||
|
||||
const [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
|
||||
const [downloadInfoMap, setDownloadInfoMap] = useState(new Map<number, IDLMetadata>());
|
||||
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
||||
@@ -60,6 +54,7 @@ export default function Home({ socket }: Props) {
|
||||
|
||||
const [url, setUrl] = useState('');
|
||||
const [workingUrl, setWorkingUrl] = useState('');
|
||||
|
||||
const [showBackdrop, setShowBackdrop] = useState(false);
|
||||
const [showToast, setShowToast] = useState(true);
|
||||
|
||||
@@ -89,9 +84,9 @@ export default function Home({ socket }: Props) {
|
||||
useEffect(() => {
|
||||
socket.onmessage = (event) => {
|
||||
const res = client.decode(event.data)
|
||||
if (showBackdrop) {
|
||||
setShowBackdrop(false)
|
||||
}
|
||||
|
||||
setShowBackdrop(false)
|
||||
|
||||
switch (typeof res.result) {
|
||||
case 'object':
|
||||
setActiveDownloads(
|
||||
@@ -127,6 +122,8 @@ export default function Home({ socket }: Props) {
|
||||
client.download(
|
||||
immediate || url || workingUrl,
|
||||
cliArgs.toString() + toFormatArgs(codes),
|
||||
availableDownloadPaths[downloadPath] ?? '',
|
||||
fileNameOverride
|
||||
)
|
||||
|
||||
setUrl('')
|
||||
@@ -154,7 +151,6 @@ export default function Home({ socket }: Props) {
|
||||
|
||||
client.formats(url)
|
||||
?.then(formats => {
|
||||
console.log(formats)
|
||||
setDownloadFormats(formats.result)
|
||||
setShowBackdrop(false)
|
||||
resetInput()
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
import { useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { debounceTime, distinctUntilChanged, map, of, takeWhile } from "rxjs";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { CliArguments } from "./classes";
|
||||
import { CliArguments } from "./features/core/argsParser";
|
||||
import I18nBuilder from "./features/core/intl";
|
||||
import {
|
||||
LanguageUnion,
|
||||
setCliArgs,
|
||||
@@ -34,16 +34,11 @@ import {
|
||||
setTheme,
|
||||
ThemeUnion
|
||||
} from "./features/settings/settingsSlice";
|
||||
import { alreadyUpdated, updated } from "./features/status/statusSlice";
|
||||
import { I18nBuilder } from "./i18n";
|
||||
import { updated } from "./features/status/statusSlice";
|
||||
import { RootState } from "./stores/store";
|
||||
import { validateDomain, validateIP } from "./utils";
|
||||
|
||||
type Props = {
|
||||
socket: WebSocket
|
||||
}
|
||||
|
||||
export default function Settings({ socket }: Props) {
|
||||
export default function Settings() {
|
||||
const settings = useSelector((state: RootState) => state.settings)
|
||||
const status = useSelector((state: RootState) => state.status)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { EightK, FourK, Hd, Sd } from "@mui/icons-material";
|
||||
import { Button, Card, CardActionArea, CardActions, CardContent, CardMedia, Chip, LinearProgress, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { IMessage } from "../interfaces";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardMedia,
|
||||
Chip,
|
||||
LinearProgress,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Typography
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ellipsis } from "../utils";
|
||||
|
||||
type Props = {
|
||||
@@ -22,6 +34,14 @@ export function StackableResult({
|
||||
size,
|
||||
stopCallback
|
||||
}: Props) {
|
||||
const [isCompleted, setIsCompleted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (percentage === '-1') {
|
||||
setIsCompleted(true)
|
||||
}
|
||||
}, [percentage])
|
||||
|
||||
const guessResolution = (xByY: string): any => {
|
||||
if (!xByY) return null;
|
||||
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
||||
@@ -31,9 +51,10 @@ export function StackableResult({
|
||||
return null;
|
||||
}
|
||||
|
||||
const percentageToNumber = () => Number(percentage.replace('%', ''))
|
||||
const percentageToNumber = () => isCompleted ? 100 : Number(percentage.replace('%', ''))
|
||||
|
||||
const roundMB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)}MiB`
|
||||
const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB`
|
||||
const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s`
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -54,21 +75,33 @@ export function StackableResult({
|
||||
<Skeleton />
|
||||
}
|
||||
<Stack direction="row" spacing={1} py={2}>
|
||||
<Chip label={'Downloading'} color="primary" />
|
||||
<Typography>{percentage}</Typography>
|
||||
<Typography>{speed}</Typography>
|
||||
<Typography>{roundMB(size ?? 0)}</Typography>
|
||||
<Chip
|
||||
label={isCompleted ? 'Completed' : 'Downloading'}
|
||||
color="primary"
|
||||
size="small"
|
||||
/>
|
||||
<Typography>{!isCompleted ? percentage : ''}</Typography>
|
||||
<Typography> {!isCompleted ? formatSpeedMiB(speed) : ''}</Typography>
|
||||
<Typography>{roundMiB(size ?? 0)}</Typography>
|
||||
{guessResolution(resolution)}
|
||||
</Stack>
|
||||
{percentage ?
|
||||
<LinearProgress variant="determinate" value={percentageToNumber()} /> :
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={percentageToNumber()}
|
||||
color={isCompleted ? "secondary" : "primary"}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<CardActions>
|
||||
<Button variant="contained" size="small" color="primary" onClick={stopCallback}>
|
||||
Stop
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
color="primary"
|
||||
onClick={stopCallback}>
|
||||
{isCompleted ? "Clear" : "Stop"}
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { on } from "../events";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
export function Statistics() {
|
||||
const dataset = new Array<number>();
|
||||
const chartRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
on('dlSpeed', (data: CustomEvent<any>) => {
|
||||
dataset.push(data.detail)
|
||||
chartRef.current.update()
|
||||
})
|
||||
}, [])
|
||||
|
||||
const data = {
|
||||
labels: dataset.map(() => ''),
|
||||
datasets: [
|
||||
{
|
||||
data: dataset,
|
||||
label: 'download speed',
|
||||
borderColor: 'rgb(53, 162, 235)',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chart">
|
||||
<Line data={data} ref={chartRef} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import i18n from "./assets/i18n.yaml";
|
||||
import i18n from "../../assets/i18n.yaml";
|
||||
|
||||
export class I18nBuilder {
|
||||
export default class I18nBuilder {
|
||||
private language: string;
|
||||
private textMap = i18n.languages;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { RPCRequest, RPCResponse } from "./types"
|
||||
import type { IDLMetadata } from './interfaces'
|
||||
import type { RPCRequest, RPCResponse, IDLMetadata } from "../../types"
|
||||
|
||||
import { getHttpRPCEndpoint } from './utils'
|
||||
import { getHttpRPCEndpoint } from '../../utils'
|
||||
|
||||
export class RPCClient {
|
||||
private socket: WebSocket
|
||||
@@ -31,7 +30,7 @@ export class RPCClient {
|
||||
})
|
||||
}
|
||||
|
||||
public download(url: string, args: string) {
|
||||
public download(url: string, args: string, pathOverride = '', renameTo = '') {
|
||||
if (url) {
|
||||
this.send({
|
||||
id: this.incrementSeq(),
|
||||
@@ -39,6 +38,8 @@ export class RPCClient {
|
||||
params: [{
|
||||
URL: url.split("?list").at(0)!,
|
||||
Params: args.split(" ").map(a => a.trim()),
|
||||
Path: pathOverride,
|
||||
Rename: renameTo,
|
||||
}]
|
||||
})
|
||||
}
|
||||
10
frontend/src/index.tsx
Normal file
10
frontend/src/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { App } from './App'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root')!)
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App></App>
|
||||
</React.StrictMode>
|
||||
)
|
||||
@@ -1,33 +0,0 @@
|
||||
export interface IMessage {
|
||||
status: string,
|
||||
progress?: string,
|
||||
size?: number,
|
||||
dlSpeed?: string
|
||||
pid: number
|
||||
}
|
||||
|
||||
export interface IDLMetadata {
|
||||
formats: Array<IDLFormat>,
|
||||
best: IDLFormat,
|
||||
thumbnail: string,
|
||||
title: string,
|
||||
}
|
||||
|
||||
export interface IDLFormat {
|
||||
format_id: string,
|
||||
format_note: string,
|
||||
fps: number,
|
||||
resolution: string,
|
||||
vcodec: string,
|
||||
acodec: string,
|
||||
}
|
||||
|
||||
export interface IDLMetadataAndPID {
|
||||
pid: number,
|
||||
metadata: IDLMetadata
|
||||
}
|
||||
|
||||
export interface IDLSpeed {
|
||||
effective: number,
|
||||
unit: string,
|
||||
}
|
||||
17
frontend/src/types.d.ts
vendored
17
frontend/src/types.d.ts
vendored
@@ -6,6 +6,7 @@ export type RPCMethods =
|
||||
| "Service.FreeSpace"
|
||||
| "Service.Formats"
|
||||
| "Service.DirectoryTree"
|
||||
| "Service.UpdateExecutable"
|
||||
|
||||
export type RPCRequest = {
|
||||
method: RPCMethods,
|
||||
@@ -41,4 +42,20 @@ export type RPCResult = {
|
||||
export type RPCParams = {
|
||||
URL: string
|
||||
Params?: string
|
||||
}
|
||||
|
||||
export interface IDLMetadata {
|
||||
formats: Array<IDLFormat>,
|
||||
best: IDLFormat,
|
||||
thumbnail: string,
|
||||
title: string,
|
||||
}
|
||||
|
||||
export interface IDLFormat {
|
||||
format_id: string,
|
||||
format_note: string,
|
||||
fps: number,
|
||||
resolution: string,
|
||||
vcodec: string,
|
||||
acodec: string,
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { IMessage } from "./interfaces"
|
||||
|
||||
/**
|
||||
* Validate an ip v4 via regex
|
||||
* @param {string} ipAddr
|
||||
@@ -65,36 +63,6 @@ export function detectSpeed(str: string): number {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a map stored in React State, in this specific impl. all maps have integer keys
|
||||
* @param k Map key
|
||||
* @param v Map value
|
||||
* @param target The target map saved in-state
|
||||
* @param callback calls React's StateAction function with the newly created Map
|
||||
* @param remove -optional- is it an update or a deletion operation?
|
||||
*/
|
||||
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)
|
||||
callback(new Map(_target))
|
||||
return;
|
||||
}
|
||||
callback(new Map(target.set(k, v)));
|
||||
}
|
||||
|
||||
export function updateInStateArray<T>(v: T, target: Array<T>, callback: Function) { }
|
||||
|
||||
/**
|
||||
* Pre like function
|
||||
* @param data
|
||||
* @returns formatted server message
|
||||
*/
|
||||
export function buildMessage(data: IMessage) {
|
||||
return `operation: ${data.status || '...'} \nprogress: ${data.progress || '?'} \nsize: ${data.size || '?'} \nspeed: ${data.dlSpeed || '?'}`;
|
||||
}
|
||||
|
||||
|
||||
export function toFormatArgs(codes: string[]): string {
|
||||
if (codes.length > 1) {
|
||||
return codes.reduce((v, a) => ` -f ${v}+${a}`)
|
||||
|
||||
Reference in New Issue
Block a user