forked from 1b-agent/opencadstudio-upstream
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:
1 parent
25166b9508
commit
d4e792b2bc
15 files changed
+546
-88
No files matched your search
+42
-3
@@ -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
|
||||
|
||||
@@ -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:?}"))
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user