The same Tauri 2 project builds for Android: mobile.html as a third Vite
entry (src/mobile/, the mockup on real data), tauri.android.conf.json, the
mobile stubs (updater, lyric alignment, vocal separation), rustls reqwest
on Android, the AAudio buffer, and a hand-driven detour build
(src-tauri/android/{env,build,deploy,dev}.sh) while Developer Mode is off.
ANDROID.md is the recipe.
Playback: the media-session plugin crate (lock-screen card, MediaStyle
notification, audio focus, headset buttons, a foreground service that
promotes itself first — Android 14 kills the app otherwise), session
restore across reload and restart, solid chrome after measuring the
backdrop-filter lag.
The catalog: Search is the desktop's top bar (the top-result rule, artist
cards), the artist page and the record page are the desktop's, Home is the
desktop's landing page, Gaps per followed shelf. Playing goes through the
desktop's own source rules, moved verbatim from state.svelte.js into
src/lib/sources.js so both apps share one resolver; the phone readies the
next three tracks and crosses boundaries gapless. Save fetches a record or
a track into the library by the download rules; Transfers shows the
burst. The raw Soulseek search stays, offered on request, a peer's copy
streaming with the lane rising as the bytes arrive. Community is the
Soulseek side (rooms, chat, buddies); the private layer waits.
The opener and storage plugins (social links, the picked library folder,
all-files access), the volume normalization switch, and the harness:
tests/mobile/smoke.mjs walks the mock (a fake network behind
src/lib/events.js) and the real phone over CDP, perf.mjs measures frames.
62 lines
2.5 KiB
JavaScript
62 lines
2.5 KiB
JavaScript
/* Transfers on the phone: the downloads the backend is running — every
|
|
Save lands here — fed by soulseek:download exactly as the desktop's
|
|
`downloads` store is (latest update wins, a terminal update without
|
|
numbers keeps the last known progress), pruned once a burst drains. */
|
|
import * as api from "../lib/api.js";
|
|
import { listen } from "../lib/events.js";
|
|
|
|
export const transfers = $state({
|
|
/** id → payload { id, username, path, name, state, queue_place, local_path, error, received, total } */
|
|
items: {},
|
|
});
|
|
|
|
export const DONE = new Set(["complete", "failed", "cancelled"]);
|
|
const HISTORY_MAX = 60;
|
|
|
|
export function list() {
|
|
return Object.values(transfers.items).sort((a, b) => b.id - a.id);
|
|
}
|
|
export function active() {
|
|
return Object.values(transfers.items).filter((d) => !DONE.has(d.state));
|
|
}
|
|
export function clearFinished() {
|
|
for (const d of Object.values(transfers.items)) if (DONE.has(d.state)) delete transfers.items[d.id];
|
|
}
|
|
export async function cancel(id) {
|
|
try { await api.cancelDownload(id); } catch { /* already gone */ }
|
|
}
|
|
|
|
/** "queued · 3rd in line", "transferring · 42%", "saved", "failed · reason" */
|
|
export function stateLine(d) {
|
|
switch (d.state) {
|
|
case "connecting": return "connecting…";
|
|
case "requested": return "asked…";
|
|
case "queued": return d.queue_place ? `queued · ${d.queue_place} ahead` : "queued";
|
|
case "transferring": return d.total ? `${Math.round((d.received / d.total) * 100)}%` : "transferring…";
|
|
case "complete": return "saved";
|
|
case "failed": return d.error ? `failed · ${d.error}` : "failed";
|
|
case "cancelled": return "cancelled";
|
|
default: return d.state || "";
|
|
}
|
|
}
|
|
|
|
let bound = false;
|
|
export async function bindTransfers() {
|
|
if (bound) return;
|
|
bound = true;
|
|
try {
|
|
await listen("soulseek:download", (ev) => {
|
|
const d = ev.payload;
|
|
if (!d || d.id == null) return;
|
|
const prev = transfers.items[d.id];
|
|
if (prev && !d.total) { d.received = prev.received; d.total = prev.total; }
|
|
transfers.items[d.id] = d;
|
|
if (DONE.has(d.state)) {
|
|
/* keep an active burst whole; prune the oldest finished past the cap */
|
|
const done = Object.values(transfers.items).filter((x) => DONE.has(x.state)).sort((a, b) => a.id - b.id);
|
|
for (const x of done.slice(0, Math.max(0, done.length - HISTORY_MAX))) delete transfers.items[x.id];
|
|
}
|
|
});
|
|
} catch { /* not a phone */ }
|
|
}
|