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:
@@ -49,7 +49,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
|
||||
### Seiten und Ansicht
|
||||
- **Hintergründe:** blanko, kariert (5 mm), liniert (7,5 mm), Punktraster (5 mm). Die **Farbe von Karos, Linien und Punkten** ist wählbar (hell, mittel, kräftig, blau, grün) und gilt auch im Export.
|
||||
- **Papierfarbe** der Anzeige: weiß, creme, dunkelgrau, schwarz; auf dunklem Papier werden dunkle Stiftfarben auf Wunsch aufgehellt. Gespeichert und exportiert wird immer auf weißem Papier.
|
||||
- **Durchgehendes Scrollen:** Seiten hängen lückenlos untereinander, eine dunkle Linie markiert die Grenze; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite. Abschaltbar (dann Seite für Seite blättern, mit Pfeilen in der Werkzeugleiste).
|
||||
- **Feste Seiten oder durchgehendes Scrollen, je Notizbuch:** Beim Anlegen wählbar (Vorgabe: feste Seiten, Seite für Seite blättern mit Pfeilen in der Werkzeugleiste), später im ☰-Menü umschaltbar. Durchgehend hängen die Seiten lückenlos untereinander, eine dunkle Linie markiert die Grenze; wer im unteren Fünftel der letzten Seite schreibt, bekommt automatisch eine neue Seite.
|
||||
- **Seiten einfügen und löschen:** ➕ in der Werkzeugleiste fügt nach der aktuellen Seite eine leere Seite ein. Löschen im ☰-Menü (mit Rückfrage); die zuletzt gelöschte Seite lässt sich dort wiederherstellen, solange die App läuft.
|
||||
- **Zoom** von 25 % bis 1000 % (`Strg + Mausrad`, zwei Finger, `Strg + +/-/0`), Verschieben mit mittlerer Maustaste, `Leertaste + Ziehen` oder einem Finger.
|
||||
- **Seiten** anhängen oder direkt nach der aktuellen Seite einfügen, **Seitentitel** vergeben; die Seitenübersicht zeigt Vorschaubilder oder ein Inhaltsverzeichnis. Seitentitel werden im PDF zu Lesezeichen.
|
||||
|
||||
@@ -93,6 +93,8 @@ $$\vec{p}_{\text{neu}} = \vec{c} - (\vec{c} - \vec{p}_{\text{alt}}) \cdot \frac{
|
||||
- Im Shader wird jeder Punkt mit `transform_scale` und `transform_offset` (aus `CameraUniforms`) direkt in Gerätekoordinaten umgerechnet.
|
||||
|
||||
### 3.3 Durchgehende Ansicht
|
||||
Ob ein Notizbuch Seite für Seite oder durchgehend angezeigt wird, steht in seinem Manifest (`layout`, siehe [`FILE_FORMAT.md`](FILE_FORMAT.md)).
|
||||
|
||||
Alle Koordinaten beziehen sich auf die **aktuelle Seite**. `PageStack` (`viewport.rs`) legt die übrigen Seiten lückenlos darunter bzw. darüber (die Grenze zeichnet die Oberfläche als dunkle Linie), waagerecht zur aktuellen Seite zentriert. Jede sichtbare Nachbarseite wird mit einer eigenen Kamera gezeichnet, die um die Lage der Seite verschoben ist (höchstens 12 Nachbarseiten).
|
||||
|
||||
Nach dem Scrollen wird die Seite in der Fenstermitte zur aktuellen Seite; die Kamera wird dabei so verschoben, dass sich das Bild nicht bewegt. Während eines Strichs, einer Auswahl, beim Ziehen des Lineals oder beim Schreiben von Text wird nicht gewechselt. Wer auf einer sichtbaren Nachbarseite zu zeichnen beginnt, wechselt zuerst auf diese Seite.
|
||||
|
||||
+3
-1
@@ -63,7 +63,8 @@ Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleib
|
||||
],
|
||||
"page_ids": [1, 2],
|
||||
"next_stroke_id": 42,
|
||||
"next_page_id": 3
|
||||
"next_page_id": 3,
|
||||
"layout": "Paged"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -76,6 +77,7 @@ Ordner wie „3. Semester › Mathe“ sind rein logisch: Die Notizbücher bleib
|
||||
| `pages` | Seiten in ihrer Reihenfolge: `id`, `background`, optional `title` und `size` (siehe unten) – damit Seitenübersicht und Ansicht sie kennen, ohne jede Seite zu laden |
|
||||
| `page_ids` | dieselbe Reihenfolge als reine ID-Liste (für ältere Versionen) |
|
||||
| `next_stroke_id`, `next_page_id` | nächste freie IDs |
|
||||
| `layout` | Anordnung der Seiten: `Paged` (Seite für Seite) oder `Continuous` (durchgehend untereinander). Seit 0.28; fehlt das Feld, gilt die frühere globale Einstellung `continuous_scroll` aus `ui.json`. Ältere App-Versionen ignorieren es, daher keine neue Formatversion |
|
||||
|
||||
---
|
||||
|
||||
|
||||
+6
-3
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user