fix(storage): Datenverlust bei Ladefehlern, neuen Seiten und Löschen verhindern
- Beschädigte Seiten werden als page_<id>.json.corrupt-<zeit> gesichert statt leer überschrieben; bei I/O-Fehlern wird die Ersatzseite als load_failed markiert und nie gespeichert - Autosave gleicht das Manifest (page_ids, Zähler) mit dem Notizbuch ab, damit neu angelegte Seiten nach einem Neustart nicht verloren gehen - Start: nicht ladbares Notizbuch bleibt unverändert, stattdessen wird ein neues angelegt - Aktuelles Notizbuch löschen: ausstehende Schreibvorgänge vorher abarbeiten, damit der Ordner nicht wieder angelegt wird - PDF-Export lädt nicht geöffnete Seiten nach und verweigert unvollständige Exporte; Ergebnis wird per Kanal an die UI gemeldet - Strg+S speichert alle geladenen Seiten und meldet Schreibfehler; flush() liefert Fehler zurück statt immer "Gespeichert" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+85
-14
@@ -11,7 +11,7 @@ use std::sync::Arc;
|
|||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use crate::document::Page;
|
use crate::document::{Notebook, Page};
|
||||||
use crate::storage::{self, NotebookManifest, PageData};
|
use crate::storage::{self, NotebookManifest, PageData};
|
||||||
|
|
||||||
/// Aktueller Status des Autosave-Controllers für OSD und UI.
|
/// Aktueller Status des Autosave-Controllers für OSD und UI.
|
||||||
@@ -138,14 +138,53 @@ impl AutosaveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Markiert eine Seite als geändert und startet/erneuert das Debounce-Fenster.
|
/// Markiert eine Seite als geändert und startet/erneuert das Debounce-Fenster.
|
||||||
|
///
|
||||||
|
/// Nicht geladene oder fehlgeschlagene Ersatzseiten werden nie gespeichert, damit ihre
|
||||||
|
/// Originaldatei auf der Festplatte nicht überschrieben wird.
|
||||||
pub fn mark_page_dirty(&mut self, page: &Page, manifest: &NotebookManifest) {
|
pub fn mark_page_dirty(&mut self, page: &Page, manifest: &NotebookManifest) {
|
||||||
let page_data = PageData::from(page);
|
if page.is_loaded && !page.load_failed {
|
||||||
self.pending_pages.insert(page.id, page_data);
|
self.pending_pages.insert(page.id, PageData::from(page));
|
||||||
|
}
|
||||||
self.pending_manifest = Some(manifest.clone());
|
self.pending_manifest = Some(manifest.clone());
|
||||||
self.last_change_time = Some(Instant::now());
|
self.last_change_time = Some(Instant::now());
|
||||||
self.status = AutosaveStatus::Pending;
|
self.status = AutosaveStatus::Pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Markiert die aktuelle Seite als geändert und gleicht das Manifest (Seitenliste,
|
||||||
|
/// aktuelle Seite, ID-Zähler) mit dem Notizbuch ab.
|
||||||
|
pub fn mark_dirty(&mut self, notebook: &Notebook, manifest: &NotebookManifest) {
|
||||||
|
let mut synced = manifest.clone();
|
||||||
|
synced.sync_from_notebook(notebook);
|
||||||
|
self.mark_page_dirty(notebook.current_page(), &synced);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sichert sofort alle geladenen Seiten und das Manifest (Strg+S) und meldet,
|
||||||
|
/// ob wirklich alles geschrieben wurde.
|
||||||
|
pub fn save_now(
|
||||||
|
&mut self,
|
||||||
|
notebook: &Notebook,
|
||||||
|
manifest: &NotebookManifest,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
for page in ¬ebook.pages {
|
||||||
|
if page.is_loaded && !page.load_failed {
|
||||||
|
self.pending_pages.insert(page.id, PageData::from(page));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut synced = manifest.clone();
|
||||||
|
synced.sync_from_notebook(notebook);
|
||||||
|
self.pending_manifest = Some(synced);
|
||||||
|
|
||||||
|
let errors = self.flush_collect_errors();
|
||||||
|
if errors.is_empty() {
|
||||||
|
// Der komplette Zustand liegt jetzt auf der Festplatte – frühere Fehler sind überholt
|
||||||
|
self.status = AutosaveStatus::Saved;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.status = AutosaveStatus::Error;
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Prüft den Worker-Kanal und stößt fällige Schreibvorgänge nach Ablauf des Debounce-Timers an.
|
/// Prüft den Worker-Kanal und stößt fällige Schreibvorgänge nach Ablauf des Debounce-Timers an.
|
||||||
///
|
///
|
||||||
/// Sollte regelmäßig im Event-Loop aufgerufen werden.
|
/// Sollte regelmäßig im Event-Loop aufgerufen werden.
|
||||||
@@ -220,40 +259,72 @@ impl AutosaveController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Führt einen synchronen Flush aller noch ausstehenden Änderungen durch (z. B. vor Beenden).
|
/// Führt einen synchronen Flush aller noch ausstehenden Änderungen durch (z. B. vor Beenden).
|
||||||
pub fn flush(&mut self) {
|
pub fn flush(&mut self) -> Result<(), String> {
|
||||||
|
let errors = self.flush_collect_errors();
|
||||||
|
if errors.is_empty() {
|
||||||
|
if self.status != AutosaveStatus::Error {
|
||||||
|
self.status = AutosaveStatus::Saved;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
self.status = AutosaveStatus::Error;
|
||||||
|
Err(errors.join("; "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schickt alle anstehenden Änderungen los, wartet, bis der Worker sie abgearbeitet hat,
|
||||||
|
/// und liefert alle dabei aufgetretenen Schreibfehler.
|
||||||
|
fn flush_collect_errors(&mut self) -> Vec<String> {
|
||||||
// Falls noch Änderungen im Debounce-Puffer liegen, sofort losschicken
|
// Falls noch Änderungen im Debounce-Puffer liegen, sofort losschicken
|
||||||
if !self.pending_pages.is_empty() || self.pending_manifest.is_some() {
|
if !self.pending_pages.is_empty() || self.pending_manifest.is_some() {
|
||||||
self.dispatch_pending();
|
self.dispatch_pending();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut errors = Vec::new();
|
||||||
let (ack_tx, ack_rx) = mpsc::channel();
|
let (ack_tx, ack_rx) = mpsc::channel();
|
||||||
if self.cmd_tx.send(AutosaveCommand::Flush(ack_tx)).is_ok() {
|
if self.cmd_tx.send(AutosaveCommand::Flush(ack_tx)).is_err() || ack_rx.recv().is_err() {
|
||||||
let _ = ack_rx.recv();
|
errors.push("Autosave-Worker ist nicht erreichbar".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restliche Antworten leeren
|
// Restliche Antworten leeren (der Worker hat alle vor dem Flush-Befehl abgearbeitet)
|
||||||
while let Ok(response) = self.resp_rx.try_recv() {
|
while let Ok(response) = self.resp_rx.try_recv() {
|
||||||
if self.in_flight_count > 0 {
|
if self.in_flight_count > 0 {
|
||||||
self.in_flight_count -= 1;
|
self.in_flight_count -= 1;
|
||||||
}
|
}
|
||||||
if let AutosaveResponse::PageSaved { result: Err(e), .. } = response {
|
match response {
|
||||||
eprintln!("[Autosave-Flush] Fehler: {e}");
|
AutosaveResponse::PageSaved {
|
||||||
|
page_id,
|
||||||
|
result: Err(e),
|
||||||
|
} => {
|
||||||
|
eprintln!("[Autosave-Flush] Seite {page_id}: {e}");
|
||||||
|
errors.push(format!("Seite {page_id}: {e}"));
|
||||||
|
}
|
||||||
|
AutosaveResponse::ManifestSaved { result: Err(e) } => {
|
||||||
|
eprintln!("[Autosave-Flush] Manifest: {e}");
|
||||||
|
errors.push(format!("Manifest: {e}"));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
errors
|
||||||
self.status = AutosaveStatus::Saved;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wechselt das Zielverzeichnis für zukünftige Speicheroperationen (z. B. beim Dokumentenwechsel),
|
/// Wechselt das Zielverzeichnis für zukünftige Speicheroperationen (z. B. beim Dokumentenwechsel),
|
||||||
/// nachdem alle noch anstehenden Schreibvorgänge des vorherigen Notizbuchs sicher geflusht wurden.
|
/// nachdem alle noch anstehenden Schreibvorgänge des vorherigen Notizbuchs sicher geflusht wurden.
|
||||||
pub fn switch_notebook_dir(&mut self, new_notebook_dir: PathBuf) {
|
/// Liefert die Schreibfehler des vorherigen Notizbuchs zurück; der Status gilt danach
|
||||||
self.flush();
|
/// für das neue Notizbuch.
|
||||||
|
pub fn switch_notebook_dir(&mut self, new_notebook_dir: PathBuf) -> Result<(), String> {
|
||||||
|
let result = self.flush();
|
||||||
self.notebook_dir = new_notebook_dir;
|
self.notebook_dir = new_notebook_dir;
|
||||||
|
self.status = AutosaveStatus::Saved;
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Beendet den Hintergrund-Worker ordnungsgemäß nach einem Flush.
|
/// Beendet den Hintergrund-Worker ordnungsgemäß nach einem Flush.
|
||||||
pub fn shutdown(&mut self) {
|
pub fn shutdown(&mut self) {
|
||||||
self.flush();
|
if let Err(e) = self.flush() {
|
||||||
|
eprintln!("[Shutdown] Nicht alle Änderungen konnten gespeichert werden: {e}");
|
||||||
|
}
|
||||||
let _ = self.cmd_tx.send(AutosaveCommand::Shutdown);
|
let _ = self.cmd_tx.send(AutosaveCommand::Shutdown);
|
||||||
if let Some(handle) = self.worker_handle.take() {
|
if let Some(handle) = self.worker_handle.take() {
|
||||||
let _ = handle.join();
|
let _ = handle.join();
|
||||||
|
|||||||
@@ -314,6 +314,9 @@ pub struct Page {
|
|||||||
pub stroke_ranges: Vec<Range<u32>>,
|
pub stroke_ranges: Vec<Range<u32>>,
|
||||||
pub is_dirty: bool,
|
pub is_dirty: bool,
|
||||||
pub is_loaded: 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 history: HistoryStack,
|
pub history: HistoryStack,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,6 +330,7 @@ impl Page {
|
|||||||
stroke_ranges: Vec::new(),
|
stroke_ranges: Vec::new(),
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
is_loaded: true,
|
is_loaded: true,
|
||||||
|
load_failed: false,
|
||||||
history: HistoryStack::new(),
|
history: HistoryStack::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,6 +345,7 @@ impl Page {
|
|||||||
stroke_ranges: Vec::new(),
|
stroke_ranges: Vec::new(),
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
is_loaded: false,
|
is_loaded: false,
|
||||||
|
load_failed: false,
|
||||||
history: HistoryStack::new(),
|
history: HistoryStack::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,18 @@ pub fn generate_pdf_document(
|
|||||||
if notebook.pages.is_empty() {
|
if notebook.pages.is_empty() {
|
||||||
return Err("Notizbuch enthält keine Seiten".into());
|
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)
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Seite {} ist nicht geladen – Export abgebrochen (vorher storage::load_all_pages aufrufen)",
|
||||||
|
page.id
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Erstes Dokument mit Seite 1 erstellen
|
// 1. Erstes Dokument mit Seite 1 erstellen
|
||||||
let title = "ink-notes Notebook";
|
let title = "ink-notes Notebook";
|
||||||
|
|||||||
+154
-142
@@ -1,6 +1,8 @@
|
|||||||
//! ink-notes: Latenzoptimierte Linux-Inking-App mit wgpu, winit, egui-Toolbar,
|
//! ink-notes: Latenzoptimierte Linux-Inking-App mit wgpu, winit, egui-Toolbar,
|
||||||
//! Seitenverwaltung, Undo/Redo, Vektor-PDF-Export und asynchroner Notizbuch-Persistenz (Autosave).
|
//! Seitenverwaltung, Undo/Redo, Vektor-PDF-Export und asynchroner Notizbuch-Persistenz (Autosave).
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc::{self, Sender};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use winit::{
|
use winit::{
|
||||||
@@ -52,6 +54,44 @@ fn update_window_title(
|
|||||||
window.set_title(&title);
|
window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exportiert das Notizbuch im Hintergrund als PDF. Noch nicht geladene Seiten werden vorher
|
||||||
|
/// von der Festplatte nachgeladen; das Ergebnis geht über `result_tx` an den Event-Loop zurück.
|
||||||
|
fn start_pdf_export(
|
||||||
|
notebook: &Notebook,
|
||||||
|
storage_dir: &Path,
|
||||||
|
title: &str,
|
||||||
|
result_tx: Sender<Result<PathBuf, String>>,
|
||||||
|
window: Arc<Window>,
|
||||||
|
) {
|
||||||
|
let mut notebook_clone = notebook.clone();
|
||||||
|
let storage_dir = storage_dir.to_path_buf();
|
||||||
|
let export_path = export::resolve_pdf_export_path(title);
|
||||||
|
println!(
|
||||||
|
"[Export] Starte PDF-Vektorexport nach '{}' (im Hintergrundthread)...",
|
||||||
|
export_path.display()
|
||||||
|
);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = storage::load_all_pages(&storage_dir, &mut notebook_clone)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.and_then(|()| {
|
||||||
|
export_notebook_to_pdf(¬ebook_clone, &export_path, ExportStrategy::FilledPolygon)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
});
|
||||||
|
match &result {
|
||||||
|
Ok(()) => println!(
|
||||||
|
"[Export] PDF erfolgreich exportiert: '{}' ({:.1?} für {} Seiten)",
|
||||||
|
export_path.display(),
|
||||||
|
start.elapsed(),
|
||||||
|
notebook_clone.pages.len()
|
||||||
|
),
|
||||||
|
Err(err) => eprintln!("[Export-Fehler] Konnte PDF nicht exportieren: {err}"),
|
||||||
|
}
|
||||||
|
let _ = result_tx.send(result.map(|()| export_path));
|
||||||
|
window.request_redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Finalisiert einen Strich (mit Spline-Tessellierung, Ecken-Erkennung & RDP oder Auto-Snap)
|
/// Finalisiert einen Strich (mit Spline-Tessellierung, Ecken-Erkennung & RDP oder Auto-Snap)
|
||||||
/// und fügt ihn in die aktuelle Seite des Notizbuchs ein.
|
/// und fügt ihn in die aktuelle Seite des Notizbuchs ein.
|
||||||
fn commit_stroke_to_notebook(
|
fn commit_stroke_to_notebook(
|
||||||
@@ -83,7 +123,7 @@ fn commit_stroke_to_notebook(
|
|||||||
page.add_stroke(stroke);
|
page.add_stroke(stroke);
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||||
autosave.mark_page_dirty(notebook.current_page(), manifest);
|
autosave.mark_dirty(notebook, manifest);
|
||||||
update_window_title(
|
update_window_title(
|
||||||
window,
|
window,
|
||||||
notebook,
|
notebook,
|
||||||
@@ -128,16 +168,24 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut storage_dir = storage_base_dir.join(&initial_id);
|
let mut storage_dir = storage_base_dir.join(&initial_id);
|
||||||
|
let mut startup_warning: Option<String> = None;
|
||||||
let (mut notebook, mut manifest) = if storage_dir.join("manifest.json").exists() {
|
let (mut notebook, mut manifest) = if storage_dir.join("manifest.json").exists() {
|
||||||
println!("[Storage] Lade Notizbuch aus: {}", storage_dir.display());
|
println!("[Storage] Lade Notizbuch aus: {}", storage_dir.display());
|
||||||
match storage::load_notebook(&storage_dir) {
|
match storage::load_notebook(&storage_dir) {
|
||||||
Ok((nb, m)) => (nb, m),
|
Ok((nb, m)) => (nb, m),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("[Storage-Fehler] Konnte Notizbuch nicht laden ({err}). Erstelle neues Notizbuch.");
|
// Den Ordner des nicht ladbaren Notizbuchs NICHT wiederverwenden, sonst würde
|
||||||
(
|
// der Autosave dessen Manifest überschreiben und alle Seiten verwaist zurücklassen.
|
||||||
Notebook::new(),
|
eprintln!(
|
||||||
NotebookManifest::new(&initial_id, &initial_title),
|
"[Storage-Fehler] Konnte Notizbuch nicht laden ({err}). '{}' bleibt unverändert, erstelle neues Notizbuch.",
|
||||||
)
|
storage_dir.display()
|
||||||
|
);
|
||||||
|
startup_warning = Some(format!(
|
||||||
|
"Notizbuch '{initial_title}' konnte nicht geladen werden – neues Notizbuch angelegt"
|
||||||
|
));
|
||||||
|
let (nb, m, dir) = storage::create_new_notebook(&storage_base_dir, &initial_title)?;
|
||||||
|
storage_dir = dir;
|
||||||
|
(nb, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -159,6 +207,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
egui_ctx.set_visuals(egui::Visuals::dark());
|
egui_ctx.set_visuals(egui::Visuals::dark());
|
||||||
|
|
||||||
// 4. In-App Gitea Updater initialisieren und im Hintergrund nach Releases fragen
|
// 4. In-App Gitea Updater initialisieren und im Hintergrund nach Releases fragen
|
||||||
|
// Ergebnisse der PDF-Exporte aus dem Hintergrundthread
|
||||||
|
let (export_tx, export_rx) = mpsc::channel::<Result<PathBuf, String>>();
|
||||||
|
|
||||||
let mut updater = UpdaterController::new();
|
let mut updater = UpdaterController::new();
|
||||||
updater.check_for_updates();
|
updater.check_for_updates();
|
||||||
|
|
||||||
@@ -174,6 +225,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
ui_state.current_notebook_id = manifest.id.clone();
|
ui_state.current_notebook_id = manifest.id.clone();
|
||||||
ui_state.document_title = manifest.title.clone();
|
ui_state.document_title = manifest.title.clone();
|
||||||
ui_state.available_notebooks = available_notebooks;
|
ui_state.available_notebooks = available_notebooks;
|
||||||
|
if let Some(warning) = startup_warning {
|
||||||
|
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||||
|
ui_state.set_export_message(warning);
|
||||||
|
}
|
||||||
let mut selection_state = SelectionState::new();
|
let mut selection_state = SelectionState::new();
|
||||||
|
|
||||||
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
// Initiale Seite und Werkzeugeinstellungen mit Canvas synchronisieren
|
||||||
@@ -265,6 +320,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, new_status);
|
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, new_status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while let Ok(result) = export_rx.try_recv() {
|
||||||
|
match result {
|
||||||
|
Ok(path) => ui_state.set_export_message(format!("PDF gespeichert: {}", path.display())),
|
||||||
|
Err(err) => ui_state.set_export_message(format!("PDF-Export fehlgeschlagen: {err}")),
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
let prev_upd = updater.status().clone();
|
let prev_upd = updater.status().clone();
|
||||||
updater.update();
|
updater.update();
|
||||||
if *updater.status() != prev_upd {
|
if *updater.status() != prev_upd {
|
||||||
@@ -347,10 +410,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
notebook.current_page(),
|
|
||||||
&manifest,
|
|
||||||
);
|
|
||||||
update_window_title(
|
update_window_title(
|
||||||
&window,
|
&window,
|
||||||
¬ebook,
|
¬ebook,
|
||||||
@@ -427,10 +487,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
);
|
);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
notebook.current_page(),
|
|
||||||
&manifest,
|
|
||||||
);
|
|
||||||
update_window_title(
|
update_window_title(
|
||||||
&window,
|
&window,
|
||||||
¬ebook,
|
¬ebook,
|
||||||
@@ -680,7 +737,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if notebook.current_page_mut().undo() {
|
if notebook.current_page_mut().undo() {
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -694,7 +751,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if notebook.current_page_mut().redo() {
|
if notebook.current_page_mut().redo() {
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -724,7 +781,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !new_ids.is_empty() {
|
if !new_ids.is_empty() {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -736,7 +793,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -745,8 +802,10 @@ 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.save_now(¬ebook, &manifest) {
|
||||||
ui_state.set_export_message("Gespeichert");
|
Ok(()) => ui_state.set_export_message("Gespeichert"),
|
||||||
|
Err(err) => ui_state.set_export_message(format!("Speichern fehlgeschlagen: {err}")),
|
||||||
|
}
|
||||||
update_window_title(
|
update_window_title(
|
||||||
&window,
|
&window,
|
||||||
¬ebook,
|
¬ebook,
|
||||||
@@ -759,38 +818,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// PDF-Export: Strg + E
|
// PDF-Export: Strg + E
|
||||||
Key::Character(c) if c.eq_ignore_ascii_case("e") && ctrl => {
|
Key::Character(c) if c.eq_ignore_ascii_case("e") && ctrl => {
|
||||||
let notebook_clone = notebook.clone();
|
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
||||||
let export_path = export::resolve_pdf_export_path(&manifest.title);
|
ui_state.set_export_message("PDF-Export läuft…");
|
||||||
let export_path_str = export_path.display().to_string();
|
|
||||||
let window_ref = window.clone();
|
|
||||||
println!(
|
|
||||||
"[Export] Starte PDF-Vektorexport nach '{}' (im Hintergrundthread)...",
|
|
||||||
export_path_str
|
|
||||||
);
|
|
||||||
ui_state.set_export_message(format!("PDF gespeichert: {}", export_path.display()));
|
|
||||||
let export_path_thread = export_path.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
match export_notebook_to_pdf(
|
|
||||||
¬ebook_clone,
|
|
||||||
&export_path_thread,
|
|
||||||
ExportStrategy::FilledPolygon,
|
|
||||||
) {
|
|
||||||
Ok(()) => {
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
println!(
|
|
||||||
"[Export] PDF erfolgreich exportiert: '{}' ({:.1?} für {} Seiten)",
|
|
||||||
export_path_thread.display(),
|
|
||||||
elapsed,
|
|
||||||
notebook_clone.pages.len()
|
|
||||||
);
|
|
||||||
window_ref.request_redraw();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("[Export-Fehler] Konnte PDF nicht exportieren: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -826,7 +855,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let color = [0.12, 0.12, 0.14, 1.0];
|
let color = [0.12, 0.12, 0.14, 1.0];
|
||||||
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message("Auswahl auf Schwarz umgefärbt".to_string());
|
ui_state.set_export_message("Auswahl auf Schwarz umgefärbt".to_string());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -846,7 +875,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let color = [0.15, 0.35, 0.85, 1.0];
|
let color = [0.15, 0.35, 0.85, 1.0];
|
||||||
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
if selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message("Auswahl auf Blau umgefärbt".to_string());
|
ui_state.set_export_message("Auswahl auf Blau umgefärbt".to_string());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -865,7 +894,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !selection_state.is_empty() {
|
if !selection_state.is_empty() {
|
||||||
if selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
if selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -889,7 +918,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
PageBackground::Blank => PageBackground::Grid5mm,
|
PageBackground::Blank => PageBackground::Grid5mm,
|
||||||
};
|
};
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -903,7 +932,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +944,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +957,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +970,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +983,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1035,7 +1064,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1133,7 +1162,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let new_stroke = geometry::shape_to_stroke(shape, old_id, stroke_color, stroke_width);
|
let new_stroke = geometry::shape_to_stroke(shape, old_id, stroke_color, stroke_width);
|
||||||
notebook.current_page_mut().replace_stroke(old_id, new_stroke);
|
notebook.current_page_mut().replace_stroke(old_id, new_stroke);
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message(format!("{shape_name} ausgerichtet! (Undo mit Strg+Z)"));
|
ui_state.set_export_message(format!("{shape_name} ausgerichtet! (Undo mit Strg+Z)"));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
return;
|
return;
|
||||||
@@ -1190,7 +1219,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let final_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
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 {
|
if !initial_strokes.is_empty() && !final_strokes.is_empty() && initial_strokes != final_strokes {
|
||||||
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
}
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1303,7 +1332,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1427,7 +1456,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1568,7 +1597,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
let removed = page.erase_at(page_pt, eraser_radius, stroke_max_radius);
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1659,7 +1688,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let final_strokes = notebook.current_page().get_strokes(&selection_state.selected_ids);
|
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 {
|
if !initial_strokes.is_empty() && !final_strokes.is_empty() && initial_strokes != final_strokes {
|
||||||
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
notebook.current_page_mut().commit_modified_strokes(initial_strokes, final_strokes);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
}
|
}
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1743,7 +1772,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !selection_state.is_empty()
|
if !selection_state.is_empty()
|
||||||
&& selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
&& selection_state.recolor_selected(notebook.current_page_mut(), color) {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message("Auswahl umgefärbt".to_string());
|
ui_state.set_export_message("Auswahl umgefärbt".to_string());
|
||||||
}
|
}
|
||||||
match ui_state.active_tool {
|
match ui_state.active_tool {
|
||||||
@@ -1778,7 +1807,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
let removed = selection_state.delete_selected(notebook.current_page_mut());
|
||||||
if !removed.is_empty() {
|
if !removed.is_empty() {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
ui_state.set_export_message(format!("{} Objekt(e) gelöscht", removed.len()));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1805,7 +1834,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !new_ids.is_empty() {
|
if !new_ids.is_empty() {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
manifest.next_stroke_id = notebook.next_stroke_id;
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
ui_state.set_export_message(format!("{} Objekt(e) eingefügt", new_ids.len()));
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1814,7 +1843,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if !selection_state.is_empty()
|
if !selection_state.is_empty()
|
||||||
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
ui_state.set_export_message("Auswahl um 90° gedreht".to_string());
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1842,7 +1871,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if notebook.current_page_mut().undo() {
|
if notebook.current_page_mut().undo() {
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1851,7 +1880,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if notebook.current_page_mut().redo() {
|
if notebook.current_page_mut().redo() {
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1863,7 +1892,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +1904,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1886,7 +1915,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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;
|
||||||
canvas.sync_page(notebook.current_page_mut());
|
canvas.sync_page(notebook.current_page_mut());
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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,7 +1926,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_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
@@ -1906,43 +1935,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let page = notebook.current_page_mut();
|
let page = notebook.current_page_mut();
|
||||||
page.background = bg;
|
page.background = bg;
|
||||||
canvas.sync_page(page);
|
canvas.sync_page(page);
|
||||||
autosave.mark_page_dirty(notebook.current_page(), &manifest);
|
autosave.mark_dirty(¬ebook, &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();
|
||||||
}
|
}
|
||||||
UiAction::ExportPdf => {
|
UiAction::ExportPdf => {
|
||||||
let notebook_clone = notebook.clone();
|
start_pdf_export(¬ebook, &storage_dir, &manifest.title, export_tx.clone(), window.clone());
|
||||||
let export_path = export::resolve_pdf_export_path(&manifest.title);
|
ui_state.set_export_message("PDF-Export läuft…");
|
||||||
let export_path_str = export_path.display().to_string();
|
|
||||||
let window_ref = window.clone();
|
|
||||||
println!(
|
|
||||||
"[Export] Starte PDF-Vektorexport nach '{}' (im Hintergrundthread)...",
|
|
||||||
export_path_str
|
|
||||||
);
|
|
||||||
ui_state.set_export_message(format!("PDF gespeichert: {}", export_path.display()));
|
|
||||||
let export_path_thread = export_path.clone();
|
|
||||||
std::thread::spawn(move || {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
match export_notebook_to_pdf(
|
|
||||||
¬ebook_clone,
|
|
||||||
&export_path_thread,
|
|
||||||
ExportStrategy::FilledPolygon,
|
|
||||||
) {
|
|
||||||
Ok(()) => {
|
|
||||||
let elapsed = start.elapsed();
|
|
||||||
println!(
|
|
||||||
"[Export] PDF erfolgreich exportiert: '{}' ({:.1?} für {} Seiten)",
|
|
||||||
export_path_thread.display(),
|
|
||||||
elapsed,
|
|
||||||
notebook_clone.pages.len()
|
|
||||||
);
|
|
||||||
window_ref.request_redraw();
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("[Export-Fehler] Konnte PDF nicht exportieren: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1958,7 +1957,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let new_notebook_dir = storage_base_dir.join(&id);
|
let new_notebook_dir = storage_base_dir.join(&id);
|
||||||
match storage::load_notebook(&new_notebook_dir) {
|
match storage::load_notebook(&new_notebook_dir) {
|
||||||
Ok((loaded_nb, loaded_manifest)) => {
|
Ok((loaded_nb, loaded_manifest)) => {
|
||||||
autosave.switch_notebook_dir(new_notebook_dir.clone());
|
if let Err(err) = autosave.switch_notebook_dir(new_notebook_dir.clone()) {
|
||||||
|
eprintln!("[Autosave-Fehler] Vorheriges Notizbuch nicht vollständig gespeichert: {err}");
|
||||||
|
}
|
||||||
notebook = loaded_nb;
|
notebook = loaded_nb;
|
||||||
manifest = loaded_manifest;
|
manifest = loaded_manifest;
|
||||||
storage_dir = new_notebook_dir;
|
storage_dir = new_notebook_dir;
|
||||||
@@ -1980,10 +1981,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
UiAction::CreateNotebook(title) => {
|
UiAction::CreateNotebook(title) => {
|
||||||
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());
|
if let Err(err) = autosave.switch_notebook_dir(new_dir.clone()) {
|
||||||
|
eprintln!("[Autosave-Fehler] Vorheriges Notizbuch nicht vollständig gespeichert: {err}");
|
||||||
|
}
|
||||||
notebook = new_nb;
|
notebook = new_nb;
|
||||||
manifest = new_manifest;
|
manifest = new_manifest;
|
||||||
storage_dir = new_dir;
|
storage_dir = new_dir;
|
||||||
@@ -2034,41 +2036,51 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
UiAction::DeleteNotebook(id) => {
|
UiAction::DeleteNotebook(id) => {
|
||||||
let target_dir = storage_base_dir.join(&id);
|
let target_dir = storage_base_dir.join(&id);
|
||||||
let is_current = id == manifest.id;
|
let is_current = id == manifest.id;
|
||||||
|
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();
|
||||||
|
}
|
||||||
match storage::delete_notebook(&target_dir) {
|
match storage::delete_notebook(&target_dir) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
ui_state.available_notebooks = storage::list_notebooks(&storage_base_dir);
|
||||||
if is_current {
|
if is_current {
|
||||||
if let Some(first) = ui_state.available_notebooks.first().cloned() {
|
// Erstes ladbares Notizbuch öffnen, sonst ein neues anlegen
|
||||||
let new_notebook_dir = storage_base_dir.join(&first.id);
|
let replacement = ui_state
|
||||||
match storage::load_notebook(&new_notebook_dir) {
|
.available_notebooks
|
||||||
Ok((loaded_nb, loaded_manifest)) => {
|
.iter()
|
||||||
autosave.switch_notebook_dir(new_notebook_dir.clone());
|
.find_map(|summary| {
|
||||||
notebook = loaded_nb;
|
let dir = storage_base_dir.join(&summary.id);
|
||||||
manifest = loaded_manifest;
|
match storage::load_notebook(&dir) {
|
||||||
storage_dir = new_notebook_dir;
|
Ok((nb, m)) => Some((nb, m, dir)),
|
||||||
selection_state.clear();
|
Err(err) => {
|
||||||
ui_state.current_notebook_id = manifest.id.clone();
|
eprintln!("[Dokument-Fehler] Konnte Dokument '{}' nicht laden: {err}", summary.id);
|
||||||
ui_state.document_title = manifest.title.clone();
|
None
|
||||||
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 Dokument '{}' nicht laden: {err}", first.id);
|
.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}");
|
||||||
}
|
}
|
||||||
|
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") {
|
|
||||||
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 {
|
} else {
|
||||||
ui_state.set_export_message("Dokument gelöscht".to_string());
|
ui_state.set_export_message("Dokument gelöscht".to_string());
|
||||||
|
|||||||
+271
-16
@@ -149,6 +149,19 @@ impl NotebookManifest {
|
|||||||
next_page_id: 2,
|
next_page_id: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Übernimmt Seitenliste, aktuelle Seite und ID-Zähler aus dem Notizbuch,
|
||||||
|
/// damit neu angelegte Seiten im Manifest referenziert werden.
|
||||||
|
pub fn sync_from_notebook(&mut self, notebook: &Notebook) {
|
||||||
|
self.page_ids = notebook.pages.iter().map(|p| p.id).collect();
|
||||||
|
self.current_page_index = notebook.current_page_index;
|
||||||
|
self.next_stroke_id = notebook.next_stroke_id;
|
||||||
|
self.next_page_id = notebook.next_page_id;
|
||||||
|
self.modified_at = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mögliche Fehler bei Speicher- und Ladeoperationen.
|
/// Mögliche Fehler bei Speicher- und Ladeoperationen.
|
||||||
@@ -347,18 +360,11 @@ pub fn save_notebook(
|
|||||||
) -> Result<(), StorageError> {
|
) -> Result<(), StorageError> {
|
||||||
fs::create_dir_all(notebook_dir)?;
|
fs::create_dir_all(notebook_dir)?;
|
||||||
|
|
||||||
manifest.page_ids = notebook.pages.iter().map(|p| p.id).collect();
|
manifest.sync_from_notebook(notebook);
|
||||||
manifest.current_page_index = notebook.current_page_index;
|
|
||||||
manifest.next_stroke_id = notebook.next_stroke_id;
|
|
||||||
manifest.next_page_id = notebook.next_page_id;
|
|
||||||
manifest.modified_at = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs();
|
|
||||||
|
|
||||||
// Alle bereits geladenen Seiten speichern
|
// Alle bereits geladenen Seiten speichern (fehlgeschlagene Ersatzseiten niemals)
|
||||||
for page in ¬ebook.pages {
|
for page in ¬ebook.pages {
|
||||||
if page.is_loaded {
|
if page.is_loaded && !page.load_failed {
|
||||||
let page_data = PageData::from(page);
|
let page_data = PageData::from(page);
|
||||||
save_page(notebook_dir, &page_data)?;
|
save_page(notebook_dir, &page_data)?;
|
||||||
}
|
}
|
||||||
@@ -383,8 +389,8 @@ pub fn load_notebook(notebook_dir: &Path) -> Result<(Notebook, NotebookManifest)
|
|||||||
let first_page = match load_page(notebook_dir, first_id) {
|
let first_page = match load_page(notebook_dir, first_id) {
|
||||||
Ok(data) => data.to_page(),
|
Ok(data) => data.to_page(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[Storage] Konnte erste Seite {first_id} nicht laden: {e}. Erstelle leere Ersatzseite.");
|
eprintln!("[Storage] Konnte erste Seite {first_id} nicht laden: {e}.");
|
||||||
Page::new(first_id, PageBackground::Grid5mm)
|
replacement_page_after_load_error(notebook_dir, first_id, &e)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
pages.push(first_page);
|
pages.push(first_page);
|
||||||
@@ -426,14 +432,77 @@ pub fn ensure_page_loaded(
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[Storage] Fehler beim Lazy Loading von Seite {page_id}: {e}");
|
eprintln!("[Storage] Fehler beim Lazy Loading von Seite {page_id}: {e}");
|
||||||
// Ersatzseite als geladen markieren, damit die App nicht hängt
|
// Ersatzseite einsetzen, damit die App nicht hängt
|
||||||
notebook.pages[page_idx].is_loaded = true;
|
let background = notebook.pages[page_idx].background;
|
||||||
notebook.pages[page_idx].rebuild_cache_if_dirty();
|
let mut page = replacement_page_after_load_error(notebook_dir, page_id, &e);
|
||||||
|
page.background = background;
|
||||||
|
page.rebuild_cache_if_dirty();
|
||||||
|
notebook.pages[page_idx] = page;
|
||||||
Err(e)
|
Err(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Erzeugt eine leere Ersatzseite für eine Seite, deren Datei nicht geladen werden konnte.
|
||||||
|
///
|
||||||
|
/// - Fehlende Datei: normale leere Seite (z. B. neu angelegt, aber noch nie gespeichert).
|
||||||
|
/// - Beschädigtes JSON: Die Datei wird als `page_<id>.json.corrupt-<zeit>` beiseite gelegt,
|
||||||
|
/// danach ist die leere Seite normal beschreibbar.
|
||||||
|
/// - Sonstige Fehler (I/O) oder fehlgeschlagenes Beiseitelegen: Die Ersatzseite wird als
|
||||||
|
/// `load_failed` markiert und nie gespeichert, damit die Originaldatei erhalten bleibt.
|
||||||
|
fn replacement_page_after_load_error(
|
||||||
|
notebook_dir: &Path,
|
||||||
|
page_id: u64,
|
||||||
|
err: &StorageError,
|
||||||
|
) -> Page {
|
||||||
|
let mut page = Page::new(page_id, PageBackground::Grid5mm);
|
||||||
|
match err {
|
||||||
|
StorageError::PageNotFound(_) => {}
|
||||||
|
StorageError::CorruptedPage(..) => {
|
||||||
|
let path = notebook_dir.join(format!("page_{page_id}.json"));
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
let backup = notebook_dir.join(format!("page_{page_id}.json.corrupt-{now}"));
|
||||||
|
match fs::rename(&path, &backup) {
|
||||||
|
Ok(()) => eprintln!(
|
||||||
|
"[Storage] Beschädigte Seite gesichert als '{}'.",
|
||||||
|
backup.display()
|
||||||
|
),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[Storage] Konnte beschädigte Seite nicht sichern ({e}). Seite wird nicht gespeichert.");
|
||||||
|
page.load_failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => page.load_failed = true,
|
||||||
|
}
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lädt alle noch nicht geladenen Seiten ohne Ersatzseiten (z. B. für den PDF-Export).
|
||||||
|
/// Schlägt fehl, sobald eine Seite nicht vollständig verfügbar ist.
|
||||||
|
pub fn load_all_pages(notebook_dir: &Path, notebook: &mut Notebook) -> Result<(), StorageError> {
|
||||||
|
for page in &mut notebook.pages {
|
||||||
|
if page.load_failed {
|
||||||
|
return Err(StorageError::CorruptedPage(
|
||||||
|
page.id,
|
||||||
|
"Seite konnte nicht geladen werden".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !page.is_loaded {
|
||||||
|
*page = match load_page(notebook_dir, page.id) {
|
||||||
|
Ok(data) => data.to_page(),
|
||||||
|
// Wie beim Lazy Loading: nie gespeicherte Seite = leere Seite
|
||||||
|
Err(StorageError::PageNotFound(_)) => Page::new(page.id, page.background),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Gibt das Standard-Verzeichnis für Notizbücher zurück (gemäß XDG-Spezifikation).
|
/// Gibt das Standard-Verzeichnis für Notizbücher zurück (gemäß XDG-Spezifikation).
|
||||||
pub fn default_storage_dir() -> PathBuf {
|
pub fn default_storage_dir() -> PathBuf {
|
||||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||||
@@ -712,7 +781,7 @@ mod tests {
|
|||||||
controller.update();
|
controller.update();
|
||||||
|
|
||||||
// Flush abwarten
|
// Flush abwarten
|
||||||
controller.flush();
|
controller.flush().expect("Flush muss gelingen");
|
||||||
|
|
||||||
// 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!(
|
||||||
@@ -773,4 +842,190 @@ mod tests {
|
|||||||
assert_eq!(final_summaries.len(), 1);
|
assert_eq!(final_summaries.len(), 1);
|
||||||
assert_eq!(final_summaries[0].id, m1.id);
|
assert_eq!(final_summaries[0].id, m1.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hilfsfunktion: Seite mit einem einzelnen Strich.
|
||||||
|
fn page_with_stroke(page_id: u64, stroke_id: u64) -> Page {
|
||||||
|
let mut page = Page::new(page_id, PageBackground::Lined);
|
||||||
|
let samples = vec![
|
||||||
|
StylusSample {
|
||||||
|
position: glam::Vec2::new(100.0, 100.0),
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp: 0.0,
|
||||||
|
},
|
||||||
|
StylusSample {
|
||||||
|
position: glam::Vec2::new(200.0, 150.0),
|
||||||
|
pressure: 0.5,
|
||||||
|
timestamp: 0.05,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let vertices = reconstruct_stroke_vertices(&samples, [0.0, 0.0, 0.0, 1.0]);
|
||||||
|
page.add_stroke(Stroke::new(
|
||||||
|
stroke_id,
|
||||||
|
samples,
|
||||||
|
vertices,
|
||||||
|
[0.0, 0.0, 0.0, 1.0],
|
||||||
|
));
|
||||||
|
page
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_corrupted_page_is_quarantined_not_overwritten() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let dir = temp_dir.path();
|
||||||
|
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
notebook.add_page(PageBackground::Grid5mm);
|
||||||
|
let mut manifest = NotebookManifest::new("nb", "Test");
|
||||||
|
save_notebook(dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
|
||||||
|
let corrupted = br#"{"id": 2, "background": "Lined", "strokes": [{"id": 1"#;
|
||||||
|
fs::write(dir.join("page_2.json"), corrupted).unwrap();
|
||||||
|
|
||||||
|
let (mut loaded, loaded_manifest) = load_notebook(dir).unwrap();
|
||||||
|
assert!(ensure_page_loaded(dir, &mut loaded, 1).is_err());
|
||||||
|
|
||||||
|
// Original wurde beiseite gelegt und bleibt Byte für Byte erhalten
|
||||||
|
let backups: Vec<_> = fs::read_dir(dir)
|
||||||
|
.unwrap()
|
||||||
|
.flatten()
|
||||||
|
.filter(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with("page_2.json.corrupt-")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(backups.len(), 1);
|
||||||
|
assert_eq!(fs::read(backups[0].path()).unwrap(), corrupted);
|
||||||
|
|
||||||
|
// Ersatzseite ist normal beschreibbar
|
||||||
|
assert!(!loaded.pages[1].load_failed);
|
||||||
|
let mut autosave = AutosaveController::new(dir.to_path_buf(), Duration::from_secs(60));
|
||||||
|
loaded.current_page_index = 1;
|
||||||
|
autosave.mark_dirty(&loaded, &loaded_manifest);
|
||||||
|
autosave.flush().unwrap();
|
||||||
|
assert!(load_page(dir, 2).is_ok());
|
||||||
|
autosave.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unreadable_page_is_never_overwritten() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let dir = temp_dir.path();
|
||||||
|
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
notebook.add_page(PageBackground::Grid5mm);
|
||||||
|
let mut manifest = NotebookManifest::new("nb", "Test");
|
||||||
|
save_notebook(dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
|
||||||
|
// Unlesbare Seite simulieren (I/O-Fehler): Inhalt liegt in einer Datei, die wir
|
||||||
|
// durch ein gleichnamiges Verzeichnis ersetzen
|
||||||
|
fs::remove_file(dir.join("page_2.json")).unwrap();
|
||||||
|
fs::create_dir(dir.join("page_2.json")).unwrap();
|
||||||
|
fs::write(dir.join("page_2.json").join("marker"), b"original").unwrap();
|
||||||
|
|
||||||
|
let (mut loaded, loaded_manifest) = load_notebook(dir).unwrap();
|
||||||
|
assert!(ensure_page_loaded(dir, &mut loaded, 1).is_err());
|
||||||
|
assert!(loaded.pages[1].load_failed);
|
||||||
|
|
||||||
|
// Navigation + Autosave + Strg+S dürfen die Seite nicht anfassen
|
||||||
|
loaded.current_page_index = 1;
|
||||||
|
let mut autosave = AutosaveController::new(dir.to_path_buf(), Duration::from_secs(60));
|
||||||
|
autosave.mark_dirty(&loaded, &loaded_manifest);
|
||||||
|
autosave.save_now(&loaded, &loaded_manifest).unwrap();
|
||||||
|
save_notebook(dir, &loaded, &mut loaded_manifest.clone()).unwrap();
|
||||||
|
autosave.shutdown();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(dir.join("page_2.json").join("marker")).unwrap(),
|
||||||
|
b"original"
|
||||||
|
);
|
||||||
|
// Die Seite bleibt im Manifest referenziert
|
||||||
|
assert_eq!(load_manifest(dir).unwrap().page_ids, vec![1, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autosave_records_new_pages_in_manifest() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let dir = temp_dir.path();
|
||||||
|
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
let mut manifest = NotebookManifest::new("nb", "Test");
|
||||||
|
save_notebook(dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
|
||||||
|
// Wie in main.rs: neue Seite per Navigation, Strich darauf, Autosave
|
||||||
|
notebook.next_page();
|
||||||
|
notebook.pages[1] = page_with_stroke(notebook.pages[1].id, 1);
|
||||||
|
let mut autosave = AutosaveController::new(dir.to_path_buf(), Duration::from_secs(60));
|
||||||
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
|
autosave.shutdown();
|
||||||
|
|
||||||
|
let (mut reloaded, reloaded_manifest) = load_notebook(dir).unwrap();
|
||||||
|
assert_eq!(reloaded_manifest.page_ids, vec![1, 2]);
|
||||||
|
assert_eq!(reloaded.pages.len(), 2);
|
||||||
|
ensure_page_loaded(dir, &mut reloaded, 1).unwrap();
|
||||||
|
assert_eq!(reloaded.pages[1].strokes.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flush_and_save_now_report_write_errors() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
// Zielpfad ist eine Datei -> Schreiben ins "Notizbuch-Verzeichnis" muss fehlschlagen
|
||||||
|
let blocked = temp_dir.path().join("blocked");
|
||||||
|
fs::write(&blocked, b"").unwrap();
|
||||||
|
|
||||||
|
let notebook = Notebook::new();
|
||||||
|
let manifest = NotebookManifest::new("nb", "Test");
|
||||||
|
let mut autosave = AutosaveController::new(blocked, Duration::from_secs(60));
|
||||||
|
|
||||||
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
|
assert!(autosave.flush().is_err());
|
||||||
|
assert_eq!(autosave.status(), crate::autosave::AutosaveStatus::Error);
|
||||||
|
|
||||||
|
assert!(autosave.save_now(¬ebook, &manifest).is_err());
|
||||||
|
assert_eq!(autosave.status(), crate::autosave::AutosaveStatus::Error);
|
||||||
|
autosave.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deleted_notebook_is_not_recreated_by_pending_autosave() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let base = temp_dir.path();
|
||||||
|
let (notebook, manifest, dir) = create_new_notebook(base, "Weg damit").unwrap();
|
||||||
|
let (_, _, other_dir) = create_new_notebook(base, "Bleibt").unwrap();
|
||||||
|
|
||||||
|
let mut autosave = AutosaveController::new(dir.clone(), Duration::from_secs(60));
|
||||||
|
autosave.mark_dirty(¬ebook, &manifest);
|
||||||
|
|
||||||
|
// Ablauf wie in main.rs beim Löschen des aktuellen Notizbuchs
|
||||||
|
autosave.flush().unwrap();
|
||||||
|
delete_notebook(&dir).unwrap();
|
||||||
|
autosave.switch_notebook_dir(other_dir).unwrap();
|
||||||
|
autosave.shutdown();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!dir.exists(),
|
||||||
|
"Gelöschtes Notizbuch darf nicht wieder auftauchen"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_all_pages_for_export() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let dir = temp_dir.path();
|
||||||
|
|
||||||
|
let mut notebook = Notebook::new();
|
||||||
|
notebook.add_page(PageBackground::Lined);
|
||||||
|
notebook.pages[1] = page_with_stroke(2, 1);
|
||||||
|
let mut manifest = NotebookManifest::new("nb", "Test");
|
||||||
|
save_notebook(dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
|
||||||
|
let (mut loaded, _) = load_notebook(dir).unwrap();
|
||||||
|
assert!(!loaded.pages[1].is_loaded);
|
||||||
|
assert!(crate::export::generate_pdf_document(&loaded, Default::default()).is_err());
|
||||||
|
|
||||||
|
load_all_pages(dir, &mut loaded).unwrap();
|
||||||
|
assert_eq!(loaded.pages[1].strokes.len(), 1);
|
||||||
|
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
||||||
|
assert!(crate::export::generate_pdf_document(&loaded, Default::default()).is_ok());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user