android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43 to 48 (each patch adversarially verified, 43's transient-notice expectation fixed), a partial round 49, the pipeline scripts, the Android Auto design and the icon comparison mock. Also the two harness changes that were never A/B'd — adaptive waits, which may only shorten a declared wait and never lengthen it, and a ship that runs the mock walk and the deploy at the same time. The A/B and its 255-step baseline are in handoff/tools; main stays at what the phone has actually walked.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,115 @@
|
||||
/* The downloads in the phone's shade.
|
||||
|
||||
The Transfers screen is inside the app: the desktop says what a Save is
|
||||
doing with its title-bar ring, the phone with a badge on the nav, and
|
||||
neither is anywhere to be seen once the app is in the background — which
|
||||
is exactly when a Save is left to run on its own. So a running Save says
|
||||
so where the phone says things: one notice with how many files are left,
|
||||
the one it is on, a bar, Stop all and Open.
|
||||
|
||||
Its own channel, so it can be silenced without silencing the player's
|
||||
card, and its own foreground service, so the fetch outlives the playback
|
||||
that happened to be keeping the process alive. Everything real is in
|
||||
crates/tauri-plugin-transfer-notice; this is the wiring — the mirror of
|
||||
transfers.svelte.js, exactly as media.svelte.js mirrors the player. */
|
||||
import * as api from "../lib/api.js";
|
||||
import { addPluginListener } from "@tauri-apps/api/core";
|
||||
import { active, cancelAll } from "./transfers.svelte.js";
|
||||
import { note } from "./notice.svelte.js";
|
||||
import { go, push, top, closeSheet } from "./nav.svelte.js";
|
||||
|
||||
/* What the phone was last told, and how often. A PLAIN object, not $state:
|
||||
nothing renders it, and the effect that drives the post must never read
|
||||
what it writes — a self-read in an $effect kills the subtree. The walk
|
||||
reads it through __ss.xfn, since a notification is not a thing a headless
|
||||
page can be asked about. */
|
||||
export const xfernote = {
|
||||
posted: false,
|
||||
updates: 0,
|
||||
clears: 0,
|
||||
max: 0,
|
||||
title: "",
|
||||
line: "",
|
||||
percent: -1,
|
||||
failed: 0,
|
||||
};
|
||||
|
||||
/* a re-post is not free, and progress ticks arrive far faster than anyone
|
||||
reads them: the lever is the rate */
|
||||
const MIN_GAP = 700;
|
||||
let timer = null;
|
||||
let lastAt = 0;
|
||||
let lastKey = "";
|
||||
|
||||
/** What is in flight, as the notice would say it — or null when nothing is. */
|
||||
function shape() {
|
||||
const live = active();
|
||||
if (!live.length) return null;
|
||||
const newest = [...live].sort((a, b) => b.id - a.id); /* the screen's own order */
|
||||
const cur = newest.find((d) => d.state === "transferring") || newest[0];
|
||||
let received = 0, total = 0, known = 0;
|
||||
for (const d of live) if (d.total) { received += d.received || 0; total += d.total; known++; }
|
||||
return {
|
||||
count: live.length,
|
||||
title: live.length === 1 ? "Saving 1 file" : `Saving ${live.length} files`,
|
||||
line: cur.state === "transferring" ? (cur.name || cur.path || "") : "waiting in their queue",
|
||||
percent: total ? Math.round((received / total) * 100) : 0,
|
||||
indeterminate: !known,
|
||||
};
|
||||
}
|
||||
const keyOf = (s) => (s ? `${s.count}|${s.title}|${s.line}|${s.percent}` : "");
|
||||
|
||||
/** Called from Mobile.svelte's effect: reads the store — so the effect
|
||||
* tracks it — and schedules at most one post per MIN_GAP. */
|
||||
export function syncShade() {
|
||||
const key = keyOf(shape());
|
||||
if (key === lastKey || timer) return;
|
||||
timer = setTimeout(post, Math.max(0, MIN_GAP - (Date.now() - lastAt)));
|
||||
}
|
||||
|
||||
/** The one place that talks to the plugin. Reads the store again on the way
|
||||
* out, so what lands in the shade is the latest, not the change that
|
||||
* scheduled it. */
|
||||
async function post() {
|
||||
timer = null;
|
||||
lastAt = Date.now();
|
||||
const s = shape();
|
||||
lastKey = keyOf(s);
|
||||
if (!s) {
|
||||
if (!xfernote.posted) return;
|
||||
xfernote.posted = false;
|
||||
xfernote.clears++;
|
||||
try { await api.clearTransferNotice(); } catch { xfernote.failed++; }
|
||||
return;
|
||||
}
|
||||
xfernote.posted = true;
|
||||
xfernote.updates++;
|
||||
if (s.count > xfernote.max) xfernote.max = s.count;
|
||||
xfernote.title = s.title;
|
||||
xfernote.line = s.line;
|
||||
xfernote.percent = s.percent;
|
||||
try { await api.transferNotice(s); } catch { xfernote.failed++; }
|
||||
}
|
||||
|
||||
let bound = false;
|
||||
/** The notice's own two buttons. Harmless where there is no plugin. */
|
||||
export async function bindXferNote() {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
try {
|
||||
await addPluginListener("transfer-notice", "action", (p) => {
|
||||
if (p?.action === "cancel") {
|
||||
/* the same cancel(id) a row's tap takes, once per live row —
|
||||
never a second path to the same thing */
|
||||
void cancelAll();
|
||||
note("Downloads stopped.");
|
||||
} else if (p?.action === "open") {
|
||||
closeSheet();
|
||||
go("you");
|
||||
if (top().n !== "transfers") push("transfers", {});
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
/* not a phone */
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user