mirror of
https://github.com/neosubhamoy/neodlp.git
synced 2026-02-04 11:52:23 +05:30
feat: added clear, copy log buttons and pagination in completed downloads
This commit is contained in:
@@ -309,7 +309,7 @@ export default function App({ children }: { children: React.ReactNode }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccessFetchingDownloadStates && downloadStates) {
|
||||
console.log("Download States fetched successfully:", downloadStates);
|
||||
// console.log("Download States fetched successfully:", downloadStates);
|
||||
setDownloadStates(downloadStates);
|
||||
}
|
||||
}, [downloadStates, isSuccessFetchingDownloadStates, setDownloadStates]);
|
||||
|
||||
81
src/components/custom/paginationBar.tsx
Normal file
81
src/components/custom/paginationBar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +1,26 @@
|
||||
import { useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { getRouteName } from "@/utils";
|
||||
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 { 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 { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
|
||||
export default function Navbar() {
|
||||
const [copied, setCopied] = useState(false);
|
||||
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 (
|
||||
<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>
|
||||
<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>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { ProxyImage } from "@/components/custom/proxyImage";
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { toast } from "sonner";
|
||||
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore } from "@/services/store";
|
||||
import { formatBitrate, formatCodec, formatDurationString, formatFileSize } from "@/utils";
|
||||
import { useCurrentVideoMetadataStore, useDownloadActionStatesStore, useLibraryPageStatesStore } from "@/services/store";
|
||||
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 { invoke } from "@tauri-apps/api/core";
|
||||
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 { useLogger } from "@/helpers/use-logger";
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty";
|
||||
import PaginationBar from "@/components/custom/paginationBar";
|
||||
|
||||
interface CompletedDownloadProps {
|
||||
state: DownloadState;
|
||||
@@ -250,16 +252,35 @@ export function CompletedDownload({ state }: CompletedDownloadProps) {
|
||||
}
|
||||
|
||||
export function CompletedDownloads({ downloads }: CompletedDownloadsProps) {
|
||||
const activeCompletedDownloadsPage = useLibraryPageStatesStore(state => state.activeCompletedDownloadsPage);
|
||||
const setActiveCompletedDownloadsPage = useLibraryPageStatesStore(state => state.setActiveCompletedDownloadsPage);
|
||||
|
||||
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 (
|
||||
<div className="w-full flex flex-col gap-2">
|
||||
{downloads.length > 0 ? (
|
||||
downloads.map((state) => {
|
||||
{paginatedCompletedDownloads.data.length > 0 ? (
|
||||
<>
|
||||
{paginatedCompletedDownloads.data.map((state) => {
|
||||
return (
|
||||
<CompletedDownload key={state.download_id} state={state} />
|
||||
);
|
||||
})
|
||||
})}
|
||||
{paginatedCompletedDownloads.pages.length > 1 && (
|
||||
<PaginationBar
|
||||
paginatedData={paginatedCompletedDownloads}
|
||||
setPage={setActiveCompletedDownloadsPage}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Empty className="mt-10">
|
||||
<EmptyHeader>
|
||||
|
||||
@@ -88,8 +88,8 @@ export default function useDownloader() {
|
||||
|
||||
const updateDownloadState = debounce((state: DownloadState) => {
|
||||
downloadStateSaver.mutate(state, {
|
||||
onSuccess: (data) => {
|
||||
console.log("Download State saved successfully:", data);
|
||||
onSuccess: (_data) => {
|
||||
// console.log("Download State saved successfully:", data);
|
||||
queryClient.invalidateQueries({ queryKey: ['download-states'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
|
||||
@@ -20,8 +20,7 @@ export default function LibraryPage() {
|
||||
const { pauseDownload } = useAppContext();
|
||||
|
||||
const incompleteDownloads = downloadStates.filter(state => state.download_status !== 'completed');
|
||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed')
|
||||
.sort((a, b) => {
|
||||
const completedDownloads = downloadStates.filter(state => state.download_status === 'completed').sort((a, b) => {
|
||||
// Latest updated first
|
||||
const dateA = a.updated_at ? new Date(a.updated_at).getTime() : 0;
|
||||
const dateB = b.updated_at ? new Date(b.updated_at).getTime() : 0;
|
||||
|
||||
@@ -114,7 +114,9 @@ export const useDownloaderPageStatesStore = create<DownloaderPageStatesStore>((s
|
||||
|
||||
export const useLibraryPageStatesStore = create<LibraryPageStatesStore>((set) => ({
|
||||
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) => ({
|
||||
|
||||
@@ -97,3 +97,21 @@ export interface DownloadProgress {
|
||||
total: 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[];
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ export interface DownloaderPageStatesStore {
|
||||
|
||||
export interface LibraryPageStatesStore {
|
||||
activeTab: string;
|
||||
activeCompletedDownloadsPage: number;
|
||||
setActiveTab: (tab: string) => void;
|
||||
setActiveCompletedDownloadsPage: (page: number) => void;
|
||||
}
|
||||
|
||||
export interface DownloadActionStatesStore {
|
||||
|
||||
45
src/utils.ts
45
src/utils.ts
@@ -1,6 +1,6 @@
|
||||
import { RoutesObj } from "@/types/route";
|
||||
import { AllRoutes } from "@/routes";
|
||||
import { DownloadProgress } from "@/types/download";
|
||||
import { DownloadProgress, Paginated } from "@/types/download";
|
||||
import { VideoFormat } from "@/types/video";
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
|
||||
@@ -402,3 +402,46 @@ export const sortByBitrate = (formats: VideoFormat[] | undefined) => {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user