improved history view in mobile
This commit is contained in:
@@ -343,55 +343,59 @@ export const History = () => {
|
|||||||
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Download History</h1>
|
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">Download History</h1>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Filter Controls */}
|
{/* Filter Controls - Responsive */}
|
||||||
{!parentTaskId && (
|
{!parentTaskId && (
|
||||||
<div className="flex gap-4 items-center">
|
<div className="space-y-4">
|
||||||
<select
|
{/* Mobile: Stacked filters */}
|
||||||
value={statusFilter}
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
<select
|
||||||
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
value={statusFilter}
|
||||||
>
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
<option value="">All Statuses</option>
|
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||||
<option value="COMPLETED">Completed</option>
|
>
|
||||||
<option value="ERROR">Error</option>
|
<option value="">All Statuses</option>
|
||||||
<option value="CANCELLED">Cancelled</option>
|
<option value="COMPLETED">Completed</option>
|
||||||
<option value="SKIPPED">Skipped</option>
|
<option value="ERROR">Error</option>
|
||||||
</select>
|
<option value="CANCELLED">Cancelled</option>
|
||||||
<select
|
<option value="SKIPPED">Skipped</option>
|
||||||
value={typeFilter}
|
</select>
|
||||||
onChange={(e) => setTypeFilter(e.target.value)}
|
<select
|
||||||
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
value={typeFilter}
|
||||||
>
|
onChange={(e) => setTypeFilter(e.target.value)}
|
||||||
<option value="">All Types</option>
|
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||||
<option value="track">Track</option>
|
>
|
||||||
<option value="album">Album</option>
|
<option value="">All Types</option>
|
||||||
<option value="playlist">Playlist</option>
|
<option value="track">Track</option>
|
||||||
<option value="artist">Artist</option>
|
<option value="album">Album</option>
|
||||||
</select>
|
<option value="playlist">Playlist</option>
|
||||||
<select
|
<option value="artist">Artist</option>
|
||||||
value={trackStatusFilter}
|
</select>
|
||||||
onChange={(e) => setTrackStatusFilter(e.target.value)}
|
<select
|
||||||
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
value={trackStatusFilter}
|
||||||
>
|
onChange={(e) => setTrackStatusFilter(e.target.value)}
|
||||||
<option value="">All Track Statuses</option>
|
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||||
<option value="SUCCESSFUL">Successful</option>
|
>
|
||||||
<option value="SKIPPED">Skipped</option>
|
<option value="">All Track Statuses</option>
|
||||||
<option value="FAILED">Failed</option>
|
<option value="SUCCESSFUL">Successful</option>
|
||||||
</select>
|
<option value="SKIPPED">Skipped</option>
|
||||||
<label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
|
<option value="FAILED">Failed</option>
|
||||||
<input
|
</select>
|
||||||
type="checkbox"
|
<label className="flex items-center gap-2 text-content-primary dark:text-content-primary-dark">
|
||||||
checked={showChildTracks}
|
<input
|
||||||
onChange={(e) => setShowChildTracks(e.target.checked)}
|
type="checkbox"
|
||||||
disabled={!!parentTaskId}
|
checked={showChildTracks}
|
||||||
/>
|
onChange={(e) => setShowChildTracks(e.target.checked)}
|
||||||
Include child tracks
|
disabled={!!parentTaskId}
|
||||||
</label>
|
className="rounded"
|
||||||
|
/>
|
||||||
|
<span className="text-sm">Include child tracks</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Table */}
|
{/* Desktop Table */}
|
||||||
<div className="overflow-x-auto">
|
<div className="hidden lg:block overflow-x-auto">
|
||||||
<table className="min-w-full">
|
<table className="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -455,39 +459,149 @@ export const History = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination Controls */}
|
{/* Mobile Card Layout */}
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="lg:hidden space-y-3">
|
||||||
<button
|
{isLoading ? (
|
||||||
onClick={() => table.previousPage()}
|
<div className="text-center p-8 text-content-muted dark:text-content-muted-dark">
|
||||||
disabled={!table.getCanPreviousPage()}
|
Loading...
|
||||||
className="p-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50"
|
</div>
|
||||||
>
|
) : table.getRowModel().rows.length === 0 ? (
|
||||||
Previous
|
<div className="text-center p-8 text-content-muted dark:text-content-muted-dark">
|
||||||
</button>
|
No history entries found.
|
||||||
<span className="text-content-primary dark:text-content-primary-dark">
|
</div>
|
||||||
Page{" "}
|
) : (
|
||||||
<strong>
|
table.getRowModel().rows.map((row) => {
|
||||||
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
const entry = row.original;
|
||||||
</strong>
|
const isParent = !entry.parent_task_id && (entry.download_type === "album" || entry.download_type === "playlist");
|
||||||
</span>
|
const isChild = !!entry.parent_task_id;
|
||||||
<button
|
const status = entry.parent_task_id ? entry.track_status : entry.status_final;
|
||||||
onClick={() => table.nextPage()}
|
const statusKey = (status || "").toUpperCase();
|
||||||
disabled={!table.getCanNextPage()}
|
const statusClass = {
|
||||||
className="p-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50"
|
COMPLETED: "text-success",
|
||||||
>
|
SUCCESSFUL: "text-success",
|
||||||
Next
|
ERROR: "text-error",
|
||||||
</button>
|
FAILED: "text-error",
|
||||||
<select
|
CANCELLED: "text-content-muted dark:text-content-muted-dark",
|
||||||
value={table.getState().pagination.pageSize}
|
SKIPPED: "text-warning",
|
||||||
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
}[statusKey] || "text-gray-500";
|
||||||
className="p-2 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
|
||||||
>
|
let cardClass = "bg-surface dark:bg-surface-secondary-dark rounded-lg border border-border dark:border-border-dark p-4";
|
||||||
{[10, 25, 50, 100].map((size) => (
|
if (isParent) {
|
||||||
<option key={size} value={size}>
|
cardClass += " border-l-4 border-l-primary";
|
||||||
Show {size}
|
} else if (isChild) {
|
||||||
</option>
|
cardClass += " ml-4 border-l-2 border-l-content-muted dark:border-l-content-muted-dark";
|
||||||
))}
|
}
|
||||||
</select>
|
|
||||||
|
return (
|
||||||
|
<div key={row.id} className={cardClass}>
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between mb-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className={`font-semibold text-content-primary dark:text-content-primary-dark truncate ${isChild ? 'text-sm' : 'text-base'}`}>
|
||||||
|
{isChild ? `└─ ${entry.item_name}` : entry.item_name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-content-secondary dark:text-content-secondary-dark truncate">
|
||||||
|
{entry.item_artist}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-sm font-semibold ${statusClass} ml-2`}>
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-y-2 gap-x-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-content-muted dark:text-content-muted-dark">Type:</span>
|
||||||
|
<span className="ml-1 capitalize text-content-primary dark:text-content-primary-dark">
|
||||||
|
{entry.download_type}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-content-muted dark:text-content-muted-dark">Source:</span>
|
||||||
|
<span className="ml-1 text-content-primary dark:text-content-primary-dark">
|
||||||
|
{getDownloadSource(entry)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="text-content-muted dark:text-content-muted-dark">Quality:</span>
|
||||||
|
<span className="ml-1 text-content-primary dark:text-content-primary-dark">
|
||||||
|
{formatQuality(entry)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<span className="text-content-muted dark:text-content-muted-dark">Completed:</span>
|
||||||
|
<span className="ml-1 text-content-primary dark:text-content-primary-dark">
|
||||||
|
{new Date(entry.timestamp_completed * 1000).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions for parent entries */}
|
||||||
|
{!parentTaskId && isParent && (
|
||||||
|
entry.total_successful || entry.total_skipped || entry.total_failed
|
||||||
|
) ? (
|
||||||
|
<div className="mt-3 pt-3 border-t border-border dark:border-border-dark flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="text-success">{entry.total_successful ?? 0} ✓</span>
|
||||||
|
<span className="text-warning">{entry.total_skipped ?? 0} ⊘</span>
|
||||||
|
<span className="text-error">{entry.total_failed ?? 0} ✗</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => viewTracksForParent(entry)}
|
||||||
|
className="px-3 py-1 text-xs rounded-md bg-primary hover:bg-primary-hover text-white"
|
||||||
|
>
|
||||||
|
View Tracks
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination Controls - Responsive */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Mobile: Stacked layout */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||||
|
<div className="flex items-center justify-center sm:justify-start gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
className="px-4 py-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
className="px-4 py-2 border bg-button-secondary hover:bg-button-secondary-hover text-button-secondary-text hover:text-button-secondary-text-hover border-border dark:border-border-dark rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-center gap-2 text-sm">
|
||||||
|
<span className="text-content-primary dark:text-content-primary-dark whitespace-nowrap">
|
||||||
|
Page{" "}
|
||||||
|
<strong>
|
||||||
|
{table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={table.getState().pagination.pageSize}
|
||||||
|
onChange={(e) => table.setPageSize(Number(e.target.value))}
|
||||||
|
className="px-3 py-1 border bg-input-background dark:bg-input-background-dark border-input-border dark:border-input-border-dark rounded-md focus:outline-none focus:ring-2 focus:ring-input-focus"
|
||||||
|
>
|
||||||
|
{[10, 25, 50, 100].map((size) => (
|
||||||
|
<option key={size} value={size}>
|
||||||
|
Show {size}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user