Skip to content

Desktop client + shared core/data layer - #17

Draft
LizAinslie wants to merge 69 commits into
mainfrom
feature/desktop-compose
Draft

Desktop client + shared core/data layer#17
LizAinslie wants to merge 69 commits into
mainfrom
feature/desktop-compose

Conversation

@LizAinslie

@LizAinslie LizAinslie commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Brings the Compose Desktop client online and moves the shared data model, Room
catalog, and playback primitives into :core so Android and desktop share one
source of truth.

Highlights

Desktop client (:desktop, new)

  • Full Compose Desktop app: Home / Explore / My Stuff, album/artist/playlist
    pages, now-playing bar + sidebar queue, context menus, metadata editing,
    provider (Jellyfin / Subsonic / local) management.
  • Playback via LibVLC (vlcj) with bundled natives for Windows/macOS; Linux
    packages depend on distro vlc.
  • OS integration: Linux MPRIS + Windows SMTC through a vendored :mediasession
    fork, plus macOS Now Playing stubs.
  • Durable local index (DesktopIndexStore) + per-source scan checkpoints, so a
    large Jellyfin/Navidrome walk survives restart.
  • MPRIS/SMTC media-key + volume handling routed through the vendored library.

Shared UI (:components, new)

  • Compose Multiplatform components for cover art, album/artist/playlist pages,
    player bar, now-playing sidebar, context-menu DSL (prediction-cone submenus),
    theme, and settings rows.
  • Android now renders the same components instead of bespoke screens.

Shared core (:core, expanded)

  • Song/AlbumItem/ArtistItem/credits/identity model.
  • KMP Room catalog (YuriDatabase, CatalogDao, entities, migrations) with an
    expect/actual @ConstructedBy constructor for Android + JVM.
  • CatalogRepository and library sources (Jellyfin, Subsonic, source registry)
    moved out of :app.
  • PlaybackEngine SPI, PlayerSession queue host, radio algorithms, and
    platform seams (appDirectories, hostOs, OsMediaControls, logging).

Android

  • Unified onto the shared :components UI and shared CatalogRepository
    (behind a LocalScanner seam for MediaStore/SAF).

Vendored :mediasession

  • Local fork of dev.toastbits:mediasession (0.1.1) with MPRIS seek-offset,
    onSetVolume, and Float/Double marshalling fixes not yet upstream.

Infra

  • CI (JDK 21 + Android SDK 37), Room migrations 4→10, LibVLC download/packaging
    tasks, deb/rpm/dmg/pkg/msi/exe targets.

Fixed along the way

  • Multi-source discographies (e.g. Lemon Demon across local / Jellyfin /
    Navidrome) seed the artist page from all sources.
  • Cross-source copies of the same recording collapse to one logical track.
  • Desktop search missing local results; album-track collapse matching mobile.
  • Seek restore, playlist member resolution, and sources freeze.

Split shared playback/library/path code into :core (android + jvm).
:desktop is Material 3 + LibVLC (vlcj), scans OS music folders, and
talks MPRIS / media keys. :app depends on :core.
Shared Compose UI for cover art, album pages, and player chrome.
Desktop: right now-playing/queue/history sidebar, bottom transport.
Album pages split cover + tracks from 600dp up (tablet/desktop).
Match Android color tokens, paint the window/shell background, and
tint the sidebar plus transport from the playing cover (folder art
or embedded tags).
Yuri chrome uses onBackground/onSurface. Cover colors stay on the
right now-playing pane. LibVLC runs on its own thread with a file
path MRL so clicks actually play; missing VLC is reported in the UI.

