android: the repair sheet, browse, every long-press sheet, You's last sections, the lyrics tools — and the walk's audio check made honest
The playlist repair (Find a local match: the library searched by the row's name, a pick relinks it through playlist_relink_item; an unknown row takes the track's identity). A peer's sheet on the raw results (play or save the folder, browse their files, message, buddy) and the Browse screen — the hub's Browse tab: a user's whole share with sizes and totals, a folder played or saved whole; a share can hold thousands of folders, so it filters and renders 80 at a time. "Look up a user" on Community → Buddies. The sheets the phone lacked: a queue row's (Play now, Move to next, Remove from queue), a catalog song's, a catalog card's and the record page's own, a raw file's (in the results and in Browse), a room name's, a buddy's, the now-playing Save (the desktop's Save menu for a streamed track or record). The artwork viewer with pinch zoom, one layer the back gesture peels. Search's recent searches and its spelling suggestion from the names this phone knows. You → Streaming, Downloads, Sharing, Lyrics, Album artwork, and Clear play history; a record's Re-fetch the cover. The lyrics tools on the sheet's face: the offset nudged and kept per track, another LRCLIB candidate chosen, the lookup forgotten and done again (lyrics.svelte.js, shared by the face and the lyrics sheet). The harness: every CDP call times out and the reload step re-attaches (a WebView that stopped answering hung a walk for ten minutes); the audio check counts SoulStream's own tracks' underruns from the mixer dump instead of the thread-wide counters that any app's notification moves — the afternoon's "dropouts" were other apps' sounds, and the browse of a 49,544-file share measured at zero; __settle resets every tab to its root through the nav; an adb step kind; the walks cover all of the above on the mock and on the phone.
This commit is contained in:
1 parent
6e39dc43f3
commit
26928b706d
21 files changed
+1294
-92
No files matched your search
@@ -0,0 +1,107 @@
|
||||
/* Lyrics on the phone — the app's own pipeline, per track, with the
|
||||
desktop's tools: the offset nudged half a second at a time and kept for
|
||||
the track, another LRCLIB candidate chosen when the words are wrong, the
|
||||
lookup forgotten and done again. One store, read by the player sheet's
|
||||
lyrics face and by the lyrics sheet. */
|
||||
import * as api from "../lib/api.js";
|
||||
import { player } from "./player.svelte.js";
|
||||
|
||||
export const lyr = $state({
|
||||
key: null, // the track's path — the identity the words belong to
|
||||
status: "none", // loading | synced | plain | instrumental | none
|
||||
source: "",
|
||||
lines: [], // [{ time_ms, text, words? }]
|
||||
plain: "",
|
||||
offset: 0, // the user's offset plus the automatic correction, ms
|
||||
userOffset: 0,
|
||||
autoOffset: 0,
|
||||
candidates: null, // LRCLIB candidates while choosing, else null
|
||||
searching: false,
|
||||
});
|
||||
|
||||
let gen = 0;
|
||||
export function identity() {
|
||||
const t = player.track;
|
||||
if (!t) return null;
|
||||
return {
|
||||
title: t.title,
|
||||
artist: t.artist || player.album?.artist || "",
|
||||
album: player.album?.title ?? null,
|
||||
durationSecs: t.duration_secs ?? null,
|
||||
path: t.path,
|
||||
key: t.path,
|
||||
};
|
||||
}
|
||||
function apply(p) {
|
||||
lyr.status = p?.status || "none";
|
||||
lyr.source = p?.source || "";
|
||||
lyr.lines = p?.synced || [];
|
||||
lyr.plain = p?.plain || "";
|
||||
lyr.userOffset = p?.offset_ms || 0;
|
||||
lyr.autoOffset = p?.auto_offset_ms || 0;
|
||||
lyr.offset = lyr.userOffset + lyr.autoOffset;
|
||||
}
|
||||
/** The words for the playing track; `force` asks the network again. */
|
||||
export async function loadLyrics(force = false) {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
if (!force && lyr.key === id.key) return;
|
||||
const g = ++gen;
|
||||
Object.assign(lyr, { key: id.key, status: "loading", source: "", lines: [], plain: "", offset: 0, userOffset: 0, autoOffset: 0, candidates: null, searching: false });
|
||||
try {
|
||||
const p = await api.lyricsGet({ title: id.title, artist: id.artist, album: id.album, durationSecs: id.durationSecs, path: id.path, refresh: force ? true : null });
|
||||
if (g !== gen) return;
|
||||
apply(p);
|
||||
} catch {
|
||||
if (g === gen) Object.assign(lyr, { status: "none", lines: [], plain: "" });
|
||||
}
|
||||
}
|
||||
|
||||
/* the desktop's nudge: half a second at a time, kept for the track */
|
||||
let persist = 0;
|
||||
export function nudge(deltaMs) {
|
||||
const id = identity();
|
||||
if (!id || !lyr.lines.length) return;
|
||||
lyr.userOffset = Math.max(-30_000, Math.min(30_000, lyr.userOffset + deltaMs));
|
||||
lyr.offset = lyr.userOffset + lyr.autoOffset;
|
||||
clearTimeout(persist);
|
||||
const value = lyr.userOffset;
|
||||
persist = setTimeout(() => { api.lyricsSetOffset(id.title, id.artist, id.durationSecs, value).catch(() => {}); }, 500);
|
||||
}
|
||||
|
||||
/** Search LRCLIB by this track's name; the candidates wait on the store. */
|
||||
export async function searchCandidates() {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
const g = gen;
|
||||
lyr.searching = true;
|
||||
lyr.candidates = [];
|
||||
try {
|
||||
const found = await api.lyricsSearch(id.title, id.artist);
|
||||
if (g === gen) lyr.candidates = found || [];
|
||||
} catch {
|
||||
if (g === gen) lyr.candidates = [];
|
||||
} finally {
|
||||
if (g === gen) lyr.searching = false;
|
||||
}
|
||||
}
|
||||
export function closeCandidates() { lyr.candidates = null; }
|
||||
/** Pin a picked candidate as this track's lyrics. */
|
||||
export async function chooseCandidate(candidateId) {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
const g = gen;
|
||||
try {
|
||||
const p = await api.lyricsSelect(id.title, id.artist, id.durationSecs, candidateId);
|
||||
if (g !== gen) return;
|
||||
apply(p);
|
||||
lyr.candidates = null;
|
||||
} catch { /* keep what was showing */ }
|
||||
}
|
||||
/** Forget the words and look again. */
|
||||
export async function forgetLyrics() {
|
||||
const id = identity();
|
||||
if (!id) return;
|
||||
try { await api.lyricsClear(id.title, id.artist, id.durationSecs); } catch { /* nothing kept */ }
|
||||
await loadLyrics(true);
|
||||
}
|
||||
Reference in new issue
Block a user