From 5f93e3e526c08359d3db82063296de835845daf2 Mon Sep 17 00:00:00 2001 From: Subhamoy Biswas Date: Sun, 13 Jul 2025 19:39:11 +0530 Subject: [PATCH] (feat): added stop all button in library, improved library layout and other minor ui improvements --- src/pages/library.tsx | 702 ++++++++++++++++++++++------------------- src/pages/settings.tsx | 8 +- src/services/store.ts | 7 +- src/types/store.ts | 5 + 4 files changed, 403 insertions(+), 319 deletions(-) diff --git a/src/pages/library.tsx b/src/pages/library.tsx index 50a3457..cc75348 100644 --- a/src/pages/library.tsx +++ b/src/pages/library.tsx @@ -6,9 +6,9 @@ import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; import { useToast } from "@/hooks/use-toast"; import { useAppContext } from "@/providers/appContextProvider"; -import { useDownloadActionStatesStore, useDownloadStatesStore } from "@/services/store"; +import { useDownloadActionStatesStore, useDownloadStatesStore, useLibraryPageStatesStore } from "@/services/store"; import { formatBitrate, formatCodec, formatDurationString, formatFileSize, formatSecToTimeString, formatSpeed } from "@/utils"; -import { AudioLines, Clock, CloudDownload, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, PackageCheck, Pause, Play, Trash2, Video, X } from "lucide-react"; +import { AudioLines, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Loader2, Music, Pause, Play, Square, Trash2, Video, X } from "lucide-react"; import { invoke } from "@tauri-apps/api/core"; import * as fs from "@tauri-apps/plugin-fs"; import { DownloadState } from "@/types/download"; @@ -18,9 +18,14 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import Heading from "@/components/heading"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Badge } from "@/components/ui/badge"; export default function LibraryPage() { + const activeTab = useLibraryPageStatesStore(state => state.activeTab); + const setActiveTab = useLibraryPageStatesStore(state => state.setActiveTab); + const downloadStates = useDownloadStatesStore(state => state.downloadStates); const downloadActions = useDownloadActionStatesStore(state => state.downloadActions); const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload); @@ -36,6 +41,9 @@ export default function LibraryPage() { const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed'); const completedDownloads = downloadStates.filter(state => state.download_status === 'completed'); + const ongoingDownloads = downloadStates.filter(state => + ['starting', 'downloading', 'queued'].includes(state.download_status) + ); const openFile = async (filePath: string | null, app: string | null) => { if (filePath && await fs.exists(filePath)) { @@ -96,329 +104,389 @@ export default function LibraryPage() { }) } + const stopOngoingDownloads = async () => { + if (ongoingDownloads.length > 0) { + try { + await invoke('pause_ongoing_downloads').then(() => { + queryClient.invalidateQueries({ queryKey: ['download-states'] }); + toast({ + title: 'Stopped downloads', + description: 'All ongoing downloads are being stopped.', + }); + }); + } catch (e) { + console.error(e); + toast({ + title: 'Failed to stop downloads', + description: 'An error occurred while trying to stop the downloads.', + variant: "destructive" + }); + } + } else { + toast({ + title: 'No ongoing downloads', + description: 'There are no ongoing downloads to stop.', + variant: "destructive" + }); + } + } + return (
-
-
- -

Incomplete Downloads

+ +
+ + Completed {completedDownloads.length > 0 && (`(${completedDownloads.length})`)} + Incomplete {(incompleteDownloads.length > 0 && ongoingDownloads.length <= 0) && (`(${incompleteDownloads.length})`)} {ongoingDownloads.length > 0 && ({ongoingDownloads.length})} + + + + + + + + Stop all ongoing downloads? + + Are you sure you want to stop all ongoing downloads? This will pause all downloads including the download queue. + + + + Cancel + stopOngoingDownloads()} + >Stop + + +
- -
-
- {incompleteDownloads.length > 0 ? ( - incompleteDownloads.map((state) => { - const itemActionStates = downloadActions[state.download_id] || { - isResuming: false, - isPausing: false, - isCanceling: false, - isDeleteFileChecked: false, - }; - return ( -
-
- - - - {state.ext && ( - - {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( - - )} -
-
-
-

{state.title}

- {((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && ( - - )} - {(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && ( -
- {state.progress}% - - { - state.downloaded && state.total - ? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})` - : null - } + +
+ {completedDownloads.length > 0 ? ( + completedDownloads.map((state) => { + const itemActionStates = downloadActions[state.download_id] || { + isResuming: false, + isPausing: false, + isCanceling: false, + isDeleteFileChecked: false, + }; + return ( +
+
+ + + + + {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( + +
+
+
+

{state.title}

+

{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}

+
+ {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'} + + + {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( + + )} + {state.filetype && state.filetype === 'audio' && ( + + )} + {(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && ( + + )} + {state.filesize ? formatFileSize(state.filesize) : 'unknown'} + + + + {state.vbr && state.abr ? ( + formatBitrate(state.vbr + state.abr) + ) : state.vbr ? ( + formatBitrate(state.vbr) + ) : state.abr ? ( + formatBitrate(state.abr) + ) : ( + 'unknown' + )} + +
+
+ {state.playlist_id && state.playlist_index && ( + + Playlist ({state.playlist_index} of {state.playlist_n_entries}) + + )} + {state.vcodec && ( + {formatCodec(state.vcodec)} + )} + {state.acodec && ( + {formatCodec(state.acodec)} + )} + {state.dynamic_range && state.dynamic_range !== 'SDR' && ( + {state.dynamic_range} + )} + {state.subtitle_id && ( + + ESUB + + )} +
- )} -
{ state.download_status && ( - `${state.download_status === 'downloading' && state.status === 'finished' ? 'Processing' : state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} ${state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? `• Speed: ${formatSpeed(state.speed)}` : ""} ${state.download_status === 'downloading' && state.eta ? `• ETA: ${formatSecToTimeString(state.eta)}` : ""}` - )}
-
-
- {state.download_status === 'paused' ? ( - - ) : ( - - )} - -
-
-
- ) - }) - ) : ( -
No Incomplete downloads!
- )} -
-
-
- -

Completed Downloads

-
- -
-
- {completedDownloads.length > 0 ? ( - completedDownloads.map((state) => { - const itemActionStates = downloadActions[state.download_id] || { - isResuming: false, - isPausing: false, - isCanceling: false, - isDeleteFileChecked: false, - }; - return ( -
-
- - - - - {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( - -
-
-
-

{state.title}

-

{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}

-
- {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'} - - - {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( - - )} - {state.filetype && state.filetype === 'audio' && ( - - )} - {(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && ( - - )} - {state.filesize ? formatFileSize(state.filesize) : 'unknown'} - - - - {state.vbr && state.abr ? ( - formatBitrate(state.vbr + state.abr) - ) : state.vbr ? ( - formatBitrate(state.vbr) - ) : state.abr ? ( - formatBitrate(state.abr) - ) : ( - 'unknown' - )} - -
-
- {state.playlist_id && state.playlist_index && ( - - Playlist ({state.playlist_index} of {state.playlist_n_entries}) - - )} - {state.vcodec && ( - {formatCodec(state.vcodec)} - )} - {state.acodec && ( - {formatCodec(state.acodec)} - )} - {state.dynamic_range && state.dynamic_range !== 'SDR' && ( - {state.dynamic_range} - )} - {state.subtitle_id && ( - - ESUB - - )} -
-
-
- - - - - - - - - Are you absolutely sure? - - This action cannot be undone! it will permanently remove this from downloads. - -
- {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} /> - -
-
- - Cancel - removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => { - setIsDeleteFileChecked(state.download_id, false); - }) - }>Remove - -
-
+ + + + + + + + Are you absolutely sure? + + This action cannot be undone! it will permanently remove this from downloads. + +
+ {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} /> + +
+
+ + Cancel + removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => { + setIsDeleteFileChecked(state.download_id, false); + }) + }>Remove + +
+
+
+
-
+ ) + }) + ) : ( +
+

Nothing!

+

No Completed Downloads

+

You have not completed any downloads yet. Complete downloading something to see here :)

- ) - }) - ) : ( -
No Completed downloads!
- )} -
+ )} +
+ + +
+ {incompleteDownloads.length > 0 ? ( + incompleteDownloads.map((state) => { + const itemActionStates = downloadActions[state.download_id] || { + isResuming: false, + isPausing: false, + isCanceling: false, + isDeleteFileChecked: false, + }; + return ( +
+
+ + + + {state.ext && ( + + {state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && ( + + )} +
+
+
+

{state.title}

+ {((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && ( + + )} + {(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && ( +
+ {state.progress}% + + { + state.downloaded && state.total + ? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})` + : null + } +
+ )} +
{ state.download_status && ( + `${state.download_status === 'downloading' && state.status === 'finished' ? 'Processing' : state.download_status.charAt(0).toUpperCase() + state.download_status.slice(1)} ${state.download_status === 'downloading' && state.status !== 'finished' && state.speed ? `• Speed: ${formatSpeed(state.speed)}` : ""} ${state.download_status === 'downloading' && state.eta ? `• ETA: ${formatSecToTimeString(state.eta)}` : ""}` + )}
+
+
+ {state.download_status === 'paused' ? ( + + ) : ( + + )} + +
+
+
+ ) + }) + ) : ( +
+

Nothing!

+

No Incomplete Downloads

+

You have all caught up! Sit back and relax or just spin up a new download to see here :)

+
+ )} +
+
+
); } \ No newline at end of file diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 603b211..31e946f 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -560,7 +560,13 @@ export default function SettingsPage() {

Extension Websocket Server

-

{isChangingWebSocketPort || isRestartingWebSocketServer ? 'Restarting...' : 'Running' }

+
+ {isChangingWebSocketPort || isRestartingWebSocketServer ? ( + <>
Restarting... + ) : ( + <>
Running + )} +
diff --git a/src/services/store.ts b/src/services/store.ts index 2f53720..d1e09e4 100644 --- a/src/services/store.ts +++ b/src/services/store.ts @@ -1,4 +1,4 @@ -import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, SettingsPageStatesStore } from '@/types/store'; +import { BasePathsStore, CurrentVideoMetadataStore, DownloadActionStatesStore, DownloaderPageStatesStore, DownloadStatesStore, KvPairsStatesStore, LibraryPageStatesStore, SettingsPageStatesStore } from '@/types/store'; import { create } from 'zustand'; export const useBasePathsStore = create((set) => ({ @@ -58,6 +58,11 @@ export const useDownloaderPageStatesStore = create((s setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index })) })); +export const useLibraryPageStatesStore = create((set) => ({ + activeTab: 'completed', + setActiveTab: (tab) => set(() => ({ activeTab: tab })) +})); + export const useDownloadActionStatesStore = create((set) => ({ downloadActions: {}, setIsResumingDownload: (download_id, isResuming) => set((state) => ({ diff --git a/src/types/store.ts b/src/types/store.ts index 4ff0e01..55c504e 100644 --- a/src/types/store.ts +++ b/src/types/store.ts @@ -47,6 +47,11 @@ export interface DownloaderPageStatesStore { setSelectedPlaylistVideoIndex: (index: string) => void; } +export interface LibraryPageStatesStore { + activeTab: string; + setActiveTab: (tab: string) => void; +} + export interface DownloadActionStatesStore { downloadActions: { [download_id: string]: {