Compare commits
7
Commits
81ccd93438
...
v0.28.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc29faddc | ||
|
|
8ea20aa783 | ||
|
|
1b30f2bd26 | ||
|
|
657a5ff16e | ||
|
|
8fddd9e2c3 | ||
|
|
1a98c9b8bb | ||
|
|
98b78d29e6 |
No files matched your search
Generated
+1
-1
@@ -1393,7 +1393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ink-notes"
|
||||
version = "0.26.2"
|
||||
version = "0.28.0"
|
||||
dependencies = [
|
||||
"ab_glyph",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ink-notes"
|
||||
version = "0.26.2"
|
||||
version = "0.28.0"
|
||||
edition = "2021"
|
||||
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
|
||||
repository = "https://gitea.pansi.eu/harald/ink-notes"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.28.0" date="2026-10-18">
|
||||
<description>
|
||||
<p>Each notebook remembers whether it flips page by page or scrolls continuously. New notebooks start with fixed pages; the layout can be chosen when creating a notebook and changed later in the menu.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.27.1" date="2026-10-18">
|
||||
<description>
|
||||
<p>The pens in the toolbar are shown as pencils in their own colour, so they are easy to tell apart.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.27.0" date="2026-10-18">
|
||||
<description>
|
||||
<p>Pictures keep their proportions when resized. A lock button in the selection tool switches between proportional and free resizing; pictures start locked, other objects free.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.26.2" date="2026-10-13">
|
||||
<description>
|
||||
<p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p>
|
||||
|
||||
@@ -84,6 +84,21 @@
|
||||
</branding>
|
||||
<content_rating type="oars-1.1" />
|
||||
<releases>
|
||||
<release version="0.28.0" date="2026-10-18">
|
||||
<description>
|
||||
<p>Each notebook remembers whether it flips page by page or scrolls continuously. New notebooks start with fixed pages; the layout can be chosen when creating a notebook and changed later in the menu.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.27.1" date="2026-10-18">
|
||||
<description>
|
||||
<p>The pens in the toolbar are shown as pencils in their own colour, so they are easy to tell apart.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.27.0" date="2026-10-18">
|
||||
<description>
|
||||
<p>Pictures keep their proportions when resized. A lock button in the selection tool switches between proportional and free resizing; pictures start locked, other objects free.</p>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.26.2" date="2026-10-13">
|
||||
<description>
|
||||
<p>Smoother scrolling through large imported PDFs: pages no longer stall the display while they are prepared, and pages scrolled past quickly are skipped.</p>
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
|
||||
|
||||
### Highlights:
|
||||
- **Seitenanordnung je Notizbuch**: Beim Anlegen wählbar – „Feste Seiten“ (Vorgabe) oder „Durchgehend“ –, später im ☰-Menü umschaltbar. Bestehende Notizbücher behalten die bisherige Einstellung
|
||||
- **Stifte in ihrer Farbe**: Die Stifte in der Werkzeugleiste erscheinen als Stifte in ihrer eigenen Farbe statt als dünner Strich und sind so auf einen Blick zu unterscheiden
|
||||
- **Bilder ohne Verzerren skalieren**: Beim Auswahlwerkzeug schaltet ein Schloss unter den Stiftfarben zwischen gleichmäßigem (🔒) und freiem Skalieren (🔓) um. Bilder sind zu Beginn gesperrt, Striche und Formen frei; am Rechner sperrt zusätzlich die Umschalttaste
|
||||
- **Flüssigeres Scrollen in großen PDFs**: Neu gerenderte PDF-Seiten halten die Anzeige nicht mehr an, und Seiten, über die man schnell hinwegscrollt, werden übersprungen statt der Reihe nach gerendert
|
||||
- **Screenshots für Flathub**: Die App-Beschreibung (Metainfo) zeigt jetzt drei Screenshots; an der App selbst ändert sich nichts
|
||||
- **Auswählen mit dem Finger**: Im Auswahlwerkzeug wählt und verschiebt ein Finger Objekte (auf leerer Fläche scrollt er weiter); Text und Platz schaffen gehen ebenfalls mit dem Finger
|
||||
|
||||
+8
-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,11 @@ 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}"),
|
||||
("Server-Einstellungen gespeichert", "Server settings saved"),
|
||||
("Server:", "Server:"),
|
||||
@@ -416,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"),
|
||||
|
||||
+35
-23
@@ -31,12 +31,12 @@ use ink_notes::osk::{self, KeyboardRequester};
|
||||
use ink_notes::pdf_doc::PdfRenderService;
|
||||
use ink_notes::platform::{self, DisplayBackend};
|
||||
use ink_notes::selection::{
|
||||
render_selection_overlay, DragMode, SelectionHandleHit, SelectionState,
|
||||
render_selection_overlay, scale_factors, DragMode, SelectionHandleHit, SelectionState,
|
||||
};
|
||||
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;
|
||||
@@ -649,12 +649,18 @@ fn selection_press(
|
||||
..
|
||||
},
|
||||
_,
|
||||
) => DragMode::Scale {
|
||||
fixed_corner_page,
|
||||
start_pos_page,
|
||||
initial_box_size,
|
||||
initial_strokes: page.get_strokes(&selection_state.selected_ids),
|
||||
},
|
||||
) => {
|
||||
let initial_strokes = page.get_strokes(&selection_state.selected_ids);
|
||||
// Schloss der Auswahl (Bilder: an, Issue #29); Shift sperrt zusätzlich
|
||||
let keep_aspect = shift || selection_state.keeps_aspect(page);
|
||||
DragMode::Scale {
|
||||
fixed_corner_page,
|
||||
start_pos_page,
|
||||
initial_box_size,
|
||||
initial_strokes,
|
||||
keep_aspect,
|
||||
}
|
||||
}
|
||||
(SelectionHandleHit::Body, Some(p)) => DragMode::Move {
|
||||
start_page: p,
|
||||
initial_strokes: page.get_strokes(&selection_state.selected_ids),
|
||||
@@ -729,18 +735,10 @@ fn selection_drag(
|
||||
DragMode::Scale {
|
||||
fixed_corner_page,
|
||||
start_pos_page,
|
||||
keep_aspect,
|
||||
..
|
||||
} => {
|
||||
let orig = *start_pos_page - *fixed_corner_page;
|
||||
let new = page_pt - *fixed_corner_page;
|
||||
let factor = |n: f32, o: f32| {
|
||||
if o.abs() > 1.0 {
|
||||
(n / o).clamp(0.05, 20.0)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
};
|
||||
let factors = Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y));
|
||||
let factors = scale_factors(*fixed_corner_page, *start_pos_page, page_pt, *keep_aspect);
|
||||
updated
|
||||
.iter_mut()
|
||||
.for_each(|s| s.scale(*fixed_corner_page, factors));
|
||||
@@ -1524,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();
|
||||
@@ -3103,6 +3104,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 3. Toolbar & Widgets rendern
|
||||
updater.update();
|
||||
ui_state.page_view = Some(page_view(&canvas, window.scale_factor() as f32));
|
||||
ui_state.selection_aspect_lock = (!selection_state.is_empty())
|
||||
.then(|| selection_state.keeps_aspect(notebook.current_page()));
|
||||
ui_state.restorable_page = last_deleted_page
|
||||
.as_ref()
|
||||
.filter(|(id, _, _)| *id == manifest.id)
|
||||
@@ -3203,6 +3206,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
update_window_title(&window, ¬ebook, &manifest, ui_state.active_tool, autosave.status());
|
||||
window.request_redraw();
|
||||
}
|
||||
UiAction::ToggleAspectLock => {
|
||||
if !selection_state.is_empty() {
|
||||
selection_state.toggle_aspect_lock(notebook.current_page());
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
UiAction::RotateSelected => {
|
||||
if !selection_state.is_empty()
|
||||
&& selection_state.rotate_selected_90_deg(notebook.current_page_mut()) {
|
||||
@@ -3556,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)
|
||||
});
|
||||
@@ -3570,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();
|
||||
@@ -3814,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 => {
|
||||
|
||||
@@ -36,9 +36,34 @@ pub enum DragMode {
|
||||
start_pos_page: Vec2,
|
||||
initial_box_size: Vec2,
|
||||
initial_strokes: Vec<Stroke>,
|
||||
/// Seitenverhältnis beibehalten (Schloss der Auswahl oder Shift gedrückt)
|
||||
keep_aspect: bool,
|
||||
},
|
||||
}
|
||||
|
||||
/// Skalierungsfaktoren, wenn der Eckgriff von `start` (relativ zur festen Ecke `fixed`)
|
||||
/// nach `current` gezogen wird. Mit `keep_aspect` wird gleichmäßig skaliert: der Faktor ist
|
||||
/// die Projektion der Zugposition auf die Diagonale, damit das Seitenverhältnis bleibt.
|
||||
pub fn scale_factors(fixed: Vec2, start: Vec2, current: Vec2, keep_aspect: bool) -> Vec2 {
|
||||
let orig = start - fixed;
|
||||
let new = current - fixed;
|
||||
if keep_aspect {
|
||||
let len_sq = orig.length_squared();
|
||||
if len_sq <= 1.0 {
|
||||
return Vec2::ONE;
|
||||
}
|
||||
return Vec2::splat((new.dot(orig) / len_sq).clamp(0.05, 20.0));
|
||||
}
|
||||
let factor = |n: f32, o: f32| {
|
||||
if o.abs() > 1.0 {
|
||||
(n / o).clamp(0.05, 20.0)
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
};
|
||||
Vec2::new(factor(new.x, orig.x), factor(new.y, orig.y))
|
||||
}
|
||||
|
||||
/// Ergebnis eines Hit-Tests auf Auswahl-Griffe (Handles).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SelectionHandleHit {
|
||||
@@ -64,6 +89,9 @@ pub struct SelectionState {
|
||||
pub selected_ids: Vec<u64>,
|
||||
pub drag_mode: DragMode,
|
||||
pub clipboard: Vec<Stroke>,
|
||||
/// Von Hand umgeschaltetes Seitenverhältnis-Schloss und die Auswahl (IDs), für die es
|
||||
/// gilt. Ändert sich die Auswahl, gilt wieder die Vorgabe (siehe [`Self::keeps_aspect`]).
|
||||
aspect_lock: Option<(Vec<u64>, bool)>,
|
||||
}
|
||||
|
||||
impl SelectionState {
|
||||
@@ -72,9 +100,28 @@ impl SelectionState {
|
||||
selected_ids: Vec::new(),
|
||||
drag_mode: DragMode::None,
|
||||
clipboard: Vec::new(),
|
||||
aspect_lock: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Ob Skalieren an den Eckgriffen das Seitenverhältnis der Auswahl beibehält. Vorgabe:
|
||||
/// ja, sobald ein Bild ausgewählt ist (Issue #29); per Knopf für diese Auswahl umschaltbar.
|
||||
pub fn keeps_aspect(&self, page: &Page) -> bool {
|
||||
match &self.aspect_lock {
|
||||
Some((ids, locked)) if *ids == self.selected_ids => *locked,
|
||||
_ => page
|
||||
.get_strokes(&self.selected_ids)
|
||||
.iter()
|
||||
.any(|s| s.is_image()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Schaltet das Seitenverhältnis-Schloss der aktuellen Auswahl um.
|
||||
pub fn toggle_aspect_lock(&mut self, page: &Page) {
|
||||
let locked = !self.keeps_aspect(page);
|
||||
self.aspect_lock = Some((self.selected_ids.clone(), locked));
|
||||
}
|
||||
|
||||
/// Prüft, ob aktuell Objekte ausgewählt sind.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.selected_ids.is_empty()
|
||||
@@ -523,6 +570,77 @@ mod tests {
|
||||
assert!((orig_pos.x - restored_pos.x).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scale_factors_free_and_keep_aspect() {
|
||||
let fixed = Vec2::new(0.0, 0.0);
|
||||
let start = Vec2::new(200.0, 100.0);
|
||||
// Frei: Breite und Höhe unabhängig
|
||||
let free = scale_factors(fixed, start, Vec2::new(400.0, 100.0), false);
|
||||
assert_eq!(free, Vec2::new(2.0, 1.0));
|
||||
// Seitenverhältnis fest: gleicher Faktor in beiden Richtungen
|
||||
let locked = scale_factors(fixed, start, Vec2::new(400.0, 100.0), true);
|
||||
assert!((locked.x - locked.y).abs() < 1e-6);
|
||||
assert!((locked.x - 1.8).abs() < 1e-4);
|
||||
// Entlang der Diagonale genau verdoppelt
|
||||
let diag = scale_factors(fixed, start, Vec2::new(400.0, 200.0), true);
|
||||
assert!((diag - Vec2::splat(2.0)).length() < 1e-5);
|
||||
// Über die feste Ecke hinaus: kein Spiegeln
|
||||
let past = scale_factors(fixed, start, Vec2::new(-50.0, -50.0), true);
|
||||
assert_eq!(past, Vec2::splat(0.05));
|
||||
// Senkrechte Linie (Breite 0): frei bleibt x unverändert, fest skaliert beides
|
||||
let line = Vec2::new(0.0, 100.0);
|
||||
assert_eq!(
|
||||
scale_factors(fixed, line, Vec2::new(30.0, 200.0), false),
|
||||
Vec2::new(1.0, 2.0)
|
||||
);
|
||||
assert_eq!(
|
||||
scale_factors(fixed, line, Vec2::new(30.0, 200.0), true),
|
||||
Vec2::splat(2.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aspect_lock_defaults_to_images_and_toggles_per_selection() {
|
||||
let mut page = Page::new(1, PageBackground::Blank);
|
||||
let corners = [
|
||||
Vec2::new(0.0, 0.0),
|
||||
Vec2::new(100.0, 0.0),
|
||||
Vec2::new(100.0, 50.0),
|
||||
Vec2::new(0.0, 50.0),
|
||||
];
|
||||
page.add_stroke(Stroke::new_image(1, "a.png".into(), corners));
|
||||
page.add_stroke(make_test_stroke(2, 200.0, 200.0, [0.1, 0.1, 0.1, 1.0]));
|
||||
let mut sel = SelectionState::new();
|
||||
|
||||
// Vorgabe: Bilder gesperrt, Striche frei
|
||||
sel.select_single(1);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(2);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
|
||||
// Umschalten gilt für diese Auswahl
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(1);
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
sel.toggle_aspect_lock(&page);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.toggle_aspect_lock(&page);
|
||||
|
||||
// Neue Auswahl: wieder die Vorgabe
|
||||
sel.select_multiple(vec![1, 2]);
|
||||
assert!(sel.keeps_aspect(&page));
|
||||
sel.select_single(1);
|
||||
assert!(
|
||||
!sel.keeps_aspect(&page),
|
||||
"gleiche Auswahl behält das Umschalten"
|
||||
);
|
||||
sel.clear();
|
||||
sel.select_single(1);
|
||||
assert!(!sel.keeps_aspect(&page));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_recolor_ignores_images() {
|
||||
let mut page = Page::new(1, PageBackground::Blank);
|
||||
|
||||
+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};
|
||||
|
||||
@@ -255,12 +255,15 @@ pub enum UiAction {
|
||||
/// Zwischenablage an dieser Stelle der aktuellen Seite einfügen (Kontextmenü)
|
||||
PasteAt(glam::Vec2),
|
||||
RotateSelected,
|
||||
/// Seitenverhältnis beim Skalieren der Auswahl sperren bzw. freigeben
|
||||
ToggleAspectLock,
|
||||
// Dokumenten- & Notizbuch-Verwaltung
|
||||
OpenNotebook(String),
|
||||
/// Neues Notizbuch im Ordner `folder` anlegen und öffnen
|
||||
CreateNotebook {
|
||||
title: String,
|
||||
folder: String,
|
||||
layout: NotebookLayout,
|
||||
},
|
||||
/// Ordnerverwaltung (siehe `library.rs`)
|
||||
CreateFolder {
|
||||
@@ -315,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
|
||||
@@ -362,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,
|
||||
@@ -410,6 +415,8 @@ pub struct UiState {
|
||||
pub confirm_delete_page: bool,
|
||||
/// Seitenzahl der zuletzt gelöschten Seite, solange sie wiederhergestellt werden kann
|
||||
pub restorable_page: Option<usize>,
|
||||
/// Seitenverhältnis-Schloss der Auswahl (`None`: nichts ausgewählt)
|
||||
pub selection_aspect_lock: Option<bool>,
|
||||
/// Fehler beim Flushen vor dem Beenden
|
||||
pub shutdown_error: Option<crate::autosave::AutosaveError>,
|
||||
/// Fensterseite der Werkzeugleiste
|
||||
@@ -479,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(),
|
||||
@@ -508,6 +516,7 @@ impl Default for UiState {
|
||||
pressed_stylus_button: None,
|
||||
confirm_delete_page: false,
|
||||
restorable_page: None,
|
||||
selection_aspect_lock: None,
|
||||
shutdown_error: None,
|
||||
sidebar_side: SidebarSide::Left,
|
||||
language: None,
|
||||
@@ -1182,8 +1191,8 @@ fn color_editor(
|
||||
changed
|
||||
}
|
||||
|
||||
/// Stift der Werkzeugleiste: ein Strich in Farbe und (angedeuteter) Stärke auf einem
|
||||
/// Stück Papier, damit auch schwarze Tinte auf der dunklen Leiste sichtbar ist.
|
||||
/// Stift der Werkzeugleiste: ein schräg liegender Stift in seiner Farbe; die Stärke steht
|
||||
/// im Tooltip.
|
||||
fn pen_preset_swatch(
|
||||
ui: &mut egui::Ui,
|
||||
preset: PenPreset,
|
||||
@@ -1193,22 +1202,49 @@ fn pen_preset_swatch(
|
||||
) -> egui::Response {
|
||||
let (rect, response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::click());
|
||||
if ui.is_rect_visible(rect) {
|
||||
let paper = rect.shrink(2.0);
|
||||
ui.painter().rect_filled(
|
||||
paper,
|
||||
egui::Rounding::same(5.0),
|
||||
egui::Color32::from_gray(245),
|
||||
);
|
||||
// Schräg liegender Stift ganz in seiner Farbe, damit die Stifte auf einen Blick zu
|
||||
// unterscheiden sind (Issue #31); die helle Kontur hält dunkle Farben sichtbar
|
||||
let [r, g, b, _] = preset.color.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8);
|
||||
let width = (1.0 + preset.width_mm * 2.5).clamp(1.5, paper.height() * 0.35);
|
||||
let inset = 3.0 + width / 2.0;
|
||||
ui.painter().line_segment(
|
||||
[
|
||||
egui::pos2(paper.left() + inset, paper.bottom() - inset),
|
||||
egui::pos2(paper.right() - inset, paper.top() + inset),
|
||||
let ink = egui::Color32::from_rgb(r, g, b);
|
||||
let c = rect.center();
|
||||
let dir = egui::vec2(1.0, -1.0).normalized();
|
||||
let perp = egui::vec2(dir.y, -dir.x);
|
||||
let half_len = size * 0.44;
|
||||
let half_w = size * 0.17;
|
||||
let p = |along: f32, across: f32| c + dir * along + perp * across;
|
||||
let tip_start = -half_len * 0.3;
|
||||
let lead_start = -half_len * 0.72;
|
||||
let lead_w = half_w * (lead_start + half_len) / (tip_start + half_len);
|
||||
let outline = egui::Stroke::new(1.0_f32, egui::Color32::from_gray(200));
|
||||
let painter = ui.painter();
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![
|
||||
p(tip_start, -half_w),
|
||||
p(half_len, -half_w),
|
||||
p(half_len, half_w),
|
||||
p(tip_start, half_w),
|
||||
],
|
||||
egui::Stroke::new(width, egui::Color32::from_rgb(r, g, b)),
|
||||
);
|
||||
ink,
|
||||
outline,
|
||||
));
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![
|
||||
p(-half_len, 0.0),
|
||||
p(tip_start, -half_w),
|
||||
p(tip_start, half_w),
|
||||
],
|
||||
egui::Color32::from_rgb(235, 210, 170),
|
||||
outline,
|
||||
));
|
||||
painter.add(egui::Shape::convex_polygon(
|
||||
vec![
|
||||
p(-half_len, 0.0),
|
||||
p(lead_start, -lead_w),
|
||||
p(lead_start, lead_w),
|
||||
],
|
||||
ink,
|
||||
egui::Stroke::NONE,
|
||||
));
|
||||
if is_selected {
|
||||
ui.painter().rect_stroke(
|
||||
rect.shrink(0.5),
|
||||
@@ -1609,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(
|
||||
@@ -3045,6 +3113,30 @@ pub fn render_ui(
|
||||
used += btn + spacing;
|
||||
}
|
||||
}
|
||||
// Auswahl: unter den 4 Stiftfarben ist Platz für das Schloss des
|
||||
// Seitenverhältnisses beim Skalieren (Bilder: gesperrt, Issue #29)
|
||||
if state.active_tool == ActiveTool::Select {
|
||||
let locked = state.selection_aspect_lock;
|
||||
let (icon, tooltip) = if locked == Some(true) {
|
||||
("🔒", t!("Seitenverhältnis gesperrt – tippen zum Freigeben"))
|
||||
} else {
|
||||
("🔓", t!("Seitenverhältnis frei – tippen zum Sperren"))
|
||||
};
|
||||
if sidebar_button(
|
||||
ui,
|
||||
swatch,
|
||||
SidebarIcon::Glyph(icon),
|
||||
locked == Some(true),
|
||||
locked.is_some(),
|
||||
None,
|
||||
tooltip,
|
||||
)
|
||||
.clicked()
|
||||
{
|
||||
actions.push(UiAction::ToggleAspectLock);
|
||||
}
|
||||
used += swatch + spacing;
|
||||
}
|
||||
let reserved =
|
||||
UiState::MAX_PALETTE_LEN as f32 * (swatch + spacing);
|
||||
if reserved > used {
|
||||
@@ -3697,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"));
|
||||
@@ -4042,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() {
|
||||
@@ -4053,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();
|
||||
@@ -5189,6 +5284,35 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_aspect_lock_button_only_with_selection() {
|
||||
// Den Knopf in der Leiste des Auswahlwerkzeugs suchen: ohne Auswahl gesperrt
|
||||
let notebook = Notebook::new();
|
||||
let toggles = |lock: Option<bool>| {
|
||||
let mut probe = UiState::new();
|
||||
probe.active_tool = ActiveTool::Select;
|
||||
render_frames(&mut probe, ¬ebook, 10);
|
||||
let bar = probe.toolbar_rect.unwrap();
|
||||
let mut found = 0;
|
||||
let mut y = bar.min.y + 4.0;
|
||||
while y < bar.max.y {
|
||||
let mut state = UiState::new();
|
||||
state.active_tool = ActiveTool::Select;
|
||||
state.selection_aspect_lock = lock;
|
||||
let ctx = render_frames(&mut state, ¬ebook, 10);
|
||||
let actions = click_at(&ctx, &mut state, ¬ebook, egui::pos2(bar.center().x, y));
|
||||
if actions.contains(&UiAction::ToggleAspectLock) {
|
||||
found += 1;
|
||||
}
|
||||
y += 4.0;
|
||||
}
|
||||
found
|
||||
};
|
||||
assert_eq!(toggles(None), 0);
|
||||
assert!(toggles(Some(true)) > 0);
|
||||
assert!(toggles(Some(false)) > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sidebar_fits_window_height_for_every_tool() {
|
||||
let notebook = Notebook::new();
|
||||
|
||||
Reference in new issue
Block a user