diff --git a/DESIGN.md b/DESIGN.md index 06ad445..c6b6ace 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -164,10 +164,10 @@ Recomputed on metadata refresh and on file change: | `complete` | everything wanted is on disk | | `ended` | series finished upstream and complete | -Default list view shows `airing` and `incomplete`. Everything else collapses -behind one toggle. A one-off season grab therefore disappears from the default -view by itself once satisfied, and a tracked show reappears by itself when a new -season is announced. Nothing to remember to flip. +The list view shows everything, ordered: `airing` and `incomplete` first, every +other status after them. A one-off season grab therefore sinks to the bottom by +itself once satisfied, and a tracked show rises again by itself when a new +season is announced. Nothing is hidden and nothing has to be flipped. Status chips carry state colour, not a single informational hue: green (`--signal-ok`) for on disk or complete, amber (`--signal-warn`) for wanted but diff --git a/web/index.html b/web/index.html index 7c12e30..8b24aca 100644 --- a/web/index.html +++ b/web/index.html @@ -147,14 +147,6 @@ - diff --git a/web/src/library.ts b/web/src/library.ts index 9f83d01..c861cc6 100644 --- a/web/src/library.ts +++ b/web/src/library.ts @@ -51,8 +51,9 @@ export async function fetchLibrary(): Promise { } /** - * §4.2: the default list shows `airing` and `incomplete`; everything else - * collapses behind one toggle. A satisfied title leaves by itself. + * §4.2: `airing` and `incomplete` sort to the top of the list; everything + * else is satisfied and sinks below it. A satisfied title steps aside by + * itself, and is still there to click. */ export function seriesNeedsAttention(series: LibrarySeries): boolean { return series.status === "airing" || series.status === "incomplete"; @@ -76,8 +77,8 @@ export function seriesStatusChipRedundant(series: { } /** - * The movie analogue of §4.2's default set: wanted and not yet on disk. - * Available and unwanted titles are satisfied and collapse with the rest. + * The movie analogue of §4.2's ordering: wanted and not yet on disk sorts + * first. Available and unwanted titles are satisfied and sink with the rest. */ export function movieNeedsAttention(movie: LibraryMovie): boolean { return movie.wanted && movie.state !== "available"; diff --git a/web/src/main.ts b/web/src/main.ts index 63132fd..55ef644 100644 --- a/web/src/main.ts +++ b/web/src/main.ts @@ -3651,6 +3651,14 @@ function loadLibraryShape(): LibraryShape { } } +/** + * Stable partition: the items that still want something keep their order and + * come first, the satisfied ones keep theirs and follow. + */ +function attentionFirst(items: T[], wants: (item: T) => boolean): T[] { + return [...items.filter(wants), ...items.filter((item) => !wants(item))]; +} + function libraryMain( movieDetail: MovieView, seriesDetail: SeriesView, @@ -3659,7 +3667,6 @@ function libraryMain( const view = must("#library"); const deck = must("#deck"); const summary = must("#library-summary"); - const toggle = must("#library-toggle"); const status = must("#library-status"); const gridBtn = must("#library-view-grid"); const listBtn = must("#library-view-list"); @@ -3677,8 +3684,6 @@ function libraryMain( }, }; - // §4.2: ONE toggle for everything the default view leaves out - let showAll = false; // §9.6 library view: the poster grid is default, per-person, localStorage let shape = loadLibraryShape(); let data: { series: LibrarySeries[]; movies: LibraryMovie[] } | null = null; @@ -3721,7 +3726,6 @@ function libraryMain( group.rows.replaceChildren(); } summary.textContent = ""; - toggle.hidden = true; } function renderGroup(group: LibraryGroup, items: T[], build: (item: T) => HTMLLIElement) { @@ -3777,17 +3781,13 @@ function libraryMain( return; } const total = library.series.length + library.movies.length; - const satisfied = - library.series.filter((series) => !seriesNeedsAttention(series)).length + - library.movies.filter((movie) => !movieNeedsAttention(movie)).length; summary.textContent = total === 0 ? "" : `${total} ${total === 1 ? "title" : "titles"}`; - toggle.hidden = satisfied === 0; - toggle.textContent = `${showAll ? "hide" : "show"} ${satisfied} satisfied`; - toggle.setAttribute("aria-expanded", String(showAll)); - const seriesShown = showAll ? library.series : library.series.filter(seriesNeedsAttention); - const moviesShown = showAll ? library.movies : library.movies.filter(movieNeedsAttention); + // §4.2: nothing is hidden. Satisfied titles sort last so what still + // wants something is what the eye lands on first. + const seriesShown = attentionFirst(library.series, seriesNeedsAttention); + const moviesShown = attentionFirst(library.movies, movieNeedsAttention); const openSeries = (id: number, origin: HTMLElement) => { navigate({ kind: "series", seriesId: id }); @@ -3801,16 +3801,26 @@ function libraryMain( if (shape === "grid") { groups.series.section.hidden = true; groups.movies.section.hidden = true; - renderLibraryRows([ - ...seriesShown.map((series) => ({ - root_id: series.root_id, - item: seriesCard(series, openSeries), - })), - ...moviesShown.map((movie) => ({ - root_id: movie.root_id, - item: movieCard(movie, openMovie), - })), - ]); + // series and movies share a root section, so the two lists interleave + // before the cards are built — otherwise a satisfied series would sit + // above a movie that still wants a file. + renderLibraryRows( + attentionFirst( + [ + ...seriesShown.map((series) => ({ + root_id: series.root_id, + wants: seriesNeedsAttention(series), + build: () => seriesCard(series, openSeries), + })), + ...moviesShown.map((movie) => ({ + root_id: movie.root_id, + wants: movieNeedsAttention(movie), + build: () => movieCard(movie, openMovie), + })), + ], + (entry) => entry.wants, + ).map((entry) => ({ root_id: entry.root_id, item: entry.build() })), + ); } else { libraryRows.replaceChildren(); libraryRows.hidden = true; @@ -3820,8 +3830,6 @@ function libraryMain( if (total === 0) { setStatus("library is empty — the search box above adds titles"); - } else if (seriesShown.length + moviesShown.length === 0) { - setStatus("nothing needs attention"); } else { setStatus(null); } @@ -3862,11 +3870,6 @@ function libraryMain( sequence += 1; } - toggle.addEventListener("click", () => { - showAll = !showAll; - render(); - }); - function repaintDownloads() { if (!view.hidden) { render();