diff --git a/CLAUDE.md b/CLAUDE.md index bda2176e8..58611158c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -114,7 +114,7 @@ Names in `commands/`, `audio/` and `src/components/` are predictable — read th | Tag workshop | [`tag-workshop.md`](docs/features/tag-workshop.md) | bulk tag editing of a selection, an album or a folder (fill, find and replace with regex, case, numbering), MusicBrainz matching through the enrichment engine, the six identifiers written into the files, field-by-field preview, journaled undo that refuses a file changed since | | Integrations | [`integrations.md`](docs/features/integrations.md) | Deezer, Last.fm, TheAudioDB, lyrics providers + editor, artist overrides, Discord RPC, OS notifications, scrobbling | | Plugins | [`plugins.md`](docs/features/plugins.md) | WASM host + sandbox, store, options, `source` / `metadata` / `ui` / `canvas` worlds, Web Radio + offline catalogue | -| UI & UX | [`ui.md`](docs/features/ui.md) | layout, 6 skins × 14 themes + custom / cover accent, visual effects level, suggestion of the day on Home, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding + first steps, settings, updater, backups | +| UI & UX | [`ui.md`](docs/features/ui.md) | layout, 6 skins × 14 themes + custom / cover accent, visual effects level, suggestion of the day on Home, immersive view, Canvas, cover slideshow, artist hero, mini-player, desktop lyrics, Wrapped, profiles, onboarding + first steps, the What's new panel, settings, updater, backups | | LAN servers | [`dlna.md`](docs/features/dlna.md) · [`mpd.md`](docs/features/mpd.md) | opt-in MediaServer and MPD control surface | ## Conventions diff --git a/docs/README.md b/docs/README.md index 50c94a2e1..41bf1c130 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ User-facing references and per-feature deep dives. The top-level [README](../REA | [DLNA / UPnP server](features/dlna.md) | Built-in MediaServer: SSDP discovery, ContentDirectory Browse, Range streaming to LAN amplifiers | | [MPD server](features/mpd.md) | Control surface for existing MPD clients (MALP, ncmpcpp, waybar): protocol subset, idle notifications, shared player actions, LAN bind rationale | | [Community-DB](features/community.md) | _Placeholder._ Opt-in shared metadata pool — companion page to [RFC-004](rfcs/RFC-004-community-database.md). Real copy fills in during Phase 2.a. | -| [UI & UX](features/ui.md) | Layout, panels, skins, immersive view, track Canvas, cover slideshow, mini-player widget, tray, statistics, dark mode, i18n, profiles, onboarding + first steps, auto-updater | +| [UI & UX](features/ui.md) | Layout, panels, skins, immersive view, track Canvas, cover slideshow, mini-player widget, tray, statistics, dark mode, i18n, profiles, What's new panel, onboarding + first steps, auto-updater | ## Architecture diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 0b0ce4574..19d9eeb7a 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -130,6 +130,8 @@ gh secret set SIGNTOOL_PFX_PASSWORD --body "" ### 1. Cut the release via release-please +A version with something to tell also gets its [What's new pages](features/ui.md#adding-the-pages-of-a-version), written by hand before the cut; a correction release needs none. + You do **not** hand-bump versions anymore. Every push to `main` — or to a maintenance branch, see [Patch releases from a maintenance branch](#patch-releases-from-a-maintenance-branch) — diff --git a/docs/architecture/storage.md b/docs/architecture/storage.md index 80a099cad..633241fbf 100644 --- a/docs/architecture/storage.md +++ b/docs/architecture/storage.md @@ -167,10 +167,10 @@ To give an owned pool to a `waveflow-core` type that knows nothing about leases, Two flavours, two stores: -| Store | Scope | Used for | -| --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `app_setting` (`app.db`) | App-wide | API keys, session keys, theme, last-active-profile | -| `profile_setting` (per `data.db`) | Per-profile | Output device, crossfade, normalize / mono / replaygain toggles, onboarding dismissal, first-steps ticks, tips and the player bar tour seen (`discovery.*`, `tips.*`, `tour.*`), sort memory | +| Store | Scope | Used for | +| --------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `app_setting` (`app.db`) | App-wide | API keys, session keys, theme, last-active-profile, the version whose [What's new](../features/ui.md#whats-new) this installation is past (`whats_new.seen_version`, absent on anything updated from a 1.x) | +| `profile_setting` (per `data.db`) | Per-profile | Output device, crossfade, normalize / mono / replaygain toggles, onboarding dismissal, first-steps ticks, tips and the player bar tour seen (`discovery.*`, `tips.*`, `tour.*`), the What's new choices (`ui.whats_new_auto_open`, `ui.whats_new_button`), sort memory | Both follow the same `INSERT … ON CONFLICT DO UPDATE` typed-value pattern (`value_text` / `value_int` / `value_real` / `value_bool` columns + a `kind` discriminator). diff --git a/docs/features/ui.md b/docs/features/ui.md index 8a2a31f82..d278d1924 100644 --- a/docs/features/ui.md +++ b/docs/features/ui.md @@ -299,7 +299,7 @@ The line being sung takes a colour of the user's choosing in the immersive lyric ### Theater mode -The immersive view as a display read from across the room — a TV, or a computer left showing what plays. A **Tv** button in the view's top bar toggles it, and Settings → Appearance → Immersive view carries the same switch (`immersive.theater_mode`, per profile, default OFF); the button writes the setting too, so a TV setup opens straight into it. What it changes, all in [`ImmersiveView`](../../src/components/player/ImmersiveView.tsx): +The immersive view as a display read from across the room — a TV, or a computer left showing what plays. A **Tv** button in the view's top bar toggles it, and Settings → Appearance → Immersive view carries the same switch (`immersive.theater_mode`, per profile, default OFF); the button writes the setting too, so a TV setup opens straight into it. The [What's new](#whats-new) panel's try is the one exception: it starts the view in theater mode for that visit and writes nothing. What it changes, all in [`ImmersiveView`](../../src/components/player/ImmersiveView.tsx): - **Two columns always**, whatever `immersive.merged_lyrics` says (a window under 900 px still collapses), and entering it opens the panel on the **Tracklist** tab — unless the view was opened from the lyrics and only starts in theater mode. - **Scaled to the window.** The foreground is laid out as if the window were 1280 px wide and scaled up with CSS `zoom` (up to ×2; a narrower window keeps ×1): ×1.5 on a 1080p TV, ×2 on a 1440p screen. One factor for everything — cover, title, lyrics, tracklist — rather than a size per element, and the panel's tab measurement compares rectangles inside the same zoomed subtree, so it holds. The blurred backdrop is outside the zoom. Menus portalled to `body` keep the normal scale. @@ -920,10 +920,44 @@ After the journey (or from Settings → General → "Player bar tour"), [`Player ### Feature cards -[`FeatureCard`](../../src/components/discovery/FeatureCard.tsx) tells one feature: a small scene ([`FeatureArt`](../../src/components/discovery/FeatureArt.tsx), one SVG per idea — crossfade, AutoMix, ReplayGain, analysis, lyrics, Canvas, animated covers, plugins, Last.fm, Discord, the server, and the first steps' eight), a title, the benefit in one sentence, badges, and its state — a switch drawn in the corner when the whole card toggles (`role="switch"`), a ring when it is on or installed, or actions underneath when it only presents. Nothing in it knows the screen it is on: the onboarding uses it, and the "What's new" panel is meant to. +[`FeatureCard`](../../src/components/discovery/FeatureCard.tsx) tells one feature: a small scene ([`FeatureArt`](../../src/components/discovery/FeatureArt.tsx), one SVG per idea — crossfade, AutoMix, ReplayGain, analysis, lyrics, Canvas, animated covers, plugins, Last.fm, Discord, the server, and the first steps' eight, plus the interface, the lyrics editor, theater mode, Release Radar and the playlist import for [What's new](#whats-new)), a title, the benefit in one sentence, badges, and its state — a switch drawn in the corner when the whole card toggles (`role="switch"`), a ring when it is on or installed, or actions underneath when it only presents. Nothing in it knows the screen it is on: the onboarding and the first steps use it. The [What's new](#whats-new) panel tells a feature at another scale, through `FeatureHero`, with the same scenes. The scenes are drawn in `currentColor` with the accent on the element that carries the idea, and animated in [`discovery.css`](../../src/components/discovery/discovery.css) with `transform` and `opacity` only — no blur, no filter ([invariant](../architecture/invariants.md#a-blurred-cover-is-blurred-once-not-once-per-frame)). Their motion stops under `prefers-reduced-motion` and under the reduced visual effects level (`data-effects="reduced"`), and each still reads standing still. The base styling is Tailwind utilities, which every skin paints; the fluid layout recomposes the cards and the modal with its tokens (surface tones, inks, the accent, no border), which the Liquid, Pulse, Editorial and Lounge materials repaint. +## What's new + +Once per update, the main window shows what changed in that version instead of leaving it to be found in Settings: [`WhatsNewPanel`](../../src/components/discovery/WhatsNewPanel.tsx), a modal carousel titled with the version, one page per feature. A page is a large illustration ([`FeatureHero`](../../src/components/discovery/FeatureHero.tsx): the [`FeatureArt`](#feature-cards) scene over a gradient of the feature's colour, with rows of its keywords sliding behind), a title, a few lines and a **Try it** button that goes to the feature. Under them: the page dots, Previous / Next, the arrow keys (in reading order, so reversed in Arabic), "Don't show at startup", and a link to the [embedded changelog](#embedded-changelog) in About, which stays the complete history. Inside the panel a key stops at the dialog, except Escape and Tab, which the modal hook hears on `document`: an arrow or Space there is not the player's. + +**The pages are written by hand**, per version, in [`src/lib/whatsNew.ts`](../../src/lib/whatsNew.ts): an id, a scene, a colour, an action, and what the action needs. Nothing is generated from the commits. + +**When it opens.** [`useWhatsNew`](../../src/hooks/useWhatsNew.ts), mounted by `AppLayout` and so never in the mini-player or the desktop lyrics, compares the running version with `app_setting['whats_new.seen_version']` once per launch, when the active profile and its libraries are known: + +| State | What happens | +| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| No marker, and nothing but a first run: one profile, no library, the onboarding never finished | A new installation. No panel (the journey has just told all of it), and the marker is set to this version, so the next launch does not take this one for an update | +| No marker, and a profile with a past: a library, several profiles, or the onboarding finished (a 1.x wrote none) | An update: every release up to this version counts as new | +| A release after the marker, up to this version | Pending: the newest such release opens when the way is clear | +| No release in that range (a correction release) | Nothing shows, and the marker moves to this version, unless it is a pre-release | + +Versions are compared as versions, never tested against `NULL` alone, which would disarm the panel after its first showing. Only the numbers decide what is pending: a release is named by its stable version, so `2.0.0-beta.1` shows the pages of `2.0.0`, and neither `beta.2` nor the stable `2.0.0` shows them again. For the same reason a pre-release never writes itself into the marker, which would call its stable seen: showing the pages writes the release's own version, and a beta with nothing to show writes nothing, so pages that land in `beta.2` still appear. The marker never moves backwards, so leaving the beta channel for an older stable does not arm a later one twice. Someone jumping over a version sees the newest release's pages only; the rest is in the changelog. + +**The marker moves when a release is shown**, by itself or by hand, not when it is found: closing the app behind another dialog leaves it to be seen later. It is app-wide because an update arrives once per installation, so a second profile does not see the panel again. + +**The way is clear** when nothing is in front of the user and they are there: no modal open (`useAnyModalOpen`, from the stack `useModalA11y` keeps, which covers the immersive view and every dialog), the onboarding neither open nor still being decided for this profile, no player bar tour or profile picker, and the window focused. The focus test is what keeps a launch `--minimized` by the OS from spending the panel in the tray: WebView2 reports a hidden page as visible. All of that has to hold for 800 ms, so a dialog closing in a chain does not find the panel in the next one's place. + +**Try it never lands on an empty screen.** A page declares what its action needs (`requires`). With an empty library, a page that needs one says so and its button becomes "Add music", which goes to the library; the lyrics editor also needs a library track playing, and says that instead. The rest adapts: the tag workshop opens on the album playing, else the one added last (as the first steps do), and theater mode, with nothing playing, starts the library shuffled, which its page says first. Theater mode from here lasts the visit: `openTheater` starts the view in it without writing `immersive.theater_mode`, so a try leaves no fullscreen behind. A page whose feature needs the network says so in its text. Settings actions name their group (`settingsGroup` on the `OPEN_VIEW_EVENT` request), which is revealed the way a search result is. + +**By hand.** A megaphone button in the top bar (`TopBar`, so both layouts; `data-topbar-whatsnew` for the fluid rail), with a dot while the release has not been seen, which only lasts when this profile turned the opening off. Settings → General has a row that opens the panel and the two choices about it, and About's changelog heading links to it, so it stays reachable when the button is hidden. A version with nothing to show (below the first release that has pages) hides all three. Both choices are per profile, through [`useProfileBooleanSetting`](../../src/hooks/useProfileSetting.ts): `ui.whats_new_auto_open` (default on; the panel's checkbox is its opposite) and `ui.whats_new_button` (default on). A profile that turned the opening off leaves the marker where it was, so another profile that did not still gets the panel. + +**Motion.** The gradient is painted once and never moves. Each keyword row is a layer of its own, translated by the compositor (`transform` only, no filter, no blur), and only the current page is mounted. The rows and the scenes pause while the window has no focus and stand still under `prefers-reduced-motion` and the [reduced effects level](#visual-effects), which also removes the plate's drop. The backdrop is flat, without the blur of the default modal shell. The dialog takes each skin's dialog rule through `[role="dialog"]` and the fluid tokens, so the materials repaint it without a rule of their own; it uses no `
` or `