1
1
mirror of https://github.com/neosubhamoy/neodlp.git synced 2026-02-04 14:12:22 +05:30

feat: added clear, copy log buttons and pagination in completed downloads

This commit is contained in:
2026-01-09 23:40:35 +05:30
Verified
parent 01f4e96101
commit 2b7ab9def4
10 changed files with 214 additions and 15 deletions

View File

@@ -309,7 +309,7 @@ export default function App({ children }: { children: React.ReactNode }) {
useEffect(() => { useEffect(() => {
if (isSuccessFetchingDownloadStates && downloadStates) { if (isSuccessFetchingDownloadStates && downloadStates) {
console.log("Download States fetched successfully:", downloadStates); // console.log("Download States fetched successfully:", downloadStates);
setDownloadStates(downloadStates); setDownloadStates(downloadStates);
} }
}, [downloadStates, isSuccessFetchingDownloadStates, setDownloadStates]); }, [downloadStates, isSuccessFetchingDownloadStates, setDownloadStates]);

View File

@@ -0,0 +1,81 @@
import { Paginated } from "@/types/download";
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious } from "@/components/ui/pagination";
export default function PaginationBar({
paginatedData,
setPage,
}: {
paginatedData: Paginated;
setPage: (page: number) => void;
}) {
return (
<Pagination className="mt-4">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setPage(paginatedData.prev_page ?? paginatedData.first_page)}
aria-disabled={!paginatedData.prev_page}
className={!paginatedData.prev_page ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
/>
</PaginationItem>
{paginatedData.pages.map((link, index, array) => {
const currentPage = paginatedData.current_page;
const pageNumber = link.page!;
// Show first page, last page, current page, and 2 pages around current
const showPage =
pageNumber === 1 ||
pageNumber === paginatedData.last_page ||
Math.abs(pageNumber - currentPage) <= 1;
// Show ellipsis if there's a gap
const prevVisiblePage = array
.slice(0, index)
.reverse()
.find((prevLink) => {
const prevPageNum = prevLink.page!;
return (
prevPageNum === 1 ||
prevPageNum === paginatedData.last_page ||
Math.abs(prevPageNum - currentPage) <= 1
);
})?.page;
const showEllipsis = showPage && prevVisiblePage && pageNumber - prevVisiblePage > 1;
if (!showPage) return null;
return (
<div key={link.page} className="contents">
{showEllipsis && (
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
)}
{showPage && (
<PaginationItem>
<PaginationLink
className="cursor-pointer"
onClick={() => setPage(link.page)}
isActive={link.active}
>
{link.label}
</PaginationLink>
</PaginationItem>
)}
</div>
);
})}
<PaginationItem>
<PaginationNext
onClick={() => setPage(paginatedData.next_page ?? paginatedData.last_page)}
aria-disabled={!paginatedData.next_page}
className={!paginatedData.next_page ? 'pointer-events-none opacity-50' : 'cursor-pointer'}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
)
}

View File

@@ -1,15 +1,26 @@
import { useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarTrigger } from "@/components/ui/sidebar";
import { getRouteName } from "@/utils"; import { getRouteName } from "@/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Terminal } from "lucide-react"; import { BrushCleaning, Check, Copy, Terminal } from "lucide-react";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { useLogger } from "@/helpers/use-logger"; import { useLogger } from "@/helpers/use-logger";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
export default function Navbar() { export default function Navbar() {
const [copied, setCopied] = useState(false);
const location = useLocation(); const location = useLocation();
const logs = useLogger().getLogs(); const logger = useLogger();
const logs = logger.getLogs();
const logText = logs.map(log => `${new Date(log.timestamp).toLocaleTimeString()} [${log.level.toUpperCase()}] ${log.context}: ${log.message}`).join('\n');
const handleCopyLogs = async () => {
await writeText(logText);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}
return ( return (
<nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-backdrop-filter:bg-background/60 border-b z-50"> <nav className="flex justify-between items-center py-3 px-4 sticky top-0 backdrop-blur supports-backdrop-filter:bg-background/60 border-b z-50">
@@ -48,6 +59,28 @@ export default function Navbar() {
)) ))
)} )}
</div> </div>
<DialogFooter>
<Button
variant="outline"
disabled={logs.length === 0}
onClick={() => logger.clearLogs()}
>
<BrushCleaning className="size-4" />
Clear Logs
</Button>
<Button
className="transition-all duration-300"
disabled={logs.length === 0}
onClick={() => handleCopyLogs()}
>
{copied ? (
<Check className="size-4" />
) : (
<Copy className="size-4" />
)}
Copy Logs
</Button>
</DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</div> </div>

