mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2025-12-19 23:39:33 +05:30
(feat): added stop all button in library, improved library layout and other minor ui improvements
This commit is contained in:
@@ -6,9 +6,9 @@ import { Progress } from "@/components/ui/progress";
|
|||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { useAppContext } from "@/providers/appContextProvider";
|
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 { 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 { invoke } from "@tauri-apps/api/core";
|
||||||
import * as fs from "@tauri-apps/plugin-fs";
|
import * as fs from "@tauri-apps/plugin-fs";
|
||||||
import { DownloadState } from "@/types/download";
|
import { DownloadState } from "@/types/download";
|
||||||
@@ -18,9 +18,14 @@ import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
|
|||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import Heading from "@/components/heading";
|
import Heading from "@/components/heading";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
||||||
|
|
||||||
export default function LibraryPage() {
|
export default function LibraryPage() {
|
||||||
|
const activeTab = useLibraryPageStatesStore(state => state.activeTab);
|
||||||
|
const setActiveTab = useLibraryPageStatesStore(state => state.setActiveTab);
|
||||||
|
|
||||||
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
const downloadStates = useDownloadStatesStore(state => state.downloadStates);
|
||||||
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
const downloadActions = useDownloadActionStatesStore(state => state.downloadActions);
|
||||||
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
const setIsResumingDownload = useDownloadActionStatesStore(state => state.setIsResumingDownload);
|
||||||
@@ -36,6 +41,9 @@ export default function LibraryPage() {
|
|||||||
|
|
||||||
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
||||||
const completedDownloads = 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) => {
|
const openFile = async (filePath: string | null, app: string | null) => {
|
||||||
if (filePath && await fs.exists(filePath)) {
|
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 (
|
return (
|
||||||
<div className="container mx-auto p-4 space-y-4">
|
<div className="container mx-auto p-4 space-y-4">
|
||||||
<Heading title="Library" description="Manage all your downloads in one place" />
|
<Heading title="Library" description="Manage all your downloads in one place" />
|
||||||
<div className="w-full fle flex-col">
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
<div className="flex w-full items-center gap-3 mb-2">
|
<div className="w-full flex items-center justify-between mb-4">
|
||||||
<CloudDownload className="size-5" />
|
<TabsList>
|
||||||
<h3 className="text-nowrap font-semibold">Incomplete Downloads</h3>
|
<TabsTrigger value="completed">Completed {completedDownloads.length > 0 && (`(${completedDownloads.length})`)}</TabsTrigger>
|
||||||
|
<TabsTrigger value="incomplete">Incomplete {(incompleteDownloads.length > 0 && ongoingDownloads.length <= 0) && (`(${incompleteDownloads.length})`)} {ongoingDownloads.length > 0 && (<Badge className="h-4 min-w-4 rounded-full px-1 font-mono tabular-nums ml-1">{ongoingDownloads.length}</Badge>)}</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
className="w-fit"
|
||||||
|
variant="destructive"
|
||||||
|
size="sm"
|
||||||
|
disabled={ongoingDownloads.length <= 0}
|
||||||
|
>
|
||||||
|
<Square className="h-4 w-4" />
|
||||||
|
Stop
|
||||||
|
</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Stop all ongoing downloads?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
Are you sure you want to stop all ongoing downloads? This will pause all downloads including the download queue.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => stopOngoingDownloads()}
|
||||||
|
>Stop</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="horizontal" className="" />
|
<TabsContent value="completed">
|
||||||
</div>
|
<div className="w-full flex flex-col gap-2">
|
||||||
<div className="w-full flex flex-col gap-2">
|
{completedDownloads.length > 0 ? (
|
||||||
{incompleteDownloads.length > 0 ? (
|
completedDownloads.map((state) => {
|
||||||
incompleteDownloads.map((state) => {
|
const itemActionStates = downloadActions[state.download_id] || {
|
||||||
const itemActionStates = downloadActions[state.download_id] || {
|
isResuming: false,
|
||||||
isResuming: false,
|
isPausing: false,
|
||||||
isPausing: false,
|
isCanceling: false,
|
||||||
isCanceling: false,
|
isDeleteFileChecked: false,
|
||||||
isDeleteFileChecked: false,
|
};
|
||||||
};
|
return (
|
||||||
return (
|
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
||||||
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
<div className="w-[30%] flex flex-col justify-between gap-2">
|
||||||
<div className="w-[30%] flex flex-col justify-between gap-2">
|
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
||||||
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border">
|
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
||||||
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
</AspectRatio>
|
||||||
</AspectRatio>
|
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
||||||
{state.ext && (
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
<Video className="w-4 h-4 mr-2" />
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
)}
|
||||||
<Video className="w-4 h-4 mr-2" />
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
)}
|
<Music className="w-4 h-4 mr-2" />
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
)}
|
||||||
<Music className="w-4 h-4 mr-2" />
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
)}
|
<File className="w-4 h-4 mr-2" />
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
)}
|
||||||
<File className="w-4 h-4 mr-2" />
|
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
||||||
)}
|
</span>
|
||||||
{state.ext.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
</div>
|
||||||
</span>
|
<div className="w-full flex flex-col justify-between gap-2">
|
||||||
)}
|
<div className="flex flex-col gap-1">
|
||||||
</div>
|
<h4 className="">{state.title}</h4>
|
||||||
<div className="w-full flex flex-col justify-between">
|
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex items-center mt-1">
|
||||||
<h4>{state.title}</h4>
|
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
||||||
{((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && (
|
<Separator orientation="vertical" />
|
||||||
<IndeterminateProgress indeterminate={true} className="w-full" />
|
<span className="text-xs text-muted-foreground flex items-center px-3">
|
||||||
)}
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
{(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && (
|
<FileVideo2 className="w-4 h-4 mr-2"/>
|
||||||
<div className="w-full flex items-center gap-2">
|
)}
|
||||||
<span className="text-sm text-nowrap">{state.progress}%</span>
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
<Progress value={state.progress} />
|
<FileAudio2 className="w-4 h-4 mr-2" />
|
||||||
<span className="text-sm text-nowrap">{
|
)}
|
||||||
state.downloaded && state.total
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})`
|
<FileQuestion className="w-4 h-4 mr-2" />
|
||||||
: null
|
)}
|
||||||
}</span>
|
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
||||||
|
</span>
|
||||||
|
<Separator orientation="vertical" />
|
||||||
|
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
||||||
|
{state.vbr && state.abr ? (
|
||||||
|
formatBitrate(state.vbr + state.abr)
|
||||||
|
) : state.vbr ? (
|
||||||
|
formatBitrate(state.vbr)
|
||||||
|
) : state.abr ? (
|
||||||
|
formatBitrate(state.abr)
|
||||||
|
) : (
|
||||||
|
'unknown'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
||||||
|
{state.playlist_id && state.playlist_index && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
||||||
|
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
||||||
|
>
|
||||||
|
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{state.vcodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.acodec && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
||||||
|
)}
|
||||||
|
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
||||||
|
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
||||||
|
)}
|
||||||
|
{state.subtitle_id && (
|
||||||
|
<span
|
||||||
|
className="border border-border py-1 px-2 rounded cursor-pointer"
|
||||||
|
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
||||||
|
>
|
||||||
|
ESUB
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="w-full flex items-center gap-2">
|
||||||
<div className="text-xs text-muted-foreground">{ state.download_status && (
|
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
||||||
`${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)}` : ""}`
|
<Play className="w-4 h-4" />
|
||||||
)}</div>
|
Open
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center gap-2 mt-2">
|
|
||||||
{state.download_status === 'paused' ? (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="w-fill"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsResumingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await resumeDownload(state)
|
|
||||||
// toast({
|
|
||||||
// title: 'Resumed Download',
|
|
||||||
// description: 'Download resumed, it will re-start shortly.',
|
|
||||||
// })
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title: 'Failed to Resume Download',
|
|
||||||
description: 'An error occurred while trying to resume the download.',
|
|
||||||
variant: "destructive"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsResumingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isResuming || itemActionStates.isCanceling}
|
|
||||||
>
|
|
||||||
{itemActionStates.isResuming ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Resuming
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
Resume
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="w-fill"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsPausingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await pauseDownload(state)
|
|
||||||
// toast({
|
|
||||||
// title: 'Paused Download',
|
|
||||||
// description: 'Download paused successfully.',
|
|
||||||
// })
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title: 'Failed to Pause Download',
|
|
||||||
description: 'An error occurred while trying to pause the download.',
|
|
||||||
variant: "destructive"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsPausingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isPausing || itemActionStates.isCanceling || state.download_status !== 'downloading' || (state.download_status === 'downloading' && state.status === 'finished')}
|
|
||||||
>
|
|
||||||
{itemActionStates.isPausing ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Pausing
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Pause className="w-4 h-4" />
|
|
||||||
Pause
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="destructive"
|
|
||||||
onClick={async () => {
|
|
||||||
setIsCancelingDownload(state.download_id, true);
|
|
||||||
try {
|
|
||||||
await cancelDownload(state)
|
|
||||||
toast({
|
|
||||||
title: 'Canceled Download',
|
|
||||||
description: 'Download canceled successfully.',
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
toast({
|
|
||||||
title: 'Failed to Cancel Download',
|
|
||||||
description: 'An error occurred while trying to cancel the download.',
|
|
||||||
variant: "destructive"
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setIsCancelingDownload(state.download_id, false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={itemActionStates.isCanceling || itemActionStates.isResuming || itemActionStates.isPausing || state.download_status === 'starting' || (state.download_status === 'downloading' && state.status === 'finished')}
|
|
||||||
>
|
|
||||||
{itemActionStates.isCanceling ? (
|
|
||||||
<>
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
Canceling
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
Cancel
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
) : (
|
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Incomplete downloads!</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="w-full fle flex-col">
|
|
||||||
<div className="flex w-full items-center gap-3 mb-2">
|
|
||||||
<PackageCheck className="size-5" />
|
|
||||||
<h3 className="text-nowrap font-semibold">Completed Downloads</h3>
|
|
||||||
</div>
|
|
||||||
<Separator orientation="horizontal" className="" />
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col gap-2">
|
|
||||||
{completedDownloads.length > 0 ? (
|
|
||||||
completedDownloads.map((state) => {
|
|
||||||
const itemActionStates = downloadActions[state.download_id] || {
|
|
||||||
isResuming: false,
|
|
||||||
isPausing: false,
|
|
||||||
isCanceling: false,
|
|
||||||
isDeleteFileChecked: false,
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
|
||||||
<div className="w-[30%] flex flex-col justify-between gap-2">
|
|
||||||
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border mb-2">
|
|
||||||
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
|
||||||
</AspectRatio>
|
|
||||||
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<Video className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<Music className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<File className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.ext?.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col justify-between gap-2">
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<h4 className="">{state.title}</h4>
|
|
||||||
<p className="text-xs text-muted-foreground">{state.channel ? state.channel : 'unknown'} {state.host ? `• ${state.host}` : 'unknown'}</p>
|
|
||||||
<div className="flex items-center mt-1">
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pr-3"><Clock className="w-4 h-4 mr-2"/> {state.duration_string ? formatDurationString(state.duration_string) : 'unknown'}</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center px-3">
|
|
||||||
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
|
||||||
<FileVideo2 className="w-4 h-4 mr-2"/>
|
|
||||||
)}
|
|
||||||
{state.filetype && state.filetype === 'audio' && (
|
|
||||||
<FileAudio2 className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
|
||||||
<FileQuestion className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
{state.filesize ? formatFileSize(state.filesize) : 'unknown'}
|
|
||||||
</span>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<span className="text-xs text-muted-foreground flex items-center pl-3"><AudioLines className="w-4 h-4 mr-2"/>
|
|
||||||
{state.vbr && state.abr ? (
|
|
||||||
formatBitrate(state.vbr + state.abr)
|
|
||||||
) : state.vbr ? (
|
|
||||||
formatBitrate(state.vbr)
|
|
||||||
) : state.abr ? (
|
|
||||||
formatBitrate(state.abr)
|
|
||||||
) : (
|
|
||||||
'unknown'
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="hidden xl:flex items-center mt-1 gap-2 flex-wrap text-xs">
|
|
||||||
{state.playlist_id && state.playlist_index && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded flex items-center cursor-pointer"
|
|
||||||
title={`${state.playlist_title ?? 'UNKNOWN PLAYLIST'}` + ' by ' + `${state.playlist_channel ?? 'UNKNOWN CHANNEL'}`}
|
|
||||||
>
|
|
||||||
<ListVideo className="w-4 h-4 mr-2" /> Playlist ({state.playlist_index} of {state.playlist_n_entries})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{state.vcodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.vcodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.acodec && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{formatCodec(state.acodec)}</span>
|
|
||||||
)}
|
|
||||||
{state.dynamic_range && state.dynamic_range !== 'SDR' && (
|
|
||||||
<span className="border border-border py-1 px-2 rounded">{state.dynamic_range}</span>
|
|
||||||
)}
|
|
||||||
{state.subtitle_id && (
|
|
||||||
<span
|
|
||||||
className="border border-border py-1 px-2 rounded cursor-pointer"
|
|
||||||
title={`EMBEDED SUBTITLE (${state.subtitle_id})`}
|
|
||||||
>
|
|
||||||
ESUB
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex items-center gap-2">
|
|
||||||
<Button size="sm" onClick={() => openFile(state.filepath, null)}>
|
|
||||||
<Play className="w-4 h-4" />
|
|
||||||
Open
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
|
||||||
<FolderInput className="w-4 h-4" />
|
|
||||||
Open in Explorer
|
|
||||||
</Button>
|
|
||||||
<AlertDialog>
|
|
||||||
<AlertDialogTrigger asChild>
|
|
||||||
<Button size="sm" variant="destructive">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
Remove
|
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
<Button size="sm" variant="outline" onClick={() => openFile(state.filepath, 'explorer')}>
|
||||||
<AlertDialogContent>
|
<FolderInput className="w-4 h-4" />
|
||||||
<AlertDialogHeader>
|
Open in Explorer
|
||||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
</Button>
|
||||||
<AlertDialogDescription>
|
<AlertDialog>
|
||||||
This action cannot be undone! it will permanently remove this from downloads.
|
<AlertDialogTrigger asChild>
|
||||||
</AlertDialogDescription>
|
<Button size="sm" variant="destructive">
|
||||||
<div className="flex items-center space-x-2">
|
<Trash2 className="w-4 h-4" />
|
||||||
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
Remove
|
||||||
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
</Button>
|
||||||
</div>
|
</AlertDialogTrigger>
|
||||||
</AlertDialogHeader>
|
<AlertDialogContent>
|
||||||
<AlertDialogFooter>
|
<AlertDialogHeader>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||||
<AlertDialogAction onClick={
|
<AlertDialogDescription>
|
||||||
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
This action cannot be undone! it will permanently remove this from downloads.
|
||||||
setIsDeleteFileChecked(state.download_id, false);
|
</AlertDialogDescription>
|
||||||
})
|
<div className="flex items-center space-x-2">
|
||||||
}>Remove</AlertDialogAction>
|
<Checkbox id="delete-file" checked={itemActionStates.isDeleteFileChecked} onCheckedChange={() => {setIsDeleteFileChecked(state.download_id, !itemActionStates.isDeleteFileChecked)}} />
|
||||||
</AlertDialogFooter>
|
<Label htmlFor="delete-file">Delete the downloaded file</Label>
|
||||||
</AlertDialogContent>
|
</div>
|
||||||
</AlertDialog>
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={
|
||||||
|
() => removeFromDownloads(state, itemActionStates.isDeleteFileChecked).then(() => {
|
||||||
|
setIsDeleteFileChecked(state.download_id, false);
|
||||||
|
})
|
||||||
|
}>Remove</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
|
<p className="text-lg font-semibold text-muted-foreground/50">No Completed Downloads</p>
|
||||||
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have not completed any downloads yet. Complete downloading something to see here :)</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
})
|
</div>
|
||||||
) : (
|
</TabsContent>
|
||||||
<div className="w-full flex items-center justify-center text-muted-foreground text-sm">No Completed downloads!</div>
|
<TabsContent value="incomplete">
|
||||||
)}
|
<div className="w-full flex flex-col gap-2">
|
||||||
</div>
|
{incompleteDownloads.length > 0 ? (
|
||||||
|
incompleteDownloads.map((state) => {
|
||||||
|
const itemActionStates = downloadActions[state.download_id] || {
|
||||||
|
isResuming: false,
|
||||||
|
isPausing: false,
|
||||||
|
isCanceling: false,
|
||||||
|
isDeleteFileChecked: false,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="p-4 border border-border rounded-lg flex gap-4" key={state.download_id}>
|
||||||
|
<div className="w-[30%] flex flex-col justify-between gap-2">
|
||||||
|
<AspectRatio ratio={16 / 9} className="w-full rounded-lg overflow-hidden border border-border">
|
||||||
|
<ProxyImage src={state.thumbnail || ""} alt="thumbnail" className="" />
|
||||||
|
</AspectRatio>
|
||||||
|
{state.ext && (
|
||||||
|
<span className="w-full flex items-center justify-center text-xs border border-border py-1 px-2 rounded">
|
||||||
|
{state.filetype && (state.filetype === 'video' || state.filetype === 'video+audio') && (
|
||||||
|
<Video className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.filetype && state.filetype === 'audio' && (
|
||||||
|
<Music className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{(!state.filetype) || (state.filetype && state.filetype !== 'video' && state.filetype !== 'audio' && state.filetype !== 'video+audio') && (
|
||||||
|
<File className="w-4 h-4 mr-2" />
|
||||||
|
)}
|
||||||
|
{state.ext.toUpperCase()} {state.resolution ? `(${state.resolution})` : null}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex flex-col justify-between">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<h4>{state.title}</h4>
|
||||||
|
{((state.download_status === 'starting') || (state.download_status === 'downloading' && state.status === 'finished')) && (
|
||||||
|
<IndeterminateProgress indeterminate={true} className="w-full" />
|
||||||
|
)}
|
||||||
|
{(state.download_status === 'downloading' || state.download_status === 'paused') && state.progress && state.status !== 'finished' && (
|
||||||
|
<div className="w-full flex items-center gap-2">
|
||||||
|
<span className="text-sm text-nowrap">{state.progress}%</span>
|
||||||
|
<Progress value={state.progress} />
|
||||||
|
<span className="text-sm text-nowrap">{
|
||||||
|
state.downloaded && state.total
|
||||||
|
? `(${formatFileSize(state.downloaded)} / ${formatFileSize(state.total)})`
|
||||||
|
: null
|
||||||
|
}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-muted-foreground">{ 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)}` : ""}`
|
||||||
|
)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex items-center gap-2 mt-2">
|
||||||
|
{state.download_status === 'paused' ? (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-fill"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsResumingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await resumeDownload(state)
|
||||||
|
// toast({
|
||||||
|
// title: 'Resumed Download',
|
||||||
|
// description: 'Download resumed, it will re-start shortly.',
|
||||||
|
// })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to Resume Download',
|
||||||
|
description: 'An error occurred while trying to resume the download.',
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsResumingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isResuming || itemActionStates.isCanceling}
|
||||||
|
>
|
||||||
|
{itemActionStates.isResuming ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Resuming
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Play className="w-4 h-4" />
|
||||||
|
Resume
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-fill"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsPausingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await pauseDownload(state)
|
||||||
|
// toast({
|
||||||
|
// title: 'Paused Download',
|
||||||
|
// description: 'Download paused successfully.',
|
||||||
|
// })
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to Pause Download',
|
||||||
|
description: 'An error occurred while trying to pause the download.',
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsPausingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isPausing || itemActionStates.isCanceling || state.download_status !== 'downloading' || (state.download_status === 'downloading' && state.status === 'finished')}
|
||||||
|
>
|
||||||
|
{itemActionStates.isPausing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Pausing
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Pause className="w-4 h-4" />
|
||||||
|
Pause
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={async () => {
|
||||||
|
setIsCancelingDownload(state.download_id, true);
|
||||||
|
try {
|
||||||
|
await cancelDownload(state)
|
||||||
|
toast({
|
||||||
|
title: 'Canceled Download',
|
||||||
|
description: 'Download canceled successfully.',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast({
|
||||||
|
title: 'Failed to Cancel Download',
|
||||||
|
description: 'An error occurred while trying to cancel the download.',
|
||||||
|
variant: "destructive"
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
setIsCancelingDownload(state.download_id, false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={itemActionStates.isCanceling || itemActionStates.isResuming || itemActionStates.isPausing || state.download_status === 'starting' || (state.download_status === 'downloading' && state.status === 'finished')}
|
||||||
|
>
|
||||||
|
{itemActionStates.isCanceling ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Canceling
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
Cancel
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex flex-col items-center gap-2 justify-center mt-27">
|
||||||
|
<h4 className="text-4xl font-bold text-muted-foreground/80 dark:text-muted">Nothing!</h4>
|
||||||
|
<p className="text-lg font-semibold text-muted-foreground/50">No Incomplete Downloads</p>
|
||||||
|
<p className="max-w-[50%] text-center text-xs text-muted-foreground/70">You have all caught up! Sit back and relax or just spin up a new download to see here :)</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -560,7 +560,13 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h3 className="">Extension Websocket Server</h3>
|
<h3 className="">Extension Websocket Server</h3>
|
||||||
<p className="text-xs text-muted-foreground">{isChangingWebSocketPort || isRestartingWebSocketServer ? 'Restarting...' : 'Running' }</p>
|
<div className="text-xs flex items-center">
|
||||||
|
{isChangingWebSocketPort || isRestartingWebSocketServer ? (
|
||||||
|
<><div className="h-1.5 w-1.5 rounded-full bg-amber-600 dark:bg-amber-500 mr-1.5 mt-0.5" /><span className="text-amber-600 dark:text-amber-500">Restarting...</span></>
|
||||||
|
) : (
|
||||||
|
<><div className="h-1.5 w-1.5 rounded-full bg-emerald-600 dark:bg-emerald-500 mr-1.5 mt-0.5" /><span className="text-emerald-600 dark:text-emerald-500">Running</span></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center">
|
<div className="flex gap-4 items-center">
|
||||||
|
|||||||
@@ -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';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
export const useBasePathsStore = create<BasePathsStore>((set) => ({
|
||||||
@@ -58,6 +58,11 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
|
|||||||
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
setSelectedPlaylistVideoIndex: (index) => set(() => ({ selectedPlaylistVideoIndex: index }))
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||||
|
activeTab: 'completed',
|
||||||
|
setActiveTab: (tab) => set(() => ({ activeTab: tab }))
|
||||||
|
}));
|
||||||
|
|
||||||
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({
|
||||||
downloadActions: {},
|
downloadActions: {},
|
||||||
setIsResumingDownload: (download_id, isResuming) => set((state) => ({
|
setIsResumingDownload: (download_id, isResuming) => set((state) => ({
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ export interface DownloaderPageStatesStore {
|
|||||||
setSelectedPlaylistVideoIndex: (index: string) => void;
|
setSelectedPlaylistVideoIndex: (index: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LibraryPageStatesStore {
|
||||||
|
activeTab: string;
|
||||||
|
setActiveTab: (tab: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DownloadActionStatesStore {
|
export interface DownloadActionStatesStore {
|
||||||
downloadActions: {
|
downloadActions: {
|
||||||
[download_id: string]: {
|
[download_id: string]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user