AkurAI Build
Menu

Public repository / c51ade7a9466ad00c3c364890a6ae2a5dae7e3fb

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

Latest change c51ade7a9466ad00c3c364890a6ae2a5dae7e3fb - akurai-ui: microphone recording and audio playback through the host (MediaRecorder bridge, native fake, testing helpers) 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)
}

#[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"]);
    }
}