improve style
This commit is contained in:
@@ -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">
|
||||||
← Back to All History
|
← 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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user