Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
113 lines
4.4 KiB
Rust
113 lines
4.4 KiB
Rust
//! Autosave ticks and the "Recover unsaved design?" window shown at launch.
|
|
|
|
use egui::RichText;
|
|
use serde_json::json;
|
|
use solvecraft_engine::recovery::{self, Autosaver, Entry};
|
|
|
|
use crate::SolveApp;
|
|
|
|
/// A command that ran at least this long counts as a big operation: autosave right after it.
|
|
pub const BIG_OPERATION_MS: f64 = 1500.0;
|
|
|
|
/// Autosave (when due, or after a big operation).
|
|
pub fn tick(app: &mut SolveApp) {
|
|
let big = std::mem::take(&mut app.big_operation);
|
|
let Some(a) = app.autosave.as_mut() else { return };
|
|
if let Err(e) = a.tick(&app.session, big) {
|
|
let msg = format!("autosave failed: {e}");
|
|
if app.status.as_ref().is_none_or(|(s, _, _)| *s != msg) {
|
|
app.set_status(msg, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Start autosaving into `dir` and queue the designs that crashed apps left there.
|
|
pub fn start(app: &mut SolveApp, dir: &std::path::Path, interval: std::time::Duration) {
|
|
match Autosaver::new(dir, interval) {
|
|
Ok(a) => {
|
|
app.recoverable = recovery::orphans(dir);
|
|
app.autosave = Some(a);
|
|
}
|
|
Err(e) => app.set_status(format!("autosave is off: {e}"), true),
|
|
}
|
|
}
|
|
|
|
/// The app is closing: keep unsaved work for the next launch, drop everything else.
|
|
pub fn close(app: &mut SolveApp) {
|
|
if let Some(a) = app.autosave.take() {
|
|
a.close(&app.session);
|
|
}
|
|
}
|
|
|
|
fn ago(secs: u64) -> String {
|
|
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(secs);
|
|
let d = now.saturating_sub(secs);
|
|
match d {
|
|
0..=59 => "just now".into(),
|
|
60..=3599 => format!("{} min ago", d / 60),
|
|
3600..=86_399 => format!("{} h ago", d / 3600),
|
|
_ => format!("{} days ago", d / 86_400),
|
|
}
|
|
}
|
|
|
|
pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
|
if app.recoverable.is_empty() {
|
|
return;
|
|
}
|
|
let dir = app.autosave.as_ref().map(|a| a.dir.clone()).or_else(recovery::default_dir);
|
|
let mut act: Option<(Entry, bool)> = None;
|
|
let mut later = false;
|
|
egui::Window::new(RichText::new("Recover unsaved design?").strong().size(13.0))
|
|
.collapsible(false)
|
|
.resizable(false)
|
|
.min_width(420.0)
|
|
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
|
.show(ctx, |ui| {
|
|
ui.label("SolveCraft closed before these designs were saved. Their last autosave can be opened.");
|
|
ui.add_space(6.0);
|
|
for e in app.recoverable.iter().take(20) {
|
|
ui.label(RichText::new(&e.name).strong());
|
|
let file = e.path.clone().unwrap_or_else(|| "never saved".into());
|
|
ui.label(RichText::new(format!("{file} · {} features · autosaved {}", e.features, ago(e.saved_at))).small().weak());
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Recover").clicked() {
|
|
act = Some((e.clone(), true));
|
|
}
|
|
if ui.button("Discard").clicked() {
|
|
act = Some((e.clone(), false));
|
|
}
|
|
});
|
|
ui.separator();
|
|
}
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Later").on_hover_text("Keep them; ask again next launch").clicked() {
|
|
later = true;
|
|
}
|
|
});
|
|
});
|
|
let dir_s = dir.map(|d| d.to_string_lossy().to_string());
|
|
if let Some((e, recover)) = act {
|
|
app.recoverable.retain(|x| x.id != e.id);
|
|
if recover {
|
|
if app.session.is_dirty() {
|
|
app.set_status("save or discard the open design's changes before recovering another", true);
|
|
app.recoverable.insert(0, e);
|
|
return;
|
|
}
|
|
if app.run("doc.recover", json!({"id": e.id, "dir": dir_s})).is_ok() {
|
|
// Ours now: autosave it under this app's entry before the old entry goes.
|
|
let kept = app.autosave.as_mut().map(|a| a.save(&app.session));
|
|
if let Some(Ok(_)) = kept {
|
|
let _ = app.run("doc.recovery_discard", json!({"id": e.id, "dir": dir_s}));
|
|
}
|
|
app.set_status(format!("recovered {}: save it to keep it", e.name), false);
|
|
}
|
|
} else {
|
|
let _ = app.run("doc.recovery_discard", json!({"id": e.id, "dir": dir_s}));
|
|
}
|
|
}
|
|
if later {
|
|
app.recoverable.clear();
|
|
}
|
|
}
|