feat: one Missing files page under Collection: folders gone, each with its album's want, last search or download, and Search, Releases and Remove; an album held before its files went is no longer shown as held again (M504 #5626)
release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m30s
release / go (push) Successful in 1m46s
release / integration (push) Successful in 5m23s
release / android (push) Successful in 5m43s
release / Build signed APK (releases and dev) (push) Successful in 6m2s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 14:44:33 -04:00
co-authored by Claude Opus 5.5
parent 38b2325d2d
commit ead0abf0cf
20 changed files with 702 additions and 390 deletions
+79
View File
@@ -0,0 +1,79 @@
import { relativeTime } from '#lib/utils/relativeTime.js';
import type { SearchResult, Want } from '#lib/api/types.js';
// How a want reads in a line, wherever it is shown: the Collection's lists
// and the Missing page's folders (M504).
// What a Search the operator ran found, for its toast.
export function searched(w: Want, r: SearchResult): string {
let said: string;
switch (r.outcome) {
case 'grabbed':
said = `Took ${r.grabbed ?? r.best_release}.`;
break;
case 'waiting':
said = `Found ${r.considered}; nothing taken. Releases shows why.`;
break;
case 'nothing':
said = r.considered ? `Found ${r.considered}, none of them ${w.title}.` : `Nothing found for ${w.title}.`;
break;
default:
said = r.error ?? 'The search failed.';
}
if (r.narrowed.length) said += ` ${r.narrowed.map((n) => n.label).join(', ')} didn't answer.`;
return said;
}
// A download still running, in a line: how far it has got, or what its
// import is doing or waiting on.
export function downloading(w: Want): string | null {
const g = w.grab;
if (!g?.live) return null;
switch (g.import_state) {
case 'downloading':
return `Downloading ${Math.floor(g.progress * 100)}% · ${g.name}`;
case 'importing':
return `Downloaded; checking it is the album and filing it · ${g.name}`;
case 'waiting':
return `Downloaded; ${g.import_reason || 'waiting to be imported'}`;
case 'review':
return `Needs you: ${g.import_reason}`;
case 'failed':
return `Import failed: ${g.import_reason}`;
}
const who = g.origin === 'operator' ? 'taken by you' : 'taken by the schedule';
return `Downloading ${g.name}, ${who} ${relativeTime(g.grabbed_at)}`;
}
// A download waiting on a person: an import to review, or one that failed.
export const needsYou = (w: Want) =>
!!w.grab?.live && (w.grab.import_state === 'review' || w.grab.import_state === 'failed');
// What the schedule last did about a want, in a line.
export function wantStatus(w: Want): string {
const live = downloading(w);
if (live) return live;
const s = w.search;
let said = 'Not searched yet';
if (s) {
const when = `Searched ${relativeTime(s.searched_at)}`;
switch (s.outcome) {
case 'nothing':
said = s.considered ? `${when}: ${s.considered} found, none of them this album` : `${when}: nothing found`;
break;
case 'waiting':
said = `${when}: ${s.considered} found, nothing taken`;
break;
case 'grabbed':
said = `${when}: took ${s.best_release}`;
break;
default:
said = `${when}: ${s.error}`;
}
}
const g = w.grab;
if (g?.import_state === 'refused') said += `. Last download refused: ${g.import_reason}`;
else if (g && !g.live && ['stalled', 'failed', 'gone'].includes(g.end_reason) && g.end_detail)
said += `. Last download given up: ${g.end_detail}`;
return said;
}