Compare commits

..
5 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 f76fad92be chore(release): bump version to 0.3.3
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 9m10s
Behebt Datenverlust nach Neustart auf einer späteren Seite (v0.2.3 – v0.3.2); Seitenauswahl mit Vorschaubildern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:32:58 +02:00
haraldandClaude Opus 5.5 2c60a03460 feat(ui): Vorschaubilder in der Seitenauswahl
- Kacheln zeigen eine verkleinerte A4-Vorschau jeder Seite (Striche als
  Linien, Textmarker halbtransparent, volle Seiten auf ~1500 Punkte
  ausgedünnt), Seitenzahl darunter, aktuelle Seite blau umrandet
- Noch nicht geladene Seiten werden bei offener Seitenauswahl nachgeladen
  (UiAction::LoadAllPages)
- Kachelgröße über PAGE_TILE (vorher feste Werte)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:03:08 +02:00
haraldandClaude Opus 5.5 b6580f197d fix(storage): Inhalt der zuletzt geöffneten Seite nach Neustart nicht überschreiben
Beim Start wurde nur die erste Seite geladen. War zuletzt eine spätere Seite
offen, war sie nach dem Neustart ein leerer, ungeladener Platzhalter: Das
Blatt erschien leer, und Zeichnen/Radieren/Hintergrund wechseln speicherte
den Platzhalter und überschrieb den bisherigen Inhalt der Seite.

- load_notebook lädt zusätzlich die aktuelle Seite
- Autosave schreibt nie ungeladene Platzhalter (Schutz ging beim Merge des
  Storage-Branches verloren)
- Regressionstests für beide Fälle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:03:08 +02:00
haraldandClaude Opus 5.5 22e35f2e77 chore(release): bump version to 0.3.2
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 9m33s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:40:44 +02:00
haraldandClaude Opus 5.5 a77e6cde90 feat(ui): einheitliche, deckende Dialoge und fingertaugliche Popover
- Dialoge (Notizbücher, Updates, Warnfenster) mit gemeinsamem deckendem
  Rahmen, ohne doppelte Titelleiste, zentriert neben der Werkzeugleiste;
  Schließen-Knopf 36 px, größere Knöpfe, Eingabefelder und Schieberegler
- Seitenauswahl als Popover neben dem Seitenknopf mit Kachelraster
  (56 × 48 px) und „Neue Seite anhängen“
- Immer nur ein Dialog oder Popover gleichzeitig offen (close_overlays);
  Seitenauswahl schließt bei Tippen daneben
- Stift-/Marker-Popover: Farbtupfer 30 px, Stärke-Vorschau 40 px hoch,
  breiterer Schieberegler, deckender Rahmen
- Auswahl-Überlagerung und Griff-Trefferprüfung rechnen mit der
  Display-Skalierung (lag bei Skalierung ≠ 1 neben den Strichen);
  Trefferradius der Griffe 18 Punkte für Finger

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 23:51:48 +02:00
8 changed files with 884 additions and 643 deletions
Generated
+1 -1
View File
@@ -1048,7 +1048,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.3.1" version = "0.3.3"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"egui", "egui",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.3.1" 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"
+2
View File
@@ -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
View File
@@ -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);
+12 -2
View File
@@ -1180,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) {
@@ -1580,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) {
@@ -2068,6 +2068,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
update_window_title(&window, &notebook, &manifest, ui_state.active_tool, autosave.status()); update_window_title(&window, &notebook, &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();
+26 -10
View File
@@ -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]);
@@ -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],
+66
View File
@@ -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, &notebook, &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, &notebook, &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"
);
}
} }
+770 -628
View File
File diff suppressed because it is too large Load Diff