UI rework: Spotify-style shell, unified Playlists, Recordings, plugin UI #32

Closed
opened 2026-10-07 19:42:07 +02:00 by SBDeveloper · 1 comment
Owner

Part of #13 (this spec reorders the remaining #13 work, see "Further Notes"). Slices are tracked under Tickets at the end.

Problem Statement

The app's UI is feature-driven, not user-friendly. Navigation is a row of text tabs (search, library, playlists, queue, settings). Playlists and Source Playlists live on two different pages, even though the listener only thinks "my playlists". The Queue is a flat list with up/down buttons. There is no persistent place for playlists, no big player view, and no search that is always at hand. The listener expects what Spotify or Deezer clients give: a bottom player, a playlist sidebar, a top navigation bar, a big player view and a real Queue.

The remaining #13 features (Radio, plugin UI views, lyrics, offline management, Recordings) also need a home in that shell. Building them in the old UI would mean building them twice.

Solution

Rework the UI into a conventional desktop music-app shell, shaped around this app's own idea: one Library across many Sources.

  • A top bar with back/forward, an always-visible search box and a settings gear.
  • A left sidebar with Library entries (Songs, Albums, Artists) and one merged list of Playlists. Rows carry a Source badge. Chips filter the list by Source (All, Deezer, Local, Offline). + creates a Playlist.
  • A bottom player bar and a big player overlay (art, controls, seek, volume, and a tab strip: Queue now, Lyrics later, plugin tabs).
  • A Queue panel like Spotify's: Now playing, Next in queue (manually added), Next from , with drag-to-reorder.
  • One Playlist type. A Deezer playlist shows up as a Playlist linked to Deezer, and Tracks from other Sources can be added to it.
  • Recording-aware rows: the same song from several Sources is one row, with a Source picker in the menu.
  • Plugin UI: a Plugin can add an overlay tab, a sidebar entry or a settings section, described declaratively and rendered by the host.

Delivery is in four phases: A headless core, B shell slices, C plugins, D Windows/macOS and fuzzy matching. The app keeps working after every slice.

User Stories

Shell and navigation

  1. As a listener, I want a bottom player bar that is always visible, so that I can control playback from any page.
  2. As a listener, I want a left sidebar listing my Playlists, so that I can reach them in one click.
  3. As a listener, I want a top bar with back and forward, so that I can return from an album to the artist I came from.
  4. As a listener, I want an always-visible search box, so that I can search without switching pages.
  5. As a listener, I want the existing / and Ctrl+K shortcuts to focus the search box, so that muscle memory keeps working.
  6. As a listener, I want a settings gear in the top bar, so that accounts, folders, offline cache and Plugin settings have one clear entry.
  7. As a listener, I want the app to open on my Library, so that I land on my music.
  8. As a listener, I want to collapse and resize the sidebar, and have the app remember it, so that the layout fits how I work.
  9. As a keyboard user, I want visible focus states and working Tab order throughout the shell, so that I can drive the app without a mouse.
  10. As a listener, I want album, artist, Playlist and search result pages reachable by links anywhere a name appears, so that I can browse naturally.
  11. As a listener, I want deezer:// and pasted deezer.com links to land on the right page in the new shell, so that existing link handling keeps working.

Sidebar and Playlists

  1. As a listener, I want one list of Playlists, so that I do not have to know which Source owns what.
  2. As a listener, I want each Playlist row to show a Source badge, so that I can tell where it lives.
  3. As a listener, I want to filter the sidebar by Source (All, Deezer, Local, Offline), so that I can focus on one part of my music.
  4. As a listener, I want an Offline filter, so that I can see what plays without a network.
  5. As a listener, I want a + button to create a Playlist, so that I do not need a separate page.
  6. As a listener, I want to rename or delete a Playlist from a right-click menu on its sidebar row, so that management is where the Playlist is.
  7. As a listener, I want to drag Tracks from any Track list onto a sidebar Playlist, so that adding is fast.
  8. As a Deezer user, I want my Deezer playlists to appear as Playlists linked to Deezer, so that I do not manage them in a separate place.
  9. As a listener, I want to add Tracks from other Sources (local files, third-party Sources) to a Deezer-linked Playlist, so that I can combine Sources in one Playlist.
  10. As a listener, I want Deezer Tracks that I add to or remove from a linked Playlist to be written to Deezer, so that my Deezer apps stay in sync.
  11. As a listener, I want Tracks that Deezer cannot hold to be marked "not on Deezer", so that I know why they are missing elsewhere.
  12. As a listener, I want changes I make in the Deezer phone app to arrive in the linked Playlist, so that both stay current.
  13. As a listener, I want my other-Source Tracks to keep their place in a linked Playlist when Deezer-side Tracks change, so that my order is not scrambled.
  14. As a listener, I want a Playlist to exist without any link, so that Playlists mixing only local and third-party Tracks still work.
  15. As a listener, I want to make a whole Playlist offline from its page, so that I can listen on a plane.
  16. As a listener, I want my existing app Playlists to keep working after the rework, so that I lose nothing.
  17. As a listener, I want copying a Deezer playlist to still be possible, so that I can keep a personal version.

