Files
SoulStream/handoff/rounds/ins-r29-crate-lib.txt
T
KeelrLeek 9f0601027c 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.
2026-09-14 07:57:41 -05:00

111 lines
3.8 KiB
Plaintext

//! The downloads notice on the phone.
//!
//! The Transfers screen only exists 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 visible 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, in the shade: how many files are left, the
//! one it is on, a progress bar, Stop all and Open.
//!
//! Its own channel, low importance, so it can be silenced without silencing
//! the player's card. Its own foreground service (`TransferService`,
//! `dataSync`) rather than the media one, because a download is not
//! playback: `MediaService` stops itself the moment the bar empties, and a
//! `mediaPlayback` service with no session is a policy violation. That
//! service is also what keeps the process alive for a Save with nothing
//! playing — the way the first real Save died, four tracks in.
//!
//! Two commands, both fed by the phone UI (src/mobile/xfernote.svelte.js):
//! `update` with what is still in flight, `clear` when the queue empties.
//! The notice's buttons arrive as the plugin event `action`
//! (`addPluginListener("transfer-notice", "action", …)`): `cancel` stops
//! everything still going, `open` brings the app forward on Transfers.
//!
//! On the desktop the plugin is registered too, so the permission set
//! resolves on every platform, but its commands answer "not here".
use serde::{Deserialize, Serialize};
use tauri::{
plugin::{Builder, TauriPlugin},
Runtime,
};
/// What the notice says: how many files are still in flight, the one it is
/// on, and how far the burst has come. `indeterminate` is a queue whose
/// sizes no peer has stated yet — a bar with no end to walk towards.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TransferNotice {
#[serde(default)]
pub count: u32,
pub title: Option<String>,
pub line: Option<String>,
#[serde(default)]
pub percent: i32,
#[serde(default)]
pub indeterminate: bool,
}
#[cfg(mobile)]
pub struct TransferNoticePlugin<R: Runtime>(tauri::plugin::PluginHandle<R>);
#[tauri::command]
fn update<R: Runtime>(app: tauri::AppHandle<R>, state: TransferNotice) -> Result<(), String> {
#[cfg(mobile)]
{
use tauri::Manager;
let notice = app.state::<TransferNoticePlugin<R>>();
notice
.0
.run_mobile_plugin::<serde_json::Value>("update", state)
.map(|_| ())
.map_err(|e| e.to_string())
}
#[cfg(not(mobile))]
{
let _ = (app, state);
Err("the downloads notice is a phone feature".into())
}
}
#[tauri::command]
fn clear<R: Runtime>(app: tauri::AppHandle<R>) -> Result<(), String> {
#[cfg(mobile)]
{
use tauri::Manager;
let notice = app.state::<TransferNoticePlugin<R>>();
notice
.0
.run_mobile_plugin::<serde_json::Value>("clear", ())
.map(|_| ())
.map_err(|e| e.to_string())
}
#[cfg(not(mobile))]
{
let _ = app;
Err("the downloads notice is a phone feature".into())
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("transfer-notice")
.invoke_handler(tauri::generate_handler![update, clear])
.setup(|app, api| {
#[cfg(target_os = "android")]
{
use tauri::Manager;
let handle = api.register_android_plugin(
"com.soulstream.transfernotice",
"TransferNoticePlugin",
)?;
app.manage(TransferNoticePlugin(handle));
}
#[cfg(not(target_os = "android"))]
{
let _ = (app, api);
}
Ok(())
})
.build()
}