android: hours on the library's count line, By format, Jump back in on the empty search
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.
This commit is contained in:
1 parent
b50577fc81
commit
f8c3b90665
6 files changed
+39
-8
No files matched your search
+3
-1
@@ -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
|
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
|
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
|
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
|
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
|
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
|
own listening (`artist_play_stats`: time played as a chip, the most
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ export function mockInvoke(cmd, args) {
|
|||||||
if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null });
|
if (cmd === "app_status") return Promise.resolve({ version: "mock", connected_as: null });
|
||||||
if (cmd === "library_albums") return Promise.resolve(albums);
|
if (cmd === "library_albums") return Promise.resolve(albums);
|
||||||
if (cmd === "library_album_stats") return Promise.resolve(albums.map(statsFor));
|
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")
|
if (cmd === "album_tracks")
|
||||||
return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60));
|
return new Promise((r) => setTimeout(() => r(tracksFor(args.albumId)), 60));
|
||||||
if (cmd === "album_art") {
|
if (cmd === "album_art") {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
import { note } from "./notice.svelte.js";
|
import { note } from "./notice.svelte.js";
|
||||||
import { fmtOf, albumFmt, fmtBytes, basename, dirname, longDur, mmss, titleFromName } from "./util.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 */
|
/* the last sheet is kept through its slide down */
|
||||||
let shown = $state(null);
|
let shown = $state(null);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
let { entry, screenEl = null } = $props();
|
let { entry, screenEl = null } = $props();
|
||||||
|
|
||||||
const TABS = [["albums", "Albums"], ["artists", "Artists"], ["tracks", "Tracks"], ["playlists", "Playlists"], ["gaps", "Gaps"]];
|
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 tab = $derived(entry.c.tab || "albums");
|
||||||
const sort = $derived(entry.c.sort || "added");
|
const sort = $derived(entry.c.sort || "added");
|
||||||
@@ -30,11 +30,25 @@
|
|||||||
if (sort === "az") return x.title.localeCompare(y.title);
|
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 === "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 === "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 y.added_at - x.added_at;
|
||||||
});
|
});
|
||||||
return l;
|
return l;
|
||||||
});
|
});
|
||||||
const totalTracks = $derived(lib.albums.reduce((n, a) => n + (a.track_count || 0), 0));
|
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());
|
const arts = $derived(artists());
|
||||||
|
|
||||||
/* ── Gaps — the desktop's (Library.svelte's gapSection): only a shelf
|
/* ── Gaps — the desktop's (Library.svelte's gapSection): only a shelf
|
||||||
@@ -101,7 +115,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div class="countline">
|
<div class="countline">
|
||||||
<span class="t-sub">{list.length} album{list.length === 1 ? "" : "s"} · {totalTracks} tracks</span>
|
<span class="t-sub">{list.length} album{list.length === 1 ? "" : "s"} · {totalTracks} tracks{hours ? " · " + hours : ""}</span>
|
||||||
<button class="iconbtn sm muted" onclick={() => patch({ view: view === "grid" ? "list" : "grid" })} aria-label="Grid or list">
|
<button class="iconbtn sm muted" onclick={() => patch({ view: view === "grid" ? "list" : "grid" })} aria-label="Grid or list">
|
||||||
<Icon name={view === "grid" ? "playlist" : "layers"} size={18} />
|
<Icon name={view === "grid" ? "playlist" : "layers"} size={18} />
|
||||||
</button>
|
</button>
|
||||||
@@ -111,7 +125,7 @@
|
|||||||
{:else if view === "grid"}
|
{:else if view === "grid"}
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
{#each list as a (a.id)}
|
{#each list as a (a.id)}
|
||||||
<Release title={a.title} sub={sort === "year" ? String(a.year || "") : a.artist} path={a.art_path} badge={statLabel(lib.stats[a.id]) || null} onclick={() => push("album", { id: a.id }, screenEl)} onmore={() => openHalf("album", { album: a })} />
|
<Release title={a.title} sub={sort === "year" ? String(a.year || "") : sort === "format" ? (statLabel(lib.stats[a.id]) || a.artist) : a.artist} path={a.art_path} badge={statLabel(lib.stats[a.id]) || null} onclick={() => push("album", { id: a.id }, screenEl)} onmore={() => openHalf("album", { album: a })} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -292,6 +292,14 @@
|
|||||||
Artists, records and songs from the catalog; a song plays from the best copy the network has, a record opens its page.
|
Artists, records and songs from the catalog; a song plays from the best copy the network has, a record opens its page.
|
||||||
{#if !online}Sign in on Community to play what the library does not hold.{/if}
|
{#if !online}Sign in on Community to play what the library does not hold.{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if lib.recents.length}
|
||||||
|
<div class="sec"><h2>Jump back in <em>· picks up where you left off</em></h2></div>
|
||||||
|
<div class="rail">
|
||||||
|
{#each lib.recents.slice(0, 8) as r (r.album_id)}
|
||||||
|
<Release title={r.title} sub={r.artist} path={r.art_path} onclick={() => push("album", { id: r.album_id }, screenEl)} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if recents.length}
|
{#if recents.length}
|
||||||
<div class="sec"><h2>Recent</h2><button class="all" onclick={() => { recents = []; keepRecents(); }}>Clear</button></div>
|
<div class="sec"><h2>Recent</h2><button class="all" onclick={() => { recents = []; keepRecents(); }}>Clear</button></div>
|
||||||
<div class="chips recent-chips" style="padding-bottom: 12px">
|
<div class="chips recent-chips" style="padding-bottom: 12px">
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ const PROBE = `(function () {
|
|||||||
plcover: !!document.querySelector('.rec-head.pl .rec-cover img'),
|
plcover: !!document.querySelector('.rec-head.pl .rec-cover img'),
|
||||||
qfirst: (document.querySelector('.q-face .row .t') || {}).textContent || '',
|
qfirst: (document.querySelector('.q-face .row .t') || {}).textContent || '',
|
||||||
rooms: document.querySelectorAll('.screen button.row .art.hash').length,
|
rooms: document.querySelectorAll('.screen button.row .art.hash').length,
|
||||||
|
jump: document.querySelectorAll('.screen .rail .release').length,
|
||||||
|
countline: (document.querySelector('.countline .t-sub') || {}).textContent || '',
|
||||||
|
firstBadge: (document.querySelector('.grid2 .release .ed-badge') || {}).textContent || '',
|
||||||
plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,
|
plthumbs: document.querySelectorAll('.pl-rows .thumb img').length,
|
||||||
imp: document.querySelectorAll('.imp-rows .trk').length,
|
imp: document.querySelectorAll('.imp-rows .trk').length,
|
||||||
impTags: [].map.call(document.querySelectorAll('.imp-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
impTags: [].map.call(document.querySelectorAll('.imp-rows .tag'), function (t) { return t.textContent.trim(); }).join('|'),
|
||||||
@@ -164,6 +167,9 @@ const MOCK_STEPS = [
|
|||||||
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'shpongel'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "search-dym", wait: 900 },
|
{ do: `(function () { var i = document.querySelector('.sfield input'); i.value = 'shpongel'; i.dispatchEvent(new Event('input', { bubbles: true })); })()`, name: "search-dym", wait: 900 },
|
||||||
{ do: `__click('.dym', null)`, name: "search-dym-pick", wait: 700 },
|
{ do: `__click('.dym', null)`, name: "search-dym-pick", wait: 700 },
|
||||||
{ do: `document.querySelectorAll('.nav button')[2].click()`, name: "library", wait: 800 },
|
{ do: `document.querySelectorAll('.nav button')[2].click()`, name: "library", wait: 800 },
|
||||||
|
/* By format: the hi-res lossless records come first */
|
||||||
|
{ do: `__click('.chips .chip', 'By format')`, name: "library-by-format", wait: 500 },
|
||||||
|
{ do: `__click('.chips .chip', 'Recently added')`, wait: 400 },
|
||||||
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, name: "artists", wait: 600 },
|
{ do: `document.querySelectorAll('.segtabs button')[1].click()`, name: "artists", wait: 600 },
|
||||||
{ do: `document.querySelectorAll('.segtabs button')[2].click()`, name: "tracks", wait: 900 },
|
{ do: `document.querySelectorAll('.segtabs button')[2].click()`, name: "tracks", wait: 900 },
|
||||||
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 300 },
|
{ do: `document.querySelectorAll('.segtabs button')[0].click()`, wait: 300 },
|
||||||
@@ -398,7 +404,7 @@ const MOCK_EXPECT = {
|
|||||||
"you-artedge": (i) => i.artEdge === "1000 px",
|
"you-artedge": (i) => i.artEdge === "1000 px",
|
||||||
"history-confirm": (i) => i.half.includes("up") && /Clear the play history/.test(i.halfText),
|
"history-confirm": (i) => i.half.includes("up") && /Clear the play history/.test(i.halfText),
|
||||||
"history-cleared": (i) => !i.half.includes("up") && /history cleared/i.test(i.notice),
|
"history-cleared": (i) => !i.half.includes("up") && /history cleared/i.test(i.notice),
|
||||||
"search-recent": (i) => i.title.startsWith("Search") && i.recents >= 1,
|
"search-recent": (i) => i.title.startsWith("Search") && i.recents >= 1 && i.jump >= 1,
|
||||||
"search-recent-pick": (i) => i.title.startsWith("Search") && (i.songs >= 4 || !!i.top),
|
"search-recent-pick": (i) => i.title.startsWith("Search") && (i.songs >= 4 || !!i.top),
|
||||||
"sleep-sheet": (i) => i.half.includes("up"),
|
"sleep-sheet": (i) => i.half.includes("up"),
|
||||||
"sleep-set": (i) => !i.half.includes("up"),
|
"sleep-set": (i) => !i.half.includes("up"),
|
||||||
@@ -409,7 +415,7 @@ const MOCK_EXPECT = {
|
|||||||
"search-dym": (i) => i.dym === "shpongle",
|
"search-dym": (i) => i.dym === "shpongle",
|
||||||
"search-dym-pick": (i) => i.sq === "shpongle" && !i.dym,
|
"search-dym-pick": (i) => i.sq === "shpongle" && !i.dym,
|
||||||
search: (i) => i.title.startsWith("Search"),
|
search: (i) => i.title.startsWith("Search"),
|
||||||
library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10,
|
library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline),
|
||||||
album: (i) => i.screen.includes("tr-push") && !i.mini,
|
album: (i) => i.screen.includes("tr-push") && !i.mini,
|
||||||
lyrics: (i) => i.sheet.includes("up") && i.lyrLines >= 3,
|
lyrics: (i) => i.sheet.includes("up") && i.lyrLines >= 3,
|
||||||
"lyrics-sheet": (i) => i.half.includes("up") && /Lyrics later/.test(i.halfText) && /Search again/.test(i.halfText),
|
"lyrics-sheet": (i) => i.half.includes("up") && /Lyrics later/.test(i.halfText) && /Search again/.test(i.halfText),
|
||||||
@@ -464,6 +470,7 @@ const MOCK_EXPECT = {
|
|||||||
gaps: (i) => i.title.startsWith("Your library") && i.gaplines >= 1,
|
gaps: (i) => i.title.startsWith("Your library") && i.gaplines >= 1,
|
||||||
"gaps-open": (i) => i.gapCards >= 3,
|
"gaps-open": (i) => i.gapCards >= 3,
|
||||||
playlists: (i) => i.title.startsWith("Your library") && i.plcards === 3 && i.plpinned,
|
playlists: (i) => i.title.startsWith("Your library") && i.plcards === 3 && i.plpinned,
|
||||||
|
"library-by-format": (i) => /^FLAC 24/.test(i.firstBadge),
|
||||||
artists: (i) => i.title.startsWith("Your library") && i.rowArt >= 3,
|
artists: (i) => i.title.startsWith("Your library") && i.rowArt >= 3,
|
||||||
playlist: (i) => i.screen.includes("tr-push") && i.title.startsWith("Late night drive") && i.plrows === 6 && i.plthumbs >= 3 && /unresolved/.test(i.pltags) && /file missing/.test(i.pltags) && /^Playlist/.test(i.plkind) && /Pinned/.test(i.plkind) && i.plfilter,
|
playlist: (i) => i.screen.includes("tr-push") && i.title.startsWith("Late night drive") && i.plrows === 6 && i.plthumbs >= 3 && /unresolved/.test(i.pltags) && /file missing/.test(i.pltags) && /^Playlist/.test(i.plkind) && /Pinned/.test(i.plkind) && i.plfilter,
|
||||||
"playlist-playing": (i) => i.mini && i.playing.includes("Track 2") && !i.toast,
|
"playlist-playing": (i) => i.mini && i.playing.includes("Track 2") && !i.toast,
|
||||||
|
|||||||
Reference in new issue
Block a user