//! 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, pub line: Option, #[serde(default)] pub percent: i32, #[serde(default)] pub indeterminate: bool, } #[cfg(mobile)] pub struct TransferNoticePlugin(tauri::plugin::PluginHandle); #[tauri::command] fn update(app: tauri::AppHandle, state: TransferNotice) -> Result<(), String> { #[cfg(mobile)] { use tauri::Manager; let notice = app.state::>(); notice .0 .run_mobile_plugin::("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(app: tauri::AppHandle) -> Result<(), String> { #[cfg(mobile)] { use tauri::Manager; let notice = app.state::>(); notice .0 .run_mobile_plugin::("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() -> TauriPlugin { 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() }