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

+46 -14
View File
@@ -366,8 +366,8 @@ impl OpenCADStudio {
self.plot_dialog = cfg.plot;
}
/// Write the config to disk only when it changed since the last write, so a
/// toggle persists immediately without thrashing the file.
/// Write the config only when it changed since the last write, so a toggle
/// persists immediately without thrashing native or browser storage.
pub(in crate::app) fn save_config(&mut self) {
let cur = self.current_config();
if self.last_saved_config.as_ref() != Some(&cur) {
@@ -423,13 +423,24 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
/// resolved (deleted since) matches nothing and falls through to the normal
/// open, which reports the miss.
pub(in crate::app) fn tab_showing(&self, path: &std::path::Path) -> Option<usize> {
let want = std::fs::canonicalize(path).ok()?;
self.tabs.iter().position(|t| {
t.current_path
.as_deref()
.and_then(|p| std::fs::canonicalize(p).ok())
.is_some_and(|p| p == want)
})
#[cfg(target_arch = "wasm32")]
{
return self
.tabs
.iter()
.position(|tab| tab.current_path.as_deref() == Some(path));
}
#[cfg(not(target_arch = "wasm32"))]
{
let want = std::fs::canonicalize(path).ok()?;
self.tabs.iter().position(|t| {
t.current_path
.as_deref()
.and_then(|p| std::fs::canonicalize(p).ok())
.is_some_and(|p| p == want)
})
}
}
/// Start the next drawing a second launch handed us, if any.
@@ -1632,12 +1643,33 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.stamp_header_sysvars(i);
self.sync_truck_solids_to_acis(i);
self.stamp_thumbnail(i, version);
let saved =
match crate::io::save_to_bytes(&self.tabs[i].scene.document, ext, version) {
let mut recent_task = Task::none();
let saved = match crate::io::save_to_bytes(
&self.tabs[i].scene.document,
ext,
version,
) {
Ok(bytes) => {
crate::sys::download_bytes(&filename, &bytes);
let cache_name = std::path::Path::new(&filename)
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| filename.clone());
let path = std::path::PathBuf::from(cache_name);
self.tabs[i].current_path = Some(path.clone());
self.tabs[i].scene.document.version = version;
self.tabs[i].dirty = false;
recent_task = Task::perform(
async move {
crate::io::web_recent::store(
&path.to_string_lossy(),
&bytes,
)
.await
.map(|_| path)
},
Message::WebRecentStored,
);
self.command_line.push_output(&format!("Saved: {filename}"));
true
}
@@ -1654,14 +1686,14 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
{
let cont = self.update(Message::TabClose(idx));
let rest = self.continue_tab_close_queue();
return Task::batch([close, cont, rest]);
return Task::batch([close, recent_task, cont, rest]);
}
} else if self.pending_close.is_some() {
let retry = self.open_unsaved_dialog_window();
return Task::batch([close, retry]);
return Task::batch([close, recent_task, retry]);
}
}
close
Task::batch([close, recent_task])
}
}
+49 -11
View File
@@ -290,18 +290,45 @@ impl OpenCADStudio {
),
Message::OpenRecent(path) => {
// Recents are read from disk every save → the path may be
// stale. Skip silently if the file no longer exists; the
// entry stays in the list so the user can clean it up.
match std::fs::metadata(&path) {
Ok(m) => self.update(Message::OpenPathPicked(Some((path, m.len())))),
Err(_) => {
self.command_line.push_error(&format!(
"Recent file no longer exists: {}",
path.display()
));
Task::none()
#[cfg(not(target_arch = "wasm32"))]
{
// Recents are read from disk every save → the path may be
// stale. Skip silently if the file no longer exists; the
// entry stays in the list so the user can clean it up.
return match std::fs::metadata(&path) {
Ok(m) => self.update(Message::OpenPathPicked(Some((path, m.len())))),
Err(_) => {
self.command_line.push_error(&format!(
"Recent file no longer exists: {}",
path.display()
));
Task::none()
}
};
}
#[cfg(target_arch = "wasm32")]
{
if let Some(idx) = self.tab_showing(&path) {
return self.update(Message::TabSwitch(idx));
}
let name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.to_string_lossy().into_owned());
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
crate::app::OPEN_PHASE_READING,
));
self.opening = Some(crate::app::OpenProgress {
name,
size_bytes: 0,
state: state.clone(),
started: Instant::now(),
});
Task::perform(
crate::io::open_recent_web(path, state),
Message::FileOpened,
)
}
}
@@ -548,6 +575,17 @@ impl OpenCADStudio {
self.drain_pending_open()
}
#[cfg(target_arch = "wasm32")]
Message::WebRecentStored(result) => match result {
Ok(path) => self.push_recent(path),
Err(error) => {
self.command_line.push_error(&format!(
"Saved download, but recent copy could not be stored: {error}"
));
Task::none()
}
},
Message::ImagePick => {
Task::perform(crate::io::pick_image_file(), Message::ImagePickResult)
}