feat(autosave): robuster Autosave mit Max-Wait, Retries, PageStore-Trait und Shutdown-Sicherheit
This commit is contained in:
+796
-85
File diff suppressed because it is too large
Load Diff
+99
-8
@@ -581,8 +581,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
match event {
|
match event {
|
||||||
WindowEvent::CloseRequested => {
|
WindowEvent::CloseRequested => {
|
||||||
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
||||||
autosave.shutdown();
|
match autosave.shutdown() {
|
||||||
target.exit();
|
Ok(()) => {
|
||||||
|
target.exit();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[Shutdown-Fehler] Konnte nicht geordnet beenden: {err}");
|
||||||
|
ui_state.shutdown_error = Some(err);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowEvent::Focused(false) => {
|
WindowEvent::Focused(false) => {
|
||||||
@@ -651,7 +659,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
match &logical_key {
|
match &logical_key {
|
||||||
Key::Named(NamedKey::Escape) => {
|
Key::Named(NamedKey::Escape) => {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
if ui_state.is_pen_menu_open
|
if ui_state.shutdown_error.is_some() {
|
||||||
|
ui_state.shutdown_error = None;
|
||||||
|
changed = true;
|
||||||
|
} else if ui_state.is_pen_menu_open
|
||||||
|| ui_state.is_highlighter_menu_open
|
|| ui_state.is_highlighter_menu_open
|
||||||
|| ui_state.show_page_jump_popup
|
|| ui_state.show_page_jump_popup
|
||||||
|| ui_state.is_document_menu_open
|
|| ui_state.is_document_menu_open
|
||||||
@@ -745,8 +756,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// Jetzt speichern (Disk Flush): Strg + S
|
// Jetzt speichern (Disk Flush): Strg + S
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("s") && ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("s") && ctrl => {
|
||||||
autosave.flush();
|
match autosave.flush() {
|
||||||
ui_state.set_export_message("Gespeichert");
|
Ok(()) => {
|
||||||
|
ui_state.set_export_message("Gespeichert");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
ui_state.set_export_message(format!(
|
||||||
|
"Speicherfehler: {}",
|
||||||
|
err.messages.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
update_window_title(
|
update_window_title(
|
||||||
&window,
|
&window,
|
||||||
¬ebook,
|
¬ebook,
|
||||||
@@ -903,6 +923,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
manifest.next_page_id = notebook.next_page_id;
|
manifest.next_page_id = notebook.next_page_id;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -915,6 +936,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
manifest.next_page_id = notebook.next_page_id;
|
manifest.next_page_id = notebook.next_page_id;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -928,6 +950,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -941,6 +964,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -954,6 +978,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -994,8 +1019,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Beenden: Strg + Q
|
// Beenden: Strg + Q
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("q") && ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("q") && ctrl => {
|
||||||
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
||||||
autosave.shutdown();
|
match autosave.shutdown() {
|
||||||
target.exit();
|
Ok(()) => {
|
||||||
|
target.exit();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[Shutdown-Fehler] Konnte nicht geordnet beenden: {err}");
|
||||||
|
ui_state.shutdown_error = Some(err);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1863,6 +1896,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -1875,6 +1909,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
manifest.next_page_id = notebook.next_page_id;
|
manifest.next_page_id = notebook.next_page_id;
|
||||||
|
manifest.pages = notebook
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.map(|p| storage::PageMeta {
|
||||||
|
id: p.id,
|
||||||
|
background: Some(p.background),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -1885,7 +1928,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
notebook.add_page(bg);
|
notebook.add_page(bg);
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
manifest.current_page_index = notebook.current_page_index;
|
||||||
manifest.next_page_id = notebook.next_page_id;
|
manifest.next_page_id = notebook.next_page_id;
|
||||||
|
manifest.pages = notebook
|
||||||
|
.pages
|
||||||
|
.iter()
|
||||||
|
.map(|p| storage::PageMeta {
|
||||||
|
id: p.id,
|
||||||
|
background: Some(p.background),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -1897,6 +1949,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, idx);
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, idx);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.current_page_index = idx;
|
manifest.current_page_index = idx;
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -1980,7 +2033,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiAction::CreateNotebook(title) => {
|
UiAction::CreateNotebook(title) => {
|
||||||
autosave.flush();
|
let _ = autosave.flush();
|
||||||
match storage::create_new_notebook(&storage_base_dir, &title) {
|
match storage::create_new_notebook(&storage_base_dir, &title) {
|
||||||
Ok((new_nb, new_manifest, new_dir)) => {
|
Ok((new_nb, new_manifest, new_dir)) => {
|
||||||
autosave.switch_notebook_dir(new_dir.clone());
|
autosave.switch_notebook_dir(new_dir.clone());
|
||||||
@@ -2010,6 +2063,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ui_state.document_title = manifest.title.clone();
|
ui_state.document_title = manifest.title.clone();
|
||||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||||
|
autosave.mark_manifest_dirty(&manifest);
|
||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||||
ui_state.set_export_message(format!("Umbenannt in '{}'", new_title));
|
ui_state.set_export_message(format!("Umbenannt in '{}'", new_title));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
@@ -2114,6 +2168,43 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
UiAction::RetryShutdown => {
|
||||||
|
println!("[Shutdown] Wiederhole Speichern vor Beenden...");
|
||||||
|
match autosave.shutdown() {
|
||||||
|
Ok(()) => {
|
||||||
|
ui_state.shutdown_error = None;
|
||||||
|
target.exit();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
ui_state.shutdown_error = Some(err);
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiAction::ExportRecoveryAndExit => {
|
||||||
|
println!("[Shutdown] Starte Notfall-Export aller ungespeicherten Daten...");
|
||||||
|
match autosave.export_recovery(Some(¬ebook)) {
|
||||||
|
Ok(paths) => {
|
||||||
|
eprintln!(
|
||||||
|
"[Recovery] Notfall-Export erfolgreich geschrieben ({} Dateien): {:?}",
|
||||||
|
paths.len(),
|
||||||
|
paths
|
||||||
|
);
|
||||||
|
target.exit();
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("[Recovery-Fehler] Konnte Notfall-Export nicht schreiben: {err}");
|
||||||
|
if let Some(ref mut se) = ui_state.shutdown_error {
|
||||||
|
se.messages.push(format!("Notfall-Export fehlgeschlagen: {err}"));
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
UiAction::ForceExit => {
|
||||||
|
println!("[Shutdown] Beenden erzwungen trotz ungespeicherter Änderungen.");
|
||||||
|
target.exit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+24
-9
@@ -785,7 +785,8 @@ pub fn delete_notebook(notebook_dir: &Path) -> Result<(), StorageError> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::autosave::AutosaveController;
|
use crate::autosave::AutosaveController;
|
||||||
use std::time::Duration;
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_atomic_save_and_reload() {
|
fn test_atomic_save_and_reload() {
|
||||||
@@ -895,16 +896,28 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_debounce_timing() {
|
fn test_debounce_timing() {
|
||||||
|
use crate::autosave::tests::MockClock;
|
||||||
|
|
||||||
let temp_dir = tempfile::tempdir().expect("Konnte temporäres Verzeichnis nicht anlegen");
|
let temp_dir = tempfile::tempdir().expect("Konnte temporäres Verzeichnis nicht anlegen");
|
||||||
let notebook_dir = temp_dir.path().to_path_buf();
|
let notebook_dir = temp_dir.path().to_path_buf();
|
||||||
|
|
||||||
|
let clock = Arc::new(MockClock::new(Instant::now()));
|
||||||
let debounce_duration = Duration::from_millis(150);
|
let debounce_duration = Duration::from_millis(150);
|
||||||
let mut controller = AutosaveController::new(notebook_dir.clone(), debounce_duration);
|
let max_wait = Duration::from_secs(10);
|
||||||
|
let store = Arc::new(crate::autosave::DiskPageStore);
|
||||||
|
|
||||||
|
let mut controller = AutosaveController::with_options(
|
||||||
|
notebook_dir.clone(),
|
||||||
|
debounce_duration,
|
||||||
|
max_wait,
|
||||||
|
store,
|
||||||
|
Arc::clone(&clock) as Arc<dyn crate::autosave::Clock>,
|
||||||
|
);
|
||||||
let manifest = NotebookManifest::new("test_nb", "Test Notizbuch");
|
let manifest = NotebookManifest::new("test_nb", "Test Notizbuch");
|
||||||
|
|
||||||
let mut page = Page::new(1, PageBackground::Grid5mm);
|
let mut page = Page::new(1, PageBackground::Grid5mm);
|
||||||
|
|
||||||
// 10 schnelle aufeinanderfolgende Änderungen (alle 5 ms, deutlich kürzer als 150 ms Debounce)
|
// 10 schnelle aufeinanderfolgende Änderungen (alle 5 ms mit MockClock, keine Wartezeit)
|
||||||
for i in 0..10 {
|
for i in 0..10 {
|
||||||
let sample = StylusSample {
|
let sample = StylusSample {
|
||||||
position: glam::Vec2::new(10.0 + i as f32, 20.0 + i as f32),
|
position: glam::Vec2::new(10.0 + i as f32, 20.0 + i as f32),
|
||||||
@@ -920,23 +933,24 @@ mod tests {
|
|||||||
page.add_stroke(stroke);
|
page.add_stroke(stroke);
|
||||||
|
|
||||||
controller.mark_page_dirty(&page, &manifest);
|
controller.mark_page_dirty(&page, &manifest);
|
||||||
|
clock.advance(Duration::from_millis(5));
|
||||||
controller.update();
|
controller.update();
|
||||||
std::thread::sleep(Duration::from_millis(5));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Während der schnellen Änderungen (ca. 50 ms vergangen) darf noch KEIN Schreibvorgang erfolgt sein!
|
// Während der schnellen Änderungen darf noch KEIN Schreibvorgang erfolgt sein!
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
controller.total_page_writes(),
|
controller.total_page_writes(),
|
||||||
0,
|
0,
|
||||||
"Vor Ablauf des Debounce-Fensters darf noch kein Disk-Write stattgefunden haben"
|
"Vor Ablauf des Debounce-Fensters darf noch kein Disk-Write stattgefunden haben"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Jetzt warten wir, bis das Debounce-Fenster abgelaufen ist (> 150 ms)
|
// Jetzt drehen wir die Mock-Uhr um 180 ms vor (> 150 ms Debounce)
|
||||||
std::thread::sleep(Duration::from_millis(180));
|
clock.advance(Duration::from_millis(180));
|
||||||
controller.update();
|
controller.update();
|
||||||
|
|
||||||
// Flush abwarten
|
// Flush abwarten
|
||||||
controller.flush();
|
let flush_res = controller.flush();
|
||||||
|
assert!(flush_res.is_ok(), "Flush muss erfolgreich sein");
|
||||||
|
|
||||||
// Es darf genau EIN tatsächlicher Schreibvorgang für die Seite stattgefunden haben
|
// Es darf genau EIN tatsächlicher Schreibvorgang für die Seite stattgefunden haben
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -949,7 +963,8 @@ mod tests {
|
|||||||
let loaded = load_page(¬ebook_dir, 1).expect("Seite muss lesbar sein");
|
let loaded = load_page(¬ebook_dir, 1).expect("Seite muss lesbar sein");
|
||||||
assert_eq!(loaded.strokes.len(), 10);
|
assert_eq!(loaded.strokes.len(), 10);
|
||||||
|
|
||||||
controller.shutdown();
|
let shutdown_res = controller.shutdown();
|
||||||
|
assert!(shutdown_res.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ pub enum UiAction {
|
|||||||
},
|
},
|
||||||
// Datenintegrität / Quarantäne
|
// Datenintegrität / Quarantäne
|
||||||
UnlockCorruptedPage(u64),
|
UnlockCorruptedPage(u64),
|
||||||
|
// Beenden-Sicherheit bei Fehlschlag
|
||||||
|
RetryShutdown,
|
||||||
|
ExportRecoveryAndExit,
|
||||||
|
ForceExit,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Zustand der grafischen Benutzeroberfläche.
|
/// Zustand der grafischen Benutzeroberfläche.
|
||||||
@@ -164,6 +168,8 @@ pub struct UiState {
|
|||||||
pub update_modal_rect: Option<egui::Rect>,
|
pub update_modal_rect: Option<egui::Rect>,
|
||||||
/// Steuert, ob Touch-Eingaben mit einem Finger zeichnen oder das Dokument verschieben (Default: false = reine Gesten-Navigation & Zoom)
|
/// Steuert, ob Touch-Eingaben mit einem Finger zeichnen oder das Dokument verschieben (Default: false = reine Gesten-Navigation & Zoom)
|
||||||
pub touch_draw_enabled: bool,
|
pub touch_draw_enabled: bool,
|
||||||
|
/// Fehler beim Flushen vor dem Beenden
|
||||||
|
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UiState {
|
impl Default for UiState {
|
||||||
@@ -195,6 +201,7 @@ impl Default for UiState {
|
|||||||
show_updater_config: false,
|
show_updater_config: false,
|
||||||
update_modal_rect: None,
|
update_modal_rect: None,
|
||||||
touch_draw_enabled: false,
|
touch_draw_enabled: false,
|
||||||
|
shutdown_error: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2022,6 +2029,95 @@ pub fn render_ui(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notfall-/Sicherheits-Dialog bei Fehlschlag des Speicherns vor dem Beenden
|
||||||
|
if let Some(err) = &state.shutdown_error {
|
||||||
|
let mut dismiss = false;
|
||||||
|
egui::Window::new("⚠️ Ungespeicherte Änderungen")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0))
|
||||||
|
.frame(
|
||||||
|
egui::Frame::none()
|
||||||
|
.fill(egui::Color32::from_rgba_premultiplied(35, 20, 20, 250))
|
||||||
|
.rounding(egui::Rounding::same(12.0))
|
||||||
|
.stroke(egui::Stroke::new(
|
||||||
|
1.5_f32,
|
||||||
|
egui::Color32::from_rgb(235, 87, 87),
|
||||||
|
))
|
||||||
|
.inner_margin(egui::Margin::symmetric(24.0, 20.0))
|
||||||
|
.shadow(egui::epaint::Shadow {
|
||||||
|
offset: egui::vec2(0.0, 8.0),
|
||||||
|
blur: 24.0,
|
||||||
|
spread: 0.0,
|
||||||
|
color: egui::Color32::from_black_alpha(200),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
ui.set_width(460.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠️");
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("Fehler beim Beenden")
|
||||||
|
.color(egui::Color32::from_rgb(255, 100, 100))
|
||||||
|
.size(16.0)
|
||||||
|
.strong(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(
|
||||||
|
"Ungespeicherte Änderungen konnten nicht gesichert werden:",
|
||||||
|
)
|
||||||
|
.size(13.0)
|
||||||
|
.color(egui::Color32::WHITE),
|
||||||
|
);
|
||||||
|
ui.add_space(6.0);
|
||||||
|
egui::Frame::none()
|
||||||
|
.fill(egui::Color32::from_black_alpha(100))
|
||||||
|
.rounding(egui::Rounding::same(6.0))
|
||||||
|
.inner_margin(egui::Margin::same(8.0))
|
||||||
|
.show(ui, |ui| {
|
||||||
|
for msg in &err.messages {
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(msg)
|
||||||
|
.size(11.5)
|
||||||
|
.color(egui::Color32::from_rgb(255, 180, 180)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_space(8.0);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new("Trotzdem beenden?")
|
||||||
|
.size(13.0)
|
||||||
|
.color(egui::Color32::WHITE),
|
||||||
|
);
|
||||||
|
ui.add_space(12.0);
|
||||||
|
ui.horizontal_wrapped(|ui| {
|
||||||
|
if ui.button("Abbrechen").clicked() {
|
||||||
|
dismiss = true;
|
||||||
|
}
|
||||||
|
if ui.button("Wiederholen").clicked() {
|
||||||
|
actions.push(UiAction::RetryShutdown);
|
||||||
|
}
|
||||||
|
if ui.button("Notfall-Export").clicked() {
|
||||||
|
actions.push(UiAction::ExportRecoveryAndExit);
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button(
|
||||||
|
egui::RichText::new("Trotzdem beenden")
|
||||||
|
.color(egui::Color32::from_rgb(255, 100, 100)),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(UiAction::ForceExit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if dismiss {
|
||||||
|
state.shutdown_error = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user