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 @@
-[![Android](https://img.shields.io/badge/Android-8.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/) +[![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/) [![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/) [![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=jetpackcompose&logoColor=white)](https://developer.android.com/compose/) [![Material 3](https://img.shields.io/badge/Material_3-6750A4?style=for-the-badge&logo=material-design&logoColor=white)](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 @@
-[![Android](https://img.shields.io/badge/Android-8.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/) +[![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/) [![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/) [![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=jetpackcompose&logoColor=white)](https://developer.android.com/compose/) [![Material 3](https://img.shields.io/badge/Material_3-6750A4?style=for-the-badge&logo=material-design&logoColor=white)](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