feat(web): persist browser state

Keep settings, aliases, and recent drawings across browser sessions.

Load Start-page videos from a Pages-generated snapshot.
This commit is contained in:
Hakan Seven committed 2026-07-29 00:44:00 +03:00
1 parent 25166b9508
commit d4e792b2bc
15 files changed
+546 -88

No files matched your search

+42 -3
View File
@@ -22,6 +22,8 @@ pub mod paper_sizes;
pub mod thumbnail;
#[cfg(target_arch = "wasm32")]
mod web_worker;
#[cfg(target_arch = "wasm32")]
pub(crate) mod web_recent;
use crate::scene::DerivedCaches;
use acadrust::entities::EntityType;
@@ -224,8 +226,45 @@ pub async fn pick_and_load_web(
let name = handle.file_name();
progress.set(crate::app::OPEN_PHASE_READING, 500, 1, 2);
let bytes = handle.read().await;
let parsed = load_web_bytes(&name, &bytes, progress.clone());
let cached = web_recent::store(&name, &bytes);
let (result, cache_result) = iced::futures::future::join(parsed, cached).await;
if result.is_err() && cache_result.is_ok() {
let _ = web_recent::remove(&name).await;
}
result
}
/// Reopen a browser-private recent copy without showing the file picker.
#[cfg(target_arch = "wasm32")]
pub async fn open_recent_web(
path: PathBuf,
progress: Arc<OpenProgressState>,
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
let name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.ok_or_else(|| "Recent drawing has no file name".to_string())?;
let bytes = web_recent::read(&name)
.await
.map_err(|error| format!("Recent copy unavailable for \"{name}\": {error}"))?;
progress.set(
crate::app::OPEN_PHASE_READING,
1000,
bytes.len(),
bytes.len(),
);
load_web_bytes(&name, &bytes, progress).await
}
#[cfg(target_arch = "wasm32")]
async fn load_web_bytes(
name: &str,
bytes: &[u8],
progress: Arc<OpenProgressState>,
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
progress.set(crate::app::OPEN_PHASE_PARSING, 1000, 0, 1);
let mut doc = match web_worker::parse_document(&name, bytes).await {
let mut doc = match web_worker::parse_document(name, bytes).await {
Ok(document) => document,
Err(error) => return Err(format!("Web parser worker: {error}")),
};
@@ -240,8 +279,8 @@ pub async fn pick_and_load_web(
let mut caches = crate::scene::build_derived_caches(&doc);
caches.corrupt_dropped = dropped;
progress.set(crate::app::OPEN_PHASE_FINALIZING, 9900, 1, 1);
let path = PathBuf::from(&name);
Ok((name, path, doc, caches))
let path = PathBuf::from(name);
Ok((name.to_string(), path, doc, caches))
}
/// Parse a CAD document from in-memory bytes, choosing the format from
+89
View File
@@ -0,0 +1,89 @@
//! Browser-backed copies of recently opened drawings.
//!
//! A web file picker exposes bytes and a display name, not a reusable native
//! path. Keep the last-opened copy in the origin-private file system (OPFS) so
//! the Start page can reopen it without asking the user to pick it again.
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
const RECENT_DIRECTORY: &str = "opencadstudio-recent";
pub async fn store(name: &str, bytes: &[u8]) -> Result<(), String> {
let directory = recent_directory(true).await?;
let options = web_sys::FileSystemGetFileOptions::new();
options.set_create(true);
let handle = JsFuture::from(directory.get_file_handle_with_options(&cache_key(name), &options))
.await
.map_err(js_error)?
.dyn_into::<web_sys::FileSystemFileHandle>()
.map_err(js_error)?;
let writable = JsFuture::from(handle.create_writable())
.await
.map_err(js_error)?
.dyn_into::<web_sys::FileSystemWritableFileStream>()
.map_err(js_error)?;
let write = writable.write_with_u8_array(bytes).map_err(js_error)?;
JsFuture::from(write).await.map_err(js_error)?;
JsFuture::from(writable.close()).await.map_err(js_error)?;
Ok(())
}
pub async fn read(name: &str) -> Result<Vec<u8>, String> {
let directory = recent_directory(false).await?;
let handle = JsFuture::from(directory.get_file_handle(&cache_key(name)))
.await
.map_err(js_error)?
.dyn_into::<web_sys::FileSystemFileHandle>()
.map_err(js_error)?;
let file = JsFuture::from(handle.get_file())
.await
.map_err(js_error)?
.dyn_into::<web_sys::File>()
.map_err(js_error)?;
let buffer = JsFuture::from(file.array_buffer())
.await
.map_err(js_error)?;
Ok(js_sys::Uint8Array::new(&buffer).to_vec())
}
pub async fn remove(name: &str) -> Result<(), String> {
let directory = recent_directory(false).await?;
JsFuture::from(directory.remove_entry(&cache_key(name)))
.await
.map_err(js_error)?;
Ok(())
}
async fn recent_directory(create: bool) -> Result<web_sys::FileSystemDirectoryHandle, String> {
let window = web_sys::window().ok_or_else(|| "browser window unavailable".to_string())?;
let root = JsFuture::from(window.navigator().storage().get_directory())
.await
.map_err(js_error)?
.dyn_into::<web_sys::FileSystemDirectoryHandle>()
.map_err(js_error)?;
let options = web_sys::FileSystemGetDirectoryOptions::new();
options.set_create(create);
JsFuture::from(root.get_directory_handle_with_options(RECENT_DIRECTORY, &options))
.await
.map_err(js_error)?
.dyn_into::<web_sys::FileSystemDirectoryHandle>()
.map_err(js_error)
}
/// Stable, short OPFS entry name. The original display name remains in
/// `AppConfig::recent`; only the browser-private cache uses this key.
fn cache_key(name: &str) -> String {
let mut hash = 0xcbf29ce484222325_u64;
for byte in name.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x100000001b3);
}
format!("{hash:016x}.cad")
}
fn js_error(value: wasm_bindgen::JsValue) -> String {
value
.as_string()
.unwrap_or_else(|| format!("browser storage error: {value:?}"))
}
+2 -2
View File
@@ -7,7 +7,7 @@ use wasm_bindgen::closure::Closure;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{ErrorEvent, MessageEvent, Worker, WorkerOptions, WorkerType};
pub(super) async fn parse_document(name: &str, bytes: Vec<u8>) -> Result<CadDocument, String> {
pub(super) async fn parse_document(name: &str, bytes: &[u8]) -> Result<CadDocument, String> {
let options = WorkerOptions::new();
options.set_type(WorkerType::Module);
let worker = Worker::new_with_options("ocs-parse-worker.js", &options).map_err(js_error)?;
@@ -55,7 +55,7 @@ pub(super) async fn parse_document(name: &str, bytes: Vec<u8>) -> Result<CadDocu
&JsValue::from_str(name),
)
.map_err(js_error)?;
let input = Uint8Array::from(bytes.as_slice());
let input = Uint8Array::from(bytes);
Reflect::set(&payload, &JsValue::from_str("bytes"), &input.buffer()).map_err(js_error)?;
let transfer = Array::new();
transfer.push(&input.buffer());