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 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 && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user