View File

@@ -1,10 +1,11 @@
import { useEffect } from "react";
import { ProxyImage } from "@/components/custom/proxyImage"; import { ProxyImage } from "@/components/custom/proxyImage";
import { AspectRatio } from "@/components/ui/aspect-ratio"; import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { toast } from "sonner"; import { toast } from "sonner";
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore } from "@/services/store"; import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useLibraryPageStatesStore } from "@/services/store";
import { formatBitrate, formatCodec, formatDurationString, formatFileSize } from "@/utils"; import { formatBitrate, formatCodec, formatDurationString, formatFileSize, paginate } from "@/utils";
import { ArrowUpRightIcon, AudioLines, CircleArrowDown, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Music, Play, Search, Trash2, Video } from "lucide-react"; import { ArrowUpRightIcon, AudioLines, CircleArrowDown, Clock, File, FileAudio2, FileQuestion, FileVideo2, FolderInput, ListVideo, Music, Play, Search, Trash2, Video } 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";
@@ -17,6 +18,7 @@ import { Label } from "@/components/ui/label";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useLogger } from "@/helpers/use-logger"; import { useLogger } from "@/helpers/use-logger";
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty"; import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
import PaginationBar from "@/components/custom/paginationBar";
interface CompletedDownloadProps { interface CompletedDownloadProps {
state: DownloadState; state: DownloadState;
@@ -250,16 +252,35 @@ export function CompletedDownload({ state }: CompletedDownloadProps) {
} }
export function CompletedDownloads({ downloads }: CompletedDownloadsProps) { export function CompletedDownloads({ downloads }: CompletedDownloadsProps) {
const activeCompletedDownloadsPage = useLibraryPageStatesStore(state => state.activeCompletedDownloadsPage);
const setActiveCompletedDownloadsPage = useLibraryPageStatesStore(state => state.setActiveCompletedDownloadsPage);
const navigate = useNavigate(); const navigate = useNavigate();
const paginatedCompletedDownloads = paginate(downloads, activeCompletedDownloadsPage, 5);
// Ensure current page is valid when downloads change
useEffect(() => {
if (downloads.length > 0 && activeCompletedDownloadsPage > paginatedCompletedDownloads.last_page) {
setActiveCompletedDownloadsPage(paginatedCompletedDownloads.last_page);
}
}, [downloads.length, activeCompletedDownloadsPage, paginatedCompletedDownloads.last_page, setActiveCompletedDownloadsPage]);
return ( return (
<div className="w-full flex flex-col gap-2"> <div className="w-full flex flex-col gap-2">
{downloads.length > 0 ? ( {paginatedCompletedDownloads.data.length > 0 ? (
downloads.map((state) => { <>
{paginatedCompletedDownloads.data.map((state) => {
return ( return (
<CompletedDownload key={state.download_id} state={state} /> <CompletedDownload key={state.download_id} state={state} />
); );
}) })}
{paginatedCompletedDownloads.pages.length > 1 && (
<PaginationBar
paginatedData={paginatedCompletedDownloads}
setPage={setActiveCompletedDownloadsPage}
/>
)}
</>
) : ( ) : (
<Empty className="mt-10"> <Empty className="mt-10">
<EmptyHeader> <EmptyHeader>

View File

@@ -88,8 +88,8 @@ export default function useDownloader() {
const updateDownloadState = debounce((state: DownloadState) => { const updateDownloadState = debounce((state: DownloadState) => {
downloadStateSaver.mutate(state, { downloadStateSaver.mutate(state, {
onSuccess: (data) => { onSuccess: (_data) => {
console.log("Download State saved successfully:", data); // console.log("Download State saved successfully:", data);
queryClient.invalidateQueries({ queryKey: ['download-states'] }); queryClient.invalidateQueries({ queryKey: ['download-states'] });
}, },
onError: (error) => { onError: (error) => {

View File

@@ -20,8 +20,7 @@ export default function LibraryPage() {
const { pauseDownload } = useAppContext(); const { pauseDownload } = useAppContext();
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').sort((a, b) => {
.sort((a, b) => {
// Latest updated first // Latest updated first
const dateA = a.updated_at ? new Date(a.updated_at).getTime() : 0; const dateA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
const dateB = b.updated_at ? new Date(b.updated_at).getTime() : 0; const dateB = b.updated_at ? new Date(b.updated_at).getTime() : 0;

View File

@@ -114,7 +114,9 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({ export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
activeTab: 'completed', activeTab: 'completed',
setActiveTab: (tab) => set(() => ({ activeTab: tab })) activeCompletedDownloadsPage: 1,
setActiveTab: (tab) => set(() => ({ activeTab: tab })),
setActiveCompletedDownloadsPage: (page) => set(() => ({ activeCompletedDownloadsPage: page }))
})); }));
export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({ export const useDownloadActionStatesStore = create<DownloadActionStatesStore>((set) => ({

View File

@@ -97,3 +97,21 @@ export interface DownloadProgress {
total: number | null; total: number | null;
eta: number | null; eta: number | null;
} }
export interface Paginated<T = any> {
current_page: number;
from: number;
first_page: number;
last_page: number;
pages: Array<{
label: string;
page: number;
active: boolean;
}>;
next_page: number | null;
per_page: number;
prev_page: number | null;
to: number;
total: number;
data: T[];
}

View File

@@ -71,7 +71,9 @@ export interface DownloaderPageStatesStore {
export interface LibraryPageStatesStore { export interface LibraryPageStatesStore {
activeTab: string; activeTab: string;
activeCompletedDownloadsPage: number;
setActiveTab: (tab: string) => void; setActiveTab: (tab: string) => void;
setActiveCompletedDownloadsPage: (page: number) => void;
} }
export interface DownloadActionStatesStore { export interface DownloadActionStatesStore {

View File

@@ -1,6 +1,6 @@
import { RoutesObj } from "@/types/route"; import { RoutesObj } from "@/types/route";
import { AllRoutes } from "@/routes"; import { AllRoutes } from "@/routes";
import { DownloadProgress } from "@/types/download"; import { DownloadProgress, Paginated } from "@/types/download";
import { VideoFormat } from "@/types/video"; import { VideoFormat } from "@/types/video";
import * as fs from "@tauri-apps/plugin-fs"; import * as fs from "@tauri-apps/plugin-fs";
@@ -402,3 +402,46 @@ export const sortByBitrate = (formats: VideoFormat[] | undefined) => {
return 0; return 0;
}); });
}; };
export const paginate = <T>(items: T[], currentPage: number, itemsPerPage: number): Paginated<T> => {
const total = items.length;
const lastPage = Math.max(1, Math.ceil(total / itemsPerPage));
// Clamp current page to valid range
const validCurrentPage = Math.max(1, Math.min(currentPage, lastPage));
// Calculate start and end indices
const startIndex = (validCurrentPage - 1) * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, total);
// Get paginated data
const data = items.slice(startIndex, endIndex);
// Calculate from/to (1-indexed)
const from = total > 0 ? startIndex + 1 : 0;
const to = total > 0 ? endIndex : 0;
// Generate pages array
const pages: Array<{ label: string; page: number; active: boolean }> = [];
for (let i = 1; i <= lastPage; i++) {
pages.push({
label: i.toString(),
page: i,
active: i === validCurrentPage
});
}
return {
current_page: validCurrentPage,
from,
first_page: 1,
last_page: lastPage,
pages,
next_page: validCurrentPage < lastPage ? validCurrentPage + 1 : null,
per_page: itemsPerPage,
prev_page: validCurrentPage > 1 ? validCurrentPage - 1 : null,
to,
total,
data
};
};