Do not use LWJGL for this — OpenAL does not decode FLAC/MP3.
Download official VideoLAN natives at build (Windows/macOS NuGet,
Debian libvlc on Linux) and point JNA at them before the engine
starts. System VLC is still used when no bundle is present.
Linux installers depend on distro vlc instead of vendoring LibVLC.
Windows and macOS installers still bundle official VideoLAN natives.
Album pages both paint through AlbumArtBackdrop so the cover wash is
the same helper: a soft radial bloom instead of a hard vertical band.
Left bento nav, center stage, right now-playing plus shared QueuePanel,
bottom bar play button tinted from the current cover. Desktop settings
is a large Discord-style modal. Android 12+ can follow system colors.
Room uses BundledSQLiteDriver so the catalog is on the KMP sqlite path.
:app now depends on :components so AlbumArtBackdrop and theme types
compile. Desktop gets the bento layout, resizable sidebars, extra local
folders, and Jellyfin/Subsonic sources. Phone nav is Home / My Stuff /
Search with a floating art-tinted mini player.
HomeRow's slot was typed as Row, which is a composable not a receiver.
Use RowScope, and pass AlbumItem/ArtistItem display strings.
Drop the 380dp nested LazyVerticalGrid so pins sit at the top of the
page and the whole Home list is the only scroller.
Bottom nav and the top bar share those names. Drop the extra Catalog
and My Stuff headlines so the tab title is not doubled.
The Explore tab uses a pill search field instead of a title. A Filters
row under it opens a sheet to pick local and remote libraries.
Pins are just pins — no radio control sitting above the grid.
Home playlist tiles use real playlist art (seed tracks from the catalog).
Desktop gets playlists with secret/public covers, pan-zoom crop, queue
drag-reorder, and song/album tag editing.
Route those screens on a per-tab back stack so back (and a re-tap of
the current tab) returns to Home, My Stuff, or Explore — whichever
opened them. Enable Android predictive back so the overlay scales off
the launching tab instead of replacing it.
Drag was adding each pointer delta to a stale fraction and then
rescaling both panes, so the handles fought the cursor. Accumulate
the gesture in pixels, start immediately (no touch slop), and only
persist on release. Wider hit target and a visible grip.
Providers is its own settings category: list, add Jellyfin or
Subsonic, then the same identity / connection / enabled / save /
delete editor. Test connection is a real button on each editor
(and on each saved server in the list).
Saving a Jellyfin or Subsonic provider now kicks off a forced first
scan of that server (queued if another sync is already running).
Desktop does the same for a new remote or extra folder instead of
waiting on a full library rescan.
Filter chips in the desktop rail scroll horizontally so Artists
no longer wraps. Album art frames stay square; non-square images
keep their ratio and sit centered in that square.
Queue / Recently played chips scroll instead of wrapping. A
TravelExplore control next to settings shows scan progress and
per-source pause/stop/resume, matching mobile. Settings is a
quiet icon instead of a filled accent button.
Partial scan skips already-indexed libraries and only pulls
new Jellyfin/Subsonic tracks. Full scan walks everything.
Pause and stop cancel the in-flight source at a page boundary
and can target all libraries or a single one.
Search uses a 40dp centered pill so the query is no longer clipped
by OutlinedTextField padding. Home opens with a greeting and up to
eight My Stuff shortcuts in a 2×4 grid (Liked Songs, pins, then
recents), followed by Jump back in / playlists / albums / artists.
Search now finds songs, albums, artists, and playlists from the
index plus live Jellyfin/Subsonic queries. Library filters sit
as chips (All, On this device, each server) instead of a sheet.
Persist the track index and per-source cursors so a scan survives
restart. Partial scans skip warm libraries (or delta if the server
grew); full scans resume from the last page and drop deleted
tracks. Pause/stop checkpoint the cursor instead of starting over.
Match on path/folder names, fold stylized letters (ø → o), and
require query tokens instead of a raw substring. Local hits render
immediately so a slow Jellyfin search cannot hide on-disk results.
The 8-up grid no longer fills with recents or random library albums.
Pins are liked songs plus actual pins; album/artist rows only use
that same collection.
Shuffle and repeat (off / all / one) live entirely in PlayerSession
so engines only play audio. Repeat-all wraps; shuffle keeps the
current track and restores original order. Seek bar sits below the
transport with a gap. Artist pages follow the Spotify layout
(banner, popular, you liked, discography chips, appears on, about).
Desktop artist pages resolve the same remote sources as mobile:
Wikipedia extract + TheAudioDB fanart/banner. Headers can be
picked from those results, set from a local file, or cleared.
The About card shows the fetched bio.
Artist pages now merge Wikipedia, TheAudioDB, Deezer, MusicBrainz,
Wikidata, plus Jellyfin/Navidrome art. About lists genres. The left
rail is only My Stuff (pins, likes, playlists). New playlists get a
name/description dialog and land in the collection.
LizAinslie and others added 21 commits August 21, 2026 22:26
Source tiles are grouped and clipped instead of stacked 1:1 in
a 3:1 grid. The crop canvas stays inside the panel. Cropped
headers write a unique .jpg so Coil actually reloads them.
Aspect no longer drives layout — every tile is the same size and
cropped to fit, grouped by source so they cannot overlap.
Search hits (Subsonic/Jellyfin) are upserted into the library like
mobile ensureTracksPresent, stored under a cross-source catalog
key, and kept across rescans so Add to playlist actually shows.
Sidebar no longer stacks extra covers and a second heart. Queue
keeps the now-playing row; its art arrow grows a 1:1 cover above
the details. Bottom tray spacing matches the sidebar gutters.
Queue, position, shuffle, and repeat save like Android and restore
paused. Playlists keep a copy of each track so Subsonic/search
adds still render after a restart even if the index missed them.
Clancy (and any local+Jellyfin+Navidrome album) is one row per
song. Cloud and explicit badges open a sources picker; the chosen
source is saved and used for playback, same as Android.
VLC no longer resets to 100% on every load. The tray slider is
the app volume, MPRIS Volume get/set matches it, and play/pause/
next/prev/volume keys work from the OS (playerctl, media keys).
Save and restore the playhead (VLC ignored setTime until play).
Playlist pages include snapshot tracks even when stored ids drifted.
Opening sources no longer scans the whole library on the UI thread.
Reorder is drag, remove is the context menu.
Search vs scan rows now share a loose title+album key, playlist
snapshots are re-seeded after every source replace, and Done
always upserts the live Track. Grep stderr for YuriPlayer.Playlist
or YuriPlayer.Index.
Add to queue now lands in the hot lane (plays next, before the
album rest) using the same QueuePanel sections as mobile.
VLC actually starts the prepared next track on end. Playlist
members render in add order from snapshots so Navidrome rows
stay visible. Hearts save to My Stuff without pinning home.
Context menus listen on the initial pointer pass so clicks aren't
eaten by the player bar or drag-to-reorder. Same song menu as
everywhere else, including the bottom-left now playing chip.
Members are stored as ordered entries (ordinal + snapshot). Adds
append at the end, drag-reorder rewrites ordinals, and live
library metadata no longer reshuffles the list.
VLC now starts with media().play instead of prepare-then-pause.
Stream URLs are minted at play time so Navidrome/Jellyfin tokens
are fresh after restart, and the hot/cold queue is what we persist.
Restore no longer prepare-pauses the engine (that hung VLC so
play/skip never ran). Play and skip both loadCurrent then
media().play, reminting stream URLs. Pause only pauses.
Claim org.mpris.MediaPlayer2.yuriplayer on a private connection,
return Variants from Properties.Get (what playerctl/GNOME expect),
and grab GNOME media keys as a fallback.
Installs yuriplayer.desktop + hicolor icons, sets WM_CLASS, and
announces MPRIS Identity so GNOME/KDE media indicators can list us.
VLC's HTTP/2 stack was dying with Protocol error and poisoning
every later play. Recycle the player on error.

