//! Sending this browser's own position, once or on a timer. use std::time::Duration; use api::{Point, Uploaded}; use leptos::prelude::*; use leptos::task::spawn_local; use wasm_bindgen::prelude::*; use crate::http; use crate::i18n::tr; #[wasm_bindgen(inline_js = r#" export function locate() { return new Promise((resolve, reject) => { if (!navigator.geolocation) return reject("This browser cannot share its location."); navigator.geolocation.getCurrentPosition(async (p) => { let battery = null; try { if (navigator.getBattery) battery = Math.round((await navigator.getBattery()).level * 100); } catch (e) {} const c = p.coords; // JSON.stringify turns a NaN heading (standing still) into null. resolve(JSON.stringify({ ts: Math.floor(p.timestamp / 1000), lat: c.latitude, lon: c.longitude, acc: c.accuracy, alt: c.altitude, speed: c.speed, bearing: c.heading, battery, })); }, (e) => reject(e.message || "The location is not available."), { enableHighAccuracy: true, timeout: 20000, maximumAge: 0 }); }); } "#)] extern "C" { #[wasm_bindgen(catch)] async fn locate() -> Result; } const STORAGE_KEY: &str = "autoSendSecs"; const MIN_SECS: u32 = 5; const DEFAULT_SECS: u32 = 30; /// Lives in the shell, so automatic sending goes on while the settings are open. #[derive(Clone, Copy)] pub struct Locator { pub auto: RwSignal, pub secs: RwSignal, pub status: RwSignal>>, busy: RwSignal, } fn storage() -> Option { window().local_storage().ok().flatten() } impl Locator { /// The choice is kept in localStorage, so automatic sending resumes after a reload. pub fn provide() -> Self { let saved = storage().and_then(|s| s.get_item(STORAGE_KEY).ok().flatten()); let locator = Locator { auto: RwSignal::new(saved.is_some()), secs: RwSignal::new(saved.unwrap_or_else(|| DEFAULT_SECS.to_string())), status: RwSignal::new(None), busy: RwSignal::new(false), }; provide_context(locator); let timer = StoredValue::new(None::); Effect::new(move |was_on: Option| { if let Some(handle) = timer.get_value() { handle.clear(); } timer.set_value(None); let secs = locator.interval(); let on = locator.auto.get(); if let Some(s) = storage() { let _ = match on { true => s.set_item(STORAGE_KEY, &secs.to_string()), false => s.remove_item(STORAGE_KEY), }; } if on { // Send at once when switched on, but not on every edit of the interval. if was_on != Some(true) { locator.send(); } let handle = set_interval_with_handle( move || locator.send(), Duration::from_secs(secs.into()), ); timer.set_value(handle.ok()); } on }); on_cleanup(move || { if let Some(handle) = timer.get_value() { handle.clear(); } }); locator } fn interval(&self) -> u32 { self.secs .get() .trim() .parse() .unwrap_or(DEFAULT_SECS) .max(MIN_SECS) } pub fn send(self) { // A slow GPS fix must not pile up requests behind it. if self.busy.get_untracked() { return; } self.busy.set(true); spawn_local(async move { let result = async { let json = locate().await.map_err(|e| { tr(&e .as_string() .unwrap_or_else(|| "The location is not available.".into())) })?; let point: Point = serde_json::from_str(&json.as_string().unwrap_or_default()) .map_err(|e| e.to_string())?; http::post::("/api/points", &[&point]) .await .map_err(|e| e.to_string())?; Ok::<_, String>(point) } .await; self.status.set(Some(result.map(|p| { let acc = p.acc.map(|a| format!(", ±{a:.0} m")).unwrap_or_default(); let time: String = js_sys::Date::new(&JsValue::from_f64(p.ts as f64 * 1000.0)) .to_locale_time_string("default") .into(); let sent = format!("{time}{acc}"); tr(&format!("Last sent at {sent}.")) }))); self.busy.set(false); }); } } #[component] pub fn LocationControls() -> impl IntoView { let locator = expect_context::(); view! {

{tr("Share from this browser")}

{tr("seconds")}
{move || { locator.status.get().map(|s| match s { Ok(text) => view! {

{text}

}.into_any(), Err(text) => view! {

{text}

}.into_any(), }) }}
} }