improve style

This commit is contained in:
Mustafa Soylu
2025-06-11 19:00:55 +02:00
parent 6aa1c08895
commit 0ec2583be7

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo, useCallback } from "react";
import apiClient from "../lib/api-client"; import apiClient from "../lib/api-client";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -15,6 +15,7 @@ type HistoryEntry = {
task_id: string; task_id: string;
item_name: string; item_name: string;
item_artist: string; item_artist: string;
item_url?: string;
download_type: "track" | "album" | "playlist" | "artist"; download_type: "track" | "album" | "playlist" | "artist";
service_used: string; service_used: string;
quality_profile: string; quality_profile: string;
@@ -30,6 +31,50 @@ type HistoryEntry = {
total_failed?: number; total_failed?: number;
}; };
const STATUS_CLASS: Record<string, string> = {
COMPLETED: "text-green-500",
ERROR: "text-red-500",
CANCELLED: "text-gray-500",
SKIPPED: "text-yellow-500",
};
const QUALITY_MAP: Record<string, Record<string, string>> = {
spotify: {
NORMAL: "OGG 96k",
HIGH: "OGG 160k",
VERY_HIGH: "OGG 320k",
},
deezer: {
MP3_128: "MP3 128k",
MP3_320: "MP3 320k",
FLAC: "FLAC (Hi-Res)",
},
};
const getDownloadSource = (entry: HistoryEntry): "Spotify" | "Deezer" | "Unknown" => {
const url = entry.item_url?.toLowerCase() || "";
const service = entry.service_used?.toLowerCase() || "";
if (url.includes("spotify.com")) return "Spotify";
if (url.includes("deezer.com")) return "Deezer";
if (service.includes("spotify")) return "Spotify";
if (service.includes("deezer")) return "Deezer";
return "Unknown";
};
const formatQuality = (entry: HistoryEntry): string => {
const sourceName = getDownloadSource(entry).toLowerCase();
const profile = entry.quality_profile || "N/A";
const sourceQuality = sourceName !== "unknown" ? QUALITY_MAP[sourceName]?.[profile] || profile : profile;
let qualityDisplay = sourceQuality;
if (entry.convert_to && entry.convert_to !== "None") {
qualityDisplay += `${entry.convert_to.toUpperCase()}`;
if (entry.bitrate && entry.bitrate !== "None") {
qualityDisplay += ` ${entry.bitrate}`;
}
}
return qualityDisplay;
};
// --- Column Definitions --- // --- Column Definitions ---
const columnHelper = createColumnHelper<HistoryEntry>(); const columnHelper = createColumnHelper<HistoryEntry>();
@@ -49,14 +94,23 @@ export const History = () => {
const [statusFilter, setStatusFilter] = useState(""); const [statusFilter, setStatusFilter] = useState("");
const [typeFilter, setTypeFilter] = useState(""); const [typeFilter, setTypeFilter] = useState("");
const [trackStatusFilter, setTrackStatusFilter] = useState(""); const [trackStatusFilter, setTrackStatusFilter] = useState("");
const [hideChildTracks, setHideChildTracks] = useState(true); const [showChildTracks, setShowChildTracks] = useState(false);
const [parentTaskId, setParentTaskId] = useState<string | null>(null); const [parentTaskId, setParentTaskId] = useState<string | null>(null);
const [parentTask, setParentTask] = useState<HistoryEntry | null>(null);
const pagination = useMemo(() => ({ pageIndex, pageSize }), [pageIndex, pageSize]); const pagination = useMemo(() => ({ pageIndex, pageSize }), [pageIndex, pageSize]);
const viewTracksForParent = (taskId: string) => { const viewTracksForParent = useCallback(
setParentTaskId(taskId); (parentEntry: HistoryEntry) => {
}; setPagination({ pageIndex: 0, pageSize });
setParentTaskId(parentEntry.task_id);
setParentTask(parentEntry);
setStatusFilter("");
setTypeFilter("");
setTrackStatusFilter("");
},
[pageSize],
);
const columns = useMemo( const columns = useMemo(
() => [ () => [
@@ -64,7 +118,7 @@ export const History = () => {
header: "Name", header: "Name",
cell: (info) => cell: (info) =>
info.row.original.parent_task_id ? ( info.row.original.parent_task_id ? (
<span className="pl-4 text-gray-400"> {info.getValue()}</span> <span className="pl-8 text-muted-foreground"> {info.getValue()}</span>
) : ( ) : (
<span className="font-semibold">{info.getValue()}</span> <span className="font-semibold">{info.getValue()}</span>
), ),
@@ -72,105 +126,83 @@ export const History = () => {
columnHelper.accessor("item_artist", { header: "Artist" }), columnHelper.accessor("item_artist", { header: "Artist" }),
columnHelper.accessor("download_type", { columnHelper.accessor("download_type", {
header: "Type", header: "Type",
cell: (info) => { cell: (info) => <span className="capitalize">{info.getValue()}</span>,
const entry = info.row.original;
if (entry.parent_task_id && entry.track_status) {
const statusClass = {
SUCCESSFUL: "text-green-500",
SKIPPED: "text-yellow-500",
FAILED: "text-red-500",
}[entry.track_status];
return (
<span className={`capitalize font-semibold ${statusClass}`}>{entry.track_status.toLowerCase()}</span>
);
}
return <span className="capitalize">{info.getValue()}</span>;
},
}), }),
columnHelper.accessor("quality_profile", { columnHelper.accessor("quality_profile", {
header: "Quality", header: "Quality",
cell: (info) => { cell: (info) => formatQuality(info.row.original),
const entry = info.row.original;
let qualityDisplay = entry.quality_profile || "N/A";
if (entry.convert_to && entry.convert_to !== "None") {
qualityDisplay = `${entry.convert_to.toUpperCase()}`;
if (entry.bitrate && entry.bitrate !== "None") {
qualityDisplay += ` ${entry.bitrate}k`;
}
qualityDisplay += ` (${entry.quality_profile || "Original"})`;
} else if (entry.bitrate && entry.bitrate !== "None") {
qualityDisplay = `${entry.bitrate}k (${entry.quality_profile || "Profile"})`;
}
return qualityDisplay;
},
}), }),
columnHelper.accessor("status_final", { columnHelper.accessor("status_final", {
header: "Status", header: "Status",
cell: (info) => { cell: (info) => {
const status = info.getValue(); const entry = info.row.original;
const statusClass = { const status = entry.parent_task_id ? entry.track_status : entry.status_final;
COMPLETED: "text-green-500", const statusKey = (status || "").toUpperCase();
ERROR: "text-red-500", const statusClass =
CANCELLED: "text-gray-500", {
SKIPPED: "text-yellow-500", COMPLETED: "text-green-500",
}[status]; SUCCESSFUL: "text-green-500",
ERROR: "text-red-500",
FAILED: "text-red-500",
CANCELLED: "text-gray-500",
SKIPPED: "text-yellow-500",
}[statusKey] || "text-gray-500";
return <span className={`font-semibold ${statusClass}`}>{status}</span>; return <span className={`font-semibold ${statusClass}`}>{status}</span>;
}, },
}), }),
columnHelper.accessor("item_url", {
id: "source",
header: parentTaskId ? "Download Source" : "Search Source",
cell: (info) => getDownloadSource(info.row.original),
}),
columnHelper.accessor("timestamp_completed", { columnHelper.accessor("timestamp_completed", {
header: "Date Completed", header: "Date Completed",
cell: (info) => new Date(info.getValue() * 1000).toLocaleString(), cell: (info) => new Date(info.getValue() * 1000).toLocaleString(),
}), }),
columnHelper.accessor("error_message", { ...(!parentTaskId
header: "Details", ? [
cell: (info) => columnHelper.display({
info.getValue() ? ( id: "actions",
<button header: "Actions",
onClick={() => cell: ({ row }) => {
toast.info("Error Details", { const entry = row.original;
description: info.getValue(), if (!entry.parent_task_id && (entry.download_type === "album" || entry.download_type === "playlist")) {
}) const hasChildren =
} (entry.total_successful ?? 0) > 0 ||
className="text-blue-500 hover:underline" (entry.total_skipped ?? 0) > 0 ||
> (entry.total_failed ?? 0) > 0;
Show Error if (hasChildren) {
</button> return (
) : null, <div className="flex items-center gap-2">
}), <button
columnHelper.display({ onClick={() => viewTracksForParent(row.original)}
id: "actions", className="px-2 py-1 text-xs rounded-md bg-blue-600 text-white hover:bg-blue-700"
header: "Actions", >
cell: ({ row }) => { View Tracks
const entry = row.original; </button>
if (!entry.parent_task_id && (entry.download_type === "album" || entry.download_type === "playlist")) { <span className="text-xs">
const hasChildren = <span className="text-green-500">{entry.total_successful ?? 0}</span> /{" "}
(entry.total_successful ?? 0) > 0 || (entry.total_skipped ?? 0) > 0 || (entry.total_failed ?? 0) > 0; <span className="text-yellow-500">{entry.total_skipped ?? 0}</span> /{" "}
if (hasChildren) { <span className="text-red-500">{entry.total_failed ?? 0}</span>
return ( </span>
<div className="flex items-center gap-2"> </div>
<button onClick={() => viewTracksForParent(entry.task_id)} className="text-blue-500 hover:underline"> );
View Tracks }
</button> }
<span className="text-xs"> return null;
<span className="text-green-500">{entry.total_successful ?? 0}</span> /{" "} },
<span className="text-yellow-500">{entry.total_skipped ?? 0}</span> /{" "} }),
<span className="text-red-500">{entry.total_failed ?? 0}</span> ]
</span> : []),
</div>
);
}
}
return null;
},
}),
], ],
[], [viewTracksForParent, parentTaskId],
); );
useEffect(() => { useEffect(() => {
const fetchHistory = async () => { const fetchHistory = async () => {
setIsLoading(true); setIsLoading(true);
setData([]);
try { try {
const params = new URLSearchParams({ const params = new URLSearchParams({
limit: `${pageSize}`, limit: `${pageSize}`,
@@ -181,15 +213,57 @@ export const History = () => {
if (statusFilter) params.append("status_final", statusFilter); if (statusFilter) params.append("status_final", statusFilter);
if (typeFilter) params.append("download_type", typeFilter); if (typeFilter) params.append("download_type", typeFilter);
if (trackStatusFilter) params.append("track_status", trackStatusFilter); if (trackStatusFilter) params.append("track_status", trackStatusFilter);
if (hideChildTracks) params.append("hide_child_tracks", "true"); if (!parentTaskId && !showChildTracks) {
params.append("hide_child_tracks", "true");
}
if (parentTaskId) params.append("parent_task_id", parentTaskId); if (parentTaskId) params.append("parent_task_id", parentTaskId);
const response = await apiClient.get<{ const response = await apiClient.get<{
entries: HistoryEntry[]; entries: HistoryEntry[];
total_count: number; total_count: number;
}>(`/history?${params.toString()}`); }>(`/history?${params.toString()}`);
setData(response.data.entries);
setTotalEntries(response.data.total_count); const originalEntries = response.data.entries;
let processedEntries = originalEntries;
// If including child tracks in the main history, group them with their parents
if (showChildTracks && !parentTaskId) {
const parents = originalEntries.filter((e) => !e.parent_task_id);
const childrenByParentId = originalEntries
.filter((e) => e.parent_task_id)
.reduce(
(acc, child) => {
const parentId = child.parent_task_id!;
if (!acc[parentId]) {
acc[parentId] = [];
}
acc[parentId].push(child);
return acc;
},
{} as Record<string, HistoryEntry[]>,
);
const groupedEntries: HistoryEntry[] = [];
parents.forEach((parent) => {
groupedEntries.push(parent);
const children = childrenByParentId[parent.task_id];
if (children) {
groupedEntries.push(...children);
}
});
processedEntries = groupedEntries;
}
// If viewing child tracks for a specific parent, filter out the parent entry from the list
const finalEntries = parentTaskId
? processedEntries.filter((entry) => entry.task_id !== parentTaskId)
: processedEntries;
setData(finalEntries);
// Adjust total count to reflect filtered entries for accurate pagination
const numFiltered = originalEntries.length - finalEntries.length;
setTotalEntries(response.data.total_count - numFiltered);
} catch { } catch {
toast.error("Failed to load history."); toast.error("Failed to load history.");
} finally { } finally {
@@ -197,7 +271,7 @@ export const History = () => {
} }
}; };
fetchHistory(); fetchHistory();
}, [pageIndex, pageSize, sorting, statusFilter, typeFilter, trackStatusFilter, hideChildTracks, parentTaskId]); }, [pageIndex, pageSize, sorting, statusFilter, typeFilter, trackStatusFilter, showChildTracks, parentTaskId]);
const table = useReactTable({ const table = useReactTable({
data, data,
@@ -216,62 +290,105 @@ export const History = () => {
setStatusFilter(""); setStatusFilter("");
setTypeFilter(""); setTypeFilter("");
setTrackStatusFilter(""); setTrackStatusFilter("");
setHideChildTracks(true); setShowChildTracks(false);
}; };
const viewParentTask = () => { const viewParentTask = () => {
setPagination({ pageIndex: 0, pageSize });
setParentTaskId(null); setParentTaskId(null);
setParentTask(null);
clearFilters(); clearFilters();
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-3xl font-bold">Download History</h1> {parentTaskId && parentTask ? (
{parentTaskId && ( <div className="space-y-4">
<button onClick={viewParentTask} className="text-blue-500 hover:underline"> <button onClick={viewParentTask} className="flex items-center gap-2 text-sm hover:underline">
&larr; Back to All History &larr; Back to All History
</button> </button>
<div className="rounded-lg border bg-gradient-to-br from-card to-muted/30 p-6 shadow-lg">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 space-y-1.5">
<h2 className="text-3xl font-bold tracking-tight">{parentTask.item_name}</h2>
<p className="text-xl text-muted-foreground">{parentTask.item_artist}</p>
<div className="pt-2">
<span className="capitalize inline-flex items-center px-3 py-1 rounded-full text-sm font-semibold bg-secondary text-secondary-foreground">
{parentTask.download_type}
</span>
</div>
</div>
<div className="space-y-2 text-sm md:text-right">
<div
className={`inline-flex items-center rounded-full border px-3 py-1 text-base font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${
STATUS_CLASS[parentTask.status_final]
}`}
>
{parentTask.status_final}
</div>
<p className="text-muted-foreground pt-2">
<span className="font-semibold text-foreground">Quality: </span>
{formatQuality(parentTask)}
</p>
<p className="text-muted-foreground">
<span className="font-semibold text-foreground">Completed: </span>
{new Date(parentTask.timestamp_completed * 1000).toLocaleString()}
</p>
</div>
</div>
</div>
<h3 className="text-2xl font-bold tracking-tight pt-4">Tracks</h3>
</div>
) : (
<h1 className="text-3xl font-bold">Download History</h1>
)} )}
{/* Filter Controls */} {/* Filter Controls */}
<div className="flex gap-4 items-center"> {!parentTaskId && (
<select <div className="flex gap-4 items-center">
value={statusFilter} <select
onChange={(e) => setStatusFilter(e.target.value)} value={statusFilter}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700" onChange={(e) => setStatusFilter(e.target.value)}
> className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
<option value="">All Statuses</option> >
<option value="COMPLETED">Completed</option> <option value="">All Statuses</option>
<option value="ERROR">Error</option> <option value="COMPLETED">Completed</option>
<option value="CANCELLED">Cancelled</option> <option value="ERROR">Error</option>
<option value="SKIPPED">Skipped</option> <option value="CANCELLED">Cancelled</option>
</select> <option value="SKIPPED">Skipped</option>
<select </select>
value={typeFilter} <select
onChange={(e) => setTypeFilter(e.target.value)} value={typeFilter}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700" onChange={(e) => setTypeFilter(e.target.value)}
> className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
<option value="">All Types</option> >
<option value="track">Track</option> <option value="">All Types</option>
<option value="album">Album</option> <option value="track">Track</option>
<option value="playlist">Playlist</option> <option value="album">Album</option>
<option value="artist">Artist</option> <option value="playlist">Playlist</option>
</select> <option value="artist">Artist</option>
<select </select>
value={trackStatusFilter} <select
onChange={(e) => setTrackStatusFilter(e.target.value)} value={trackStatusFilter}
className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700" onChange={(e) => setTrackStatusFilter(e.target.value)}
> className="p-2 border rounded-md dark:bg-gray-800 dark:border-gray-700"
<option value="">All Track Statuses</option> >
<option value="SUCCESSFUL">Successful</option> <option value="">All Track Statuses</option>
<option value="SKIPPED">Skipped</option> <option value="SUCCESSFUL">Successful</option>
<option value="FAILED">Failed</option> <option value="SKIPPED">Skipped</option>
</select> <option value="FAILED">Failed</option>
<label className="flex items-center gap-2"> </select>
<input type="checkbox" checked={hideChildTracks} onChange={(e) => setHideChildTracks(e.target.checked)} /> <label className="flex items-center gap-2">
Hide Child Tracks <input
</label> type="checkbox"
</div> checked={showChildTracks}
onChange={(e) => setShowChildTracks(e.target.checked)}
disabled={!!parentTaskId}
/>
Include child tracks
</label>
</div>
)}
{/* Table */} {/* Table */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@@ -316,12 +433,17 @@ export const History = () => {
!row.original.parent_task_id && !row.original.parent_task_id &&
(row.original.download_type === "album" || row.original.download_type === "playlist"); (row.original.download_type === "album" || row.original.download_type === "playlist");
const isChild = !!row.original.parent_task_id; const isChild = !!row.original.parent_task_id;
const rowClass = isParent ? "bg-gray-800 font-semibold" : isChild ? "bg-gray-900" : ""; let rowClass = "hover:bg-muted/50";
if (isParent) {
rowClass += " bg-muted/50 font-semibold hover:bg-muted";
} else if (isChild) {
rowClass += " border-t border-dashed border-muted-foreground/20";
}
return ( return (
<tr key={row.id} className={`border-b dark:border-gray-700 ${rowClass}`}> <tr key={row.id} className={`border-b border-border ${rowClass}`}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<td key={cell.id} className="p-2"> <td key={cell.id} className="p-3">
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</td> </td>
))} ))}