Never lose work: atomic saves (temporary file, flush, rename) keeping the replaced version as <file>.bak; autosave of unsaved changes into a recovery folder (every 5 min, preference autosave_minutes, and after commands over 1.5 s; skipped when unchanged; entries locked while their app runs); Recover unsaved design? at launch; doc.recovery_list / doc.recover / doc.recovery_discard; kill-mid-save test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
01b349842c
commit
f40356a74f
11 files changed
+679
-1
No files matched your search
@@ -33,6 +33,7 @@ pub mod params_dialog;
|
||||
#[cfg(test)]
|
||||
mod preselect_tests;
|
||||
pub mod preview;
|
||||
pub mod recovery_ui;
|
||||
pub mod ref_images;
|
||||
pub mod scenario;
|
||||
pub mod selection;
|
||||
@@ -163,6 +164,14 @@ pub struct SolveApp {
|
||||
pub esc_handled: bool,
|
||||
pub status: Option<(String, f64, bool)>,
|
||||
pub quit_requested: bool,
|
||||
/// Autosave into the recovery folder (the desktop app turns it on).
|
||||
pub autosave: Option<solvecraft_engine::recovery::Autosaver>,
|
||||
/// Designs left by crashed apps, offered for recovery.
|
||||
pub recoverable: Vec<solvecraft_engine::recovery::Entry>,
|
||||
/// The last command was a big operation: autosave after it.
|
||||
pub big_operation: bool,
|
||||
/// Autosave interval (minutes; 0 = only after big operations). A preference.
|
||||
pub autosave_minutes: f64,
|
||||
/// The application bar is the window's title bar (Windows, Linux: no OS decorations).
|
||||
pub custom_titlebar: bool,
|
||||
/// macOS: the traffic lights sit over the application bar's left end.
|
||||
@@ -206,6 +215,10 @@ impl SolveApp {
|
||||
esc_handled: false,
|
||||
status: None,
|
||||
quit_requested: false,
|
||||
autosave: None,
|
||||
recoverable: Vec::new(),
|
||||
big_operation: false,
|
||||
autosave_minutes: 5.0,
|
||||
custom_titlebar: false,
|
||||
integrated_titlebar: false,
|
||||
frame_ms: 0.0,
|
||||
@@ -241,6 +254,7 @@ impl SolveApp {
|
||||
"recent": self.home.recent,
|
||||
"shortcut_box": self.sbox.prefs(),
|
||||
"shortcuts": self.keymap.prefs(),
|
||||
"autosave_minutes": self.autosave_minutes,
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
@@ -264,6 +278,9 @@ impl SolveApp {
|
||||
if let Some(a) = flag("auto_project") {
|
||||
self.session.auto_project = a;
|
||||
}
|
||||
if let Some(m) = v.get("autosave_minutes").and_then(Value::as_f64).filter(|m| m.is_finite()) {
|
||||
self.autosave_minutes = m.clamp(0.0, 600.0);
|
||||
}
|
||||
if let Some(s) = v.get("visual_style").and_then(Value::as_u64) {
|
||||
self.ui.visual_style = u8::try_from(s.min(2)).unwrap_or(0);
|
||||
}
|
||||
@@ -296,7 +313,9 @@ impl SolveApp {
|
||||
|
||||
/// Run a command programmatically (JSON parameters, no dialogs).
|
||||
pub fn run(&mut self, id: &str, params: Value) -> Result<Value, String> {
|
||||
let t0 = now_ms();
|
||||
let r = self.session.execute(id, ¶ms).map_err(|e| e.to_string());
|
||||
self.big_operation |= now_ms() - t0 >= recovery_ui::BIG_OPERATION_MS;
|
||||
match &r {
|
||||
Ok(_) => {
|
||||
self.session.echo(format!("{id}: done"));
|
||||
@@ -604,6 +623,7 @@ impl SolveApp {
|
||||
self.collect_screenshots(ctx);
|
||||
self.issue_screenshots(ctx);
|
||||
toolbar::shortcuts(self, ctx);
|
||||
recovery_ui::tick(self);
|
||||
for f in ctx.input(|i| i.raw.dropped_files.clone()) {
|
||||
let p = f.path().to_string_lossy().to_string();
|
||||
if !p.is_empty() {
|
||||
@@ -663,6 +683,7 @@ impl SolveApp {
|
||||
dialogs::show(self, ui.ctx());
|
||||
sketch_palette::show(self, ui.ctx());
|
||||
params_dialog::show(self, ui.ctx());
|
||||
recovery_ui::show(self, ui.ctx());
|
||||
context_menu::show(self, ui.ctx());
|
||||
delete::show(self, ui.ctx());
|
||||
if self.custom_titlebar {
|
||||
|
||||
Reference in new issue
Block a user