use super::{
atomic_write::{temp_sibling, write_synced},
design_paths::{autosave_file_name, is_autosave_file_name, legacy_autosave_file_name},
save_helpers::{custom_material_snapshot_for_save, design_file_text},
};
use crate::{
EditorModel, MainWindow,
gui::{editor::state::EditorState, show_toast},
settings::SettingsPersister,
};
use indicatrix_cut_core::native::DesignExtras;
use indicatrix_vault::db::sqlite::Database;
use slint::{ComponentHandle, ModelRc, SharedString, VecModel};
use std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, Mutex},
time::Duration,
};
use tracing::warn;
const AUTOSAVE_INTERVAL: Duration = Duration::from_secs(120);
thread_local! {
static AUTOSAVE_TIMER: RefCell<Option<slint::Timer>> = const { RefCell::new(None) };
static LAST_AUTOSAVE_PATH: RefCell<Option<PathBuf>> = const { RefCell::new(None) };
}
fn take_last_autosave_path() -> Option<PathBuf> {
LAST_AUTOSAVE_PATH.with(|cell| cell.borrow_mut().take())
}
pub(super) fn setup_autosave_timer(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
db: &Arc<Mutex<Database>>,
) {
let state = Rc::clone(state);
let db = Arc::clone(db);
let ui_weak = ui.as_weak();
let timer = slint::Timer::default();
timer.start(slint::TimerMode::Repeated, AUTOSAVE_INTERVAL, move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
crate::gui::editor::stall_guard::stall_guard("autosave_tick", || {
run_autosave_tick(&ui, &state, &db);
});
});
AUTOSAVE_TIMER.with(|cell| *cell.borrow_mut() = Some(timer));
super::on_save_completed(|_ui, _state| {
if let Some(stale) = take_last_autosave_path() {
let _ = std::fs::remove_file(stale);
}
});
}
fn run_autosave_tick(ui: &MainWindow, state: &Rc<RefCell<EditorState>>, db: &Arc<Mutex<Database>>) {
if ui.global::<EditorModel>().get_solve_running() {
return;
}
let (design, asc_filename, printed_proportions, history_entries, file_extras) = {
let st = state.borrow();
if !st.is_dirty() {
return;
}
(
st.design.clone(),
st.asc_filename.clone(),
st.printed_proportions,
st.history.description_log().to_vec(),
st.file_extras.clone(),
)
};
let custom_material = custom_material_snapshot_for_save(&design, db);
let path = autosave_path(asc_filename.as_deref());
LAST_AUTOSAVE_PATH.with(|cell| *cell.borrow_mut() = Some(path.clone()));
let ui_weak = ui.as_weak();
std::thread::spawn(move || {
let extras = DesignExtras {
custom_material: custom_material.as_ref(),
history_entries: &history_entries,
metadata: Some(&file_extras.metadata),
attachments: &file_extras.attachments,
};
let Ok(native_toml) =
design_file_text(&design, printed_proportions.as_ref(), &extras, false)
else {
return;
};
let result = write_autosave(&path, &native_toml);
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
if let Err(e) = result {
show_toast(
&ui,
&format!("Autosave to {} failed: {e}", path.display()),
"error",
);
}
});
});
}
fn autosave_dir() -> PathBuf {
crate::settings::store::default_settings_path()
.parent()
.map_or_else(std::env::temp_dir, std::path::Path::to_path_buf)
}
pub(super) fn autosave_path(asc_filename: Option<&str>) -> PathBuf {
autosave_dir().join(autosave_file_name(asc_filename))
}
pub(super) fn delete_autosaves_for(asc_filename: Option<&str>) {
let dir = autosave_dir();
let _ = std::fs::remove_file(dir.join(autosave_file_name(asc_filename)));
let _ = std::fs::remove_file(dir.join(legacy_autosave_file_name(asc_filename)));
}
#[must_use]
pub(in crate::gui::editor) fn find_leftover_autosave() -> Option<PathBuf> {
find_all_leftover_autosaves()
.into_iter()
.max_by_key(|path| std::fs::metadata(path).and_then(|m| m.modified()).ok())
}
fn find_all_leftover_autosaves() -> Vec<PathBuf> {
std::fs::read_dir(autosave_dir())
.into_iter()
.flatten()
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|n| n.to_str())
.is_some_and(is_autosave_file_name)
})
.collect()
}
pub(in crate::gui::editor) fn delete_leftover_autosave(path: &Path) {
let _ = std::fs::remove_file(path);
}
pub(super) fn write_autosave(path: &Path, native_toml: &str) -> Result<(), String> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let tmp = temp_sibling(path);
write_synced(&tmp, native_toml).map_err(|e| e.to_string())?;
std::fs::rename(&tmp, path).map_err(|e| e.to_string())?;
Ok(())
}
pub(super) fn record_recent_native_file(ui: &MainWindow, native_path_display: &str) {
let Some(persister) = SettingsPersister::installed_for_this_thread() else {
warn!(
"No settings persister is installed; not recording {native_path_display} as a recent file"
);
return;
};
persister.update(|file| {
file.settings
.record_recent_native_file(native_path_display.to_string());
});
ui.set_recent_native_files(ModelRc::new(VecModel::from(
persister
.snapshot()
.settings
.recent_native_files
.into_iter()
.map(SharedString::from)
.collect::<Vec<_>>(),
)));
}