AkurAI Build
Menu

Public repository / master

AkurAI-Framework / crates/ui/src/platform.rs

Latest change fee5b6c2cde89fa6bf8dc51068af7336b56a88b8 - akurai-ui: platform::open_url (hand a URL, e.g. a native shell's scheme, to the window) by Ólafur Búi Ólafsson

//! Small platform services: timers, clock, storage, document.

use crate::callbacks;
use crate::host;

/// Run `f` once after `ms` milliseconds.
pub fn set_timeout(ms: u32, f: impl FnOnce() + 'static) {
    let mut f = Some(f);
    let mut run = crate::dom::in_scope(crate::reactive::current_scope(), move |()| {
        if let Some(f) = f.take() {
            f()
        }
    });
    let id = callbacks::once(move |_, _| run(()));
    host::set_timeout(ms, id);
}

/// Milliseconds since the Unix epoch.
pub fn now_ms() -> f64 {
    host::now_ms()
}

/// The browser's UTC offset in minutes (east positive; Iceland is 0).
pub fn tz_offset_minutes() -> i32 {
    host::tz_offset_minutes()
}

/// `localStorage` read (`None` when missing or unavailable).
pub fn storage_get(key: &str) -> Option<String> {
    host::storage_get(key)
}

/// `localStorage` write (silently ignored when unavailable).
pub fn storage_set(key: &str, value: &str) {
    host::storage_set(key, value)
}

/// Set `document.title`.
pub fn set_title(title: &str) {
    host::set_title(title)
}

/// Set an attribute on `<html>` (e.g. `data-theme`).
pub fn set_root_attr(name: &str, value: &str) {
    host::set_root_attr(name, value)
}

/// `console.log` (stderr off-browser).
pub fn log(message: &str) {
    host::log(message)
}

/// Focus the first focusable element inside `node`.
pub fn focus_first(node: host::Node) -> bool {
    host::focus_first(node)
}

/// The focused element, if it has a handle.
pub fn active_element() -> Option<host::Node> {
    host::active_element()
}

/// Focus `node`.
pub fn focus(node: host::Node) {
    host::focus(node)
}

/// Whether `node` is `ancestor` or inside it.
pub fn contains(ancestor: host::Node, node: host::Node) -> bool {
    host::contains(ancestor, node)
}

/// Read a DOM property as text (e.g. an input's current `value`).
pub fn prop(node: host::Node, name: &str) -> String {
    host::get_prop_str(node, name)
}

/// A finished microphone clip.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Recording {
    /// e.g. `audio/webm;codecs=opus`
    pub mime: String,
    /// The clip, base64-encoded (ready for a JSON body or a data URL).
    pub base64: String,
}

/// Start recording from the microphone. The browser asks for permission on
/// the first use; a refusal surfaces from [`record_stop`].
pub fn record_start() -> bool {
    host::record_start()
}

/// Stop recording and receive the clip, or the reason there is none.
pub fn record_stop(done: impl FnOnce(Result<Recording, String>) + 'static) {
    let mut done = Some(done);
    let mut run = crate::dom::in_scope(
        crate::reactive::current_scope(),
        move |r: Result<Recording, String>| {
            if let Some(done) = done.take() {
                done(r)
            }
        },
    );
    let id = crate::callbacks::once(move |status, body| {
        run(if status == 1 {
            let (mime, base64) = body.split_once('\n').unwrap_or(("audio/webm", &body));
            Ok(Recording {
                mime: mime.to_string(),
                base64: base64.to_string(),
            })
        } else {
            Err(body)
        })
    });
    host::record_stop(id);
}

/// Play audio from a URL or a `data:` URL.
pub fn play_audio(src: &str) {
    host::play_audio(src)
}

/// Navigate the window to `url` (e.g. a custom scheme a native shell handles;
/// the shell refuses the navigation, so the page stays).
pub fn open_url(url: &str) {
    host::open_url(url)
}

/// A fullscreen video layer above the page (live TV): a new `src` switches,
/// the same `src` only changes the volume (0.0-1.0), an empty `src` removes it.
pub fn stream_video(src: &str, volume: f64) {
    host::stream_video(src, volume.clamp(0.0, 1.0))
}

/// The page's one long-lived audio stream (a radio): a new `src` switches to it,
/// the same `src` only changes the volume (0.0-1.0, clamped), an empty `src` stops.
pub fn stream_audio(src: &str, volume: f64) {
    host::stream_audio(src, volume.clamp(0.0, 1.0))
}

#[cfg(test)]
mod audio_tests {
    use super::*;
    use crate::reactive::signal;
    use crate::testing as t;

    #[test]
    fn recording_hands_back_the_clip_or_the_refusal_and_audio_plays() {
        t::reset();
        assert!(record_start());
        assert!(t::recording());
        let got = signal(None::<Result<Recording, String>>);
        record_stop(move |r| got.set(Some(r)));
        assert!(!t::recording());
        t::finish_recording("audio/webm;codecs=opus", "AAEC");
        assert_eq!(
            got.get(),
            Some(Ok(Recording {
                mime: "audio/webm;codecs=opus".into(),
                base64: "AAEC".into()
            }))
        );
        record_start();
        record_stop(move |r| got.set(Some(r)));
        t::fail_recording("NotAllowedError");
        assert_eq!(got.get(), Some(Err("NotAllowedError".into())));
        play_audio("data:audio/wav;base64,UklGR");
        assert_eq!(t::played_audio(), vec!["data:audio/wav;base64,UklGR"]);
    }

    #[test]
    fn one_stream_plays_switches_changes_volume_and_stops() {
        t::reset();
        assert_eq!(t::audio_stream(), None);
        stream_audio("https://radio.example/a", 0.6);
        assert_eq!(
            t::audio_stream(),
            Some(("https://radio.example/a".into(), 0.6))
        );
        stream_audio("https://radio.example/a", 1.4);
        assert_eq!(
            t::audio_stream(),
            Some(("https://radio.example/a".into(), 1.0))
        );
        stream_audio("https://radio.example/b", -1.0);
        assert_eq!(
            t::audio_stream(),
            Some(("https://radio.example/b".into(), 0.0))
        );
        stream_audio("", 0.5);
        assert_eq!(t::audio_stream(), None);
    }

    #[test]
    fn open_url_hands_the_url_to_the_host() {
        t::reset();
        open_url("familyhub://tv/stop");
        assert_eq!(t::opened_urls(), vec!["familyhub://tv/stop"]);
    }

    #[test]
    fn one_fullscreen_video_layer_shows_switches_and_goes() {
        t::reset();
        stream_video("https://tv.example/a.m3u8", 0.8);
        assert_eq!(
            t::video_stream(),
            Some(("https://tv.example/a.m3u8".into(), 0.8))
        );
        stream_video("https://tv.example/a.m3u8", 2.0);
        assert_eq!(
            t::video_stream(),
            Some(("https://tv.example/a.m3u8".into(), 1.0))
        );
        stream_video("", 0.0);
        assert_eq!(t::video_stream(), None);
    }
}