Player bar and big player

  1. As a listener, I want the bottom bar to show art, title, artist, play/pause, previous, next, shuffle, repeat, seek and volume, so that it behaves like other players.
  2. As a listener, I want to click the cover or an expand button to open a big player view, so that I can see the song large.
  3. As a listener, I want to close the big player with Esc or a button, so that I return where I was.
  4. As a listener, I want the big player to have tabs, with Queue available, so that I can see what comes next without leaving.
  5. As a listener, I want a lyrics tab in the big player later, so that I can sing along.
  6. As a listener, I want the big player to be a view inside the window, so that it feels fast and tiling window managers behave.
  7. As a listener, I want the current Track's Source and quality shown, so that I know what I am hearing.

Queue

  1. As a listener, I want the Queue shown as Now playing, Next in queue, and Next from , so that I understand what plays when.
  2. As a listener, I want Tracks I add manually to play before the rest of the context, so that "add to queue" does what I mean.
  3. As a listener, I want to drag Queue entries to reorder them, so that I do not click up/down repeatedly.
  4. As a listener, I want to remove an entry from the Queue with one click, so that the list stays tidy.
  5. As a listener, I want to clear the Queue, so that I can start over.
  6. As a listener, I want starting a new Playlist or album to replace the context part but keep the manually queued Tracks, so that I do not lose them.
  7. As a listener, I want the Queue panel to open from the bottom bar without leaving my page, so that I can peek at it.
  8. As a listener, I want Radio Tracks shown in the Queue under their own heading, so that I can skip or remove them.
  9. As a listener, I want an autoplay toggle in the Queue panel, so that I can turn Radio on or off.
  10. As a listener, I want my Queue to survive a restart as it does now, so that I can resume.
  11. As a listener, I want Queue entries to show the Track's Source, so that I know what plays from where.

Track rows and Recordings

  1. As a listener, I want a Track row to show only play and favourite on hover, so that lists look clean.
  2. As a listener, I want a ⋯ button and a right-click menu on every Track with play next, add to queue, add to Playlist, favourite, go to album, go to artist and make offline, so that all actions are one place.
  3. As a listener, I want the same song from several Sources to appear as one row, so that my Library has no duplicates.
  4. As a listener, I want ISRC matches to group automatically, so that the common case needs no effort.
  5. As a listener, I want to choose which Source plays for a Recording, so that I can prefer local or lossless.
  6. As a listener, I want to set a Source ranking, so that Recordings pick the right Source by default.
  7. As a listener, I want playback to fall back to the next Source in my ranking when a Track is unavailable, so that music keeps playing.
  8. As a listener, I want to un-group a Recording, so that a wrong match is undone without touching my Playlists.
  9. As a listener, I want a Recording with one Track to look like any other row, so that grouping is invisible when it does not apply.

Radio

  1. As a listener, I want playback to continue when the Queue runs out, so that music does not stop.
  2. As a listener, I want Radio Tracks to come from the Source of the last played Track, so that the continuation is relevant.
  3. As a listener, I want Radio to end cleanly if the Source cannot continue or is offline, so that playback stops gracefully.

Plugins and plugin UI

  1. As a user, I want to install a Plugin from a file or URL, see its requested capabilities, and consent, so that I stay in control.
  2. As a user, I want to enable, disable and uninstall Plugins, so that I can manage them.
  3. As a user, I want a Plugin to be able to add an overlay tab, so that features like lyrics appear where I listen.
  4. As a user, I want a Plugin to be able to add a sidebar entry that opens its own page, so that a Plugin can offer a full view.
  5. As a user, I want a Plugin to be able to add a settings section, so that Plugin settings sit with the others.
  6. As a user, I want Plugin UI to match the app's look, so that the app feels like one product.
  7. As a user, I want a Plugin's UI to be denied unless I consented to the UI capability, so that consent is meaningful.
  8. As a user, I want a misbehaving Plugin to affect only its own view, so that it cannot break the app.
  9. As a Plugin author, I want to describe my view declaratively (text, lists, buttons, images, Track lists) and handle events, so that I do not need a web runtime.
  10. As a Plugin author, I want my UI to receive playback events (current Track, position), so that a lyrics view can follow the song.
  11. As a Plugin author, I want the bundled lyrics Plugin to use the same UI API as third parties, so that the contract is proven.
  12. As a user, I want lyrics for the current Track in the big player, so that I can sing along.
  13. As a user, I want Discord presence and scrobbling to remain optional event Plugins that do not need UI, so that nothing is shared without my choice.

