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,75 @@
|
||||
<script>
|
||||
/* The artwork viewer — the desktop's lightbox: the cover alone, as large
|
||||
as the screen allows, at the full resolution of the canonical cover
|
||||
for a library record (the thumbnail shows first, the full one lands
|
||||
over it). Two fingers zoom and pan; one finger pans a zoomed cover;
|
||||
a tap on the unzoomed cover, or the back gesture, closes it. */
|
||||
import { nav, closeViewer } from "./nav.svelte.js";
|
||||
import { art } from "./art.svelte.js";
|
||||
import * as api from "../lib/api.js";
|
||||
|
||||
const key = $derived(nav.viewerKey);
|
||||
let full = $state("");
|
||||
$effect(() => {
|
||||
const k = key;
|
||||
full = "";
|
||||
scale = 1; tx = 0; ty = 0;
|
||||
if (!k) return;
|
||||
if (/^https?:/i.test(k)) { full = k; return; }
|
||||
if (/^stream:/.test(k)) return; /* a stream's tag art: the thumbnail is all there is */
|
||||
let live = true;
|
||||
api.albumArt(k, 2000).then((u) => { if (live && u) full = u; }, () => { /* the thumbnail stays */ });
|
||||
return () => { live = false; };
|
||||
});
|
||||
const src = $derived(full || (key ? art[key] : "") || "");
|
||||
|
||||
/* pinch to zoom, pan while zoomed — pointer events, both fingers tracked */
|
||||
let scale = $state(1);
|
||||
let tx = $state(0);
|
||||
let ty = $state(0);
|
||||
const pts = new Map();
|
||||
let pinch = null; /* { d, s, cx, cy, tx, ty } at the pinch's start */
|
||||
let pan = null; /* { x, y, tx, ty } at a one-finger pan's start */
|
||||
let moved = false;
|
||||
const dist = () => { const [a, b] = [...pts.values()]; return Math.hypot(a.x - b.x, a.y - b.y) || 1; };
|
||||
const center = () => { const [a, b] = [...pts.values()]; return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }; };
|
||||
function down(e) {
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
e.currentTarget.setPointerCapture(e.pointerId);
|
||||
moved = false;
|
||||
if (pts.size === 2) { const c = center(); pinch = { d: dist(), s: scale, cx: c.x, cy: c.y, tx, ty }; pan = null; }
|
||||
else if (pts.size === 1 && scale > 1) pan = { x: e.clientX, y: e.clientY, tx, ty };
|
||||
}
|
||||
function move(e) {
|
||||
if (!pts.has(e.pointerId)) return;
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
if (pts.size === 2 && pinch) {
|
||||
moved = true;
|
||||
scale = Math.min(6, Math.max(1, pinch.s * (dist() / pinch.d)));
|
||||
const c = center();
|
||||
tx = pinch.tx + (c.x - pinch.cx);
|
||||
ty = pinch.ty + (c.y - pinch.cy);
|
||||
} else if (pts.size === 1 && pan) {
|
||||
const dx = e.clientX - pan.x;
|
||||
const dy = e.clientY - pan.y;
|
||||
if (Math.abs(dx) + Math.abs(dy) > 4) moved = true;
|
||||
tx = pan.tx + dx;
|
||||
ty = pan.ty + dy;
|
||||
}
|
||||
}
|
||||
function up(e) {
|
||||
pts.delete(e.pointerId);
|
||||
if (pts.size < 2) pinch = null;
|
||||
if (pts.size === 0) pan = null;
|
||||
if (scale <= 1.04) { scale = 1; tx = 0; ty = 0; }
|
||||
}
|
||||
function tap() {
|
||||
if (!moved && scale === 1) closeViewer();
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if nav.viewer}
|
||||
<div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up} role="presentation">
|
||||
{#if src}<img src={src} alt="" draggable="false" style:transform={scale === 1 ? "" : `translate(${tx}px, ${ty}px) scale(${scale})`} />{/if}
|
||||
</div>
|
||||
{/if}
|
||||
Reference in new issue
Block a user