enable viewing results as listview
This commit is contained in:
@@ -4,11 +4,13 @@ import {
|
|||||||
Dashboard,
|
Dashboard,
|
||||||
// Download,
|
// Download,
|
||||||
Menu, Settings as SettingsIcon,
|
Menu, Settings as SettingsIcon,
|
||||||
|
FormatListBulleted,
|
||||||
SettingsEthernet,
|
SettingsEthernet,
|
||||||
Storage
|
Storage
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
|
CircularProgress,
|
||||||
createTheme, CssBaseline,
|
createTheme, CssBaseline,
|
||||||
Divider,
|
Divider,
|
||||||
IconButton, List,
|
IconButton, List,
|
||||||
@@ -17,16 +19,16 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { grey } from "@mui/material/colors";
|
import { grey } from "@mui/material/colors";
|
||||||
import ListItemButton from '@mui/material/ListItemButton';
|
import ListItemButton from '@mui/material/ListItemButton';
|
||||||
import { useMemo, useState } from "react";
|
import { lazy, Suspense, useMemo, useState } from "react";
|
||||||
import { Provider, useSelector } from "react-redux";
|
import { Provider, useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router, Link, Route,
|
BrowserRouter as Router, Link, Route,
|
||||||
Routes
|
Routes
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import { AppBar } from "./components/AppBar";
|
import { AppBar } from "./components/AppBar";
|
||||||
import { Drawer } from "./components/Drawer";
|
import { Drawer } from "./components/Drawer";
|
||||||
|
import { toggleListView } from "./features/settings/settingsSlice";
|
||||||
import Home from "./Home";
|
import Home from "./Home";
|
||||||
import Settings from "./Settings";
|
|
||||||
import { RootState, store } from './stores/store';
|
import { RootState, store } from './stores/store';
|
||||||
import { formatGiB, getWebSocketEndpoint } from "./utils";
|
import { formatGiB, getWebSocketEndpoint } from "./utils";
|
||||||
|
|
||||||
@@ -35,6 +37,7 @@ function AppContent() {
|
|||||||
|
|
||||||
const settings = useSelector((state: RootState) => state.settings)
|
const settings = useSelector((state: RootState) => state.settings)
|
||||||
const status = useSelector((state: RootState) => state.status)
|
const status = useSelector((state: RootState) => state.status)
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), [])
|
const socket = useMemo(() => new WebSocket(getWebSocketEndpoint()), [])
|
||||||
|
|
||||||
@@ -54,6 +57,8 @@ function AppContent() {
|
|||||||
setOpen(!open)
|
setOpen(!open)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Settings = lazy(() => import('./Settings'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Router>
|
<Router>
|
||||||
@@ -132,6 +137,12 @@ function AppContent() {
|
|||||||
<ListItemText primary="Home" />
|
<ListItemText primary="Home" />
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</Link>
|
</Link>
|
||||||
|
<ListItemButton onClick={() => dispatch(toggleListView())}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<FormatListBulleted />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText primary="List view" />
|
||||||
|
</ListItemButton>
|
||||||
<Link to={'/settings'} style={
|
<Link to={'/settings'} style={
|
||||||
{
|
{
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
@@ -158,7 +169,11 @@ function AppContent() {
|
|||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home socket={socket} />} />
|
<Route path="/" element={<Home socket={socket} />} />
|
||||||
<Route path="/settings" element={<Settings socket={socket} />} />
|
<Route path="/settings" element={
|
||||||
|
<Suspense fallback={<CircularProgress />}>
|
||||||
|
<Settings socket={socket} />
|
||||||
|
</Suspense>
|
||||||
|
} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -19,9 +19,10 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
import { Fragment, useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { StackableResult } from "./components/StackableResult";
|
import { DownloadsCardView } from "./components/DownloadsCardView";
|
||||||
|
import { DownloadsListView } from "./components/DownloadsListView";
|
||||||
import { CliArguments } from "./features/core/argsParser";
|
import { CliArguments } from "./features/core/argsParser";
|
||||||
import I18nBuilder from "./features/core/intl";
|
import I18nBuilder from "./features/core/intl";
|
||||||
import { RPCClient } from "./features/core/rpcClient";
|
import { RPCClient } from "./features/core/rpcClient";
|
||||||
@@ -41,7 +42,7 @@ export default function Home({ socket }: Props) {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
// ephemeral state
|
// ephemeral state
|
||||||
const [activeDownloads, setActiveDownloads] = useState(new Array<RPCResult>());
|
const [activeDownloads, setActiveDownloads] = useState<Array<RPCResult>>();
|
||||||
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
const [downloadFormats, setDownloadFormats] = useState<IDLMetadata>();
|
||||||
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
const [pickedVideoFormat, setPickedVideoFormat] = useState('');
|
||||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
||||||
@@ -56,7 +57,7 @@ export default function Home({ socket }: Props) {
|
|||||||
const [url, setUrl] = useState('');
|
const [url, setUrl] = useState('');
|
||||||
const [workingUrl, setWorkingUrl] = useState('');
|
const [workingUrl, setWorkingUrl] = useState('');
|
||||||
|
|
||||||
const [showBackdrop, setShowBackdrop] = useState(false);
|
const [showBackdrop, setShowBackdrop] = useState(true);
|
||||||
const [showToast, setShowToast] = useState(true);
|
const [showToast, setShowToast] = useState(true);
|
||||||
|
|
||||||
// memos
|
// memos
|
||||||
@@ -105,10 +106,10 @@ export default function Home({ socket }: Props) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeDownloads.length > 0 && showBackdrop) {
|
if (activeDownloads && activeDownloads.length >= 0) {
|
||||||
setShowBackdrop(false)
|
setShowBackdrop(false)
|
||||||
}
|
}
|
||||||
}, [activeDownloads, showBackdrop])
|
}, [activeDownloads?.length])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
client.directoryTree()
|
client.directoryTree()
|
||||||
@@ -137,6 +138,7 @@ export default function Home({ socket }: Props) {
|
|||||||
|
|
||||||
setUrl('')
|
setUrl('')
|
||||||
setWorkingUrl('')
|
setWorkingUrl('')
|
||||||
|
setShowBackdrop(true)
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetInput()
|
resetInput()
|
||||||
@@ -460,25 +462,11 @@ export default function Home({ socket }: Props) {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid> : null
|
</Grid> : null
|
||||||
}
|
}
|
||||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
{
|
||||||
{
|
settings.listView ?
|
||||||
activeDownloads.map(download => (
|
<DownloadsListView downloads={activeDownloads ?? []} abortFunction={abort} /> :
|
||||||
<Grid item xs={4} sm={8} md={6} key={download.id}>
|
<DownloadsCardView downloads={activeDownloads ?? []} abortFunction={abort} />
|
||||||
<Fragment>
|
}
|
||||||
<StackableResult
|
|
||||||
title={download.info.title}
|
|
||||||
thumbnail={download.info.thumbnail}
|
|
||||||
percentage={download.progress.percentage}
|
|
||||||
stopCallback={() => abort(download.id)}
|
|
||||||
resolution={download.info.resolution ?? ''}
|
|
||||||
speed={download.progress.speed}
|
|
||||||
size={download.info.filesize_approx ?? 0}
|
|
||||||
/>
|
|
||||||
</Fragment>
|
|
||||||
</Grid>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
<Snackbar
|
<Snackbar
|
||||||
open={showToast === status.connected}
|
open={showToast === status.connected}
|
||||||
autoHideDuration={1500}
|
autoHideDuration={1500}
|
||||||
|
|||||||
33
frontend/src/components/DownloadsCardView.tsx
Normal file
33
frontend/src/components/DownloadsCardView.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Grid } from "@mui/material"
|
||||||
|
import { Fragment } from "react"
|
||||||
|
import type { RPCResult } from "../types"
|
||||||
|
import { StackableResult } from "./StackableResult"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
downloads: RPCResult[]
|
||||||
|
abortFunction: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadsCardView({ downloads, abortFunction }: Props) {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||||
|
{
|
||||||
|
downloads.map(download => (
|
||||||
|
<Grid item xs={4} sm={8} md={6} key={download.id}>
|
||||||
|
<Fragment>
|
||||||
|
<StackableResult
|
||||||
|
title={download.info.title}
|
||||||
|
thumbnail={download.info.thumbnail}
|
||||||
|
percentage={download.progress.percentage}
|
||||||
|
stopCallback={() => abortFunction(download.id)}
|
||||||
|
resolution={download.info.resolution ?? ''}
|
||||||
|
speed={download.progress.speed}
|
||||||
|
size={download.info.filesize_approx ?? 0}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
</Grid>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
70
frontend/src/components/DownloadsListView.tsx
Normal file
70
frontend/src/components/DownloadsListView.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Button, CircularProgress, Grid, LinearProgress, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"
|
||||||
|
import { RPCResult } from "../types"
|
||||||
|
import { ellipsis, formatSpeedMiB, roundMiB } from "../utils"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
downloads: RPCResult[]
|
||||||
|
abortFunction: Function
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DownloadsListView({ downloads, abortFunction }: Props) {
|
||||||
|
return (
|
||||||
|
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<TableContainer component={Paper} sx={{ minHeight: '65vh' }} elevation={2}>
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>
|
||||||
|
<Typography fontWeight={500} fontSize={15}>Title</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography fontWeight={500} fontSize={15}>Progress</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography fontWeight={500} fontSize={15}>Speed</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography fontWeight={500} fontSize={15}>Size</Typography>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Typography fontWeight={500} fontSize={15}>Actions</Typography>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
downloads.map(download => (
|
||||||
|
<TableRow key={download.id}>
|
||||||
|
<TableCell>{ellipsis(download.info.title, 80)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<LinearProgress
|
||||||
|
value={
|
||||||
|
download.progress.percentage === '-1' ? 100 :
|
||||||
|
Number(download.progress.percentage.replace('%', ''))
|
||||||
|
}
|
||||||
|
variant="determinate"
|
||||||
|
color={download.progress.percentage === '-1' ? 'success' : 'primary'}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{formatSpeedMiB(download.progress.speed)}</TableCell>
|
||||||
|
<TableCell>{roundMiB(download.info.filesize_approx ?? 0)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
onClick={() => abortFunction(download.id)}
|
||||||
|
>
|
||||||
|
{download.progress.percentage === '-1' ? 'Remove' : 'Stop'}
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Typography
|
Typography
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ellipsis } from "../utils";
|
import { ellipsis, formatSpeedMiB, roundMiB } from "../utils";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string,
|
title: string,
|
||||||
@@ -42,6 +42,8 @@ export function StackableResult({
|
|||||||
}
|
}
|
||||||
}, [percentage])
|
}, [percentage])
|
||||||
|
|
||||||
|
const percentageToNumber = () => isCompleted ? 100 : Number(percentage.replace('%', ''))
|
||||||
|
|
||||||
const guessResolution = (xByY: string): any => {
|
const guessResolution = (xByY: string): any => {
|
||||||
if (!xByY) return null;
|
if (!xByY) return null;
|
||||||
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
if (xByY.includes('4320')) return (<EightK color="primary" />);
|
||||||
@@ -51,11 +53,6 @@ export function StackableResult({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentageToNumber = () => isCompleted ? 100 : Number(percentage.replace('%', ''))
|
|
||||||
|
|
||||||
const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB`
|
|
||||||
const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardActionArea>
|
<CardActionArea>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface SettingsState {
|
|||||||
fileRenaming: boolean
|
fileRenaming: boolean
|
||||||
pathOverriding: boolean
|
pathOverriding: boolean
|
||||||
enableCustomArgs: boolean
|
enableCustomArgs: boolean
|
||||||
|
listView: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
@@ -27,6 +28,7 @@ const initialState: SettingsState = {
|
|||||||
fileRenaming: localStorage.getItem("file-renaming") === "true",
|
fileRenaming: localStorage.getItem("file-renaming") === "true",
|
||||||
pathOverriding: localStorage.getItem("path-overriding") === "true",
|
pathOverriding: localStorage.getItem("path-overriding") === "true",
|
||||||
enableCustomArgs: localStorage.getItem("enable-custom-args") === "true",
|
enableCustomArgs: localStorage.getItem("enable-custom-args") === "true",
|
||||||
|
listView: localStorage.getItem("listview") === "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const settingsSlice = createSlice({
|
export const settingsSlice = createSlice({
|
||||||
@@ -73,6 +75,10 @@ export const settingsSlice = createSlice({
|
|||||||
state.enableCustomArgs = action.payload
|
state.enableCustomArgs = action.payload
|
||||||
localStorage.setItem("enable-custom-args", action.payload.toString())
|
localStorage.setItem("enable-custom-args", action.payload.toString())
|
||||||
},
|
},
|
||||||
|
toggleListView: (state) => {
|
||||||
|
state.listView = !state.listView
|
||||||
|
localStorage.setItem("listview", state.listView.toString())
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -87,6 +93,7 @@ export const {
|
|||||||
setFileRenaming,
|
setFileRenaming,
|
||||||
setPathOverriding,
|
setPathOverriding,
|
||||||
setEnableCustomArgs,
|
setEnableCustomArgs,
|
||||||
|
toggleListView
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
@@ -83,4 +83,7 @@ export function getHttpRPCEndpoint() {
|
|||||||
|
|
||||||
export function formatGiB(bytes: number) {
|
export function formatGiB(bytes: number) {
|
||||||
return `${(bytes / 1_000_000_000).toFixed(0)}GiB`
|
return `${(bytes / 1_000_000_000).toFixed(0)}GiB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const roundMiB = (bytes: number) => `${(bytes / 1_000_000).toFixed(2)} MiB`
|
||||||
|
export const formatSpeedMiB = (val: number) => `${roundMiB(val)}/s`
|
||||||
Reference in New Issue
Block a user