Merge fix/storage-datensicherheit in main
Konfliktlösung: Speicher-Architektur des Branches (Formatversionierung, Papierkorb, Backups, Instance-Lock, robuster Autosave) übernommen und die dort fehlenden Korrekturen aus main darauf übertragen: - Autosave gleicht das Manifest bei jeder Änderung mit dem Notizbuch ab (mark_dirty); per Taste N/Bild-ab angelegte Seiten gingen sonst nach einem Neustart verloren - Start öffnet keine beschädigten Notizbücher automatisch und verwendet den Ordner eines nicht ladbaren Notizbuchs nicht wieder - PDF-Export lädt nicht geöffnete Seiten nach (load_all_pages), verweigert unvollständige Exporte und meldet das echte Ergebnis an die UI - Cargo.toml: minisign-verify und fs2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
7e99d59042
19 files changed
+3402
-627
No files matched your search
+891
-153
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,221 @@
|
||||
//! Crash-Simulationstesthelfer für ink-notes.
|
||||
//! Wird von scripts/crash-test.sh verwendet, um unvollständige Schreibvorgänge
|
||||
//! und SIGKILL (kill -9) Robustheit zu verifizieren.
|
||||
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
use std::time::Duration;
|
||||
|
||||
use glam::Vec2;
|
||||
use ink_notes::document::{Notebook, PageBackground, StylusSample};
|
||||
use ink_notes::spline::{finalize_stroke_with_config, SplineConfig};
|
||||
use ink_notes::storage::{
|
||||
cleanup_orphaned_temp_files, load_notebook, load_page, save_manifest, save_notebook, save_page,
|
||||
NotebookManifest, PageData,
|
||||
};
|
||||
|
||||
fn run_init(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let nb = Notebook::new();
|
||||
let id = dir
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("crash_test");
|
||||
let mut m = NotebookManifest::new(id, "Crash Test Notebook");
|
||||
m.sync_from_notebook(&nb);
|
||||
save_notebook(dir, &nb, &mut m)?;
|
||||
println!("[init] Initialized clean notebook in {:?}", dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_worker(dir: &Path) {
|
||||
let (mut notebook, mut manifest) = match load_notebook(dir) {
|
||||
Ok((nb, m)) => (nb, m),
|
||||
Err(_) => {
|
||||
let nb = Notebook::new();
|
||||
let id = dir
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("crash_test");
|
||||
let mut m = NotebookManifest::new(id, "Crash Test Notebook");
|
||||
m.sync_from_notebook(&nb);
|
||||
let _ = save_notebook(dir, &nb, &mut m);
|
||||
(nb, m)
|
||||
}
|
||||
};
|
||||
|
||||
let spline_cfg = SplineConfig::default();
|
||||
let mut stroke_counter = manifest.next_stroke_id;
|
||||
|
||||
println!("[worker] Started writing loop in {:?}", dir);
|
||||
|
||||
loop {
|
||||
// Erzeuge neuen Strich mit einigen Samples
|
||||
let base_x = (stroke_counter % 500) as f32;
|
||||
let samples = vec![
|
||||
StylusSample {
|
||||
position: Vec2::new(base_x, 100.0),
|
||||
pressure: 0.5,
|
||||
timestamp: 0.0,
|
||||
},
|
||||
StylusSample {
|
||||
position: Vec2::new(base_x + 10.0, 150.0),
|
||||
pressure: 0.7,
|
||||
timestamp: 0.01,
|
||||
},
|
||||
StylusSample {
|
||||
position: Vec2::new(base_x + 20.0, 200.0),
|
||||
pressure: 0.6,
|
||||
timestamp: 0.02,
|
||||
},
|
||||
];
|
||||
|
||||
let stroke = finalize_stroke_with_config(
|
||||
stroke_counter,
|
||||
&samples,
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
&spline_cfg,
|
||||
);
|
||||
stroke_counter += 1;
|
||||
manifest.next_stroke_id = stroke_counter;
|
||||
|
||||
let page = notebook.current_page_mut();
|
||||
page.add_stroke(stroke);
|
||||
|
||||
let page_data = PageData::from(&*page);
|
||||
|
||||
// 1. Schreibe Seite atomar
|
||||
if let Err(e) = save_page(dir, &page_data) {
|
||||
eprintln!("[worker] Error saving page: {:?}", e);
|
||||
}
|
||||
|
||||
// 2. Aktualisiere Manifest atomar
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
if let Err(e) = save_manifest(dir, &manifest) {
|
||||
eprintln!("[worker] Error saving manifest: {:?}", e);
|
||||
}
|
||||
|
||||
// 3. Gelegentlich Gesamtsave (wie Autosave-Flush)
|
||||
if stroke_counter % 5 == 0 {
|
||||
if let Err(e) = save_notebook(dir, ¬ebook, &mut manifest) {
|
||||
eprintln!("[worker] Error saving notebook: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Evtl. neue Seite anlegen und sofort sichern, bevor Manifest aktualisiert wird
|
||||
if stroke_counter % 25 == 0 {
|
||||
let page_idx = notebook.add_page(PageBackground::DotGrid);
|
||||
notebook.current_page_index = page_idx;
|
||||
let new_page_data = PageData::from(notebook.current_page());
|
||||
if let Err(e) = save_page(dir, &new_page_data) {
|
||||
eprintln!("[worker] Error saving new page: {:?}", e);
|
||||
}
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
if let Err(e) = save_manifest(dir, &manifest) {
|
||||
eprintln!("[worker] Error saving manifest: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_verify(dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("[verify] Checking notebook in {:?}", dir);
|
||||
|
||||
// 1. Bereinige eventuelle verwaiste .tmp-Dateien
|
||||
let _ = cleanup_orphaned_temp_files(dir, Duration::from_secs(0));
|
||||
|
||||
// Prüfe, ob das Verzeichnis noch gar kein Notizbuch enthält (Prozess vor erstem Flush getötet)
|
||||
let manifest_path = dir.join("manifest.json");
|
||||
let manifest_bak = dir.join("manifest.json.bak");
|
||||
if !manifest_path.exists() && !manifest_bak.exists() {
|
||||
println!("[verify] Notebook not yet created on disk (killed prior to first save). Valid empty state.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 2. Notizbuch laden
|
||||
let (_notebook, manifest) = match load_notebook(dir) {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
eprintln!("[verify] FAILED to load notebook: {:?}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"[verify] Manifest loaded successfully: title='{}', pages={}, next_stroke_id={}",
|
||||
manifest.title,
|
||||
manifest.pages.len(),
|
||||
manifest.next_stroke_id
|
||||
);
|
||||
|
||||
// 3. Alle im Manifest registrierten Seiten laden und prüfen
|
||||
for page_meta in &manifest.pages {
|
||||
match load_page(dir, page_meta.id) {
|
||||
Ok(page_data) => {
|
||||
let page = page_data.to_page();
|
||||
println!(
|
||||
"[verify] Page {} verified: {} strokes, background={:?}",
|
||||
page_meta.id,
|
||||
page.strokes.len(),
|
||||
page.background
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"[verify] FAILED to load page {} referenced by manifest: {:?}",
|
||||
page_meta.id, e
|
||||
);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Prüfen ob Quarantäne-Dateien existieren
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy();
|
||||
if name.contains(".corrupt-") {
|
||||
println!("[verify] Notice: Found quarantined file: {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[verify] SUCCESS: All pages and manifest are fully consistent!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 3 {
|
||||
eprintln!("Usage: crash_sim <init|worker|verify> <notebook_dir>");
|
||||
process::exit(2);
|
||||
}
|
||||
|
||||
let command = &args[1];
|
||||
let notebook_dir = Path::new(&args[2]);
|
||||
|
||||
match command.as_str() {
|
||||
"init" => match run_init(notebook_dir) {
|
||||
Ok(_) => process::exit(0),
|
||||
Err(e) => {
|
||||
eprintln!("[init] Error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
"worker" => {
|
||||
run_worker(notebook_dir);
|
||||
}
|
||||
"verify" => match run_verify(notebook_dir) {
|
||||
Ok(_) => process::exit(0),
|
||||
Err(e) => {
|
||||
eprintln!("[verify] Error: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
},
|
||||
other => {
|
||||
eprintln!("Unknown command: {}", other);
|
||||
process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -104,6 +104,8 @@ pub struct CanvasState {
|
||||
pub camera_buffer: wgpu::Buffer,
|
||||
pub camera_bind_group: wgpu::BindGroup,
|
||||
pub current_ink_color: [f32; 4],
|
||||
pub current_stroke_width_mm: f32,
|
||||
pub current_tool: crate::document::StrokeTool,
|
||||
|
||||
// Statische Hintergrund-Geometrie
|
||||
pub paper_buffer: wgpu::Buffer,
|
||||
@@ -374,6 +376,8 @@ impl CanvasState {
|
||||
camera_buffer,
|
||||
camera_bind_group,
|
||||
current_ink_color: default_ink_color,
|
||||
current_stroke_width_mm: 0.5,
|
||||
current_tool: crate::document::StrokeTool::Pen,
|
||||
paper_buffer,
|
||||
background_buffer,
|
||||
background_vertex_capacity: Self::INITIAL_BACKGROUND_CAPACITY,
|
||||
@@ -502,10 +506,14 @@ impl CanvasState {
|
||||
|
||||
/// Konfiguriert die Strichbreite in Millimetern.
|
||||
pub fn set_stroke_width_mm(&mut self, width_mm: f32) {
|
||||
// 10 Einheiten = 1 mm. Radius = (width_mm * 10.0) / 2.0 = width_mm * 5.0.
|
||||
let base_radius = (width_mm * 5.0).max(0.25);
|
||||
self.spline_config.min_radius = (base_radius * 0.5).max(0.15);
|
||||
self.spline_config.max_radius = (base_radius * 1.5).max(0.35);
|
||||
self.current_stroke_width_mm = width_mm;
|
||||
self.spline_config =
|
||||
SplineConfig::from_width_units(width_mm * crate::document::UNITS_PER_MM);
|
||||
}
|
||||
|
||||
/// Setzt das aktive Zeichenwerkzeug für den Stift.
|
||||
pub fn set_tool(&mut self, tool: crate::document::StrokeTool) {
|
||||
self.current_tool = tool;
|
||||
}
|
||||
|
||||
/// Setzt den Seitenhintergrund und aktualisiert bei Bedarf den Vertex-Puffer.
|
||||
@@ -643,6 +651,10 @@ impl CanvasState {
|
||||
self.current_ink_color,
|
||||
&self.spline_config,
|
||||
);
|
||||
let mut stroke = stroke;
|
||||
stroke.tool = self.current_tool;
|
||||
stroke.width = self.current_stroke_width_mm * crate::document::UNITS_PER_MM;
|
||||
stroke.finalized = true;
|
||||
|
||||
// Temporären GPU-Live-Buffer vollständig leeren
|
||||
self.live_samples.clear();
|
||||
|
||||
+62
-5
@@ -103,6 +103,16 @@ pub fn point_to_segment_distance(p: glam::Vec2, a: glam::Vec2, b: glam::Vec2) ->
|
||||
(p - projection).length()
|
||||
}
|
||||
|
||||
/// Das Werkzeug, mit dem ein Strich gezeichnet wurde.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum StrokeTool {
|
||||
#[default]
|
||||
Pen,
|
||||
Highlighter,
|
||||
Shape,
|
||||
}
|
||||
|
||||
/// Ein einzelner Vektor-Strich auf einer Seite.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Stroke {
|
||||
@@ -112,6 +122,9 @@ pub struct Stroke {
|
||||
pub aabb: Aabb,
|
||||
pub color: [f32; 4],
|
||||
pub shape: Option<crate::geometry::RecognizedShape>,
|
||||
pub tool: StrokeTool,
|
||||
pub width: f32,
|
||||
pub finalized: bool,
|
||||
}
|
||||
|
||||
impl Stroke {
|
||||
@@ -141,6 +154,9 @@ impl Stroke {
|
||||
aabb,
|
||||
color,
|
||||
shape: None,
|
||||
tool: StrokeTool::Pen,
|
||||
width: 5.0,
|
||||
finalized: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +173,22 @@ impl Stroke {
|
||||
/// Verknüpft diesen Strich mit einer mathematisch erkannten Form.
|
||||
pub fn with_shape(mut self, shape: crate::geometry::RecognizedShape) -> Self {
|
||||
self.shape = Some(shape);
|
||||
self.tool = StrokeTool::Shape;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tool(mut self, tool: StrokeTool) -> Self {
|
||||
self.tool = tool;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_width(mut self, width: f32) -> Self {
|
||||
self.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_finalized(mut self, finalized: bool) -> Self {
|
||||
self.finalized = finalized;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -304,6 +336,22 @@ pub enum PageBackground {
|
||||
DotGrid,
|
||||
}
|
||||
|
||||
/// Persistenzstatus einer Seite zum Schutz vor unbeabsichtigtem Überschreiben bei Ladefehlern.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum PersistState {
|
||||
#[default]
|
||||
Normal,
|
||||
ReadOnlyAfterLoadError,
|
||||
}
|
||||
|
||||
/// Warnung beim Laden einer Seite (beschädigte Datei oder I/O-Fehler).
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PageLoadWarning {
|
||||
pub page_id: u64,
|
||||
pub message: String,
|
||||
pub quarantine_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
/// Eine einzelne Notizbuchseite mit Strichen, Hintergrund und gecachten Meshes.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Page {
|
||||
@@ -314,9 +362,7 @@ pub struct Page {
|
||||
pub stroke_ranges: Vec<Range<u32>>,
|
||||
pub is_dirty: bool,
|
||||
pub is_loaded: bool,
|
||||
/// Laden von der Festplatte ist fehlgeschlagen; die Seite ist ein leerer Ersatz und
|
||||
/// darf niemals gespeichert werden, damit die Originaldatei nicht überschrieben wird.
|
||||
pub load_failed: bool,
|
||||
pub persist: PersistState,
|
||||
pub history: HistoryStack,
|
||||
}
|
||||
|
||||
@@ -330,7 +376,7 @@ impl Page {
|
||||
stroke_ranges: Vec::new(),
|
||||
is_dirty: false,
|
||||
is_loaded: true,
|
||||
load_failed: false,
|
||||
persist: PersistState::Normal,
|
||||
history: HistoryStack::new(),
|
||||
}
|
||||
}
|
||||
@@ -345,11 +391,19 @@ impl Page {
|
||||
stroke_ranges: Vec::new(),
|
||||
is_dirty: false,
|
||||
is_loaded: false,
|
||||
load_failed: false,
|
||||
persist: PersistState::Normal,
|
||||
history: HistoryStack::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_readonly(&self) -> bool {
|
||||
self.persist == PersistState::ReadOnlyAfterLoadError
|
||||
}
|
||||
|
||||
pub fn unlock_after_load_error(&mut self) {
|
||||
self.persist = PersistState::Normal;
|
||||
}
|
||||
|
||||
/// Fügt einen neuen Strich hinzu und registriert ihn im History-Stack.
|
||||
pub fn add_stroke(&mut self, stroke: Stroke) {
|
||||
self.history.push(HistoryAction::AddStroke(stroke.clone()));
|
||||
@@ -592,6 +646,7 @@ pub struct Notebook {
|
||||
pub current_page_index: usize,
|
||||
pub next_stroke_id: u64,
|
||||
pub next_page_id: u64,
|
||||
pub load_warnings: std::collections::HashMap<u64, PageLoadWarning>,
|
||||
}
|
||||
|
||||
impl Notebook {
|
||||
@@ -601,6 +656,7 @@ impl Notebook {
|
||||
current_page_index: 0,
|
||||
next_stroke_id: 1,
|
||||
next_page_id: 1,
|
||||
load_warnings: std::collections::HashMap::new(),
|
||||
};
|
||||
// Erste Standardseite anlegen (5mm Grid)
|
||||
nb.add_page(PageBackground::Grid5mm);
|
||||
@@ -619,6 +675,7 @@ impl Notebook {
|
||||
current_page_index,
|
||||
next_stroke_id,
|
||||
next_page_id,
|
||||
load_warnings: std::collections::HashMap::new(),
|
||||
};
|
||||
if nb.pages.is_empty() {
|
||||
nb.add_page(PageBackground::Grid5mm);
|
||||
|
||||
+3
-5
@@ -140,11 +140,9 @@ pub fn generate_pdf_document(
|
||||
return Err("Notizbuch enthält keine Seiten".into());
|
||||
}
|
||||
// Nicht geladene Seiten würden sonst stillschweigend als leere Seiten exportiert
|
||||
if let Some(page) = notebook
|
||||
.pages
|
||||
.iter()
|
||||
.find(|p| !p.is_loaded || p.load_failed)
|
||||
{
|
||||
if let Some(page) = notebook.pages.iter().find(|p| {
|
||||
!p.is_loaded || p.persist == crate::document::PersistState::ReadOnlyAfterLoadError
|
||||
}) {
|
||||
return Err(format!(
|
||||
"Seite {} ist nicht geladen – Export abgebrochen (vorher storage::load_all_pages aufrufen)",
|
||||
page.id
|
||||
|
||||
@@ -1411,6 +1411,9 @@ pub fn shape_to_stroke(
|
||||
let (samples, vertices) = shape_to_mesh(&shape, width_units, color);
|
||||
let mut stroke = Stroke::new(stroke_id, samples, vertices, color);
|
||||
stroke.shape = Some(shape);
|
||||
stroke.width = width_units;
|
||||
stroke.tool = crate::document::StrokeTool::Shape;
|
||||
stroke.finalized = true;
|
||||
stroke
|
||||
}
|
||||
|
||||
|
||||
+283
-85
@@ -159,9 +159,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// 1. Notizbuch aus Storage laden oder initialisieren (Session-Wiederherstellung)
|
||||
let storage_base_dir = storage::default_storage_dir();
|
||||
let _ = storage::cleanup_trash(
|
||||
&storage_base_dir,
|
||||
std::time::Duration::from_secs(30 * 24 * 3600),
|
||||
);
|
||||
let available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
|
||||
let (initial_id, initial_title) = if let Some(first) = available_notebooks.first() {
|
||||
// Beschädigte Notizbücher nicht automatisch öffnen
|
||||
let (initial_id, initial_title) = if let Some(first) = available_notebooks
|
||||
.iter()
|
||||
.find(|s| s.status == storage::NotebookStatus::Normal)
|
||||
{
|
||||
(first.id.clone(), first.title.clone())
|
||||
} else {
|
||||
("default".to_string(), "Standard-Notizbuch".to_string())
|
||||
@@ -231,6 +239,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
let mut selection_state = SelectionState::new();
|
||||
|
||||
let mut _notebook_lock = match storage::NotebookLock::acquire(&storage_dir) {
|
||||
Ok(lock) => {
|
||||
autosave.set_read_only(false);
|
||||
Some(lock)
|
||||
}
|
||||
Err(storage::StorageError::AlreadyLocked(msg)) => {
|
||||
eprintln!("[Storage-Lock] {msg}");
|
||||
autosave.set_read_only(true);
|
||||
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)");
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}");
|
||||
autosave.set_read_only(true);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
canvas.set_stroke_color(ui_state.pen_color);
|
||||
@@ -410,7 +436,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(
|
||||
&window,
|
||||
¬ebook,
|
||||
@@ -487,7 +513,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(
|
||||
&window,
|
||||
¬ebook,
|
||||
@@ -638,8 +664,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
match event {
|
||||
WindowEvent::CloseRequested => {
|
||||
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
||||
autosave.shutdown();
|
||||
target.exit();
|
||||
match autosave.shutdown() {
|
||||
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) => {
|
||||
@@ -708,7 +742,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
match &logical_key {
|
||||
Key::Named(NamedKey::Escape) => {
|
||||
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.show_page_jump_popup
|
||||
|| ui_state.is_document_menu_open
|
||||
@@ -737,7 +774,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if notebook.current_page_mut().undo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -751,7 +788,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if notebook.current_page_mut().redo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -781,7 +818,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !new_ids.is_empty() {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -793,7 +830,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -802,9 +839,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Jetzt speichern (Disk Flush): Strg + S
|
||||
Key::Character(c) if c.eq_ignore_ascii_case("s") && ctrl => {
|
||||
match autosave.save_now(¬ebook, &manifest) {
|
||||
Ok(()) => ui_state.set_export_message("Gespeichert"),
|
||||
Err(err) => ui_state.set_export_message(format!("Speichern fehlgeschlagen: {err}")),
|
||||
match autosave.flush() {
|
||||
Ok(()) => {
|
||||
ui_state.set_export_message("Gespeichert");
|
||||
}
|
||||
Err(err) => {
|
||||
ui_state.set_export_message(format!(
|
||||
"Speicherfehler: {}",
|
||||
err.messages.join(", ")
|
||||
));
|
||||
}
|
||||
}
|
||||
update_window_title(
|
||||
&window,
|
||||
@@ -855,7 +899,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let color = [0.12, 0.12, 0.14, 1.0];
|
||||
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message("Auswahl auf Schwarz umgefärbt".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -875,7 +919,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let color = [0.15, 0.35, 0.85, 1.0];
|
||||
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message("Auswahl auf Blau umgefärbt".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -894,7 +938,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !selection_state.is_empty() {
|
||||
if selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -918,7 +962,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
PageBackground::Blank => PageBackground::Grid5mm,
|
||||
};
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -932,7 +976,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
manifest.next_page_id = notebook.next_page_id;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -944,7 +990,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
manifest.next_page_id = notebook.next_page_id;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -957,7 +1005,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -970,7 +1020,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -983,7 +1035,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1023,8 +1077,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Beenden: Strg + Q
|
||||
Key::Character(c) if c.eq_ignore_ascii_case("q") && ctrl => {
|
||||
println!("[Shutdown] Sichere alle ungespeicherten Daten auf Festplatte...");
|
||||
autosave.shutdown();
|
||||
target.exit();
|
||||
match autosave.shutdown() {
|
||||
Ok(()) => {
|
||||
target.exit();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[Shutdown-Fehler] Konnte nicht geordnet beenden: {err}");
|
||||
ui_state.shutdown_error = Some(err);
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1064,7 +1126,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1162,7 +1224,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let new_stroke = geometry::shape_to_stroke(shape, old_id, stroke_color, stroke_width);
|
||||
notebook.current_page_mut().replace_stroke(old_id, new_stroke);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message(format!("{shape_name} ausgerichtet! (Undo mit Strg+Z)"));
|
||||
window.request_redraw();
|
||||
return;
|
||||
@@ -1219,7 +1281,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let final_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||
if !initial_strokes.is_empty() && !final_strokes.is_empty() && initial_strokes != final_strokes {
|
||||
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1332,7 +1394,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1456,7 +1518,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1597,7 +1659,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1688,7 +1750,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let final_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
||||
if !initial_strokes.is_empty() && !final_strokes.is_empty() && initial_strokes != final_strokes {
|
||||
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1772,7 +1834,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !selection_state.is_empty()
|
||||
&& selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message("Auswahl umgefärbt".to_string());
|
||||
}
|
||||
match ui_state.active_tool {
|
||||
@@ -1807,7 +1869,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
||||
if !removed.is_empty() {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1834,7 +1896,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !new_ids.is_empty() {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1843,7 +1905,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if !selection_state.is_empty()
|
||||
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1871,7 +1933,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if notebook.current_page_mut().undo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1880,7 +1942,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if notebook.current_page_mut().redo() {
|
||||
selection_state.clear();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1892,7 +1954,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, curr);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1904,7 +1968,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
manifest.next_page_id = notebook.next_page_id;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.pages = notebook
|
||||
.pages
|
||||
.iter()
|
||||
.map(|p| storage::PageMeta {
|
||||
id: p.id,
|
||||
background: Some(p.background),
|
||||
})
|
||||
.collect();
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1914,8 +1988,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
notebook.add_page(bg);
|
||||
manifest.current_page_index = notebook.current_page_index;
|
||||
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());
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1926,7 +2010,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, idx);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
manifest.current_page_index = idx;
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1935,7 +2021,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let page = notebook.current_page_mut();
|
||||
page.background = bg;
|
||||
canvas.sync_page(page);
|
||||
autosave.mark_dirty(¬ebook, &manifest);
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1957,9 +2043,25 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let new_notebook_dir = storage_base_dir.join(&id);
|
||||
match storage::load_notebook(&new_notebook_dir) {
|
||||
Ok((loaded_nb, loaded_manifest)) => {
|
||||
if let Err(err) = autosave.switch_notebook_dir(new_notebook_dir.clone()) {
|
||||
eprintln!("[Autosave-Fehler] Vorheriges Notizbuch nicht vollständig gespeichert: {err}");
|
||||
}
|
||||
let _ = autosave.flush();
|
||||
match storage::NotebookLock::acquire(&new_notebook_dir) {
|
||||
Ok(lock) => {
|
||||
_notebook_lock = Some(lock);
|
||||
autosave.set_read_only(false);
|
||||
}
|
||||
Err(storage::StorageError::AlreadyLocked(msg)) => {
|
||||
eprintln!("[Storage-Lock] {msg}");
|
||||
_notebook_lock = None;
|
||||
autosave.set_read_only(true);
|
||||
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)");
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}");
|
||||
_notebook_lock = None;
|
||||
autosave.set_read_only(true);
|
||||
}
|
||||
}
|
||||
autosave.switch_notebook_dir(new_notebook_dir.clone());
|
||||
notebook = loaded_nb;
|
||||
manifest = loaded_manifest;
|
||||
storage_dir = new_notebook_dir;
|
||||
@@ -1970,7 +2072,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ui_state.is_document_menu_open = false;
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
ui_state.set_export_message(format!("Dokument '{}' geöffnet", manifest.title));
|
||||
if _notebook_lock.is_some() {
|
||||
ui_state.set_export_message(format!("Dokument '{}' geöffnet", manifest.title));
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -1981,11 +2085,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
UiAction::CreateNotebook(title) => {
|
||||
let _ = autosave.flush();
|
||||
match storage::create_new_notebook(&storage_base_dir, &title) {
|
||||
Ok((new_nb, new_manifest, new_dir)) => {
|
||||
if let Err(err) = autosave.switch_notebook_dir(new_dir.clone()) {
|
||||
eprintln!("[Autosave-Fehler] Vorheriges Notizbuch nicht vollständig gespeichert: {err}");
|
||||
}
|
||||
_notebook_lock = storage::NotebookLock::acquire(&new_dir).ok();
|
||||
autosave.set_read_only(_notebook_lock.is_none());
|
||||
autosave.switch_notebook_dir(new_dir.clone());
|
||||
notebook = new_nb;
|
||||
manifest = new_manifest;
|
||||
storage_dir = new_dir;
|
||||
@@ -2012,6 +2117,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Ok(()) => {
|
||||
ui_state.document_title = manifest.title.clone();
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
manifest.sync_from_notebook(¬ebook);
|
||||
autosave.mark_manifest_dirty(&manifest);
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
ui_state.set_export_message(format!("Umbenannt in '{}'", new_title));
|
||||
window.request_redraw();
|
||||
@@ -2036,54 +2143,66 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
UiAction::DeleteNotebook(id) => {
|
||||
let target_dir = storage_base_dir.join(&id);
|
||||
let is_current = id == manifest.id;
|
||||
let deleted_title = ui_state
|
||||
.available_notebooks
|
||||
.iter()
|
||||
.find(|s| s.id == id)
|
||||
.map(|s| s.title.clone())
|
||||
.unwrap_or_else(|| id.clone());
|
||||
|
||||
if is_current {
|
||||
// Ausstehende Autosave-Schreibvorgänge VOR dem Löschen abarbeiten,
|
||||
// sonst legen sie den gelöschten Ordner danach wieder an.
|
||||
let _ = autosave.flush();
|
||||
drop(_notebook_lock.take());
|
||||
}
|
||||
match storage::delete_notebook(&target_dir) {
|
||||
Ok(()) => {
|
||||
|
||||
match storage::delete_notebook(&target_dir, &storage_base_dir) {
|
||||
Ok(trash_path) => {
|
||||
ui_state.last_deleted_notebook = Some((trash_path, deleted_title));
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
if is_current {
|
||||
// Erstes ladbares Notizbuch öffnen, sonst ein neues anlegen
|
||||
let replacement = ui_state
|
||||
if let Some(first) = ui_state
|
||||
.available_notebooks
|
||||
.iter()
|
||||
.find_map(|summary| {
|
||||
let dir = storage_base_dir.join(&summary.id);
|
||||
match storage::load_notebook(&dir) {
|
||||
Ok((nb, m)) => Some((nb, m, dir)),
|
||||
Err(err) => {
|
||||
eprintln!("[Dokument-Fehler] Konnte Dokument '{}' nicht laden: {err}", summary.id);
|
||||
None
|
||||
}
|
||||
.find(|s| s.status != storage::NotebookStatus::Damaged)
|
||||
.cloned()
|
||||
{
|
||||
let new_notebook_dir = storage_base_dir.join(&first.id);
|
||||
match storage::load_notebook(&new_notebook_dir) {
|
||||
Ok((loaded_nb, loaded_manifest)) => {
|
||||
_notebook_lock = storage::NotebookLock::acquire(&new_notebook_dir).ok();
|
||||
autosave.set_read_only(_notebook_lock.is_none());
|
||||
autosave.switch_notebook_dir(new_notebook_dir.clone());
|
||||
notebook = loaded_nb;
|
||||
manifest = loaded_manifest;
|
||||
storage_dir = new_notebook_dir;
|
||||
selection_state.clear();
|
||||
ui_state.current_notebook_id = manifest.id.clone();
|
||||
ui_state.document_title = manifest.title.clone();
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
ui_state.set_export_message(format!("Gewechselt zu '{}'", manifest.title));
|
||||
}
|
||||
})
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| storage::create_new_notebook(&storage_base_dir, "Standard-Notizbuch"));
|
||||
match replacement {
|
||||
Ok((new_nb, new_manifest, new_dir)) => {
|
||||
if let Err(err) = autosave.switch_notebook_dir(new_dir.clone()) {
|
||||
eprintln!("[Autosave-Fehler] Vorheriges Notizbuch nicht vollständig gespeichert: {err}");
|
||||
Err(err) => {
|
||||
eprintln!("[Dokument-Fehler] Konnte Dokument '{}' nicht laden: {err}", first.id);
|
||||
}
|
||||
notebook = new_nb;
|
||||
manifest = new_manifest;
|
||||
storage_dir = new_dir;
|
||||
selection_state.clear();
|
||||
ui_state.current_notebook_id = manifest.id.clone();
|
||||
ui_state.document_title = manifest.title.clone();
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
ui_state.set_export_message(format!("Gewechselt zu '{}'", manifest.title));
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[Dokument-Fehler] Konnte kein Ersatz-Notizbuch anlegen: {err}");
|
||||
ui_state.set_export_message(format!("Dokument gelöscht, aber kein Ersatz-Notizbuch: {err}"));
|
||||
}
|
||||
} else if let Ok((new_nb, new_manifest, new_dir)) = storage::create_new_notebook(&storage_base_dir, "Standard-Notizbuch") {
|
||||
_notebook_lock = storage::NotebookLock::acquire(&new_dir).ok();
|
||||
autosave.set_read_only(_notebook_lock.is_none());
|
||||
autosave.switch_notebook_dir(new_dir.clone());
|
||||
notebook = new_nb;
|
||||
manifest = new_manifest;
|
||||
storage_dir = new_dir;
|
||||
selection_state.clear();
|
||||
ui_state.current_notebook_id = manifest.id.clone();
|
||||
ui_state.document_title = manifest.title.clone();
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
canvas.sync_page(notebook.current_page_mut());
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
ui_state.set_export_message("Standard-Notizbuch erstellt".to_string());
|
||||
}
|
||||
} else {
|
||||
ui_state.set_export_message("Dokument gelöscht".to_string());
|
||||
ui_state.set_export_message("Dokument in Papierkorb verschoben".to_string());
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -2093,6 +2212,40 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::UndoDeleteNotebook => {
|
||||
if let Some((trash_path, _title)) = ui_state.last_deleted_notebook.take() {
|
||||
match storage::restore_notebook_from_trash(&storage_base_dir, &trash_path) {
|
||||
Ok(_restored_dir) => {
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
ui_state.set_export_message("Notizbuch wiederhergestellt".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[Dokument-Fehler] Konnte Notizbuch nicht wiederherstellen: {err}");
|
||||
ui_state.set_export_message(format!("Fehler beim Wiederherstellen: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::RestoreNotebookBackup(id) => {
|
||||
let target_dir = storage_base_dir.join(&id);
|
||||
match storage::restore_manifest_from_backup(&target_dir) {
|
||||
Ok(()) => {
|
||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||
ui_state.set_export_message("Manifest aus Backup wiederhergestellt".to_string());
|
||||
window.request_redraw();
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("[Dokument-Fehler] Konnte Backup für '{id}' nicht wiederherstellen: {err}");
|
||||
ui_state.set_export_message(format!("Wiederherstellung fehlgeschlagen: {err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
UiAction::OpenNotebookFolder(id) => {
|
||||
let target_dir = storage_base_dir.join(&id);
|
||||
ui_state.set_export_message(format!("Ordner: {}", target_dir.display()));
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::CheckForUpdates => {
|
||||
updater.check_for_updates();
|
||||
window.request_redraw();
|
||||
@@ -2118,6 +2271,51 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
ui_state.set_export_message("Server-Einstellungen gespeichert");
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::UnlockCorruptedPage(page_id) => {
|
||||
notebook.load_warnings.remove(&page_id);
|
||||
if let Some(page) = notebook.pages.iter_mut().find(|p| p.id == page_id) {
|
||||
page.unlock_after_load_error();
|
||||
}
|
||||
autosave.mark_dirty(¬ebook, &mut manifest);
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -105,6 +105,18 @@ impl Default for SplineConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl SplineConfig {
|
||||
/// Erzeugt eine Spline-Konfiguration aus der Strichbreite in internen Einheiten (10 Einheiten = 1 mm).
|
||||
pub fn from_width_units(width_units: f32) -> Self {
|
||||
let base_radius = (width_units / 2.0).max(0.25);
|
||||
Self {
|
||||
min_radius: (base_radius * 0.5).max(0.15),
|
||||
max_radius: (base_radius * 1.5).max(0.35),
|
||||
samples_per_segment: 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Berechnet den druckabhängigen Radius r(p) = min_r + p * (max_r - min_r).
|
||||
pub fn calculate_radius(pressure: f32, min_radius: f32, max_radius: f32) -> f32 {
|
||||
let p = pressure.clamp(0.0, 1.0);
|
||||
@@ -653,7 +665,10 @@ pub fn finalize_stroke_with_config(
|
||||
.map(|v| InkVertex::new(v.position[0], v.position[1], color))
|
||||
.collect();
|
||||
|
||||
Stroke::new(stroke_id, decimated, ink_vertices, color)
|
||||
let mut stroke = Stroke::new(stroke_id, decimated, ink_vertices, color);
|
||||
stroke.width = config.min_radius + config.max_radius;
|
||||
stroke.finalized = true;
|
||||
stroke
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+1449
-307
File diff suppressed because it is too large.
Load diff
@@ -113,6 +113,9 @@ pub enum UiAction {
|
||||
new_title: String,
|
||||
},
|
||||
DeleteNotebook(String),
|
||||
UndoDeleteNotebook,
|
||||
RestoreNotebookBackup(String),
|
||||
OpenNotebookFolder(String),
|
||||
RefreshNotebooks,
|
||||
// Gitea Release & Updater
|
||||
CheckForUpdates,
|
||||
@@ -123,6 +126,12 @@ pub enum UiAction {
|
||||
server_url: String,
|
||||
auth_token: Option<String>,
|
||||
},
|
||||
// Datenintegrität / Quarantäne
|
||||
UnlockCorruptedPage(u64),
|
||||
// Beenden-Sicherheit bei Fehlschlag
|
||||
RetryShutdown,
|
||||
ExportRecoveryAndExit,
|
||||
ForceExit,
|
||||
}
|
||||
|
||||
/// Zustand der grafischen Benutzeroberfläche.
|
||||
@@ -162,6 +171,10 @@ pub struct UiState {
|
||||
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)
|
||||
pub touch_draw_enabled: bool,
|
||||
/// Zuletzt gelöschtes Notizbuch für Rückgängig während der Sitzung (Papierkorb-Pfad, Titel)
|
||||
pub last_deleted_notebook: Option<(std::path::PathBuf, String)>,
|
||||
/// Fehler beim Flushen vor dem Beenden
|
||||
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
||||
}
|
||||
|
||||
impl Default for UiState {
|
||||
@@ -193,6 +206,8 @@ impl Default for UiState {
|
||||
show_updater_config: false,
|
||||
update_modal_rect: None,
|
||||
touch_draw_enabled: false,
|
||||
last_deleted_notebook: None,
|
||||
shutdown_error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1546,11 +1561,17 @@ pub fn render_ui(
|
||||
.size(13.0)
|
||||
.color(egui::Color32::WHITE),
|
||||
);
|
||||
if is_current {
|
||||
if summary.status == crate::storage::NotebookStatus::Damaged {
|
||||
ui.colored_label(egui::Color32::from_rgb(255, 100, 100), "⚠️ Beschädigt");
|
||||
} else if is_current {
|
||||
ui.colored_label(egui::Color32::from_rgb(46, 204, 113), "● Aktiv");
|
||||
}
|
||||
});
|
||||
let subtext = format!("{} Seite(n)", summary.page_count);
|
||||
let subtext = if let Some(ref err) = summary.error {
|
||||
format!("Fehler: {err}")
|
||||
} else {
|
||||
format!("{} Seite(n)", summary.page_count)
|
||||
};
|
||||
ui.label(
|
||||
egui::RichText::new(subtext)
|
||||
.size(11.0)
|
||||
@@ -1559,27 +1580,47 @@ pub fn render_ui(
|
||||
});
|
||||
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
if state.available_notebooks.len() > 1
|
||||
&& ui
|
||||
.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
|
||||
.on_hover_text("Notizbuch unwiderruflich löschen")
|
||||
if summary.status == crate::storage::NotebookStatus::Damaged {
|
||||
if ui
|
||||
.button(egui::RichText::new("🔄 Backup").size(11.0).color(egui::Color32::from_rgb(100, 200, 255)))
|
||||
.on_hover_text("Aus manifest.json.bak wiederherstellen")
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
|
||||
}
|
||||
{
|
||||
actions.push(UiAction::RestoreNotebookBackup(summary.id.clone()));
|
||||
}
|
||||
if ui
|
||||
.button(egui::RichText::new("📁 Ordner").size(11.0))
|
||||
.on_hover_text("Pfad in Statusleiste anzeigen")
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::OpenNotebookFolder(summary.id.clone()));
|
||||
}
|
||||
} else {
|
||||
if state.available_notebooks.len() > 1
|
||||
&& ui
|
||||
.button(egui::RichText::new("🗑").size(11.0).color(egui::Color32::from_rgb(255, 120, 120)))
|
||||
.on_hover_text("Notizbuch in Papierkorb verschieben")
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::DeleteNotebook(summary.id.clone()));
|
||||
}
|
||||
|
||||
if ui.button(egui::RichText::new("✏").size(11.0))
|
||||
.on_hover_text("Titel umbenennen")
|
||||
.clicked()
|
||||
{
|
||||
state.renaming_notebook_id = Some(summary.id.clone());
|
||||
state.renaming_title_input = summary.title.clone();
|
||||
if ui.button(egui::RichText::new("✏").size(11.0))
|
||||
.on_hover_text("Titel umbenennen")
|
||||
.clicked()
|
||||
{
|
||||
state.renaming_notebook_id = Some(summary.id.clone());
|
||||
state.renaming_title_input = summary.title.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if frame_resp.response.interact(egui::Sense::click()).clicked() && !is_current {
|
||||
if summary.status != crate::storage::NotebookStatus::Damaged
|
||||
&& frame_resp.response.interact(egui::Sense::click()).clicked()
|
||||
&& !is_current
|
||||
{
|
||||
actions.push(UiAction::OpenNotebook(summary.id.clone()));
|
||||
state.is_document_menu_open = false;
|
||||
}
|
||||
@@ -1589,6 +1630,23 @@ pub fn render_ui(
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_, ref title)) = state.last_deleted_notebook {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(format!("'{title}' gelöscht"))
|
||||
.size(11.0)
|
||||
.color(egui::Color32::from_rgb(255, 180, 100)),
|
||||
);
|
||||
if ui
|
||||
.button(egui::RichText::new("↩ Rückgängig").size(11.0).color(egui::Color32::from_rgb(100, 200, 255)))
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::UndoDeleteNotebook);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.separator();
|
||||
ui.add_space(4.0);
|
||||
@@ -1932,6 +1990,183 @@ pub fn render_ui(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Nicht-wegklickbare Warnung bei Ladefehlern / Quarantäne (Block 1)
|
||||
if let Some(warning) = notebook.load_warnings.get(¬ebook.current_page().id) {
|
||||
egui::Window::new("⚠️ Datenintegritäts-Warnung")
|
||||
.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, 245))
|
||||
.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(180),
|
||||
}),
|
||||
)
|
||||
.show(ctx, |ui| {
|
||||
ui.set_width(420.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.colored_label(egui::Color32::from_rgb(255, 80, 80), "⚠️");
|
||||
ui.label(
|
||||
egui::RichText::new(format!(
|
||||
"Seite {} konnte nicht fehlerfrei geladen werden!",
|
||||
warning.page_id
|
||||
))
|
||||
.strong()
|
||||
.size(15.0)
|
||||
.color(egui::Color32::WHITE),
|
||||
);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
egui::RichText::new(format!("Fehlermeldung: {}", warning.message))
|
||||
.size(12.0)
|
||||
.color(egui::Color32::from_gray(200)),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
if let Some(ref q_path) = warning.quarantine_path {
|
||||
ui.label(
|
||||
egui::RichText::new("Die beschädigte Originaldatei wurde in Quarantäne gesichert:")
|
||||
.size(12.0)
|
||||
.color(egui::Color32::from_gray(220)),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
ui.monospace(
|
||||
egui::RichText::new(q_path.display().to_string())
|
||||
.size(11.0)
|
||||
.color(egui::Color32::from_rgb(255, 215, 0)),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Um Datenverlust zu verhindern, ist diese Seite schreibgeschützt.\n\
|
||||
Wenn Sie diese Seite bearbeiten möchten, können Sie den Schreibschutz aufheben.",
|
||||
)
|
||||
.size(11.5)
|
||||
.color(egui::Color32::from_gray(180)),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
if ui
|
||||
.button(
|
||||
egui::RichText::new("Als leere Seite bearbeiten (Schreibschutz aufheben)")
|
||||
.color(egui::Color32::WHITE)
|
||||
.size(12.5),
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::UnlockCorruptedPage(warning.page_id));
|
||||
}
|
||||
} else {
|
||||
ui.label(
|
||||
egui::RichText::new(
|
||||
"Zugriffsfehler (I/O). Die Datei existiert, kann aber nicht gelesen werden.\n\
|
||||
Die Seite bleibt schreibgeschützt, um ein Überschreiben zu verhindern.",
|
||||
)
|
||||
.size(12.0)
|
||||
.color(egui::Color32::from_gray(200)),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -2058,12 +2293,16 @@ mod tests {
|
||||
title: "Mathematik I".to_string(),
|
||||
page_count: 3,
|
||||
modified_at: 100,
|
||||
status: crate::storage::NotebookStatus::Normal,
|
||||
error: None,
|
||||
},
|
||||
NotebookSummary {
|
||||
id: "nb-2".to_string(),
|
||||
title: "Physik II".to_string(),
|
||||
page_count: 5,
|
||||
modified_at: 200,
|
||||
status: crate::storage::NotebookStatus::Normal,
|
||||
error: None,
|
||||
},
|
||||
];
|
||||
state.current_notebook_id = "nb-1".to_string();
|
||||
|
||||
Reference in new issue
Block a user