yuriLog { } is the common DSL: logcat on Android, slf4j/logback
on desktop (Darwin os_log later). Tokens are redacted.
Replace android.util.Log and leftover printStackTrace with the KMP
yuriLog DSL. AlbumLog and PlaylistLog stay as domain helpers on top
of the same facade. Tags are short names; platforms prefix YuriPlayer.
…a layer onto it; fix multi-source discography/artist dedupe. Desktop: migrate Track→Song, keep JSON index for now.
Fork the upstream dev.toastbits:mediasession library into :mediasession so
we can ship MPRIS/SMTC fixes without waiting on upstream releases. This is
not a dependency swap — it is intentionally vendored.

Local changes over upstream 0.1.1:
- Convert MPRIS Seek/SetPosition microseconds to milliseconds before
  invoking onSeek/onSetPosition (PlayerInterface + SessionInterface).
- Add the onSetVolume callback (upstream omitted it) so volume changes made
  from a media key / playerctl round-trip to the app.
- Normalize Volume/Rate as Float in the commonMediaSession API while
  marshalling to Double on the DBus wire.
- Keep the commonMain/jvmMain split so the API surface is platform-agnostic
  (future Darwin target) and the dbus-java + JNA/SMTC bindings stay JVM-only.

Upstream: dev.toastbits:mediasession 0.1.1
@LizAinslie LizAinslie self-assigned this Aug 27, 2026
@LizAinslie LizAinslie added the enhancement New feature or request label Aug 27, 2026
LizAinslie and others added 7 commits August 27, 2026 09:16
vlcj only searches /usr/lib*. NixOS keeps libvlc.so in the store and
exposes it via /run/current-system/sw (IntelliJ PATH often omits that).
Parse wrapProgram scripts, set VLC_PLUGIN_PATH via setenv, and register
a vlcj DiscoveryDirectoryProvider so native init succeeds.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant