cranpose-services 0.22.0

Multiplatform service contracts and platform integrations for Cranpose apps
Documentation
use std::rc::Rc;

use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
use wasm_bindgen_futures::JsFuture;

use super::{FilePickerError, PickerFuture};
use crate::content::{
    ContentEntry, ContentError, ContentFolder, ContentFolderRef, ContentFuture, ContentHandle,
    ContentMetadata, ContentStream, ContentStreamRef, blob::BlobContent,
};

#[wasm_bindgen(module = "/src/file_picker/web_folder.mjs")]
extern "C" {
    #[wasm_bindgen(js_name = pickFolder)]
    fn pick() -> js_sys::Promise;
    #[wasm_bindgen(js_name = folderName)]
    fn name(folder: &JsValue) -> String;
    #[wasm_bindgen(js_name = isFolder)]
    fn is_folder(entry: &JsValue) -> bool;
    fn entries(folder: &JsValue) -> JsValue;
    fn files(folder: &JsValue) -> JsValue;
    #[wasm_bindgen(js_name = nextEntry)]
    fn next(iterator: &JsValue) -> js_sys::Promise;
}

fn error(value: JsValue) -> ContentError {
    ContentError::Io(format!("browser folder: {value:?}"))
}

pub(super) fn pick_folder() -> PickerFuture<Result<Option<ContentFolderRef>, FilePickerError>> {
    Box::pin(async {
        let folder = JsFuture::from(pick()).await.map_err(error)?;
        Ok((!folder.is_null()).then(|| Rc::new(BrowserFolder(folder)) as ContentFolderRef))
    })
}

struct BrowserFolder(JsValue);

impl ContentFolder for BrowserFolder {
    fn metadata(&self) -> ContentMetadata {
        ContentMetadata::named(name(&self.0))
    }

    fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
        Box::pin(async move {
            let iterator = entries(&self.0);
            let mut children = Vec::new();
            loop {
                let child = JsFuture::from(next(&iterator)).await.map_err(error)?;
                if child.is_null() {
                    return Ok(children);
                }
                children.push(if is_folder(&child) {
                    ContentEntry::Folder(Rc::new(Self(child)))
                } else {
                    ContentEntry::File(BlobContent::handle(child.unchecked_into()))
                });
            }
        })
    }

    fn stream_files(&self) -> Option<ContentStreamRef> {
        Some(Rc::new(BrowserFiles(files(&self.0))))
    }
}

struct BrowserFiles(JsValue);

impl ContentStream for BrowserFiles {
    fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
        Box::pin(async move {
            let file = JsFuture::from(next(&self.0)).await.map_err(error)?;
            Ok((!file.is_null()).then(|| BlobContent::handle(file.unchecked_into())))
        })
    }
}