Compare commits

...
10 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 cbf699b680 chore(release): bump version to 0.26.2
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m41s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:26:42 +02:00
haraldandClaude Opus 5.5 ade20ab3dc perf: Veraltete PDF-Render-Aufträge beim Scrollen verwerfen (Issue #28)
Beim schnellen Scrollen durch ein großes PDF hat der Render-Thread jede
vorbeigezogene Seite der Reihe nach gerendert; die gerade sichtbare
Seite musste warten.

- Gemeinsame Warteschlange (Mutex + Condvar) statt Kanal: Der
  Render-Thread nimmt den wichtigsten wartenden Auftrag zuerst.
- Je Bild meldet der Event-Loop zwischen begin_frame und end_frame die
  benötigten Seiten an (die aktuelle zuerst); end_frame verwirft
  wartende Aufträge und fertige Ergebnisse für Seiten, die nicht mehr
  gebraucht werden. Die vorab gerenderte nächste Seite bleibt erhalten.

Gemessen (Xvfb, Debug-Build, PDF mit 60 Seiten, 400 Mausrad-Schritte):
die letzte Seite erscheint nach 0,12–0,16 s statt 0,63–1,56 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:48:31 +02:00
haraldandClaude Opus 5.5 f24e7750e6 perf: Mipmaps importierter PDF-Seiten im Render-Thread (Issue #28)
Beim Scrollen durch große PDFs stockte die Anzeige bei jeder neu
gerenderten Seite: Der Haupt-Thread erzeugte die Mipmaps, bei 2560 px
etwa 100–200 ms, beim Hineinzoomen (5120 px) bis zu 1 s.

- Der PDF-Render-Thread liefert die fertigen Mipmap-Stufen mit; der
  Haupt-Thread lädt sie nur noch hoch (TextureSource::ReadyMips).
- mip_chain halbiert mit einem 2×2-Mittelwert statt imageops::resize:
  etwa 7-mal schneller (2560 px: ~15 ms statt ~100–200 ms).
- Belegter Grafikspeicher wird aus den tatsächlichen Stufen berechnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:22:21 +02:00
haraldandClaude Opus 5.5 097933882c chore(flatpak): ideas.txt und issue.txt nicht mehr überspringen
Die Dateien gibt es nicht mehr.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 19:28:35 +02:00
haraldandClaude Opus 5.5 b3578175c2 chore(release): bump version to 0.26.1
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m25s
Screenshot-URLs im Metainfo zeigen auf das Release-Tag statt auf main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:29:56 +02:00
haraldandClaude Opus 5.5 1cb02bf8e9 docs: Screenshot einer beschrifteten PDF-Folie für Flathub
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:20:31 +02:00
haraldandClaude Opus 5.5 26c4191e0f docs: Screenshots für Flathub
- packaging/screenshots/notes.png: Notizen mit Textmarker, Handschrift
  und Textfeldern
- packaging/screenshots/shapes.png: erkannte Formen (Achsenkreuz,
  Dreieck, Kreis, Oktaeder, Sechseck, Ellipse)
- Metainfo: Screenshots mit englischer und deutscher Beschriftung

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:16:01 +02:00
haraldandClaude Opus 5.5 7d683d6e67 chore(release): bump version to 0.26.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 8m16s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:20:31 +02:00
haraldandClaude Opus 5.5 e399c357d8 feat: Flatpak-Paket (Vorbereitung für Flathub)
- packaging/flatpak/eu.pansi.ink-notes.yml: Runtime freedesktop 26.08,
  Rust aus rust-stable, Abhängigkeiten offline aus cargo-sources.json.
  Rechte: Wayland/X11, GPU, Lesen von Bildern/Downloads/Schreibtisch,
  Schreiben in den Dokumente-Ordner (kein Zugriff auf den ganzen
  Home-Ordner).
- Im Flatpak sucht und installiert ink-notes keine Updates selbst
  (Vorgabe von Flathub); der Update-Dialog verweist auf die
  Software-Verwaltung.
- Export-Ordner aus user-dirs.dirs (z. B. ~/Dokumente) statt fest
  ~/Documents; nur dieser Ordner ist im Flatpak beschreibbar.
- Metainfo und Desktop-Datei auf Englisch mit deutscher Übersetzung,
  Release-Einträge, unterstützte Eingabegeräte.
- Test: cargo-sources.json deckt Cargo.lock ab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:18:37 +02:00
haraldandClaude Opus 5.5 dd702524ca fix: Auswählen, Text und Platz schaffen mit dem Finger (Issue #26)
Ohne „Mit dem Finger zeichnen“ verschob ein Finger immer das Blatt, auch
im Auswahlwerkzeug – Objekte ließen sich mit dem Finger weder auswählen
noch verschieben. Jetzt nehmen Text und Platz schaffen den Finger immer,
das Auswahlwerkzeug, wenn der Finger ein Objekt oder die Auswahl trifft;
auf leerer Fläche verschiebt er weiter das Blatt. Halten öffnet dabei
kein Kontextmenü.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:12:25 +02:00
21 changed files with 6176 additions and 116 deletions
Generated
+1 -1
View File
@@ -1393,7 +1393,7 @@ dependencies = [
[[package]] [[package]]
name = "ink-notes" name = "ink-notes"
version = "0.25.0" version = "0.26.2"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"arboard", "arboard",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "ink-notes" name = "ink-notes"
version = "0.25.0" version = "0.26.2"
edition = "2021" edition = "2021"
description = "Notizbuch-App für Stift, Touch und Maus unter Linux" description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes" repository = "https://gitea.pansi.eu/harald/ink-notes"
+12
View File
@@ -74,6 +74,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
### Updates ### Updates
- Die App prüft auf neue Versionen auf Gitea und installiert sie auf Wunsch. Installiert wird nur, wenn die Minisign-Signatur und die SHA-256-Prüfsumme stimmen (Details unten). - Die App prüft auf neue Versionen auf Gitea und installiert sie auf Wunsch. Installiert wird nur, wenn die Minisign-Signatur und die SHA-256-Prüfsumme stimmen (Details unten).
- Als Flatpak sucht ink-notes nicht selbst nach Updates; die kommen dann über die Software-Verwaltung (`flatpak update`).
--- ---
@@ -190,6 +191,17 @@ Mehr dazu in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), das Speicherformat
Veröffentlicht wird über Gitea Actions ([`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml)), sobald ein Tag `vX.Y.Z` gepusht wird: Tests, Pakete bauen, signieren, Release anlegen. Manuell geht es mit `GITEA_TOKEN=… ./scripts/publish-gitea-release.sh`. Veröffentlicht wird über Gitea Actions ([`.gitea/workflows/release.yaml`](.gitea/workflows/release.yaml)), sobald ein Tag `vX.Y.Z` gepusht wird: Tests, Pakete bauen, signieren, Release anlegen. Manuell geht es mit `GITEA_TOKEN=… ./scripts/publish-gitea-release.sh`.
### Flatpak
Das Manifest liegt in [`packaging/flatpak/`](packaging/flatpak/) (Runtime `org.freedesktop.Platform` 26.08, Rust aus der SDK-Erweiterung `rust-stable`). Lokal bauen und installieren:
```bash
flatpak-builder --user --install --force-clean target/flatpak-build packaging/flatpak/eu.pansi.ink-notes.yml
flatpak run eu.pansi.ink-notes
```
Nach jeder Änderung an `Cargo.lock` muss `packaging/flatpak/cargo-sources.json` mit dem [flatpak-cargo-generator](https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo) neu erzeugt werden (`python3 flatpak-cargo-generator.py Cargo.lock -o packaging/flatpak/cargo-sources.json`). In der Sandbox darf ink-notes Bilder, Downloads und Schreibtisch lesen und in den Dokumente-Ordner exportieren; die Notizbücher liegen unter `~/.var/app/eu.pansi.ink-notes/`.
### Release-Signatur einrichten (einmalig) ### Release-Signatur einrichten (einmalig)
Updates werden in der App nur mit gültiger Minisign-Signatur installiert. Signiert wird mit [rsign2](https://crates.io/crates/rsign2) (`cargo install rsign2 --locked`). Schlüsselpaar ohne Passwort erzeugen (es wird nur als CI-Secret verwendet): Updates werden in der App nur mit gültiger Minisign-Signatur installiert. Signiert wird mit [rsign2](https://crates.io/crates/rsign2) (`cargo install rsign2 --locked`). Schlüsselpaar ohne Passwort erzeugen (es wird nur als CI-Secret verwendet):
+9 -5
View File
@@ -1,18 +1,22 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=ink-notes Name=ink-notes
GenericName=Notizbuch- & Inking-App GenericName=Note-Taking & Inking App
Comment=Handschriftliche Notizen mit Stift, Touch und Maus; PDFs beschriften GenericName[de]=Notizbuch- & Inking-App
Comment=Handwritten notes with pen, touch and mouse; annotate PDFs
Comment[de]=Handschriftliche Notizen mit Stift, Touch und Maus; PDFs beschriften
Exec=ink-notes %F Exec=ink-notes %F
Icon=eu.pansi.ink-notes Icon=eu.pansi.ink-notes
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Office;Graphics;Utility; Categories=Office;Graphics;
Keywords=Note;Notiz;Pen;Stift;Ink;Inking;Stylus;Drawing;Zeichnen;PDF;Vector;Text;Annotation; Keywords=Note;Pen;Ink;Inking;Stylus;Drawing;PDF;Vector;Text;Annotation;
Keywords[de]=Notiz;Stift;Ink;Inking;Stylus;Zeichnen;PDF;Vektor;Text;Annotation;
StartupWMClass=eu.pansi.ink-notes StartupWMClass=eu.pansi.ink-notes
MimeType=application/x-inknotes; MimeType=application/x-inknotes;
Actions=NewWindow; Actions=NewWindow;
[Desktop Action NewWindow] [Desktop Action NewWindow]
Name=Neues Fenster Name=New Window
Name[de]=Neues Fenster
Exec=ink-notes Exec=ink-notes
+91 -13
View File
@@ -4,22 +4,34 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license> <project_license>MIT</project_license>
<name>ink-notes</name> <name>ink-notes</name>
<summary>Notizbuch-App für Stift, Touch und Maus</summary> <summary>Handwritten notes with pen, touch and mouse</summary>
<summary xml:lang="de">Notizbuch-App für Stift, Touch und Maus</summary>
<description> <description>
<p> <p>
ink-notes ist eine Notizbuch-App für Linux, geschrieben in Rust mit wgpu. Handschrift wird als ink-notes is a notebook app for Linux tablets and convertibles. Handwriting is stored as
vectors; write with a pen, your finger, a touchpad or a mouse.
</p>
<p xml:lang="de">
ink-notes ist eine Notizbuch-App für Linux-Tablets und Convertibles. Handschrift wird als
Vektoren gespeichert; geschrieben wird mit Stift, Finger, Touchpad oder Maus. Vektoren gespeichert; geschrieben wird mit Stift, Finger, Touchpad oder Maus.
</p> </p>
<p>Funktionen:</p> <p>Features:</p>
<p xml:lang="de">Funktionen:</p>
<ul> <ul>
<li>Stift und Textmarker mit Druckstärke, Glättung beim Schreiben und Ecken-Erhaltung</li> <li>Pens and highlighter with pressure, smoothing while writing and sharp corners</li>
<li>Radierer für ganze Striche, Teilstriche oder nur Textmarker; virtuelles Lineal</li> <li xml:lang="de">Stifte und Textmarker mit Druckstärke, Glättung beim Schreiben und Ecken-Erhaltung</li>
<li>Formerkennung (Linien, Pfeile, Kreise, Ellipsen, Rechtecke, Dreiecke, Rauten, Koordinatensysteme)</li> <li>Shape recognition: hold at the end of a stroke to get clean lines, arrows, circles, rectangles, triangles and coordinate systems</li>
<li>Auswählen, Verschieben, Drehen, Skalieren und Umfärben; Rückgängig für alle Änderungen</li> <li xml:lang="de">Formerkennung: am Strichende halten für saubere Linien, Pfeile, Kreise, Rechtecke, Dreiecke und Koordinatensysteme</li>
<li>Textfelder, Bilder und PDF-Import zum Beschriften (Querformat-Folien bekommen Querformat-Seiten)</li> <li>Eraser for whole strokes, partial strokes or highlighter only; virtual ruler</li>
<li>Durchgehendes Scrollen, Zoom von 25 bis 1000 Prozent, Papier- und Musterfarben</li> <li xml:lang="de">Radierer für ganze Striche, Teilstriche oder nur Textmarker; virtuelles Lineal</li>
<li>Mehrere Notizbücher, automatisches Speichern im Hintergrund</li> <li>Select, move, rotate, scale and recolor; undo for every change</li>
<li>Export als PDF (mit durchsuchbarem Text), PNG oder SVG</li> <li xml:lang="de">Auswählen, Verschieben, Drehen, Skalieren und Umfärben; Rückgängig für alle Änderungen</li>
<li>Text boxes, images and PDF import to annotate lecture slides and worksheets</li>
<li xml:lang="de">Textfelder, Bilder und PDF-Import zum Beschriften von Vorlesungsfolien und Übungsblättern</li>
<li>Notebooks in folders, continuous scrolling, automatic saving in the background</li>
<li xml:lang="de">Notizbücher in Ordnern, durchgehendes Scrollen, automatisches Speichern im Hintergrund</li>
<li>Export as PDF (with searchable text), PNG or SVG</li>
<li xml:lang="de">Export als PDF (mit durchsuchbarem Text), PNG oder SVG</li>
</ul> </ul>
</description> </description>
<launchable type="desktop-id">eu.pansi.ink-notes.desktop</launchable> <launchable type="desktop-id">eu.pansi.ink-notes.desktop</launchable>
@@ -29,12 +41,78 @@
<categories> <categories>
<category>Office</category> <category>Office</category>
<category>Graphics</category> <category>Graphics</category>
<category>Utility</category>
</categories> </categories>
<keywords>
<keyword>notes</keyword>
<keyword>handwriting</keyword>
<keyword>stylus</keyword>
<keyword>pen</keyword>
<keyword>PDF</keyword>
<keyword>annotate</keyword>
</keywords>
<supports>
<control>pointing</control>
<control>touch</control>
<control>tablet</control>
</supports>
<developer id="eu.pansi"> <developer id="eu.pansi">
<name>Harald</name> <name>Harald Pansi</name>
</developer> </developer>
<url type="homepage">https://gitea.pansi.eu/harald/ink-notes</url> <url type="homepage">https://gitea.pansi.eu/harald/ink-notes</url>
<url type="bugtracker">https://gitea.pansi.eu/harald/ink-notes/issues</url> <url type="bugtracker">https://gitea.pansi.eu/harald/ink-notes/issues</url>
<url type="vcs-browser">https://gitea.pansi.eu/harald/ink-notes</url>
<screenshots>
<screenshot type="default">
<caption>Notes with highlighter, handwriting and text boxes</caption>
<caption xml:lang="de">Notizen mit Textmarker, Handschrift und Textfeldern</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/notes.png</image>
</screenshot>
<screenshot>
<caption>Recognized shapes: coordinate system, triangle, circle, octahedron</caption>
<caption xml:lang="de">Erkannte Formen: Koordinatensystem, Dreieck, Kreis, Oktaeder</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/shapes.png</image>
</screenshot>
<screenshot>
<caption>Annotating imported PDF slides</caption>
<caption xml:lang="de">Importierte PDF-Folien beschriften</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/pdf.png</image>
</screenshot>
</screenshots>
<branding>
<color type="primary" scheme_preference="light">#a8c8f0</color>
<color type="primary" scheme_preference="dark">#1f3a5f</color>
</branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases>
<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>
</description>
</release>
<release version="0.26.1" date="2026-10-10">
<description>
<p>Screenshots in the app description for software centers. The app itself is unchanged.</p>
</description>
</release>
<release version="0.26.0" date="2026-10-10">
<description>
<p>Select and move objects with a finger; text and make-space tools also work with a finger. Exports go to the documents folder of the user directories.</p>
</description>
</release>
<release version="0.25.0" date="2026-10-09">
<description>
<p>English user interface; the language follows the system and can be changed in the menu. Holding to snap a shape tolerates a slightly trembling pen.</p>
</description>
</release>
<release version="0.24.0" date="2026-10-09">
<description>
<p>Axis cross from two crossing strokes.</p>
</description>
</release>
<release version="0.23.0" date="2026-10-09">
<description>
<p>The on-screen keyboard of GNOME appears when tapping a text field.</p>
</description>
</release>
</releases>
</component> </component>
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
# Flatpak-Manifest für ink-notes (Flathub).
#
# Lokal bauen und installieren (aus dem Repository-Wurzelverzeichnis):
# flatpak-builder --user --install --force-clean target/flatpak-build \
# packaging/flatpak/eu.pansi.ink-notes.yml
#
# cargo-sources.json nach jeder Änderung an Cargo.lock neu erzeugen:
# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/flatpak/cargo-sources.json
# (flatpak-builder-tools, https://github.com/flatpak/flatpak-builder-tools/tree/master/cargo)
#
# Für die Einreichung bei Flathub die Quelle `type: dir` durch das Release-Tag ersetzen
# (type: git, url, tag, commit).
id: eu.pansi.ink-notes
runtime: org.freedesktop.Platform
runtime-version: '26.08'
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
command: ink-notes
finish-args:
- --socket=wayland
- --socket=fallback-x11
- --share=ipc
- --device=dri
# Die eingebaute Dateiauswahl (Bilder und PDFs einfügen) arbeitet ohne Desktop-Portal und
# bietet Bilder, Downloads und Schreibtisch an; Export (PDF, PNG, SVG) in den Dokumente-Ordner.
# Ziehen aus dem Dateimanager geht über das Dokumenten-Portal auch aus anderen Ordnern.
- --filesystem=xdg-pictures:ro
- --filesystem=xdg-download:ro
- --filesystem=xdg-desktop:ro
- --filesystem=xdg-documents
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
env:
CARGO_HOME: /run/build/ink-notes/cargo
modules:
- name: ink-notes
buildsystem: simple
build-commands:
- cargo --offline build --release --locked
- install -Dm755 target/release/ink-notes /app/bin/ink-notes
- install -Dm644 packaging/eu.pansi.ink-notes.desktop /app/share/applications/eu.pansi.ink-notes.desktop
- install -Dm644 packaging/eu.pansi.ink-notes.metainfo.xml /app/share/metainfo/eu.pansi.ink-notes.metainfo.xml
- install -Dm644 assets/ink-notes.svg /app/share/icons/hicolor/scalable/apps/eu.pansi.ink-notes.svg
- install -Dm644 assets/ink-notes.png /app/share/icons/hicolor/256x256/apps/eu.pansi.ink-notes.png
sources:
- type: dir
path: ../..
skip:
- target
- .git
- .flatpak-builder
- cargo-sources.json
+9 -5
View File
@@ -1,18 +1,22 @@
[Desktop Entry] [Desktop Entry]
Version=1.0 Version=1.0
Name=ink-notes Name=ink-notes
GenericName=Notizbuch- & Inking-App GenericName=Note-Taking & Inking App
Comment=Handschriftliche Notizen mit Stift, Touch und Maus; PDFs beschriften GenericName[de]=Notizbuch- & Inking-App
Comment=Handwritten notes with pen, touch and mouse; annotate PDFs
Comment[de]=Handschriftliche Notizen mit Stift, Touch und Maus; PDFs beschriften
Exec=ink-notes %F Exec=ink-notes %F
Icon=eu.pansi.ink-notes Icon=eu.pansi.ink-notes
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Office;Graphics;Utility; Categories=Office;Graphics;
Keywords=Note;Notiz;Pen;Stift;Ink;Inking;Stylus;Drawing;Zeichnen;PDF;Vector;Text;Annotation; Keywords=Note;Pen;Ink;Inking;Stylus;Drawing;PDF;Vector;Text;Annotation;
Keywords[de]=Notiz;Stift;Ink;Inking;Stylus;Zeichnen;PDF;Vektor;Text;Annotation;
StartupWMClass=eu.pansi.ink-notes StartupWMClass=eu.pansi.ink-notes
MimeType=application/x-inknotes; MimeType=application/x-inknotes;
Actions=NewWindow; Actions=NewWindow;
[Desktop Action NewWindow] [Desktop Action NewWindow]
Name=Neues Fenster Name=New Window
Name[de]=Neues Fenster
Exec=ink-notes Exec=ink-notes
+91 -13
View File
@@ -4,22 +4,34 @@
<metadata_license>CC0-1.0</metadata_license> <metadata_license>CC0-1.0</metadata_license>
<project_license>MIT</project_license> <project_license>MIT</project_license>
<name>ink-notes</name> <name>ink-notes</name>
<summary>Notizbuch-App für Stift, Touch und Maus</summary> <summary>Handwritten notes with pen, touch and mouse</summary>
<summary xml:lang="de">Notizbuch-App für Stift, Touch und Maus</summary>
<description> <description>
<p> <p>
ink-notes ist eine Notizbuch-App für Linux, geschrieben in Rust mit wgpu. Handschrift wird als ink-notes is a notebook app for Linux tablets and convertibles. Handwriting is stored as
vectors; write with a pen, your finger, a touchpad or a mouse.
</p>
<p xml:lang="de">
ink-notes ist eine Notizbuch-App für Linux-Tablets und Convertibles. Handschrift wird als
Vektoren gespeichert; geschrieben wird mit Stift, Finger, Touchpad oder Maus. Vektoren gespeichert; geschrieben wird mit Stift, Finger, Touchpad oder Maus.
</p> </p>
<p>Funktionen:</p> <p>Features:</p>
<p xml:lang="de">Funktionen:</p>
<ul> <ul>
<li>Stift und Textmarker mit Druckstärke, Glättung beim Schreiben und Ecken-Erhaltung</li> <li>Pens and highlighter with pressure, smoothing while writing and sharp corners</li>
<li>Radierer für ganze Striche, Teilstriche oder nur Textmarker; virtuelles Lineal</li> <li xml:lang="de">Stifte und Textmarker mit Druckstärke, Glättung beim Schreiben und Ecken-Erhaltung</li>
<li>Formerkennung (Linien, Pfeile, Kreise, Ellipsen, Rechtecke, Dreiecke, Rauten, Koordinatensysteme)</li> <li>Shape recognition: hold at the end of a stroke to get clean lines, arrows, circles, rectangles, triangles and coordinate systems</li>
<li>Auswählen, Verschieben, Drehen, Skalieren und Umfärben; Rückgängig für alle Änderungen</li> <li xml:lang="de">Formerkennung: am Strichende halten für saubere Linien, Pfeile, Kreise, Rechtecke, Dreiecke und Koordinatensysteme</li>
<li>Textfelder, Bilder und PDF-Import zum Beschriften (Querformat-Folien bekommen Querformat-Seiten)</li> <li>Eraser for whole strokes, partial strokes or highlighter only; virtual ruler</li>
<li>Durchgehendes Scrollen, Zoom von 25 bis 1000 Prozent, Papier- und Musterfarben</li> <li xml:lang="de">Radierer für ganze Striche, Teilstriche oder nur Textmarker; virtuelles Lineal</li>
<li>Mehrere Notizbücher, automatisches Speichern im Hintergrund</li> <li>Select, move, rotate, scale and recolor; undo for every change</li>
<li>Export als PDF (mit durchsuchbarem Text), PNG oder SVG</li> <li xml:lang="de">Auswählen, Verschieben, Drehen, Skalieren und Umfärben; Rückgängig für alle Änderungen</li>
<li>Text boxes, images and PDF import to annotate lecture slides and worksheets</li>
<li xml:lang="de">Textfelder, Bilder und PDF-Import zum Beschriften von Vorlesungsfolien und Übungsblättern</li>
<li>Notebooks in folders, continuous scrolling, automatic saving in the background</li>
<li xml:lang="de">Notizbücher in Ordnern, durchgehendes Scrollen, automatisches Speichern im Hintergrund</li>
<li>Export as PDF (with searchable text), PNG or SVG</li>
<li xml:lang="de">Export als PDF (mit durchsuchbarem Text), PNG oder SVG</li>
</ul> </ul>
</description> </description>
<launchable type="desktop-id">eu.pansi.ink-notes.desktop</launchable> <launchable type="desktop-id">eu.pansi.ink-notes.desktop</launchable>
@@ -29,12 +41,78 @@
<categories> <categories>
<category>Office</category> <category>Office</category>
<category>Graphics</category> <category>Graphics</category>
<category>Utility</category>
</categories> </categories>
<keywords>
<keyword>notes</keyword>
<keyword>handwriting</keyword>
<keyword>stylus</keyword>
<keyword>pen</keyword>
<keyword>PDF</keyword>
<keyword>annotate</keyword>
</keywords>
<supports>
<control>pointing</control>
<control>touch</control>
<control>tablet</control>
</supports>
<developer id="eu.pansi"> <developer id="eu.pansi">
<name>Harald</name> <name>Harald Pansi</name>
</developer> </developer>
<url type="homepage">https://gitea.pansi.eu/harald/ink-notes</url> <url type="homepage">https://gitea.pansi.eu/harald/ink-notes</url>
<url type="bugtracker">https://gitea.pansi.eu/harald/ink-notes/issues</url> <url type="bugtracker">https://gitea.pansi.eu/harald/ink-notes/issues</url>
<url type="vcs-browser">https://gitea.pansi.eu/harald/ink-notes</url>
<screenshots>
<screenshot type="default">
<caption>Notes with highlighter, handwriting and text boxes</caption>
<caption xml:lang="de">Notizen mit Textmarker, Handschrift und Textfeldern</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/notes.png</image>
</screenshot>
<screenshot>
<caption>Recognized shapes: coordinate system, triangle, circle, octahedron</caption>
<caption xml:lang="de">Erkannte Formen: Koordinatensystem, Dreieck, Kreis, Oktaeder</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/shapes.png</image>
</screenshot>
<screenshot>
<caption>Annotating imported PDF slides</caption>
<caption xml:lang="de">Importierte PDF-Folien beschriften</caption>
<image>https://gitea.pansi.eu/harald/ink-notes/raw/tag/v0.26.1/packaging/screenshots/pdf.png</image>
</screenshot>
</screenshots>
<branding>
<color type="primary" scheme_preference="light">#a8c8f0</color>
<color type="primary" scheme_preference="dark">#1f3a5f</color>
</branding>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases>
<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>
</description>
</release>
<release version="0.26.1" date="2026-10-10">
<description>
<p>Screenshots in the app description for software centers. The app itself is unchanged.</p>
</description>
</release>
<release version="0.26.0" date="2026-10-10">
<description>
<p>Select and move objects with a finger; text and make-space tools also work with a finger. Exports go to the documents folder of the user directories.</p>
</description>
</release>
<release version="0.25.0" date="2026-10-09">
<description>
<p>English user interface; the language follows the system and can be changed in the menu. Holding to snap a shape tolerates a slightly trembling pen.</p>
</description>
</release>
<release version="0.24.0" date="2026-10-09">
<description>
<p>Axis cross from two crossing strokes.</p>
</description>
</release>
<release version="0.23.0" date="2026-10-09">
<description>
<p>The on-screen keyboard of GNOME appears when tapping a text field.</p>
</description>
</release>
</releases>
</component> </component>
+4
View File
@@ -3,6 +3,10 @@
Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04). Notizbuch-App für Stift, Touch und Maus unter Linux (getestet mit Zorin OS 18 / Ubuntu 24.04).
### Highlights: ### Highlights:
- **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
- **Flatpak**: Manifest unter packaging/flatpak (Vorbereitung für Flathub); Exporte landen im Dokumente-Ordner aus den Benutzerordnern (z. B. ~/Dokumente)
- **Englische Oberfläche / English interface**: ink-notes folgt der Systemsprache (Deutsch oder Englisch); umstellen im ☰-Menü unter „Sprache / Language“ - **Englische Oberfläche / English interface**: ink-notes folgt der Systemsprache (Deutsch oder Englisch); umstellen im ☰-Menü unter „Sprache / Language“
- **Halten zum Einrasten**: verträgt jetzt das leichte Zittern eines ruhenden Stifts, damit Formen auch im normalen Stift-Modus einrasten - **Halten zum Einrasten**: verträgt jetzt das leichte Zittern eines ruhenden Stifts, damit Formen auch im normalen Stift-Modus einrasten
- **Achsenkreuz aus zwei Strichen**: Eine waagerechte und eine senkrechte Gerade über Kreuz zeichnen – die zweite mit Halten oder dem Formen-Werkzeug – und beide werden zum Achsenkreuz mit Pfeilspitzen an allen vier Enden; Rückgängig stellt die erste Gerade wieder her - **Achsenkreuz aus zwei Strichen**: Eine waagerechte und eine senkrechte Gerade über Kreuz zeichnen – die zweite mit Halten oder dem Formen-Werkzeug – und beide werden zum Achsenkreuz mit Pfeilspitzen an allen vier Enden; Rückgängig stellt die erste Gerade wieder her
Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

+13 -2
View File
@@ -46,7 +46,9 @@ pub fn sanitize_filename_component(title: &str) -> String {
} }
} }
/// Ermittelt das Standard-Dokumentenverzeichnis ($XDG_DOCUMENTS_DIR mit Fallback ~/Documents). /// Ermittelt das Standard-Dokumentenverzeichnis: `$XDG_DOCUMENTS_DIR`, sonst der Eintrag in
/// `~/.config/user-dirs.dirs` (z. B. „Dokumente“), sonst `~/Documents`. Im Flatpak ist nur
/// dieser Ordner beschreibbar, der Fallback allein träfe bei deutschen Systemen daneben.
pub fn default_documents_dir() -> PathBuf { pub fn default_documents_dir() -> PathBuf {
if let Ok(dir) = std::env::var("XDG_DOCUMENTS_DIR") { if let Ok(dir) = std::env::var("XDG_DOCUMENTS_DIR") {
if !dir.trim().is_empty() { if !dir.trim().is_empty() {
@@ -56,7 +58,16 @@ pub fn default_documents_dir() -> PathBuf {
if let Ok(home) = std::env::var("HOME") { if let Ok(home) = std::env::var("HOME") {
if !home.trim().is_empty() { if !home.trim().is_empty() {
return PathBuf::from(home).join("Documents"); let home = PathBuf::from(home);
let user_dirs =
std::fs::read_to_string(home.join(".config/user-dirs.dirs")).unwrap_or_default();
if let Some(dir) =
crate::file_picker::parse_user_dir(&user_dirs, "XDG_DOCUMENTS_DIR", &home)
.filter(|dir| *dir != home)
{
return dir;
}
return home.join("Documents");
} }
} }
+2
View File
@@ -66,9 +66,11 @@ pub const EN: &[(&str, &str)] = &[
("Die beschädigte Originaldatei wurde in Quarantäne gesichert:", "The damaged original file was saved to quarantine:"), ("Die beschädigte Originaldatei wurde in Quarantäne gesichert:", "The damaged original file was saved to quarantine:"),
("Die Datei ist kein lesbares PDF", "The file is not a readable PDF"), ("Die Datei ist kein lesbares PDF", "The file is not a readable PDF"),
("Die einzige Seite eines Notizbuchs kann nicht gelöscht werden", "The only page of a notebook cannot be deleted"), ("Die einzige Seite eines Notizbuchs kann nicht gelöscht werden", "The only page of a notebook cannot be deleted"),
("Die Flatpak-Version wird über die Software-Verwaltung aktualisiert.", "The Flatpak version is updated through the software manager."),
("Die heruntergeladene Datei existiert nicht mehr.", "The downloaded file no longer exists."), ("Die heruntergeladene Datei existiert nicht mehr.", "The downloaded file no longer exists."),
("die oberste Ebene", "the top level"), ("die oberste Ebene", "the top level"),
("Die Zwischenablage ist leer", "The clipboard is empty"), ("Die Zwischenablage ist leer", "The clipboard is empty"),
("Diese Version wird über die Software-Verwaltung aktualisiert (z. B. GNOME Software oder „flatpak update“).", "This version is updated through the software manager (e.g. GNOME Software or “flatpak update”)."),
("Dieser Ordner ist leer.", "This folder is empty."), ("Dieser Ordner ist leer.", "This folder is empty."),
("Dokument '{title}' erstellt", "Document '{title}' created"), ("Dokument '{title}' erstellt", "Document '{title}' created"),
("Dokument '{title}' geöffnet", "Document '{title}' opened"), ("Dokument '{title}' geöffnet", "Document '{title}' opened"),
+94 -27
View File
@@ -45,26 +45,57 @@ pub fn quad_vertices(corners: &[glam::Vec2; 4]) -> [ImageVertex; 6] {
[tl, tr, bl, bl, tr, br] [tl, tr, bl, bl, tr, br]
} }
/// Mipmap-Stufen eines Bildes, jeweils halbe Kantenlänge bis 1 × 1. /// Mipmap-Stufen eines Bildes, jeweils halbe Kantenlänge bis 1 × 1. Für eine gerenderte
pub fn mip_chain(base: &RgbaImage) -> Vec<RgbaImage> { /// PDF-Seite dauert das spürbar (2560 px: einige 10 ms); deshalb erzeugt sie der
let mut levels = vec![base.clone()]; /// PDF-Render-Thread und nicht der Haupt-Thread.
let Some(mut current) = pub fn mip_chain(base: RgbaImage) -> Vec<RgbaImage> {
image::RgbaImage::from_raw(base.width, base.height, base.pixels.clone()) let mut levels = vec![base];
else { loop {
return levels; let current = &levels[levels.len() - 1];
}; if current.width <= 1 && current.height <= 1 {
while current.width() > 1 || current.height() > 1 { break;
let (w, h) = ((current.width() / 2).max(1), (current.height() / 2).max(1)); }
current = image::imageops::resize(&current, w, h, image::imageops::FilterType::Triangle); let next = half_size(current);
levels.push(RgbaImage { levels.push(next);
width: w,
height: h,
pixels: current.as_raw().clone(),
});
} }
levels levels
} }
/// Halbiert ein Bild: Mittelwert aus je 2 × 2 Pixeln (bei ungerader Kante entfällt die
/// letzte Spalte bzw. Zeile, bei Kantenlänge 1 wird nur in der anderen Richtung gemittelt).
fn half_size(image: &RgbaImage) -> RgbaImage {
let (w, h) = (image.width as usize, image.height as usize);
let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
let mut pixels = vec![0u8; nw * nh * 4];
if image.pixels.len() < w * h * 4 {
return RgbaImage {
width: nw as u32,
height: nh as u32,
pixels,
};
}
for y in 0..nh {
let (y0, y1) = ((2 * y).min(h - 1), (2 * y + 1).min(h - 1));
let (row0, row1) = (&image.pixels[y0 * w * 4..], &image.pixels[y1 * w * 4..]);
let out = &mut pixels[y * nw * 4..(y + 1) * nw * 4];
for x in 0..nw {
let (x0, x1) = ((2 * x).min(w - 1) * 4, (2 * x + 1).min(w - 1) * 4);
for c in 0..4 {
let sum = row0[x0 + c] as u16
+ row0[x1 + c] as u16
+ row1[x0 + c] as u16
+ row1[x1 + c] as u16;
out[x * 4 + c] = ((sum + 2) / 4) as u8;
}
}
}
RgbaImage {
width: nw as u32,
height: nh as u32,
pixels,
}
}
/// Name der Ersatztextur für Bilder, deren Datei fehlt oder beschädigt ist. /// Name der Ersatztextur für Bilder, deren Datei fehlt oder beschädigt ist.
const MISSING: &str = ""; const MISSING: &str = "";
/// Bis zu dieser Grafikspeichermenge bleiben Texturen anderer Seiten erhalten (schnelles /// Bis zu dieser Grafikspeichermenge bleiben Texturen anderer Seiten erhalten (schnelles
@@ -74,6 +105,8 @@ const TEXTURE_CACHE_BYTES: u64 = 384 * 1024 * 1024;
/// Antwort einer Texturquelle für einen Schlüssel. /// Antwort einer Texturquelle für einen Schlüssel.
pub enum TextureSource { pub enum TextureSource {
Ready(RgbaImage), Ready(RgbaImage),
/// Fertige Mipmap-Stufen (siehe [`mip_chain`]), z. B. vom PDF-Render-Thread
ReadyMips(Vec<RgbaImage>),
/// Wird gerade erzeugt; später erneut fragen /// Wird gerade erzeugt; später erneut fragen
Pending, Pending,
Failed(String), Failed(String),
@@ -233,10 +266,16 @@ impl ImageLayer {
continue; continue;
} }
let (bind_group, bytes) = match load(&key) { let (bind_group, bytes) = match load(&key) {
TextureSource::Ready(image) => ( TextureSource::Ready(image) => {
self.upload_texture(device, queue, &key, &image), self.upload_texture(device, queue, &key, mip_chain(image))
image.pixels.len() as u64 * 4 / 3, }
), TextureSource::ReadyMips(levels) if !levels.is_empty() => {
self.upload_texture(device, queue, &key, levels)
}
TextureSource::ReadyMips(_) => {
eprintln!("[Bilder] '{key}' kann nicht angezeigt werden: leeres Bild");
(self.missing_texture(device, queue), 4)
}
// Noch in Arbeit (PDF-Seite wird gerendert): beim nächsten Bild erneut fragen // Noch in Arbeit (PDF-Seite wird gerendert): beim nächsten Bild erneut fragen
TextureSource::Pending => continue, TextureSource::Pending => continue,
TextureSource::Failed(err) => { TextureSource::Failed(err) => {
@@ -342,17 +381,19 @@ impl ImageLayer {
height: 1, height: 1,
pixels: vec![200, 200, 200, 255], pixels: vec![200, 200, 200, 255],
}; };
self.upload_texture(device, queue, MISSING, &gray) self.upload_texture(device, queue, MISSING, vec![gray]).0
} }
/// Lädt die Mipmap-Stufen hoch (Stufe 0 = volle Größe); liefert die Bind Group und den
/// belegten Grafikspeicher.
fn upload_texture( fn upload_texture(
&self, &self,
device: &wgpu::Device, device: &wgpu::Device,
queue: &wgpu::Queue, queue: &wgpu::Queue,
label: &str, label: &str,
image: &RgbaImage, levels: Vec<RgbaImage>,
) -> wgpu::BindGroup { ) -> (wgpu::BindGroup, u64) {
let levels = mip_chain(image); let image = &levels[0];
let texture = device.create_texture(&wgpu::TextureDescriptor { let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some(label), label: Some(label),
size: wgpu::Extent3d { size: wgpu::Extent3d {
@@ -389,8 +430,9 @@ impl ImageLayer {
}, },
); );
} }
let bytes = levels.iter().map(|l| l.pixels.len() as u64).sum();
let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
device.create_bind_group(&wgpu::BindGroupDescriptor { let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Image Texture Bind Group"), label: Some("Image Texture Bind Group"),
layout: &self.texture_layout, layout: &self.texture_layout,
entries: &[ entries: &[
@@ -403,7 +445,8 @@ impl ImageLayer {
resource: wgpu::BindingResource::Sampler(&self.sampler), resource: wgpu::BindingResource::Sampler(&self.sampler),
}, },
], ],
}) });
(bind_group, bytes)
} }
} }
@@ -439,11 +482,35 @@ mod tests {
height: 3, height: 3,
pixels: vec![128; 8 * 3 * 4], pixels: vec![128; 8 * 3 * 4],
}; };
let levels = mip_chain(&base); let levels = mip_chain(base);
let sizes: Vec<(u32, u32)> = levels.iter().map(|l| (l.width, l.height)).collect(); let sizes: Vec<(u32, u32)> = levels.iter().map(|l| (l.width, l.height)).collect();
assert_eq!(sizes, vec![(8, 3), (4, 1), (2, 1), (1, 1)]); assert_eq!(sizes, vec![(8, 3), (4, 1), (2, 1), (1, 1)]);
for l in &levels { for l in &levels {
assert_eq!(l.pixels.len(), (l.width * l.height * 4) as usize); assert_eq!(l.pixels.len(), (l.width * l.height * 4) as usize);
} }
} }
#[test]
fn test_half_size_averages_two_by_two_blocks() {
// 3 × 2: links schwarz/weiß übereinander, Mitte grau; die dritte Spalte entfällt
let px = |v: u8| [v, v, v, 255];
let pixels = [px(0), px(100), px(7), px(255), px(100), px(7)].concat();
let base = RgbaImage {
width: 3,
height: 2,
pixels,
};
let half = half_size(&base);
assert_eq!((half.width, half.height), (1, 1));
// (0 + 100 + 255 + 100) / 4 = 113,75 → 114
assert_eq!(half.pixels, vec![114, 114, 114, 255]);
// Kantenlänge 1: nur in der anderen Richtung mitteln
let column = RgbaImage {
width: 1,
height: 2,
pixels: [px(10), px(20)].concat(),
};
assert_eq!(half_size(&column).pixels, vec![15, 15, 15, 255]);
}
} }
+66 -19
View File
@@ -596,6 +596,31 @@ fn insert_image_on_page(
)); ));
} }
/// Trifft ein Finger auf `screen` die Auswahl (Rahmen, Griffe) oder ein Objekt? Dann wählt bzw.
/// verschiebt er, sonst verschiebt er das Blatt.
fn finger_grabs_selection(
screen: (f32, f32),
canvas: &CanvasState,
page: &ink_notes::document::Page,
pixels_per_point: f32,
selection_state: &SelectionState,
) -> bool {
let on_selection = !selection_state.is_empty()
&& !matches!(
selection_state.hit_test_handles(
page,
canvas,
Vec2::new(screen.0, screen.1),
pixels_per_point
),
SelectionHandleHit::None
);
on_selection
|| canvas
.screen_to_page(screen.0, screen.1)
.is_some_and(|p| page.find_stroke_at(p, 25.0).is_some())
}
/// Auswahlwerkzeug: Druck auf `screen` (physische Pixel) greift einen Griff, die Auswahl /// Auswahlwerkzeug: Druck auf `screen` (physische Pixel) greift einen Griff, die Auswahl
/// oder das Objekt darunter oder beginnt einen Auswahlrahmen. Mit `shift` wird die Auswahl /// oder das Objekt darunter oder beginnt einen Auswahlrahmen. Mit `shift` wird die Auswahl
/// erweitert. `true`, wenn etwas begonnen wurde. /// erweitert. `true`, wenn etwas begonnen wurde.
@@ -916,25 +941,21 @@ fn texture_for(
assets: &AssetStore, assets: &AssetStore,
pdf_renderer: &mut PdfRenderService, pdf_renderer: &mut PdfRenderService,
) -> TextureSource { ) -> TextureSource {
let result = match key.contains('#') { if !key.contains('#') {
true => { return match assets.load_rgba(key, ink_notes::assets::MAX_DISPLAY_DIMENSION) {
let Some((Ok(path), page, max_pixels)) = ink_notes::pdf_doc::parse_page_key(key) Ok(image) => TextureSource::Ready(image),
.map(|(asset, page, px)| (assets.path_of(asset), page, px)) Err(err) => TextureSource::Failed(err.to_string()),
else { };
return TextureSource::Failed(t!("Ungültiger PDF-Verweis '{key}'", key = key)); }
}; let Some((Ok(path), page, max_pixels)) = ink_notes::pdf_doc::parse_page_key(key)
match pdf_renderer.take(key, path, page, max_pixels) { .map(|(asset, page, px)| (assets.path_of(asset), page, px))
Some(result) => result, else {
None => return TextureSource::Pending, return TextureSource::Failed(t!("Ungültiger PDF-Verweis '{key}'", key = key));
}
}
false => assets
.load_rgba(key, ink_notes::assets::MAX_DISPLAY_DIMENSION)
.map_err(|e| e.to_string()),
}; };
match result { match pdf_renderer.take(key, path, page, max_pixels) {
Ok(image) => TextureSource::Ready(image), Some(Ok(levels)) => TextureSource::ReadyMips(levels),
Err(err) => TextureSource::Failed(err), Some(Err(err)) => TextureSource::Failed(err),
None => TextureSource::Pending,
} }
} }
@@ -2820,7 +2841,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
cursor_screen_pos = (tx, ty); cursor_screen_pos = (tx, ty);
current_pressure = pressure; current_pressure = pressure;
if !ui_state.touch_draw_enabled { // Ohne Finger-Zeichnen verschiebt ein Finger das Blatt – außer bei
// Werkzeugen, die nicht zeichnen: Text und Platz schaffen nehmen den
// Finger immer, Auswählen, wenn er ein Objekt oder die Auswahl trifft
// (Issue #26)
let finger_uses_tool = match ui_state.active_tool {
ActiveTool::Text | ActiveTool::Space => true,
ActiveTool::Select => {
focus_page_at!((tx, ty));
finger_grabs_selection(
(tx, ty),
&canvas,
notebook.current_page(),
egui_ctx.pixels_per_point(),
&selection_state,
)
}
_ => false,
};
if finger_uses_tool {
// Halten soll hier kein Kontextmenü öffnen
long_press.cancel();
}
if !ui_state.touch_draw_enabled && !finger_uses_tool {
// Standard: 1 Finger scrollt / verschiebt den Canvas (Pan) // Standard: 1 Finger scrollt / verschiebt den Canvas (Pan)
is_touch_panning = true; is_touch_panning = true;
touch_pan_last_pos = (tx, ty); touch_pan_last_pos = (tx, ty);
@@ -3877,6 +3920,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
canvas.set_neighbor_pages(&[]); canvas.set_neighbor_pages(&[]);
} }
let assets = AssetStore::new(&storage_dir); let assets = AssetStore::new(&storage_dir);
// PDF-Seiten, die nach diesem Bild nicht mehr gebraucht werden (schnelles
// Scrollen), verwirft end_frame, bevor sie gerendert werden
pdf_renderer.begin_frame();
canvas.prepare_images(&mut |key| texture_for(key, &assets, &mut pdf_renderer)); canvas.prepare_images(&mut |key| texture_for(key, &assets, &mut pdf_renderer));
// Nächste PDF-Seite vorab rendern, damit Blättern sofort klappt // Nächste PDF-Seite vorab rendern, damit Blättern sofort klappt
if !canvas.is_waiting_for_images() { if !canvas.is_waiting_for_images() {
@@ -3887,6 +3933,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
} }
} }
pdf_renderer.end_frame();
match canvas.render(&clipped_primitives, &screen_descriptor, &full_output.textures_delta) { match canvas.render(&clipped_primitives, &screen_descriptor, &full_output.textures_delta) {
Ok(_) => {} Ok(_) => {}
Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => {
+239 -29
View File
@@ -1,11 +1,13 @@
//! Lesen und Rendern importierter PDF-Dateien mit `hayro` (reines Rust, nur CPU). //! Lesen und Rendern importierter PDF-Dateien mit `hayro` (reines Rust, nur CPU).
//! //!
//! Das Rendern einer Seite kann einige hundert Millisekunden dauern; [`PdfRenderService`] //! Das Rendern einer Seite kann einige hundert Millisekunden dauern; [`PdfRenderService`]
//! erledigt es deshalb in einem Hintergrund-Thread und meldet fertige Seiten zurück. //! erledigt es deshalb in einem Hintergrund-Thread, samt Mipmaps für die Anzeige, und meldet
//! fertige Seiten zurück.
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::mpsc::{self, Receiver};
use std::sync::{Arc, Condvar, Mutex};
use hayro::hayro_interpret::InterpreterSettings; use hayro::hayro_interpret::InterpreterSettings;
use hayro::hayro_syntax::{LoadPdfError, Pdf}; use hayro::hayro_syntax::{LoadPdfError, Pdf};
@@ -104,30 +106,107 @@ struct RenderRequest {
path: PathBuf, path: PathBuf,
page: usize, page: usize,
max_pixels: u32, max_pixels: u32,
/// Wichtigkeit: kleiner zuerst (Reihenfolge der Anfragen im aktuellen Bild)
rank: usize,
} }
/// Ergebnis eines Render-Auftrags. /// Wichtigkeit einer vorab gerenderten Seite: nach allen sichtbaren.
pub type RenderResult = Result<RgbaImage, String>; const PREFETCH_RANK: usize = usize::MAX;
/// Warteschlange der noch nicht begonnenen Render-Aufträge, geteilt mit dem Render-Thread.
#[derive(Default)]
struct RenderQueue {
requests: Vec<RenderRequest>,
shutdown: bool,
}
impl RenderQueue {
/// Fügt einen Auftrag hinzu oder macht einen wartenden wichtiger.
fn push(&mut self, request: RenderRequest) {
match self.requests.iter_mut().find(|r| r.key == request.key) {
Some(queued) => queued.rank = queued.rank.min(request.rank),
None => self.requests.push(request),
}
}
/// Nimmt den wichtigsten Auftrag heraus.
fn pop(&mut self) -> Option<RenderRequest> {
let (index, _) = self
.requests
.iter()
.enumerate()
.min_by_key(|(_, r)| r.rank)?;
Some(self.requests.swap_remove(index))
}
/// Verwirft Aufträge, für die `rank_of` keinen Rang mehr liefert, und ordnet die übrigen
/// neu. Liefert die Schlüssel der verworfenen.
fn retain(&mut self, rank_of: impl Fn(&str) -> Option<usize>) -> Vec<String> {
let mut dropped = Vec::new();
self.requests.retain_mut(|r| match rank_of(&r.key) {
Some(rank) => {
r.rank = rank;
true
}
None => {
dropped.push(r.key.clone());
false
}
});
dropped
}
}
type SharedQueue = Arc<(Mutex<RenderQueue>, Condvar)>;
/// Ergebnis eines Render-Auftrags: die Mipmap-Stufen der Seite (siehe
/// [`crate::image_layer::mip_chain`]), Stufe 0 in voller Größe.
pub type RenderResult = Result<Vec<RgbaImage>, String>;
/// Rendert PDF-Seiten im Hintergrund. Fertige Seiten holt der Event-Loop mit /// Rendert PDF-Seiten im Hintergrund. Fertige Seiten holt der Event-Loop mit
/// [`PdfRenderService::take`] ab; `on_ready` weckt ihn dafür auf. /// [`PdfRenderService::take`] ab; `on_ready` weckt ihn dafür auf.
///
/// Je Bild meldet der Event-Loop zwischen [`Self::begin_frame`] und [`Self::end_frame`] mit
/// `take` alle Seiten an, die er gerade braucht (die wichtigste zuerst). Gerendert wird die
/// wichtigste wartende Seite zuerst; Seiten, die beim schnellen Scrollen schon wieder aus dem
/// Bild sind, verwirft `end_frame`, bevor der Render-Thread sie beginnt.
pub struct PdfRenderService { pub struct PdfRenderService {
requests: Sender<RenderRequest>, queue: SharedQueue,
worker: std::thread::JoinHandle<()>,
results: Receiver<(String, RenderResult)>, results: Receiver<(String, RenderResult)>,
/// Wartende oder gerade gerenderte Seiten
pending: HashSet<String>, pending: HashSet<String>,
ready: HashMap<String, RenderResult>, ready: HashMap<String, RenderResult>,
/// Im aktuellen Bild angeforderte Seiten in ihrer Reihenfolge
wanted: Vec<String>,
/// Zuletzt vorab angeforderte Seite (bleibt erhalten, bis sie abgeholt wird)
prefetched: Option<String>,
} }
impl PdfRenderService { impl PdfRenderService {
pub fn new(on_ready: impl Fn() + Send + 'static) -> Self { pub fn new(on_ready: impl Fn() + Send + 'static) -> Self {
let (req_tx, req_rx) = mpsc::channel::<RenderRequest>(); let queue: SharedQueue = Arc::default();
let (res_tx, res_rx) = mpsc::channel(); let (res_tx, res_rx) = mpsc::channel();
std::thread::Builder::new() let worker_queue = Arc::clone(&queue);
let worker = std::thread::Builder::new()
.name("pdf-render".into()) .name("pdf-render".into())
.spawn(move || { .spawn(move || {
// Zuletzt geöffnetes PDF behalten: Blättern rendert meist dieselbe Datei // Zuletzt geöffnetes PDF behalten: Blättern rendert meist dieselbe Datei
let mut open_pdf: Option<(PathBuf, Pdf)> = None; let mut open_pdf: Option<(PathBuf, Pdf)> = None;
while let Ok(request) = req_rx.recv() { loop {
let request = {
let (lock, ready) = &*worker_queue;
let mut queue = lock.lock().unwrap_or_else(|e| e.into_inner());
loop {
if queue.shutdown {
return;
}
if let Some(request) = queue.pop() {
break request;
}
queue = ready.wait(queue).unwrap_or_else(|e| e.into_inner());
}
};
if open_pdf.as_ref().map(|(p, _)| p) != Some(&request.path) { if open_pdf.as_ref().map(|(p, _)| p) != Some(&request.path) {
open_pdf = std::fs::read(&request.path) open_pdf = std::fs::read(&request.path)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
@@ -137,36 +216,64 @@ impl PdfRenderService {
.map(|pdf| (request.path.clone(), pdf)); .map(|pdf| (request.path.clone(), pdf));
} }
let result = match &open_pdf { let result = match &open_pdf {
Some((_, pdf)) => render_page(pdf, request.page, request.max_pixels), // Mipmaps hier statt im Haupt-Thread: sonst stockt das Scrollen bei
// jeder neu gerenderten Seite (Issue #28)
Some((_, pdf)) => render_page(pdf, request.page, request.max_pixels)
.map(crate::image_layer::mip_chain),
None => Err(t!("PDF kann nicht geöffnet werden").into()), None => Err(t!("PDF kann nicht geöffnet werden").into()),
}; };
if res_tx.send((request.key, result)).is_err() { if res_tx.send((request.key, result)).is_err() {
break; return;
} }
on_ready(); on_ready();
} }
}) })
.expect("PDF-Render-Thread konnte nicht gestartet werden"); .expect("PDF-Render-Thread konnte nicht gestartet werden");
Self { Self {
requests: req_tx, queue,
worker,
results: res_rx, results: res_rx,
pending: HashSet::new(), pending: HashSet::new(),
ready: HashMap::new(), ready: HashMap::new(),
wanted: Vec::new(),
prefetched: None,
} }
} }
/// Beginnt die Anfragen eines neuen Bildes (siehe [`Self::end_frame`]).
pub fn begin_frame(&mut self) {
self.wanted.clear();
}
/// Verwirft wartende Aufträge und fertige Ergebnisse für Seiten, die in diesem Bild nicht
/// mehr angefordert wurden (außer der vorab angeforderten), und ordnet die übrigen nach
/// der Reihenfolge der Anfragen.
pub fn end_frame(&mut self) {
let rank_of = |key: &str| {
self.wanted
.iter()
.position(|k| k == key)
.or_else(|| (self.prefetched.as_deref() == Some(key)).then_some(PREFETCH_RANK))
};
let dropped = {
let mut queue = self.queue.0.lock().unwrap_or_else(|e| e.into_inner());
queue.retain(rank_of)
};
for key in dropped {
self.pending.remove(&key);
}
self.ready.retain(|key, _| rank_of(key).is_some());
}
/// Rendert eine Seite vorab (z. B. die nächste), ohne auf das Ergebnis zu warten; /// Rendert eine Seite vorab (z. B. die nächste), ohne auf das Ergebnis zu warten;
/// [`Self::take`] liefert es später sofort. /// [`Self::take`] liefert es später sofort.
pub fn prefetch(&mut self, key: &str, path: PathBuf, page: usize) { pub fn prefetch(&mut self, key: &str, path: PathBuf, page: usize) {
self.collect(); self.collect();
if self.ready.contains_key(key) || self.pending.contains(key) { self.prefetched = Some(key.to_string());
if self.ready.contains_key(key) {
return; return;
} }
// Vorab gerenderte Seiten nicht unbegrenzt aufheben self.request(key, path, page, DISPLAY_RENDER_PIXELS, PREFETCH_RANK);
if self.ready.len() >= 2 {
self.ready.clear();
}
self.request(key, path, page, DISPLAY_RENDER_PIXELS);
} }
fn collect(&mut self) { fn collect(&mut self) {
@@ -176,21 +283,37 @@ impl PdfRenderService {
} }
} }
/// Erteilt den Auftrag, falls noch keiner läuft. `false`, wenn der Thread beendet ist. /// Erteilt den Auftrag bzw. macht einen wartenden wichtiger. `false`, wenn der
fn request(&mut self, key: &str, path: PathBuf, page: usize, max_pixels: u32) -> bool { /// Render-Thread beendet ist.
if !self.pending.insert(key.to_string()) { fn request(
&mut self,
key: &str,
path: PathBuf,
page: usize,
max_pixels: u32,
rank: usize,
) -> bool {
if self.worker.is_finished() {
self.pending.remove(key);
return false;
}
let (lock, ready) = &*self.queue;
let mut queue = lock.lock().unwrap_or_else(|e| e.into_inner());
// Steht ein angefragter Auftrag nicht mehr in der Warteschlange, rendert ihn der
// Thread gerade: nicht doppelt erteilen
let waiting = queue.requests.iter().any(|r| r.key == key);
if self.pending.contains(key) && !waiting {
return true; return true;
} }
let request = RenderRequest { self.pending.insert(key.to_string());
queue.push(RenderRequest {
key: key.to_string(), key: key.to_string(),
path, path,
page, page,
max_pixels, max_pixels,
}; rank,
if self.requests.send(request).is_err() { });
self.pending.remove(key); ready.notify_one();
return false;
}
true true
} }
@@ -204,16 +327,29 @@ impl PdfRenderService {
max_pixels: u32, max_pixels: u32,
) -> Option<RenderResult> { ) -> Option<RenderResult> {
self.collect(); self.collect();
if self.prefetched.as_deref() == Some(key) {
self.prefetched = None;
}
if let Some(result) = self.ready.remove(key) { if let Some(result) = self.ready.remove(key) {
return Some(result); return Some(result);
} }
if !self.request(key, path, page, max_pixels) { let rank = self.wanted.len();
self.wanted.push(key.to_string());
if !self.request(key, path, page, max_pixels, rank) {
return Some(Err(t!("PDF-Render-Thread beendet").into())); return Some(Err(t!("PDF-Render-Thread beendet").into()));
} }
None None
} }
} }
impl Drop for PdfRenderService {
fn drop(&mut self) {
let (lock, ready) = &*self.queue;
lock.lock().unwrap_or_else(|e| e.into_inner()).shutdown = true;
ready.notify_all();
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use super::*; use super::*;
@@ -303,8 +439,12 @@ pub(crate) mod tests {
wake_rx wake_rx
.recv_timeout(std::time::Duration::from_secs(20)) .recv_timeout(std::time::Duration::from_secs(20))
.unwrap(); .unwrap();
let image = service.take("a#0", path.clone(), 0, px).unwrap().unwrap(); let levels = service.take("a#0", path.clone(), 0, px).unwrap().unwrap();
assert_eq!(image.width, DISPLAY_RENDER_PIXELS); assert_eq!(levels[0].width, DISPLAY_RENDER_PIXELS);
assert_eq!(
(levels.last().unwrap().width, levels.last().unwrap().height),
(1, 1)
);
// Vorab rendern: danach sofort verfügbar // Vorab rendern: danach sofort verfügbar
service.prefetch("a#0", path.clone(), 0); service.prefetch("a#0", path.clone(), 0);
@@ -314,6 +454,76 @@ pub(crate) mod tests {
assert!(service.take("a#0", path, 0, px).unwrap().is_ok()); assert!(service.take("a#0", path, 0, px).unwrap().is_ok());
} }
fn request(key: &str, rank: usize) -> RenderRequest {
RenderRequest {
key: key.into(),
path: PathBuf::new(),
page: 0,
max_pixels: 1,
rank,
}
}
#[test]
fn test_render_queue_priorities_and_stale_requests() {
let mut queue = RenderQueue::default();
queue.push(request("a", 2));
queue.push(request("b", 0));
queue.push(request("c", 1));
// Erneute Anfrage macht einen wartenden Auftrag wichtiger, ohne ihn zu verdoppeln
queue.push(request("a", 0));
assert_eq!(queue.requests.len(), 3);
assert_eq!(queue.pop().map(|r| r.key), Some("a".into()));
// Nach dem Scrollen wird nur noch „c“ gebraucht: „b“ fällt weg
let dropped = queue.retain(|key| (key == "c").then_some(0));
assert_eq!(dropped, vec!["b".to_string()]);
assert_eq!(queue.pop().map(|r| r.key), Some("c".into()));
assert!(queue.pop().is_none());
}
#[test]
fn test_end_frame_drops_unwanted_results_but_keeps_prefetch() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("a.pdf");
std::fs::write(&path, sample_pdf(&[(400.0, 300.0, 0), (400.0, 300.0, 0)])).unwrap();
let (wake_tx, wake_rx) = mpsc::channel();
let mut service = PdfRenderService::new(move || {
let _ = wake_tx.send(());
});
let px = DISPLAY_RENDER_PIXELS;
let wait = || {
wake_rx
.recv_timeout(std::time::Duration::from_secs(20))
.unwrap()
};
// Bild 1 braucht Seite 1; vorab gerendert wird Seite 2
service.begin_frame();
assert!(service.take("a#0", path.clone(), 0, px).is_none());
service.prefetch("a#1", path.clone(), 1);
service.end_frame();
wait();
wait();
// Bild 2 braucht keine Seite mehr (weggescrollt): Seite 1 wird verworfen,
// die vorab gerenderte Seite 2 bleibt
service.begin_frame();
service.collect();
service.end_frame();
assert!(!service.ready.contains_key("a#0"));
assert!(service.ready.contains_key("a#1"));
assert!(service.pending.is_empty());
service.begin_frame();
assert!(service.take("a#1", path.clone(), 1, px).unwrap().is_ok());
// Seite 1 wird bei Bedarf neu gerendert
assert!(service.take("a#0", path.clone(), 0, px).is_none());
service.end_frame();
wait();
assert!(service.take("a#0", path, 0, px).unwrap().is_ok());
}
#[test] #[test]
fn test_parse_page_key() { fn test_parse_page_key() {
assert_eq!( assert_eq!(
+14 -1
View File
@@ -4281,6 +4281,18 @@ pub fn render_ui(
ui.label(egui::RichText::new(format!("({})", runtime_name)).color(egui::Color32::from_gray(150))); ui.label(egui::RichText::new(format!("({})", runtime_name)).color(egui::Color32::from_gray(150)));
}); });
if !updater.runtime().self_update_allowed() {
ui.add_space(8.0);
ui.label(
egui::RichText::new(t!(
"Diese Version wird über die Software-Verwaltung aktualisiert \
(z. B. GNOME Software oder „flatpak update“)."
))
.color(egui::Color32::from_gray(210)),
);
return;
}
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.label(egui::RichText::new(t!("Server:")).color(egui::Color32::from_gray(180))); ui.label(egui::RichText::new(t!("Server:")).color(egui::Color32::from_gray(180)));
ui.label(egui::RichText::new(format!("{}/{}/{}", updater.config().server_url, updater.config().repo_owner, updater.config().repo_name)).color(egui::Color32::from_gray(210))); ui.label(egui::RichText::new(format!("{}/{}/{}", updater.config().server_url, updater.config().repo_owner, updater.config().repo_name)).color(egui::Color32::from_gray(210)));
@@ -4446,7 +4458,8 @@ pub fn render_ui(
let action_label = match updater.runtime() { let action_label = match updater.runtime() {
crate::updater::RuntimeEnvironment::AppImage { .. } => t!("🚀 AppImage aktualisieren & ausführen"), crate::updater::RuntimeEnvironment::AppImage { .. } => t!("🚀 AppImage aktualisieren & ausführen"),
crate::updater::RuntimeEnvironment::DebPackage { .. } => t!("📦 Paket mit Software-Center öffnen / installieren"), crate::updater::RuntimeEnvironment::DebPackage { .. } => t!("📦 Paket mit Software-Center öffnen / installieren"),
crate::updater::RuntimeEnvironment::PortableOrDev { .. } => t!("📂 Im Dateimanager anzeigen"), crate::updater::RuntimeEnvironment::PortableOrDev { .. }
| crate::updater::RuntimeEnvironment::Flatpak => t!("📂 Im Dateimanager anzeigen"),
}; };
ui.horizontal(|ui| { ui.horizontal(|ui| {
+41
View File
@@ -118,11 +118,20 @@ pub enum RuntimeEnvironment {
DebPackage { binary_path: PathBuf }, DebPackage { binary_path: PathBuf },
/// Aus Quellcode / Entwicklungs-Binary (z.B. target/debug oder target/release) /// Aus Quellcode / Entwicklungs-Binary (z.B. target/debug oder target/release)
PortableOrDev { binary_path: PathBuf }, PortableOrDev { binary_path: PathBuf },
/// Als Flatpak installiert: Updates kommen über die Software-Verwaltung, ink-notes lädt
/// selbst nichts herunter (Vorgabe von Flathub)
Flatpak,
} }
impl RuntimeEnvironment { impl RuntimeEnvironment {
/// Erkennt die aktuelle Laufzeitumgebung anhand der Umgebung und des Binärpfads. /// Erkennt die aktuelle Laufzeitumgebung anhand der Umgebung und des Binärpfads.
pub fn detect() -> Self { pub fn detect() -> Self {
if is_flatpak(
std::env::var("FLATPAK_ID").ok().as_deref(),
Path::new("/.flatpak-info").exists(),
) {
return RuntimeEnvironment::Flatpak;
}
let appimage_var = std::env::var("APPIMAGE").ok(); let appimage_var = std::env::var("APPIMAGE").ok();
let current_exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("ink-notes")); let current_exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("ink-notes"));
Self::detect_from(appimage_var.as_deref(), &current_exe) Self::detect_from(appimage_var.as_deref(), &current_exe)
@@ -156,8 +165,20 @@ impl RuntimeEnvironment {
RuntimeEnvironment::AppImage { .. } => t!("AppImage (Portabel)"), RuntimeEnvironment::AppImage { .. } => t!("AppImage (Portabel)"),
RuntimeEnvironment::DebPackage { .. } => t!("System-Paket (.deb)"), RuntimeEnvironment::DebPackage { .. } => t!("System-Paket (.deb)"),
RuntimeEnvironment::PortableOrDev { .. } => t!("Entwicklung / Lokal"), RuntimeEnvironment::PortableOrDev { .. } => t!("Entwicklung / Lokal"),
RuntimeEnvironment::Flatpak => "Flatpak",
} }
} }
/// Ob ink-notes selbst nach Updates suchen und sie installieren darf
pub fn self_update_allowed(&self) -> bool {
!matches!(self, RuntimeEnvironment::Flatpak)
}
}
/// Läuft ink-notes in einer Flatpak-Sandbox (`FLATPAK_ID` gesetzt oder `/.flatpak-info`
/// vorhanden)?
pub fn is_flatpak(flatpak_id: Option<&str>, info_file_exists: bool) -> bool {
flatpak_id.is_some_and(|id| !id.trim().is_empty()) || info_file_exists
} }
/// Bereinigt und validiert einen Asset-Dateinamen gegen Path Traversal Angriffe. /// Bereinigt und validiert einen Asset-Dateinamen gegen Path Traversal Angriffe.
@@ -578,6 +599,9 @@ impl UpdaterController {
/// Fragt im Hintergrund die Gitea-API ab. /// Fragt im Hintergrund die Gitea-API ab.
pub fn check_for_updates(&mut self) { pub fn check_for_updates(&mut self) {
if !self.runtime.self_update_allowed() {
return;
}
if matches!( if matches!(
self.status, self.status,
UpdaterStatus::Checking | UpdaterStatus::Downloading { .. } UpdaterStatus::Checking | UpdaterStatus::Downloading { .. }
@@ -887,6 +911,10 @@ impl UpdaterController {
} }
Ok(()) Ok(())
} }
RuntimeEnvironment::Flatpak => Err(t!(
"Die Flatpak-Version wird über die Software-Verwaltung aktualisiert."
)
.to_string()),
} }
} }
@@ -1074,6 +1102,19 @@ dcdeb7ac87d9e6cb8022e5a71bc7f2c56d779b4fce4efdd5bae5be3b0214884a ink-notes_0.1.
); );
} }
#[test]
fn test_flatpak_detection_disables_self_update() {
assert!(is_flatpak(Some("eu.pansi.ink-notes"), false));
assert!(is_flatpak(None, true));
assert!(!is_flatpak(Some(" "), false));
assert!(!is_flatpak(None, false));
assert!(!RuntimeEnvironment::Flatpak.self_update_allowed());
assert!(
RuntimeEnvironment::detect_from(None, Path::new("/usr/bin/ink-notes"))
.self_update_allowed()
);
}
#[test] #[test]
fn test_https_enforcement() { fn test_https_enforcement() {
let config = UpdaterConfig { let config = UpdaterConfig {
+33
View File
@@ -0,0 +1,33 @@
//! Das Flatpak baut ohne Netz aus `packaging/flatpak/cargo-sources.json`; jede Abhängigkeit
//! aus `Cargo.lock` muss dort stehen, sonst schlägt der Flatpak-Build fehl.
#[test]
fn cargo_sources_cover_cargo_lock() {
let lock = include_str!("../Cargo.lock");
let sources = include_str!("../packaging/flatpak/cargo-sources.json");
let mut missing = Vec::new();
for package in lock.split("[[package]]").skip(1) {
let field = |key: &str| {
package.lines().find_map(|line| {
line.strip_prefix(key)
.map(|rest| rest.trim().trim_matches('"').to_string())
})
};
// Nur Pakete aus crates.io (das eigene Paket hat keine Quelle)
if field("source = ").is_none() {
continue;
}
let (Some(name), Some(version)) = (field("name = "), field("version = ")) else {
continue;
};
let crate_file = format!("/{name}-{version}.crate\"");
if !sources.contains(&crate_file) {
missing.push(format!("{name} {version}"));
}
}
assert!(
missing.is_empty(),
"cargo-sources.json ist veraltet (flatpak-cargo-generator neu ausführen), es fehlen: {}",
missing.join(", ")
);
}