Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5764e8c0ec | ||
|
|
b1e7f1750e | ||
|
|
da371436cd | ||
|
|
cf54f44377 | ||
|
|
bc3f137669 | ||
|
|
1a6617a4ec | ||
|
|
27e6b9d4ac | ||
|
|
3e4b1bd07e | ||
|
|
66d98c0cad | ||
|
|
c72b3fe1c0 | ||
|
|
a1a574a725 | ||
|
|
7dbc06d102 | ||
|
|
2c43b47e96 | ||
|
|
ffe0e46e3d | ||
|
|
fe6935a6b7 | ||
|
|
7b558113cf | ||
|
|
24fe32a973 | ||
|
|
b5a3a3c096 | ||
|
|
63561737e1 |
4
.github/workflows/build-production-apk.yml
vendored
@@ -61,11 +61,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p apk-output
|
mkdir -p apk-output
|
||||||
|
|
||||||
# Standard Flavor - Universal APK
|
# Standard Flavor
|
||||||
cp android/app/build/outputs/apk/standard/release/app-standard-release.apk \
|
cp android/app/build/outputs/apk/standard/release/app-standard-release.apk \
|
||||||
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard.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 \
|
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-release.apk \
|
||||||
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk
|
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/pr-build-check.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
- name: Gradle Cache
|
- name: Gradle Cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -69,14 +69,14 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Build-Ergebnis pruefen
|
- name: Build-Ergebnis pruefen
|
||||||
run: |
|
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"
|
echo "✅ Standard Debug APK erfolgreich gebaut"
|
||||||
ls -lh android/app/build/outputs/apk/standard/debug/*.apk
|
ls -lh android/app/build/outputs/apk/standard/debug/*.apk
|
||||||
else
|
else
|
||||||
echo "❌ Standard Debug APK Build fehlgeschlagen"
|
echo "❌ Standard Debug APK Build fehlgeschlagen"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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"
|
echo "✅ F-Droid Debug APK erfolgreich gebaut"
|
||||||
ls -lh android/app/build/outputs/apk/fdroid/debug/*.apk
|
ls -lh android/app/build/outputs/apk/fdroid/debug/*.apk
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -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
|
## [1.8.0] - 2026-02-10
|
||||||
|
|
||||||
### 🚨 CRITICAL BUGFIX (Tag neu erstellt)
|
### 🚨 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.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.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
|
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1
|
||||||
|
|||||||
92
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
|
## [1.8.0] - 2026-02-10
|
||||||
|
|
||||||
### 🚨 CRITICAL BUGFIX (Tag recreated)
|
### 🚨 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.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.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
|
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1
|
||||||
|
|||||||
@@ -94,10 +94,10 @@ Nutze die [Feature Request Template](https://github.com/inventory69/simple-notes
|
|||||||
Dokumentations-Verbesserungen sind auch Contributions!
|
Dokumentations-Verbesserungen sind auch Contributions!
|
||||||
|
|
||||||
**Dateien:**
|
**Dateien:**
|
||||||
- `README.md` / `README.en.md` - Übersicht
|
- `README.de.md` / `README.md` - Übersicht
|
||||||
- `QUICKSTART.md` / `QUICKSTART.en.md` - Schritt-für-Schritt Anleitung
|
- `QUICKSTART.de.md` / `QUICKSTART.md` - Schritt-für-Schritt Anleitung
|
||||||
- `DOCS.md` / `DOCS.en.md` - Technische Details
|
- `docs/DOCS.de.md` / `docs/DOCS.md` - Technische Details
|
||||||
- `server/README.md` / `server/README.en.md` - Server Setup
|
- `server/README.de.md` / `server/README.md` - Server Setup
|
||||||
|
|
||||||
**Bitte:** Halte beide Sprachen (DE/EN) synchron!
|
**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!
|
Documentation improvements are also contributions!
|
||||||
|
|
||||||
**Files:**
|
**Files:**
|
||||||
- `README.md` / `README.en.md` - Overview
|
- `README.de.md` / `README.md` - Overview
|
||||||
- `QUICKSTART.md` / `QUICKSTART.en.md` - Step-by-step guide
|
- `QUICKSTART.de.md` / `QUICKSTART.md` - Step-by-step guide
|
||||||
- `DOCS.md` / `DOCS.en.md` - Technical details
|
- `docs/DOCS.de.md` / `docs/DOCS.md` - Technical details
|
||||||
- `server/README.md` / `server/README.en.md` - Server setup
|
- `server/README.de.md` / `server/README.md` - Server setup
|
||||||
|
|
||||||
**Please:** Keep both languages (DE/EN) in sync!
|
**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).
|
Ö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! 🚀**
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- ✅ Android 8.0+ Smartphone/Tablet
|
- ✅ Android 7.0+ Smartphone/Tablet
|
||||||
- ✅ WLAN-Verbindung
|
- ✅ WLAN-Verbindung
|
||||||
- ✅ Eigener Server mit Docker (optional - für Self-Hosting)
|
- ✅ 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
|
### Schritt 2: App installieren
|
||||||
|
|
||||||
1. **APK herunterladen:** [Neueste Version](https://github.com/inventory69/simple-notes-sync/releases/latest)
|
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:**
|
2. **Installation erlauben:**
|
||||||
- Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren
|
- Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren
|
||||||
@@ -261,7 +261,7 @@ Für zuverlässigen Auto-Sync:
|
|||||||
## 🆘 Weitere Hilfe
|
## 🆘 Weitere Hilfe
|
||||||
|
|
||||||
- **GitHub Issues:** [Problem melden](https://github.com/inventory69/simple-notes-sync/issues)
|
- **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)
|
- **Server Setup Details:** [server/README.md](server/README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- ✅ Android 8.0+ smartphone/tablet
|
- ✅ Android 7.0+ smartphone/tablet
|
||||||
- ✅ WiFi connection
|
- ✅ WiFi connection
|
||||||
- ✅ Own server with Docker (optional - for self-hosting)
|
- ✅ 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
|
### Step 2: Install App
|
||||||
|
|
||||||
1. **Download APK:** [Latest version](https://github.com/inventory69/simple-notes-sync/releases/latest)
|
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:**
|
2. **Allow installation:**
|
||||||
- Android: Settings → Security → Enable "Unknown sources" for your browser
|
- 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.
|
> **💡 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
|
- ✅ Success? → Continue to step 4
|
||||||
- ❌ Error? → See [Troubleshooting](#troubleshooting)
|
- ❌ Error? → See [Troubleshooting](#troubleshooting)
|
||||||
|
|
||||||
@@ -261,8 +261,8 @@ For reliable auto-sync:
|
|||||||
## 🆘 Further Help
|
## 🆘 Further Help
|
||||||
|
|
||||||
- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues)
|
- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues)
|
||||||
- **Complete docs:** [DOCS.en.md](DOCS.en.md)
|
- **Complete docs:** [DOCS.md](docs/DOCS.md)
|
||||||
- **Server setup details:** [server/README.en.md](server/README.en.md)
|
- **Server setup details:** [server/README.md](server/README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
10
README.de.md
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://www.android.com/)
|
[](https://www.android.com/)
|
||||||
[](https://kotlinlang.org/)
|
[](https://kotlinlang.org/)
|
||||||
[](https://developer.android.com/compose/)
|
[](https://developer.android.com/compose/)
|
||||||
[](https://m3.material.io/)
|
[](https://m3.material.io/)
|
||||||
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" /></a>
|
|||||||
- 📝 **Offline-first** – Funktioniert ohne Internet
|
- 📝 **Offline-first** – Funktioniert ohne Internet
|
||||||
- 📊 **Flexible Ansichten** – Listen- und Grid-Layout
|
- 📊 **Flexible Ansichten** – Listen- und Grid-Layout
|
||||||
- ✅ **Checklisten** – Tap-to-Check, Drag & Drop
|
- ✅ **Checklisten** – Tap-to-Check, Drag & Drop
|
||||||
- 🌍 **Mehrsprachig** – Deutsch/Englisch mit Sprachauswahl
|
|
||||||
- 🔄 **Konfigurierbare Sync-Trigger** – onSave, onResume, WiFi, periodisch (15/30/60 Min), Boot
|
- 🔄 **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)
|
- 🔒 **Self-hosted** – Deine Daten bleiben bei dir (WebDAV)
|
||||||
- 💾 **Lokales Backup** – Export/Import als JSON-Datei (optional verschlüsselt)
|
- 💾 **Lokales Backup** – Export/Import als JSON-Datei (optional verschlüsselt)
|
||||||
- 🖥️ **Desktop-Integration** – Markdown-Export für Obsidian, VS Code, Typora
|
- 🖥️ **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
|
- 🎨 **Material Design 3** – Dynamischer Dark/Light Mode & Farben
|
||||||
|
|
||||||
➡️ **Vollständige Feature-Liste:** [docs/FEATURES.de.md](docs/FEATURES.de.md)
|
➡️ **Vollständige Feature-Liste:** [docs/FEATURES.de.md](docs/FEATURES.de.md)
|
||||||
@@ -138,6 +140,6 @@ MIT License – siehe [LICENSE](LICENSE)
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
**v1.7.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
**v1.8.1** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
10
README.md
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://www.android.com/)
|
[](https://www.android.com/)
|
||||||
[](https://kotlinlang.org/)
|
[](https://kotlinlang.org/)
|
||||||
[](https://developer.android.com/compose/)
|
[](https://developer.android.com/compose/)
|
||||||
[](https://m3.material.io/)
|
[](https://m3.material.io/)
|
||||||
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" /></a>
|
|||||||
- 📝 **Offline-first** - Works without internet
|
- 📝 **Offline-first** - Works without internet
|
||||||
- 📊 **Flexible views** - Switch between list and grid layout
|
- 📊 **Flexible views** - Switch between list and grid layout
|
||||||
- ✅ **Checklists** - Tap-to-check, drag & drop
|
- ✅ **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
|
- 🔄 **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)
|
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
|
||||||
- 💾 **Local backup** - Export/Import as JSON file (encryption available)
|
- 💾 **Local backup** - Export/Import as JSON file (encryption available)
|
||||||
- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
|
- 🖥️ **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
|
- 🎨 **Material Design 3** - Dynamic dark/light mode & colors based on system settings
|
||||||
|
|
||||||
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
|
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
|
||||||
@@ -148,6 +150,6 @@ MIT License - see [LICENSE](LICENSE)
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
<br /><br />
|
<br /><br />
|
||||||
|
|
||||||
**v1.7.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
**v1.8.1** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 20 // 🎉 v1.8.0: Widgets, Sorting, UI Polish, Post-Update Changelog
|
versionCode = 21 // 🐛 v1.8.1: Checklist Fixes, Widget Sorting, ProGuard Audit
|
||||||
versionName = "1.8.0" // 🎉 v1.8.0: Major Feature Release
|
versionName = "1.8.1" // 🐛 v1.8.1: Bugfix & Polish Release
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
20
android/app/proguard-rules.pro
vendored
@@ -77,3 +77,23 @@
|
|||||||
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
|
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
|
||||||
# This class only exists on API 35+ but Compose handles the fallback gracefully
|
# This class only exists on API 35+ but Compose handles the fallback gracefully
|
||||||
-dontwarn android.text.Layout$TextInclusionStrategy
|
-dontwarn android.text.Layout$TextInclusionStrategy
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
# v1.8.1: Widget & Compose Fixes
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Glance Widget ActionCallbacks (instanziiert via Reflection durch actionRunCallback<T>())
|
||||||
|
# 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.** {
|
||||||
|
<methods>;
|
||||||
|
}
|
||||||
|
-keep class androidx.compose.ui.text.TextLayoutResult { *; }
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ data class Note(
|
|||||||
val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,
|
val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,
|
||||||
// v1.4.0: Checklisten-Felder
|
// v1.4.0: Checklisten-Felder
|
||||||
val noteType: NoteType = NoteType.TEXT,
|
val noteType: NoteType = NoteType.TEXT,
|
||||||
val checklistItems: List<ChecklistItem>? = null
|
val checklistItems: List<ChecklistItem>? = null,
|
||||||
|
// 🆕 v1.8.1 (IMPL_03): Persistierte Sortierung
|
||||||
|
val checklistSortOption: String? = null
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Serialisiert Note zu JSON
|
* Serialisiert Note zu JSON
|
||||||
@@ -71,13 +73,20 @@ data class Note(
|
|||||||
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
||||||
*/
|
*/
|
||||||
fun toMarkdown(): String {
|
fun toMarkdown(): String {
|
||||||
|
// 🆕 v1.8.1 (IMPL_03): Sortierung im Frontmatter
|
||||||
|
val sortLine = if (noteType == NoteType.CHECKLIST && checklistSortOption != null) {
|
||||||
|
"\nsort: $checklistSortOption"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
val header = """
|
val header = """
|
||||||
---
|
---
|
||||||
id: $id
|
id: $id
|
||||||
created: ${formatISO8601(createdAt)}
|
created: ${formatISO8601(createdAt)}
|
||||||
updated: ${formatISO8601(updatedAt)}
|
updated: ${formatISO8601(updatedAt)}
|
||||||
device: $deviceId
|
device: $deviceId
|
||||||
type: ${noteType.name.lowercase()}
|
type: ${noteType.name.lowercase()}$sortLine
|
||||||
---
|
---
|
||||||
|
|
||||||
# $title
|
# $title
|
||||||
@@ -119,6 +128,14 @@ type: ${noteType.name.lowercase()}
|
|||||||
NoteType.TEXT
|
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
|
// Parsen der Basis-Note
|
||||||
val rawNote = gson.fromJson(json, NoteRaw::class.java)
|
val rawNote = gson.fromJson(json, NoteRaw::class.java)
|
||||||
|
|
||||||
@@ -158,7 +175,8 @@ type: ${noteType.name.lowercase()}
|
|||||||
deviceId = rawNote.deviceId,
|
deviceId = rawNote.deviceId,
|
||||||
syncStatus = rawNote.syncStatus ?: SyncStatus.LOCAL_ONLY,
|
syncStatus = rawNote.syncStatus ?: SyncStatus.LOCAL_ONLY,
|
||||||
noteType = noteType,
|
noteType = noteType,
|
||||||
checklistItems = checklistItems
|
checklistItems = checklistItems,
|
||||||
|
checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
|
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
|
||||||
@@ -246,6 +264,9 @@ type: ${noteType.name.lowercase()}
|
|||||||
else -> NoteType.TEXT
|
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
|
// v1.4.0: Parse Content basierend auf Typ
|
||||||
// FIX: Robusteres Parsing - suche nach dem Titel-Header und extrahiere den Rest
|
// FIX: Robusteres Parsing - suche nach dem Titel-Header und extrahiere den Rest
|
||||||
val titleLineIndex = contentBlock.lines().indexOfFirst { it.startsWith("# ") }
|
val titleLineIndex = contentBlock.lines().indexOfFirst { it.startsWith("# ") }
|
||||||
@@ -300,7 +321,8 @@ type: ${noteType.name.lowercase()}
|
|||||||
deviceId = metadata["device"] ?: "desktop",
|
deviceId = metadata["device"] ?: "desktop",
|
||||||
syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
|
syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
|
||||||
noteType = noteType,
|
noteType = noteType,
|
||||||
checklistItems = checklistItems
|
checklistItems = checklistItems,
|
||||||
|
checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
|
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
|
||||||
|
|||||||
@@ -52,9 +52,10 @@ data class SyncProgress(
|
|||||||
/**
|
/**
|
||||||
* Ob das Banner sichtbar sein soll
|
* Ob das Banner sichtbar sein soll
|
||||||
* Silent syncs zeigen nie ein Banner
|
* Silent syncs zeigen nie ein Banner
|
||||||
|
* 🆕 v1.8.1 (IMPL_12): INFO ist immer sichtbar (nicht vom silent-Flag betroffen)
|
||||||
*/
|
*/
|
||||||
val isVisible: Boolean
|
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)
|
* Ob gerade ein aktiver Sync läuft (mit Spinner)
|
||||||
@@ -95,5 +96,8 @@ enum class SyncPhase {
|
|||||||
COMPLETED,
|
COMPLETED,
|
||||||
|
|
||||||
/** Sync mit Fehler abgebrochen */
|
/** Sync mit Fehler abgebrochen */
|
||||||
ERROR
|
ERROR,
|
||||||
|
|
||||||
|
/** 🆕 v1.8.1 (IMPL_12): Kurzfristige Info-Meldung (nicht sync-bezogen) */
|
||||||
|
INFO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "═══════════════════════════════════════")
|
Logger.d(TAG, "═══════════════════════════════════════")
|
||||||
@@ -104,7 +105,42 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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
|
// 🔥 v1.1.2: Performance-Optimierung - Skip Sync wenn keine lokalen Änderungen
|
||||||
@@ -122,7 +158,7 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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)
|
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config)
|
||||||
@@ -143,7 +179,7 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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
|
// ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync
|
||||||
@@ -167,7 +203,7 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
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")
|
Logger.d(TAG, " SyncService: $syncService")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +224,7 @@ class SyncWorker(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "📍 Step 4: Processing result")
|
Logger.d(TAG, "📍 Step 7: Processing result")
|
||||||
Logger.d(
|
Logger.d(
|
||||||
TAG,
|
TAG,
|
||||||
"📦 Sync result: success=${result.isSuccess}, " +
|
"📦 Sync result: success=${result.isSuccess}, " +
|
||||||
@@ -198,10 +234,13 @@ class SyncWorker(
|
|||||||
|
|
||||||
if (result.isSuccess) {
|
if (result.isSuccess) {
|
||||||
if (BuildConfig.DEBUG) {
|
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")
|
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
|
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
|
||||||
// UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
|
// UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
|
||||||
if (result.syncedCount > 0) {
|
if (result.syncedCount > 0) {
|
||||||
@@ -248,9 +287,13 @@ class SyncWorker(
|
|||||||
Result.success()
|
Result.success()
|
||||||
} else {
|
} else {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "📍 Step 5: Failure path")
|
Logger.d(TAG, "📍 Step 8: Failure path")
|
||||||
}
|
}
|
||||||
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||||
|
|
||||||
|
// 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
|
||||||
|
SyncStateManager.markError(result.errorMessage)
|
||||||
|
|
||||||
NotificationHelper.showSyncError(
|
NotificationHelper.showSyncError(
|
||||||
applicationContext,
|
applicationContext,
|
||||||
result.errorMessage ?: "Unbekannter Fehler"
|
result.errorMessage ?: "Unbekannter Fehler"
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ class WifiSyncReceiver : BroadcastReceiver() {
|
|||||||
return
|
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)
|
// Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
|
||||||
if (isConnectedToWifi(context)) {
|
if (isConnectedToWifi(context)) {
|
||||||
scheduleSyncWork(context)
|
scheduleSyncWork(context)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
|
|||||||
* v1.5.0: NoteEditor Redesign
|
* v1.5.0: NoteEditor Redesign
|
||||||
* v1.8.0: IMPL_023 - Drag & Drop Fix (pointerInput key + Handle-only drag)
|
* 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.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(
|
class DragDropListState(
|
||||||
private val state: LazyListState,
|
private val state: LazyListState,
|
||||||
@@ -36,8 +37,14 @@ class DragDropListState(
|
|||||||
|
|
||||||
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
|
||||||
private var draggingItemInitialOffset 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<Job?>(null)
|
private var overscrollJob by mutableStateOf<Job?>(null)
|
||||||
|
|
||||||
|
// 🆕 v1.8.1 IMPL_14: Visual-Index des Separators (-1 = kein Separator)
|
||||||
|
var separatorVisualIndex by mutableStateOf(-1)
|
||||||
|
|
||||||
val draggingItemOffset: Float
|
val draggingItemOffset: Float
|
||||||
get() = draggingItemLayoutInfo?.let { item ->
|
get() = draggingItemLayoutInfo?.let { item ->
|
||||||
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
|
||||||
@@ -47,9 +54,28 @@ class DragDropListState(
|
|||||||
get() = state.layoutInfo.visibleItemsInfo
|
get() = state.layoutInfo.visibleItemsInfo
|
||||||
.firstOrNull { it.index == draggingItemIndex }
|
.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) {
|
fun onDragStart(offset: Offset, itemIndex: Int) {
|
||||||
draggingItemIndex = itemIndex
|
draggingItemIndex = itemIndex
|
||||||
draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f
|
val info = draggingItemLayoutInfo
|
||||||
|
draggingItemInitialOffset = info?.offset?.toFloat() ?: 0f
|
||||||
|
draggingItemSize = info?.size ?: 0
|
||||||
draggingItemDraggedDelta = 0f
|
draggingItemDraggedDelta = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +83,7 @@ class DragDropListState(
|
|||||||
draggingItemDraggedDelta = 0f
|
draggingItemDraggedDelta = 0f
|
||||||
draggingItemIndex = null
|
draggingItemIndex = null
|
||||||
draggingItemInitialOffset = 0f
|
draggingItemInitialOffset = 0f
|
||||||
|
draggingItemSize = 0
|
||||||
overscrollJob?.cancel()
|
overscrollJob?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +92,19 @@ class DragDropListState(
|
|||||||
|
|
||||||
val draggingItem = draggingItemLayoutInfo ?: return
|
val draggingItem = draggingItemLayoutInfo ?: return
|
||||||
val startOffset = draggingItem.offset + draggingItemOffset
|
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
|
// 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter
|
||||||
// Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"),
|
// 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.
|
// wird geprüft ob das gezogene Item den MITTELPUNKT des Targets überspannt.
|
||||||
// Dies verhindert Oszillation bei Items unterschiedlicher Größe.
|
// 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 ->
|
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 {
|
run {
|
||||||
val targetCenter = item.offset + item.size / 2
|
val targetCenter = item.offset + item.size / 2
|
||||||
startOffset < targetCenter && endOffset > targetCenter
|
startOffset < targetCenter && endOffset > targetCenter
|
||||||
@@ -89,15 +120,19 @@ class DragDropListState(
|
|||||||
null
|
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) {
|
if (scrollToIndex != null) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
|
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)
|
// 🆕 v1.8.0: IMPL_023b — Index-Update NACH dem Move (verhindert Race-Condition)
|
||||||
draggingItemIndex = targetItem.index
|
draggingItemIndex = targetItem.index
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
onMove(draggingItem.index, targetItem.index)
|
onMove(fromDataIndex, toDataIndex)
|
||||||
draggingItemIndex = targetItem.index
|
draggingItemIndex = targetItem.index
|
||||||
}
|
}
|
||||||
} else {
|
} 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")
|
@Suppress("UnusedPrivateProperty")
|
||||||
private val LazyListItemInfo.offsetEnd: Int
|
private val LazyListItemInfo.offsetEnd: Int
|
||||||
get() = this.offset + this.size
|
get() = this.offset + this.size
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.ui.editor
|
package dev.dettmer.simplenotes.ui.editor
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
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.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -70,8 +66,10 @@ import dev.dettmer.simplenotes.utils.showToast
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private const val LAYOUT_DELAY_MS = 100L
|
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 ITEM_CORNER_RADIUS_DP = 8
|
||||||
private const val DRAGGING_ITEM_Z_INDEX = 10f
|
private const val DRAGGING_ITEM_Z_INDEX = 10f
|
||||||
|
private val DRAGGING_ELEVATION_DP = 8.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main Composable for the Note Editor screen.
|
* 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
|
@Suppress("LongParameterList") // Compose functions commonly have many callback parameters
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChecklistEditor(
|
private fun ChecklistEditor(
|
||||||
@@ -351,6 +409,9 @@ private fun ChecklistEditor(
|
|||||||
onMove = onMove
|
onMove = onMove
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🆕 v1.8.1 (IMPL_05): Auto-Scroll bei Zeilenumbruch
|
||||||
|
var scrollToItemIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
|
|
||||||
// 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen
|
// 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen
|
||||||
val uncheckedCount = items.count { !it.isChecked }
|
val uncheckedCount = items.count { !it.isChecked }
|
||||||
val checkedCount = items.count { it.isChecked }
|
val checkedCount = items.count { it.isChecked }
|
||||||
@@ -359,70 +420,78 @@ private fun ChecklistEditor(
|
|||||||
val showSeparator = shouldShowSeparator && uncheckedCount > 0 && checkedCount > 0
|
val showSeparator = shouldShowSeparator && uncheckedCount > 0 && checkedCount > 0
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
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(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
|
// 🆕 v1.8.1 IMPL_14: Unchecked Items (Visual Index 0..uncheckedCount-1)
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
items = items,
|
items = if (showSeparator) items.subList(0, uncheckedCount) else items,
|
||||||
key = { _, item -> item.id }
|
key = { _, item -> item.id }
|
||||||
) { index, item ->
|
) { index, item ->
|
||||||
// 🆕 v1.8.0 (IMPL_017): Separator vor dem ersten Checked-Item
|
DraggableChecklistItem(
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
val shouldFocus = item.id == focusNewItemId
|
|
||||||
|
|
||||||
// v1.5.0: Clear focus request after handling
|
|
||||||
LaunchedEffect(shouldFocus) {
|
|
||||||
if (shouldFocus) {
|
|
||||||
onFocusHandled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 v1.8.0 (IMPL_017): AnimatedVisibility für sanfte Übergänge
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = true,
|
|
||||||
enter = fadeIn() + slideInVertically(),
|
|
||||||
exit = fadeOut() + slideOutVertically()
|
|
||||||
) {
|
|
||||||
ChecklistItemRow(
|
|
||||||
item = item,
|
item = item,
|
||||||
onTextChange = { onTextChange(item.id, it) },
|
visualIndex = index,
|
||||||
onCheckedChange = { onCheckedChange(item.id, it) },
|
dragDropState = dragDropState,
|
||||||
onDelete = { onDelete(item.id) },
|
focusNewItemId = focusNewItemId,
|
||||||
onAddNewItem = { onAddNewItemAfter(item.id) },
|
onTextChange = onTextChange,
|
||||||
requestFocus = shouldFocus,
|
onCheckedChange = onCheckedChange,
|
||||||
// 🆕 v1.8.0: IMPL_023 - Drag state übergeben
|
onDelete = onDelete,
|
||||||
isDragging = isDragging,
|
onAddNewItemAfter = onAddNewItemAfter,
|
||||||
// 🆕 v1.8.0: IMPL_023 - Gradient während Drag ausblenden
|
onFocusHandled = onFocusHandled,
|
||||||
isAnyItemDragging = dragDropState.draggingItemIndex != null,
|
onHeightChanged = { scrollToItemIndex = index } // 🆕 v1.8.1 (IMPL_05)
|
||||||
// 🆕 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)
|
// 🆕 v1.8.1 IMPL_14: Separator als eigenes LazyColumn-Item
|
||||||
.shadow(elevation, shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp))
|
if (showSeparator) {
|
||||||
.background(
|
item(key = "separator") {
|
||||||
color = MaterialTheme.colorScheme.surface,
|
CheckedItemsSeparator(
|
||||||
shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp)
|
checkedCount = checkedCount,
|
||||||
|
isDragActive = dragDropState.draggingItemIndex != null
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 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,
|
||||||
|
visualIndex = visualIndex,
|
||||||
|
dragDropState = dragDropState,
|
||||||
|
focusNewItemId = focusNewItemId,
|
||||||
|
onTextChange = onTextChange,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
onDelete = onDelete,
|
||||||
|
onAddNewItemAfter = onAddNewItemAfter,
|
||||||
|
onFocusHandled = onFocusHandled,
|
||||||
|
onHeightChanged = { scrollToItemIndex = visualIndex } // 🆕 v1.8.1 (IMPL_05)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import dev.dettmer.simplenotes.models.Note
|
|||||||
import dev.dettmer.simplenotes.models.NoteType
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.models.SyncStatus
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
|
import dev.dettmer.simplenotes.sync.SyncStateManager
|
||||||
import dev.dettmer.simplenotes.sync.SyncWorker
|
import dev.dettmer.simplenotes.sync.SyncWorker
|
||||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
@@ -90,7 +91,13 @@ class NoteEditorViewModel(
|
|||||||
val noteTypeString = savedStateHandle.get<String>(ARG_NOTE_TYPE) ?: NoteType.TEXT.name
|
val noteTypeString = savedStateHandle.get<String>(ARG_NOTE_TYPE) ?: NoteType.TEXT.name
|
||||||
|
|
||||||
if (noteId != null) {
|
if (noteId != null) {
|
||||||
// Load existing note
|
loadExistingNote(noteId)
|
||||||
|
} else {
|
||||||
|
initNewNote(noteTypeString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadExistingNote(noteId: String) {
|
||||||
existingNote = storage.loadNote(noteId)
|
existingNote = storage.loadNote(noteId)
|
||||||
existingNote?.let { note ->
|
existingNote?.let { note ->
|
||||||
currentNoteType = note.noteType
|
currentNoteType = note.noteType
|
||||||
@@ -109,6 +116,17 @@ class NoteEditorViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (note.noteType == NoteType.CHECKLIST) {
|
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 {
|
val items = note.checklistItems?.sortedBy { it.order }?.map {
|
||||||
ChecklistItemState(
|
ChecklistItemState(
|
||||||
id = it.id,
|
id = it.id,
|
||||||
@@ -120,13 +138,12 @@ class NoteEditorViewModel(
|
|||||||
// 🆕 v1.8.0 (IMPL_017): Sortierung sicherstellen (falls alte Daten unsortiert sind)
|
// 🆕 v1.8.0 (IMPL_017): Sortierung sicherstellen (falls alte Daten unsortiert sind)
|
||||||
_checklistItems.value = sortChecklistItems(items)
|
_checklistItems.value = sortChecklistItems(items)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
private fun initNewNote(noteTypeString: String) {
|
||||||
// New note
|
|
||||||
currentNoteType = try {
|
currentNoteType = try {
|
||||||
NoteType.valueOf(noteTypeString)
|
NoteType.valueOf(noteTypeString)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
|
||||||
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT: ${e.message}")
|
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT")
|
||||||
NoteType.TEXT
|
NoteType.TEXT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +164,18 @@ class NoteEditorViewModel(
|
|||||||
_checklistItems.value = listOf(ChecklistItemState.createEmpty(0))
|
_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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -173,11 +202,28 @@ class NoteEditorViewModel(
|
|||||||
* 🆕 v1.8.0 (IMPL_017): Sortiert Checklist-Items mit Unchecked oben, Checked unten.
|
* 🆕 v1.8.0 (IMPL_017): Sortiert Checklist-Items mit Unchecked oben, Checked unten.
|
||||||
* Stabile Sortierung: Relative Reihenfolge innerhalb jeder Gruppe bleibt erhalten.
|
* 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<ChecklistItemState>): List<ChecklistItemState> {
|
private fun sortChecklistItems(items: List<ChecklistItemState>): List<ChecklistItemState> {
|
||||||
|
val sorted = when (_lastChecklistSortOption.value) {
|
||||||
|
ChecklistSortOption.MANUAL,
|
||||||
|
ChecklistSortOption.UNCHECKED_FIRST -> {
|
||||||
val unchecked = items.filter { !it.isChecked }
|
val unchecked = items.filter { !it.isChecked }
|
||||||
val checked = 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)
|
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 {
|
fun addChecklistItemAfter(afterItemId: String): String {
|
||||||
val newItem = ChecklistItemState.createEmpty(0)
|
val newItem = ChecklistItemState.createEmpty(0)
|
||||||
_checklistItems.update { items ->
|
_checklistItems.update { items ->
|
||||||
val index = items.indexOfFirst { it.id == afterItemId }
|
val index = items.indexOfFirst { it.id == afterItemId }
|
||||||
if (index >= 0) {
|
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()
|
val newList = items.toMutableList()
|
||||||
newList.add(index + 1, newItem)
|
newList.add(effectiveIndex, newItem)
|
||||||
// Update order values
|
// Update order values
|
||||||
newList.mapIndexed { i, item -> item.copy(order = i) }
|
newList.mapIndexed { i, item -> item.copy(order = i) }
|
||||||
} else {
|
} else {
|
||||||
@@ -214,12 +281,46 @@ class NoteEditorViewModel(
|
|||||||
return newItem.id
|
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 {
|
fun addChecklistItemAtEnd(): String {
|
||||||
val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size)
|
val newItem = ChecklistItemState.createEmpty(0)
|
||||||
_checklistItems.update { items -> items + newItem }
|
_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
|
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<ChecklistItemState>): 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) {
|
fun deleteChecklistItem(itemId: String) {
|
||||||
_checklistItems.update { items ->
|
_checklistItems.update { items ->
|
||||||
val filtered = items.filter { it.id != itemId }
|
val filtered = items.filter { it.id != itemId }
|
||||||
@@ -238,15 +339,18 @@ class NoteEditorViewModel(
|
|||||||
val fromItem = items.getOrNull(fromIndex) ?: return@update items
|
val fromItem = items.getOrNull(fromIndex) ?: return@update items
|
||||||
val toItem = items.getOrNull(toIndex) ?: 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 mutableList = items.toMutableList()
|
||||||
val item = mutableList.removeAt(fromIndex)
|
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
|
// Update order values
|
||||||
mutableList.mapIndexed { index, i -> i.copy(order = index) }
|
mutableList.mapIndexed { index, i -> i.copy(order = index) }
|
||||||
}
|
}
|
||||||
@@ -348,6 +452,7 @@ class NoteEditorViewModel(
|
|||||||
content = "", // Empty for checklists
|
content = "", // Empty for checklists
|
||||||
noteType = NoteType.CHECKLIST,
|
noteType = NoteType.CHECKLIST,
|
||||||
checklistItems = validItems,
|
checklistItems = validItems,
|
||||||
|
checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
|
||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
syncStatus = SyncStatus.PENDING
|
syncStatus = SyncStatus.PENDING
|
||||||
)
|
)
|
||||||
@@ -357,6 +462,7 @@ class NoteEditorViewModel(
|
|||||||
content = "",
|
content = "",
|
||||||
noteType = NoteType.CHECKLIST,
|
noteType = NoteType.CHECKLIST,
|
||||||
checklistItems = validItems,
|
checklistItems = validItems,
|
||||||
|
checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
|
||||||
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
|
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
|
||||||
syncStatus = SyncStatus.LOCAL_ONLY
|
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
|
// 🌟 v1.6.0: Trigger onSave Sync
|
||||||
triggerOnSaveSync()
|
triggerOnSaveSync()
|
||||||
@@ -406,17 +512,33 @@ class NoteEditorViewModel(
|
|||||||
val success = withContext(Dispatchers.IO) {
|
val success = withContext(Dispatchers.IO) {
|
||||||
webdavService.deleteNoteFromServer(noteId)
|
webdavService.deleteNoteFromServer(noteId)
|
||||||
}
|
}
|
||||||
|
// 🆕 v1.8.1 (IMPL_12): Banner-Feedback statt stiller Log-Einträge
|
||||||
if (success) {
|
if (success) {
|
||||||
Logger.d(TAG, "Note $noteId deleted from server")
|
Logger.d(TAG, "Note $noteId deleted from server")
|
||||||
|
SyncStateManager.showInfo(
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
dev.dettmer.simplenotes.R.string.snackbar_deleted_from_server
|
||||||
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Logger.w(TAG, "Failed to delete note $noteId from server")
|
Logger.w(TAG, "Failed to delete note $noteId from server")
|
||||||
|
SyncStateManager.showError(
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
dev.dettmer.simplenotes.R.string.snackbar_server_delete_failed
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "Error deleting note from server: ${e.message}")
|
Logger.e(TAG, "Error deleting note from server: ${e.message}")
|
||||||
|
SyncStateManager.showError(
|
||||||
|
getApplication<Application>().getString(
|
||||||
|
dev.dettmer.simplenotes.R.string.snackbar_server_error,
|
||||||
|
e.message ?: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_DELETED))
|
|
||||||
_events.emit(NoteEditorEvent.NavigateBack)
|
_events.emit(NoteEditorEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,6 +635,7 @@ class NoteEditorViewModel(
|
|||||||
Logger.d(TAG, "📤 Triggering onSave sync")
|
Logger.d(TAG, "📤 Triggering onSave sync")
|
||||||
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
.addTag(Constants.SYNC_WORK_TAG)
|
.addTag(Constants.SYNC_WORK_TAG)
|
||||||
|
.addTag(Constants.SYNC_ONSAVE_TAG) // 🆕 v1.8.1 (IMPL_08B): Bypassed globalen Cooldown
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance(getApplication()).enqueue(syncRequest)
|
WorkManager.getInstance(getApplication()).enqueue(syncRequest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import dev.dettmer.simplenotes.R
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 🆕 v1.8.0 (IMPL_017): Visueller Separator zwischen unchecked und checked Items
|
* 🆕 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:
|
* Zeigt eine dezente Linie mit Anzahl der erledigten Items:
|
||||||
* ── 3 completed ──
|
* ── 3 completed ──
|
||||||
@@ -22,7 +23,8 @@ import dev.dettmer.simplenotes.R
|
|||||||
@Composable
|
@Composable
|
||||||
fun CheckedItemsSeparator(
|
fun CheckedItemsSeparator(
|
||||||
checkedCount: Int,
|
checkedCount: Int,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
isDragActive: Boolean = false // 🆕 v1.8.1 IMPL_14
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -32,7 +34,10 @@ fun CheckedItemsSeparator(
|
|||||||
) {
|
) {
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
color = if (isDragActive)
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -42,13 +47,19 @@ fun CheckedItemsSeparator(
|
|||||||
checkedCount
|
checkedCount
|
||||||
),
|
),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
color = MaterialTheme.colorScheme.outline,
|
color = if (isDragActive)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.outline,
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
modifier = Modifier.padding(horizontal = 12.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
color = MaterialTheme.colorScheme.outlineVariant
|
color = if (isDragActive)
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.outlineVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -71,6 +71,7 @@ fun ChecklistItemRow(
|
|||||||
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
|
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
|
||||||
isAnyItemDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Hide gradient during any drag
|
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
|
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
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
@@ -92,17 +93,14 @@ fun ChecklistItemRow(
|
|||||||
// 🆕 v1.8.0: ScrollState für dynamischen Gradient
|
// 🆕 v1.8.0: ScrollState für dynamischen Gradient
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
// 🆕 v1.8.0: Scroll-basierter Ansatz aktiv wenn Höhe berechnet wurde
|
// 🆕 v1.8.1: IMPL_05 - Letzte Zeilenanzahl tracken für Auto-Scroll
|
||||||
val useScrollClipping = hasOverflow && collapsedHeightDp != null
|
var lastLineCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// 🆕 v1.8.0: Dynamische Gradient-Sichtbarkeit basierend auf Scroll-Position
|
// 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf)
|
||||||
val showGradient = useScrollClipping && !isFocused && !isAnyItemDragging
|
// derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert.
|
||||||
val showTopGradient by remember {
|
val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging
|
||||||
derivedStateOf { showGradient && scrollState.value > 0 }
|
val showTopGradient = showGradient && scrollState.value > 0
|
||||||
}
|
val showBottomGradient = showGradient && scrollState.value < scrollState.maxValue
|
||||||
val showBottomGradient by remember {
|
|
||||||
derivedStateOf { showGradient && scrollState.value < scrollState.maxValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
|
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
|
||||||
LaunchedEffect(requestFocus) {
|
LaunchedEffect(requestFocus) {
|
||||||
@@ -173,7 +171,7 @@ fun ChecklistItemRow(
|
|||||||
Box(modifier = Modifier.weight(1f)) {
|
Box(modifier = Modifier.weight(1f)) {
|
||||||
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
|
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
|
||||||
Box(
|
Box(
|
||||||
modifier = if (!isFocused && useScrollClipping) {
|
modifier = if (!isFocused && hasOverflow && collapsedHeightDp != null) {
|
||||||
Modifier
|
Modifier
|
||||||
.heightIn(max = collapsedHeightDp!!)
|
.heightIn(max = collapsedHeightDp!!)
|
||||||
.verticalScroll(scrollState)
|
.verticalScroll(scrollState)
|
||||||
@@ -216,13 +214,16 @@ fun ChecklistItemRow(
|
|||||||
onNext = { onAddNewItem() }
|
onNext = { onAddNewItem() }
|
||||||
),
|
),
|
||||||
singleLine = false,
|
singleLine = false,
|
||||||
// maxLines nur als Fallback bis collapsedHeight berechnet ist
|
// 🆕 v1.8.1: maxLines IMMER Int.MAX_VALUE — keine Oszillation möglich
|
||||||
maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES,
|
// 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),
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
onTextLayout = { textLayoutResult ->
|
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) {
|
if (!isAnyItemDragging) {
|
||||||
val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES
|
val overflow = lineCount > COLLAPSED_MAX_LINES
|
||||||
hasOverflow = overflow
|
hasOverflow = overflow
|
||||||
// Höhe der ersten 5 Zeilen berechnen (einmalig)
|
// Höhe der ersten 5 Zeilen berechnen (einmalig)
|
||||||
if (overflow && collapsedHeightDp == null) {
|
if (overflow && collapsedHeightDp == null) {
|
||||||
@@ -230,7 +231,16 @@ fun ChecklistItemRow(
|
|||||||
textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp()
|
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 ->
|
decorationBox = { innerTextField ->
|
||||||
Box {
|
Box {
|
||||||
|
|||||||
@@ -236,6 +236,11 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
kotlinx.coroutines.delay(2000L)
|
kotlinx.coroutines.delay(2000L)
|
||||||
SyncStateManager.reset()
|
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 -> {
|
dev.dettmer.simplenotes.sync.SyncPhase.ERROR -> {
|
||||||
kotlinx.coroutines.delay(4000L)
|
kotlinx.coroutines.delay(4000L)
|
||||||
SyncStateManager.reset()
|
SyncStateManager.reset()
|
||||||
|
|||||||
@@ -470,12 +470,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (success) {
|
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 {
|
} else {
|
||||||
_showToast.emit(getString(R.string.snackbar_server_delete_failed))
|
SyncStateManager.showError(getString(R.string.snackbar_server_delete_failed))
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_showToast.emit(getString(R.string.snackbar_server_error, e.message ?: ""))
|
SyncStateManager.showError(getString(R.string.snackbar_server_error, e.message ?: ""))
|
||||||
} finally {
|
} finally {
|
||||||
// Remove from pending deletions
|
// Remove from pending deletions
|
||||||
_pendingDeletions.value = _pendingDeletions.value - noteId
|
_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 {
|
val message = when {
|
||||||
failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount)
|
failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount)
|
||||||
successCount == 0 -> getString(R.string.snackbar_server_delete_failed)
|
successCount == 0 -> getString(R.string.snackbar_server_delete_failed)
|
||||||
@@ -517,7 +518,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
successCount + failCount
|
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
|
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<android.app.Application>().getSharedPreferences(
|
||||||
|
Constants.PREFS_NAME,
|
||||||
|
android.content.Context.MODE_PRIVATE
|
||||||
|
)
|
||||||
|
|
||||||
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft
|
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft
|
||||||
// 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant
|
// 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant
|
||||||
if (!SyncStateManager.tryStartSync(source)) {
|
if (!SyncStateManager.tryStartSync(source)) {
|
||||||
@@ -571,6 +583,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (nach tryStartSync, vor Launch)
|
||||||
|
SyncStateManager.markGlobalSyncStarted(prefs)
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
// Check for unsynced changes (Banner zeigt bereits PREPARING)
|
// Check for unsynced changes (Banner zeigt bereits PREPARING)
|
||||||
@@ -636,7 +651,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
return
|
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()) {
|
if (!canTriggerAutoSync()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -665,6 +685,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// Update last sync timestamp
|
// Update last sync timestamp
|
||||||
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
|
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 {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
// Check for unsynced changes
|
// Check for unsynced changes
|
||||||
@@ -692,9 +715,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
|
|
||||||
if (result.isSuccess && result.syncedCount > 0) {
|
if (result.isSuccess && result.syncedCount > 0) {
|
||||||
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
|
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))
|
SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
|
||||||
_showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount))
|
|
||||||
loadNotes()
|
loadNotes()
|
||||||
} else if (result.isSuccess) {
|
} else if (result.isSuccess) {
|
||||||
Logger.d(TAG, "ℹ️ Auto-sync ($source): No changes")
|
Logger.d(TAG, "ℹ️ Auto-sync ($source): No changes")
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.ui.main
|
package dev.dettmer.simplenotes.ui.main
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.withLink
|
import androidx.compose.ui.text.withLink
|
||||||
import androidx.compose.ui.text.withStyle
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.dettmer.simplenotes.BuildConfig
|
import dev.dettmer.simplenotes.BuildConfig
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
|
|||||||
@@ -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<ChecklistItem>,
|
||||||
|
sortOptionName: String?
|
||||||
|
): List<ChecklistItem> {
|
||||||
|
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<ChecklistItem>,
|
||||||
|
sortOptionName: String?
|
||||||
|
): String {
|
||||||
|
val sorted = sortChecklistItemsForPreview(items, sortOptionName)
|
||||||
|
return sorted.joinToString("\n") { item ->
|
||||||
|
val prefix = if (item.isChecked) "☑️" else "☐"
|
||||||
|
"$prefix ${item.text}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,10 +149,9 @@ fun NoteCardCompact(
|
|||||||
text = when (note.noteType) {
|
text = when (note.noteType) {
|
||||||
NoteType.TEXT -> note.content
|
NoteType.TEXT -> note.content
|
||||||
NoteType.CHECKLIST -> {
|
NoteType.CHECKLIST -> {
|
||||||
note.checklistItems
|
// 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
|
||||||
?.joinToString("\n") { item ->
|
note.checklistItems?.let { items ->
|
||||||
val prefix = if (item.isChecked) "✅" else "☐"
|
generateChecklistPreview(items, note.checklistSortOption)
|
||||||
"$prefix ${item.text}"
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -163,10 +163,9 @@ fun NoteCardGrid(
|
|||||||
text = when (note.noteType) {
|
text = when (note.noteType) {
|
||||||
NoteType.TEXT -> note.content
|
NoteType.TEXT -> note.content
|
||||||
NoteType.CHECKLIST -> {
|
NoteType.CHECKLIST -> {
|
||||||
note.checklistItems
|
// 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
|
||||||
?.joinToString("\n") { item ->
|
note.checklistItems?.let { items ->
|
||||||
val prefix = if (item.isChecked) "✅" else "☐"
|
generateChecklistPreview(items, note.checklistSortOption)
|
||||||
"$prefix ${item.text}"
|
|
||||||
} ?: ""
|
} ?: ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
import androidx.compose.material.icons.filled.ErrorOutline
|
import androidx.compose.material.icons.filled.ErrorOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LinearProgressIndicator
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
@@ -51,11 +52,13 @@ fun SyncProgressBanner(
|
|||||||
// Farbe animiert wechseln je nach State
|
// Farbe animiert wechseln je nach State
|
||||||
val isError = progress.phase == SyncPhase.ERROR
|
val isError = progress.phase == SyncPhase.ERROR
|
||||||
val isCompleted = progress.phase == SyncPhase.COMPLETED
|
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(
|
val backgroundColor by animateColorAsState(
|
||||||
targetValue = when {
|
targetValue = when {
|
||||||
isError -> MaterialTheme.colorScheme.errorContainer
|
isError -> MaterialTheme.colorScheme.errorContainer
|
||||||
|
isInfo -> MaterialTheme.colorScheme.secondaryContainer // 🆕 v1.8.1 (IMPL_12)
|
||||||
else -> MaterialTheme.colorScheme.primaryContainer
|
else -> MaterialTheme.colorScheme.primaryContainer
|
||||||
},
|
},
|
||||||
label = "bannerColor"
|
label = "bannerColor"
|
||||||
@@ -64,6 +67,7 @@ fun SyncProgressBanner(
|
|||||||
val contentColor by animateColorAsState(
|
val contentColor by animateColorAsState(
|
||||||
targetValue = when {
|
targetValue = when {
|
||||||
isError -> MaterialTheme.colorScheme.onErrorContainer
|
isError -> MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
isInfo -> MaterialTheme.colorScheme.onSecondaryContainer // 🆕 v1.8.1 (IMPL_12)
|
||||||
else -> MaterialTheme.colorScheme.onPrimaryContainer
|
else -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
},
|
},
|
||||||
label = "bannerContentColor"
|
label = "bannerContentColor"
|
||||||
@@ -89,7 +93,7 @@ fun SyncProgressBanner(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
// Icon: Spinner (aktiv), Checkmark (completed), Error (error)
|
// Icon: Spinner (aktiv), Checkmark (completed), Error (error), Info (info)
|
||||||
when {
|
when {
|
||||||
isCompleted -> {
|
isCompleted -> {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -99,6 +103,14 @@ fun SyncProgressBanner(
|
|||||||
tint = contentColor
|
tint = contentColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
isInfo -> { // 🆕 v1.8.1 (IMPL_12)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
tint = contentColor
|
||||||
|
)
|
||||||
|
}
|
||||||
isError -> {
|
isError -> {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ErrorOutline,
|
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.IMPORTING_MARKDOWN -> stringResource(R.string.sync_phase_importing_markdown)
|
||||||
SyncPhase.COMPLETED -> stringResource(R.string.sync_phase_completed)
|
SyncPhase.COMPLETED -> stringResource(R.string.sync_phase_completed)
|
||||||
SyncPhase.ERROR -> stringResource(R.string.sync_phase_error)
|
SyncPhase.ERROR -> stringResource(R.string.sync_phase_error)
|
||||||
|
SyncPhase.INFO -> "" // 🆕 v1.8.1 (IMPL_12): INFO nutzt immer resultMessage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,4 +82,11 @@ object Constants {
|
|||||||
|
|
||||||
// 📋 v1.8.0: Post-Update Changelog
|
// 📋 v1.8.0: Post-Update Changelog
|
||||||
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,13 +38,18 @@ class NoteWidget : GlanceAppWidget() {
|
|||||||
// Responsive Breakpoints — schmale + breite Spalten
|
// Responsive Breakpoints — schmale + breite Spalten
|
||||||
val SIZE_SMALL = DpSize(110.dp, 80.dp) // Schmal+kurz: nur Titel
|
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_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_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_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
|
val SIZE_WIDE_LARGE = DpSize(250.dp, 250.dp) // Breit+groß: voller Inhalt
|
||||||
}
|
}
|
||||||
|
|
||||||
override val sizeMode = SizeMode.Responsive(
|
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
|
override val stateDefinition = PreferencesGlanceStateDefinition
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.glance.GlanceId
|
|||||||
import androidx.glance.action.ActionParameters
|
import androidx.glance.action.ActionParameters
|
||||||
import androidx.glance.appwidget.action.ActionCallback
|
import androidx.glance.appwidget.action.ActionCallback
|
||||||
import androidx.glance.appwidget.state.updateAppWidgetState
|
import androidx.glance.appwidget.state.updateAppWidgetState
|
||||||
|
import dev.dettmer.simplenotes.models.ChecklistSortOption
|
||||||
import dev.dettmer.simplenotes.models.SyncStatus
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
@@ -51,14 +52,32 @@ class ToggleChecklistItemAction : ActionCallback {
|
|||||||
} else item
|
} else item
|
||||||
} ?: return
|
} ?: 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(
|
val updatedNote = note.copy(
|
||||||
checklistItems = updatedItems,
|
checklistItems = sortedItems,
|
||||||
updatedAt = System.currentTimeMillis(),
|
updatedAt = System.currentTimeMillis(),
|
||||||
syncStatus = SyncStatus.PENDING
|
syncStatus = SyncStatus.PENDING
|
||||||
)
|
)
|
||||||
|
|
||||||
storage.saveNote(updatedNote)
|
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
|
// 🐛 FIX: Glance-State ändern um Re-Render zu erzwingen
|
||||||
updateAppWidgetState(context, glanceId) { prefs ->
|
updateAppWidgetState(context, glanceId) { prefs ->
|
||||||
|
|||||||
@@ -38,9 +38,11 @@ import androidx.glance.layout.width
|
|||||||
import androidx.glance.text.Text
|
import androidx.glance.text.Text
|
||||||
import androidx.glance.text.TextStyle
|
import androidx.glance.text.TextStyle
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
|
import dev.dettmer.simplenotes.models.ChecklistSortOption
|
||||||
import dev.dettmer.simplenotes.models.Note
|
import dev.dettmer.simplenotes.models.Note
|
||||||
import dev.dettmer.simplenotes.models.NoteType
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
|
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
|
* 🆕 v1.8.0: Glance Composable Content für das Notiz-Widget
|
||||||
@@ -52,6 +54,7 @@ import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
|
|||||||
// ── Size Classification ──
|
// ── Size Classification ──
|
||||||
|
|
||||||
private val WIDGET_HEIGHT_SMALL_THRESHOLD = 110.dp
|
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
|
private val WIDGET_SIZE_MEDIUM_THRESHOLD = 250.dp
|
||||||
|
|
||||||
// 🆕 v1.8.0: Increased preview lengths for better text visibility
|
// 🆕 v1.8.0: Increased preview lengths for better text visibility
|
||||||
@@ -60,12 +63,40 @@ private const val TEXT_PREVIEW_FULL_LENGTH = 300
|
|||||||
|
|
||||||
private fun DpSize.toSizeClass(): WidgetSizeClass = when {
|
private fun DpSize.toSizeClass(): WidgetSizeClass = when {
|
||||||
height < WIDGET_HEIGHT_SMALL_THRESHOLD -> WidgetSizeClass.SMALL
|
height < WIDGET_HEIGHT_SMALL_THRESHOLD -> WidgetSizeClass.SMALL
|
||||||
width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_MED
|
|
||||||
|
// 🆕 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
|
width < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_TALL
|
||||||
height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.WIDE_MED
|
|
||||||
|
height < WIDGET_HEIGHT_SCROLL_THRESHOLD -> WidgetSizeClass.WIDE_MED
|
||||||
|
height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.WIDE_SCROLL
|
||||||
else -> WidgetSizeClass.WIDE_TALL
|
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
|
@Composable
|
||||||
fun NoteWidgetContent(
|
fun NoteWidgetContent(
|
||||||
note: Note?,
|
note: Note?,
|
||||||
@@ -177,16 +208,30 @@ 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) {
|
when (note.noteType) {
|
||||||
NoteType.TEXT -> TextNoteFullView(note)
|
NoteType.TEXT -> Box(modifier = contentClickModifier) {
|
||||||
NoteType.CHECKLIST -> ChecklistFullView(
|
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,
|
note = note,
|
||||||
isLocked = isLocked,
|
isLocked = isLocked,
|
||||||
glanceId = glanceId
|
glanceId = glanceId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WidgetSizeClass.WIDE_MED -> Box(modifier = contentClickModifier) {
|
WidgetSizeClass.WIDE_MED -> Box(modifier = contentClickModifier) {
|
||||||
when (note.noteType) {
|
when (note.noteType) {
|
||||||
@@ -200,10 +245,22 @@ 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) {
|
when (note.noteType) {
|
||||||
NoteType.TEXT -> TextNoteFullView(note)
|
NoteType.TEXT -> Box(modifier = contentClickModifier) {
|
||||||
NoteType.CHECKLIST -> ChecklistFullView(
|
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,
|
note = note,
|
||||||
isLocked = isLocked,
|
isLocked = isLocked,
|
||||||
glanceId = glanceId
|
glanceId = glanceId
|
||||||
@@ -214,6 +271,8 @@ fun NoteWidgetContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionsleiste — Lock/Unlock + Refresh + Open in App
|
* Optionsleiste — Lock/Unlock + Refresh + Open in App
|
||||||
@@ -370,13 +429,35 @@ private fun ChecklistCompactView(
|
|||||||
isLocked: Boolean,
|
isLocked: Boolean,
|
||||||
glanceId: GlanceId
|
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 visibleItems = items.take(maxItems)
|
||||||
val remainingCount = items.size - visibleItems.size
|
val remainingCount = items.size - visibleItems.size
|
||||||
val checkedCount = items.count { it.isChecked }
|
|
||||||
|
|
||||||
Column(modifier = GlanceModifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
|
Column(modifier = GlanceModifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
|
||||||
|
var separatorShown = false
|
||||||
visibleItems.forEach { item ->
|
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) {
|
if (isLocked) {
|
||||||
Row(
|
Row(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
@@ -385,7 +466,7 @@ private fun ChecklistCompactView(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (item.isChecked) "✅" else "☐",
|
text = if (item.isChecked) "☑️" else "☐", // 🆕 v1.8.1 (IMPL_06)
|
||||||
style = TextStyle(fontSize = 14.sp)
|
style = TextStyle(fontSize = 14.sp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = GlanceModifier.width(6.dp))
|
Spacer(modifier = GlanceModifier.width(6.dp))
|
||||||
@@ -443,15 +524,41 @@ private fun ChecklistFullView(
|
|||||||
isLocked: Boolean,
|
isLocked: Boolean,
|
||||||
glanceId: GlanceId
|
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(
|
LazyColumn(
|
||||||
modifier = GlanceModifier
|
modifier = GlanceModifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 8.dp)
|
||||||
) {
|
) {
|
||||||
items(items.size) { index ->
|
items(totalItems) { index ->
|
||||||
val item = items[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) {
|
if (isLocked) {
|
||||||
Row(
|
Row(
|
||||||
@@ -461,7 +568,7 @@ private fun ChecklistFullView(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = if (item.isChecked) "✅" else "☐",
|
text = if (item.isChecked) "☑️" else "☐", // 🆕 v1.8.1 (IMPL_06)
|
||||||
style = TextStyle(fontSize = 16.sp)
|
style = TextStyle(fontSize = 16.sp)
|
||||||
)
|
)
|
||||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ package dev.dettmer.simplenotes.widget
|
|||||||
* 🆕 v1.8.0: Size classification for responsive Note Widget layouts
|
* 🆕 v1.8.0: Size classification for responsive Note Widget layouts
|
||||||
*
|
*
|
||||||
* Determines which layout variant to use based on widget dimensions.
|
* 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 {
|
enum class WidgetSizeClass {
|
||||||
SMALL, // Nur Titel
|
SMALL, // Nur Titel
|
||||||
NARROW_MED, // Schmal, Vorschau
|
NARROW_MED, // Schmal, Vorschau (CompactView)
|
||||||
|
NARROW_SCROLL, // 🆕 v1.8.1: Schmal, scrollbare Liste (150dp+)
|
||||||
NARROW_TALL, // Schmal, voller Inhalt
|
NARROW_TALL, // Schmal, voller Inhalt
|
||||||
WIDE_MED, // Breit, Vorschau
|
WIDE_MED, // Breit, Vorschau (CompactView)
|
||||||
|
WIDE_SCROLL, // 🆕 v1.8.1: Breit, scrollbare Liste (150dp+)
|
||||||
WIDE_TALL // Breit, voller Inhalt
|
WIDE_TALL // Breit, voller Inhalt
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.ui.editor
|
package dev.dettmer.simplenotes.ui.editor
|
||||||
|
|
||||||
|
import dev.dettmer.simplenotes.models.ChecklistSortOption
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
@@ -174,4 +175,204 @@ class ChecklistSortingTest {
|
|||||||
assertEquals(1, sorted[1].order)
|
assertEquals(1, sorted[1].order)
|
||||||
assertEquals(2, sorted[2].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<ChecklistItemState>,
|
||||||
|
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<ChecklistItemState>,
|
||||||
|
sortOption: ChecklistSortOption
|
||||||
|
): List<ChecklistItemState> {
|
||||||
|
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<ChecklistItemState>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
# ⚡ v1.3.1: detekt Configuration
|
# ⚡ v1.8.1: detekt Configuration
|
||||||
# Pragmatic rules for simple-notes-sync
|
# Pragmatic rules for simple-notes-sync
|
||||||
|
|
||||||
build:
|
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
|
excludeCorrectable: false
|
||||||
|
|
||||||
config:
|
config:
|
||||||
|
|||||||
@@ -276,8 +276,9 @@ Schritt-für-Schritt:
|
|||||||
|
|
||||||
### Daten-Schutz
|
### Daten-Schutz
|
||||||
- ✅ **Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion
|
- ✅ **Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion
|
||||||
- ✅ **Keine Verschlüsselung** - Klartextformat für Lesbarkeit
|
- ✅ **Optionale Verschlüsselung** _(v1.7.0+)_ - Backup-Dateien mit Passwort schützen
|
||||||
- ⚠️ **Sensible Daten?** - Backup-Datei selbst verschlüsseln (z.B. 7-Zip mit Passwort)
|
- ✅ **Menschenlesbar** - Klartextformat (JSON) wenn unverschlüsselt
|
||||||
|
- ⚠️ **Sensible Daten?** - Verschlüsselung aktivieren oder externe Tools nutzen (z.B. 7-Zip)
|
||||||
|
|
||||||
### Empfehlungen
|
### Empfehlungen
|
||||||
- 🔐 Backup-Dateien in verschlüsseltem Container speichern
|
- 🔐 Backup-Dateien in verschlüsseltem Container speichern
|
||||||
@@ -321,4 +322,4 @@ Schritt-für-Schritt:
|
|||||||
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
|
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
|
||||||
- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown
|
- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown
|
||||||
|
|
||||||
**Letzte Aktualisierung:** v1.2.1 (2026-01-05)
|
**Letzte Aktualisierung:** v1.8.1 (2026-02-11)
|
||||||
|
|||||||
@@ -276,8 +276,9 @@ Step-by-step:
|
|||||||
|
|
||||||
### Data Protection
|
### Data Protection
|
||||||
- ✅ **Locally stored** - No cloud upload without your action
|
- ✅ **Locally stored** - No cloud upload without your action
|
||||||
- ✅ **No encryption** - Plain text format for readability
|
- ✅ **Optional encryption** _(v1.7.0+)_ - Password-protect backup files
|
||||||
- ⚠️ **Sensitive data?** - Encrypt backup file yourself (e.g., 7-Zip with password)
|
- ✅ **Human-readable** - Plain JSON format when unencrypted
|
||||||
|
- ⚠️ **Sensitive data?** - Enable encryption or use external tools (e.g., 7-Zip)
|
||||||
|
|
||||||
### Recommendations
|
### Recommendations
|
||||||
- 🔐 Store backup files in encrypted container
|
- 🔐 Store backup files in encrypted container
|
||||||
@@ -317,8 +318,8 @@ Step-by-step:
|
|||||||
---
|
---
|
||||||
|
|
||||||
**📚 See also:**
|
**📚 See also:**
|
||||||
- [QUICKSTART.en.md](../QUICKSTART.en.md) - App installation and setup
|
- [QUICKSTART.md](../QUICKSTART.md) - App installation and setup
|
||||||
- [FEATURES.en.md](FEATURES.en.md) - Complete feature list
|
- [FEATURES.md](FEATURES.md) - Complete feature list
|
||||||
- [DESKTOP.en.md](DESKTOP.en.md) - Desktop integration with Markdown
|
- [DESKTOP.md](DESKTOP.md) - Desktop integration with Markdown
|
||||||
|
|
||||||
**Last update:** v1.2.1 (2026-01-05)
|
**Last update:** v1.8.1 (2026-02-11)
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ git push origin fix/my-bug
|
|||||||
|
|
||||||
## 📱 Installation auf Gerät
|
## 📱 Installation auf Gerät
|
||||||
|
|
||||||
## 📱 Installation auf Gerät
|
|
||||||
|
|
||||||
### Mit ADB (Empfohlen - sauberes Testing)
|
### Mit ADB (Empfohlen - sauberes Testing)
|
||||||
```bash
|
```bash
|
||||||
# Gerät verbinden
|
# Gerät verbinden
|
||||||
|
|||||||
@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
|
|||||||
|
|
||||||
## 🔮 Roadmap
|
## 🔮 Roadmap
|
||||||
|
|
||||||
### v1.1
|
Siehe [UPCOMING.md](UPCOMING.md) für die vollständige Roadmap und geplante Features.
|
||||||
- [ ] Suche & Filter
|
|
||||||
- [ ] Dark Mode
|
|
||||||
- [ ] Tags/Kategorien
|
|
||||||
- [ ] Markdown Preview
|
|
||||||
|
|
||||||
### v2.0
|
|
||||||
- [ ] Desktop Client (Flutter)
|
|
||||||
- [ ] End-to-End Verschlüsselung
|
|
||||||
- [ ] Shared Notes (Collaboration)
|
|
||||||
- [ ] Attachment Support
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Letzte Aktualisierung:** 25. Dezember 2025
|
**Letzte Aktualisierung:** Februar 2026
|
||||||
|
|||||||
14
docs/DOCS.md
@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
|
|||||||
|
|
||||||
## 🔮 Roadmap
|
## 🔮 Roadmap
|
||||||
|
|
||||||
### v1.1
|
See [UPCOMING.md](UPCOMING.md) for the full roadmap and planned features.
|
||||||
- [ ] Search & Filter
|
|
||||||
- [ ] Dark Mode
|
|
||||||
- [ ] Tags/Categories
|
|
||||||
- [ ] Markdown Preview
|
|
||||||
|
|
||||||
### v2.0
|
|
||||||
- [ ] Desktop Client (Flutter)
|
|
||||||
- [ ] End-to-End Encryption
|
|
||||||
- [ ] Shared Notes (Collaboration)
|
|
||||||
- [ ] Attachment Support
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Last updated:** December 25, 2025
|
**Last updated:** February 2026
|
||||||
|
|||||||
@@ -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)_
|
## 🌍 Mehrsprachigkeit _(NEU in v1.5.0)_
|
||||||
|
|
||||||
### Unterstützte Sprachen
|
### Unterstützte Sprachen
|
||||||
@@ -129,9 +173,12 @@
|
|||||||
### Sync-Mechanismus
|
### Sync-Mechanismus
|
||||||
- ✅ **Upload** - Lokale Änderungen zum Server
|
- ✅ **Upload** - Lokale Änderungen zum Server
|
||||||
- ✅ **Download** - Server-Änderungen in App
|
- ✅ **Download** - Server-Änderungen in App
|
||||||
|
- ✅ **Parallele Downloads** _(NEU in v1.8.0)_ - Bis zu 5 gleichzeitige Downloads
|
||||||
- ✅ **Konflikt-Erkennung** - Bei gleichzeitigen Änderungen
|
- ✅ **Konflikt-Erkennung** - Bei gleichzeitigen Änderungen
|
||||||
- ✅ **Konfliktfreies Merging** - Last-Write-Wins via Timestamp
|
- ✅ **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
|
- ✅ **Fehlerbehandlung** - Retry bei Netzwerkproblemen
|
||||||
- ✅ **Offline-First** - App funktioniert ohne Server
|
- ✅ **Offline-First** - App funktioniert ohne Server
|
||||||
|
|
||||||
@@ -140,6 +187,9 @@
|
|||||||
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
|
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
|
||||||
- ✅ **Username/Password** - Basic Authentication
|
- ✅ **Username/Password** - Basic Authentication
|
||||||
- ✅ **Connection Test** - In Einstellungen testen
|
- ✅ **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)_
|
- ✅ **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/`
|
- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/`
|
||||||
|
|
||||||
@@ -196,11 +246,12 @@
|
|||||||
## 🛠️ Technische Details
|
## 🛠️ Technische Details
|
||||||
|
|
||||||
### Plattform
|
### Plattform
|
||||||
- ✅ **Android 8.0+** (API 26+)
|
- ✅ **Android 7.0+** (API 24+)
|
||||||
- ✅ **Target SDK 36** (Android 15)
|
- ✅ **Target SDK 36** (Android 15)
|
||||||
- ✅ **Kotlin** - Moderne Programmiersprache
|
- ✅ **Kotlin** - Moderne Programmiersprache
|
||||||
|
- ✅ **Jetpack Compose** - Deklaratives UI-Framework
|
||||||
- ✅ **Material Design 3** - Neueste Design-Richtlinien
|
- ✅ **Material Design 3** - Neueste Design-Richtlinien
|
||||||
- ✅ **ViewBinding** - Typ-sichere View-Referenzen
|
- ✅ **Jetpack Glance** _(v1.8.0)_ - Widget-Framework
|
||||||
|
|
||||||
### Architektur
|
### Architektur
|
||||||
- ✅ **MVVM-Light** - Einfache Architektur
|
- ✅ **MVVM-Light** - Einfache Architektur
|
||||||
@@ -218,6 +269,7 @@
|
|||||||
- ✅ **Gson** - JSON Serialization
|
- ✅ **Gson** - JSON Serialization
|
||||||
- ✅ **WorkManager** - Background Tasks
|
- ✅ **WorkManager** - Background Tasks
|
||||||
- ✅ **OkHttp** - HTTP Client (via Sardine)
|
- ✅ **OkHttp** - HTTP Client (via Sardine)
|
||||||
|
- ✅ **Glance** _(v1.8.0)_ - Widget-Framework
|
||||||
|
|
||||||
### Build-Varianten
|
### Build-Varianten
|
||||||
- ✅ **Standard** - Universal APK (100% FOSS, keine Google-Dependencies)
|
- ✅ **Standard** - Universal APK (100% FOSS, keine Google-Dependencies)
|
||||||
@@ -247,22 +299,12 @@
|
|||||||
|
|
||||||
## 🔮 Zukünftige Features
|
## 🔮 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
|
### v2.0.0 - Legacy Cleanup
|
||||||
- ⏳ **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen
|
- ⏳ **Veraltete Activities entfernen** - Durch Compose-Varianten ersetzen
|
||||||
- ⏳ **Erledigte Items** - Durchstreichen/Abhaken
|
- ⏳ **LocalBroadcastManager → SharedFlow** - Moderne Event-Architektur
|
||||||
- ⏳ **Drag & Drop** - Items neu anordnen
|
- ⏳ **WebDavSyncService aufteilen** - SyncOrchestrator, NoteUploader, NoteDownloader
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)_
|
## 🌍 Multilingual Support _(NEW in v1.5.0)_
|
||||||
|
|
||||||
### Supported Languages
|
### Supported Languages
|
||||||
@@ -129,9 +173,12 @@
|
|||||||
### Sync Mechanism
|
### Sync Mechanism
|
||||||
- ✅ **Upload** - Local changes to server
|
- ✅ **Upload** - Local changes to server
|
||||||
- ✅ **Download** - Server changes to app
|
- ✅ **Download** - Server changes to app
|
||||||
|
- ✅ **Parallel downloads** _(NEW in v1.8.0)_ - Up to 5 simultaneous downloads
|
||||||
- ✅ **Conflict detection** - On simultaneous changes
|
- ✅ **Conflict detection** - On simultaneous changes
|
||||||
- ✅ **Conflict-free merging** - Last-Write-Wins via timestamp
|
- ✅ **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
|
- ✅ **Error handling** - Retry on network issues
|
||||||
- ✅ **Offline-first** - App works without server
|
- ✅ **Offline-first** - App works without server
|
||||||
|
|
||||||
@@ -140,6 +187,9 @@
|
|||||||
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
|
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
|
||||||
- ✅ **Username/password** - Basic authentication
|
- ✅ **Username/password** - Basic authentication
|
||||||
- ✅ **Connection test** - Test in settings
|
- ✅ **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)_
|
- ✅ **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/`
|
- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/`
|
||||||
|
|
||||||
@@ -196,11 +246,12 @@
|
|||||||
## 🛠️ Technical Details
|
## 🛠️ Technical Details
|
||||||
|
|
||||||
### Platform
|
### Platform
|
||||||
- ✅ **Android 8.0+** (API 26+)
|
- ✅ **Android 7.0+** (API 24+)
|
||||||
- ✅ **Target SDK 36** (Android 15)
|
- ✅ **Target SDK 36** (Android 15)
|
||||||
- ✅ **Kotlin** - Modern programming language
|
- ✅ **Kotlin** - Modern programming language
|
||||||
|
- ✅ **Jetpack Compose** - Declarative UI framework
|
||||||
- ✅ **Material Design 3** - Latest design guidelines
|
- ✅ **Material Design 3** - Latest design guidelines
|
||||||
- ✅ **ViewBinding** - Type-safe view references
|
- ✅ **Jetpack Glance** _(v1.8.0)_ - Widget framework
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
- ✅ **MVVM-Light** - Simple architecture
|
- ✅ **MVVM-Light** - Simple architecture
|
||||||
@@ -218,6 +269,7 @@
|
|||||||
- ✅ **Gson** - JSON serialization
|
- ✅ **Gson** - JSON serialization
|
||||||
- ✅ **WorkManager** - Background tasks
|
- ✅ **WorkManager** - Background tasks
|
||||||
- ✅ **OkHttp** - HTTP client (via Sardine)
|
- ✅ **OkHttp** - HTTP client (via Sardine)
|
||||||
|
- ✅ **Glance** _(v1.8.0)_ - Widget framework
|
||||||
|
|
||||||
### Build Variants
|
### Build Variants
|
||||||
- ✅ **Standard** - Universal APK (100% FOSS, no Google dependencies)
|
- ✅ **Standard** - Universal APK (100% FOSS, no Google dependencies)
|
||||||
@@ -247,22 +299,12 @@
|
|||||||
|
|
||||||
## 🔮 Future Features
|
## 🔮 Future Features
|
||||||
|
|
||||||
Planned for upcoming versions:
|
Planned for upcoming versions – see [UPCOMING.md](UPCOMING.md) for the full roadmap.
|
||||||
|
|
||||||
### v1.4.0 - Checklists
|
### v2.0.0 - Legacy Cleanup
|
||||||
- ⏳ **Checklist notes** - New note type with checkboxes
|
- ⏳ **Remove deprecated Activities** - Replace with Compose equivalents
|
||||||
- ⏳ **Completed items** - Strike-through/check off
|
- ⏳ **LocalBroadcastManager → SharedFlow** - Modern event architecture
|
||||||
- ⏳ **Drag & drop** - Reorder items
|
- ⏳ **WebDavSyncService split** - SyncOrchestrator, NoteUploader, NoteDownloader
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
146
docs/SELF_SIGNED_SSL.de.md
Normal file
@@ -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
|
||||||
|
<base-config>
|
||||||
|
<trust-anchors>
|
||||||
|
<certificates src="system" />
|
||||||
|
<certificates src="user" /> <!-- ← Aktiviert Self-Signed Support -->
|
||||||
|
</trust-anchors>
|
||||||
|
</base-config>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
@@ -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`
|
- ✅ **Pinterest-artiges Staggered Grid** - Lückenfreies Layout mit dynamischen Vorschauzeilen
|
||||||
- **Intelligente Größen** - Kleine Notizen (kurzer Text, wenige Checklist-Items) kompakt dargestellt
|
- ✅ **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht wechseln
|
||||||
- **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht in Einstellungen wechseln
|
- ✅ **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
|
||||||
- **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
|
|
||||||
- **120 FPS optimiert** - Lazy Loading für flüssiges Scrollen bei vielen Notizen
|
|
||||||
|
|
||||||
### 🔧 Server-Ordner Prüfung
|
### 📡 Sync-Verbesserungen
|
||||||
|
|
||||||
- **WebDAV Folder Check** - Prüft ob der Ordner auf dem Server existiert und beschreibbar ist
|
- ✅ **WiFi-Only Sync Toggle** - Nur über WiFi synchronisieren
|
||||||
- **Bessere Fehlermeldungen** - Hilfreiche Hinweise bei Server-Problemen
|
- ✅ **VPN-Unterstützung** - Sync funktioniert korrekt über VPN-Tunnels
|
||||||
- **Connection-Test Verbesserung** - Prüft Read/Write Permissions
|
- ✅ **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)
|
## v1.7.1 - Android 9 Fix & VPN ✅
|
||||||
- **Verbesserte Progress-Dialoge** - Material Design 3 konform
|
|
||||||
|
> **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
|
### 🎨 UI Features
|
||||||
|
|
||||||
- **Widget** - Schnellzugriff vom Homescreen
|
|
||||||
- **Kategorien/Tags** - Notizen organisieren
|
- **Kategorien/Tags** - Notizen organisieren
|
||||||
- **Suche** - Volltextsuche in Notizen
|
- **Suche** - Volltextsuche in Notizen
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
- ✅ **Pinterest-style staggered grid** - Gapless layout with dynamic preview lines
|
||||||
- **Smart sizing** - Small notes (short text, few checklist items) displayed compactly
|
- ✅ **Layout toggle** - Switch between list and grid in settings
|
||||||
- **Layout toggle** - Switch between List and Grid view in settings
|
- ✅ **Adaptive columns** - 2-3 columns based on screen size
|
||||||
- **Adaptive columns** - 2-3 columns based on screen size
|
|
||||||
- **120 FPS optimized** - Lazy loading for smooth scrolling with many notes
|
|
||||||
|
|
||||||
### 🔧 Server Folder Check
|
### 📡 Sync Improvements
|
||||||
|
|
||||||
- **WebDAV folder check** - Checks if folder exists and is writable on server
|
- ✅ **WiFi-only sync toggle** - Sync only when connected to WiFi
|
||||||
- **Better error messages** - Helpful hints for server problems
|
- ✅ **VPN support** - Sync works correctly through VPN tunnels
|
||||||
- **Connection test improvement** - Checks read/write permissions
|
- ✅ **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)
|
## v1.7.1 - Android 9 Fix & VPN ✅
|
||||||
- **Improved progress dialogs** - Material Design 3 compliant
|
|
||||||
|
> **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
|
### 🎨 UI Features
|
||||||
|
|
||||||
- **Widget** - Quick access from homescreen
|
|
||||||
- **Categories/Tags** - Organize notes
|
- **Categories/Tags** - Organize notes
|
||||||
- **Search** - Full-text search in notes
|
- **Search** - Full-text search in notes
|
||||||
|
|
||||||
|
|||||||
@@ -5,34 +5,50 @@ Diese Verzeichnisstruktur enthält alle Metadaten für die F-Droid-Veröffentlic
|
|||||||
## Struktur
|
## Struktur
|
||||||
|
|
||||||
```
|
```
|
||||||
fastlane/metadata/android/de-DE/
|
fastlane/metadata/android/
|
||||||
├── title.txt # App-Name (max 50 Zeichen)
|
├── de-DE/ # Deutsche Lokalisierung (primär)
|
||||||
├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
|
│ ├── title.txt # App-Name (max 50 Zeichen)
|
||||||
├── full_description.txt # Vollständige Beschreibung (max 4000 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/
|
├── changelogs/
|
||||||
│ └── 1.txt # Changelog für Version 1
|
│ ├── 1.txt ... 21.txt
|
||||||
└── images/
|
└── images/
|
||||||
└── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
|
└── phoneScreenshots/
|
||||||
├── 1.png # Hauptansicht (Notizliste)
|
|
||||||
├── 2.png # Notiz-Editor
|
|
||||||
├── 3.png # Settings
|
|
||||||
└── 4.png # Empty State
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 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
|
## Screenshots erstellen
|
||||||
|
|
||||||
Verwende einen Android Emulator oder physisches Gerät mit:
|
Verwende ein physisches Gerät oder Emulator mit:
|
||||||
- Material You Theme aktiviert
|
- Material You Theme aktiviert
|
||||||
- Deutsche Sprache
|
- Deutsche/Englische Sprache je nach Locale
|
||||||
- Screenshots in hoher Auflösung (1080x2400 empfohlen)
|
- 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
|
## F-Droid Build-Konfiguration
|
||||||
|
|
||||||
Die App verwendet den `fdroid` Build-Flavor ohne proprietäre Dependencies.
|
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
|
||||||
|
|||||||
@@ -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
|
- Verbesserte Sync-Performance durch optimierten Code
|
||||||
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
|
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
|
||||||
- Speichereffizientere Datenverarbeitung
|
- Speichereffizientere Datenverarbeitung
|
||||||
- Datenschutz-Hinweis fur Datei-Logging hinzugefugt
|
- Datenschutz-Hinweis für Datei-Logging hinzugefügt
|
||||||
|
|||||||
13
fastlane/metadata/android/de-DE/changelogs/21.txt
Normal file
@@ -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
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
v1.2.2 - Rückwärtskompatibilität für v1.2.0 User
|
v1.2.2 - Rückwärtskompatibilität für v1.2.0 User
|
||||||
|
|
||||||
Kritische Fehlerbehebung
|
Kritische Fehlerbehebung
|
||||||
• Server-Wiederherstellung findet jetzt ALLE Notizen (Root + /notes/)
|
• Server-Restore findet jetzt ALLE Notizen (Root + /notes/)
|
||||||
• User die von v1.2.0 upgraden verlieren keine Daten mehr
|
• Upgrade von v1.2.0 ohne Datenverlust
|
||||||
• Alte Notizen aus Root-Ordner werden beim Restore gefunden
|
• Alte Notizen aus Root-Ordner werden gefunden
|
||||||
|
|
||||||
Technische Details
|
Technische Details
|
||||||
• Dual-Mode Download nur bei Server-Restore aktiv
|
• 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
|
• Automatische Deduplication verhindert Duplikate
|
||||||
• Sanfte Migration: Neue Uploads gehen in /notes/, alte bleiben lesbar
|
• Sanfte Migration: Uploads → /notes/, alte bleiben lesbar
|
||||||
|
|||||||
@@ -1,29 +1,31 @@
|
|||||||
Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisation und modernen Features.
|
Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisation und modernen Features.
|
||||||
|
|
||||||
Hauptfunktionen:
|
Hauptfunktionen:
|
||||||
• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop)
|
• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop, Sortierung)
|
||||||
• NEU: Raster-Ansicht (Grid View) für Notizen
|
• Raster- und Listen-Ansicht mit Notizfarben
|
||||||
|
• Homescreen-Widgets (Quick-Note, Checkliste mit interaktiven Checkboxen)
|
||||||
• Multi-Device Sync (Handy, Tablet, Desktop)
|
• Multi-Device Sync (Handy, Tablet, Desktop)
|
||||||
• WebDAV-Synchronisation mit eigenem Server (Nextcloud, ownCloud, etc.)
|
• WebDAV-Synchronisation mit eigenem Server (Nextcloud, ownCloud, etc.)
|
||||||
• Markdown-Export und Import für Desktop-Editoren (Obsidian, VS Code)
|
• 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
|
• Konfigurierbare Sync-Trigger: onSave, onResume, WiFi, periodisch, Boot
|
||||||
• Komplett offline nutzbar
|
• Komplett offline nutzbar – keine Werbung, keine Tracker
|
||||||
• Keine Werbung, keine Tracker
|
|
||||||
|
|
||||||
Datenschutz & Sicherheit:
|
Datenschutz & Sicherheit:
|
||||||
• Alle Daten bleiben bei dir – keine Cloud, keine Tracking-Bibliotheken
|
• Alle Daten bleiben bei dir – keine Cloud, keine Tracking-Bibliotheken
|
||||||
• Unterstützung für selbstsignierte SSL-Zertifikate (Self-signed SSL)
|
• Unterstützung für selbstsignierte SSL-Zertifikate
|
||||||
• SHA-256 Hash des Signaturzertifikats in App und Releases sichtbar
|
• Verschlüsselte lokale Backups
|
||||||
|
|
||||||
Synchronisation:
|
Synchronisation:
|
||||||
• Automatisch oder manuell, optimierte Performance, periodischer Sync optional
|
• Parallele Downloads (bis zu 5 gleichzeitig)
|
||||||
• Intelligente Konfliktlösung, Lösch-Tracking, Batch-Aktionen
|
• Live Sync-Fortschritt mit Phasen-Anzeige
|
||||||
|
• Intelligente Konfliktlösung, Server-Löschungs-Erkennung
|
||||||
|
• Post-Update Changelog-Dialog
|
||||||
|
|
||||||
UI & Design:
|
UI & Design:
|
||||||
• Moderne Jetpack Compose Oberfläche
|
• Moderne Jetpack Compose Oberfläche
|
||||||
• Material Design 3, Dynamic Colors, Dark Mode
|
• Material Design 3, Dynamic Colors, Dark Mode
|
||||||
• Animationen und Live Sync-Status
|
• Notiz- und Checklisten-Sortierung (Titel, Datum, Farbe, alphabetisch)
|
||||||
|
|
||||||
Mehrsprachig:
|
Mehrsprachig:
|
||||||
• Deutsch und Englisch, automatische Erkennung, App-Sprachauswahl
|
• Deutsch und Englisch, automatische Erkennung, App-Sprachauswahl
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 140 KiB |
BIN
fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 199 KiB |
8
fastlane/metadata/android/en-US/changelogs/1.txt
Normal file
@@ -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
|
||||||
5
fastlane/metadata/android/en-US/changelogs/2.txt
Normal file
@@ -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
|
||||||
13
fastlane/metadata/android/en-US/changelogs/21.txt
Normal file
@@ -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
|
||||||
@@ -1,29 +1,31 @@
|
|||||||
Simple Notes Sync is a minimalist note-taking app with WebDAV sync and modern features.
|
Simple Notes Sync is a minimalist note-taking app with WebDAV sync and modern features.
|
||||||
|
|
||||||
Key Features:
|
Key Features:
|
||||||
• Text notes and checklists (tap-to-check, drag & drop)
|
• Text notes and checklists (tap-to-check, drag & drop, sorting)
|
||||||
• NEW: Grid view for notes
|
• Grid and list view with note color support
|
||||||
|
• Homescreen widgets (quick-note, checklist with interactive checkboxes)
|
||||||
• Multi-device sync (phone, tablet, desktop)
|
• Multi-device sync (phone, tablet, desktop)
|
||||||
• WebDAV sync with your own server (Nextcloud, ownCloud, etc.)
|
• WebDAV sync with your own server (Nextcloud, ownCloud, etc.)
|
||||||
• Markdown export/import for desktop editors (Obsidian, VS Code)
|
• 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
|
• Configurable sync triggers: onSave, onResume, WiFi, periodic, boot
|
||||||
• Fully usable offline
|
• Fully usable offline – no ads, no trackers
|
||||||
• No ads, no trackers
|
|
||||||
|
|
||||||
Privacy & Security:
|
Privacy & Security:
|
||||||
• Your data stays with you – no cloud, no tracking libraries
|
• Your data stays with you – no cloud, no tracking libraries
|
||||||
• Support for self-signed SSL certificates
|
• Support for self-signed SSL certificates
|
||||||
• SHA-256 hash of signing certificate shown in app and releases
|
• Encrypted local backups
|
||||||
|
|
||||||
Synchronization:
|
Synchronization:
|
||||||
• Automatic or manual, optimized performance, optional periodic sync
|
• Parallel downloads (up to 5 simultaneous)
|
||||||
• Smart conflict resolution, deletion tracking, batch actions
|
• Live sync progress with phase indicators
|
||||||
|
• Smart conflict resolution, server deletion detection
|
||||||
|
• Post-update changelog dialog
|
||||||
|
|
||||||
UI & Design:
|
UI & Design:
|
||||||
• Modern Jetpack Compose interface
|
• Modern Jetpack Compose interface
|
||||||
• Material Design 3, dynamic colors, dark mode
|
• Material Design 3, dynamic colors, dark mode
|
||||||
• Animations and live sync status
|
• Note & checklist sorting (title, date, color, alphabetical)
|
||||||
|
|
||||||
Multilingual:
|
Multilingual:
|
||||||
• English and German, automatic detection, in-app language selector
|
• English and German, automatic detection, in-app language selector
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 289 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 140 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/6.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 199 KiB |