feat: Seitenanordnung je Notizbuch (Issue #30, Schritt 1)

Ob ein Notizbuch Seite für Seite oder durchgehend angezeigt wird, steht
jetzt im Manifest (`layout`). Beim Anlegen ist „Feste Seiten“
vorgewählt; im ☰-Menü lässt sich die Anordnung des offenen Notizbuchs
umschalten. Notizbücher ohne Angabe behalten die frühere globale
Einstellung, ältere App-Versionen ignorieren das Feld.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-18 21:58:55 +02:00
1 parent 1b30f2bd26
commit 8ea20aa783
8 files changed
+128 -26

No files matched your search

+6 -3
View File
@@ -20,8 +20,6 @@ pub const EN: &[(&str, &str)] = &[
("Alle", "All"),
("Alles gespeichert", "All saved"),
("Als leere Seite bearbeiten (Schreibschutz aufheben)", "Edit as blank page (remove write protection)"),
("An: Seiten hängen untereinander, Mausrad und Finger scrollen (Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, bekommt automatisch eine neue Seite.\nAus: Seite für Seite blättern.", "On: pages are stacked, mouse wheel and finger scroll (Ctrl+wheel zooms); writing at the bottom of the last page adds a new page automatically.\nOff: flip page by page."),
("ANSICHT", "VIEW"),
("AppImage (Portabel)", "AppImage (portable)"),
("Asset-Name darf nicht leer sein.", "Asset name must not be empty."),
("Auch per Strg+V aus der Zwischenablage oder durch Ziehen einer Bilddatei ins Fenster", "Also with Ctrl+V from the clipboard or by dragging an image file into the window"),
@@ -267,6 +265,7 @@ pub const EN: &[(&str, &str)] = &[
("Schwarz", "Black"),
("Sechseck (Hexagon)", "Hexagon"),
("Seite als", "Page as"),
("Seite für Seite blättern", "Flip page by page"),
("Seite ist schreibgeschützt", "Page is read-only"),
("Seite konnte nicht geladen werden", "Page could not be loaded"),
("Seite wird exportiert …", "Exporting page …"),
@@ -285,7 +284,9 @@ pub const EN: &[(&str, &str)] = &[
("Seite {number} – {title}", "Page {number} – {title}"),
("Seite {number}", "Page {number}"),
("Seiten ({count})", "Pages ({count})"),
("Seiten durchgehend scrollen", "Scroll pages continuously"),
("SEITEN DIESES NOTIZBUCHS", "PAGES OF THIS NOTEBOOK"),
("Seiten hängen untereinander, Mausrad und Finger scrollen (Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, bekommt automatisch eine neue Seite.", "Pages are stacked, mouse wheel and finger scroll (Ctrl+wheel zooms); writing at the bottom of the last page adds a new page automatically."),
("Seiten:", "Pages:"),
("Seitenverhältnis frei – tippen zum Sperren", "Aspect ratio free – tap to lock"),
("Seitenverhältnis gesperrt – tippen zum Freigeben", "Aspect ratio locked – tap to unlock"),
("Serialisierungsfehler: {e}", "Serialization error: {e}"),
@@ -418,12 +419,14 @@ pub const EN: &[(&str, &str)] = &[
("📂 Ordner öffnen", "📂 Open folder"),
("📄 Als PDF exportieren (Strg+E)", "📄 Export as PDF (Ctrl+E)"),
("📄 PDF importieren …", "📄 Import PDF …"),
("📄 Feste Seiten", "📄 Fixed pages"),
("📄 PDF importieren", "📄 Import PDF"),
("📋 Einfügen", "📋 Paste"),
("📑 Inhalt", "📑 Contents"),
("📚 Alle", "📚 All"),
("📚 Notizbücher", "📚 Notebooks"),
("📚 Oberste Ebene", "📚 Top level"),
("📜 Durchgehend", "📜 Continuous"),
("📦 Paket mit Software-Center öffnen / installieren", "📦 Open / install package with the software center"),
("🔄 Suche nach Updates …", "🔄 Checking for updates …"),
("🔄 Updates & Versionen", "🔄 Updates & versions"),
+12 -6
View File
@@ -36,7 +36,7 @@ use ink_notes::selection::{
use ink_notes::settings::{
SidebarSide, StylusAction, StylusButton, UiSettings, MAX_STYLUS_BUTTONS,
};
use ink_notes::storage::{self, NotebookManifest};
use ink_notes::storage::{self, NotebookLayout, NotebookManifest};
use ink_notes::t;
use ink_notes::ui::{self, ActiveTool, UiAction, UiState};
use ink_notes::updater::UpdaterController;
@@ -1522,6 +1522,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
event_loop.run(move |event, target| {
// Anordnung der Seiten gehört zum offenen Notizbuch (Issue #30)
ui_state.continuous_scroll =
manifest.layout_or(ui_settings.continuous_scroll) == NotebookLayout::Continuous;
match event {
Event::AboutToWait => {
let prev_status = autosave.status();
@@ -3562,11 +3565,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
window.request_redraw();
}
UiAction::CreateNotebook { title, folder } => {
UiAction::CreateNotebook { title, folder, layout } => {
let _ = autosave.flush();
match storage::create_new_notebook(&storage_base_dir, &title) {
Ok((new_nb, new_manifest, new_dir)) => {
Ok((new_nb, mut new_manifest, new_dir)) => {
let id = new_manifest.id.clone();
new_manifest.layout = Some(layout);
change_library(&storage_base_dir, &mut ui_state, |library| {
library.move_notebook(&id, &folder)
});
@@ -3576,6 +3580,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
notebook = new_nb;
manifest = new_manifest;
storage_dir = new_dir;
autosave.mark_manifest_dirty(&manifest);
selection_state.clear();
ui_state.current_notebook_id = manifest.id.clone();
ui_state.document_title = manifest.title.clone();
@@ -3820,9 +3825,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
window.request_redraw();
}
UiAction::SetContinuousScroll(on) => {
ui_settings.continuous_scroll = on;
save_ui_settings(&ui_settings, ui_settings_path.as_deref(), &mut ui_state);
UiAction::SetNotebookLayout(layout) => {
manifest.layout = Some(layout);
ui_state.continuous_scroll = layout == NotebookLayout::Continuous;
autosave.mark_manifest_dirty(&manifest);
window.request_redraw();
}
UiAction::SaveStylusButtons => {
+2 -1
View File
@@ -212,7 +212,8 @@ pub struct UiSettings {
pub pattern_color: PatternColor,
/// Druckkennlinie des Stifts
pub pressure_curve: PressureCurve,
/// Seiten durchgehend untereinander anzeigen (sonst einzeln blättern)
/// Frühere globale Einstellung „Seiten durchgehend scrollen“: gilt nur noch für Notizbücher
/// ohne eigene Anordnung im Manifest (vor 0.28 angelegt), wird nicht mehr geändert
pub continuous_scroll: bool,
/// Stifte der Werkzeugleiste und der gewählte davon
pub pen_presets: [PenPreset; PEN_PRESET_COUNT],
+49
View File
@@ -280,9 +280,32 @@ pub struct NotebookManifest {
pub page_ids: Vec<u64>,
pub next_stroke_id: u64,
pub next_page_id: u64,
/// Anordnung der Seiten. Fehlt bei Notizbüchern von vor 0.28; dann gilt die frühere
/// globale Einstellung `continuous_scroll` aus `ui.json` (siehe [`Self::layout_or`]).
#[serde(default, skip_serializing_if = "Option::is_none")]
pub layout: Option<NotebookLayout>,
}
/// Anordnung der Seiten eines Notizbuchs (Issue #30).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum NotebookLayout {
/// Feste Seiten: Seite für Seite blättern
#[default]
Paged,
/// Seiten hängen untereinander und werden durchgehend gescrollt
Continuous,
}
impl NotebookManifest {
/// Anordnung des Notizbuchs; ohne eigene Angabe die frühere globale Einstellung.
pub fn layout_or(&self, legacy_continuous: bool) -> NotebookLayout {
self.layout.unwrap_or(if legacy_continuous {
NotebookLayout::Continuous
} else {
NotebookLayout::Paged
})
}
pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@@ -299,6 +322,7 @@ impl NotebookManifest {
page_ids: vec![1],
next_stroke_id: 1,
next_page_id: 2,
layout: Some(NotebookLayout::Paged),
}
}
@@ -1604,6 +1628,31 @@ mod tests {
assert!(shutdown_res.is_ok());
}
#[test]
fn test_notebook_layout_is_saved_and_old_manifests_fall_back() {
let temp_dir = tempfile::tempdir().unwrap();
let (_nb, mut manifest, dir) = create_new_notebook(temp_dir.path(), "Layout").unwrap();
// Neue Notizbücher: feste Seiten, unabhängig von der früheren globalen Einstellung
assert_eq!(manifest.layout, Some(NotebookLayout::Paged));
assert_eq!(manifest.layout_or(true), NotebookLayout::Paged);
manifest.layout = Some(NotebookLayout::Continuous);
save_manifest(&dir, &manifest).unwrap();
let (_nb, loaded) = load_notebook(&dir).unwrap();
assert_eq!(loaded.layout, Some(NotebookLayout::Continuous));
// Manifest von vor 0.28 ohne `layout`: es gilt die globale Einstellung
let mut json: serde_json::Value =
serde_json::from_str(&fs::read_to_string(dir.join("manifest.json")).unwrap()).unwrap();
json.as_object_mut().unwrap().remove("layout");
let old: NotebookManifest = serde_json::from_value(json).unwrap();
assert_eq!(old.layout, None);
assert_eq!(old.layout_or(true), NotebookLayout::Continuous);
assert_eq!(old.layout_or(false), NotebookLayout::Paged);
// … und schreibt beim Speichern auch keins dazu
assert!(!serde_json::to_string(&old).unwrap().contains("\"layout\""));
}
#[test]
fn test_notebook_management() {
let temp_dir = tempfile::tempdir().expect("Konnte temporäres Verzeichnis nicht anlegen");
+53 -14
View File
@@ -22,7 +22,7 @@ use crate::settings::{
PenPreset, PressureCurve, SidebarSide, DEFAULT_HIGHLIGHTER_COLORS, DEFAULT_PEN_PRESETS,
DEFAULT_TEXT_COLORS, PEN_PRESET_COUNT,
};
use crate::storage::NotebookSummary;
use crate::storage::{NotebookLayout, NotebookSummary};
use crate::text::{TextEditing, TextFont, TEXT_SIZES_PT};
use crate::{t, tn};
@@ -263,6 +263,7 @@ pub enum UiAction {
CreateNotebook {
title: String,
folder: String,
layout: NotebookLayout,
},
/// Ordnerverwaltung (siehe `library.rs`)
CreateFolder {
@@ -317,8 +318,8 @@ pub enum UiAction {
SaveStylusButtons,
/// Druckkurve speichern (gilt während des Ziehens bereits über `UiState::pressure_curve`)
SavePressureCurve(PressureCurve),
/// Seiten durchgehend untereinander statt einzeln anzeigen (wird gespeichert)
SetContinuousScroll(bool),
/// Anordnung der Seiten des offenen Notizbuchs (wird im Manifest gespeichert)
SetNotebookLayout(NotebookLayout),
/// Farbe von Karos, Linien und Punkten (wird gespeichert, gilt auch für den Export)
SetPatternColor(PatternColor),
/// Bilddatei auf der aktuellen Seite einfügen
@@ -364,6 +365,8 @@ pub struct UiState {
pub document_title: String,
pub available_notebooks: Vec<NotebookSummary>,
pub new_notebook_title_input: String,
/// Anordnung für das nächste neue Notizbuch
pub new_notebook_layout: NotebookLayout,
pub renaming_notebook_id: Option<String>,
/// Ordner der Notizbücher und der gerade angezeigte Ordner im Dokumenten-Manager
pub library: crate::library::Library,
@@ -483,6 +486,7 @@ impl Default for UiState {
document_title: t!("Standard-Notizbuch").to_string(),
available_notebooks: Vec::new(),
new_notebook_title_input: String::new(),
new_notebook_layout: NotebookLayout::Paged,
renaming_notebook_id: None,
library: crate::library::Library::default(),
notebook_folder: String::new(),
@@ -1641,6 +1645,38 @@ fn page_title_list(ui: &mut egui::Ui, notebook: &Notebook) -> Option<usize> {
chosen
}
/// Auswahl der Seitenanordnung eines Notizbuchs. `true`, wenn sie geändert wurde.
fn layout_picker(ui: &mut egui::Ui, layout: &mut NotebookLayout) -> bool {
let mut changed = false;
ui.horizontal_wrapped(|ui| {
for (option, label, tooltip) in [
(
NotebookLayout::Paged,
t!("📄 Feste Seiten"),
t!("Seite für Seite blättern"),
),
(
NotebookLayout::Continuous,
t!("📜 Durchgehend"),
t!("Seiten hängen untereinander, Mausrad und Finger scrollen \
(Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, \
bekommt automatisch eine neue Seite."),
),
] {
if ui
.selectable_label(*layout == option, label)
.on_hover_text(tooltip)
.clicked()
&& *layout != option
{
*layout = option;
changed = true;
}
}
});
changed
}
/// Auswahlknopf für eine Papierfarbe: Farbmuster und Name.
fn paper_theme_button(ui: &mut egui::Ui, theme: PaperTheme, selected: bool) -> egui::Response {
let response = ui.add_sized(
@@ -3753,17 +3789,15 @@ pub fn render_ui(
}
});
section(ui, t!("ANSICHT"));
if ui
.checkbox(&mut state.continuous_scroll, t!("Seiten durchgehend scrollen"))
.on_hover_text(
t!("An: Seiten hängen untereinander, Mausrad und Finger scrollen \
(Strg+Mausrad zoomt); wer unten auf der letzten Seite schreibt, \
bekommt automatisch eine neue Seite.\nAus: Seite für Seite blättern."),
)
.changed()
{
actions.push(UiAction::SetContinuousScroll(state.continuous_scroll));
section(ui, t!("SEITEN DIESES NOTIZBUCHS"));
let mut layout = if state.continuous_scroll {
NotebookLayout::Continuous
} else {
NotebookLayout::Paged
};
if layout_picker(ui, &mut layout) {
state.continuous_scroll = layout == NotebookLayout::Continuous;
actions.push(UiAction::SetNotebookLayout(layout));
}
section(ui, t!("ZOOM"));
@@ -4098,6 +4132,7 @@ pub fn render_ui(
actions.push(UiAction::CreateNotebook {
title,
folder: folder.clone(),
layout: state.new_notebook_layout,
});
state.new_notebook_title_input.clear();
} else if folder_btn.clicked() {
@@ -4109,6 +4144,10 @@ pub fn render_ui(
}
}
});
ui.horizontal(|ui| {
ui.label(t!("Seiten:"));
layout_picker(ui, &mut state.new_notebook_layout);
});
ui.add_space(8.0);
ui.separator();