diff --git a/.github/workflows/build-production-apk.yml b/.github/workflows/build-production-apk.yml
index e742e22..da60748 100644
--- a/.github/workflows/build-production-apk.yml
+++ b/.github/workflows/build-production-apk.yml
@@ -61,11 +61,11 @@ jobs:
run: |
mkdir -p apk-output
- # Standard Flavor - Universal APK
+ # Standard Flavor
cp android/app/build/outputs/apk/standard/release/app-standard-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard.apk
- # F-Droid Flavor - Universal APK
+ # F-Droid Flavor
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk
diff --git a/.github/workflows/pr-build-check.yml b/.github/workflows/pr-build-check.yml
index fd0fc2f..698116f 100644
--- a/.github/workflows/pr-build-check.yml
+++ b/.github/workflows/pr-build-check.yml
@@ -18,7 +18,7 @@ jobs:
distribution: 'temurin'
java-version: '17'
- name: Gradle Cache
- uses: actions/cache@v3
+ uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -69,14 +69,14 @@ jobs:
continue-on-error: true
- name: Build-Ergebnis pruefen
run: |
- if [ -f "android/app/build/outputs/apk/standard/debug/app-standard-universal-debug.apk" ]; then
+ if [ -f "android/app/build/outputs/apk/standard/debug/app-standard-debug.apk" ]; then
echo "✅ Standard Debug APK erfolgreich gebaut"
ls -lh android/app/build/outputs/apk/standard/debug/*.apk
else
echo "❌ Standard Debug APK Build fehlgeschlagen"
exit 1
fi
- if [ -f "android/app/build/outputs/apk/fdroid/debug/app-fdroid-universal-debug.apk" ]; then
+ if [ -f "android/app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk" ]; then
echo "✅ F-Droid Debug APK erfolgreich gebaut"
ls -lh android/app/build/outputs/apk/fdroid/debug/*.apk
else
diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md
index 237af28..77004c3 100644
--- a/CHANGELOG.de.md
+++ b/CHANGELOG.de.md
@@ -8,6 +8,83 @@ Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
+## [1.8.1] - 2026-02-11
+
+### 🛠️ Bugfix & Polish Release
+
+Checklisten-Fixes, Widget-Verbesserungen, Sync-Härtung und Code-Qualität.
+
+### 🐛 Fehlerbehebungen
+
+**Checklisten-Sortierung Persistenz** ([7dbc06d](https://github.com/inventory69/simple-notes-sync/commit/7dbc06d))
+- Sortier-Option wurde beim erneuten Öffnen einer Checkliste nicht angewendet
+- Ursache: `sortChecklistItems()` sortierte immer unchecked-first statt `_lastChecklistSortOption` zu lesen
+- Alle Sortier-Modi werden nun korrekt wiederhergestellt (Manuell, Alphabetisch, Unchecked/Checked First)
+
+**Widget-Scroll bei Standard-Größe** ([c72b3fe](https://github.com/inventory69/simple-notes-sync/commit/c72b3fe))
+- Scrollen funktionierte nicht bei Standard-3×2-Widget-Größe (110–150dp Höhe)
+- Neue Größenklassen `NARROW_SCROLL` und `WIDE_SCROLL` mit 150dp-Schwelle
+- `clickable`-Modifier bei entsperrten Checklisten entfernt, um Scrollen zu ermöglichen
+
+**Auto-Sync Toast entfernt** ([fe6935a](https://github.com/inventory69/simple-notes-sync/commit/fe6935a))
+- Unerwartete Toast-Benachrichtigung bei automatischem Hintergrund-Sync entfernt
+- Stiller Auto-Sync bleibt still; nur Fehler werden angezeigt
+
+**Gradient- & Drag-Regression** ([24fe32a](https://github.com/inventory69/simple-notes-sync/commit/24fe32a))
+- Gradient-Overlay-Regression bei langen Checklisten-Items behoben
+- Drag-and-Drop-Flackern beim Verschieben zwischen Bereichen behoben
+
+### 🆕 Neue Funktionen
+
+**Widget-Checklisten: Sortierung & Trennlinien** ([66d98c0](https://github.com/inventory69/simple-notes-sync/commit/66d98c0))
+- Widgets übernehmen die gespeicherte Sortier-Option aus dem Editor
+- Visuelle Trennlinie zwischen unerledigten/erledigten Items (MANUAL & UNCHECKED_FIRST)
+- Auto-Sortierung beim Abhaken von Checkboxen im Widget
+- Emoji-Änderung: ✅ → ☑️ für erledigte Items
+
+**Checklisten-Vorschau-Sortierung** ([2c43b47](https://github.com/inventory69/simple-notes-sync/commit/2c43b47))
+- Hauptbildschirm-Vorschau (NoteCard, NoteCardCompact, NoteCardGrid) zeigt gespeicherte Sortierung
+- Neuer `ChecklistPreviewHelper` mit geteilter Sortier-Logik
+
+**Auto-Scroll bei Zeilenumbruch** ([3e4b1bd](https://github.com/inventory69/simple-notes-sync/commit/3e4b1bd))
+- Checklisten-Editor scrollt automatisch wenn Text in eine neue Zeile umbricht
+- Cursor bleibt am unteren Rand sichtbar während der Eingabe
+
+**Separator Drag Cross-Boundary** ([7b55811](https://github.com/inventory69/simple-notes-sync/commit/7b55811))
+- Drag-and-Drop funktioniert nun über die Checked/Unchecked-Trennlinie hinweg
+- Items wechseln automatisch ihren Status beim Verschieben über die Grenze
+- Extrahiertes `DraggableChecklistItem`-Composable für Wiederverwendbarkeit
+
+### 🔄 Verbesserungen
+
+**Sync-Ratenlimit & Akkuschutz** ([ffe0e46](https://github.com/inventory69/simple-notes-sync/commit/ffe0e46), [a1a574a](https://github.com/inventory69/simple-notes-sync/commit/a1a574a))
+- Globaler 30-Sekunden-Cooldown zwischen Sync-Operationen (Auto/WiFi/Periodisch)
+- onSave-Syncs umgehen den globalen Cooldown (behalten eigenen 5s-Throttle)
+- Neuer `SyncStateManager`-Singleton für zentrales State-Tracking
+- Verhindert Akkuverbrauch durch schnelle aufeinanderfolgende Syncs
+
+**Toast → Banner-Migration** ([27e6b9d](https://github.com/inventory69/simple-notes-sync/commit/27e6b9d))
+- Alle nicht-interaktiven Benachrichtigungen auf einheitliches Banner-System migriert
+- Server-Lösch-Ergebnisse als INFO/ERROR-Banner angezeigt
+- INFO-Phase zu SyncPhase-Enum mit Auto-Hide (2,5s) hinzugefügt
+- Snackbars mit Undo-Aktionen bleiben unverändert
+
+**ProGuard-Regeln Audit** ([6356173](https://github.com/inventory69/simple-notes-sync/commit/6356173))
+- Fehlende Keep-Regeln für Widget-ActionCallback-Klassen hinzugefügt
+- Compose-spezifische ProGuard-Regeln hinzugefügt
+- Verhindert ClassNotFoundException in Release-Builds
+
+### 🧹 Code-Qualität
+
+**Detekt-Compliance** ([1a6617a](https://github.com/inventory69/simple-notes-sync/commit/1a6617a))
+- Alle 12 Detekt-Findings behoben (0 Issues verbleibend)
+- `NoteEditorViewModel.loadNote()` refactored um Verschachtelungstiefe zu reduzieren
+- Konstanten für Magic Numbers im Editor extrahiert
+- Unbenutzte Imports aus `UpdateChangelogSheet` entfernt
+- `maxIssues: 0` in Detekt-Konfiguration gesetzt
+
+---
+
## [1.8.0] - 2026-02-10
### 🚨 CRITICAL BUGFIX (Tag neu erstellt)
@@ -891,6 +968,21 @@ Das komplette UI wurde von XML-Views auf Jetpack Compose migriert. Die App ist j
---
+[1.8.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.1
+[1.8.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.0
+[1.7.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.2
+[1.7.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.1
+[1.7.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.0
+[1.6.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.1
+[1.6.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.0
+[1.5.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.5.0
+[1.4.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.1
+[1.4.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.0
+[1.3.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.2
+[1.3.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.1
+[1.3.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.0
+[1.2.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.2
+[1.2.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.1
[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0
[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d5806a..45a8394 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,83 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
+## [1.8.1] - 2026-02-11
+
+### 🛠️ Bugfix & Polish Release
+
+Checklist fixes, widget improvements, sync hardening, and code quality cleanup.
+
+### 🐛 Bug Fixes
+
+**Checklist Sort Persistence** ([7dbc06d](https://github.com/inventory69/simple-notes-sync/commit/7dbc06d))
+- Fixed sort option not applied when reopening a checklist
+- Root cause: `sortChecklistItems()` always sorted unchecked-first instead of reading `_lastChecklistSortOption`
+- Now correctly restores all sort modes (Manual, Alphabetical, Unchecked/Checked First)
+
+**Widget Scroll on Standard Size** ([c72b3fe](https://github.com/inventory69/simple-notes-sync/commit/c72b3fe))
+- Fixed scroll not working on standard 3×2 widget size (110–150dp height)
+- Added `NARROW_SCROLL` and `WIDE_SCROLL` size classes with 150dp threshold
+- Removed `clickable` modifier from unlocked checklists to enable scrolling
+
+**Auto-Sync Toast Removed** ([fe6935a](https://github.com/inventory69/simple-notes-sync/commit/fe6935a))
+- Removed unexpected toast notification on automatic background sync
+- Silent auto-sync stays silent; only errors are shown
+
+**Gradient & Drag Regression** ([24fe32a](https://github.com/inventory69/simple-notes-sync/commit/24fe32a))
+- Fixed gradient overlay regression on long checklist items
+- Fixed drag-and-drop flicker when moving items between boundaries
+
+### 🆕 New Features
+
+**Widget Checklist Sorting & Separators** ([66d98c0](https://github.com/inventory69/simple-notes-sync/commit/66d98c0))
+- Widgets now apply saved sort option from the editor
+- Visual separator between unchecked/checked items (MANUAL & UNCHECKED_FIRST modes)
+- Auto-sort when toggling checkboxes in the widget
+- Changed ✅ → ☑️ emoji for checked items
+
+**Checklist Preview Sorting** ([2c43b47](https://github.com/inventory69/simple-notes-sync/commit/2c43b47))
+- Main screen preview (NoteCard, NoteCardCompact, NoteCardGrid) now respects saved sort option
+- New `ChecklistPreviewHelper` with shared sorting logic
+
+**Auto-Scroll on Line Wrap** ([3e4b1bd](https://github.com/inventory69/simple-notes-sync/commit/3e4b1bd))
+- Checklist editor auto-scrolls when typing causes text to wrap to a new line
+- Keeps cursor visible at bottom of list during editing
+
+**Separator Drag Cross-Boundary** ([7b55811](https://github.com/inventory69/simple-notes-sync/commit/7b55811))
+- Drag-and-drop now works across the checked/unchecked separator
+- Items auto-toggle their checked state when dragged across boundaries
+- Extracted `DraggableChecklistItem` composable for reusability
+
+### 🔄 Improvements
+
+**Sync Rate-Limiting & Battery Protection** ([ffe0e46](https://github.com/inventory69/simple-notes-sync/commit/ffe0e46), [a1a574a](https://github.com/inventory69/simple-notes-sync/commit/a1a574a))
+- Global 30-second cooldown between sync operations (auto/WiFi/periodic)
+- onSave syncs bypass global cooldown (retain own 5s throttle)
+- New `SyncStateManager` singleton for centralized state tracking
+- Prevents battery drain from rapid successive syncs
+
+**Toast → Banner Migration** ([27e6b9d](https://github.com/inventory69/simple-notes-sync/commit/27e6b9d))
+- All non-interactive notifications migrated to unified Banner system
+- Server-delete results show as INFO/ERROR banners
+- Added INFO phase to SyncPhase enum with auto-hide (2.5s)
+- Snackbars with Undo actions remain unchanged
+
+**ProGuard Rules Audit** ([6356173](https://github.com/inventory69/simple-notes-sync/commit/6356173))
+- Added missing keep rules for Widget ActionCallback classes
+- Added Compose-specific ProGuard rules
+- Prevents ClassNotFoundException in release builds
+
+### 🧹 Code Quality
+
+**Detekt Compliance** ([1a6617a](https://github.com/inventory69/simple-notes-sync/commit/1a6617a))
+- Resolved all 12 detekt findings (0 issues remaining)
+- Refactored `NoteEditorViewModel.loadNote()` to reduce nesting depth
+- Extracted constants for magic numbers in editor
+- Removed unused imports from `UpdateChangelogSheet`
+- Set `maxIssues: 0` in detekt config
+
+---
+
## [1.8.0] - 2026-02-10
### 🚨 CRITICAL BUGFIX (Tag recreated)
@@ -890,6 +967,21 @@ The complete UI has been migrated from XML Views to Jetpack Compose. The app is
---
+[1.8.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.1
+[1.8.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.0
+[1.7.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.2
+[1.7.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.1
+[1.7.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.0
+[1.6.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.1
+[1.6.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.0
+[1.5.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.5.0
+[1.4.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.1
+[1.4.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.0
+[1.3.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.2
+[1.3.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.1
+[1.3.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.0
+[1.2.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.2
+[1.2.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.1
[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0
[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2cd591e..94da28d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -94,10 +94,10 @@ Nutze die [Feature Request Template](https://github.com/inventory69/simple-notes
Dokumentations-Verbesserungen sind auch Contributions!
**Dateien:**
-- `README.md` / `README.en.md` - Übersicht
-- `QUICKSTART.md` / `QUICKSTART.en.md` - Schritt-für-Schritt Anleitung
-- `DOCS.md` / `DOCS.en.md` - Technische Details
-- `server/README.md` / `server/README.en.md` - Server Setup
+- `README.de.md` / `README.md` - Übersicht
+- `QUICKSTART.de.md` / `QUICKSTART.md` - Schritt-für-Schritt Anleitung
+- `docs/DOCS.de.md` / `docs/DOCS.md` - Technische Details
+- `server/README.de.md` / `server/README.md` - Server Setup
**Bitte:** Halte beide Sprachen (DE/EN) synchron!
@@ -219,10 +219,10 @@ Use the [Feature Request Template](https://github.com/inventory69/simple-notes-s
Documentation improvements are also contributions!
**Files:**
-- `README.md` / `README.en.md` - Overview
-- `QUICKSTART.md` / `QUICKSTART.en.md` - Step-by-step guide
-- `DOCS.md` / `DOCS.en.md` - Technical details
-- `server/README.md` / `server/README.en.md` - Server setup
+- `README.de.md` / `README.md` - Overview
+- `QUICKSTART.de.md` / `QUICKSTART.md` - Step-by-step guide
+- `docs/DOCS.de.md` / `docs/DOCS.md` - Technical details
+- `server/README.de.md` / `server/README.md` - Server setup
**Please:** Keep both languages (DE/EN) in sync!
@@ -260,4 +260,4 @@ By contributing, you agree that your code will be published under the [MIT Licen
Öffne ein [Issue](https://github.com/inventory69/simple-notes-sync/issues) oder nutze die [Question Template](https://github.com/inventory69/simple-notes-sync/issues/new/choose).
-**Frohe Weihnachten & Happy Coding! 🎄**
+**Happy Coding! 🚀**
diff --git a/QUICKSTART.de.md b/QUICKSTART.de.md
index c6ac210..4e6202c 100644
--- a/QUICKSTART.de.md
+++ b/QUICKSTART.de.md
@@ -8,7 +8,7 @@
## Voraussetzungen
-- ✅ Android 8.0+ Smartphone/Tablet
+- ✅ Android 7.0+ Smartphone/Tablet
- ✅ WLAN-Verbindung
- ✅ Eigener Server mit Docker (optional - für Self-Hosting)
@@ -52,7 +52,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
### Schritt 2: App installieren
1. **APK herunterladen:** [Neueste Version](https://github.com/inventory69/simple-notes-sync/releases/latest)
- - Wähle: `simple-notes-sync-vX.X.X-standard-universal.apk`
+ - Wähle: `simple-notes-sync-vX.X.X-standard.apk`
2. **Installation erlauben:**
- Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren
@@ -261,7 +261,7 @@ Für zuverlässigen Auto-Sync:
## 🆘 Weitere Hilfe
- **GitHub Issues:** [Problem melden](https://github.com/inventory69/simple-notes-sync/issues)
-- **Vollständige Docs:** [DOCS.md](DOCS.md)
+- **Vollständige Docs:** [DOCS.md](docs/DOCS.md)
- **Server Setup Details:** [server/README.md](server/README.md)
---
diff --git a/QUICKSTART.md b/QUICKSTART.md
index 4010d1b..de8b68c 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -8,7 +8,7 @@
## Prerequisites
-- ✅ Android 8.0+ smartphone/tablet
+- ✅ Android 7.0+ smartphone/tablet
- ✅ WiFi connection
- ✅ Own server with Docker (optional - for self-hosting)
@@ -52,7 +52,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
### Step 2: Install App
1. **Download APK:** [Latest version](https://github.com/inventory69/simple-notes-sync/releases/latest)
- - Choose: `simple-notes-sync-vX.X.X-standard-universal.apk`
+ - Choose: `simple-notes-sync-vX.X.X-standard.apk`
2. **Allow installation:**
- Android: Settings → Security → Enable "Unknown sources" for your browser
@@ -77,7 +77,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
> **💡 Note:** Enter only the base URL (without `/notes`). The app automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown export.
-4. **Press "Test connection"****
+4. **Press "Test connection"**
- ✅ Success? → Continue to step 4
- ❌ Error? → See [Troubleshooting](#troubleshooting)
@@ -261,8 +261,8 @@ For reliable auto-sync:
## 🆘 Further Help
- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues)
-- **Complete docs:** [DOCS.en.md](DOCS.en.md)
-- **Server setup details:** [server/README.en.md](server/README.en.md)
+- **Complete docs:** [DOCS.md](docs/DOCS.md)
+- **Server setup details:** [server/README.md](server/README.md)
---
diff --git a/README.de.md b/README.de.md
index 72c086b..3ce6431 100644
--- a/README.de.md
+++ b/README.de.md
@@ -8,7 +8,7 @@
-[](https://www.android.com/)
+[](https://www.android.com/)
[](https://kotlinlang.org/)
[](https://developer.android.com/compose/)
[](https://m3.material.io/)
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" />
- 📝 **Offline-first** – Funktioniert ohne Internet
- 📊 **Flexible Ansichten** – Listen- und Grid-Layout
- ✅ **Checklisten** – Tap-to-Check, Drag & Drop
-- 🌍 **Mehrsprachig** – Deutsch/Englisch mit Sprachauswahl
- 🔄 **Konfigurierbare Sync-Trigger** – onSave, onResume, WiFi, periodisch (15/30/60 Min), Boot
+- 📌 **Widgets** – Home-Screen Quick-Note und Notizlisten-Widget
+- 🔀 **Smartes Sortieren** – Nach Titel, Änderungsdatum, Erstelldatum, Farbe
+- ⚡ **Paralleler Sync** – Lädt bis zu 5 Notizen gleichzeitig herunter
+- 🌍 **Mehrsprachig** – Deutsch/Englisch mit Sprachauswahl
- 🔒 **Self-hosted** – Deine Daten bleiben bei dir (WebDAV)
- 💾 **Lokales Backup** – Export/Import als JSON-Datei (optional verschlüsselt)
- 🖥️ **Desktop-Integration** – Markdown-Export für Obsidian, VS Code, Typora
-- 🔋 **Akkuschonend** – ~0.2% mit Defaults, bis zu ~1.0% mit Periodic Sync
- 🎨 **Material Design 3** – Dynamischer Dark/Light Mode & Farben
➡️ **Vollständige Feature-Liste:** [docs/FEATURES.de.md](docs/FEATURES.de.md)
@@ -138,6 +140,6 @@ MIT License – siehe [LICENSE](LICENSE)
-**v1.7.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
+**v1.8.1** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
diff --git a/README.md b/README.md
index 58ac206..9d3efda 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-[](https://www.android.com/)
+[](https://www.android.com/)
[](https://kotlinlang.org/)
[](https://developer.android.com/compose/)
[](https://m3.material.io/)
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" />
- 📝 **Offline-first** - Works without internet
- 📊 **Flexible views** - Switch between list and grid layout
- ✅ **Checklists** - Tap-to-check, drag & drop
-- 🌍 **Multilingual** - English/German with language selector
- 🔄 **Configurable sync triggers** - onSave, onResume, WiFi-connect, periodic (15/30/60 min), boot
+- 📌 **Widgets** - Home screen quick-note and note list widgets
+- 🔀 **Smart sorting** - By title, date modified, date created, color
+- ⚡ **Parallel sync** - Downloads up to 5 notes simultaneously
+- 🌍 **Multilingual** - English/German with language selector
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
- 💾 **Local backup** - Export/Import as JSON file (encryption available)
- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
-- 🔋 **Battery-friendly** - ~0.2% with defaults, up to ~1.0% with periodic sync
- 🎨 **Material Design 3** - Dynamic dark/light mode & colors based on system settings
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
@@ -148,6 +150,6 @@ MIT License - see [LICENSE](LICENSE)
-**v1.7.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
+**v1.8.1** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 117f9d4..455fcab 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -20,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
- versionCode = 20 // 🎉 v1.8.0: Widgets, Sorting, UI Polish, Post-Update Changelog
- versionName = "1.8.0" // 🎉 v1.8.0: Major Feature Release
+ versionCode = 21 // 🐛 v1.8.1: Checklist Fixes, Widget Sorting, ProGuard Audit
+ versionName = "1.8.1" // 🐛 v1.8.1: Bugfix & Polish Release
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
index 2da83ec..076e42a 100644
--- a/android/app/proguard-rules.pro
+++ b/android/app/proguard-rules.pro
@@ -77,3 +77,23 @@
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
# This class only exists on API 35+ but Compose handles the fallback gracefully
-dontwarn android.text.Layout$TextInclusionStrategy
+
+# ═══════════════════════════════════════════════════════════════════════
+# v1.8.1: Widget & Compose Fixes
+# ═══════════════════════════════════════════════════════════════════════
+
+# Glance Widget ActionCallbacks (instanziiert via Reflection durch actionRunCallback
())
+# Ohne diese Rule findet R8 die Klassen nicht zur Laufzeit → Widget-Crash
+-keep class dev.dettmer.simplenotes.widget.*Action { *; }
+-keep class dev.dettmer.simplenotes.widget.*Receiver { *; }
+
+# Glance Widget State (Preferences-basiert, intern via Reflection)
+-keep class androidx.glance.appwidget.state.** { *; }
+-keep class androidx.datastore.preferences.** { *; }
+
+# Compose Text Layout: Verhindert dass R8 onTextLayout-Callbacks
+# als Side-Effect-Free optimiert (behebt Gradient-Regression)
+-keepclassmembers class androidx.compose.foundation.text.** {
+ ;
+}
+-keep class androidx.compose.ui.text.TextLayoutResult { *; }
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
index 467cc68..0245293 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
@@ -24,7 +24,9 @@ data class Note(
val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,
// v1.4.0: Checklisten-Felder
val noteType: NoteType = NoteType.TEXT,
- val checklistItems: List? = null
+ val checklistItems: List? = null,
+ // 🆕 v1.8.1 (IMPL_03): Persistierte Sortierung
+ val checklistSortOption: String? = null
) {
/**
* Serialisiert Note zu JSON
@@ -71,13 +73,20 @@ data class Note(
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
*/
fun toMarkdown(): String {
+ // 🆕 v1.8.1 (IMPL_03): Sortierung im Frontmatter
+ val sortLine = if (noteType == NoteType.CHECKLIST && checklistSortOption != null) {
+ "\nsort: $checklistSortOption"
+ } else {
+ ""
+ }
+
val header = """
---
id: $id
created: ${formatISO8601(createdAt)}
updated: ${formatISO8601(updatedAt)}
device: $deviceId
-type: ${noteType.name.lowercase()}
+type: ${noteType.name.lowercase()}$sortLine
---
# $title
@@ -119,6 +128,14 @@ type: ${noteType.name.lowercase()}
NoteType.TEXT
}
+ // 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung laden
+ val checklistSortOption = if (jsonObject.has("checklistSortOption") &&
+ !jsonObject.get("checklistSortOption").isJsonNull) {
+ jsonObject.get("checklistSortOption").asString
+ } else {
+ null
+ }
+
// Parsen der Basis-Note
val rawNote = gson.fromJson(json, NoteRaw::class.java)
@@ -158,7 +175,8 @@ type: ${noteType.name.lowercase()}
deviceId = rawNote.deviceId,
syncStatus = rawNote.syncStatus ?: SyncStatus.LOCAL_ONLY,
noteType = noteType,
- checklistItems = checklistItems
+ checklistItems = checklistItems,
+ checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
@@ -246,6 +264,9 @@ type: ${noteType.name.lowercase()}
else -> NoteType.TEXT
}
+ // 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung aus YAML laden
+ val checklistSortOption = metadata["sort"]
+
// v1.4.0: Parse Content basierend auf Typ
// FIX: Robusteres Parsing - suche nach dem Titel-Header und extrahiere den Rest
val titleLineIndex = contentBlock.lines().indexOfFirst { it.startsWith("# ") }
@@ -300,7 +321,8 @@ type: ${noteType.name.lowercase()}
deviceId = metadata["device"] ?: "desktop",
syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
noteType = noteType,
- checklistItems = checklistItems
+ checklistItems = checklistItems,
+ checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt
index f2f25c0..5483509 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncProgress.kt
@@ -52,9 +52,10 @@ data class SyncProgress(
/**
* Ob das Banner sichtbar sein soll
* Silent syncs zeigen nie ein Banner
+ * 🆕 v1.8.1 (IMPL_12): INFO ist immer sichtbar (nicht vom silent-Flag betroffen)
*/
val isVisible: Boolean
- get() = !silent && phase != SyncPhase.IDLE
+ get() = phase == SyncPhase.INFO || (!silent && phase != SyncPhase.IDLE)
/**
* Ob gerade ein aktiver Sync läuft (mit Spinner)
@@ -95,5 +96,8 @@ enum class SyncPhase {
COMPLETED,
/** Sync mit Fehler abgebrochen */
- ERROR
+ ERROR,
+
+ /** 🆕 v1.8.1 (IMPL_12): Kurzfristige Info-Meldung (nicht sync-bezogen) */
+ INFO
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt
index ccdbd52..a98297d 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt
@@ -214,4 +214,91 @@ object SyncStateManager {
}
}
}
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown
+ // ═══════════════════════════════════════════════════════════════════════
+
+ /**
+ * Prüft ob seit dem letzten erfolgreichen Sync-Start genügend Zeit vergangen ist.
+ * Wird von ALLEN Sync-Triggern als erste Prüfung aufgerufen.
+ *
+ * @return true wenn ein neuer Sync erlaubt ist
+ */
+ fun canSyncGlobally(prefs: android.content.SharedPreferences): Boolean {
+ val lastGlobalSync = prefs.getLong(dev.dettmer.simplenotes.utils.Constants.KEY_LAST_GLOBAL_SYNC_TIME, 0)
+ val now = System.currentTimeMillis()
+ val elapsed = now - lastGlobalSync
+
+ if (elapsed < dev.dettmer.simplenotes.utils.Constants.MIN_GLOBAL_SYNC_INTERVAL_MS) {
+ val remainingSec = (dev.dettmer.simplenotes.utils.Constants.MIN_GLOBAL_SYNC_INTERVAL_MS - elapsed) / 1000
+ dev.dettmer.simplenotes.utils.Logger.d(TAG, "⏳ Global sync cooldown active - wait ${remainingSec}s")
+ return false
+ }
+ return true
+ }
+
+ /**
+ * Markiert den aktuellen Zeitpunkt als letzten Sync-Start (global).
+ * Aufzurufen wenn ein Sync tatsächlich startet (nach allen Checks).
+ */
+ fun markGlobalSyncStarted(prefs: android.content.SharedPreferences) {
+ prefs.edit().putLong(dev.dettmer.simplenotes.utils.Constants.KEY_LAST_GLOBAL_SYNC_TIME, System.currentTimeMillis()).apply()
+ }
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // 🆕 v1.8.1 (IMPL_12): Info-Meldungen über das Banner-System
+ // ═══════════════════════════════════════════════════════════════════════
+
+ /**
+ * Zeigt eine kurzfristige Info-Meldung im Banner an.
+ * Wird für nicht-sync-bezogene Benachrichtigungen verwendet
+ * (z.B. Server-Delete-Ergebnisse).
+ *
+ * ACHTUNG: Wenn gerade ein Sync läuft (isSyncing), wird die Meldung
+ * ignoriert — der Sync-Progress hat Vorrang.
+ *
+ * Auto-Hide erfolgt über ComposeMainActivity (2.5s).
+ */
+ fun showInfo(message: String) {
+ synchronized(lock) {
+ // Nicht während aktivem Sync anzeigen — Sync-Fortschritt hat Vorrang
+ if (isSyncing) {
+ Logger.d(TAG, "ℹ️ Info suppressed during sync: $message")
+ return
+ }
+
+ _syncProgress.value = SyncProgress(
+ phase = SyncPhase.INFO,
+ resultMessage = message,
+ silent = false // INFO ist nie silent
+ )
+
+ Logger.d(TAG, "ℹ️ Showing info: $message")
+ }
+ }
+
+ /**
+ * Zeigt eine Fehlermeldung im Banner an, auch außerhalb eines Syncs.
+ * Für nicht-sync-bezogene Fehler (z.B. Server-Delete fehlgeschlagen).
+ *
+ * Auto-Hide erfolgt über ComposeMainActivity (4s).
+ */
+ fun showError(message: String?) {
+ synchronized(lock) {
+ // Nicht während aktivem Sync anzeigen
+ if (isSyncing) {
+ Logger.d(TAG, "❌ Error suppressed during sync: $message")
+ return
+ }
+
+ _syncProgress.value = SyncProgress(
+ phase = SyncPhase.ERROR,
+ resultMessage = message,
+ silent = false
+ )
+
+ Logger.e(TAG, "❌ Showing error: $message")
+ }
+ }
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
index 3863098..1c6f2a8 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
@@ -73,6 +73,7 @@ class SyncWorker(
}
}
+ @Suppress("LongMethod") // Linear sync flow with debug logging — splitting would hurt readability
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "═══════════════════════════════════════")
@@ -104,7 +105,42 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 2: Checking for unsynced changes (Performance Pre-Check)")
+ Logger.d(TAG, "📍 Step 2: SyncStateManager coordination & global cooldown (v1.8.1)")
+ }
+
+ // 🆕 v1.8.1 (IMPL_08): SyncStateManager-Koordination
+ // Verhindert dass Foreground und Background gleichzeitig syncing-State haben
+ val prefs = applicationContext.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
+
+ // 🆕 v1.8.1 (IMPL_08B): onSave-Syncs bypassen den globalen Cooldown
+ // Grund: User hat explizit gespeichert → erwartet zeitnahen Sync
+ // Der eigene 5s-Throttle + isSyncing-Mutex reichen als Schutz
+ val isOnSaveSync = tags.contains(Constants.SYNC_ONSAVE_TAG)
+
+ // Globaler Cooldown-Check (nicht für onSave-Syncs)
+ if (!isOnSaveSync && !SyncStateManager.canSyncGlobally(prefs)) {
+ Logger.d(TAG, "⏭️ SyncWorker: Global sync cooldown active - skipping")
+ if (BuildConfig.DEBUG) {
+ Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (cooldown)")
+ Logger.d(TAG, "═══════════════════════════════════════")
+ }
+ return@withContext Result.success()
+ }
+
+ if (!SyncStateManager.tryStartSync("worker-${tags.firstOrNull() ?: "unknown"}", silent = true)) {
+ Logger.d(TAG, "⏭️ SyncWorker: Another sync already in progress - skipping")
+ if (BuildConfig.DEBUG) {
+ Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (already syncing)")
+ Logger.d(TAG, "═══════════════════════════════════════")
+ }
+ return@withContext Result.success()
+ }
+
+ // Globalen Cooldown markieren
+ SyncStateManager.markGlobalSyncStarted(prefs)
+
+ if (BuildConfig.DEBUG) {
+ Logger.d(TAG, "📍 Step 3: Checking for unsynced changes (Performance Pre-Check)")
}
// 🔥 v1.1.2: Performance-Optimierung - Skip Sync wenn keine lokalen Änderungen
@@ -122,7 +158,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 2.5: Checking sync gate (canSync)")
+ Logger.d(TAG, "📍 Step 4: Checking sync gate (canSync)")
}
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config)
@@ -143,7 +179,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 3: Checking server reachability (Pre-Check)")
+ Logger.d(TAG, "📍 Step 5: Checking server reachability (Pre-Check)")
}
// ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync
@@ -167,7 +203,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 3: Server reachable - proceeding with sync")
+ Logger.d(TAG, "📍 Step 6: Server reachable - proceeding with sync")
Logger.d(TAG, " SyncService: $syncService")
}
@@ -188,7 +224,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 4: Processing result")
+ Logger.d(TAG, "📍 Step 7: Processing result")
Logger.d(
TAG,
"📦 Sync result: success=${result.isSuccess}, " +
@@ -198,10 +234,13 @@ class SyncWorker(
if (result.isSuccess) {
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 5: Success path")
+ Logger.d(TAG, "📍 Step 8: Success path")
}
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
+ // 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
+ SyncStateManager.markCompleted()
+
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
// UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
if (result.syncedCount > 0) {
@@ -248,9 +287,13 @@ class SyncWorker(
Result.success()
} else {
if (BuildConfig.DEBUG) {
- Logger.d(TAG, "📍 Step 5: Failure path")
+ Logger.d(TAG, "📍 Step 8: Failure path")
}
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
+
+ // 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
+ SyncStateManager.markError(result.errorMessage)
+
NotificationHelper.showSyncError(
applicationContext,
result.errorMessage ?: "Unbekannter Fehler"
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
index ea90e82..263252c 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
@@ -27,6 +27,16 @@ class WifiSyncReceiver : BroadcastReceiver() {
return
}
+ // 🆕 v1.8.1 (IMPL_08): Globaler Cooldown (verhindert Doppel-Trigger mit NetworkMonitor)
+ if (!SyncStateManager.canSyncGlobally(prefs)) {
+ return
+ }
+
+ // 🆕 v1.8.1 (IMPL_08): Auch KEY_SYNC_TRIGGER_WIFI_CONNECT prüfen (Konsistenz mit NetworkMonitor)
+ if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, Constants.DEFAULT_TRIGGER_WIFI_CONNECT)) {
+ return
+ }
+
// Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
if (isConnectedToWifi(context)) {
scheduleSyncWork(context)
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt
index 96be346..09d393a 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropListState.kt
@@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
* v1.5.0: NoteEditor Redesign
* v1.8.0: IMPL_023 - Drag & Drop Fix (pointerInput key + Handle-only drag)
* v1.8.0: IMPL_023b - Flicker-Fix (Straddle-Target-Center-Erkennung statt Mittelpunkt)
+ * v1.8.1: IMPL_14 - Separator als eigenes Item, Cross-Boundary-Drag mit Auto-Toggle
*/
class DragDropListState(
private val state: LazyListState,
@@ -36,8 +37,14 @@ class DragDropListState(
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableFloatStateOf(0f)
+ // 🆕 v1.8.1: Item-Größe beim Drag-Start fixieren
+ // Verhindert dass Höhenänderungen die Swap-Erkennung destabilisieren
+ private var draggingItemSize by mutableStateOf(0)
private var overscrollJob by mutableStateOf(null)
+ // 🆕 v1.8.1 IMPL_14: Visual-Index des Separators (-1 = kein Separator)
+ var separatorVisualIndex by mutableStateOf(-1)
+
val draggingItemOffset: Float
get() = draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
@@ -47,9 +54,28 @@ class DragDropListState(
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggingItemIndex }
+ /**
+ * 🆕 v1.8.1 IMPL_14: Visual-Index → Data-Index Konvertierung.
+ * Wenn ein Separator existiert, sind alle Items nach dem Separator um 1 verschoben.
+ */
+ fun visualToDataIndex(visualIndex: Int): Int {
+ if (separatorVisualIndex < 0) return visualIndex
+ return if (visualIndex > separatorVisualIndex) visualIndex - 1 else visualIndex
+ }
+
+ /**
+ * 🆕 v1.8.1 IMPL_14: Data-Index → Visual-Index Konvertierung.
+ */
+ fun dataToVisualIndex(dataIndex: Int): Int {
+ if (separatorVisualIndex < 0) return dataIndex
+ return if (dataIndex >= separatorVisualIndex) dataIndex + 1 else dataIndex
+ }
+
fun onDragStart(offset: Offset, itemIndex: Int) {
draggingItemIndex = itemIndex
- draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f
+ val info = draggingItemLayoutInfo
+ draggingItemInitialOffset = info?.offset?.toFloat() ?: 0f
+ draggingItemSize = info?.size ?: 0
draggingItemDraggedDelta = 0f
}
@@ -57,6 +83,7 @@ class DragDropListState(
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0f
+ draggingItemSize = 0
overscrollJob?.cancel()
}
@@ -65,15 +92,19 @@ class DragDropListState(
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
- val endOffset = startOffset + draggingItem.size
+ // 🆕 v1.8.1: Fixierte Item-Größe für stabile Swap-Erkennung
+ val endOffset = startOffset + draggingItemSize
// 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter
// Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"),
// wird geprüft ob das gezogene Item den MITTELPUNKT des Targets überspannt.
// Dies verhindert Oszillation bei Items unterschiedlicher Größe.
- // Zusätzlich: Nur adjazente Items (Index ± 1) als Swap-Kandidaten.
+ // 🆕 v1.8.1 IMPL_14: Separator überspringen, Adjazenz berücksichtigt Separator-Lücke
val targetItem = state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
- (item.index == draggingItem.index - 1 || item.index == draggingItem.index + 1) &&
+ // Separator überspringen
+ item.index != separatorVisualIndex &&
+ // Nur adjazente Items (Separator-Lücke wird übersprungen)
+ isAdjacentSkippingSeparator(draggingItem.index, item.index) &&
run {
val targetCenter = item.offset + item.size / 2
startOffset < targetCenter && endOffset > targetCenter
@@ -88,16 +119,20 @@ class DragDropListState(
} else {
null
}
+
+ // 🆕 v1.8.1 IMPL_14: Visual-Indizes zu Data-Indizes konvertieren für onMove
+ val fromDataIndex = visualToDataIndex(draggingItem.index)
+ val toDataIndex = visualToDataIndex(targetItem.index)
if (scrollToIndex != null) {
scope.launch {
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
- onMove(draggingItem.index, targetItem.index)
+ onMove(fromDataIndex, toDataIndex)
// 🆕 v1.8.0: IMPL_023b — Index-Update NACH dem Move (verhindert Race-Condition)
draggingItemIndex = targetItem.index
}
} else {
- onMove(draggingItem.index, targetItem.index)
+ onMove(fromDataIndex, toDataIndex)
draggingItemIndex = targetItem.index
}
} else {
@@ -121,6 +156,26 @@ class DragDropListState(
}
}
+ /**
+ * 🆕 v1.8.1 IMPL_14: Prüft ob zwei Visual-Indizes adjazent sind,
+ * wobei der Separator übersprungen wird.
+ * Beispiel: Items bei Visual 1 und Visual 3 sind adjazent wenn Separator bei Visual 2 liegt.
+ */
+ private fun isAdjacentSkippingSeparator(indexA: Int, indexB: Int): Boolean {
+ val diff = kotlin.math.abs(indexA - indexB)
+ if (diff == 1) {
+ // Direkt benachbart — aber NICHT wenn der Separator dazwischen liegt
+ val between = minOf(indexA, indexB) + 1
+ return between != separatorVisualIndex || separatorVisualIndex < 0
+ }
+ if (diff == 2 && separatorVisualIndex >= 0) {
+ // 2 Positionen entfernt — adjazent wenn Separator dazwischen
+ val between = minOf(indexA, indexB) + 1
+ return between == separatorVisualIndex
+ }
+ return false
+ }
+
@Suppress("UnusedPrivateProperty")
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt
index 83c49d6..f071bcc 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt
@@ -1,11 +1,6 @@
package dev.dettmer.simplenotes.ui.editor
-import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInVertically
-import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -19,6 +14,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -70,8 +66,10 @@ import dev.dettmer.simplenotes.utils.showToast
import kotlin.math.roundToInt
private const val LAYOUT_DELAY_MS = 100L
+private const val AUTO_SCROLL_DELAY_MS = 50L
private const val ITEM_CORNER_RADIUS_DP = 8
private const val DRAGGING_ITEM_Z_INDEX = 10f
+private val DRAGGING_ELEVATION_DP = 8.dp
/**
* Main Composable for the Note Editor screen.
@@ -327,6 +325,66 @@ private fun TextNoteContent(
)
}
+/**
+ * 🆕 v1.8.1 IMPL_14: Extrahiertes Composable für ein einzelnes draggbares Checklist-Item.
+ * Entkoppelt von der Separator-Logik — wiederverwendbar für unchecked und checked Items.
+ */
+@Suppress("LongParameterList") // Compose callbacks — cannot be reduced without wrapper class
+@Composable
+private fun LazyItemScope.DraggableChecklistItem(
+ item: ChecklistItemState,
+ visualIndex: Int,
+ dragDropState: DragDropListState,
+ focusNewItemId: String?,
+ onTextChange: (String, String) -> Unit,
+ onCheckedChange: (String, Boolean) -> Unit,
+ onDelete: (String) -> Unit,
+ onAddNewItemAfter: (String) -> Unit,
+ onFocusHandled: () -> Unit,
+ onHeightChanged: () -> Unit, // 🆕 v1.8.1 (IMPL_05)
+) {
+ val isDragging = dragDropState.draggingItemIndex == visualIndex
+ val elevation by animateDpAsState(
+ targetValue = if (isDragging) DRAGGING_ELEVATION_DP else 0.dp,
+ label = "elevation"
+ )
+
+ val shouldFocus = item.id == focusNewItemId
+
+ LaunchedEffect(shouldFocus) {
+ if (shouldFocus) {
+ onFocusHandled()
+ }
+ }
+
+ ChecklistItemRow(
+ item = item,
+ onTextChange = { onTextChange(item.id, it) },
+ onCheckedChange = { onCheckedChange(item.id, it) },
+ onDelete = { onDelete(item.id) },
+ onAddNewItem = { onAddNewItemAfter(item.id) },
+ requestFocus = shouldFocus,
+ isDragging = isDragging,
+ isAnyItemDragging = dragDropState.draggingItemIndex != null,
+ dragModifier = Modifier.dragContainer(dragDropState, visualIndex),
+ onHeightChanged = onHeightChanged, // 🆕 v1.8.1 (IMPL_05)
+ modifier = Modifier
+ .then(if (!isDragging) Modifier.animateItem() else Modifier)
+ .offset {
+ IntOffset(
+ 0,
+ if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
+ )
+ }
+ .zIndex(if (isDragging) DRAGGING_ITEM_Z_INDEX else 0f)
+ .shadow(elevation, shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp))
+ .background(
+ color = MaterialTheme.colorScheme.surface,
+ shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp)
+ )
+ )
+}
+
@Suppress("LongParameterList") // Compose functions commonly have many callback parameters
@Composable
private fun ChecklistEditor(
@@ -351,6 +409,9 @@ private fun ChecklistEditor(
onMove = onMove
)
+ // 🆕 v1.8.1 (IMPL_05): Auto-Scroll bei Zeilenumbruch
+ var scrollToItemIndex by remember { mutableStateOf(null) }
+
// 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen
val uncheckedCount = items.count { !it.isChecked }
val checkedCount = items.count { it.isChecked }
@@ -359,70 +420,78 @@ private fun ChecklistEditor(
val showSeparator = shouldShowSeparator && uncheckedCount > 0 && checkedCount > 0
Column(modifier = modifier) {
+ // 🆕 v1.8.1 IMPL_14: Separator-Position für DragDropState aktualisieren
+ val separatorVisualIndex = if (showSeparator) uncheckedCount else -1
+ LaunchedEffect(separatorVisualIndex) {
+ dragDropState.separatorVisualIndex = separatorVisualIndex
+ }
+
+ // 🆕 v1.8.1 (IMPL_05): Auto-Scroll wenn ein Item durch Zeilenumbruch wächst
+ LaunchedEffect(scrollToItemIndex) {
+ scrollToItemIndex?.let { index ->
+ delay(AUTO_SCROLL_DELAY_MS) // Warten bis Layout-Pass abgeschlossen
+ val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
+ if (index >= lastVisibleIndex - 1) {
+ listState.animateScrollToItem(
+ index = minOf(index + 1, items.size + if (showSeparator) 1 else 0),
+ scrollOffset = 0
+ )
+ }
+ scrollToItemIndex = null
+ }
+ }
+
LazyColumn(
state = listState,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
+ // 🆕 v1.8.1 IMPL_14: Unchecked Items (Visual Index 0..uncheckedCount-1)
itemsIndexed(
- items = items,
+ items = if (showSeparator) items.subList(0, uncheckedCount) else items,
key = { _, item -> item.id }
) { index, item ->
- // 🆕 v1.8.0 (IMPL_017): Separator vor dem ersten Checked-Item
- if (showSeparator && index == uncheckedCount) {
- CheckedItemsSeparator(checkedCount = checkedCount)
- }
-
- val isDragging = dragDropState.draggingItemIndex == index
- val elevation by animateDpAsState(
- targetValue = if (isDragging) 8.dp else 0.dp,
- label = "elevation"
+ DraggableChecklistItem(
+ item = item,
+ visualIndex = index,
+ dragDropState = dragDropState,
+ focusNewItemId = focusNewItemId,
+ onTextChange = onTextChange,
+ onCheckedChange = onCheckedChange,
+ onDelete = onDelete,
+ onAddNewItemAfter = onAddNewItemAfter,
+ onFocusHandled = onFocusHandled,
+ onHeightChanged = { scrollToItemIndex = index } // 🆕 v1.8.1 (IMPL_05)
)
+ }
- val shouldFocus = item.id == focusNewItemId
-
- // v1.5.0: Clear focus request after handling
- LaunchedEffect(shouldFocus) {
- if (shouldFocus) {
- onFocusHandled()
- }
+ // 🆕 v1.8.1 IMPL_14: Separator als eigenes LazyColumn-Item
+ if (showSeparator) {
+ item(key = "separator") {
+ CheckedItemsSeparator(
+ checkedCount = checkedCount,
+ isDragActive = dragDropState.draggingItemIndex != null
+ )
}
- // 🆕 v1.8.0 (IMPL_017): AnimatedVisibility für sanfte Übergänge
- AnimatedVisibility(
- visible = true,
- enter = fadeIn() + slideInVertically(),
- exit = fadeOut() + slideOutVertically()
- ) {
- ChecklistItemRow(
+ // 🆕 v1.8.1 IMPL_14: Checked Items (Visual Index uncheckedCount+1..)
+ itemsIndexed(
+ items = items.subList(uncheckedCount, items.size),
+ key = { _, item -> item.id }
+ ) { index, item ->
+ val visualIndex = uncheckedCount + 1 + index // +1 für Separator
+ DraggableChecklistItem(
item = item,
- onTextChange = { onTextChange(item.id, it) },
- onCheckedChange = { onCheckedChange(item.id, it) },
- onDelete = { onDelete(item.id) },
- onAddNewItem = { onAddNewItemAfter(item.id) },
- requestFocus = shouldFocus,
- // 🆕 v1.8.0: IMPL_023 - Drag state übergeben
- isDragging = isDragging,
- // 🆕 v1.8.0: IMPL_023 - Gradient während Drag ausblenden
- isAnyItemDragging = dragDropState.draggingItemIndex != null,
- // 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle
- dragModifier = Modifier.dragContainer(dragDropState, index),
- modifier = Modifier
- .animateItem() // 🆕 v1.8.0 (IMPL_017): LazyColumn Item-Animation
- .offset {
- IntOffset(
- 0,
- if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
- )
- }
- // 🆕 v1.8.0: IMPL_023 - Gedraggtes Item liegt über anderen
- .zIndex(if (isDragging) DRAGGING_ITEM_Z_INDEX else 0f)
- .shadow(elevation, shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp))
- .background(
- color = MaterialTheme.colorScheme.surface,
- shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp)
- )
+ visualIndex = visualIndex,
+ dragDropState = dragDropState,
+ focusNewItemId = focusNewItemId,
+ onTextChange = onTextChange,
+ onCheckedChange = onCheckedChange,
+ onDelete = onDelete,
+ onAddNewItemAfter = onAddNewItemAfter,
+ onFocusHandled = onFocusHandled,
+ onHeightChanged = { scrollToItemIndex = visualIndex } // 🆕 v1.8.1 (IMPL_05)
)
}
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt
index 8f634a5..4319c19 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt
@@ -13,6 +13,7 @@ import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.storage.NotesStorage
+import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.sync.SyncWorker
import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.utils.Constants
@@ -90,64 +91,92 @@ class NoteEditorViewModel(
val noteTypeString = savedStateHandle.get(ARG_NOTE_TYPE) ?: NoteType.TEXT.name
if (noteId != null) {
- // Load existing note
- existingNote = storage.loadNote(noteId)
- existingNote?.let { note ->
- currentNoteType = note.noteType
- _uiState.update { state ->
- state.copy(
- title = note.title,
- content = note.content,
- noteType = note.noteType,
- isNewNote = false,
- toolbarTitle = if (note.noteType == NoteType.CHECKLIST) {
- ToolbarTitle.EDIT_CHECKLIST
- } else {
- ToolbarTitle.EDIT_NOTE
- }
- )
- }
-
- if (note.noteType == NoteType.CHECKLIST) {
- val items = note.checklistItems?.sortedBy { it.order }?.map {
- ChecklistItemState(
- id = it.id,
- text = it.text,
- isChecked = it.isChecked,
- order = it.order
- )
- } ?: emptyList()
- // 🆕 v1.8.0 (IMPL_017): Sortierung sicherstellen (falls alte Daten unsortiert sind)
- _checklistItems.value = sortChecklistItems(items)
- }
- }
+ loadExistingNote(noteId)
} else {
- // New note
- currentNoteType = try {
- NoteType.valueOf(noteTypeString)
- } catch (e: IllegalArgumentException) {
- Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT: ${e.message}")
- NoteType.TEXT
- }
-
+ initNewNote(noteTypeString)
+ }
+ }
+
+ private fun loadExistingNote(noteId: String) {
+ existingNote = storage.loadNote(noteId)
+ existingNote?.let { note ->
+ currentNoteType = note.noteType
_uiState.update { state ->
state.copy(
- noteType = currentNoteType,
- isNewNote = true,
- toolbarTitle = if (currentNoteType == NoteType.CHECKLIST) {
- ToolbarTitle.NEW_CHECKLIST
+ title = note.title,
+ content = note.content,
+ noteType = note.noteType,
+ isNewNote = false,
+ toolbarTitle = if (note.noteType == NoteType.CHECKLIST) {
+ ToolbarTitle.EDIT_CHECKLIST
} else {
- ToolbarTitle.NEW_NOTE
+ ToolbarTitle.EDIT_NOTE
}
)
}
- // Add first empty item for new checklists
- if (currentNoteType == NoteType.CHECKLIST) {
- _checklistItems.value = listOf(ChecklistItemState.createEmpty(0))
+ if (note.noteType == NoteType.CHECKLIST) {
+ loadChecklistData(note)
}
}
}
+
+ private fun loadChecklistData(note: Note) {
+ // 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung laden
+ note.checklistSortOption?.let { sortName ->
+ _lastChecklistSortOption.value = parseSortOption(sortName)
+ }
+
+ val items = note.checklistItems?.sortedBy { it.order }?.map {
+ ChecklistItemState(
+ id = it.id,
+ text = it.text,
+ isChecked = it.isChecked,
+ order = it.order
+ )
+ } ?: emptyList()
+ // 🆕 v1.8.0 (IMPL_017): Sortierung sicherstellen (falls alte Daten unsortiert sind)
+ _checklistItems.value = sortChecklistItems(items)
+ }
+
+ private fun initNewNote(noteTypeString: String) {
+ currentNoteType = try {
+ NoteType.valueOf(noteTypeString)
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
+ Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT")
+ NoteType.TEXT
+ }
+
+ _uiState.update { state ->
+ state.copy(
+ noteType = currentNoteType,
+ isNewNote = true,
+ toolbarTitle = if (currentNoteType == NoteType.CHECKLIST) {
+ ToolbarTitle.NEW_CHECKLIST
+ } else {
+ ToolbarTitle.NEW_NOTE
+ }
+ )
+ }
+
+ // Add first empty item for new checklists
+ if (currentNoteType == NoteType.CHECKLIST) {
+ _checklistItems.value = listOf(ChecklistItemState.createEmpty(0))
+ }
+ }
+
+ /**
+ * Safely parse a ChecklistSortOption from its string name.
+ * Falls back to MANUAL if the name is unknown (e.g., from older app versions).
+ */
+ private fun parseSortOption(sortName: String): ChecklistSortOption {
+ return try {
+ ChecklistSortOption.valueOf(sortName)
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
+ Logger.w(TAG, "Unknown sort option '$sortName', using MANUAL")
+ ChecklistSortOption.MANUAL
+ }
+ }
// ═══════════════════════════════════════════════════════════════════════
// Actions
@@ -173,11 +202,28 @@ class NoteEditorViewModel(
* 🆕 v1.8.0 (IMPL_017): Sortiert Checklist-Items mit Unchecked oben, Checked unten.
* Stabile Sortierung: Relative Reihenfolge innerhalb jeder Gruppe bleibt erhalten.
*/
+ /**
+ * Sortiert Checklist-Items basierend auf der aktuellen Sortier-Option.
+ * 🆕 v1.8.1 (IMPL_03-FIX): Berücksichtigt jetzt _lastChecklistSortOption
+ * anstatt immer unchecked-first zu sortieren.
+ */
private fun sortChecklistItems(items: List): List {
- val unchecked = items.filter { !it.isChecked }
- val checked = items.filter { it.isChecked }
+ val sorted = when (_lastChecklistSortOption.value) {
+ ChecklistSortOption.MANUAL,
+ ChecklistSortOption.UNCHECKED_FIRST -> {
+ val unchecked = items.filter { !it.isChecked }
+ val checked = items.filter { it.isChecked }
+ unchecked + checked
+ }
+ ChecklistSortOption.CHECKED_FIRST ->
+ items.sortedByDescending { it.isChecked }
+ ChecklistSortOption.ALPHABETICAL_ASC ->
+ items.sortedBy { it.text.lowercase() }
+ ChecklistSortOption.ALPHABETICAL_DESC ->
+ items.sortedByDescending { it.text.lowercase() }
+ }
- return (unchecked + checked).mapIndexed { index, item ->
+ return sorted.mapIndexed { index, item ->
item.copy(order = index)
}
}
@@ -198,13 +244,34 @@ class NoteEditorViewModel(
}
}
+ /**
+ * 🆕 v1.8.1 (IMPL_15): Fügt ein neues Item nach dem angegebenen Item ein.
+ *
+ * Guard: Bei MANUAL/UNCHECKED_FIRST wird sichergestellt, dass das neue (unchecked)
+ * Item nicht innerhalb der checked-Sektion eingefügt wird. Falls das Trigger-Item
+ * checked ist, wird stattdessen vor dem ersten checked Item eingefügt.
+ */
fun addChecklistItemAfter(afterItemId: String): String {
val newItem = ChecklistItemState.createEmpty(0)
_checklistItems.update { items ->
val index = items.indexOfFirst { it.id == afterItemId }
if (index >= 0) {
+ val currentSort = _lastChecklistSortOption.value
+ val hasSeparator = currentSort == ChecklistSortOption.MANUAL ||
+ currentSort == ChecklistSortOption.UNCHECKED_FIRST
+
+ // 🆕 v1.8.1 (IMPL_15): Wenn das Trigger-Item checked ist und ein Separator
+ // existiert, darf das neue unchecked Item nicht in die checked-Sektion.
+ // → Stattdessen vor dem ersten checked Item einfügen.
+ val effectiveIndex = if (hasSeparator && items[index].isChecked) {
+ val firstCheckedIndex = items.indexOfFirst { it.isChecked }
+ if (firstCheckedIndex >= 0) firstCheckedIndex else index + 1
+ } else {
+ index + 1
+ }
+
val newList = items.toMutableList()
- newList.add(index + 1, newItem)
+ newList.add(effectiveIndex, newItem)
// Update order values
newList.mapIndexed { i, item -> item.copy(order = i) }
} else {
@@ -213,12 +280,46 @@ class NoteEditorViewModel(
}
return newItem.id
}
-
+
+ /**
+ * 🆕 v1.8.1 (IMPL_15): Fügt ein neues Item an der semantisch korrekten Position ein.
+ *
+ * Bei MANUAL/UNCHECKED_FIRST: Vor dem ersten checked Item (= direkt über dem Separator).
+ * Bei allen anderen Modi: Am Ende der Liste (kein Separator sichtbar).
+ *
+ * Verhindert, dass checked Items über den Separator springen oder das neue Item
+ * unter dem Separator erscheint.
+ */
fun addChecklistItemAtEnd(): String {
- val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size)
- _checklistItems.update { items -> items + newItem }
+ val newItem = ChecklistItemState.createEmpty(0)
+ _checklistItems.update { items ->
+ val insertIndex = calculateInsertIndexForNewItem(items)
+ val newList = items.toMutableList()
+ newList.add(insertIndex, newItem)
+ newList.mapIndexed { i, item -> item.copy(order = i) }
+ }
return newItem.id
}
+
+ /**
+ * 🆕 v1.8.1 (IMPL_15): Berechnet die korrekte Insert-Position für ein neues unchecked Item.
+ *
+ * - MANUAL / UNCHECKED_FIRST: Vor dem ersten checked Item (direkt über dem Separator)
+ * - Alle anderen Modi: Am Ende der Liste (kein Separator, kein visuelles Problem)
+ *
+ * Falls keine checked Items existieren, wird am Ende eingefügt.
+ */
+ private fun calculateInsertIndexForNewItem(items: List): Int {
+ val currentSort = _lastChecklistSortOption.value
+ return when (currentSort) {
+ ChecklistSortOption.MANUAL,
+ ChecklistSortOption.UNCHECKED_FIRST -> {
+ val firstCheckedIndex = items.indexOfFirst { it.isChecked }
+ if (firstCheckedIndex >= 0) firstCheckedIndex else items.size
+ }
+ else -> items.size
+ }
+ }
fun deleteChecklistItem(itemId: String) {
_checklistItems.update { items ->
@@ -238,15 +339,18 @@ class NoteEditorViewModel(
val fromItem = items.getOrNull(fromIndex) ?: return@update items
val toItem = items.getOrNull(toIndex) ?: return@update items
- // 🆕 v1.8.0 (IMPL_017): Drag nur innerhalb der gleichen Gruppe erlauben
- // (checked ↔ checked, unchecked ↔ unchecked)
- if (fromItem.isChecked != toItem.isChecked) {
- return@update items // Kein Move über Gruppen-Grenze
- }
-
val mutableList = items.toMutableList()
val item = mutableList.removeAt(fromIndex)
- mutableList.add(toIndex, item)
+
+ // 🆕 v1.8.1 IMPL_14: Cross-Boundary Move mit Auto-Toggle
+ // Wenn ein Item die Grenze überschreitet, wird es automatisch checked/unchecked.
+ val movedItem = if (fromItem.isChecked != toItem.isChecked) {
+ item.copy(isChecked = toItem.isChecked)
+ } else {
+ item
+ }
+
+ mutableList.add(toIndex, movedItem)
// Update order values
mutableList.mapIndexed { index, i -> i.copy(order = index) }
}
@@ -348,6 +452,7 @@ class NoteEditorViewModel(
content = "", // Empty for checklists
noteType = NoteType.CHECKLIST,
checklistItems = validItems,
+ checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING
)
@@ -357,6 +462,7 @@ class NoteEditorViewModel(
content = "",
noteType = NoteType.CHECKLIST,
checklistItems = validItems,
+ checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
syncStatus = SyncStatus.LOCAL_ONLY
)
@@ -366,7 +472,7 @@ class NoteEditorViewModel(
}
}
- _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED))
+ // 🆕 v1.8.1 (IMPL_12): NOTE_SAVED Toast entfernt — NavigateBack ist ausreichend
// 🌟 v1.6.0: Trigger onSave Sync
triggerOnSaveSync()
@@ -406,17 +512,33 @@ class NoteEditorViewModel(
val success = withContext(Dispatchers.IO) {
webdavService.deleteNoteFromServer(noteId)
}
+ // 🆕 v1.8.1 (IMPL_12): Banner-Feedback statt stiller Log-Einträge
if (success) {
Logger.d(TAG, "Note $noteId deleted from server")
+ SyncStateManager.showInfo(
+ getApplication().getString(
+ dev.dettmer.simplenotes.R.string.snackbar_deleted_from_server
+ )
+ )
} else {
Logger.w(TAG, "Failed to delete note $noteId from server")
+ SyncStateManager.showError(
+ getApplication().getString(
+ dev.dettmer.simplenotes.R.string.snackbar_server_delete_failed
+ )
+ )
}
} catch (e: Exception) {
Logger.e(TAG, "Error deleting note from server: ${e.message}")
+ SyncStateManager.showError(
+ getApplication().getString(
+ dev.dettmer.simplenotes.R.string.snackbar_server_error,
+ e.message ?: ""
+ )
+ )
}
}
- _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_DELETED))
_events.emit(NoteEditorEvent.NavigateBack)
}
}
@@ -513,6 +635,7 @@ class NoteEditorViewModel(
Logger.d(TAG, "📤 Triggering onSave sync")
val syncRequest = OneTimeWorkRequestBuilder()
.addTag(Constants.SYNC_WORK_TAG)
+ .addTag(Constants.SYNC_ONSAVE_TAG) // 🆕 v1.8.1 (IMPL_08B): Bypassed globalen Cooldown
.build()
WorkManager.getInstance(getApplication()).enqueue(syncRequest)
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/CheckedItemsSeparator.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/CheckedItemsSeparator.kt
index 1b8e1c1..60a7807 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/CheckedItemsSeparator.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/CheckedItemsSeparator.kt
@@ -15,6 +15,7 @@ import dev.dettmer.simplenotes.R
/**
* 🆕 v1.8.0 (IMPL_017): Visueller Separator zwischen unchecked und checked Items
+ * 🆕 v1.8.1 (IMPL_14): Drag-Awareness — Primary-Farbe während Drag als visueller Hinweis
*
* Zeigt eine dezente Linie mit Anzahl der erledigten Items:
* ── 3 completed ──
@@ -22,7 +23,8 @@ import dev.dettmer.simplenotes.R
@Composable
fun CheckedItemsSeparator(
checkedCount: Int,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ isDragActive: Boolean = false // 🆕 v1.8.1 IMPL_14
) {
Row(
modifier = modifier
@@ -32,7 +34,10 @@ fun CheckedItemsSeparator(
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
- color = MaterialTheme.colorScheme.outlineVariant
+ color = if (isDragActive)
+ MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
+ else
+ MaterialTheme.colorScheme.outlineVariant
)
Text(
@@ -42,13 +47,19 @@ fun CheckedItemsSeparator(
checkedCount
),
style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.outline,
+ color = if (isDragActive)
+ MaterialTheme.colorScheme.primary
+ else
+ MaterialTheme.colorScheme.outline,
modifier = Modifier.padding(horizontal = 12.dp)
)
HorizontalDivider(
modifier = Modifier.weight(1f),
- color = MaterialTheme.colorScheme.outlineVariant
+ color = if (isDragActive)
+ MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
+ else
+ MaterialTheme.colorScheme.outlineVariant
)
}
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt
index 8076448..575f976 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt
@@ -24,8 +24,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -71,6 +71,7 @@ fun ChecklistItemRow(
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
isAnyItemDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Hide gradient during any drag
dragModifier: Modifier = Modifier, // 🆕 v1.8.0: IMPL_023 - Drag modifier for handle
+ onHeightChanged: (() -> Unit)? = null, // 🆕 v1.8.1: IMPL_05 - Auto-scroll callback
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
@@ -92,17 +93,14 @@ fun ChecklistItemRow(
// 🆕 v1.8.0: ScrollState für dynamischen Gradient
val scrollState = rememberScrollState()
- // 🆕 v1.8.0: Scroll-basierter Ansatz aktiv wenn Höhe berechnet wurde
- val useScrollClipping = hasOverflow && collapsedHeightDp != null
+ // 🆕 v1.8.1: IMPL_05 - Letzte Zeilenanzahl tracken für Auto-Scroll
+ var lastLineCount by remember { mutableIntStateOf(0) }
- // 🆕 v1.8.0: Dynamische Gradient-Sichtbarkeit basierend auf Scroll-Position
- val showGradient = useScrollClipping && !isFocused && !isAnyItemDragging
- val showTopGradient by remember {
- derivedStateOf { showGradient && scrollState.value > 0 }
- }
- val showBottomGradient by remember {
- derivedStateOf { showGradient && scrollState.value < scrollState.maxValue }
- }
+ // 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf)
+ // derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert.
+ val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging
+ val showTopGradient = showGradient && scrollState.value > 0
+ val showBottomGradient = showGradient && scrollState.value < scrollState.maxValue
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
LaunchedEffect(requestFocus) {
@@ -173,7 +171,7 @@ fun ChecklistItemRow(
Box(modifier = Modifier.weight(1f)) {
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
Box(
- modifier = if (!isFocused && useScrollClipping) {
+ modifier = if (!isFocused && hasOverflow && collapsedHeightDp != null) {
Modifier
.heightIn(max = collapsedHeightDp!!)
.verticalScroll(scrollState)
@@ -216,13 +214,16 @@ fun ChecklistItemRow(
onNext = { onAddNewItem() }
),
singleLine = false,
- // maxLines nur als Fallback bis collapsedHeight berechnet ist
- maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES,
+ // 🆕 v1.8.1: maxLines IMMER Int.MAX_VALUE — keine Oszillation möglich
+ // Höhenbegrenzung erfolgt ausschließlich über heightIn-Modifier oben.
+ // Vorher: maxLines=5 → lineCount gedeckelt → Overflow nie erkannt → Deadlock
+ maxLines = Int.MAX_VALUE,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
onTextLayout = { textLayoutResult ->
- // 🆕 v1.8.0: Overflow erkennen - ABER NUR wenn kein Drag aktiv ist
+ // 🆕 v1.8.1: lineCount ist jetzt akkurat (maxLines=MAX_VALUE deckelt nicht)
+ val lineCount = textLayoutResult.lineCount
if (!isAnyItemDragging) {
- val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES
+ val overflow = lineCount > COLLAPSED_MAX_LINES
hasOverflow = overflow
// Höhe der ersten 5 Zeilen berechnen (einmalig)
if (overflow && collapsedHeightDp == null) {
@@ -230,7 +231,16 @@ fun ChecklistItemRow(
textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp()
}
}
+ // Reset wenn Text gekürzt wird
+ if (!overflow) {
+ collapsedHeightDp = null
+ }
}
+ // 🆕 v1.8.1 (IMPL_05): Höhenänderung bei Zeilenumbruch melden
+ if (isFocused && lineCount > lastLineCount && lastLineCount > 0) {
+ onHeightChanged?.invoke()
+ }
+ lastLineCount = lineCount
},
decorationBox = { innerTextField ->
Box {
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt
index 3ba829c..e965120 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt
@@ -236,6 +236,11 @@ class ComposeMainActivity : ComponentActivity() {
kotlinx.coroutines.delay(2000L)
SyncStateManager.reset()
}
+ // 🆕 v1.8.1 (IMPL_12): INFO-Meldungen nach 2.5s ausblenden
+ dev.dettmer.simplenotes.sync.SyncPhase.INFO -> {
+ kotlinx.coroutines.delay(2500L)
+ SyncStateManager.reset()
+ }
dev.dettmer.simplenotes.sync.SyncPhase.ERROR -> {
kotlinx.coroutines.delay(4000L)
SyncStateManager.reset()
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt
index 9843eaa..a850d16 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt
@@ -470,12 +470,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
if (success) {
- _showToast.emit(getString(R.string.snackbar_deleted_from_server))
+ // 🆕 v1.8.1 (IMPL_12): Toast → Banner INFO
+ SyncStateManager.showInfo(getString(R.string.snackbar_deleted_from_server))
} else {
- _showToast.emit(getString(R.string.snackbar_server_delete_failed))
+ SyncStateManager.showError(getString(R.string.snackbar_server_delete_failed))
}
} catch (e: Exception) {
- _showToast.emit(getString(R.string.snackbar_server_error, e.message ?: ""))
+ SyncStateManager.showError(getString(R.string.snackbar_server_error, e.message ?: ""))
} finally {
// Remove from pending deletions
_pendingDeletions.value = _pendingDeletions.value - noteId
@@ -507,7 +508,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
- // Show aggregated toast
+ // 🆕 v1.8.1 (IMPL_12): Toast → Banner INFO/ERROR
val message = when {
failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount)
successCount == 0 -> getString(R.string.snackbar_server_delete_failed)
@@ -517,7 +518,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
successCount + failCount
)
}
- _showToast.emit(message)
+ if (failCount > 0) {
+ SyncStateManager.showError(message)
+ } else {
+ SyncStateManager.showInfo(message)
+ }
}
}
@@ -555,6 +560,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
+ // 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (verhindert Auto-Sync direkt danach)
+ // Manueller Sync prüft NICHT den globalen Cooldown (User will explizit synchronisieren)
+ val prefs = getApplication().getSharedPreferences(
+ Constants.PREFS_NAME,
+ android.content.Context.MODE_PRIVATE
+ )
+
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft
// 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant
if (!SyncStateManager.tryStartSync(source)) {
@@ -571,6 +583,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
+ // 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (nach tryStartSync, vor Launch)
+ SyncStateManager.markGlobalSyncStarted(prefs)
+
viewModelScope.launch {
try {
// Check for unsynced changes (Banner zeigt bereits PREPARING)
@@ -636,7 +651,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
- // Throttling check
+ // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (alle Trigger teilen sich diesen)
+ if (!SyncStateManager.canSyncGlobally(prefs)) {
+ return
+ }
+
+ // Throttling check (eigener 60s-Cooldown für onResume)
if (!canTriggerAutoSync()) {
return
}
@@ -665,6 +685,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Update last sync timestamp
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
+ // 🆕 v1.8.1 (IMPL_08): Globalen Sync-Cooldown markieren
+ SyncStateManager.markGlobalSyncStarted(prefs)
+
viewModelScope.launch {
try {
// Check for unsynced changes
@@ -692,9 +715,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (result.isSuccess && result.syncedCount > 0) {
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
- // Silent Sync mit echten Änderungen → trotzdem markCompleted (wird silent behandelt)
+ // 🆕 v1.8.1 (IMPL_11): Kein Toast bei Silent-Sync
+ // Das Banner-System respektiert silent=true korrekt (markCompleted → IDLE)
+ // Toast wurde fälschlicherweise trotzdem angezeigt
SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
- _showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount))
loadNotes()
} else if (result.isSuccess) {
Logger.d(TAG, "ℹ️ Auto-sync ($source): No changes")
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt
index 429c468..9aad79b 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/UpdateChangelogSheet.kt
@@ -1,8 +1,6 @@
package dev.dettmer.simplenotes.ui.main
import android.content.Context
-import android.content.Intent
-import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -32,7 +30,6 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
-import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/ChecklistPreviewHelper.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/ChecklistPreviewHelper.kt
new file mode 100644
index 0000000..e67ab37
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/ChecklistPreviewHelper.kt
@@ -0,0 +1,63 @@
+package dev.dettmer.simplenotes.ui.main.components
+
+import dev.dettmer.simplenotes.models.ChecklistItem
+import dev.dettmer.simplenotes.models.ChecklistSortOption
+
+/**
+ * 🆕 v1.8.1 (IMPL_03): Helper-Funktionen für die Checklisten-Vorschau in Main Activity.
+ *
+ * Stellt sicher, dass die Sortierung aus dem Editor konsistent
+ * in allen Preview-Components (NoteCard, NoteCardCompact, NoteCardGrid)
+ * angezeigt wird.
+ */
+
+/**
+ * Sortiert Checklist-Items für die Vorschau basierend auf der
+ * gespeicherten Sortier-Option.
+ */
+fun sortChecklistItemsForPreview(
+ items: List,
+ sortOptionName: String?
+): List {
+ val sortOption = try {
+ sortOptionName?.let { ChecklistSortOption.valueOf(it) }
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
+ null
+ } ?: ChecklistSortOption.MANUAL
+
+ return when (sortOption) {
+ ChecklistSortOption.MANUAL,
+ ChecklistSortOption.UNCHECKED_FIRST ->
+ items.sortedBy { it.isChecked }
+
+ ChecklistSortOption.CHECKED_FIRST ->
+ items.sortedByDescending { it.isChecked }
+
+ ChecklistSortOption.ALPHABETICAL_ASC ->
+ items.sortedBy { it.text.lowercase() }
+
+ ChecklistSortOption.ALPHABETICAL_DESC ->
+ items.sortedByDescending { it.text.lowercase() }
+ }
+}
+
+/**
+ * Generiert den Vorschau-Text für eine Checkliste mit korrekter
+ * Sortierung und passenden Emojis.
+ *
+ * @param items Die Checklisten-Items
+ * @param sortOptionName Der Name der ChecklistSortOption (oder null für MANUAL)
+ * @return Formatierter Preview-String mit Emojis und Zeilenumbrüchen
+ *
+ * 🆕 v1.8.1 (IMPL_06): Emoji-Änderung (☑️ statt ✅ für checked items)
+ */
+fun generateChecklistPreview(
+ items: List,
+ sortOptionName: String?
+): String {
+ val sorted = sortChecklistItemsForPreview(items, sortOptionName)
+ return sorted.joinToString("\n") { item ->
+ val prefix = if (item.isChecked) "☑️" else "☐"
+ "$prefix ${item.text}"
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt
index d04a2f3..4f90014 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardCompact.kt
@@ -149,11 +149,10 @@ fun NoteCardCompact(
text = when (note.noteType) {
NoteType.TEXT -> note.content
NoteType.CHECKLIST -> {
- note.checklistItems
- ?.joinToString("\n") { item ->
- val prefix = if (item.isChecked) "✅" else "☐"
- "$prefix ${item.text}"
- } ?: ""
+ // 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
+ note.checklistItems?.let { items ->
+ generateChecklistPreview(items, note.checklistSortOption)
+ } ?: ""
}
},
style = MaterialTheme.typography.bodySmall,
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt
index 606b0e7..bf1aec2 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCardGrid.kt
@@ -163,11 +163,10 @@ fun NoteCardGrid(
text = when (note.noteType) {
NoteType.TEXT -> note.content
NoteType.CHECKLIST -> {
- note.checklistItems
- ?.joinToString("\n") { item ->
- val prefix = if (item.isChecked) "✅" else "☐"
- "$prefix ${item.text}"
- } ?: ""
+ // 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
+ note.checklistItems?.let { items ->
+ generateChecklistPreview(items, note.checklistSortOption)
+ } ?: ""
}
},
style = MaterialTheme.typography.bodySmall,
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt
index 2f397d8..ad66218 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncProgressBanner.kt
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
+import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
@@ -51,11 +52,13 @@ fun SyncProgressBanner(
// Farbe animiert wechseln je nach State
val isError = progress.phase == SyncPhase.ERROR
val isCompleted = progress.phase == SyncPhase.COMPLETED
- val isResult = isError || isCompleted
+ val isInfo = progress.phase == SyncPhase.INFO // 🆕 v1.8.1 (IMPL_12)
+ val isResult = isError || isCompleted || isInfo
val backgroundColor by animateColorAsState(
targetValue = when {
isError -> MaterialTheme.colorScheme.errorContainer
+ isInfo -> MaterialTheme.colorScheme.secondaryContainer // 🆕 v1.8.1 (IMPL_12)
else -> MaterialTheme.colorScheme.primaryContainer
},
label = "bannerColor"
@@ -64,6 +67,7 @@ fun SyncProgressBanner(
val contentColor by animateColorAsState(
targetValue = when {
isError -> MaterialTheme.colorScheme.onErrorContainer
+ isInfo -> MaterialTheme.colorScheme.onSecondaryContainer // 🆕 v1.8.1 (IMPL_12)
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
label = "bannerContentColor"
@@ -89,7 +93,7 @@ fun SyncProgressBanner(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
- // Icon: Spinner (aktiv), Checkmark (completed), Error (error)
+ // Icon: Spinner (aktiv), Checkmark (completed), Error (error), Info (info)
when {
isCompleted -> {
Icon(
@@ -99,6 +103,14 @@ fun SyncProgressBanner(
tint = contentColor
)
}
+ isInfo -> { // 🆕 v1.8.1 (IMPL_12)
+ Icon(
+ imageVector = Icons.Outlined.Info,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = contentColor
+ )
+ }
isError -> {
Icon(
imageVector = Icons.Filled.ErrorOutline,
@@ -187,5 +199,6 @@ private fun phaseToString(phase: SyncPhase): String {
SyncPhase.IMPORTING_MARKDOWN -> stringResource(R.string.sync_phase_importing_markdown)
SyncPhase.COMPLETED -> stringResource(R.string.sync_phase_completed)
SyncPhase.ERROR -> stringResource(R.string.sync_phase_error)
+ SyncPhase.INFO -> "" // 🆕 v1.8.1 (IMPL_12): INFO nutzt immer resultMessage
}
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
index bbd94f2..da66aee 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
@@ -82,4 +82,11 @@ object Constants {
// 📋 v1.8.0: Post-Update Changelog
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version"
+
+ // 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (über alle Trigger hinweg)
+ const val KEY_LAST_GLOBAL_SYNC_TIME = "last_global_sync_timestamp"
+ const val MIN_GLOBAL_SYNC_INTERVAL_MS = 30_000L // 30 Sekunden
+
+ // 🆕 v1.8.1 (IMPL_08B): onSave-Sync Worker-Tag (bypassed globalen Cooldown)
+ const val SYNC_ONSAVE_TAG = "onsave"
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidget.kt b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidget.kt
index 4c3a2cf..ce6ab78 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidget.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidget.kt
@@ -36,15 +36,20 @@ class NoteWidget : GlanceAppWidget() {
companion object {
// Responsive Breakpoints — schmale + breite Spalten
- val SIZE_SMALL = DpSize(110.dp, 80.dp) // Schmal+kurz: nur Titel
- val SIZE_NARROW_MEDIUM = DpSize(110.dp, 110.dp) // Schmal+mittel: Vorschau
- val SIZE_NARROW_LARGE = DpSize(110.dp, 250.dp) // Schmal+groß: voller Inhalt
- val SIZE_WIDE_MEDIUM = DpSize(250.dp, 110.dp) // Breit+mittel: Vorschau
- val SIZE_WIDE_LARGE = DpSize(250.dp, 250.dp) // Breit+groß: voller Inhalt
+ val SIZE_SMALL = DpSize(110.dp, 80.dp) // Schmal+kurz: nur Titel
+ val SIZE_NARROW_MEDIUM = DpSize(110.dp, 110.dp) // Schmal+mittel: Vorschau
+ val SIZE_NARROW_SCROLL = DpSize(110.dp, 150.dp) // 🆕 v1.8.1: Schmal+scroll (Standard 3x2)
+ val SIZE_NARROW_LARGE = DpSize(110.dp, 250.dp) // Schmal+groß: voller Inhalt
+ val SIZE_WIDE_MEDIUM = DpSize(250.dp, 110.dp) // Breit+mittel: Vorschau
+ val SIZE_WIDE_SCROLL = DpSize(250.dp, 150.dp) // 🆕 v1.8.1: Breit+scroll (Standard 3x2 breit)
+ val SIZE_WIDE_LARGE = DpSize(250.dp, 250.dp) // Breit+groß: voller Inhalt
}
override val sizeMode = SizeMode.Responsive(
- setOf(SIZE_SMALL, SIZE_NARROW_MEDIUM, SIZE_NARROW_LARGE, SIZE_WIDE_MEDIUM, SIZE_WIDE_LARGE)
+ setOf(
+ SIZE_SMALL, SIZE_NARROW_MEDIUM, SIZE_NARROW_SCROLL, SIZE_NARROW_LARGE,
+ SIZE_WIDE_MEDIUM, SIZE_WIDE_SCROLL, SIZE_WIDE_LARGE
+ )
)
override val stateDefinition = PreferencesGlanceStateDefinition
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetActions.kt b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetActions.kt
index 902ffde..386e8bf 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetActions.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetActions.kt
@@ -5,6 +5,7 @@ import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.state.updateAppWidgetState
+import dev.dettmer.simplenotes.models.ChecklistSortOption
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.Logger
@@ -51,14 +52,32 @@ class ToggleChecklistItemAction : ActionCallback {
} else item
} ?: return
+ // 🆕 v1.8.1 (IMPL_04): Auto-Sort nach Toggle
+ // Konsistent mit NoteEditorViewModel.updateChecklistItemChecked
+ val sortOption = try {
+ note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
+ ?: ChecklistSortOption.MANUAL
+
+ val sortedItems = if (sortOption == ChecklistSortOption.MANUAL ||
+ sortOption == ChecklistSortOption.UNCHECKED_FIRST) {
+ val unchecked = updatedItems.filter { !it.isChecked }
+ val checked = updatedItems.filter { it.isChecked }
+ (unchecked + checked).mapIndexed { index, item ->
+ item.copy(order = index)
+ }
+ } else {
+ updatedItems.mapIndexed { index, item -> item.copy(order = index) }
+ }
+
val updatedNote = note.copy(
- checklistItems = updatedItems,
+ checklistItems = sortedItems,
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING
)
storage.saveNote(updatedNote)
- Logger.d(TAG, "Toggled checklist item '$itemId' in widget")
+ Logger.d(TAG, "Toggled + auto-sorted checklist item '$itemId' in widget")
// 🐛 FIX: Glance-State ändern um Re-Render zu erzwingen
updateAppWidgetState(context, glanceId) { prefs ->
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetContent.kt b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetContent.kt
index 89eb842..09c2111 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetContent.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/widget/NoteWidgetContent.kt
@@ -38,9 +38,11 @@ import androidx.glance.layout.width
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import dev.dettmer.simplenotes.R
+import dev.dettmer.simplenotes.models.ChecklistSortOption
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
+import dev.dettmer.simplenotes.ui.main.components.sortChecklistItemsForPreview
/**
* 🆕 v1.8.0: Glance Composable Content für das Notiz-Widget
@@ -52,6 +54,7 @@ import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
// ── Size Classification ──
private val WIDGET_HEIGHT_SMALL_THRESHOLD = 110.dp
+private val WIDGET_HEIGHT_SCROLL_THRESHOLD = 150.dp // 🆕 v1.8.1: Scrollbare Ansicht
private val WIDGET_SIZE_MEDIUM_THRESHOLD = 250.dp
// 🆕 v1.8.0: Increased preview lengths for better text visibility
@@ -59,11 +62,39 @@ private const val TEXT_PREVIEW_COMPACT_LENGTH = 120
private const val TEXT_PREVIEW_FULL_LENGTH = 300
private fun DpSize.toSizeClass(): WidgetSizeClass = when {
- height < WIDGET_HEIGHT_SMALL_THRESHOLD -> WidgetSizeClass.SMALL
- width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_MED
- width < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_TALL
- height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.WIDE_MED
- else -> WidgetSizeClass.WIDE_TALL
+ height < WIDGET_HEIGHT_SMALL_THRESHOLD -> WidgetSizeClass.SMALL
+
+ // 🆕 v1.8.1: Neue ScrollView-Schwelle bei 150dp Höhe
+ width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_HEIGHT_SCROLL_THRESHOLD -> WidgetSizeClass.NARROW_MED
+ width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_SCROLL
+ width < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_TALL
+
+ height < WIDGET_HEIGHT_SCROLL_THRESHOLD -> WidgetSizeClass.WIDE_MED
+ height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.WIDE_SCROLL
+ else -> WidgetSizeClass.WIDE_TALL
+}
+
+/**
+ * 🆕 v1.8.1 (IMPL_04): Separator zwischen erledigten und unerledigten Items im Widget.
+ * Glance-kompatible Version von CheckedItemsSeparator.
+ */
+@Composable
+private fun WidgetCheckedItemsSeparator(checkedCount: Int) {
+ Row(
+ modifier = GlanceModifier
+ .fillMaxWidth()
+ .padding(vertical = 4.dp, horizontal = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "── $checkedCount ✔ ──",
+ style = TextStyle(
+ color = GlanceTheme.colors.outline,
+ fontSize = 11.sp
+ )
+ )
+ }
}
@Composable
@@ -177,14 +208,28 @@ fun NoteWidgetContent(
}
}
- WidgetSizeClass.NARROW_TALL -> Box(modifier = contentClickModifier) {
+ // 🆕 v1.8.1 (IMPL_09): Scrollbare Größe (150dp+ Höhe)
+ WidgetSizeClass.NARROW_SCROLL,
+ WidgetSizeClass.NARROW_TALL -> {
when (note.noteType) {
- NoteType.TEXT -> TextNoteFullView(note)
- NoteType.CHECKLIST -> ChecklistFullView(
- note = note,
- isLocked = isLocked,
- glanceId = glanceId
- )
+ NoteType.TEXT -> Box(modifier = contentClickModifier) {
+ TextNoteFullView(note)
+ }
+ NoteType.CHECKLIST -> {
+ // 🆕 v1.8.1: Locked: Click -> Options | Unlocked: kein Click -> Scroll frei
+ val checklistBoxModifier = if (isLocked) {
+ contentClickModifier
+ } else {
+ GlanceModifier.fillMaxSize()
+ }
+ Box(modifier = checklistBoxModifier) {
+ ChecklistFullView(
+ note = note,
+ isLocked = isLocked,
+ glanceId = glanceId
+ )
+ }
+ }
}
}
@@ -200,14 +245,28 @@ fun NoteWidgetContent(
}
}
- WidgetSizeClass.WIDE_TALL -> Box(modifier = contentClickModifier) {
+ // 🆕 v1.8.1 (IMPL_09): Scrollbare Größe (150dp+ Höhe)
+ WidgetSizeClass.WIDE_SCROLL,
+ WidgetSizeClass.WIDE_TALL -> {
when (note.noteType) {
- NoteType.TEXT -> TextNoteFullView(note)
- NoteType.CHECKLIST -> ChecklistFullView(
- note = note,
- isLocked = isLocked,
- glanceId = glanceId
- )
+ NoteType.TEXT -> Box(modifier = contentClickModifier) {
+ TextNoteFullView(note)
+ }
+ NoteType.CHECKLIST -> {
+ // 🆕 v1.8.1: Locked: Click -> Options | Unlocked: kein Click -> Scroll frei
+ val checklistBoxModifier = if (isLocked) {
+ contentClickModifier
+ } else {
+ GlanceModifier.fillMaxSize()
+ }
+ Box(modifier = checklistBoxModifier) {
+ ChecklistFullView(
+ note = note,
+ isLocked = isLocked,
+ glanceId = glanceId
+ )
+ }
+ }
}
}
}
@@ -370,13 +429,35 @@ private fun ChecklistCompactView(
isLocked: Boolean,
glanceId: GlanceId
) {
- val items = note.checklistItems?.sortedBy { it.order } ?: return
+ // 🆕 v1.8.1 (IMPL_04): Sortierung aus Editor übernehmen
+ val items = note.checklistItems?.let { rawItems ->
+ sortChecklistItemsForPreview(rawItems, note.checklistSortOption)
+ } ?: return
+
+ // 🆕 v1.8.1 (IMPL_04): Separator-Logik
+ val uncheckedCount = items.count { !it.isChecked }
+ val checkedCount = items.count { it.isChecked }
+ val sortOption = try {
+ note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
+ ?: ChecklistSortOption.MANUAL
+
+ val showSeparator = (sortOption == ChecklistSortOption.MANUAL ||
+ sortOption == ChecklistSortOption.UNCHECKED_FIRST) &&
+ uncheckedCount > 0 && checkedCount > 0
+
val visibleItems = items.take(maxItems)
val remainingCount = items.size - visibleItems.size
- val checkedCount = items.count { it.isChecked }
Column(modifier = GlanceModifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
+ var separatorShown = false
visibleItems.forEach { item ->
+ // 🆕 v1.8.1: Separator vor dem ersten checked Item anzeigen
+ if (showSeparator && !separatorShown && item.isChecked) {
+ WidgetCheckedItemsSeparator(checkedCount = checkedCount)
+ separatorShown = true
+ }
+
if (isLocked) {
Row(
modifier = GlanceModifier
@@ -385,7 +466,7 @@ private fun ChecklistCompactView(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = if (item.isChecked) "✅" else "☐",
+ text = if (item.isChecked) "☑️" else "☐", // 🆕 v1.8.1 (IMPL_06)
style = TextStyle(fontSize = 14.sp)
)
Spacer(modifier = GlanceModifier.width(6.dp))
@@ -443,15 +524,41 @@ private fun ChecklistFullView(
isLocked: Boolean,
glanceId: GlanceId
) {
- val items = note.checklistItems?.sortedBy { it.order } ?: return
+ // 🆕 v1.8.1 (IMPL_04): Sortierung aus Editor übernehmen
+ val items = note.checklistItems?.let { rawItems ->
+ sortChecklistItemsForPreview(rawItems, note.checklistSortOption)
+ } ?: return
+
+ // 🆕 v1.8.1 (IMPL_04): Separator-Logik
+ val uncheckedCount = items.count { !it.isChecked }
+ val checkedCount = items.count { it.isChecked }
+ val sortOption = try {
+ note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
+ } catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
+ ?: ChecklistSortOption.MANUAL
+
+ val showSeparator = (sortOption == ChecklistSortOption.MANUAL ||
+ sortOption == ChecklistSortOption.UNCHECKED_FIRST) &&
+ uncheckedCount > 0 && checkedCount > 0
+
+ // 🆕 v1.8.1: Berechne die Gesamtanzahl der Elemente inklusive Separator
+ val totalItems = items.size + if (showSeparator) 1 else 0
LazyColumn(
modifier = GlanceModifier
.fillMaxSize()
.padding(horizontal = 8.dp)
) {
- items(items.size) { index ->
- val item = items[index]
+ items(totalItems) { index ->
+ // 🆕 v1.8.1: Separator an Position uncheckedCount einfügen
+ if (showSeparator && index == uncheckedCount) {
+ WidgetCheckedItemsSeparator(checkedCount = checkedCount)
+ return@items
+ }
+
+ // Tatsächlichen Item-Index berechnen (nach Separator um 1 verschoben)
+ val itemIndex = if (showSeparator && index > uncheckedCount) index - 1 else index
+ val item = items.getOrNull(itemIndex) ?: return@items
if (isLocked) {
Row(
@@ -461,7 +568,7 @@ private fun ChecklistFullView(
verticalAlignment = Alignment.CenterVertically
) {
Text(
- text = if (item.isChecked) "✅" else "☐",
+ text = if (item.isChecked) "☑️" else "☐", // 🆕 v1.8.1 (IMPL_06)
style = TextStyle(fontSize = 16.sp)
)
Spacer(modifier = GlanceModifier.width(8.dp))
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/widget/WidgetSizeClass.kt b/android/app/src/main/java/dev/dettmer/simplenotes/widget/WidgetSizeClass.kt
index 872040d..3c15848 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/widget/WidgetSizeClass.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/widget/WidgetSizeClass.kt
@@ -4,11 +4,14 @@ package dev.dettmer.simplenotes.widget
* 🆕 v1.8.0: Size classification for responsive Note Widget layouts
*
* Determines which layout variant to use based on widget dimensions.
+ * 🆕 v1.8.1: Added NARROW_SCROLL and WIDE_SCROLL for scrollable mid-size widgets
*/
enum class WidgetSizeClass {
- SMALL, // Nur Titel
- NARROW_MED, // Schmal, Vorschau
- NARROW_TALL, // Schmal, voller Inhalt
- WIDE_MED, // Breit, Vorschau
- WIDE_TALL // Breit, voller Inhalt
+ SMALL, // Nur Titel
+ NARROW_MED, // Schmal, Vorschau (CompactView)
+ NARROW_SCROLL, // 🆕 v1.8.1: Schmal, scrollbare Liste (150dp+)
+ NARROW_TALL, // Schmal, voller Inhalt
+ WIDE_MED, // Breit, Vorschau (CompactView)
+ WIDE_SCROLL, // 🆕 v1.8.1: Breit, scrollbare Liste (150dp+)
+ WIDE_TALL // Breit, voller Inhalt
}
diff --git a/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt b/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt
index 768586f..9f1699e 100644
--- a/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt
+++ b/android/app/src/test/java/dev/dettmer/simplenotes/ui/editor/ChecklistSortingTest.kt
@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.ui.editor
+import dev.dettmer.simplenotes.models.ChecklistSortOption
import org.junit.Assert.*
import org.junit.Test
@@ -174,4 +175,204 @@ class ChecklistSortingTest {
assertEquals(1, sorted[1].order)
assertEquals(2, sorted[2].order)
}
+
+ // ═══════════════════════════════════════════════════════════════════════
+ // 🆕 v1.8.1 (IMPL_15): Tests für Add-Item Insert-Position
+ // ═══════════════════════════════════════════════════════════════════════
+
+ /**
+ * Simulates calculateInsertIndexForNewItem() from NoteEditorViewModel.
+ * Tests the insert position logic for new unchecked items.
+ */
+ private fun calculateInsertIndexForNewItem(
+ items: List,
+ sortOption: ChecklistSortOption
+ ): Int {
+ return when (sortOption) {
+ ChecklistSortOption.MANUAL,
+ ChecklistSortOption.UNCHECKED_FIRST -> {
+ val firstCheckedIndex = items.indexOfFirst { it.isChecked }
+ if (firstCheckedIndex >= 0) firstCheckedIndex else items.size
+ }
+ else -> items.size
+ }
+ }
+
+ /**
+ * Simulates the full addChecklistItemAtEnd() logic:
+ * 1. Calculate insert index
+ * 2. Insert new item
+ * 3. Reassign order values
+ */
+ private fun simulateAddItemAtEnd(
+ items: List,
+ sortOption: ChecklistSortOption
+ ): List {
+ val newItem = ChecklistItemState(id = "new", text = "", isChecked = false, order = 0)
+ val insertIndex = calculateInsertIndexForNewItem(items, sortOption)
+ val newList = items.toMutableList()
+ newList.add(insertIndex, newItem)
+ return newList.mapIndexed { i, item -> item.copy(order = i) }
+ }
+
+ @Test
+ fun `IMPL_15 - add item at end inserts before separator in MANUAL mode`() {
+ // Ausgangslage: 2 unchecked, 1 checked (sortiert)
+ val items = listOf(
+ item("a", checked = false, order = 0),
+ item("b", checked = false, order = 1),
+ item("c", checked = true, order = 2)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ // Neues Item muss an Index 2 stehen (vor dem checked Item)
+ assertEquals(4, result.size)
+ assertEquals("a", result[0].id)
+ assertEquals("b", result[1].id)
+ assertEquals("new", result[2].id) // ← Neues Item VOR Separator
+ assertFalse(result[2].isChecked)
+ assertEquals("c", result[3].id) // ← Checked Item bleibt UNTER Separator
+ assertTrue(result[3].isChecked)
+ }
+
+ @Test
+ fun `IMPL_15 - add item at end inserts before separator in UNCHECKED_FIRST mode`() {
+ val items = listOf(
+ item("a", checked = false, order = 0),
+ item("b", checked = true, order = 1),
+ item("c", checked = true, order = 2)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.UNCHECKED_FIRST)
+
+ assertEquals(4, result.size)
+ assertEquals("a", result[0].id)
+ assertEquals("new", result[1].id) // ← Neues Item direkt nach letztem unchecked
+ assertFalse(result[1].isChecked)
+ assertEquals("b", result[2].id)
+ assertEquals("c", result[3].id)
+ }
+
+ @Test
+ fun `IMPL_15 - add item at end appends at end in CHECKED_FIRST mode`() {
+ val items = listOf(
+ item("a", checked = true, order = 0),
+ item("b", checked = false, order = 1)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.CHECKED_FIRST)
+
+ assertEquals(3, result.size)
+ assertEquals("a", result[0].id)
+ assertEquals("b", result[1].id)
+ assertEquals("new", result[2].id) // ← Am Ende (kein Separator)
+ }
+
+ @Test
+ fun `IMPL_15 - add item at end appends at end in ALPHABETICAL_ASC mode`() {
+ val items = listOf(
+ item("a", checked = false, order = 0),
+ item("b", checked = true, order = 1)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.ALPHABETICAL_ASC)
+
+ assertEquals(3, result.size)
+ assertEquals("new", result[2].id) // ← Am Ende
+ }
+
+ @Test
+ fun `IMPL_15 - add item at end appends at end in ALPHABETICAL_DESC mode`() {
+ val items = listOf(
+ item("a", checked = true, order = 0),
+ item("b", checked = false, order = 1)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.ALPHABETICAL_DESC)
+
+ assertEquals(3, result.size)
+ assertEquals("new", result[2].id) // ← Am Ende
+ }
+
+ @Test
+ fun `IMPL_15 - add item with no checked items appends at end`() {
+ val items = listOf(
+ item("a", checked = false, order = 0),
+ item("b", checked = false, order = 1)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ assertEquals(3, result.size)
+ assertEquals("new", result[2].id) // Kein checked Item → ans Ende
+ }
+
+ @Test
+ fun `IMPL_15 - add item with all checked items inserts at position 0`() {
+ val items = listOf(
+ item("a", checked = true, order = 0),
+ item("b", checked = true, order = 1)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ assertEquals(3, result.size)
+ assertEquals("new", result[0].id) // ← Ganz oben (vor allen checked Items)
+ assertFalse(result[0].isChecked)
+ assertEquals("a", result[1].id)
+ assertEquals("b", result[2].id)
+ }
+
+ @Test
+ fun `IMPL_15 - add item to empty list in MANUAL mode`() {
+ val items = emptyList()
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ assertEquals(1, result.size)
+ assertEquals("new", result[0].id)
+ assertEquals(0, result[0].order)
+ }
+
+ @Test
+ fun `IMPL_15 - order values are sequential after add item`() {
+ val items = listOf(
+ item("a", checked = false, order = 0),
+ item("b", checked = false, order = 1),
+ item("c", checked = true, order = 2)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ result.forEachIndexed { index, item ->
+ assertEquals("Order at index $index should be $index", index, item.order)
+ }
+ }
+
+ @Test
+ fun `IMPL_15 - existing items do not change position after add item`() {
+ // Kernforderung: Kein Item darf sich verschieben
+ val items = listOf(
+ item("cashews", checked = false, order = 0),
+ item("noodles", checked = false, order = 1),
+ item("coffee", checked = true, order = 2)
+ )
+
+ val result = simulateAddItemAtEnd(items, ChecklistSortOption.MANUAL)
+
+ // Relative Reihenfolge der bestehenden Items prüfen
+ val existingIds = result.filter { it.id != "new" }.map { it.id }
+ assertEquals(listOf("cashews", "noodles", "coffee"), existingIds)
+
+ // Cashews und Noodles müssen VOR dem neuen Item sein
+ val cashewsIdx = result.indexOfFirst { it.id == "cashews" }
+ val noodlesIdx = result.indexOfFirst { it.id == "noodles" }
+ val newIdx = result.indexOfFirst { it.id == "new" }
+ val coffeeIdx = result.indexOfFirst { it.id == "coffee" }
+
+ assertTrue("Cashews before new", cashewsIdx < newIdx)
+ assertTrue("Noodles before new", noodlesIdx < newIdx)
+ assertTrue("New before Coffee", newIdx < coffeeIdx)
+ }
}
diff --git a/android/config/detekt/detekt.yml b/android/config/detekt/detekt.yml
index fbd5409..0c3a7d7 100644
--- a/android/config/detekt/detekt.yml
+++ b/android/config/detekt/detekt.yml
@@ -1,8 +1,8 @@
-# ⚡ v1.3.1: detekt Configuration
+# ⚡ v1.8.1: detekt Configuration
# Pragmatic rules for simple-notes-sync
build:
- maxIssues: 100 # Allow existing issues for v1.3.1 release, fix in v1.4.0
+ maxIssues: 0 # v1.8.1: All issues resolved
excludeCorrectable: false
config:
diff --git a/docs/BACKUP.de.md b/docs/BACKUP.de.md
index b29062a..5ca5520 100644
--- a/docs/BACKUP.de.md
+++ b/docs/BACKUP.de.md
@@ -276,8 +276,9 @@ Schritt-für-Schritt:
### Daten-Schutz
- ✅ **Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion
-- ✅ **Keine Verschlüsselung** - Klartextformat für Lesbarkeit
-- ⚠️ **Sensible Daten?** - Backup-Datei selbst verschlüsseln (z.B. 7-Zip mit Passwort)
+- ✅ **Optionale Verschlüsselung** _(v1.7.0+)_ - Backup-Dateien mit Passwort schützen
+- ✅ **Menschenlesbar** - Klartextformat (JSON) wenn unverschlüsselt
+- ⚠️ **Sensible Daten?** - Verschlüsselung aktivieren oder externe Tools nutzen (z.B. 7-Zip)
### Empfehlungen
- 🔐 Backup-Dateien in verschlüsseltem Container speichern
@@ -321,4 +322,4 @@ Schritt-für-Schritt:
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown
-**Letzte Aktualisierung:** v1.2.1 (2026-01-05)
+**Letzte Aktualisierung:** v1.8.1 (2026-02-11)
diff --git a/docs/BACKUP.md b/docs/BACKUP.md
index 13123c8..3210190 100644
--- a/docs/BACKUP.md
+++ b/docs/BACKUP.md
@@ -276,8 +276,9 @@ Step-by-step:
### Data Protection
- ✅ **Locally stored** - No cloud upload without your action
-- ✅ **No encryption** - Plain text format for readability
-- ⚠️ **Sensitive data?** - Encrypt backup file yourself (e.g., 7-Zip with password)
+- ✅ **Optional encryption** _(v1.7.0+)_ - Password-protect backup files
+- ✅ **Human-readable** - Plain JSON format when unencrypted
+- ⚠️ **Sensitive data?** - Enable encryption or use external tools (e.g., 7-Zip)
### Recommendations
- 🔐 Store backup files in encrypted container
@@ -317,8 +318,8 @@ Step-by-step:
---
**📚 See also:**
-- [QUICKSTART.en.md](../QUICKSTART.en.md) - App installation and setup
-- [FEATURES.en.md](FEATURES.en.md) - Complete feature list
-- [DESKTOP.en.md](DESKTOP.en.md) - Desktop integration with Markdown
+- [QUICKSTART.md](../QUICKSTART.md) - App installation and setup
+- [FEATURES.md](FEATURES.md) - Complete feature list
+- [DESKTOP.md](DESKTOP.md) - Desktop integration with Markdown
-**Last update:** v1.2.1 (2026-01-05)
+**Last update:** v1.8.1 (2026-02-11)
diff --git a/docs/DEBUG_APK.md b/docs/DEBUG_APK.md
index ec3d84a..d1eea60 100644
--- a/docs/DEBUG_APK.md
+++ b/docs/DEBUG_APK.md
@@ -48,8 +48,6 @@ git push origin fix/my-bug
## 📱 Installation auf Gerät
-## 📱 Installation auf Gerät
-
### Mit ADB (Empfohlen - sauberes Testing)
```bash
# Gerät verbinden
diff --git a/docs/DOCS.de.md b/docs/DOCS.de.md
index 5197e07..e9374d0 100644
--- a/docs/DOCS.de.md
+++ b/docs/DOCS.de.md
@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 🔮 Roadmap
-### v1.1
-- [ ] Suche & Filter
-- [ ] Dark Mode
-- [ ] Tags/Kategorien
-- [ ] Markdown Preview
-
-### v2.0
-- [ ] Desktop Client (Flutter)
-- [ ] End-to-End Verschlüsselung
-- [ ] Shared Notes (Collaboration)
-- [ ] Attachment Support
+Siehe [UPCOMING.md](UPCOMING.md) für die vollständige Roadmap und geplante Features.
---
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
---
-**Letzte Aktualisierung:** 25. Dezember 2025
+**Letzte Aktualisierung:** Februar 2026
diff --git a/docs/DOCS.md b/docs/DOCS.md
index bd2a2bc..57148e7 100644
--- a/docs/DOCS.md
+++ b/docs/DOCS.md
@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 🔮 Roadmap
-### v1.1
-- [ ] Search & Filter
-- [ ] Dark Mode
-- [ ] Tags/Categories
-- [ ] Markdown Preview
-
-### v2.0
-- [ ] Desktop Client (Flutter)
-- [ ] End-to-End Encryption
-- [ ] Shared Notes (Collaboration)
-- [ ] Attachment Support
+See [UPCOMING.md](UPCOMING.md) for the full roadmap and planned features.
---
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
---
-**Last updated:** December 25, 2025
+**Last updated:** February 2026
diff --git a/docs/FEATURES.de.md b/docs/FEATURES.de.md
index 77a17d7..f4a3c84 100644
--- a/docs/FEATURES.de.md
+++ b/docs/FEATURES.de.md
@@ -37,6 +37,50 @@
---
+## 📊 Ansichten & Layout _(NEU in v1.7.0+)_
+
+### Darstellungsmodi
+- ✅ **Listenansicht** - Klassisches Listen-Layout
+- ✅ **Rasteransicht** _(NEU in v1.7.0)_ - Pinterest-artiges Staggered Grid mit dynamischen Vorschauzeilen
+- ✅ **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht wechseln
+- ✅ **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
+- ✅ **Grid als Standard** _(v1.8.0)_ - Neue Installationen starten im Grid-Modus
+
+### Notiz-Sortierung _(NEU in v1.8.0)_
+- ✅ **Nach Änderungsdatum** - Neueste oder älteste zuerst
+- ✅ **Nach Erstelldatum** - Nach Erstellungszeitpunkt
+- ✅ **Nach Titel** - A-Z oder Z-A
+- ✅ **Nach Typ** - Textnotizen vs. Checklisten
+- ✅ **Persistente Einstellungen** - Sortier-Option bleibt nach App-Neustart
+- ✅ **Sortier-Dialog** - Richtungswahl im Hauptbildschirm
+
+### Checklisten-Sortierung _(NEU in v1.8.0)_
+- ✅ **Manuell** - Eigene Drag & Drop Reihenfolge
+- ✅ **Alphabetisch** - A-Z Sortierung
+- ✅ **Offene zuerst** - Unerledigte Items oben
+- ✅ **Erledigte zuletzt** - Abgehakte Items unten
+- ✅ **Visueller Trenner** - Zwischen offenen/erledigten Gruppen mit Anzahl
+- ✅ **Auto-Sortierung** - Neu sortieren beim Abhaken/Öffnen
+- ✅ **Drag über Grenzen** - Items wechseln Status beim Überqueren des Trenners
+
+---
+
+## 📌 Homescreen-Widgets _(NEU in v1.8.0)_
+
+### Widget-Features
+- ✅ **Textnotiz-Widget** - Beliebige Notiz auf dem Homescreen anzeigen
+- ✅ **Checklisten-Widget** - Interaktive Checkboxen mit Sync zum Server
+- ✅ **5 Größenklassen** - SMALL, NARROW_MED, NARROW_TALL, WIDE_MED, WIDE_TALL
+- ✅ **Material You Farben** - Dynamische Farben passend zum System-Theme
+- ✅ **Einstellbare Transparenz** - Hintergrund-Opazität (0-100%)
+- ✅ **Sperr-Umschalter** - Versehentliche Bearbeitungen verhindern
+- ✅ **Auto-Aktualisierung** - Updates nach Sync-Abschluss
+- ✅ **Konfigurations-Activity** - Notiz-Auswahl und Einstellungen
+- ✅ **Checklisten-Sortierung** _(v1.8.1)_ - Widgets übernehmen Sortier-Option
+- ✅ **Visuelle Trenner** _(v1.8.1)_ - Zwischen offenen/erledigten Items
+
+---
+
## 🌍 Mehrsprachigkeit _(NEU in v1.5.0)_
### Unterstützte Sprachen
@@ -129,9 +173,12 @@
### Sync-Mechanismus
- ✅ **Upload** - Lokale Änderungen zum Server
- ✅ **Download** - Server-Änderungen in App
+- ✅ **Parallele Downloads** _(NEU in v1.8.0)_ - Bis zu 5 gleichzeitige Downloads
- ✅ **Konflikt-Erkennung** - Bei gleichzeitigen Änderungen
- ✅ **Konfliktfreies Merging** - Last-Write-Wins via Timestamp
-- ✅ **Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT
+- ✅ **Server-Löschungs-Erkennung** _(NEU in v1.8.0)_ - Erkennt auf anderen Geräten gelöschte Notizen
+- ✅ **Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT, DELETED_ON_SERVER
+- ✅ **Live Fortschritts-UI** _(NEU in v1.8.0)_ - Phasen-Anzeige mit Upload/Download-Zählern
- ✅ **Fehlerbehandlung** - Retry bei Netzwerkproblemen
- ✅ **Offline-First** - App funktioniert ohne Server
@@ -140,6 +187,9 @@
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
- ✅ **Username/Password** - Basic Authentication
- ✅ **Connection Test** - In Einstellungen testen
+- ✅ **WiFi-Only Sync** _(NEU in v1.7.0)_ - Option nur über WiFi zu synchronisieren
+- ✅ **VPN-Unterstützung** _(NEU in v1.7.0)_ - Sync funktioniert korrekt über VPN-Tunnels
+- ✅ **Self-Signed SSL** _(NEU in v1.7.0)_ - Unterstützung für selbstsignierte Zertifikate
- ✅ **Server-URL Normalisierung** - Automatisches `/notes/` und `/notes-md/` _(NEU in v1.2.1)_
- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/`
@@ -196,11 +246,12 @@
## 🛠️ Technische Details
### Plattform
-- ✅ **Android 8.0+** (API 26+)
+- ✅ **Android 7.0+** (API 24+)
- ✅ **Target SDK 36** (Android 15)
- ✅ **Kotlin** - Moderne Programmiersprache
+- ✅ **Jetpack Compose** - Deklaratives UI-Framework
- ✅ **Material Design 3** - Neueste Design-Richtlinien
-- ✅ **ViewBinding** - Typ-sichere View-Referenzen
+- ✅ **Jetpack Glance** _(v1.8.0)_ - Widget-Framework
### Architektur
- ✅ **MVVM-Light** - Einfache Architektur
@@ -218,6 +269,7 @@
- ✅ **Gson** - JSON Serialization
- ✅ **WorkManager** - Background Tasks
- ✅ **OkHttp** - HTTP Client (via Sardine)
+- ✅ **Glance** _(v1.8.0)_ - Widget-Framework
### Build-Varianten
- ✅ **Standard** - Universal APK (100% FOSS, keine Google-Dependencies)
@@ -247,22 +299,12 @@
## 🔮 Zukünftige Features
-Geplant für kommende Versionen:
+Geplant für kommende Versionen – siehe [UPCOMING.md](UPCOMING.md) für die vollständige Roadmap.
-### v1.4.0 - Checklisten
-- ⏳ **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen
-- ⏳ **Erledigte Items** - Durchstreichen/Abhaken
-- ⏳ **Drag & Drop** - Items neu anordnen
-
-### v1.5.0 - Internationalisierung
-- ⏳ **Mehrsprachigkeit** - Deutsch + Englisch UI
-- ⏳ **Sprachauswahl** - In Einstellungen wählbar
-- ⏳ **Vollständige Übersetzung** - Alle Strings in beiden Sprachen
-
-### v1.6.0 - Modern APIs
-- ⏳ **LocalBroadcastManager ersetzen** - SharedFlow stattdessen
-- ⏳ **PackageInfo Flags** - PackageInfoFlags.of() verwenden
-- ⏳ **Komplexitäts-Refactoring** - Lange Funktionen aufteilen
+### v2.0.0 - Legacy Cleanup
+- ⏳ **Veraltete Activities entfernen** - Durch Compose-Varianten ersetzen
+- ⏳ **LocalBroadcastManager → SharedFlow** - Moderne Event-Architektur
+- ⏳ **WebDavSyncService aufteilen** - SyncOrchestrator, NoteUploader, NoteDownloader
---
@@ -305,4 +347,4 @@ A: Ja! Lade die APK direkt von GitHub oder nutze F-Droid.
---
-**Letzte Aktualisierung:** v1.3.2 (2026-01-10)
+**Letzte Aktualisierung:** v1.8.1 (2026-02-11)
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index 7ed87ae..521d3cb 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -37,6 +37,50 @@
---
+## 📊 Views & Layout _(NEW in v1.7.0+)_
+
+### Display Modes
+- ✅ **List View** - Classic list layout
+- ✅ **Grid View** _(NEW in v1.7.0)_ - Pinterest-style staggered grid with dynamic preview lines
+- ✅ **Layout toggle** - Switch between list and grid in settings
+- ✅ **Adaptive columns** - 2-3 columns based on screen size
+- ✅ **Grid as default** _(v1.8.0)_ - New installations default to grid view
+
+### Note Sorting _(NEW in v1.8.0)_
+- ✅ **Sort by Updated** - Newest or oldest first
+- ✅ **Sort by Created** - By creation date
+- ✅ **Sort by Title** - A-Z or Z-A
+- ✅ **Sort by Type** - Text notes vs checklists
+- ✅ **Persistent preferences** - Sort option saved across app restarts
+- ✅ **Sort dialog** - Direction toggle in main screen
+
+### Checklist Sorting _(NEW in v1.8.0)_
+- ✅ **Manual** - Custom drag & drop order
+- ✅ **Alphabetical** - A-Z sorting
+- ✅ **Unchecked First** - Unchecked items on top
+- ✅ **Checked Last** - Checked items at bottom
+- ✅ **Visual separator** - Between unchecked/checked groups with count
+- ✅ **Auto-sort on toggle** - Re-sorts when checking/unchecking items
+- ✅ **Drag across boundaries** - Items auto-toggle state when crossing separator
+
+---
+
+## 📌 Homescreen Widgets _(NEW in v1.8.0)_
+
+### Widget Features
+- ✅ **Text note widget** - Display any note on homescreen
+- ✅ **Checklist widget** - Interactive checkboxes that sync to server
+- ✅ **5 size classes** - SMALL, NARROW_MED, NARROW_TALL, WIDE_MED, WIDE_TALL
+- ✅ **Material You colors** - Dynamic colors matching system theme
+- ✅ **Configurable opacity** - Background transparency (0-100%)
+- ✅ **Lock toggle** - Prevent accidental edits
+- ✅ **Auto-refresh** - Updates after sync completion
+- ✅ **Configuration activity** - Note selection and settings
+- ✅ **Checklist sorting** _(v1.8.1)_ - Widgets respect saved sort option
+- ✅ **Visual separators** _(v1.8.1)_ - Between unchecked/checked items
+
+---
+
## 🌍 Multilingual Support _(NEW in v1.5.0)_
### Supported Languages
@@ -129,9 +173,12 @@
### Sync Mechanism
- ✅ **Upload** - Local changes to server
- ✅ **Download** - Server changes to app
+- ✅ **Parallel downloads** _(NEW in v1.8.0)_ - Up to 5 simultaneous downloads
- ✅ **Conflict detection** - On simultaneous changes
- ✅ **Conflict-free merging** - Last-Write-Wins via timestamp
-- ✅ **Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT
+- ✅ **Server deletion detection** _(NEW in v1.8.0)_ - Detects notes deleted on other devices
+- ✅ **Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT, DELETED_ON_SERVER
+- ✅ **Live progress UI** _(NEW in v1.8.0)_ - Phase indicators with upload/download counters
- ✅ **Error handling** - Retry on network issues
- ✅ **Offline-first** - App works without server
@@ -140,6 +187,9 @@
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
- ✅ **Username/password** - Basic authentication
- ✅ **Connection test** - Test in settings
+- ✅ **WiFi-only sync** _(NEW in v1.7.0)_ - Option to sync only on WiFi
+- ✅ **VPN support** _(NEW in v1.7.0)_ - Sync works correctly through VPN tunnels
+- ✅ **Self-signed SSL** _(NEW in v1.7.0)_ - Support for self-signed certificates
- ✅ **Server URL normalization** - Automatic `/notes/` and `/notes-md/` _(NEW in v1.2.1)_
- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/`
@@ -196,11 +246,12 @@
## 🛠️ Technical Details
### Platform
-- ✅ **Android 8.0+** (API 26+)
+- ✅ **Android 7.0+** (API 24+)
- ✅ **Target SDK 36** (Android 15)
- ✅ **Kotlin** - Modern programming language
+- ✅ **Jetpack Compose** - Declarative UI framework
- ✅ **Material Design 3** - Latest design guidelines
-- ✅ **ViewBinding** - Type-safe view references
+- ✅ **Jetpack Glance** _(v1.8.0)_ - Widget framework
### Architecture
- ✅ **MVVM-Light** - Simple architecture
@@ -218,6 +269,7 @@
- ✅ **Gson** - JSON serialization
- ✅ **WorkManager** - Background tasks
- ✅ **OkHttp** - HTTP client (via Sardine)
+- ✅ **Glance** _(v1.8.0)_ - Widget framework
### Build Variants
- ✅ **Standard** - Universal APK (100% FOSS, no Google dependencies)
@@ -247,22 +299,12 @@
## 🔮 Future Features
-Planned for upcoming versions:
+Planned for upcoming versions – see [UPCOMING.md](UPCOMING.md) for the full roadmap.
-### v1.4.0 - Checklists
-- ⏳ **Checklist notes** - New note type with checkboxes
-- ⏳ **Completed items** - Strike-through/check off
-- ⏳ **Drag & drop** - Reorder items
-
-### v1.5.0 - Internationalization
-- ⏳ **Multi-language** - German + English UI
-- ⏳ **Language selection** - Selectable in settings
-- ⏳ **Full translation** - All strings in both languages
-
-### v1.6.0 - Modern APIs
-- ⏳ **Replace LocalBroadcastManager** - Use SharedFlow instead
-- ⏳ **PackageInfo Flags** - Use PackageInfoFlags.of()
-- ⏳ **Complexity refactoring** - Split long functions
+### v2.0.0 - Legacy Cleanup
+- ⏳ **Remove deprecated Activities** - Replace with Compose equivalents
+- ⏳ **LocalBroadcastManager → SharedFlow** - Modern event architecture
+- ⏳ **WebDavSyncService split** - SyncOrchestrator, NoteUploader, NoteDownloader
---
@@ -305,4 +347,4 @@ A: Yes! Download the APK directly from GitHub or use F-Droid.
---
-**Last update:** v1.3.2 (2026-01-10)
+**Last update:** v1.8.1 (2026-02-11)
diff --git a/docs/SELF_SIGNED_SSL.de.md b/docs/SELF_SIGNED_SSL.de.md
new file mode 100644
index 0000000..5f2c334
--- /dev/null
+++ b/docs/SELF_SIGNED_SSL.de.md
@@ -0,0 +1,146 @@
+# Selbstsignierte SSL-Zertifikate
+
+**Seit:** v1.7.0
+**Status:** ✅ Unterstützt
+
+**🌍 Sprachen:** **Deutsch** · [English](SELF_SIGNED_SSL.md)
+
+---
+
+## Übersicht
+
+Simple Notes Sync unterstützt die Verbindung zu WebDAV-Servern mit selbstsignierten SSL-Zertifikaten, z.B.:
+- ownCloud/Nextcloud mit selbstsignierten Zertifikaten
+- Synology NAS mit Standard-Zertifikaten
+- Raspberry Pi oder Home-Server
+- Interne Firmen-Server mit privaten CAs
+
+## Anleitung
+
+### Schritt 1: CA-Zertifikat des Servers exportieren
+
+**Auf deinem Server:**
+
+1. Finde deine Zertifikatsdatei (meist `.crt`, `.pem` oder `.der` Format)
+2. Falls du das Zertifikat selbst erstellt hast, hast du es bereits
+3. Für Synology NAS: Systemsteuerung → Sicherheit → Zertifikat → Exportieren
+4. Für ownCloud/Nextcloud: Meist unter `/etc/ssl/certs/` auf dem Server
+
+### Schritt 2: Zertifikat auf Android installieren
+
+**Auf deinem Android-Gerät:**
+
+1. **Übertrage** die `.crt` oder `.pem` Datei auf dein Handy (per E-Mail, USB, etc.)
+
+2. **Öffne Einstellungen** → Sicherheit → Weitere Sicherheitseinstellungen (oder Verschlüsselung & Anmeldedaten)
+
+3. **Von Speicher installieren** / "Zertifikat installieren"
+ - Wähle "CA-Zertifikat"
+ - **Warnung:** Android zeigt eine Sicherheitswarnung. Das ist normal.
+ - Tippe auf "Trotzdem installieren"
+
+4. **Navigiere** zu deiner Zertifikatsdatei und wähle sie aus
+
+5. **Benenne** es erkennbar (z.B. "Mein ownCloud CA")
+
+6. ✅ **Fertig!** Das Zertifikat wird nun systemweit vertraut
+
+### Schritt 3: Simple Notes Sync verbinden
+
+1. Öffne Simple Notes Sync
+2. Gehe zu **Einstellungen** → **Server-Einstellungen**
+3. Gib deine **`https://` Server-URL** wie gewohnt ein
+4. Die App vertraut nun deinem selbstsignierten Zertifikat ✅
+
+---
+
+## Sicherheitshinweise
+
+### ⚠️ Wichtig
+
+- Die Installation eines CA-Zertifikats gewährt Vertrauen für **alle** von dieser CA signierten Zertifikate
+- Installiere nur Zertifikate aus vertrauenswürdigen Quellen
+- Android warnt dich vor der Installation – lies die Warnung sorgfältig
+
+### 🔒 Warum das sicher ist
+
+- Du installierst das Zertifikat **manuell** (bewusste Entscheidung)
+- Die App nutzt Androids nativen Trust Store (keine eigene Validierung)
+- Du kannst das Zertifikat jederzeit in den Android-Einstellungen entfernen
+- F-Droid und Google Play konform (kein "allen vertrauen" Hack)
+
+---
+
+## Fehlerbehebung
+
+### Zertifikat nicht vertraut
+
+**Problem:** App zeigt weiterhin SSL-Fehler nach Zertifikatsinstallation
+
+**Lösungen:**
+1. **Installation prüfen:** Einstellungen → Sicherheit → Vertrauenswürdige Anmeldedaten → Tab "Nutzer"
+2. **Zertifikatstyp prüfen:** Muss ein CA-Zertifikat sein, kein Server-Zertifikat
+3. **App neustarten:** Simple Notes Sync schließen und wieder öffnen
+4. **URL prüfen:** Muss `https://` verwenden (nicht `http://`)
+
+### Selbstsigniert vs. CA-signiert
+
+| Typ | Installation nötig | Sicherheit |
+|-----|-------------------|------------|
+| **Selbstsigniert** | ✅ Ja | Manuelles Vertrauen |
+| **Let's Encrypt** | ❌ Nein | Automatisch |
+| **Private CA** | ✅ Ja (CA-Root) | Automatisch für alle CA-signierten Zertifikate |
+
+---
+
+## Alternative: Let's Encrypt (Empfohlen)
+
+Wenn dein Server öffentlich erreichbar ist, erwäge **Let's Encrypt** für kostenlose, automatisch erneuerte SSL-Zertifikate:
+
+- Keine manuelle Zertifikatsinstallation nötig
+- Von allen Geräten automatisch vertraut
+- Einfacher für Endbenutzer
+
+---
+
+## Technische Details
+
+### Implementierung
+
+- Nutzt Androids **Network Security Config**
+- Vertraut sowohl System- als auch Benutzer-CA-Zertifikaten
+- Kein eigener TrustManager oder HostnameVerifier
+- F-Droid und Play Store konform
+
+### Konfiguration
+
+Datei: `android/app/src/main/res/xml/network_security_config.xml`
+
+```xml
+
+
+
+
+
+
+```
+
+---
+
+## FAQ
+
+**F: Muss ich das Zertifikat nach App-Updates neu installieren?**
+A: Nein, Zertifikate werden systemweit gespeichert, nicht pro App.
+
+**F: Kann ich dasselbe Zertifikat für mehrere Apps verwenden?**
+A: Ja, einmal installiert funktioniert es für alle Apps die Benutzerzertifikaten vertrauen.
+
+**F: Wie entferne ich ein Zertifikat?**
+A: Einstellungen → Sicherheit → Vertrauenswürdige Anmeldedaten → Tab "Nutzer" → Zertifikat antippen → Entfernen
+
+**F: Funktioniert das auf Android 14+?**
+A: Ja, getestet auf Android 7 bis 15 (API 24-35).
+
+---
+
+**Hilfe nötig?** Erstelle ein Issue auf [GitHub](https://github.com/inventory69/simple-notes-sync/issues)
diff --git a/docs/UPCOMING.de.md b/docs/UPCOMING.de.md
index 1805bf4..1eb7d77 100644
--- a/docs/UPCOMING.de.md
+++ b/docs/UPCOMING.de.md
@@ -60,28 +60,91 @@
---
-## v1.7.0 - Staggered Grid Layout
+## v1.7.0 - Grid View, WiFi-Only & VPN ✅
-> **Status:** Geplant 📝
+> **Status:** Released 🎉 (Januar 2026)
-### 🎨 Adaptives Layout
+### 🎨 Grid Layout
-- **Staggered Grid** - Pinterest-artiges Layout mit `LazyVerticalStaggeredGrid`
-- **Intelligente Größen** - Kleine Notizen (kurzer Text, wenige Checklist-Items) kompakt dargestellt
-- **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht in Einstellungen wechseln
-- **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
-- **120 FPS optimiert** - Lazy Loading für flüssiges Scrollen bei vielen Notizen
+- ✅ **Pinterest-artiges Staggered Grid** - Lückenfreies Layout mit dynamischen Vorschauzeilen
+- ✅ **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht wechseln
+- ✅ **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
-### 🔧 Server-Ordner Prüfung
+### 📡 Sync-Verbesserungen
-- **WebDAV Folder Check** - Prüft ob der Ordner auf dem Server existiert und beschreibbar ist
-- **Bessere Fehlermeldungen** - Hilfreiche Hinweise bei Server-Problemen
-- **Connection-Test Verbesserung** - Prüft Read/Write Permissions
+- ✅ **WiFi-Only Sync Toggle** - Nur über WiFi synchronisieren
+- ✅ **VPN-Unterstützung** - Sync funktioniert korrekt über VPN-Tunnels
+- ✅ **Self-Signed SSL** - Dokumentation und Unterstützung für selbstsignierte Zertifikate
+- ✅ **Server-Wechsel-Erkennung** - Alle Notizen auf PENDING zurückgesetzt bei URL-Änderung
-### 🔧 Technische Verbesserungen
+---
-- **Code-Refactoring** - LargeClass Komponenten aufteilen (WebDavSyncService, SettingsActivity)
-- **Verbesserte Progress-Dialoge** - Material Design 3 konform
+## v1.7.1 - Android 9 Fix & VPN ✅
+
+> **Status:** Released 🎉 (Februar 2026)
+
+- ✅ **Android 9 Crash Fix** - `getForegroundInfo()` für WorkManager auf API 28 implementiert
+- ✅ **VPN-Kompatibilität** - WiFi Socket-Binding erkennt Wireguard VPN-Interfaces
+- ✅ **SafeSardineWrapper** - Saubere HTTP-Verbindungs-Bereinigung
+
+---
+
+## v1.7.2 - Timestamp & Löschungs-Fixes ✅
+
+> **Status:** Released 🎉 (Februar 2026)
+
+- ✅ **Server-mtime als Wahrheitsquelle** - Behebt Timestamp-Probleme mit externen Editoren
+- ✅ **Deletion Tracker Mutex** - Thread-sichere Batch-Löschungen
+- ✅ **ISO8601 Timezone-Parsing** - Multi-Format-Unterstützung
+- ✅ **E-Tag Batch-Caching** - Performance-Verbesserung
+- ✅ **Memory Leak Prävention** - SafeSardineWrapper mit Closeable
+
+---
+
+## v1.8.0 - Widgets, Sortierung & Erweiterter Sync ✅
+
+> **Status:** Released 🎉 (Februar 2026)
+
+### 📌 Homescreen-Widgets
+
+- ✅ **Volles Jetpack Glance Framework** - 5 responsive Größenklassen
+- ✅ **Interaktive Checklisten** - Checkboxen die zum Server synchronisieren
+- ✅ **Material You Farben** - Dynamische Farben mit einstellbarer Opazität
+- ✅ **Sperr-Umschalter** - Versehentliche Bearbeitungen verhindern
+- ✅ **Konfigurations-Activity** - Notiz-Auswahl und Einstellungen
+
+### 📊 Sortierung
+
+- ✅ **Notiz-Sortierung** - Nach Titel, Änderungsdatum, Erstelldatum, Typ
+- ✅ **Checklisten-Sortierung** - Manuell, alphabetisch, offene zuerst, erledigte zuletzt
+- ✅ **Visuelle Trenner** - Zwischen offenen/erledigten Gruppen
+- ✅ **Drag über Grenzen** - Auto-Toggle beim Überqueren des Trenners
+
+### 🔄 Sync-Verbesserungen
+
+- ✅ **Parallele Downloads** - Bis zu 5 gleichzeitig (konfigurierbar)
+- ✅ **Server-Löschungs-Erkennung** - Erkennt auf anderen Clients gelöschte Notizen
+- ✅ **Live Sync-Fortschritt** - Phasen-Anzeige mit Zählern
+- ✅ **Sync-Status Legende** - Hilfe-Dialog für alle Sync-Icons
+
+### ✨ UX
+
+- ✅ **Post-Update Changelog** - Zeigt lokalisierten Changelog nach Update
+- ✅ **Grid als Standard** - Neue Installationen starten im Grid-Modus
+- ✅ **Toast → Banner Migration** - Einheitliches Benachrichtigungssystem
+
+---
+
+## v1.8.1 - Bugfix & Polish ✅
+
+> **Status:** Released 🎉 (Februar 2026)
+
+- ✅ **Checklisten-Sortierung Persistenz** - Sortier-Option korrekt wiederhergestellt
+- ✅ **Widget Scroll Fix** - Scroll funktioniert auf Standard 3×2 Widget-Größe
+- ✅ **Widget Checklisten-Sortierung** - Widgets übernehmen gespeicherte Sortier-Option
+- ✅ **Drag Cross-Boundary** - Drag & Drop über Checked/Unchecked-Trenner
+- ✅ **Sync Rate-Limiting** - Globaler 30s Cooldown zwischen Auto-Syncs
+- ✅ **Detekt: 0 Issues** - Alle 12 Findings behoben
---
@@ -110,7 +173,6 @@
### 🎨 UI Features
-- **Widget** - Schnellzugriff vom Homescreen
- **Kategorien/Tags** - Notizen organisieren
- **Suche** - Volltextsuche in Notizen
diff --git a/docs/UPCOMING.md b/docs/UPCOMING.md
index e759986..24ff873 100644
--- a/docs/UPCOMING.md
+++ b/docs/UPCOMING.md
@@ -60,28 +60,91 @@
---
-## v1.7.0 - Staggered Grid Layout
+## v1.7.0 - Grid View, WiFi-Only & VPN ✅
-> **Status:** Planned 📝
+> **Status:** Released 🎉 (January 2026)
-### 🎨 Adaptive Layout
+### 🎨 Grid Layout
-- **Staggered Grid** - Pinterest-style layout using `LazyVerticalStaggeredGrid`
-- **Smart sizing** - Small notes (short text, few checklist items) displayed compactly
-- **Layout toggle** - Switch between List and Grid view in settings
-- **Adaptive columns** - 2-3 columns based on screen size
-- **120 FPS optimized** - Lazy loading for smooth scrolling with many notes
+- ✅ **Pinterest-style staggered grid** - Gapless layout with dynamic preview lines
+- ✅ **Layout toggle** - Switch between list and grid in settings
+- ✅ **Adaptive columns** - 2-3 columns based on screen size
-### 🔧 Server Folder Check
+### 📡 Sync Improvements
-- **WebDAV folder check** - Checks if folder exists and is writable on server
-- **Better error messages** - Helpful hints for server problems
-- **Connection test improvement** - Checks read/write permissions
+- ✅ **WiFi-only sync toggle** - Sync only when connected to WiFi
+- ✅ **VPN support** - Sync works correctly through VPN tunnels
+- ✅ **Self-signed SSL** - Documentation and support for self-signed certificates
+- ✅ **Server change detection** - All notes reset to PENDING when server URL changes
-### 🔧 Technical Improvements
+---
-- **Code refactoring** - Split LargeClass components (WebDavSyncService, SettingsActivity)
-- **Improved progress dialogs** - Material Design 3 compliant
+## v1.7.1 - Android 9 Fix & VPN ✅
+
+> **Status:** Released 🎉 (February 2026)
+
+- ✅ **Android 9 crash fix** - Implemented `getForegroundInfo()` for WorkManager on API 28
+- ✅ **VPN compatibility** - WiFi socket binding detects Wireguard VPN interfaces
+- ✅ **SafeSardineWrapper** - Proper HTTP connection cleanup
+
+---
+
+## v1.7.2 - Timestamp & Deletion Fixes ✅
+
+> **Status:** Released 🎉 (February 2026)
+
+- ✅ **Server mtime as source of truth** - Fixes external editor timestamp issues
+- ✅ **Deletion tracker mutex** - Thread-safe batch deletes
+- ✅ **ISO8601 timezone parsing** - Multi-format support
+- ✅ **E-Tag batch caching** - Performance improvement
+- ✅ **Memory leak prevention** - SafeSardineWrapper with Closeable
+
+---
+
+## v1.8.0 - Widgets, Sorting & Advanced Sync ✅
+
+> **Status:** Released 🎉 (February 2026)
+
+### 📌 Homescreen Widgets
+
+- ✅ **Full Jetpack Glance framework** - 5 responsive size classes
+- ✅ **Interactive checklists** - Checkboxes that sync to server
+- ✅ **Material You colors** - Dynamic colors with configurable opacity
+- ✅ **Lock toggle** - Prevent accidental edits
+- ✅ **Configuration activity** - Note selection and settings
+
+### 📊 Sorting
+
+- ✅ **Note sorting** - By title, date modified, date created, type
+- ✅ **Checklist sorting** - Manual, alphabetical, unchecked first, checked last
+- ✅ **Visual separators** - Between unchecked/checked groups
+- ✅ **Drag across boundaries** - Auto-toggle state on cross-boundary drag
+
+### 🔄 Sync Improvements
+
+- ✅ **Parallel downloads** - Up to 5 simultaneous (configurable)
+- ✅ **Server deletion detection** - Detects notes deleted on other clients
+- ✅ **Live sync progress** - Phase indicators with counters
+- ✅ **Sync status legend** - Help dialog explaining all sync icons
+
+### ✨ UX
+
+- ✅ **Post-update changelog** - Shows localized changelog on first launch after update
+- ✅ **Grid as default** - New installations default to grid view
+- ✅ **Toast → Banner migration** - Unified notification system
+
+---
+
+## v1.8.1 - Bugfix & Polish ✅
+
+> **Status:** Released 🎉 (February 2026)
+
+- ✅ **Checklist sort persistence** - Sort option correctly restored when reopening
+- ✅ **Widget scroll fix** - Scroll works on standard 3×2 widget size
+- ✅ **Widget checklist sorting** - Widgets apply saved sort option
+- ✅ **Drag cross-boundary** - Drag & drop across checked/unchecked separator
+- ✅ **Sync rate-limiting** - Global 30s cooldown between auto-syncs
+- ✅ **Detekt: 0 issues** - All 12 findings resolved
---
@@ -110,7 +173,6 @@
### 🎨 UI Features
-- **Widget** - Quick access from homescreen
- **Categories/Tags** - Organize notes
- **Search** - Full-text search in notes
diff --git a/fastlane/README.md b/fastlane/README.md
index 388a08f..67a8834 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -5,34 +5,50 @@ Diese Verzeichnisstruktur enthält alle Metadaten für die F-Droid-Veröffentlic
## Struktur
```
-fastlane/metadata/android/de-DE/
-├── title.txt # App-Name (max 50 Zeichen)
-├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
-├── full_description.txt # Vollständige Beschreibung (max 4000 Zeichen)
-├── changelogs/
-│ └── 1.txt # Changelog für Version 1
-└── images/
- └── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
- ├── 1.png # Hauptansicht (Notizliste)
- ├── 2.png # Notiz-Editor
- ├── 3.png # Settings
- └── 4.png # Empty State
+fastlane/metadata/android/
+├── de-DE/ # Deutsche Lokalisierung (primär)
+│ ├── title.txt # App-Name (max 50 Zeichen)
+│ ├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
+│ ├── full_description.txt # Vollständige Beschreibung (max 4000 Zeichen)
+│ ├── changelogs/
+│ │ ├── 1.txt ... 21.txt # Changelogs pro versionCode (max 500 Zeichen!)
+│ └── images/
+│ └── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
+│ ├── 1.png ... 5.png
+└── en-US/ # Englische Lokalisierung
+ ├── title.txt
+ ├── short_description.txt
+ ├── full_description.txt
+ ├── changelogs/
+ │ ├── 1.txt ... 21.txt
+ └── images/
+ └── phoneScreenshots/
```
+## Wichtige Limits
+
+| Feld | Max. Länge | Hinweis |
+|------|-----------|---------|
+| `title.txt` | 50 Zeichen | App-Name |
+| `short_description.txt` | 80 Zeichen | Kurzbeschreibung |
+| `full_description.txt` | 4000 Zeichen | Vollständige Beschreibung |
+| `changelogs/*.txt` | **500 Bytes** | Pro versionCode, **Bytes nicht Zeichen!** |
+
+> **Achtung:** Changelogs werden in **Bytes** gemessen! UTF-8 Umlaute (ä, ö, ü) zählen als 2 Bytes.
+
## Screenshots erstellen
-Verwende einen Android Emulator oder physisches Gerät mit:
+Verwende ein physisches Gerät oder Emulator mit:
- Material You Theme aktiviert
-- Deutsche Sprache
+- Deutsche/Englische Sprache je nach Locale
- Screenshots in hoher Auflösung (1080x2400 empfohlen)
-### Screenshot-Reihenfolge:
-1. **Notizliste** - Mit mehreren Beispiel-Notizen, Sync-Status sichtbar
-2. **Editor** - Zeige eine bearbeitete Notiz mit Titel und Inhalt
-3. **Settings** - Server-Konfiguration mit erfolgreichem Server-Status
-4. **Empty State** - Schöne leere Ansicht mit Material 3 Card
-
## F-Droid Build-Konfiguration
Die App verwendet den `fdroid` Build-Flavor ohne proprietäre Dependencies.
-Siehe `build.gradle.kts` für Details.
+Siehe `android/app/build.gradle.kts` für Details.
+
+## Aktuelle Version
+
+- **versionName:** 1.8.1
+- **versionCode:** 21
diff --git a/fastlane/metadata/android/de-DE/changelogs/10.txt b/fastlane/metadata/android/de-DE/changelogs/10.txt
index f0003c2..645d129 100644
--- a/fastlane/metadata/android/de-DE/changelogs/10.txt
+++ b/fastlane/metadata/android/de-DE/changelogs/10.txt
@@ -1,5 +1,5 @@
-Unter der Haube haben wir ordentlich aufgeraumt:
+Unter der Haube haben wir ordentlich aufgeräumt:
- Verbesserte Sync-Performance durch optimierten Code
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
- Speichereffizientere Datenverarbeitung
-- Datenschutz-Hinweis fur Datei-Logging hinzugefugt
+- Datenschutz-Hinweis für Datei-Logging hinzugefügt
diff --git a/fastlane/metadata/android/de-DE/changelogs/21.txt b/fastlane/metadata/android/de-DE/changelogs/21.txt
new file mode 100644
index 0000000..06eae2b
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/21.txt
@@ -0,0 +1,13 @@
+🛠️ v1.8.1 — CHECKLISTEN & SYNC FIXES
+
+• Behoben: Sortierung ging beim Öffnen verloren
+• Behoben: Widget-Scroll bei 3x2 defekt
+• Behoben: Toast bei Auto-Sync & Drag-Flackern
+• Neu: Widget-Checklisten mit Sortierung
+• Neu: Checklisten-Sortierung in Vorschau
+• Neu: Auto-Scroll bei Zeilenumbruch
+• Verbessert: Sync-Ratenlimit & Akkuschutz
+• Verbessert: Toasts → Banner-System
+• Verbessert: ProGuard für Widgets & Compose
+
+https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.de.md
\ No newline at end of file
diff --git a/fastlane/metadata/android/de-DE/changelogs/7.txt b/fastlane/metadata/android/de-DE/changelogs/7.txt
index 5f2c138..e60cfee 100644
--- a/fastlane/metadata/android/de-DE/changelogs/7.txt
+++ b/fastlane/metadata/android/de-DE/changelogs/7.txt
@@ -1,12 +1,12 @@
v1.2.2 - Rückwärtskompatibilität für v1.2.0 User
Kritische Fehlerbehebung
-• Server-Wiederherstellung findet jetzt ALLE Notizen (Root + /notes/)
-• User die von v1.2.0 upgraden verlieren keine Daten mehr
-• Alte Notizen aus Root-Ordner werden beim Restore gefunden
+• Server-Restore findet jetzt ALLE Notizen (Root + /notes/)
+• Upgrade von v1.2.0 ohne Datenverlust
+• Alte Notizen aus Root-Ordner werden gefunden
Technische Details
• Dual-Mode Download nur bei Server-Restore aktiv
-• Normale Syncs bleiben schnell (scannen nur /notes/)
+• Normale Syncs bleiben schnell (nur /notes/)
• Automatische Deduplication verhindert Duplikate
-• Sanfte Migration: Neue Uploads gehen in /notes/, alte bleiben lesbar
+• Sanfte Migration: Uploads → /notes/, alte bleiben lesbar
diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt
index 3a6dc8d..c333751 100644
--- a/fastlane/metadata/android/de-DE/full_description.txt
+++ b/fastlane/metadata/android/de-DE/full_description.txt
@@ -1,29 +1,31 @@
Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisation und modernen Features.
Hauptfunktionen:
-• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop)
-• NEU: Raster-Ansicht (Grid View) für Notizen
+• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop, Sortierung)
+• Raster- und Listen-Ansicht mit Notizfarben
+• Homescreen-Widgets (Quick-Note, Checkliste mit interaktiven Checkboxen)
• Multi-Device Sync (Handy, Tablet, Desktop)
• WebDAV-Synchronisation mit eigenem Server (Nextcloud, ownCloud, etc.)
• Markdown-Export und Import für Desktop-Editoren (Obsidian, VS Code)
-• NEU: WiFi-only Sync, VPN-Unterstützung, Verschlüsselung für lokale Backups
+• WiFi-only Sync, VPN-Unterstützung, parallele Downloads
• Konfigurierbare Sync-Trigger: onSave, onResume, WiFi, periodisch, Boot
-• Komplett offline nutzbar
-• Keine Werbung, keine Tracker
+• Komplett offline nutzbar – keine Werbung, keine Tracker
Datenschutz & Sicherheit:
• Alle Daten bleiben bei dir – keine Cloud, keine Tracking-Bibliotheken
-• Unterstützung für selbstsignierte SSL-Zertifikate (Self-signed SSL)
-• SHA-256 Hash des Signaturzertifikats in App und Releases sichtbar
+• Unterstützung für selbstsignierte SSL-Zertifikate
+• Verschlüsselte lokale Backups
Synchronisation:
-• Automatisch oder manuell, optimierte Performance, periodischer Sync optional
-• Intelligente Konfliktlösung, Lösch-Tracking, Batch-Aktionen
+• Parallele Downloads (bis zu 5 gleichzeitig)
+• Live Sync-Fortschritt mit Phasen-Anzeige
+• Intelligente Konfliktlösung, Server-Löschungs-Erkennung
+• Post-Update Changelog-Dialog
UI & Design:
• Moderne Jetpack Compose Oberfläche
• Material Design 3, Dynamic Colors, Dark Mode
-• Animationen und Live Sync-Status
+• Notiz- und Checklisten-Sortierung (Titel, Datum, Farbe, alphabetisch)
Mehrsprachig:
• Deutsch und Englisch, automatische Erkennung, App-Sprachauswahl
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png
index 098f9e2..d6282e2 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png
index 3d01bb6..74dc93c 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png
index ba6a867..2d2649a 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png
index 53ba65d..41a5a2b 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png
index 607d151..cff4bd4 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png
new file mode 100644
index 0000000..f8e34a1
Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png
index 2a77315..a93837a 100644
Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png differ
diff --git a/fastlane/metadata/android/en-US/changelogs/1.txt b/fastlane/metadata/android/en-US/changelogs/1.txt
new file mode 100644
index 0000000..6f81b1f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/1.txt
@@ -0,0 +1,8 @@
+• Material Design 3 with Dynamic Colors
+• Swipe-to-Delete with confirmation dialog
+• Server Backup & Restore feature
+• Improved Empty State view
+• German localization
+• Splash Screen Support (Android 12+)
+• Performance improvements
+• Bug fixes
diff --git a/fastlane/metadata/android/en-US/changelogs/2.txt b/fastlane/metadata/android/en-US/changelogs/2.txt
new file mode 100644
index 0000000..11b94f7
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/2.txt
@@ -0,0 +1,5 @@
+• Configurable sync interval (15/30/60 minutes)
+• Transparent battery usage display (measured: 0.4%/day at 30min)
+• Doze Mode optimizations for more reliable background syncs
+• About section with app information and GitHub links
+• Various bugfixes and performance improvements
diff --git a/fastlane/metadata/android/en-US/changelogs/21.txt b/fastlane/metadata/android/en-US/changelogs/21.txt
new file mode 100644
index 0000000..058e48d
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/21.txt
@@ -0,0 +1,13 @@
+🛠️ v1.8.1 — CHECKLIST & SYNC FIXES
+
+• Fixed: Sort lost when reopening checklists
+• Fixed: Widget scroll on 3x2 size
+• Fixed: Toast on auto-sync & drag flicker
+• New: Widget checklists with sorting & separator
+• New: Checklist sorting in main preview
+• New: Auto-scroll on line wrap in editor
+• Improved: Sync rate-limiting & battery save
+• Improved: Toasts → unified Banner system
+• Improved: ProGuard for Widgets & Compose
+
+https://github.com/inventory69/simple-notes-sync/blob/main/CHANGELOG.md
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 166ee44..893be46 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -1,29 +1,31 @@
Simple Notes Sync is a minimalist note-taking app with WebDAV sync and modern features.
Key Features:
-• Text notes and checklists (tap-to-check, drag & drop)
-• NEW: Grid view for notes
+• Text notes and checklists (tap-to-check, drag & drop, sorting)
+• Grid and list view with note color support
+• Homescreen widgets (quick-note, checklist with interactive checkboxes)
• Multi-device sync (phone, tablet, desktop)
• WebDAV sync with your own server (Nextcloud, ownCloud, etc.)
• Markdown export/import for desktop editors (Obsidian, VS Code)
-• NEW: WiFi-only sync, VPN support, encryption for local backups
+• WiFi-only sync, VPN support, parallel downloads
• Configurable sync triggers: onSave, onResume, WiFi, periodic, boot
-• Fully usable offline
-• No ads, no trackers
+• Fully usable offline – no ads, no trackers
Privacy & Security:
• Your data stays with you – no cloud, no tracking libraries
• Support for self-signed SSL certificates
-• SHA-256 hash of signing certificate shown in app and releases
+• Encrypted local backups
Synchronization:
-• Automatic or manual, optimized performance, optional periodic sync
-• Smart conflict resolution, deletion tracking, batch actions
+• Parallel downloads (up to 5 simultaneous)
+• Live sync progress with phase indicators
+• Smart conflict resolution, server deletion detection
+• Post-update changelog dialog
UI & Design:
• Modern Jetpack Compose interface
• Material Design 3, dynamic colors, dark mode
-• Animations and live sync status
+• Note & checklist sorting (title, date, color, alphabetical)
Multilingual:
• English and German, automatic detection, in-app language selector
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index 098f9e2..d6282e2 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index 3d01bb6..74dc93c 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index ba6a867..2d2649a 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png
index 53ba65d..41a5a2b 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png
index 607d151..cff4bd4 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
new file mode 100644
index 0000000..f8e34a1
Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
index 2a77315..a93837a 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7.png differ