Offline and settings

  1. As a listener, I want offline cache size, the caching Quality cap and disallowed Tracks shown under settings, so that I can manage disk.
  2. As a listener, I want an offline marker on Track rows and Playlists, so that I know what plays without a network.
  3. As a listener, I want accounts, music folders and the Source ranking under settings, so that configuration has one home.

Quality and migration

  1. As a listener, I want the new UI to keep all current functionality (search, library, favourites, links, folders, account, offline), so that nothing regresses.
  2. As a listener, I want the old tab UI removed once replaced, so that there is one UI to maintain.

Implementation Decisions

Delivery phases (each slice keeps the app working; one child issue per slice, Part of this spec, with blocked-by notes):

  • A, headless core: A1 Queue origin and context. A2 Radio. A3 linked Playlists. A4 Recordings. During A, the old UI gets a minimal patch that merges its two playlist pages into one so the app keeps working. It is throwaway, replaced in B2.
  • B, shell slices: B1 shell, router, top bar. B2 sidebar (needs A3). B3 bottom bar and big player overlay. B4 Queue panel (needs A1, A2). B5 Track menus and Recording Source picker (needs A4). The old tab UI components are deleted as each is replaced.
  • C, plugins: C1 install, manifest, consent, lifecycle (#13 Public plugin API, headless). C2 declarative UI API and the three slots, with an ADR. C3 lyrics plugin. After C1: the plugin registry, Discord presence and scrobbling event Plugins.
  • D: Windows/macOS support and the fuzzy-match suggestion UI for Recordings.

Shell

  • Dark theme only. Hand-rolled CSS, no component library. Light token cleanup (colours, spacing, focus). Sidebar is resizable and collapsible, with width and collapsed state persisted. No Home page: core holds no recommendation data.
  • Navigation is a small in-app router with history (back/forward), in plain Svelte, no SvelteKit. Routes: Songs, Albums, Artists, Playlist, album, artist, search results, settings, plugin pages.
  • Search is always visible in the top bar and routes to a results page. Existing keyboard shortcuts stay.
  • The big player is an in-window overlay, closed with Esc, with a tab strip. Built-in tabs: Queue now; Lyrics arrives as a Plugin tab.
  • The Queue panel is a right-hand slide-in, also reachable as an overlay tab.
  • Track rows show play and favourite on hover. A ⋯ button and a right-click open one shared Track menu. Tracks drag onto sidebar Playlists.
  • The sidebar lists Library entries (Songs, Albums, Artists) and one merged Playlist list with Source badges, Source filter chips (All, Deezer, Local, Offline), + to create, a right-click menu for rename and delete.
  • A UI view registry holds the shell's slots (overlay tab, sidebar entry, settings section). Built-ins register through it, and Plugin views register through it later.

Queue (core)

  • Each Queue entry carries an origin: manual, context or radio. The Queue stores the context it was started from (label and reference, e.g. a Playlist or album).
  • Manual entries play before context entries. Starting a new context replaces the context entries and keeps manual entries. A clear action removes everything.
  • Storage stays a flat ordered list. Drag-reorder replaces up/down moves. The persisted Queue format must read old saved Queues.
  • Radio: when the Queue runs out and autoplay is on, the host asks the Source of the last played Track for a continuation. Appended Tracks are ordinary entries with origin radio. A Source without continuation appends nothing and playback ends cleanly. Autoplay is a setting with a toggle in the Queue panel.

Playlists (core)

  • One Playlist type replaces the Playlist / Source Playlist split. The glossary term Source Playlist is removed; a new term for a Playlist linked to a Source is added, and Playlist is redefined (see Further Notes). A Playlist has zero or one link to a Source.
  • A Deezer-linked Playlist mirrors the Deezer playlist. Tracks of the linked Source that are added or removed in-app are written through to the Source. Tracks from other Sources are stored app-side only and marked as not on the linked Source.
  • Sync: the Source owns its Tracks. Syncing replaces the linked Source's Tracks and keeps app-side extras anchored after the Track that preceded them. If that anchor Track disappears, the extra re-anchors to the previous surviving Track, or the start.
  • Existing app Playlists and existing Deezer Source Playlist behaviour (read, create, edit, copy) carry over. Existing stored Playlists load unchanged. Offline marking of a whole Playlist keeps working for all Playlists.

Recordings (core)

  • Implements ADR 0003. A Recording groups equivalent Library Tracks; Tracks keep their single Source, so TrackRef, Playlists, the Queue and Favourites are unchanged. Ungrouping is a deletion.
  • ISRC matches group automatically. Fuzzy matching is out of this spec and arrives in phase D as user-accepted suggestions.
  • Source ranking is a user setting. Each Recording may pin a Track. Playing a Recording plays the best available Track by pin, then ranking, falling back down the ranking when a Track is unavailable (Source offline, file missing, offline-disallowed).
  • Matching applies to Library Tracks only, not Catalog results, albums or artists. The Library exposes Recording rows, and a one-Track Recording is indistinguishable from a plain entry.

Plugin UI (phase C)

  • A Plugin describes its UI declaratively as a JSON tree (text, list, button, image, Track list and similar primitives) and handles events. The host renders it with native Svelte components, so it follows the theme and stays sandboxed. A UI capability in the manifest is required and consent-gated, like other capabilities. The existing Plugin manifest already reserves an optional UI bundle and a UI views capability.
  • Slots: overlay tab, sidebar entry (own routed page), settings section.
  • Plugin UI receives playback events needed for lyrics (current Track, position).
  • Plugin failures are contained to their own view.
  • A new ADR records the rendering model and why an iframe was rejected. The lyrics Plugin is the first consumer.

Docs

  • GLOSSARY.md: remove Source Playlist, redefine Playlist, add the linked-Playlist term, add Queue origin and context to Queue. ADRs: one for linked Playlists, one for declarative Plugin UI.

Testing Decisions

  • A good test drives external behaviour only: what a caller gets back from the public API or a pure function, not how internals are arranged.
  • Seam 1 (primary): the Core public API, as integration tests under the existing core tests, using the fake Source plugin and the null audio output. Covers: Queue origin and context rules; Radio append, off switch and clean end; linked Playlists (write-through, remote sync, anchored extras, unlinked Playlists, existing data loading); Recordings (ISRC grouping, ranking, pin, fallback, ungroup); Plugin install, consent and lifecycle; and the declarative UI tree a Plugin returns and its event round-trips.
  • Seam 2: pure UI logic in ui/src/lib with vitest, following the existing shortcuts tests. Only logic extracted into plain functions: router and history stack, sidebar Source filtering, Queue section grouping, drag-reorder index math, persisted layout state. No component or DOM tests.
  • Svelte markup, CSS and overlay visuals are not unit tested. They are checked by running the app.
  • Prior art: the existing Queue, Playlists, deezer_playlists, offline and plugin_host core tests, and shortcuts.test.ts.

Out of Scope

  • Home page and any recommendations.
  • Light theme and a component library.
  • Fuzzy Recording matching and its suggestion UI (phase D).
  • Windows/macOS support (phase D).
  • Social integrations beyond the plugin API they need (Discord presence and scrobbling are event Plugins, scheduled after C1).
  • Plugin registry design (scheduled after C1, own issue).
  • Mobile or responsive layouts below a desktop window width.

Further Notes

Reordering of #13: Offline Tracks (item 1) and MPRIS/media keys/notifications (item 5) are done. The remaining items move as follows. Radio becomes A2. Track matching (item 8) becomes A4 (ISRC only now). The Public plugin API (item 3) is split into C1 (install flow) and C2 (UI API). The Plugin registry (item 4) follows C1. Social integrations (item 6) follow C1, with lyrics moved to C3. Windows/macOS (item 7) moves to D. #13's task list and delivery order are updated to match when child issues are created.

Terminology: the glossary currently defines Source Playlist as holding only one Source's Tracks. That no longer holds once a Playlist can link to a Source and carry Tracks from others. The glossary and an ADR must be updated before slice A3 starts.

Interaction to watch: Offline Tracks in linked Playlists, since app-side Tracks of other Sources can be offline too. Offline marking works per Track and so is unaffected, but "make Playlist offline" must cover all Tracks in the Playlist regardless of Source.

Tickets

Status 2026-10-08: all phases (A to D) are done, including #28 Windows/macOS, #30 fuzzy matching and #22 plugin registry.

New tickets for this spec:

  • #33 Glossary and ADR: linked Playlists
  • #34 Queue origin and context
  • #39 Linked Playlists (blocked by #33)
  • #36 Shell, router and top bar
  • #40 Sidebar with merged Playlists (blocked by #39, #36)
  • #41 Bottom player bar and big player overlay (blocked by #36)
  • #42 Queue panel (blocked by #34, #17, #41)
  • #43 Track menus and Recording Source picker (blocked by #29, #40, #31)
  • #44 Settings page and old UI removal (blocked by #40, #42, #43, #31)
  • #45 Declarative plugin UI and slots (blocked by #41, #44, #19)

Existing #13 tickets this spec reshapes (bodies updated):

  • #17 Radio (now blocked by #34, adds origin radio and old-UI toggle)
  • #18, #19, #20 plugin install, enforcement, URL install (unchanged)
  • #21 now DSP and playback events only; UI views moved to #45
  • #29 Recordings with ISRC, #30 fuzzy suggestions (UI lands in the new shell)
  • #31 offline UI wiring (blocks #43 and #44)
  • #27 lyrics Plugin (now blocked by #45, needs a provider decision)
  • #28 Windows/macOS (now blocked by #44)
Part of #13 (this spec reorders the remaining #13 work, see "Further Notes"). Slices are tracked under Tickets at the end. ## Problem Statement The app's UI is feature-driven, not user-friendly. Navigation is a row of text tabs (search, library, playlists, queue, settings). Playlists and Source Playlists live on two different pages, even though the listener only thinks "my playlists". The Queue is a flat list with up/down buttons. There is no persistent place for playlists, no big player view, and no search that is always at hand. The listener expects what Spotify or Deezer clients give: a bottom player, a playlist sidebar, a top navigation bar, a big player view and a real Queue. The remaining #13 features (Radio, plugin UI views, lyrics, offline management, Recordings) also need a home in that shell. Building them in the old UI would mean building them twice. ## Solution Rework the UI into a conventional desktop music-app shell, shaped around this app's own idea: one Library across many Sources. - A **top bar** with back/forward, an always-visible search box and a settings gear. - A **left sidebar** with Library entries (Songs, Albums, Artists) and one merged list of **Playlists**. Rows carry a Source badge. Chips filter the list by Source (All, Deezer, Local, Offline). `+` creates a Playlist. - A **bottom player bar** and a **big player overlay** (art, controls, seek, volume, and a tab strip: Queue now, Lyrics later, plugin tabs). - A **Queue panel** like Spotify's: Now playing, Next in queue (manually added), Next from <context>, with drag-to-reorder. - **One Playlist type.** A Deezer playlist shows up as a Playlist linked to Deezer, and Tracks from other Sources can be added to it. - **Recording-aware rows**: the same song from several Sources is one row, with a Source picker in the menu. - **Plugin UI**: a Plugin can add an overlay tab, a sidebar entry or a settings section, described declaratively and rendered by the host. Delivery is in four phases: A headless core, B shell slices, C plugins, D Windows/macOS and fuzzy matching. The app keeps working after every slice. ## User Stories Shell and navigation 1. As a listener, I want a bottom player bar that is always visible, so that I can control playback from any page. 2. As a listener, I want a left sidebar listing my Playlists, so that I can reach them in one click. 3. As a listener, I want a top bar with back and forward, so that I can return from an album to the artist I came from. 4. As a listener, I want an always-visible search box, so that I can search without switching pages. 5. As a listener, I want the existing `/` and Ctrl+K shortcuts to focus the search box, so that muscle memory keeps working. 6. As a listener, I want a settings gear in the top bar, so that accounts, folders, offline cache and Plugin settings have one clear entry. 7. As a listener, I want the app to open on my Library, so that I land on my music. 8. As a listener, I want to collapse and resize the sidebar, and have the app remember it, so that the layout fits how I work. 9. As a keyboard user, I want visible focus states and working Tab order throughout the shell, so that I can drive the app without a mouse. 10. As a listener, I want album, artist, Playlist and search result pages reachable by links anywhere a name appears, so that I can browse naturally. 11. As a listener, I want deezer:// and pasted deezer.com links to land on the right page in the new shell, so that existing link handling keeps working. Sidebar and Playlists 12. As a listener, I want one list of Playlists, so that I do not have to know which Source owns what. 13. As a listener, I want each Playlist row to show a Source badge, so that I can tell where it lives. 14. As a listener, I want to filter the sidebar by Source (All, Deezer, Local, Offline), so that I can focus on one part of my music. 15. As a listener, I want an Offline filter, so that I can see what plays without a network. 16. As a listener, I want a `+` button to create a Playlist, so that I do not need a separate page. 17. As a listener, I want to rename or delete a Playlist from a right-click menu on its sidebar row, so that management is where the Playlist is. 18. As a listener, I want to drag Tracks from any Track list onto a sidebar Playlist, so that adding is fast. 19. As a Deezer user, I want my Deezer playlists to appear as Playlists linked to Deezer, so that I do not manage them in a separate place. 20. As a listener, I want to add Tracks from other Sources (local files, third-party Sources) to a Deezer-linked Playlist, so that I can combine Sources in one Playlist. 21. As a listener, I want Deezer Tracks that I add to or remove from a linked Playlist to be written to Deezer, so that my Deezer apps stay in sync. 22. As a listener, I want Tracks that Deezer cannot hold to be marked "not on Deezer", so that I know why they are missing elsewhere. 23. As a listener, I want changes I make in the Deezer phone app to arrive in the linked Playlist, so that both stay current. 24. As a listener, I want my other-Source Tracks to keep their place in a linked Playlist when Deezer-side Tracks change, so that my order is not scrambled. 25. As a listener, I want a Playlist to exist without any link, so that Playlists mixing only local and third-party Tracks still work. 26. As a listener, I want to make a whole Playlist offline from its page, so that I can listen on a plane. 27. As a listener, I want my existing app Playlists to keep working after the rework, so that I lose nothing. 28. As a listener, I want copying a Deezer playlist to still be possible, so that I can keep a personal version. Player bar and big player 29. As a listener, I want the bottom bar to show art, title, artist, play/pause, previous, next, shuffle, repeat, seek and volume, so that it behaves like other players. 30. As a listener, I want to click the cover or an expand button to open a big player view, so that I can see the song large. 31. As a listener, I want to close the big player with Esc or a button, so that I return where I was. 32. As a listener, I want the big player to have tabs, with Queue available, so that I can see what comes next without leaving. 33. As a listener, I want a lyrics tab in the big player later, so that I can sing along. 34. As a listener, I want the big player to be a view inside the window, so that it feels fast and tiling window managers behave. 35. As a listener, I want the current Track's Source and quality shown, so that I know what I am hearing. Queue 36. As a listener, I want the Queue shown as Now playing, Next in queue, and Next from <context>, so that I understand what plays when. 37. As a listener, I want Tracks I add manually to play before the rest of the context, so that "add to queue" does what I mean. 38. As a listener, I want to drag Queue entries to reorder them, so that I do not click up/down repeatedly. 39. As a listener, I want to remove an entry from the Queue with one click, so that the list stays tidy. 40. As a listener, I want to clear the Queue, so that I can start over. 41. As a listener, I want starting a new Playlist or album to replace the context part but keep the manually queued Tracks, so that I do not lose them. 42. As a listener, I want the Queue panel to open from the bottom bar without leaving my page, so that I can peek at it. 43. As a listener, I want Radio Tracks shown in the Queue under their own heading, so that I can skip or remove them. 44. As a listener, I want an autoplay toggle in the Queue panel, so that I can turn Radio on or off. 45. As a listener, I want my Queue to survive a restart as it does now, so that I can resume. 46. As a listener, I want Queue entries to show the Track's Source, so that I know what plays from where. Track rows and Recordings 47. As a listener, I want a Track row to show only play and favourite on hover, so that lists look clean. 48. As a listener, I want a `⋯` button and a right-click menu on every Track with play next, add to queue, add to Playlist, favourite, go to album, go to artist and make offline, so that all actions are one place. 49. As a listener, I want the same song from several Sources to appear as one row, so that my Library has no duplicates. 50. As a listener, I want ISRC matches to group automatically, so that the common case needs no effort. 51. As a listener, I want to choose which Source plays for a Recording, so that I can prefer local or lossless. 52. As a listener, I want to set a Source ranking, so that Recordings pick the right Source by default. 53. As a listener, I want playback to fall back to the next Source in my ranking when a Track is unavailable, so that music keeps playing. 54. As a listener, I want to un-group a Recording, so that a wrong match is undone without touching my Playlists. 55. As a listener, I want a Recording with one Track to look like any other row, so that grouping is invisible when it does not apply. Radio 56. As a listener, I want playback to continue when the Queue runs out, so that music does not stop. 57. As a listener, I want Radio Tracks to come from the Source of the last played Track, so that the continuation is relevant. 58. As a listener, I want Radio to end cleanly if the Source cannot continue or is offline, so that playback stops gracefully. Plugins and plugin UI 59. As a user, I want to install a Plugin from a file or URL, see its requested capabilities, and consent, so that I stay in control. 60. As a user, I want to enable, disable and uninstall Plugins, so that I can manage them. 61. As a user, I want a Plugin to be able to add an overlay tab, so that features like lyrics appear where I listen. 62. As a user, I want a Plugin to be able to add a sidebar entry that opens its own page, so that a Plugin can offer a full view. 63. As a user, I want a Plugin to be able to add a settings section, so that Plugin settings sit with the others. 64. As a user, I want Plugin UI to match the app's look, so that the app feels like one product. 65. As a user, I want a Plugin's UI to be denied unless I consented to the UI capability, so that consent is meaningful. 66. As a user, I want a misbehaving Plugin to affect only its own view, so that it cannot break the app. 67. As a Plugin author, I want to describe my view declaratively (text, lists, buttons, images, Track lists) and handle events, so that I do not need a web runtime. 68. As a Plugin author, I want my UI to receive playback events (current Track, position), so that a lyrics view can follow the song. 69. As a Plugin author, I want the bundled lyrics Plugin to use the same UI API as third parties, so that the contract is proven. 70. As a user, I want lyrics for the current Track in the big player, so that I can sing along. 71. As a user, I want Discord presence and scrobbling to remain optional event Plugins that do not need UI, so that nothing is shared without my choice. Offline and settings 72. As a listener, I want offline cache size, the caching Quality cap and disallowed Tracks shown under settings, so that I can manage disk. 73. As a listener, I want an offline marker on Track rows and Playlists, so that I know what plays without a network. 74. As a listener, I want accounts, music folders and the Source ranking under settings, so that configuration has one home. Quality and migration 75. As a listener, I want the new UI to keep all current functionality (search, library, favourites, links, folders, account, offline), so that nothing regresses. 76. As a listener, I want the old tab UI removed once replaced, so that there is one UI to maintain. ## Implementation Decisions Delivery phases (each slice keeps the app working; one child issue per slice, `Part of` this spec, with blocked-by notes): - **A, headless core**: A1 Queue origin and context. A2 Radio. A3 linked Playlists. A4 Recordings. During A, the old UI gets a minimal patch that merges its two playlist pages into one so the app keeps working. It is throwaway, replaced in B2. - **B, shell slices**: B1 shell, router, top bar. B2 sidebar (needs A3). B3 bottom bar and big player overlay. B4 Queue panel (needs A1, A2). B5 Track menus and Recording Source picker (needs A4). The old tab UI components are deleted as each is replaced. - **C, plugins**: C1 install, manifest, consent, lifecycle (#13 Public plugin API, headless). C2 declarative UI API and the three slots, with an ADR. C3 lyrics plugin. After C1: the plugin registry, Discord presence and scrobbling event Plugins. - **D**: Windows/macOS support and the fuzzy-match suggestion UI for Recordings. Shell - Dark theme only. Hand-rolled CSS, no component library. Light token cleanup (colours, spacing, focus). Sidebar is resizable and collapsible, with width and collapsed state persisted. No Home page: core holds no recommendation data. - Navigation is a small in-app router with history (back/forward), in plain Svelte, no SvelteKit. Routes: Songs, Albums, Artists, Playlist, album, artist, search results, settings, plugin pages. - Search is always visible in the top bar and routes to a results page. Existing keyboard shortcuts stay. - The big player is an in-window overlay, closed with Esc, with a tab strip. Built-in tabs: Queue now; Lyrics arrives as a Plugin tab. - The Queue panel is a right-hand slide-in, also reachable as an overlay tab. - Track rows show play and favourite on hover. A `⋯` button and a right-click open one shared Track menu. Tracks drag onto sidebar Playlists. - The sidebar lists Library entries (Songs, Albums, Artists) and one merged Playlist list with Source badges, Source filter chips (All, Deezer, Local, Offline), `+` to create, a right-click menu for rename and delete. - A UI view registry holds the shell's slots (overlay tab, sidebar entry, settings section). Built-ins register through it, and Plugin views register through it later. Queue (core) - Each Queue entry carries an origin: `manual`, `context` or `radio`. The Queue stores the context it was started from (label and reference, e.g. a Playlist or album). - Manual entries play before context entries. Starting a new context replaces the context entries and keeps manual entries. A clear action removes everything. - Storage stays a flat ordered list. Drag-reorder replaces up/down moves. The persisted Queue format must read old saved Queues. - Radio: when the Queue runs out and autoplay is on, the host asks the Source of the last played Track for a continuation. Appended Tracks are ordinary entries with origin `radio`. A Source without continuation appends nothing and playback ends cleanly. Autoplay is a setting with a toggle in the Queue panel. Playlists (core) - One Playlist type replaces the Playlist / Source Playlist split. The glossary term **Source Playlist** is removed; a new term for a Playlist linked to a Source is added, and **Playlist** is redefined (see Further Notes). A Playlist has zero or one link to a Source. - A Deezer-linked Playlist mirrors the Deezer playlist. Tracks of the linked Source that are added or removed in-app are written through to the Source. Tracks from other Sources are stored app-side only and marked as not on the linked Source. - Sync: the Source owns its Tracks. Syncing replaces the linked Source's Tracks and keeps app-side extras anchored after the Track that preceded them. If that anchor Track disappears, the extra re-anchors to the previous surviving Track, or the start. - Existing app Playlists and existing Deezer Source Playlist behaviour (read, create, edit, copy) carry over. Existing stored Playlists load unchanged. Offline marking of a whole Playlist keeps working for all Playlists. Recordings (core) - Implements ADR 0003. A Recording groups equivalent Library Tracks; Tracks keep their single Source, so `TrackRef`, Playlists, the Queue and Favourites are unchanged. Ungrouping is a deletion. - ISRC matches group automatically. Fuzzy matching is out of this spec and arrives in phase D as user-accepted suggestions. - Source ranking is a user setting. Each Recording may pin a Track. Playing a Recording plays the best available Track by pin, then ranking, falling back down the ranking when a Track is unavailable (Source offline, file missing, offline-disallowed). - Matching applies to Library Tracks only, not Catalog results, albums or artists. The Library exposes Recording rows, and a one-Track Recording is indistinguishable from a plain entry. Plugin UI (phase C) - A Plugin describes its UI declaratively as a JSON tree (text, list, button, image, Track list and similar primitives) and handles events. The host renders it with native Svelte components, so it follows the theme and stays sandboxed. A UI capability in the manifest is required and consent-gated, like other capabilities. The existing Plugin manifest already reserves an optional UI bundle and a UI views capability. - Slots: overlay tab, sidebar entry (own routed page), settings section. - Plugin UI receives playback events needed for lyrics (current Track, position). - Plugin failures are contained to their own view. - A new ADR records the rendering model and why an iframe was rejected. The lyrics Plugin is the first consumer. Docs - `GLOSSARY.md`: remove Source Playlist, redefine Playlist, add the linked-Playlist term, add Queue origin and context to Queue. ADRs: one for linked Playlists, one for declarative Plugin UI. ## Testing Decisions - A good test drives external behaviour only: what a caller gets back from the public API or a pure function, not how internals are arranged. - **Seam 1 (primary): the `Core` public API**, as integration tests under the existing core tests, using the fake Source plugin and the null audio output. Covers: Queue origin and context rules; Radio append, off switch and clean end; linked Playlists (write-through, remote sync, anchored extras, unlinked Playlists, existing data loading); Recordings (ISRC grouping, ranking, pin, fallback, ungroup); Plugin install, consent and lifecycle; and the declarative UI tree a Plugin returns and its event round-trips. - **Seam 2: pure UI logic in `ui/src/lib` with vitest**, following the existing shortcuts tests. Only logic extracted into plain functions: router and history stack, sidebar Source filtering, Queue section grouping, drag-reorder index math, persisted layout state. No component or DOM tests. - Svelte markup, CSS and overlay visuals are not unit tested. They are checked by running the app. - Prior art: the existing Queue, Playlists, deezer_playlists, offline and plugin_host core tests, and `shortcuts.test.ts`. ## Out of Scope - Home page and any recommendations. - Light theme and a component library. - Fuzzy Recording matching and its suggestion UI (phase D). - Windows/macOS support (phase D). - Social integrations beyond the plugin API they need (Discord presence and scrobbling are event Plugins, scheduled after C1). - Plugin registry design (scheduled after C1, own issue). - Mobile or responsive layouts below a desktop window width. ## Further Notes Reordering of #13: Offline Tracks (item 1) and MPRIS/media keys/notifications (item 5) are done. The remaining items move as follows. Radio becomes A2. Track matching (item 8) becomes A4 (ISRC only now). The Public plugin API (item 3) is split into C1 (install flow) and C2 (UI API). The Plugin registry (item 4) follows C1. Social integrations (item 6) follow C1, with lyrics moved to C3. Windows/macOS (item 7) moves to D. #13's task list and delivery order are updated to match when child issues are created. Terminology: the glossary currently defines **Source Playlist** as holding only one Source's Tracks. That no longer holds once a Playlist can link to a Source and carry Tracks from others. The glossary and an ADR must be updated before slice A3 starts. Interaction to watch: Offline Tracks in linked Playlists, since app-side Tracks of other Sources can be offline too. Offline marking works per Track and so is unaffected, but "make Playlist offline" must cover all Tracks in the Playlist regardless of Source. ## Tickets Status 2026-10-08: all phases (A to D) are done, including #28 Windows/macOS, #30 fuzzy matching and #22 plugin registry. New tickets for this spec: - [x] #33 Glossary and ADR: linked Playlists - [x] #34 Queue origin and context - [x] #39 Linked Playlists (blocked by #33) - [x] #36 Shell, router and top bar - [x] #40 Sidebar with merged Playlists (blocked by #39, #36) - [x] #41 Bottom player bar and big player overlay (blocked by #36) - [x] #42 Queue panel (blocked by #34, #17, #41) - [x] #43 Track menus and Recording Source picker (blocked by #29, #40, #31) - [x] #44 Settings page and old UI removal (blocked by #40, #42, #43, #31) - [x] #45 Declarative plugin UI and slots (blocked by #41, #44, #19) Existing #13 tickets this spec reshapes (bodies updated): - #17 Radio (now blocked by #34, adds origin `radio` and old-UI toggle) - #18, #19, #20 plugin install, enforcement, URL install (unchanged) - #21 now DSP and playback events only; UI views moved to #45 - #29 Recordings with ISRC, #30 fuzzy suggestions (UI lands in the new shell) - #31 offline UI wiring (blocks #43 and #44) - #27 lyrics Plugin (now blocked by #45, needs a provider decision) - #28 Windows/macOS (now blocked by #44)
Author
Owner

Verified in code: every item is delivered (#22, #28, #30 closed, code and tests present). Closing.

Verified in code: every item is delivered (#22, #28, #30 closed, code and tests present). Closing.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SBDeveloper/medley#32
No description provided.