unify action box across all pages: consistent border/rounded style, green tint for "all good" states
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
All checks were successful
Build and Push Docker Image / build (push) Successful in 35s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -122,10 +122,36 @@ export function ExecutePage() {
|
|||||||
const errors = totalCounts.error ?? 0;
|
const errors = totalCounts.error ?? 0;
|
||||||
const jobs = data?.jobs ?? [];
|
const jobs = data?.jobs ?? [];
|
||||||
|
|
||||||
|
const running = totalCounts.running ?? 0;
|
||||||
|
const allDone = totalCounts.all > 0 && pending === 0 && running === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h1 className="text-xl font-bold mb-4">Execute Jobs</h1>
|
||||||
<h1 className="text-xl font-bold m-0">Execute Jobs</h1>
|
|
||||||
|
<div className={`rounded-lg px-4 py-3 mb-6 flex items-center gap-3 flex-wrap ${allDone ? 'border border-green-200 bg-green-50' : 'border border-gray-200'}`}>
|
||||||
|
{totalCounts.all === 0 && !loading && (
|
||||||
|
<span className="text-sm text-gray-500">No jobs yet. Go to <Link to="/review" className="text-blue-600 hover:underline">Review</Link> and approve items first.</span>
|
||||||
|
)}
|
||||||
|
{totalCounts.all === 0 && loading && (
|
||||||
|
<span className="text-sm text-gray-400">Loading...</span>
|
||||||
|
)}
|
||||||
|
{allDone && (
|
||||||
|
<span className="text-sm font-medium text-green-800">All jobs completed</span>
|
||||||
|
)}
|
||||||
|
{running > 0 && (
|
||||||
|
<span className="text-sm font-medium">{running} job{running !== 1 ? 's' : ''} running</span>
|
||||||
|
)}
|
||||||
|
{pending > 0 && (
|
||||||
|
<>
|
||||||
|
<span className="text-sm font-medium">{pending} job{pending !== 1 ? 's' : ''} pending</span>
|
||||||
|
<Button size="sm" onClick={startAll}>Run all pending</Button>
|
||||||
|
<Button size="sm" variant="secondary" onClick={clearQueue}>Clear queue</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{(done > 0 || errors > 0) && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={clearCompleted}>Clear done/errors</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FilterTabs
|
<FilterTabs
|
||||||
@@ -135,17 +161,6 @@ export function ExecutePage() {
|
|||||||
onFilterChange={(key) => navigate({ to: '/execute', search: { filter: key } as never })}
|
onFilterChange={(key) => navigate({ to: '/execute', search: { filter: key } as never })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex gap-3 mb-6">
|
|
||||||
{pending > 0 && <Button onClick={startAll}>▶ Run all pending</Button>}
|
|
||||||
{pending > 0 && <Button variant="secondary" onClick={clearQueue}>✕ Clear queue</Button>}
|
|
||||||
{(done > 0 || errors > 0) && <Button variant="secondary" onClick={clearCompleted}>✕ Clear done/errors</Button>}
|
|
||||||
{totalCounts.all === 0 && !loading && (
|
|
||||||
<p className="text-gray-500 m-0">
|
|
||||||
No jobs yet. Go to <Link to="/review">Review</Link> and approve items first.
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading && !data && <div className="text-gray-400 py-8 text-center">Loading…</div>}
|
{loading && !data && <div className="text-gray-400 py-8 text-center">Loading…</div>}
|
||||||
|
|
||||||
{jobs.length > 0 && (
|
{jobs.length > 0 && (
|
||||||
|
|||||||
@@ -24,19 +24,31 @@ export function PathsPage() {
|
|||||||
|
|
||||||
useEffect(() => { if (cache === null) load(); }, []);
|
useEffect(() => { if (cache === null) load(); }, []);
|
||||||
|
|
||||||
|
const allGood = paths.length > 0 && paths.every((p) => p.accessible);
|
||||||
|
const hasBroken = paths.some((p) => !p.accessible);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h1 className="text-xl font-bold mb-4">Paths</h1>
|
||||||
<h1 className="text-lg font-bold">Paths</h1>
|
|
||||||
|
<div className={`rounded-lg px-4 py-3 mb-6 flex items-center gap-3 flex-wrap ${allGood ? 'border border-green-200 bg-green-50' : 'border border-gray-200'}`}>
|
||||||
|
{paths.length === 0 && !loading && (
|
||||||
|
<span className="text-sm text-gray-500">No media items scanned yet. Run a scan first.</span>
|
||||||
|
)}
|
||||||
|
{paths.length === 0 && loading && (
|
||||||
|
<span className="text-sm text-gray-400">Checking paths...</span>
|
||||||
|
)}
|
||||||
|
{allGood && (
|
||||||
|
<span className="text-sm font-medium text-green-800">All {paths.length} paths accessible</span>
|
||||||
|
)}
|
||||||
|
{hasBroken && (
|
||||||
|
<span className="text-sm font-medium text-red-800">{paths.filter((p) => !p.accessible).length} path{paths.filter((p) => !p.accessible).length !== 1 ? 's' : ''} not mounted</span>
|
||||||
|
)}
|
||||||
<Button variant="secondary" size="sm" onClick={load} disabled={loading}>
|
<Button variant="secondary" size="sm" onClick={load} disabled={loading}>
|
||||||
{loading ? 'Checking…' : 'Refresh'}
|
{loading ? 'Checking...' : 'Refresh'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{paths.length === 0 && !loading && (
|
|
||||||
<p className="text-gray-500">No media items scanned yet. Run a scan first.</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{paths.length > 0 && (
|
{paths.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<table className="w-full text-left border-collapse">
|
<table className="w-full text-left border-collapse">
|
||||||
|
|||||||
@@ -231,12 +231,19 @@ export function AudioListPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h1 className="text-xl font-bold mb-4">Audio Review</h1>
|
||||||
<h1 className="text-xl font-bold m-0">Audio Review</h1>
|
|
||||||
{hasPending && <Button size="sm" onClick={approveAll}>Approve all pending</Button>}
|
<div className={`rounded-lg px-4 py-3 mb-6 flex items-center gap-3 flex-wrap ${hasPending ? 'border border-gray-200' : 'border border-green-200 bg-green-50'}`}>
|
||||||
|
{hasPending ? (
|
||||||
|
<>
|
||||||
|
<span className="text-sm font-medium">{totalCounts.needs_action} item{totalCounts.needs_action !== 1 ? 's' : ''} need{totalCounts.needs_action === 1 ? 's' : ''} review</span>
|
||||||
|
<Button size="sm" onClick={approveAll}>Approve all pending</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm font-medium text-green-800">All items reviewed</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter tabs */}
|
|
||||||
<FilterTabs
|
<FilterTabs
|
||||||
tabs={FILTER_TABS}
|
tabs={FILTER_TABS}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
|
|||||||
@@ -174,14 +174,11 @@ export function ScanPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<h1 className="text-xl font-bold mb-4">Library Scan</h1>
|
||||||
<h1 className="text-xl font-bold m-0">Library Scan</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status card */}
|
<div className="border border-gray-200 rounded-lg px-4 py-3 mb-6">
|
||||||
<div className="border border-gray-200 rounded-lg p-4 mb-6">
|
<div className="flex items-center flex-wrap gap-2 mb-3">
|
||||||
<div className="flex items-center flex-wrap gap-2 mb-4">
|
<span className="text-sm font-medium">{statusLabel || (running ? 'Scan in progress…' : 'Scan idle')}</span>
|
||||||
<strong>{statusLabel || (running ? 'Scan in progress…' : 'Scan idle')}</strong>
|
|
||||||
{running ? (
|
{running ? (
|
||||||
<Button variant="secondary" size="sm" onClick={stopScan}>Stop</Button>
|
<Button variant="secondary" size="sm" onClick={stopScan}>Stop</Button>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -76,14 +76,12 @@ function StatusPills({ g }: { g: SubSeriesGroup }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Extraction bar ──────────────────────────────────────────────────────────
|
// ─── Action box ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function ExtractionBar({ count, onExtract }: { count: number; onExtract: () => void }) {
|
function ActionBox({ count, onExtract }: { count: number | null; onExtract: () => void }) {
|
||||||
const [extracting, setExtracting] = useState(false);
|
const [extracting, setExtracting] = useState(false);
|
||||||
const [result, setResult] = useState('');
|
const [result, setResult] = useState('');
|
||||||
|
|
||||||
if (count === 0) return null;
|
|
||||||
|
|
||||||
const handleExtract = async () => {
|
const handleExtract = async () => {
|
||||||
setExtracting(true);
|
setExtracting(true);
|
||||||
setResult('');
|
setResult('');
|
||||||
@@ -95,14 +93,20 @@ function ExtractionBar({ count, onExtract }: { count: number; onExtract: () => v
|
|||||||
setExtracting(false);
|
setExtracting(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allDone = count !== null && count === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 px-4 py-3 mb-4 rounded-lg border border-amber-200 bg-amber-50">
|
<div className={`rounded-lg px-4 py-3 mb-6 flex items-center gap-3 flex-wrap ${allDone ? 'border border-green-200 bg-green-50' : 'border border-gray-200'}`}>
|
||||||
<span className="text-sm font-medium text-amber-900">
|
{count === null && <span className="text-sm text-gray-400">Loading...</span>}
|
||||||
{count} item{count !== 1 ? 's have' : ' has'} embedded subtitles ready to extract
|
{allDone && <span className="text-sm font-medium text-green-800">All subtitles extracted</span>}
|
||||||
</span>
|
{count !== null && count > 0 && (
|
||||||
<Button size="sm" onClick={handleExtract} disabled={extracting}>
|
<>
|
||||||
{extracting ? 'Queuing...' : 'Extract All'}
|
<span className="text-sm font-medium">{count} item{count !== 1 ? 's have' : ' has'} embedded subtitles to extract</span>
|
||||||
</Button>
|
<Button size="sm" onClick={handleExtract} disabled={extracting}>
|
||||||
|
{extracting ? 'Queuing...' : 'Extract All'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{result && <span className="text-sm text-gray-600">{result}</span>}
|
{result && <span className="text-sm text-gray-600">{result}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -219,9 +223,7 @@ export function SubtitleExtractPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold mb-4">Subtitle Extraction</h1>
|
<h1 className="text-xl font-bold mb-4">Subtitle Extraction</h1>
|
||||||
|
|
||||||
{embeddedCount !== null && (
|
<ActionBox count={embeddedCount} onExtract={refresh} />
|
||||||
<ExtractionBar count={embeddedCount} onExtract={refresh} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FilterTabs
|
<FilterTabs
|
||||||
tabs={FILTER_TABS}
|
tabs={FILTER_TABS}
|
||||||
|
|||||||
@@ -256,10 +256,22 @@ export function SubtitleListPage() {
|
|||||||
if (loading && !summary) return <div className="text-gray-400 py-8 text-center">Loading...</div>;
|
if (loading && !summary) return <div className="text-gray-400 py-8 text-center">Loading...</div>;
|
||||||
if (!summary) return <div className="text-red-600">Failed to load subtitle summary.</div>;
|
if (!summary) return <div className="text-red-600">Failed to load subtitle summary.</div>;
|
||||||
|
|
||||||
|
const totalFiles = summary.categories.reduce((sum, c) => sum + c.fileCount, 0);
|
||||||
|
const langCount = new Set(summary.categories.map((c) => c.language)).size;
|
||||||
|
const hasFiles = totalFiles > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold mb-4">Subtitle Manager</h1>
|
<h1 className="text-xl font-bold mb-4">Subtitle Manager</h1>
|
||||||
|
|
||||||
|
<div className={`rounded-lg px-4 py-3 mb-6 flex items-center gap-3 flex-wrap ${hasFiles ? 'border border-gray-200' : 'border border-gray-200'}`}>
|
||||||
|
{hasFiles ? (
|
||||||
|
<span className="text-sm font-medium">{totalFiles} extracted file{totalFiles !== 1 ? 's' : ''} across {langCount} language{langCount !== 1 ? 's' : ''} — select which to keep below</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-sm text-gray-500">No extracted subtitle files yet. Extract subtitles first.</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<LanguageSummary
|
<LanguageSummary
|
||||||
categories={summary.categories}
|
categories={summary.categories}
|
||||||
keepLanguages={summary.keepLanguages}
|
keepLanguages={summary.keepLanguages}
|
||||||
|
|||||||
Reference in New Issue
Block a user