Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f76fad92be | ||
|
|
2c60a03460 | ||
|
|
b6580f197d | ||
|
|
22e35f2e77 | ||
|
|
a77e6cde90 | ||
|
|
40eb9a666f | ||
|
|
85d1ba1647 |
Generated
+1
-1
@@ -1048,7 +1048,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"egui",
|
"egui",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ink-notes"
|
name = "ink-notes"
|
||||||
version = "0.3.0"
|
version = "0.3.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux"
|
description = "Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux"
|
||||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux (Zorin OS 18 / Ubuntu 24.04).
|
Latenzoptimierte Vektor-Notizbuch- und Inking-Anwendung für Linux (Zorin OS 18 / Ubuntu 24.04).
|
||||||
|
|
||||||
### Highlights:
|
### Highlights:
|
||||||
|
- **Wichtige Fehlerbehebung**: Nach einem Neustart auf einer späteren Seite konnte Weiterschreiben den bisherigen Inhalt dieser Seite überschreiben (betrifft v0.2.3 – v0.3.2)
|
||||||
- **Neue Werkzeugleiste**: Schmale senkrechte Leiste mit Symbol-Knöpfen, links oder rechts andockbar; selten genutzte Funktionen im ☰-Menü, das Blatt wird daneben zentriert
|
- **Neue Werkzeugleiste**: Schmale senkrechte Leiste mit Symbol-Knöpfen, links oder rechts andockbar; selten genutzte Funktionen im ☰-Menü, das Blatt wird daneben zentriert
|
||||||
|
- **Einheitliche Dialoge**: Deckende Fenster ohne doppelte Titel, Seitenauswahl mit Vorschaubildern, größere Bedienelemente in allen Popovern
|
||||||
- **UI per Stift und Touch bedienbar**: Werkzeugleiste, Popover und Dialoge reagieren jetzt auch auf Stift und Finger, auch bei Display-Skalierung
|
- **UI per Stift und Touch bedienbar**: Werkzeugleiste, Popover und Dialoge reagieren jetzt auch auf Stift und Finger, auch bei Display-Skalierung
|
||||||
- **Aktive Stylus-Unterstützung (Lazarite / MPP / AES)**: Natives Wayland-Tablet-Protokoll mit präziser Druck- und Neigungserfassung
|
- **Aktive Stylus-Unterstützung (Lazarite / MPP / AES)**: Natives Wayland-Tablet-Protokoll mit präziser Druck- und Neigungserfassung
|
||||||
- **Touchscreen Multi-Touch**: Stufenloses 2-Finger Pinch-to-Zoom und Panning mit umschaltbarem Finger-Zeichenmodus
|
- **Touchscreen Multi-Touch**: Stufenloses 2-Finger Pinch-to-Zoom und Panning mit umschaltbarem Finger-Zeichenmodus
|
||||||
|
|||||||
+6
-1
@@ -312,7 +312,12 @@ 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.
|
||||||
pub fn mark_page_dirty(&mut self, page: &Page, manifest: &NotebookManifest) {
|
pub fn mark_page_dirty(&mut self, page: &Page, manifest: &NotebookManifest) {
|
||||||
if self.read_only || page.persist == crate::document::PersistState::ReadOnlyAfterLoadError {
|
// Nicht geladene Platzhalter nie schreiben: Sie sind leer und würden den Inhalt
|
||||||
|
// der Seitendatei überschreiben
|
||||||
|
if self.read_only
|
||||||
|
|| !page.is_loaded
|
||||||
|
|| page.persist == crate::document::PersistState::ReadOnlyAfterLoadError
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let page_data = PageData::from(page);
|
let page_data = PageData::from(page);
|
||||||
|
|||||||
+18
-23
@@ -55,21 +55,6 @@ fn update_window_title(
|
|||||||
window.set_title(&title);
|
window.set_title(&title);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prüft, ob ein Bildschirmpunkt (physische Pixel) auf einem UI-Element liegt.
|
|
||||||
///
|
|
||||||
/// egui arbeitet in logischen Punkten; Touch- und Stift-Positionen kommen in physischen
|
|
||||||
/// Pixeln. `layer_id_at` erfasst jedes egui-Element des letzten Frames (Leiste, Popover,
|
|
||||||
/// Fenster), auch wenn egui den Zeiger selbst noch nicht kennt (Stift, erster Touch).
|
|
||||||
fn is_screen_point_in_ui(
|
|
||||||
egui_ctx: &egui::Context,
|
|
||||||
ui_state: &UiState,
|
|
||||||
screen_pos: (f32, f32),
|
|
||||||
scale: f32,
|
|
||||||
) -> bool {
|
|
||||||
let pos = egui::pos2(screen_pos.0 / scale, screen_pos.1 / scale);
|
|
||||||
ui_state.is_point_in_ui(pos.x, pos.y) || egui_ctx.layer_id_at(pos).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exportiert das Notizbuch im Hintergrund als PDF. Noch nicht geladene Seiten werden vorher
|
/// 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.
|
/// von der Festplatte nachgeladen; das Ergebnis geht über `result_tx` an den Event-Loop zurück.
|
||||||
fn start_pdf_export(
|
fn start_pdf_export(
|
||||||
@@ -270,7 +255,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
Err(storage::StorageError::AlreadyLocked(msg)) => {
|
Err(storage::StorageError::AlreadyLocked(msg)) => {
|
||||||
eprintln!("[Storage-Lock] {msg}");
|
eprintln!("[Storage-Lock] {msg}");
|
||||||
autosave.set_read_only(true);
|
autosave.set_read_only(true);
|
||||||
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)");
|
ui_state.set_export_message("⚠ In anderer Instanz geöffnet (Schreibschutz)");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -477,7 +462,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.events
|
.events
|
||||||
.push(egui::Event::PointerMoved(ui_pos));
|
.push(egui::Event::PointerMoved(ui_pos));
|
||||||
if stylus_ui_capture
|
if stylus_ui_capture
|
||||||
|| is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale)
|
|| ui::is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale)
|
||||||
{
|
{
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -532,7 +517,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
stylus_tool_id = Some(tool.id());
|
stylus_tool_id = Some(tool.id());
|
||||||
let (sx, sy) = stylus_screen_pos;
|
let (sx, sy) = stylus_screen_pos;
|
||||||
|
|
||||||
if is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) {
|
if ui::is_screen_point_in_ui(&egui_ctx, &ui_state, (sx, sy), scale) {
|
||||||
// In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames
|
// In die Eingabe-Warteschlange des NÄCHSTEN egui-Frames
|
||||||
// (egui_ctx.input_mut würde vom nächsten Frame überschrieben)
|
// (egui_ctx.input_mut würde vom nächsten Frame überschrieben)
|
||||||
let ui_pos = egui::pos2(sx / scale, sy / scale);
|
let ui_pos = egui::pos2(sx / scale, sy / scale);
|
||||||
@@ -710,7 +695,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 2. Prüfen, ob das UI Pointer-Interaktionen beansprucht (Event-Isolation)
|
// 2. Prüfen, ob das UI Pointer-Interaktionen beansprucht (Event-Isolation)
|
||||||
// Konsumiert Klicks innerhalb der abgerundeten Pill-Toolbar, des Popovers oder offener Menüs
|
// Konsumiert Klicks innerhalb der abgerundeten Pill-Toolbar, des Popovers oder offener Menüs
|
||||||
let is_pointer_in_ui = egui_ctx.wants_pointer_input()
|
let is_pointer_in_ui = egui_ctx.wants_pointer_input()
|
||||||
|| is_screen_point_in_ui(
|
|| ui::is_screen_point_in_ui(
|
||||||
&egui_ctx,
|
&egui_ctx,
|
||||||
&ui_state,
|
&ui_state,
|
||||||
cursor_screen_pos,
|
cursor_screen_pos,
|
||||||
@@ -1195,7 +1180,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// 2. Objektauswahl & Transformation im Select-Modus
|
// 2. Objektauswahl & Transformation im Select-Modus
|
||||||
if ui_state.active_tool == ActiveTool::Select {
|
if ui_state.active_tool == ActiveTool::Select {
|
||||||
let screen_vec = Vec2::new(sx, sy);
|
let screen_vec = Vec2::new(sx, sy);
|
||||||
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec);
|
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec, egui_ctx.pixels_per_point());
|
||||||
match hit {
|
match hit {
|
||||||
SelectionHandleHit::RotateHandle { center_page } => {
|
SelectionHandleHit::RotateHandle { center_page } => {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
if let Some(page_pt) = canvas.screen_to_page(sx, sy) {
|
||||||
@@ -1539,7 +1524,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
match touch.phase {
|
match touch.phase {
|
||||||
TouchPhase::Started => {
|
TouchPhase::Started => {
|
||||||
// Event-Isolation für Touch-Eingaben
|
// Event-Isolation für Touch-Eingaben
|
||||||
let is_touch_in_ui = is_screen_point_in_ui(
|
let is_touch_in_ui = ui::is_screen_point_in_ui(
|
||||||
&egui_ctx,
|
&egui_ctx,
|
||||||
&ui_state,
|
&ui_state,
|
||||||
(tx, ty),
|
(tx, ty),
|
||||||
@@ -1595,7 +1580,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
} else if ui_state.active_tool == ActiveTool::Select {
|
} else if ui_state.active_tool == ActiveTool::Select {
|
||||||
let screen_vec = Vec2::new(tx, ty);
|
let screen_vec = Vec2::new(tx, ty);
|
||||||
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec);
|
let hit = selection_state.hit_test_handles(notebook.current_page(), &canvas, screen_vec, egui_ctx.pixels_per_point());
|
||||||
match hit {
|
match hit {
|
||||||
SelectionHandleHit::RotateHandle { center_page } => {
|
SelectionHandleHit::RotateHandle { center_page } => {
|
||||||
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
if let Some(page_pt) = canvas.screen_to_page(tx, ty) {
|
||||||
@@ -2083,6 +2068,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
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::LoadAllPages => {
|
||||||
|
// Für die Vorschaubilder der Seitenauswahl; Fehler werden wie beim
|
||||||
|
// Blättern behandelt (Ersatzseite, Warnung beim Öffnen der Seite)
|
||||||
|
for idx in 0..notebook.pages.len() {
|
||||||
|
if !notebook.pages[idx].is_loaded {
|
||||||
|
let _ = storage::ensure_page_loaded(&storage_dir, &mut notebook, idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
UiAction::GoToPage(idx) => {
|
UiAction::GoToPage(idx) => {
|
||||||
if idx < notebook.pages.len() {
|
if idx < notebook.pages.len() {
|
||||||
selection_state.clear();
|
selection_state.clear();
|
||||||
@@ -2133,7 +2128,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
eprintln!("[Storage-Lock] {msg}");
|
eprintln!("[Storage-Lock] {msg}");
|
||||||
_notebook_lock = None;
|
_notebook_lock = None;
|
||||||
autosave.set_read_only(true);
|
autosave.set_read_only(true);
|
||||||
ui_state.set_export_message("⚠️ In anderer Instanz geöffnet (Schreibschutz)");
|
ui_state.set_export_message("⚠ In anderer Instanz geöffnet (Schreibschutz)");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}");
|
eprintln!("[Storage-Lock] Konnte Lock nicht erwerben: {e}");
|
||||||
|
|||||||
+28
-12
@@ -115,12 +115,15 @@ impl SelectionState {
|
|||||||
Page::selection_aabb(&strokes)
|
Page::selection_aabb(&strokes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Führt Hit-Testing auf die Auswahlgriffe (Drehen, Ecken, Körper) in Bildschirmkoordinaten durch.
|
/// Führt Hit-Testing auf die Auswahlgriffe (Drehen, Ecken, Körper) in Bildschirmkoordinaten
|
||||||
|
/// (physische Pixel) durch. Abstände und Trefferradien sind in logischen Punkten festgelegt
|
||||||
|
/// und werden mit `pixels_per_point` skaliert, damit sie zur gezeichneten Überlagerung passen.
|
||||||
pub fn hit_test_handles(
|
pub fn hit_test_handles(
|
||||||
&self,
|
&self,
|
||||||
page: &Page,
|
page: &Page,
|
||||||
canvas: &CanvasState,
|
canvas: &CanvasState,
|
||||||
screen_pos: Vec2,
|
screen_pos: Vec2,
|
||||||
|
pixels_per_point: f32,
|
||||||
) -> SelectionHandleHit {
|
) -> SelectionHandleHit {
|
||||||
let Some(aabb) = self.get_selection_aabb(page) else {
|
let Some(aabb) = self.get_selection_aabb(page) else {
|
||||||
return SelectionHandleHit::None;
|
return SelectionHandleHit::None;
|
||||||
@@ -134,9 +137,10 @@ impl SelectionState {
|
|||||||
let screen_center = (screen_min + screen_max) * 0.5;
|
let screen_center = (screen_min + screen_max) * 0.5;
|
||||||
let page_center = (aabb.min + aabb.max) * 0.5;
|
let page_center = (aabb.min + aabb.max) * 0.5;
|
||||||
|
|
||||||
// 1. Drehgriff testen (26 px oberhalb des oberen Randes)
|
// 1. Drehgriff testen (oberhalb des oberen Randes, wie gezeichnet)
|
||||||
let rot_screen = Vec2::new(screen_center.x, screen_min.y - 26.0);
|
let ppp = pixels_per_point;
|
||||||
if (screen_pos - rot_screen).length() <= 14.0 {
|
let rot_screen = Vec2::new(screen_center.x, screen_min.y - ROTATE_HANDLE_OFFSET * ppp);
|
||||||
|
if (screen_pos - rot_screen).length() <= HANDLE_HIT_RADIUS * ppp {
|
||||||
return SelectionHandleHit::RotateHandle {
|
return SelectionHandleHit::RotateHandle {
|
||||||
center_page: page_center,
|
center_page: page_center,
|
||||||
};
|
};
|
||||||
@@ -164,7 +168,7 @@ impl SelectionState {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
if (screen_pos - corners_screen[i]).length() <= 12.0 {
|
if (screen_pos - corners_screen[i]).length() <= HANDLE_HIT_RADIUS * ppp {
|
||||||
return SelectionHandleHit::CornerHandle {
|
return SelectionHandleHit::CornerHandle {
|
||||||
corner_idx: i,
|
corner_idx: i,
|
||||||
fixed_corner_page: fixed_corners_page[i],
|
fixed_corner_page: fixed_corners_page[i],
|
||||||
@@ -273,6 +277,16 @@ impl SelectionState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rendert das Auswahl-Overlay (Bounding Box, Ecken, Dreh-Griff, Marquee Box) über egui.
|
/// Rendert das Auswahl-Overlay (Bounding Box, Ecken, Dreh-Griff, Marquee Box) über egui.
|
||||||
|
/// Abstand des Drehgriffs über dem Auswahlrahmen (logische Punkte).
|
||||||
|
const ROTATE_HANDLE_OFFSET: f32 = 24.0;
|
||||||
|
/// Trefferradius der Griffe (logische Punkte), groß genug für Finger.
|
||||||
|
const HANDLE_HIT_RADIUS: f32 = 18.0;
|
||||||
|
|
||||||
|
/// Wandelt Fensterpixel (physisch) in egui-Koordinaten (logische Punkte) um.
|
||||||
|
fn to_ui(screen: [f32; 2], pixels_per_point: f32) -> egui::Pos2 {
|
||||||
|
egui::pos2(screen[0] / pixels_per_point, screen[1] / pixels_per_point)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_selection_overlay(
|
pub fn render_selection_overlay(
|
||||||
ctx: &egui::Context,
|
ctx: &egui::Context,
|
||||||
selection: &SelectionState,
|
selection: &SelectionState,
|
||||||
@@ -283,6 +297,8 @@ pub fn render_selection_overlay(
|
|||||||
egui::Order::Foreground,
|
egui::Order::Foreground,
|
||||||
egui::Id::new("selection_overlay_layer"),
|
egui::Id::new("selection_overlay_layer"),
|
||||||
));
|
));
|
||||||
|
// Fensterpixel sind physisch, egui zeichnet in logischen Punkten
|
||||||
|
let ppp = ctx.pixels_per_point();
|
||||||
|
|
||||||
// 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box)
|
// 1. Wenn aktuell ein Auswahlrahmen gezogen wird (Marquee Box)
|
||||||
if let DragMode::BoxSelect {
|
if let DragMode::BoxSelect {
|
||||||
@@ -290,8 +306,8 @@ pub fn render_selection_overlay(
|
|||||||
current_page,
|
current_page,
|
||||||
} = selection.drag_mode
|
} = selection.drag_mode
|
||||||
{
|
{
|
||||||
let p1 = canvas.page_to_screen(start_page);
|
let p1 = to_ui(canvas.page_to_screen(start_page), ppp);
|
||||||
let p2 = canvas.page_to_screen(current_page);
|
let p2 = to_ui(canvas.page_to_screen(current_page), ppp);
|
||||||
let min_x = p1[0].min(p2[0]);
|
let min_x = p1[0].min(p2[0]);
|
||||||
let min_y = p1[1].min(p2[1]);
|
let min_y = p1[1].min(p2[1]);
|
||||||
let max_x = p1[0].max(p2[0]);
|
let max_x = p1[0].max(p2[0]);
|
||||||
@@ -303,14 +319,14 @@ pub fn render_selection_overlay(
|
|||||||
painter.rect_filled(
|
painter.rect_filled(
|
||||||
rect,
|
rect,
|
||||||
egui::Rounding::same(4.0_f32),
|
egui::Rounding::same(4.0_f32),
|
||||||
egui::Color32::from_rgba_premultiplied(40, 120, 240, 35),
|
egui::Color32::from_rgba_unmultiplied(40, 120, 240, 35),
|
||||||
);
|
);
|
||||||
painter.rect_stroke(
|
painter.rect_stroke(
|
||||||
rect,
|
rect,
|
||||||
egui::Rounding::same(4.0_f32),
|
egui::Rounding::same(4.0_f32),
|
||||||
egui::Stroke::new(
|
egui::Stroke::new(
|
||||||
1.5_f32,
|
1.5_f32,
|
||||||
egui::Color32::from_rgba_premultiplied(80, 160, 255, 220),
|
egui::Color32::from_rgba_unmultiplied(80, 160, 255, 220),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -321,8 +337,8 @@ pub fn render_selection_overlay(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let p_min = canvas.page_to_screen(aabb.min);
|
let p_min = to_ui(canvas.page_to_screen(aabb.min), ppp);
|
||||||
let p_max = canvas.page_to_screen(aabb.max);
|
let p_max = to_ui(canvas.page_to_screen(aabb.max), ppp);
|
||||||
|
|
||||||
let screen_min = egui::pos2(p_min[0].min(p_max[0]), p_min[1].min(p_max[1]));
|
let screen_min = egui::pos2(p_min[0].min(p_max[0]), p_min[1].min(p_max[1]));
|
||||||
let screen_max = egui::pos2(p_min[0].max(p_max[0]), p_min[1].max(p_max[1]));
|
let screen_max = egui::pos2(p_min[0].max(p_max[0]), p_min[1].max(p_max[1]));
|
||||||
@@ -338,7 +354,7 @@ pub fn render_selection_overlay(
|
|||||||
|
|
||||||
// Dreh-Stängel (Stalk) und Drehgriff
|
// Dreh-Stängel (Stalk) und Drehgriff
|
||||||
let top_center = egui::pos2(rect.center().x, rect.min.y);
|
let top_center = egui::pos2(rect.center().x, rect.min.y);
|
||||||
let rot_handle = egui::pos2(rect.center().x, rect.min.y - 24.0_f32);
|
let rot_handle = egui::pos2(rect.center().x, rect.min.y - ROTATE_HANDLE_OFFSET);
|
||||||
|
|
||||||
painter.line_segment(
|
painter.line_segment(
|
||||||
[top_center, rot_handle],
|
[top_center, rot_handle],
|
||||||
|
|||||||
@@ -697,6 +697,15 @@ pub fn load_notebook(notebook_dir: &Path) -> Result<(Notebook, NotebookManifest)
|
|||||||
);
|
);
|
||||||
notebook.load_warnings = load_warnings;
|
notebook.load_warnings = load_warnings;
|
||||||
|
|
||||||
|
// Die zuletzt geöffnete Seite sofort laden: Sie ist nach dem Start die aktuelle Seite,
|
||||||
|
// und ein leerer Platzhalter würde beim Weiterschreiben ihren Inhalt überschreiben.
|
||||||
|
let current = notebook.current_page_index;
|
||||||
|
if !notebook.pages[current].is_loaded {
|
||||||
|
if let Err(e) = ensure_page_loaded(notebook_dir, &mut notebook, current) {
|
||||||
|
eprintln!("[Storage] Aktuelle Seite {current} konnte nicht geladen werden: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((notebook, manifest))
|
Ok((notebook, manifest))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2167,4 +2176,61 @@ mod tests {
|
|||||||
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
assert_eq!(loaded.pages[1].background, PageBackground::Lined);
|
||||||
assert!(crate::export::generate_pdf_document(&loaded, Default::default()).is_ok());
|
assert!(crate::export::generate_pdf_document(&loaded, Default::default()).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_restart_on_later_page_keeps_its_content() {
|
||||||
|
// Regression: Beim Start wurde nur Seite 1 geladen; war zuletzt eine spätere Seite
|
||||||
|
// offen, war sie ein leerer Platzhalter, und Weiterschreiben überschrieb ihren Inhalt.
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let (mut notebook, mut manifest, dir) =
|
||||||
|
create_new_notebook(temp_dir.path(), "Neustart").unwrap();
|
||||||
|
notebook.add_page(PageBackground::Lined);
|
||||||
|
notebook.pages[1] = page_with_one_stroke(notebook.pages[1].id, PageBackground::Lined);
|
||||||
|
notebook.current_page_index = 1;
|
||||||
|
save_notebook(&dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
|
||||||
|
let (mut loaded, mut loaded_manifest) = load_notebook(&dir).unwrap();
|
||||||
|
assert_eq!(loaded.current_page_index, 1);
|
||||||
|
assert!(
|
||||||
|
loaded.current_page().is_loaded,
|
||||||
|
"aktuelle Seite muss geladen sein"
|
||||||
|
);
|
||||||
|
assert_eq!(loaded.current_page().strokes.len(), 1);
|
||||||
|
|
||||||
|
// Weiterschreiben und speichern darf den bisherigen Inhalt nicht verlieren
|
||||||
|
let extra = page_with_one_stroke(99, PageBackground::Lined).strokes[0].clone();
|
||||||
|
loaded.current_page_mut().strokes.push(extra);
|
||||||
|
let mut autosave = AutosaveController::new(dir.clone(), Duration::from_secs(60));
|
||||||
|
autosave.mark_dirty(&loaded, &mut loaded_manifest);
|
||||||
|
autosave.flush().unwrap();
|
||||||
|
autosave.shutdown().unwrap();
|
||||||
|
|
||||||
|
let (reloaded, _) = load_notebook(&dir).unwrap();
|
||||||
|
assert_eq!(reloaded.current_page().strokes.len(), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_autosave_never_writes_unloaded_placeholder() {
|
||||||
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
let (mut notebook, mut manifest, dir) =
|
||||||
|
create_new_notebook(temp_dir.path(), "Platzhalter").unwrap();
|
||||||
|
notebook.add_page(PageBackground::Lined);
|
||||||
|
notebook.pages[1] = page_with_one_stroke(notebook.pages[1].id, PageBackground::Lined);
|
||||||
|
save_notebook(&dir, ¬ebook, &mut manifest).unwrap();
|
||||||
|
let page_file = dir.join(format!("page_{}.json", notebook.pages[1].id));
|
||||||
|
let before = fs::read(&page_file).unwrap();
|
||||||
|
|
||||||
|
// Platzhalter (nicht geladen) als aktuelle Seite markieren und speichern
|
||||||
|
let placeholder = Page::new_unloaded(notebook.pages[1].id, PageBackground::Lined);
|
||||||
|
let mut autosave = AutosaveController::new(dir.clone(), Duration::from_secs(60));
|
||||||
|
autosave.mark_page_dirty(&placeholder, &manifest);
|
||||||
|
autosave.flush().unwrap();
|
||||||
|
autosave.shutdown().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
fs::read(&page_file).unwrap(),
|
||||||
|
before,
|
||||||
|
"Seitendatei wurde überschrieben"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user