refactor(frontend): adopt the lifecycle and the notification surface

The remaining views, brought onto the two mechanisms added earlier in
this series.

The lifecycle: every cached primary view moves its document listeners,
intervals and event subscriptions off connect/disconnect and onto
`viewActivated`/`viewDeactivated`, so `autotag-view` stops fielding
keystrokes from Settings, `downloads-view`'s 30 s clock stops ticking
for the session, and an off-screen view stops rendering on every
search keystroke. `autotag-view` keeps a document listener only for
Escape, whose dialogs Phase 5 migrates to wa-dialog anyway.

The voice: the silent failures now speak — scan and full rescan (with
a guard against the double-click the coalescing window allowed), job
pause/resume/cancel, playlist delete, download request pause/remove/
clear, add and rename library, add-to-playlist, playlist track
removal, autotag's dialogs and its apply, and favourite reverts. Both
private toasts are gone, along with their CSS and keyframes. Playlist
delete (single and the multi-select loop), download-request removal,
download-client removal and a queue clear over 20 tracks ask first.

Loading, empty and failed become three states rather than one, in
`track-list` and `genre-details` — the first is on the first screen a
new user ever sees — and the Settings index panel seeds itself with
`GetIndexStatus()` instead of waiting forever for a change event.
`smart-playlist-editor` and `download-picker` take the request-version
guard `explore-view` already had.

`track-details` loads through one memoised dynamic import in all ten
openers, which is what takes its 42 kB out of the startup chunk: an
un-upgraded custom element is a real HTMLElement on which `?.show()`
throws, so each opener awaits it before touching the element its
template already rendered.
This commit is contained in:
2026-08-12 01:19:47 -04:00
parent c8bc6db9fa
commit 2518385330
18 changed files with 930 additions and 375 deletions
@@ -3,6 +3,7 @@ import { customElement, property, state } from 'lit/decorators.js';
import { library } from '@go/models';
import { PreviewSmartPlaylist } from '@go/playlist/Service';
import { libraryStore } from '@store/library-store';
import { describeError } from '@utils/describe-error';
import { designTokens } from '../../styles/tokens.css';
import '@components/combobox/combobox.ts';
@@ -675,6 +676,14 @@ export class SmartPlaylistEditor extends LitElement {
}, 300);
}
/**
* Monotonic request version. Debouncing only coalesces keystrokes
* *within* its window; a query slower than 300 ms overlaps the next
* one and whichever answers last used to win (errors.M8). This is
* `explore-view`'s guard, checked on all three paths.
*/
private previewVersion = 0;
private async runPreview() {
// Skip preview if any rule is incomplete
const incomplete = this.ruleRows.some(
@@ -690,19 +699,30 @@ export class SmartPlaylistEditor extends LitElement {
}
const json = this.buildRulesJSON();
const version = ++this.previewVersion;
this.previewLoading = true;
this.previewError = '';
try {
const tracks = await PreviewSmartPlaylist(json);
if (version !== this.previewVersion) return;
this.previewTracks = tracks ?? [];
} catch (error) {
if (version !== this.previewVersion) return;
console.error('Smart playlist preview failed:', error);
this.previewError =
error instanceof Error ? error.message : String(error);
this.previewError = describeError(
error,
'The preview could not be built for these rules.',
);
this.previewTracks = [];
} finally {
this.previewLoading = false;
if (version === this.previewVersion) {
this.previewLoading = false;
}
}
}