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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 17:16:57 +01:00
parent dd82318828
commit 6363a133dd
6 changed files with 90 additions and 45 deletions

View File

@@ -122,10 +122,36 @@ export function ExecutePage() {
const errors = totalCounts.error ?? 0;
const jobs = data?.jobs ?? [];
const running = totalCounts.running ?? 0;
const allDone = totalCounts.all > 0 && pending === 0 && running === 0;
return (
<div>
<div className="flex items-center gap-3 mb-4">
<h1 className="text-xl font-bold m-0">Execute Jobs</h1>
<h1 className="text-xl font-bold mb-4">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>
<FilterTabs
@@ -135,17 +161,6 @@ export function ExecutePage() {
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>}
{jobs.length > 0 && (

View File

@@ -24,19 +24,31 @@ export function PathsPage() {
useEffect(() => { if (cache === null) load(); }, []);
const allGood = paths.length > 0 && paths.every((p) => p.accessible);
const hasBroken = paths.some((p) => !p.accessible);
return (
<div>
<div className="flex items-center gap-3 mb-4">
<h1 className="text-lg font-bold">Paths</h1>
<h1 className="text-xl font-bold mb-4">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}>
{loading ? 'Checking' : 'Refresh'}
{loading ? 'Checking...' : 'Refresh'}
</Button>
</div>
{paths.length === 0 && !loading && (
<p className="text-gray-500">No media items scanned yet. Run a scan first.</p>
)}
{paths.length > 0 && (
<>
<table className="w-full text-left border-collapse">

View File

@@ -231,12 +231,19 @@ export function AudioListPage() {
return (
<div>
<div className="flex items-center gap-3 mb-4">
<h1 className="text-xl font-bold m-0">Audio Review</h1>
{hasPending && <Button size="sm" onClick={approveAll}>Approve all pending</Button>}
<h1 className="text-xl font-bold mb-4">Audio Review</h1>
<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>
{/* Filter tabs */}
<FilterTabs
tabs={FILTER_TABS}
filter={filter}

View File

@@ -174,14 +174,11 @@ export function ScanPage() {
return (
<div>
<div className="flex items-center gap-3 mb-4">
<h1 className="text-xl font-bold m-0">Library Scan</h1>
</div>
<h1 className="text-xl font-bold mb-4">Library Scan</h1>
{/* Status card */}
<div className="border border-gray-200 rounded-lg p-4 mb-6">
<div className="flex items-center flex-wrap gap-2 mb-4">
<strong>{statusLabel || (running ? 'Scan in progress…' : 'Scan idle')}</strong>
<div className="border border-gray-200 rounded-lg px-4 py-3 mb-6">
<div className="flex items-center flex-wrap gap-2 mb-3">
<span className="text-sm font-medium">{statusLabel || (running ? 'Scan in progress…' : 'Scan idle')}</span>
{running ? (
<Button variant="secondary" size="sm" onClick={stopScan}>Stop</Button>
) : (

View File

@@ -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 [result, setResult] = useState('');
if (count === 0) return null;
const handleExtract = async () => {
setExtracting(true);
setResult('');
@@ -95,14 +93,20 @@ function ExtractionBar({ count, onExtract }: { count: number; onExtract: () => v
setExtracting(false);
};
const allDone = count !== null && count === 0;
return (
<div className="flex items-center gap-3 px-4 py-3 mb-4 rounded-lg border border-amber-200 bg-amber-50">
<span className="text-sm font-medium text-amber-900">
{count} item{count !== 1 ? 's have' : ' has'} embedded subtitles ready to extract
</span>
<Button size="sm" onClick={handleExtract} disabled={extracting}>
{extracting ? 'Queuing...' : 'Extract All'}
</Button>
<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'}`}>
{count === null && <span className="text-sm text-gray-400">Loading...</span>}
{allDone && <span className="text-sm font-medium text-green-800">All subtitles extracted</span>}
{count !== null && count > 0 && (
<>
<span className="text-sm font-medium">{count} item{count !== 1 ? 's have' : ' has'} embedded subtitles to extract</span>
<Button size="sm" onClick={handleExtract} disabled={extracting}>
{extracting ? 'Queuing...' : 'Extract All'}
</Button>
</>
)}
{result && <span className="text-sm text-gray-600">{result}</span>}
</div>
);
@@ -219,9 +223,7 @@ export function SubtitleExtractPage() {
<div>
<h1 className="text-xl font-bold mb-4">Subtitle Extraction</h1>
{embeddedCount !== null && (
<ExtractionBar count={embeddedCount} onExtract={refresh} />
)}
<ActionBox count={embeddedCount} onExtract={refresh} />
<FilterTabs
tabs={FILTER_TABS}

View File

@@ -256,10 +256,22 @@ export function SubtitleListPage() {
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>;
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 (
<div>
<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
categories={summary.categories}
keepLanguages={summary.keepLanguages}