mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 12:37:08 -06:00
M15.2: web app depth (WEB-1): browser storage, Web Audio, worker renders, WebGPU viewer, PWA
- Persistence: settings, shortcuts, recent projects, imported media, saved projects and auto-saves live in OPFS (IndexedDB fallback) through an in-memory mirror flushed in the background (apps/effectcraft-web/src/store.rs, persist.rs); ConfigStore gains FileOps so auto-save / recovery work in the browser store; the session snapshot restores the last project + editor state on reload; File > Open Recent works from storage. - Audio: Web Audio output (AudioWorklet, ScriptProcessor fallback) fed by AudioPlayback::pump on wasm32; A/V sync from AudioContext.currentTime; meters; unlock on first gesture. - Background jobs: engine::offload (WorkerRequest/WorkerReply protocol, Offload trait, run_request); Render Queue, tracker, mask tracker, Warp Stabilizer and Roto Freeze run in Web Workers (a second engine instance on the shared compiled module), progress streamed back; cancel terminates the worker. - GPU: WebGPU viewer frames drawn straight from the compositor's texture (gpu_display on wasm32 when the viewer draws plainly); async readback (Gpu::read_display_async) for pixels. - PWA: manifest, service worker precaching the build (xtask fills list/version), icons copied from assets/app-icon; COOP/COEP kept from cached responses; offline reload works. - window.effectcraft: saveToBrowser, listStored, removeStored, flush; info() reports storage, audio, gpu, workers. - Tests: native store + protocol + offload (render, track, cancel) tests; smoke.mjs extended (GPU path, worker render responsiveness, audio gesture + playback, reload persistence, Open Recent, offline reload). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c406b71cba
commit
0bee1ab2da
@@ -92,9 +92,68 @@ pub fn info() -> JsValue {
|
||||
}
|
||||
v["version"] = json!(env!("CARGO_PKG_VERSION"));
|
||||
v["threads"] = json!(false);
|
||||
v["workers"] = from_js(&crate::worker::stats());
|
||||
v["audio"] = from_js(&crate::audio::state());
|
||||
if let Some(e) = crate::persist::last_error() {
|
||||
v["storage"]["error"] = json!(e);
|
||||
}
|
||||
to_js(&v)
|
||||
}
|
||||
|
||||
/// Save the project to browser storage without downloading it: `path` (default: its current
|
||||
/// path, or `/<project name>.ecproj`). Resolves with `{path, bytes}`.
|
||||
#[wasm_bindgen(js_name = saveToBrowser)]
|
||||
pub fn save_to_browser(path: Option<String>) -> js_sys::Promise {
|
||||
js_sys::Promise::new(&mut |resolve, reject| {
|
||||
let path = path.clone();
|
||||
crate::post(move |app| {
|
||||
let p = path.or_else(|| app.session.path.clone()).unwrap_or_else(|| format!("/{}.ecproj", app.session.project_name()));
|
||||
let p = if p.starts_with('/') { p } else { format!("/{p}") };
|
||||
crate::files::save_quietly(&p);
|
||||
match app.session.execute("file.saveAs", json!({"path": p})) {
|
||||
Ok(v) => {
|
||||
let _ = resolve.call1(&JsValue::NULL, &to_js(&v));
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = reject.call1(&JsValue::NULL, &JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// What browser storage holds: `{backend, usage, quota, persisted, files: [{path, size,
|
||||
/// modified}], config: [name]}` (render outputs, which are not stored, are left out).
|
||||
#[wasm_bindgen(js_name = listStored)]
|
||||
pub async fn list_stored() -> JsValue {
|
||||
let est = wasm_bindgen_futures::JsFuture::from(crate::persist::store_estimate()).await.map(|v| from_js(&v)).unwrap_or(json!({}));
|
||||
let files: Vec<Value> = crate::files::STORE
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|f| f.3)
|
||||
.map(|(path, size, modified, _)| json!({"path": path, "size": size, "modified": modified}))
|
||||
.collect();
|
||||
let config: Vec<Value> =
|
||||
crate::files::STORE.lock().list(crate::store::CONFIG).into_iter().map(|(k, ..)| json!(k.trim_start_matches(crate::store::CONFIG))).collect();
|
||||
let mut v = est;
|
||||
v["files"] = json!(files);
|
||||
v["config"] = json!(config);
|
||||
v["pending"] = json!(crate::files::STORE.lock().has_pending());
|
||||
to_js(&v)
|
||||
}
|
||||
|
||||
/// Remove a stored file (media, project, auto-save). Resolves with whether it existed.
|
||||
#[wasm_bindgen(js_name = removeStored)]
|
||||
pub fn remove_stored(path: String) -> bool {
|
||||
crate::files::STORE.remove_file(&path)
|
||||
}
|
||||
|
||||
/// Resolves once every change is written to browser storage.
|
||||
#[wasm_bindgen]
|
||||
pub fn flush() -> js_sys::Promise {
|
||||
crate::persist::flushed()
|
||||
}
|
||||
|
||||
/// Settle the promises whose replies arrived (called every frame).
|
||||
pub(crate) fn poll_replies() {
|
||||
PENDING.with(|p| {
|
||||
|
||||
Reference in New Issue
Block a user