From f8c3b90665ad8de9351f544376b7587d3bb0b23a Mon Sep 17 00:00:00 2001 From: KeelrLeek <156356974+KeelrLeek@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:31:16 -0500 Subject: [PATCH] android: hours on the library's count line, By format, Jump back in on the empty search MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop Library summary's hours on the phone's count line; a By format order from the file stats (lossless by depth and rate first, then by bitrate, the badge as the card's line); the empty Search page's Jump back in — the last records played — as the desktop's empty search shows them. --- ANDROID.md | 4 +++- src/lib/mockLibrary.js | 2 +- src/mobile/HalfSheet.svelte | 2 +- src/mobile/screens/Library.svelte | 20 +++++++++++++++++--- src/mobile/screens/Search.svelte | 8 ++++++++ tests/mobile/smoke.mjs | 11 +++++++++-- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/ANDROID.md b/ANDROID.md index 732c8e9..21ed982 100644 --- a/ANDROID.md +++ b/ANDROID.md @@ -213,7 +213,9 @@ Save menu: Save to library (at once when the cache is byte-complete, else through the download pipeline) and Save this record for the streamed record the queue holds. A moment after boot the phone warms the library's thumbnails the desktop's way (`library_art_prewarm` at the cards' edge, one decode at a -time behind the tiles' gate). The Library's cards wear the desktop's file stats (`library_album_stats`, +time behind the tiles' gate). The Library's count line carries the collection's hours and the sort +offers By format (lossless by depth and rate first, then by bitrate); the +empty Search page shows Jump back in, the desktop's. The Library's cards wear the desktop's file stats (`library_album_stats`, one call for the whole library: FLAC 24/96, MP3 320k, the size on the list rows and the record page), and the artist page shows this phone's own listening (`artist_play_stats`: time played as a chip, the most diff --git a/src/lib/mockLibrary.js b/src/lib/mockLibrary.js index c2ea1b0..3238986 100644 --- a/src/lib/mockLibrary.js +++ b/src/lib/mockLibrary.js @@ -325,7 +325,7 @@ export function mockInvoke(cmd, args) { if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null }); if (cmd === "library_albums") return Promise.resolve(albums); if (cmd === "library_album_stats") return Promise.resolve(albums.map(statsFor)); - if (cmd === "recent_albums") return Promise.resolve([]); + if (cmd === "recent_albums") return Promise.resolve(albums.slice(0, 3).map((a, i) => ({ kind: "library", album_id: a.id, title: a.title, artist: a.artist, art_path: a.art_path, last_played_at: Math.floor(Date.now() / 1000) - 3600 * (i + 1) }))); if (cmd === "album_tracks") return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60)); if (cmd === "album_art") { diff --git a/src/mobile/HalfSheet.svelte b/src/mobile/HalfSheet.svelte index eff238a..ea8d1c3 100644 --- a/src/mobile/HalfSheet.svelte +++ b/src/mobile/HalfSheet.svelte @@ -21,7 +21,7 @@ import { note } from "./notice.svelte.js"; import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.js"; - const SORTS = [["added", "Recently added"], ["az", "A–Z"], ["artist", "By artist"], ["year", "By year"]]; + const SORTS = [["added", "Recently added"], ["az", "A–Z"], ["artist", "By artist"], ["year", "By year"], ["format", "By format"]]; /* the last sheet is kept through its slide down */ let shown = $state(null); diff --git a/src/mobile/screens/Library.svelte b/src/mobile/screens/Library.svelte index 67168ea..180ff69 100644 --- a/src/mobile/screens/Library.svelte +++ b/src/mobile/screens/Library.svelte @@ -18,7 +18,7 @@ let { entry, screenEl = null } = $props(); const TABS = [["albums", "Albums"], ["artists", "Artists"], ["tracks", "Tracks"], ["playlists", "Playlists"], ["gaps", "Gaps"]]; - const SORTS = [["added", "Recently added"], ["az", "A–Z"], ["artist", "By artist"], ["year", "By year"]]; + const SORTS = [["added", "Recently added"], ["az", "A–Z"], ["artist", "By artist"], ["year", "By year"], ["format", "By format"]]; const tab = $derived(entry.c.tab || "albums"); const sort = $derived(entry.c.sort || "added"); @@ -30,11 +30,25 @@ if (sort === "az") return x.title.localeCompare(y.title); if (sort === "artist") return x.artist.localeCompare(y.artist) || (x.year || 0) - (y.year || 0); if (sort === "year") return (y.year || 0) - (x.year || 0) || y.added_at - x.added_at; + if (sort === "format") { + /* lossless first, then the higher bitrate, then the name */ + const fx = fmtRank(lib.stats[x.id]); + const fy = fmtRank(lib.stats[y.id]); + return fy - fx || x.title.localeCompare(y.title); + } return y.added_at - x.added_at; }); return l; }); const totalTracks = $derived(lib.albums.reduce((n, a) => n + (a.track_count || 0), 0)); + const totalSecs = $derived(Object.values(lib.stats).reduce((n, st) => n + (st.secs || 0), 0)); + const hours = $derived(totalSecs >= 3600 ? Math.round(totalSecs / 3600) + " hours" : totalSecs ? Math.round(totalSecs / 60) + " min" : ""); + /* By format: a number the sort can compare — lossless by depth and rate, lossy by bitrate */ + function fmtRank(st) { + if (!st || !st.formats?.length) return 0; + if (st.bit_depth && st.sample_rate) return 1_000_000 + st.bit_depth * 1000 + Math.round(st.sample_rate / 1000); + return st.avg_kbps || 1; + } const arts = $derived(artists()); /* ── Gaps — the desktop's (Library.svelte's gapSection): only a shelf @@ -101,7 +115,7 @@ {/each}