19 Commits

Author SHA1 Message Date
inventory69
5764e8c0ec Release v1.8.1: Checklist Sorting, Sync Improvements & UX Polish 2026-02-11 22:01:22 +01:00
inventory69
b1e7f1750e chore: update screenshots for fdroid metadata in both de-DE and en-US 2026-02-11 21:54:47 +01:00
inventory69
da371436cd fix(editor): IMPL_15 - Add Item respects separator position in checklist
Changes:
- NoteEditorViewModel.kt: Add calculateInsertIndexForNewItem() helper method
- NoteEditorViewModel.kt: Rewrite addChecklistItemAtEnd() to insert before
  first checked item (MANUAL/UNCHECKED_FIRST) instead of appending at list end
- NoteEditorViewModel.kt: Add cross-boundary guard to addChecklistItemAfter()
  preventing new unchecked items from being inserted inside checked section
- ChecklistSortingTest.kt: Add ChecklistSortOption import
- ChecklistSortingTest.kt: Add 10 IMPL_15 unit tests covering all sort modes,
  edge cases (empty list, all checked, no checked), and position stability

Root cause: addChecklistItemAtEnd() appended new unchecked items at the end
of the flat list, after checked items. The UI splits items by count
(subList(0, uncheckedCount)), not by isChecked state — causing checked items
to appear above the separator and new items below it.

Fix: Insert new items at the semantically correct position per sort mode.
MANUAL/UNCHECKED_FIRST: before first checked item (above separator).
All other modes: at list end (no separator visible, no visual issue).

All 19 unit tests pass (9 existing + 10 new). No UI changes required.
2026-02-11 21:34:18 +01:00
inventory69
cf54f44377 docs(v1.8.1): comprehensive metadata & documentation update for F-Droid release
Complete overhaul of all metadata and documentation for v1.8.1:

F-Droid Critical:
- Fix broken umlauts in de-DE/10.txt changelog (für, hinzugefügt, aufgeräumt)
- Shorten de-DE/7.txt changelog for brevity
- Remove trailing newline from en-US/title.txt

Version & Accuracy:
- Update README badges (Android 7.0+) and footer (v1.8.1)
- Add widgets, sorting, parallel sync to highlights
- Fix APK names in workflows (removed "universal")
- Update cache action v3 → v4 in PR workflow
- Fix CONTRIBUTING.md filename references
- Update QUICKSTART guides (APK name, typo, Android version, docs links)

Feature Documentation:
- Update full_description.txt (both locales) - remove NEW/NEU labels, add v1.8.0+ features
- Major FEATURES.md update - add Views & Layout, Widgets sections, updated tech stack
- Add UPCOMING.md v1.7.0-1.8.1 as released sections
- Update DOCS.md roadmap references and dates to Feb 2026
- Create missing en-US changelogs 1.txt, 2.txt

Supplementary Fixes:
- Update BACKUP.md - add encryption docs, fix cross-references
- Add CHANGELOG.md reference links v1.2.1-v1.8.1 (15 missing)
- Fix DEBUG_APK.md duplicate header
- Rewrite fastlane/README.md with both locales and limits table
- Create SELF_SIGNED_SSL.de.md (full German translation)

Affects: 26 files across READMEs, docs/, fastlane/, workflows
2026-02-11 15:45:51 +01:00
inventory69
bc3f137669 docs: add v1.8.1 changelogs and F-Droid release notes
Changes:
- CHANGELOG.md: Add v1.8.1 section (Bug Fixes, New Features, Improvements, Code Quality)
- CHANGELOG.de.md: Add v1.8.1 section (German translation)
- fastlane/en-US/changelogs/21.txt: F-Droid release notes (EN, 496 chars)
- fastlane/de-DE/changelogs/21.txt: F-Droid release notes (DE, 493 chars)

Changelog covers all 13 branch commits:
- 4 bug fixes (sort persistence, widget scroll, auto-sync toast, drag flicker)
- 4 new features (widget sorting, preview sorting, auto-scroll, cross-boundary drag)
- 3 improvements (sync rate-limiting, toast→banner migration, ProGuard audit)
- 1 code quality (detekt compliance, 0 issues)

F-Droid changelogs stay under 500-char limit.
In-app assets are auto-generated by copyChangelogsToAssets build task.
2026-02-11 11:39:21 +01:00
inventory69
1a6617a4ec chore(quality): resolve all detekt findings for v1.8.1 release
Changes:
- UpdateChangelogSheet.kt: Remove 3 unused imports (Intent, Uri, withStyle)
- NoteEditorScreen.kt: Extract DRAGGING_ELEVATION_DP and AUTO_SCROLL_DELAY_MS constants
- NoteEditorScreen.kt: Add @Suppress("LongParameterList") to DraggableChecklistItem
- NoteEditorViewModel.kt: Refactor loadNote() into loadExistingNote(), loadChecklistData(), initNewNote()
- NoteEditorViewModel.kt: Extract parseSortOption() utility for safe enum parsing
- NoteEditorViewModel.kt: Fix NestedBlockDepth and SwallowedException findings
- ChecklistPreviewHelper.kt: Suppress SwallowedException for intentional fallback
- NoteWidgetActions.kt: Suppress SwallowedException for intentional fallback
- NoteWidgetContent.kt: Suppress SwallowedException in ChecklistCompactView + ChecklistFullView
- SyncWorker.kt: Suppress LongMethod on doWork() (linear flow, debug logging)
- detekt.yml: Update maxIssues from 100 to 0, update version comment

All 12 detekt findings resolved. Build compiles clean, 0 lint errors.
2026-02-11 11:22:10 +01:00
inventory69
27e6b9d4ac refactor(ui): IMPL_12 - Migrate Toast notifications to Banner system
Changes:
- SyncProgress.kt: Add INFO phase to SyncPhase enum
- SyncProgress.kt: Update isVisible to always show INFO phase
- SyncStateManager.kt: Add showInfo() and showError() methods
- SyncProgressBanner.kt: Add INFO icon (Icons.Outlined.Info) + secondaryContainer color
- SyncProgressBanner.kt: Add INFO branch in phaseToString()
- ComposeMainActivity.kt: Add auto-hide for INFO phase (2.5s delay)
- MainViewModel.kt: Replace Toast with Banner in deleteNoteFromServer()
- MainViewModel.kt: Replace Toast with Banner in deleteMultipleNotesFromServer()
- NoteEditorViewModel.kt: Import SyncStateManager
- NoteEditorViewModel.kt: Add Banner feedback (INFO/ERROR) in editor deleteNote()
- NoteEditorViewModel.kt: Remove NOTE_SAVED toast (NavigateBack is sufficient)
- NoteEditorViewModel.kt: Remove NOTE_DELETED toast (already done)

All non-interactive notifications now use the unified Banner system.
Server-delete results show as INFO (success) or ERROR (failure) banners.
Snackbars with Undo actions and NOTE_IS_EMPTY validation toasts remain unchanged.
2026-02-11 11:00:52 +01:00
inventory69
3e4b1bd07e feat(editor): IMPL_05 - Auto-scroll on line wrap in checklist editor
Changes:
- ChecklistItemRow.kt: Import mutableIntStateOf
- ChecklistItemRow.kt: Add onHeightChanged callback parameter
- ChecklistItemRow.kt: Track lastLineCount state for line wrap detection
- ChecklistItemRow.kt: Detect line count increase in onTextLayout and trigger callback
- NoteEditorScreen.kt: Add scrollToItemIndex state in ChecklistEditor
- NoteEditorScreen.kt: Add LaunchedEffect for auto-scroll on height change
- NoteEditorScreen.kt: Pass onHeightChanged callback to ChecklistItemRow

When typing in a checklist item at the bottom of the list, the editor now
automatically scrolls to keep the cursor visible when text wraps to a new line.
2026-02-11 10:42:08 +01:00
inventory69
66d98c0cad feat(widget): IMPL_04 - Add sorting & separator to widget checklists
Changes:
- NoteWidgetContent.kt: Import sortChecklistItemsForPreview and ChecklistSortOption
- NoteWidgetContent.kt: Add WidgetCheckedItemsSeparator composable
- NoteWidgetContent.kt: Apply note.checklistSortOption in ChecklistCompactView
- NoteWidgetContent.kt: Apply note.checklistSortOption in ChecklistFullView
- NoteWidgetContent.kt: Integrate separator for MANUAL/UNCHECKED_FIRST modes
- NoteWidgetContent.kt: Change  to ☑️ emoji (IMPL_06)
- NoteWidgetActions.kt: Add auto-sort after toggle in ToggleChecklistItemAction

Widgets now match editor behavior: apply saved sort option, show separator
between unchecked/checked items, and auto-sort when toggling checkboxes.
2026-02-11 10:32:38 +01:00
inventory69
c72b3fe1c0 fix(widget): IMPL_09 - Fix scroll bug in standard widget size (3x2)
Changes:
- WidgetSizeClass.kt: Add NARROW_SCROLL and WIDE_SCROLL enum values
- NoteWidget.kt: Add SIZE_NARROW_SCROLL (110x150dp) and SIZE_WIDE_SCROLL (250x150dp) breakpoints
- NoteWidget.kt: Update sizeMode to include new breakpoints
- NoteWidgetContent.kt: Add WIDGET_HEIGHT_SCROLL_THRESHOLD (150dp)
- NoteWidgetContent.kt: Update toSizeClass() to classify 150dp+ height as SCROLL sizes
- NoteWidgetContent.kt: Combine NARROW_SCROLL + NARROW_TALL into single when branch
- NoteWidgetContent.kt: Combine WIDE_SCROLL + WIDE_TALL into single when branch
- NoteWidgetContent.kt: Remove clickable modifier from unlocked checklists to enable scrolling
- Locked checklists retain clickable for options, unlocked checklists scroll freely
2026-02-11 10:20:48 +01:00
inventory69
a1a574a725 fix(sync): IMPL_08B - Bypass global cooldown for onSave syncs
Changes:
- Constants.kt: Add SYNC_ONSAVE_TAG constant for worker tagging
- NoteEditorViewModel.triggerOnSaveSync(): Tag SyncWorker with "onsave" tag
- SyncWorker.doWork(): Skip global cooldown check for onSave-tagged workers
- onSave retains 3 protection layers: 5s own throttle, tryStartSync mutex, syncMutex
- Auto/WiFi/periodic syncs still respect 30s global cooldown
2026-02-11 10:15:33 +01:00
inventory69
7dbc06d102 fix(editor): IMPL_03-FIX - Fix sort option not applied when reopening checklist
Changes:
- NoteEditorViewModel.sortChecklistItems(): Use _lastChecklistSortOption.value instead of always sorting unchecked-first
- Supports all sort modes: MANUAL, UNCHECKED_FIRST, CHECKED_FIRST, ALPHABETICAL_ASC/DESC
- reloadFromStorage() also benefits from fix (uses same sortChecklistItems method)
- Root cause: loadNote() correctly restored _lastChecklistSortOption but sortChecklistItems() ignored it
2026-02-11 09:35:25 +01:00
inventory69
2c43b47e96 feat(preview): IMPL_03 - Add checklist sorting in main preview
Changes:
- Note.kt: Add checklistSortOption field (String?, nullable for backward compatibility)
- Note.fromJson(): Parse checklistSortOption from JSON
- Note.fromMarkdown(): Parse sort field from YAML frontmatter
- Note.toMarkdown(): Include sort field in YAML frontmatter for checklists
- ChecklistPreviewHelper.kt: New file with sortChecklistItemsForPreview() and generateChecklistPreview()
- NoteEditorViewModel.loadNote(): Load and restore checklistSortOption from note
- NoteEditorViewModel.saveNote(): Persist checklistSortOption when saving (both new and existing notes)
- NoteCardCompact.kt: Use generateChecklistPreview() for sorted preview (includes IMPL_06 emoji change)
- NoteCardGrid.kt: Use generateChecklistPreview() for sorted preview (includes IMPL_06 emoji change)
2026-02-11 09:30:18 +01:00
inventory69
ffe0e46e3d feat(sync): IMPL_08 - Add global sync rate-limiting & battery protection
Changes:
- Constants.kt: Add KEY_LAST_GLOBAL_SYNC_TIME and MIN_GLOBAL_SYNC_INTERVAL_MS (30s)
- SyncStateManager.kt: Add canSyncGlobally() and markGlobalSyncStarted() methods
- MainViewModel.triggerAutoSync(): Check global cooldown before individual throttle
- MainViewModel.triggerAutoSync(): Mark global sync start before viewModelScope.launch
- MainViewModel.triggerManualSync(): Mark global sync start after tryStartSync()
- SyncWorker.doWork(): Add SyncStateManager.tryStartSync() coordination with silent=true
- SyncWorker.doWork(): Check global cooldown before expensive server checks
- SyncWorker.doWork(): Call markCompleted()/markError() to update SyncStateManager
- WifiSyncReceiver: Add global cooldown check and KEY_SYNC_TRIGGER_WIFI_CONNECT validation
2026-02-11 09:13:06 +01:00
inventory69
fe6935a6b7 fix(sync): IMPL_11 - Remove unexpected toast on auto-sync
- Remove toast emission in triggerAutoSync() (L697)
- Silent sync now fully respects silent=true flag
- Notes list still updates via loadNotes(), no UX loss
- Toast was bypassing SyncStateManager's silent handling

Changes:
- android/app/.../ui/main/MainViewModel.kt: Remove _showToast.emit() on auto-sync success
2026-02-11 09:06:30 +01:00
inventory69
7b558113cf feat(editor): IMPL_14 - Separator drag cross-boundary with auto-toggle
Separator is now its own LazyColumn item instead of being rendered
inline inside the first checked item's composable. This fixes:

Bug A: Separator disappearing during drag (was hidden as workaround
for height inflation). Now always visible with primary color highlight.

Bug B: Cross-boundary move blocked (isChecked != toItem.isChecked
returned early). Now auto-toggles isChecked when crossing boundary
— like Google Tasks.

Bug C: Drag flicker at separator boundary (draggingItemIndex updated
even when onMove was a no-op → oscillation). Index remapping via
visualToDataIndex()/dataToVisualIndex() ensures correct data indices.

Architecture changes:
- DragDropListState: separatorVisualIndex, index remapping functions,
  isAdjacentSkippingSeparator() skips separator in swap detection
- NoteEditorScreen: Extracted DraggableChecklistItem composable,
  3 LazyColumn blocks (unchecked items, separator, checked items),
  removed hardcoded AnimatedVisibility(visible=true) wrapper
- NoteEditorViewModel: moveChecklistItem() allows cross-boundary
  moves with automatic isChecked toggle
- CheckedItemsSeparator: isDragActive parameter for visual feedback

Files changed:
- DragDropListState.kt (+56 lines)
- NoteEditorScreen.kt (refactored, net +84 lines)
- NoteEditorViewModel.kt (simplified cross-boundary logic)
- CheckedItemsSeparator.kt (drag-awareness parameter)
2026-02-11 08:56:48 +01:00
inventory69
24fe32a973 fix(editor): IMPL_13 - Fix gradient regression & drag-flicker
Bug A1: Gradient never showed because maxLines capped lineCount,
making lineCount > maxLines impossible. Fixed by always setting
maxLines = Int.MAX_VALUE and using hasVisualOverflow + heightIn(max)
for clipped overflow detection.

Bug A2: derivedStateOf captured stale val from Detekt refactor
(commit 1da1a63). Replaced with direct computation.

Bug B1: animateItem() on dragged item conflicted with manual offset
(from IMPL_017 commit 900dad7). Fixed with conditional Modifier:
if (!isDragging) Modifier.animateItem() else Modifier.

Bug B2: Item size could change during drag. Added size snapshot
in DragDropListState.onDragStart for stable endOffset calculation.

Files changed:
- ChecklistItemRow.kt: hasVisualOverflow, direct lineCount check,
  maxLines=Int.MAX_VALUE always, heightIn(max=collapsedHeightDp)
- NoteEditorScreen.kt: conditional animateItem on isDragging
- DragDropListState.kt: draggingItemSize snapshot for stable drag
2026-02-11 08:49:13 +01:00
inventory69
b5a3a3c096 Merge branch 'main' into release/v1.8.1 2026-02-11 08:39:21 +01:00
inventory69
63561737e1 feat(proguard): IMPL_07 - Add missing Widget & Compose ProGuard rules
- Add Widget ActionCallback rules to prevent R8 removal (fixes potential widget crashes)
- Add Glance state preservation rules for widget preferences
- Add Compose TextLayout rules to prevent gradient regression in release builds
- Bump version to 1.8.1 (versionCode 21, versionName "1.8.1")

Fixes:
- Widget actions (ToggleChecklistItemAction, etc.) could be stripped by R8
- Glance widget state (DataStore-based) needs reflection protection
- Compose onTextLayout callbacks could be optimized away causing gradient issues

Files changed:
- android/app/proguard-rules.pro: Added v1.8.1 rules section (+17 lines)
- android/app/build.gradle.kts: Version bump 1.8.0 → 1.8.1
2026-02-10 23:10:43 +01:00
68 changed files with 1882 additions and 405 deletions

View File

@@ -61,11 +61,11 @@ jobs:
run: |
mkdir -p apk-output
# Standard Flavor - Universal APK
# Standard Flavor
cp android/app/build/outputs/apk/standard/release/app-standard-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard.apk
# F-Droid Flavor - Universal APK
# F-Droid Flavor
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk

View File

@@ -18,7 +18,7 @@ jobs:
distribution: 'temurin'
java-version: '17'
- name: Gradle Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@@ -69,14 +69,14 @@ jobs:
continue-on-error: true
- name: Build-Ergebnis pruefen
run: |
if [ -f "android/app/build/outputs/apk/standard/debug/app-standard-universal-debug.apk" ]; then
if [ -f "android/app/build/outputs/apk/standard/debug/app-standard-debug.apk" ]; then
echo "✅ Standard Debug APK erfolgreich gebaut"
ls -lh android/app/build/outputs/apk/standard/debug/*.apk
else
echo "❌ Standard Debug APK Build fehlgeschlagen"
exit 1
fi
if [ -f "android/app/build/outputs/apk/fdroid/debug/app-fdroid-universal-debug.apk" ]; then
if [ -f "android/app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk" ]; then
echo "✅ F-Droid Debug APK erfolgreich gebaut"
ls -lh android/app/build/outputs/apk/fdroid/debug/*.apk
else

View File

@@ -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 (110150dp Höhe)
- Neue Größenklassen `NARROW_SCROLL` und `WIDE_SCROLL` mit 150dp-Schwelle
- `clickable`-Modifier bei entsperrten Checklisten entfernt, um Scrollen zu ermöglichen
**Auto-Sync Toast entfernt** ([fe6935a](https://github.com/inventory69/simple-notes-sync/commit/fe6935a))
- Unerwartete Toast-Benachrichtigung bei automatischem Hintergrund-Sync entfernt
- Stiller Auto-Sync bleibt still; nur Fehler werden angezeigt
**Gradient- & Drag-Regression** ([24fe32a](https://github.com/inventory69/simple-notes-sync/commit/24fe32a))
- Gradient-Overlay-Regression bei langen Checklisten-Items behoben
- Drag-and-Drop-Flackern beim Verschieben zwischen Bereichen behoben
### 🆕 Neue Funktionen
**Widget-Checklisten: Sortierung & Trennlinien** ([66d98c0](https://github.com/inventory69/simple-notes-sync/commit/66d98c0))
- Widgets übernehmen die gespeicherte Sortier-Option aus dem Editor
- Visuelle Trennlinie zwischen unerledigten/erledigten Items (MANUAL & UNCHECKED_FIRST)
- Auto-Sortierung beim Abhaken von Checkboxen im Widget
- Emoji-Änderung: ✅ → ☑️ für erledigte Items
**Checklisten-Vorschau-Sortierung** ([2c43b47](https://github.com/inventory69/simple-notes-sync/commit/2c43b47))
- Hauptbildschirm-Vorschau (NoteCard, NoteCardCompact, NoteCardGrid) zeigt gespeicherte Sortierung
- Neuer `ChecklistPreviewHelper` mit geteilter Sortier-Logik
**Auto-Scroll bei Zeilenumbruch** ([3e4b1bd](https://github.com/inventory69/simple-notes-sync/commit/3e4b1bd))
- Checklisten-Editor scrollt automatisch wenn Text in eine neue Zeile umbricht
- Cursor bleibt am unteren Rand sichtbar während der Eingabe
**Separator Drag Cross-Boundary** ([7b55811](https://github.com/inventory69/simple-notes-sync/commit/7b55811))
- Drag-and-Drop funktioniert nun über die Checked/Unchecked-Trennlinie hinweg
- Items wechseln automatisch ihren Status beim Verschieben über die Grenze
- Extrahiertes `DraggableChecklistItem`-Composable für Wiederverwendbarkeit
### 🔄 Verbesserungen
**Sync-Ratenlimit & Akkuschutz** ([ffe0e46](https://github.com/inventory69/simple-notes-sync/commit/ffe0e46), [a1a574a](https://github.com/inventory69/simple-notes-sync/commit/a1a574a))
- Globaler 30-Sekunden-Cooldown zwischen Sync-Operationen (Auto/WiFi/Periodisch)
- onSave-Syncs umgehen den globalen Cooldown (behalten eigenen 5s-Throttle)
- Neuer `SyncStateManager`-Singleton für zentrales State-Tracking
- Verhindert Akkuverbrauch durch schnelle aufeinanderfolgende Syncs
**Toast → Banner-Migration** ([27e6b9d](https://github.com/inventory69/simple-notes-sync/commit/27e6b9d))
- Alle nicht-interaktiven Benachrichtigungen auf einheitliches Banner-System migriert
- Server-Lösch-Ergebnisse als INFO/ERROR-Banner angezeigt
- INFO-Phase zu SyncPhase-Enum mit Auto-Hide (2,5s) hinzugefügt
- Snackbars mit Undo-Aktionen bleiben unverändert
**ProGuard-Regeln Audit** ([6356173](https://github.com/inventory69/simple-notes-sync/commit/6356173))
- Fehlende Keep-Regeln für Widget-ActionCallback-Klassen hinzugefügt
- Compose-spezifische ProGuard-Regeln hinzugefügt
- Verhindert ClassNotFoundException in Release-Builds
### 🧹 Code-Qualität
**Detekt-Compliance** ([1a6617a](https://github.com/inventory69/simple-notes-sync/commit/1a6617a))
- Alle 12 Detekt-Findings behoben (0 Issues verbleibend)
- `NoteEditorViewModel.loadNote()` refactored um Verschachtelungstiefe zu reduzieren
- Konstanten für Magic Numbers im Editor extrahiert
- Unbenutzte Imports aus `UpdateChangelogSheet` entfernt
- `maxIssues: 0` in Detekt-Konfiguration gesetzt
---
## [1.8.0] - 2026-02-10
### 🚨 CRITICAL BUGFIX (Tag neu erstellt)
@@ -891,6 +968,21 @@ Das komplette UI wurde von XML-Views auf Jetpack Compose migriert. Die App ist j
---
[1.8.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.1
[1.8.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.0
[1.7.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.2
[1.7.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.1
[1.7.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.0
[1.6.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.1
[1.6.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.0
[1.5.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.5.0
[1.4.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.1
[1.4.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.0
[1.3.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.2
[1.3.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.1
[1.3.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.0
[1.2.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.2
[1.2.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.1
[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0
[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1

View File

@@ -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 (110150dp height)
- Added `NARROW_SCROLL` and `WIDE_SCROLL` size classes with 150dp threshold
- Removed `clickable` modifier from unlocked checklists to enable scrolling
**Auto-Sync Toast Removed** ([fe6935a](https://github.com/inventory69/simple-notes-sync/commit/fe6935a))
- Removed unexpected toast notification on automatic background sync
- Silent auto-sync stays silent; only errors are shown
**Gradient & Drag Regression** ([24fe32a](https://github.com/inventory69/simple-notes-sync/commit/24fe32a))
- Fixed gradient overlay regression on long checklist items
- Fixed drag-and-drop flicker when moving items between boundaries
### 🆕 New Features
**Widget Checklist Sorting & Separators** ([66d98c0](https://github.com/inventory69/simple-notes-sync/commit/66d98c0))
- Widgets now apply saved sort option from the editor
- Visual separator between unchecked/checked items (MANUAL & UNCHECKED_FIRST modes)
- Auto-sort when toggling checkboxes in the widget
- Changed ✅ → ☑️ emoji for checked items
**Checklist Preview Sorting** ([2c43b47](https://github.com/inventory69/simple-notes-sync/commit/2c43b47))
- Main screen preview (NoteCard, NoteCardCompact, NoteCardGrid) now respects saved sort option
- New `ChecklistPreviewHelper` with shared sorting logic
**Auto-Scroll on Line Wrap** ([3e4b1bd](https://github.com/inventory69/simple-notes-sync/commit/3e4b1bd))
- Checklist editor auto-scrolls when typing causes text to wrap to a new line
- Keeps cursor visible at bottom of list during editing
**Separator Drag Cross-Boundary** ([7b55811](https://github.com/inventory69/simple-notes-sync/commit/7b55811))
- Drag-and-drop now works across the checked/unchecked separator
- Items auto-toggle their checked state when dragged across boundaries
- Extracted `DraggableChecklistItem` composable for reusability
### 🔄 Improvements
**Sync Rate-Limiting & Battery Protection** ([ffe0e46](https://github.com/inventory69/simple-notes-sync/commit/ffe0e46), [a1a574a](https://github.com/inventory69/simple-notes-sync/commit/a1a574a))
- Global 30-second cooldown between sync operations (auto/WiFi/periodic)
- onSave syncs bypass global cooldown (retain own 5s throttle)
- New `SyncStateManager` singleton for centralized state tracking
- Prevents battery drain from rapid successive syncs
**Toast → Banner Migration** ([27e6b9d](https://github.com/inventory69/simple-notes-sync/commit/27e6b9d))
- All non-interactive notifications migrated to unified Banner system
- Server-delete results show as INFO/ERROR banners
- Added INFO phase to SyncPhase enum with auto-hide (2.5s)
- Snackbars with Undo actions remain unchanged
**ProGuard Rules Audit** ([6356173](https://github.com/inventory69/simple-notes-sync/commit/6356173))
- Added missing keep rules for Widget ActionCallback classes
- Added Compose-specific ProGuard rules
- Prevents ClassNotFoundException in release builds
### 🧹 Code Quality
**Detekt Compliance** ([1a6617a](https://github.com/inventory69/simple-notes-sync/commit/1a6617a))
- Resolved all 12 detekt findings (0 issues remaining)
- Refactored `NoteEditorViewModel.loadNote()` to reduce nesting depth
- Extracted constants for magic numbers in editor
- Removed unused imports from `UpdateChangelogSheet`
- Set `maxIssues: 0` in detekt config
---
## [1.8.0] - 2026-02-10
### 🚨 CRITICAL BUGFIX (Tag recreated)
@@ -890,6 +967,21 @@ The complete UI has been migrated from XML Views to Jetpack Compose. The app is
---
[1.8.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.1
[1.8.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.8.0
[1.7.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.2
[1.7.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.1
[1.7.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.7.0
[1.6.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.1
[1.6.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.6.0
[1.5.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.5.0
[1.4.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.1
[1.4.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.4.0
[1.3.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.2
[1.3.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.1
[1.3.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.3.0
[1.2.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.2
[1.2.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.1
[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0
[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1

View File

@@ -94,10 +94,10 @@ Nutze die [Feature Request Template](https://github.com/inventory69/simple-notes
Dokumentations-Verbesserungen sind auch Contributions!
**Dateien:**
- `README.md` / `README.en.md` - Übersicht
- `QUICKSTART.md` / `QUICKSTART.en.md` - Schritt-für-Schritt Anleitung
- `DOCS.md` / `DOCS.en.md` - Technische Details
- `server/README.md` / `server/README.en.md` - Server Setup
- `README.de.md` / `README.md` - Übersicht
- `QUICKSTART.de.md` / `QUICKSTART.md` - Schritt-für-Schritt Anleitung
- `docs/DOCS.de.md` / `docs/DOCS.md` - Technische Details
- `server/README.de.md` / `server/README.md` - Server Setup
**Bitte:** Halte beide Sprachen (DE/EN) synchron!
@@ -219,10 +219,10 @@ Use the [Feature Request Template](https://github.com/inventory69/simple-notes-s
Documentation improvements are also contributions!
**Files:**
- `README.md` / `README.en.md` - Overview
- `QUICKSTART.md` / `QUICKSTART.en.md` - Step-by-step guide
- `DOCS.md` / `DOCS.en.md` - Technical details
- `server/README.md` / `server/README.en.md` - Server setup
- `README.de.md` / `README.md` - Overview
- `QUICKSTART.de.md` / `QUICKSTART.md` - Step-by-step guide
- `docs/DOCS.de.md` / `docs/DOCS.md` - Technical details
- `server/README.de.md` / `server/README.md` - Server setup
**Please:** Keep both languages (DE/EN) in sync!
@@ -260,4 +260,4 @@ By contributing, you agree that your code will be published under the [MIT Licen
Öffne ein [Issue](https://github.com/inventory69/simple-notes-sync/issues) oder nutze die [Question Template](https://github.com/inventory69/simple-notes-sync/issues/new/choose).
**Frohe Weihnachten & Happy Coding! 🎄**
**Happy Coding! 🚀**

View File

@@ -8,7 +8,7 @@
## Voraussetzungen
- ✅ Android 8.0+ Smartphone/Tablet
- ✅ Android 7.0+ Smartphone/Tablet
- ✅ WLAN-Verbindung
- ✅ Eigener Server mit Docker (optional - für Self-Hosting)
@@ -52,7 +52,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
### Schritt 2: App installieren
1. **APK herunterladen:** [Neueste Version](https://github.com/inventory69/simple-notes-sync/releases/latest)
- Wähle: `simple-notes-sync-vX.X.X-standard-universal.apk`
- Wähle: `simple-notes-sync-vX.X.X-standard.apk`
2. **Installation erlauben:**
- Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren
@@ -261,7 +261,7 @@ Für zuverlässigen Auto-Sync:
## 🆘 Weitere Hilfe
- **GitHub Issues:** [Problem melden](https://github.com/inventory69/simple-notes-sync/issues)
- **Vollständige Docs:** [DOCS.md](DOCS.md)
- **Vollständige Docs:** [DOCS.md](docs/DOCS.md)
- **Server Setup Details:** [server/README.md](server/README.md)
---

View File

@@ -8,7 +8,7 @@
## Prerequisites
- ✅ Android 8.0+ smartphone/tablet
- ✅ Android 7.0+ smartphone/tablet
- ✅ WiFi connection
- ✅ Own server with Docker (optional - for self-hosting)
@@ -52,7 +52,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
### Step 2: Install App
1. **Download APK:** [Latest version](https://github.com/inventory69/simple-notes-sync/releases/latest)
- Choose: `simple-notes-sync-vX.X.X-standard-universal.apk`
- Choose: `simple-notes-sync-vX.X.X-standard.apk`
2. **Allow installation:**
- Android: Settings → Security → Enable "Unknown sources" for your browser
@@ -77,7 +77,7 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
> **💡 Note:** Enter only the base URL (without `/notes`). The app automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown export.
4. **Press "Test connection"****
4. **Press "Test connection"**
- ✅ Success? → Continue to step 4
- ❌ Error? → See [Troubleshooting](#troubleshooting)
@@ -261,8 +261,8 @@ For reliable auto-sync:
## 🆘 Further Help
- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues)
- **Complete docs:** [DOCS.en.md](DOCS.en.md)
- **Server setup details:** [server/README.en.md](server/README.en.md)
- **Complete docs:** [DOCS.md](docs/DOCS.md)
- **Server setup details:** [server/README.md](server/README.md)
---

View File

@@ -8,7 +8,7 @@
<div align="center">
[![Android](https://img.shields.io/badge/Android-8.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/)
[![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/)
[![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/)
[![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=jetpackcompose&logoColor=white)](https://developer.android.com/compose/)
[![Material 3](https://img.shields.io/badge/Material_3-6750A4?style=for-the-badge&logo=material-design&logoColor=white)](https://m3.material.io/)
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" /></a>
- 📝 **Offline-first** Funktioniert ohne Internet
- 📊 **Flexible Ansichten** Listen- und Grid-Layout
-**Checklisten** Tap-to-Check, Drag & Drop
- 🌍 **Mehrsprachig** Deutsch/Englisch mit Sprachauswahl
- 🔄 **Konfigurierbare Sync-Trigger** onSave, onResume, WiFi, periodisch (15/30/60 Min), Boot
- 📌 **Widgets** Home-Screen Quick-Note und Notizlisten-Widget
- 🔀 **Smartes Sortieren** Nach Titel, Änderungsdatum, Erstelldatum, Farbe
-**Paralleler Sync** Lädt bis zu 5 Notizen gleichzeitig herunter
- 🌍 **Mehrsprachig** Deutsch/Englisch mit Sprachauswahl
- 🔒 **Self-hosted** Deine Daten bleiben bei dir (WebDAV)
- 💾 **Lokales Backup** Export/Import als JSON-Datei (optional verschlüsselt)
- 🖥️ **Desktop-Integration** Markdown-Export für Obsidian, VS Code, Typora
- 🔋 **Akkuschonend** ~0.2% mit Defaults, bis zu ~1.0% mit Periodic Sync
- 🎨 **Material Design 3** Dynamischer Dark/Light Mode & Farben
➡️ **Vollständige Feature-Liste:** [docs/FEATURES.de.md](docs/FEATURES.de.md)
@@ -138,6 +140,6 @@ MIT License siehe [LICENSE](LICENSE)
<div align="center">
<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>

View File

@@ -8,7 +8,7 @@
<div align="center">
[![Android](https://img.shields.io/badge/Android-8.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/)
[![Android](https://img.shields.io/badge/Android-7.0%2B-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://www.android.com/)
[![Kotlin](https://img.shields.io/badge/Kotlin-7F52FF?style=for-the-badge&logo=kotlin&logoColor=white)](https://kotlinlang.org/)
[![Jetpack Compose](https://img.shields.io/badge/Jetpack%20Compose-4285F4?style=for-the-badge&logo=jetpackcompose&logoColor=white)](https://developer.android.com/compose/)
[![Material 3](https://img.shields.io/badge/Material_3-6750A4?style=for-the-badge&logo=material-design&logoColor=white)](https://m3.material.io/)
@@ -66,12 +66,14 @@ alt="Get it on F-Droid" align="center" height="80" /></a>
- 📝 **Offline-first** - Works without internet
- 📊 **Flexible views** - Switch between list and grid layout
-**Checklists** - Tap-to-check, drag & drop
- 🌍 **Multilingual** - English/German with language selector
- 🔄 **Configurable sync triggers** - onSave, onResume, WiFi-connect, periodic (15/30/60 min), boot
- 📌 **Widgets** - Home screen quick-note and note list widgets
- 🔀 **Smart sorting** - By title, date modified, date created, color
-**Parallel sync** - Downloads up to 5 notes simultaneously
- 🌍 **Multilingual** - English/German with language selector
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
- 💾 **Local backup** - Export/Import as JSON file (encryption available)
- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
- 🔋 **Battery-friendly** - ~0.2% with defaults, up to ~1.0% with periodic sync
- 🎨 **Material Design 3** - Dynamic dark/light mode & colors based on system settings
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
@@ -148,6 +150,6 @@ MIT License - see [LICENSE](LICENSE)
<div align="center">
<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>

View File

@@ -20,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
versionCode = 20 // 🎉 v1.8.0: Widgets, Sorting, UI Polish, Post-Update Changelog
versionName = "1.8.0" // 🎉 v1.8.0: Major Feature Release
versionCode = 21 // 🐛 v1.8.1: Checklist Fixes, Widget Sorting, ProGuard Audit
versionName = "1.8.1" // 🐛 v1.8.1: Bugfix & Polish Release
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -77,3 +77,23 @@
# v1.7.1: Suppress TextInclusionStrategy warnings on older Android versions
# This class only exists on API 35+ but Compose handles the fallback gracefully
-dontwarn android.text.Layout$TextInclusionStrategy
# ═══════════════════════════════════════════════════════════════════════
# v1.8.1: Widget & Compose Fixes
# ═══════════════════════════════════════════════════════════════════════
# Glance Widget ActionCallbacks (instanziiert via Reflection durch actionRunCallback<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 { *; }

View File

@@ -24,7 +24,9 @@ data class Note(
val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,
// v1.4.0: Checklisten-Felder
val noteType: NoteType = NoteType.TEXT,
val checklistItems: List<ChecklistItem>? = null
val checklistItems: List<ChecklistItem>? = null,
// 🆕 v1.8.1 (IMPL_03): Persistierte Sortierung
val checklistSortOption: String? = null
) {
/**
* Serialisiert Note zu JSON
@@ -71,13 +73,20 @@ data class Note(
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
*/
fun toMarkdown(): String {
// 🆕 v1.8.1 (IMPL_03): Sortierung im Frontmatter
val sortLine = if (noteType == NoteType.CHECKLIST && checklistSortOption != null) {
"\nsort: $checklistSortOption"
} else {
""
}
val header = """
---
id: $id
created: ${formatISO8601(createdAt)}
updated: ${formatISO8601(updatedAt)}
device: $deviceId
type: ${noteType.name.lowercase()}
type: ${noteType.name.lowercase()}$sortLine
---
# $title
@@ -119,6 +128,14 @@ type: ${noteType.name.lowercase()}
NoteType.TEXT
}
// 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung laden
val checklistSortOption = if (jsonObject.has("checklistSortOption") &&
!jsonObject.get("checklistSortOption").isJsonNull) {
jsonObject.get("checklistSortOption").asString
} else {
null
}
// Parsen der Basis-Note
val rawNote = gson.fromJson(json, NoteRaw::class.java)
@@ -158,7 +175,8 @@ type: ${noteType.name.lowercase()}
deviceId = rawNote.deviceId,
syncStatus = rawNote.syncStatus ?: SyncStatus.LOCAL_ONLY,
noteType = noteType,
checklistItems = checklistItems
checklistItems = checklistItems,
checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
@@ -246,6 +264,9 @@ type: ${noteType.name.lowercase()}
else -> NoteType.TEXT
}
// 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung aus YAML laden
val checklistSortOption = metadata["sort"]
// v1.4.0: Parse Content basierend auf Typ
// FIX: Robusteres Parsing - suche nach dem Titel-Header und extrahiere den Rest
val titleLineIndex = contentBlock.lines().indexOfFirst { it.startsWith("# ") }
@@ -300,7 +321,8 @@ type: ${noteType.name.lowercase()}
deviceId = metadata["device"] ?: "desktop",
syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
noteType = noteType,
checklistItems = checklistItems
checklistItems = checklistItems,
checklistSortOption = checklistSortOption // 🆕 v1.8.1 (IMPL_03)
)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")

View File

@@ -52,9 +52,10 @@ data class SyncProgress(
/**
* Ob das Banner sichtbar sein soll
* Silent syncs zeigen nie ein Banner
* 🆕 v1.8.1 (IMPL_12): INFO ist immer sichtbar (nicht vom silent-Flag betroffen)
*/
val isVisible: Boolean
get() = !silent && phase != SyncPhase.IDLE
get() = phase == SyncPhase.INFO || (!silent && phase != SyncPhase.IDLE)
/**
* Ob gerade ein aktiver Sync läuft (mit Spinner)
@@ -95,5 +96,8 @@ enum class SyncPhase {
COMPLETED,
/** Sync mit Fehler abgebrochen */
ERROR
ERROR,
/** 🆕 v1.8.1 (IMPL_12): Kurzfristige Info-Meldung (nicht sync-bezogen) */
INFO
}

View File

@@ -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")
}
}
}

View File

@@ -73,6 +73,7 @@ class SyncWorker(
}
}
@Suppress("LongMethod") // Linear sync flow with debug logging — splitting would hurt readability
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "═══════════════════════════════════════")
@@ -104,7 +105,42 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 2: Checking for unsynced changes (Performance Pre-Check)")
Logger.d(TAG, "📍 Step 2: SyncStateManager coordination & global cooldown (v1.8.1)")
}
// 🆕 v1.8.1 (IMPL_08): SyncStateManager-Koordination
// Verhindert dass Foreground und Background gleichzeitig syncing-State haben
val prefs = applicationContext.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
// 🆕 v1.8.1 (IMPL_08B): onSave-Syncs bypassen den globalen Cooldown
// Grund: User hat explizit gespeichert → erwartet zeitnahen Sync
// Der eigene 5s-Throttle + isSyncing-Mutex reichen als Schutz
val isOnSaveSync = tags.contains(Constants.SYNC_ONSAVE_TAG)
// Globaler Cooldown-Check (nicht für onSave-Syncs)
if (!isOnSaveSync && !SyncStateManager.canSyncGlobally(prefs)) {
Logger.d(TAG, "⏭️ SyncWorker: Global sync cooldown active - skipping")
if (BuildConfig.DEBUG) {
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (cooldown)")
Logger.d(TAG, "═══════════════════════════════════════")
}
return@withContext Result.success()
}
if (!SyncStateManager.tryStartSync("worker-${tags.firstOrNull() ?: "unknown"}", silent = true)) {
Logger.d(TAG, "⏭️ SyncWorker: Another sync already in progress - skipping")
if (BuildConfig.DEBUG) {
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS (already syncing)")
Logger.d(TAG, "═══════════════════════════════════════")
}
return@withContext Result.success()
}
// Globalen Cooldown markieren
SyncStateManager.markGlobalSyncStarted(prefs)
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 3: Checking for unsynced changes (Performance Pre-Check)")
}
// 🔥 v1.1.2: Performance-Optimierung - Skip Sync wenn keine lokalen Änderungen
@@ -122,7 +158,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 2.5: Checking sync gate (canSync)")
Logger.d(TAG, "📍 Step 4: Checking sync gate (canSync)")
}
// 🆕 v1.7.0: Zentrale Sync-Gate Prüfung (WiFi-Only, Offline Mode, Server Config)
@@ -143,7 +179,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 3: Checking server reachability (Pre-Check)")
Logger.d(TAG, "📍 Step 5: Checking server reachability (Pre-Check)")
}
// ⭐ KRITISCH: Server-Erreichbarkeits-Check VOR Sync
@@ -167,7 +203,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 3: Server reachable - proceeding with sync")
Logger.d(TAG, "📍 Step 6: Server reachable - proceeding with sync")
Logger.d(TAG, " SyncService: $syncService")
}
@@ -188,7 +224,7 @@ class SyncWorker(
}
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Processing result")
Logger.d(TAG, "📍 Step 7: Processing result")
Logger.d(
TAG,
"📦 Sync result: success=${result.isSuccess}, " +
@@ -198,10 +234,13 @@ class SyncWorker(
if (result.isSuccess) {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 5: Success path")
Logger.d(TAG, "📍 Step 8: Success path")
}
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
// 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
SyncStateManager.markCompleted()
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
// UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
if (result.syncedCount > 0) {
@@ -248,9 +287,13 @@ class SyncWorker(
Result.success()
} else {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 5: Failure path")
Logger.d(TAG, "📍 Step 8: Failure path")
}
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
// 🆕 v1.8.1 (IMPL_08): SyncStateManager aktualisieren
SyncStateManager.markError(result.errorMessage)
NotificationHelper.showSyncError(
applicationContext,
result.errorMessage ?: "Unbekannter Fehler"

View File

@@ -27,6 +27,16 @@ class WifiSyncReceiver : BroadcastReceiver() {
return
}
// 🆕 v1.8.1 (IMPL_08): Globaler Cooldown (verhindert Doppel-Trigger mit NetworkMonitor)
if (!SyncStateManager.canSyncGlobally(prefs)) {
return
}
// 🆕 v1.8.1 (IMPL_08): Auch KEY_SYNC_TRIGGER_WIFI_CONNECT prüfen (Konsistenz mit NetworkMonitor)
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, Constants.DEFAULT_TRIGGER_WIFI_CONNECT)) {
return
}
// Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
if (isConnectedToWifi(context)) {
scheduleSyncWork(context)

View File

@@ -25,6 +25,7 @@ import kotlinx.coroutines.launch
* v1.5.0: NoteEditor Redesign
* v1.8.0: IMPL_023 - Drag & Drop Fix (pointerInput key + Handle-only drag)
* v1.8.0: IMPL_023b - Flicker-Fix (Straddle-Target-Center-Erkennung statt Mittelpunkt)
* v1.8.1: IMPL_14 - Separator als eigenes Item, Cross-Boundary-Drag mit Auto-Toggle
*/
class DragDropListState(
private val state: LazyListState,
@@ -36,8 +37,14 @@ class DragDropListState(
private var draggingItemDraggedDelta by mutableFloatStateOf(0f)
private var draggingItemInitialOffset by mutableFloatStateOf(0f)
// 🆕 v1.8.1: Item-Größe beim Drag-Start fixieren
// Verhindert dass Höhenänderungen die Swap-Erkennung destabilisieren
private var draggingItemSize by mutableStateOf(0)
private var overscrollJob by mutableStateOf<Job?>(null)
// 🆕 v1.8.1 IMPL_14: Visual-Index des Separators (-1 = kein Separator)
var separatorVisualIndex by mutableStateOf(-1)
val draggingItemOffset: Float
get() = draggingItemLayoutInfo?.let { item ->
draggingItemInitialOffset + draggingItemDraggedDelta - item.offset
@@ -47,9 +54,28 @@ class DragDropListState(
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggingItemIndex }
/**
* 🆕 v1.8.1 IMPL_14: Visual-Index → Data-Index Konvertierung.
* Wenn ein Separator existiert, sind alle Items nach dem Separator um 1 verschoben.
*/
fun visualToDataIndex(visualIndex: Int): Int {
if (separatorVisualIndex < 0) return visualIndex
return if (visualIndex > separatorVisualIndex) visualIndex - 1 else visualIndex
}
/**
* 🆕 v1.8.1 IMPL_14: Data-Index → Visual-Index Konvertierung.
*/
fun dataToVisualIndex(dataIndex: Int): Int {
if (separatorVisualIndex < 0) return dataIndex
return if (dataIndex >= separatorVisualIndex) dataIndex + 1 else dataIndex
}
fun onDragStart(offset: Offset, itemIndex: Int) {
draggingItemIndex = itemIndex
draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f
val info = draggingItemLayoutInfo
draggingItemInitialOffset = info?.offset?.toFloat() ?: 0f
draggingItemSize = info?.size ?: 0
draggingItemDraggedDelta = 0f
}
@@ -57,6 +83,7 @@ class DragDropListState(
draggingItemDraggedDelta = 0f
draggingItemIndex = null
draggingItemInitialOffset = 0f
draggingItemSize = 0
overscrollJob?.cancel()
}
@@ -65,15 +92,19 @@ class DragDropListState(
val draggingItem = draggingItemLayoutInfo ?: return
val startOffset = draggingItem.offset + draggingItemOffset
val endOffset = startOffset + draggingItem.size
// 🆕 v1.8.1: Fixierte Item-Größe für stabile Swap-Erkennung
val endOffset = startOffset + draggingItemSize
// 🆕 v1.8.0: IMPL_023b — Straddle-Target-Center + Adjazenz-Filter
// Statt den Mittelpunkt des gezogenen Items zu prüfen ("liegt mein Zentrum im Target?"),
// wird geprüft ob das gezogene Item den MITTELPUNKT des Targets überspannt.
// Dies verhindert Oszillation bei Items unterschiedlicher Größe.
// Zusätzlich: Nur adjazente Items (Index ± 1) als Swap-Kandidaten.
// 🆕 v1.8.1 IMPL_14: Separator überspringen, Adjazenz berücksichtigt Separator-Lücke
val targetItem = state.layoutInfo.visibleItemsInfo.firstOrNull { item ->
(item.index == draggingItem.index - 1 || item.index == draggingItem.index + 1) &&
// Separator überspringen
item.index != separatorVisualIndex &&
// Nur adjazente Items (Separator-Lücke wird übersprungen)
isAdjacentSkippingSeparator(draggingItem.index, item.index) &&
run {
val targetCenter = item.offset + item.size / 2
startOffset < targetCenter && endOffset > targetCenter
@@ -89,15 +120,19 @@ class DragDropListState(
null
}
// 🆕 v1.8.1 IMPL_14: Visual-Indizes zu Data-Indizes konvertieren für onMove
val fromDataIndex = visualToDataIndex(draggingItem.index)
val toDataIndex = visualToDataIndex(targetItem.index)
if (scrollToIndex != null) {
scope.launch {
state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
onMove(draggingItem.index, targetItem.index)
onMove(fromDataIndex, toDataIndex)
// 🆕 v1.8.0: IMPL_023b — Index-Update NACH dem Move (verhindert Race-Condition)
draggingItemIndex = targetItem.index
}
} else {
onMove(draggingItem.index, targetItem.index)
onMove(fromDataIndex, toDataIndex)
draggingItemIndex = targetItem.index
}
} else {
@@ -121,6 +156,26 @@ class DragDropListState(
}
}
/**
* 🆕 v1.8.1 IMPL_14: Prüft ob zwei Visual-Indizes adjazent sind,
* wobei der Separator übersprungen wird.
* Beispiel: Items bei Visual 1 und Visual 3 sind adjazent wenn Separator bei Visual 2 liegt.
*/
private fun isAdjacentSkippingSeparator(indexA: Int, indexB: Int): Boolean {
val diff = kotlin.math.abs(indexA - indexB)
if (diff == 1) {
// Direkt benachbart — aber NICHT wenn der Separator dazwischen liegt
val between = minOf(indexA, indexB) + 1
return between != separatorVisualIndex || separatorVisualIndex < 0
}
if (diff == 2 && separatorVisualIndex >= 0) {
// 2 Positionen entfernt — adjazent wenn Separator dazwischen
val between = minOf(indexA, indexB) + 1
return between == separatorVisualIndex
}
return false
}
@Suppress("UnusedPrivateProperty")
private val LazyListItemInfo.offsetEnd: Int
get() = this.offset + this.size

View File

@@ -1,11 +1,6 @@
package dev.dettmer.simplenotes.ui.editor
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -19,6 +14,7 @@ import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -70,8 +66,10 @@ import dev.dettmer.simplenotes.utils.showToast
import kotlin.math.roundToInt
private const val LAYOUT_DELAY_MS = 100L
private const val AUTO_SCROLL_DELAY_MS = 50L
private const val ITEM_CORNER_RADIUS_DP = 8
private const val DRAGGING_ITEM_Z_INDEX = 10f
private val DRAGGING_ELEVATION_DP = 8.dp
/**
* Main Composable for the Note Editor screen.
@@ -327,6 +325,66 @@ private fun TextNoteContent(
)
}
/**
* 🆕 v1.8.1 IMPL_14: Extrahiertes Composable für ein einzelnes draggbares Checklist-Item.
* Entkoppelt von der Separator-Logik — wiederverwendbar für unchecked und checked Items.
*/
@Suppress("LongParameterList") // Compose callbacks — cannot be reduced without wrapper class
@Composable
private fun LazyItemScope.DraggableChecklistItem(
item: ChecklistItemState,
visualIndex: Int,
dragDropState: DragDropListState,
focusNewItemId: String?,
onTextChange: (String, String) -> Unit,
onCheckedChange: (String, Boolean) -> Unit,
onDelete: (String) -> Unit,
onAddNewItemAfter: (String) -> Unit,
onFocusHandled: () -> Unit,
onHeightChanged: () -> Unit, // 🆕 v1.8.1 (IMPL_05)
) {
val isDragging = dragDropState.draggingItemIndex == visualIndex
val elevation by animateDpAsState(
targetValue = if (isDragging) DRAGGING_ELEVATION_DP else 0.dp,
label = "elevation"
)
val shouldFocus = item.id == focusNewItemId
LaunchedEffect(shouldFocus) {
if (shouldFocus) {
onFocusHandled()
}
}
ChecklistItemRow(
item = item,
onTextChange = { onTextChange(item.id, it) },
onCheckedChange = { onCheckedChange(item.id, it) },
onDelete = { onDelete(item.id) },
onAddNewItem = { onAddNewItemAfter(item.id) },
requestFocus = shouldFocus,
isDragging = isDragging,
isAnyItemDragging = dragDropState.draggingItemIndex != null,
dragModifier = Modifier.dragContainer(dragDropState, visualIndex),
onHeightChanged = onHeightChanged, // 🆕 v1.8.1 (IMPL_05)
modifier = Modifier
.then(if (!isDragging) Modifier.animateItem() else Modifier)
.offset {
IntOffset(
0,
if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
)
}
.zIndex(if (isDragging) DRAGGING_ITEM_Z_INDEX else 0f)
.shadow(elevation, shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp))
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp)
)
)
}
@Suppress("LongParameterList") // Compose functions commonly have many callback parameters
@Composable
private fun ChecklistEditor(
@@ -351,6 +409,9 @@ private fun ChecklistEditor(
onMove = onMove
)
// 🆕 v1.8.1 (IMPL_05): Auto-Scroll bei Zeilenumbruch
var scrollToItemIndex by remember { mutableStateOf<Int?>(null) }
// 🆕 v1.8.0 (IMPL_017 + IMPL_020): Separator nur bei MANUAL und UNCHECKED_FIRST anzeigen
val uncheckedCount = items.count { !it.isChecked }
val checkedCount = items.count { it.isChecked }
@@ -359,70 +420,78 @@ private fun ChecklistEditor(
val showSeparator = shouldShowSeparator && uncheckedCount > 0 && checkedCount > 0
Column(modifier = modifier) {
// 🆕 v1.8.1 IMPL_14: Separator-Position für DragDropState aktualisieren
val separatorVisualIndex = if (showSeparator) uncheckedCount else -1
LaunchedEffect(separatorVisualIndex) {
dragDropState.separatorVisualIndex = separatorVisualIndex
}
// 🆕 v1.8.1 (IMPL_05): Auto-Scroll wenn ein Item durch Zeilenumbruch wächst
LaunchedEffect(scrollToItemIndex) {
scrollToItemIndex?.let { index ->
delay(AUTO_SCROLL_DELAY_MS) // Warten bis Layout-Pass abgeschlossen
val lastVisibleIndex = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0
if (index >= lastVisibleIndex - 1) {
listState.animateScrollToItem(
index = minOf(index + 1, items.size + if (showSeparator) 1 else 0),
scrollOffset = 0
)
}
scrollToItemIndex = null
}
}
LazyColumn(
state = listState,
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
// 🆕 v1.8.1 IMPL_14: Unchecked Items (Visual Index 0..uncheckedCount-1)
itemsIndexed(
items = items,
items = if (showSeparator) items.subList(0, uncheckedCount) else items,
key = { _, item -> item.id }
) { index, item ->
// 🆕 v1.8.0 (IMPL_017): Separator vor dem ersten Checked-Item
if (showSeparator && index == uncheckedCount) {
CheckedItemsSeparator(checkedCount = checkedCount)
}
val isDragging = dragDropState.draggingItemIndex == index
val elevation by animateDpAsState(
targetValue = if (isDragging) 8.dp else 0.dp,
label = "elevation"
)
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(
DraggableChecklistItem(
item = item,
onTextChange = { onTextChange(item.id, it) },
onCheckedChange = { onCheckedChange(item.id, it) },
onDelete = { onDelete(item.id) },
onAddNewItem = { onAddNewItemAfter(item.id) },
requestFocus = shouldFocus,
// 🆕 v1.8.0: IMPL_023 - Drag state übergeben
isDragging = isDragging,
// 🆕 v1.8.0: IMPL_023 - Gradient während Drag ausblenden
isAnyItemDragging = dragDropState.draggingItemIndex != null,
// 🆕 v1.8.0: IMPL_023 - Drag nur auf Handle
dragModifier = Modifier.dragContainer(dragDropState, index),
modifier = Modifier
.animateItem() // 🆕 v1.8.0 (IMPL_017): LazyColumn Item-Animation
.offset {
IntOffset(
0,
if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0
visualIndex = index,
dragDropState = dragDropState,
focusNewItemId = focusNewItemId,
onTextChange = onTextChange,
onCheckedChange = onCheckedChange,
onDelete = onDelete,
onAddNewItemAfter = onAddNewItemAfter,
onFocusHandled = onFocusHandled,
onHeightChanged = { scrollToItemIndex = index } // 🆕 v1.8.1 (IMPL_05)
)
}
// 🆕 v1.8.0: IMPL_023 - Gedraggtes Item liegt über anderen
.zIndex(if (isDragging) DRAGGING_ITEM_Z_INDEX else 0f)
.shadow(elevation, shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp))
.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(ITEM_CORNER_RADIUS_DP.dp)
// 🆕 v1.8.1 IMPL_14: Separator als eigenes LazyColumn-Item
if (showSeparator) {
item(key = "separator") {
CheckedItemsSeparator(
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)
)
}
}

View File

@@ -13,6 +13,7 @@ import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.sync.SyncWorker
import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.utils.Constants
@@ -90,7 +91,13 @@ class NoteEditorViewModel(
val noteTypeString = savedStateHandle.get<String>(ARG_NOTE_TYPE) ?: NoteType.TEXT.name
if (noteId != null) {
// Load existing note
loadExistingNote(noteId)
} else {
initNewNote(noteTypeString)
}
}
private fun loadExistingNote(noteId: String) {
existingNote = storage.loadNote(noteId)
existingNote?.let { note ->
currentNoteType = note.noteType
@@ -109,6 +116,17 @@ class NoteEditorViewModel(
}
if (note.noteType == NoteType.CHECKLIST) {
loadChecklistData(note)
}
}
}
private fun loadChecklistData(note: Note) {
// 🆕 v1.8.1 (IMPL_03): Gespeicherte Sortierung laden
note.checklistSortOption?.let { sortName ->
_lastChecklistSortOption.value = parseSortOption(sortName)
}
val items = note.checklistItems?.sortedBy { it.order }?.map {
ChecklistItemState(
id = it.id,
@@ -120,13 +138,12 @@ class NoteEditorViewModel(
// 🆕 v1.8.0 (IMPL_017): Sortierung sicherstellen (falls alte Daten unsortiert sind)
_checklistItems.value = sortChecklistItems(items)
}
}
} else {
// New note
private fun initNewNote(noteTypeString: String) {
currentNoteType = try {
NoteType.valueOf(noteTypeString)
} catch (e: IllegalArgumentException) {
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT: ${e.message}")
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) {
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT")
NoteType.TEXT
}
@@ -147,6 +164,18 @@ class NoteEditorViewModel(
_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.
* 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> {
val sorted = when (_lastChecklistSortOption.value) {
ChecklistSortOption.MANUAL,
ChecklistSortOption.UNCHECKED_FIRST -> {
val unchecked = items.filter { !it.isChecked }
val checked = items.filter { it.isChecked }
unchecked + checked
}
ChecklistSortOption.CHECKED_FIRST ->
items.sortedByDescending { it.isChecked }
ChecklistSortOption.ALPHABETICAL_ASC ->
items.sortedBy { it.text.lowercase() }
ChecklistSortOption.ALPHABETICAL_DESC ->
items.sortedByDescending { it.text.lowercase() }
}
return (unchecked + checked).mapIndexed { index, item ->
return sorted.mapIndexed { index, item ->
item.copy(order = index)
}
}
@@ -198,13 +244,34 @@ class NoteEditorViewModel(
}
}
/**
* 🆕 v1.8.1 (IMPL_15): Fügt ein neues Item nach dem angegebenen Item ein.
*
* Guard: Bei MANUAL/UNCHECKED_FIRST wird sichergestellt, dass das neue (unchecked)
* Item nicht innerhalb der checked-Sektion eingefügt wird. Falls das Trigger-Item
* checked ist, wird stattdessen vor dem ersten checked Item eingefügt.
*/
fun addChecklistItemAfter(afterItemId: String): String {
val newItem = ChecklistItemState.createEmpty(0)
_checklistItems.update { items ->
val index = items.indexOfFirst { it.id == afterItemId }
if (index >= 0) {
val currentSort = _lastChecklistSortOption.value
val hasSeparator = currentSort == ChecklistSortOption.MANUAL ||
currentSort == ChecklistSortOption.UNCHECKED_FIRST
// 🆕 v1.8.1 (IMPL_15): Wenn das Trigger-Item checked ist und ein Separator
// existiert, darf das neue unchecked Item nicht in die checked-Sektion.
// → Stattdessen vor dem ersten checked Item einfügen.
val effectiveIndex = if (hasSeparator && items[index].isChecked) {
val firstCheckedIndex = items.indexOfFirst { it.isChecked }
if (firstCheckedIndex >= 0) firstCheckedIndex else index + 1
} else {
index + 1
}
val newList = items.toMutableList()
newList.add(index + 1, newItem)
newList.add(effectiveIndex, newItem)
// Update order values
newList.mapIndexed { i, item -> item.copy(order = i) }
} else {
@@ -214,12 +281,46 @@ class NoteEditorViewModel(
return newItem.id
}
/**
* 🆕 v1.8.1 (IMPL_15): Fügt ein neues Item an der semantisch korrekten Position ein.
*
* Bei MANUAL/UNCHECKED_FIRST: Vor dem ersten checked Item (= direkt über dem Separator).
* Bei allen anderen Modi: Am Ende der Liste (kein Separator sichtbar).
*
* Verhindert, dass checked Items über den Separator springen oder das neue Item
* unter dem Separator erscheint.
*/
fun addChecklistItemAtEnd(): String {
val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size)
_checklistItems.update { items -> items + newItem }
val newItem = ChecklistItemState.createEmpty(0)
_checklistItems.update { items ->
val insertIndex = calculateInsertIndexForNewItem(items)
val newList = items.toMutableList()
newList.add(insertIndex, newItem)
newList.mapIndexed { i, item -> item.copy(order = i) }
}
return newItem.id
}
/**
* 🆕 v1.8.1 (IMPL_15): Berechnet die korrekte Insert-Position für ein neues unchecked Item.
*
* - MANUAL / UNCHECKED_FIRST: Vor dem ersten checked Item (direkt über dem Separator)
* - Alle anderen Modi: Am Ende der Liste (kein Separator, kein visuelles Problem)
*
* Falls keine checked Items existieren, wird am Ende eingefügt.
*/
private fun calculateInsertIndexForNewItem(items: List<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) {
_checklistItems.update { items ->
val filtered = items.filter { it.id != itemId }
@@ -238,15 +339,18 @@ class NoteEditorViewModel(
val fromItem = items.getOrNull(fromIndex) ?: return@update items
val toItem = items.getOrNull(toIndex) ?: return@update items
// 🆕 v1.8.0 (IMPL_017): Drag nur innerhalb der gleichen Gruppe erlauben
// (checked ↔ checked, unchecked ↔ unchecked)
if (fromItem.isChecked != toItem.isChecked) {
return@update items // Kein Move über Gruppen-Grenze
}
val mutableList = items.toMutableList()
val item = mutableList.removeAt(fromIndex)
mutableList.add(toIndex, item)
// 🆕 v1.8.1 IMPL_14: Cross-Boundary Move mit Auto-Toggle
// Wenn ein Item die Grenze überschreitet, wird es automatisch checked/unchecked.
val movedItem = if (fromItem.isChecked != toItem.isChecked) {
item.copy(isChecked = toItem.isChecked)
} else {
item
}
mutableList.add(toIndex, movedItem)
// Update order values
mutableList.mapIndexed { index, i -> i.copy(order = index) }
}
@@ -348,6 +452,7 @@ class NoteEditorViewModel(
content = "", // Empty for checklists
noteType = NoteType.CHECKLIST,
checklistItems = validItems,
checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING
)
@@ -357,6 +462,7 @@ class NoteEditorViewModel(
content = "",
noteType = NoteType.CHECKLIST,
checklistItems = validItems,
checklistSortOption = _lastChecklistSortOption.value.name, // 🆕 v1.8.1 (IMPL_03)
deviceId = DeviceIdGenerator.getDeviceId(getApplication()),
syncStatus = SyncStatus.LOCAL_ONLY
)
@@ -366,7 +472,7 @@ class NoteEditorViewModel(
}
}
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED))
// 🆕 v1.8.1 (IMPL_12): NOTE_SAVED Toast entfernt — NavigateBack ist ausreichend
// 🌟 v1.6.0: Trigger onSave Sync
triggerOnSaveSync()
@@ -406,17 +512,33 @@ class NoteEditorViewModel(
val success = withContext(Dispatchers.IO) {
webdavService.deleteNoteFromServer(noteId)
}
// 🆕 v1.8.1 (IMPL_12): Banner-Feedback statt stiller Log-Einträge
if (success) {
Logger.d(TAG, "Note $noteId deleted from server")
SyncStateManager.showInfo(
getApplication<Application>().getString(
dev.dettmer.simplenotes.R.string.snackbar_deleted_from_server
)
)
} else {
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) {
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)
}
}
@@ -513,6 +635,7 @@ class NoteEditorViewModel(
Logger.d(TAG, "📤 Triggering onSave sync")
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag(Constants.SYNC_WORK_TAG)
.addTag(Constants.SYNC_ONSAVE_TAG) // 🆕 v1.8.1 (IMPL_08B): Bypassed globalen Cooldown
.build()
WorkManager.getInstance(getApplication()).enqueue(syncRequest)
}

View File

@@ -15,6 +15,7 @@ import dev.dettmer.simplenotes.R
/**
* 🆕 v1.8.0 (IMPL_017): Visueller Separator zwischen unchecked und checked Items
* 🆕 v1.8.1 (IMPL_14): Drag-Awareness — Primary-Farbe während Drag als visueller Hinweis
*
* Zeigt eine dezente Linie mit Anzahl der erledigten Items:
* ── 3 completed ──
@@ -22,7 +23,8 @@ import dev.dettmer.simplenotes.R
@Composable
fun CheckedItemsSeparator(
checkedCount: Int,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
isDragActive: Boolean = false // 🆕 v1.8.1 IMPL_14
) {
Row(
modifier = modifier
@@ -32,7 +34,10 @@ fun CheckedItemsSeparator(
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outlineVariant
color = if (isDragActive)
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.outlineVariant
)
Text(
@@ -42,13 +47,19 @@ fun CheckedItemsSeparator(
checkedCount
),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
color = if (isDragActive)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.outline,
modifier = Modifier.padding(horizontal = 12.dp)
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outlineVariant
color = if (isDragActive)
MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.outlineVariant
)
}
}

View File

@@ -24,8 +24,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@@ -71,6 +71,7 @@ fun ChecklistItemRow(
isDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Drag state
isAnyItemDragging: Boolean = false, // 🆕 v1.8.0: IMPL_023 - Hide gradient during any drag
dragModifier: Modifier = Modifier, // 🆕 v1.8.0: IMPL_023 - Drag modifier for handle
onHeightChanged: (() -> Unit)? = null, // 🆕 v1.8.1: IMPL_05 - Auto-scroll callback
modifier: Modifier = Modifier
) {
val focusRequester = remember { FocusRequester() }
@@ -92,17 +93,14 @@ fun ChecklistItemRow(
// 🆕 v1.8.0: ScrollState für dynamischen Gradient
val scrollState = rememberScrollState()
// 🆕 v1.8.0: Scroll-basierter Ansatz aktiv wenn Höhe berechnet wurde
val useScrollClipping = hasOverflow && collapsedHeightDp != null
// 🆕 v1.8.1: IMPL_05 - Letzte Zeilenanzahl tracken für Auto-Scroll
var lastLineCount by remember { mutableIntStateOf(0) }
// 🆕 v1.8.0: Dynamische Gradient-Sichtbarkeit basierend auf Scroll-Position
val showGradient = useScrollClipping && !isFocused && !isAnyItemDragging
val showTopGradient by remember {
derivedStateOf { showGradient && scrollState.value > 0 }
}
val showBottomGradient by remember {
derivedStateOf { showGradient && scrollState.value < scrollState.maxValue }
}
// 🆕 v1.8.1: Gradient-Sichtbarkeit direkt berechnet (kein derivedStateOf)
// derivedStateOf mit remember{} fängt showGradient als stale val — nie aktualisiert.
val showGradient = hasOverflow && collapsedHeightDp != null && !isFocused && !isAnyItemDragging
val showTopGradient = showGradient && scrollState.value > 0
val showBottomGradient = showGradient && scrollState.value < scrollState.maxValue
// v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items)
LaunchedEffect(requestFocus) {
@@ -173,7 +171,7 @@ fun ChecklistItemRow(
Box(modifier = Modifier.weight(1f)) {
// Scrollbarer Wrapper: begrenzt Höhe auf ~5 Zeilen wenn collapsed
Box(
modifier = if (!isFocused && useScrollClipping) {
modifier = if (!isFocused && hasOverflow && collapsedHeightDp != null) {
Modifier
.heightIn(max = collapsedHeightDp!!)
.verticalScroll(scrollState)
@@ -216,13 +214,16 @@ fun ChecklistItemRow(
onNext = { onAddNewItem() }
),
singleLine = false,
// maxLines nur als Fallback bis collapsedHeight berechnet ist
maxLines = if (isFocused || useScrollClipping) Int.MAX_VALUE else COLLAPSED_MAX_LINES,
// 🆕 v1.8.1: maxLines IMMER Int.MAX_VALUE — keine Oszillation möglich
// Höhenbegrenzung erfolgt ausschließlich über heightIn-Modifier oben.
// Vorher: maxLines=5 → lineCount gedeckelt → Overflow nie erkannt → Deadlock
maxLines = Int.MAX_VALUE,
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
onTextLayout = { textLayoutResult ->
// 🆕 v1.8.0: Overflow erkennen - ABER NUR wenn kein Drag aktiv ist
// 🆕 v1.8.1: lineCount ist jetzt akkurat (maxLines=MAX_VALUE deckelt nicht)
val lineCount = textLayoutResult.lineCount
if (!isAnyItemDragging) {
val overflow = textLayoutResult.lineCount > COLLAPSED_MAX_LINES
val overflow = lineCount > COLLAPSED_MAX_LINES
hasOverflow = overflow
// Höhe der ersten 5 Zeilen berechnen (einmalig)
if (overflow && collapsedHeightDp == null) {
@@ -230,7 +231,16 @@ fun ChecklistItemRow(
textLayoutResult.getLineBottom(COLLAPSED_MAX_LINES - 1).toDp()
}
}
// Reset wenn Text gekürzt wird
if (!overflow) {
collapsedHeightDp = null
}
}
// 🆕 v1.8.1 (IMPL_05): Höhenänderung bei Zeilenumbruch melden
if (isFocused && lineCount > lastLineCount && lastLineCount > 0) {
onHeightChanged?.invoke()
}
lastLineCount = lineCount
},
decorationBox = { innerTextField ->
Box {

View File

@@ -236,6 +236,11 @@ class ComposeMainActivity : ComponentActivity() {
kotlinx.coroutines.delay(2000L)
SyncStateManager.reset()
}
// 🆕 v1.8.1 (IMPL_12): INFO-Meldungen nach 2.5s ausblenden
dev.dettmer.simplenotes.sync.SyncPhase.INFO -> {
kotlinx.coroutines.delay(2500L)
SyncStateManager.reset()
}
dev.dettmer.simplenotes.sync.SyncPhase.ERROR -> {
kotlinx.coroutines.delay(4000L)
SyncStateManager.reset()

View File

@@ -470,12 +470,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
if (success) {
_showToast.emit(getString(R.string.snackbar_deleted_from_server))
// 🆕 v1.8.1 (IMPL_12): Toast → Banner INFO
SyncStateManager.showInfo(getString(R.string.snackbar_deleted_from_server))
} else {
_showToast.emit(getString(R.string.snackbar_server_delete_failed))
SyncStateManager.showError(getString(R.string.snackbar_server_delete_failed))
}
} catch (e: Exception) {
_showToast.emit(getString(R.string.snackbar_server_error, e.message ?: ""))
SyncStateManager.showError(getString(R.string.snackbar_server_error, e.message ?: ""))
} finally {
// Remove from pending deletions
_pendingDeletions.value = _pendingDeletions.value - noteId
@@ -507,7 +508,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
}
// Show aggregated toast
// 🆕 v1.8.1 (IMPL_12): Toast → Banner INFO/ERROR
val message = when {
failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount)
successCount == 0 -> getString(R.string.snackbar_server_delete_failed)
@@ -517,7 +518,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
successCount + failCount
)
}
_showToast.emit(message)
if (failCount > 0) {
SyncStateManager.showError(message)
} else {
SyncStateManager.showInfo(message)
}
}
}
@@ -555,6 +560,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
// 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (verhindert Auto-Sync direkt danach)
// Manueller Sync prüft NICHT den globalen Cooldown (User will explizit synchronisieren)
val prefs = getApplication<android.app.Application>().getSharedPreferences(
Constants.PREFS_NAME,
android.content.Context.MODE_PRIVATE
)
// 🆕 v1.7.0: Feedback wenn Sync bereits läuft
// 🆕 v1.8.0: tryStartSync setzt sofort PREPARING → Banner erscheint instant
if (!SyncStateManager.tryStartSync(source)) {
@@ -571,6 +583,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
// 🆕 v1.8.1 (IMPL_08): Globalen Cooldown markieren (nach tryStartSync, vor Launch)
SyncStateManager.markGlobalSyncStarted(prefs)
viewModelScope.launch {
try {
// Check for unsynced changes (Banner zeigt bereits PREPARING)
@@ -636,7 +651,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
return
}
// Throttling check
// 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (alle Trigger teilen sich diesen)
if (!SyncStateManager.canSyncGlobally(prefs)) {
return
}
// Throttling check (eigener 60s-Cooldown für onResume)
if (!canTriggerAutoSync()) {
return
}
@@ -665,6 +685,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Update last sync timestamp
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
// 🆕 v1.8.1 (IMPL_08): Globalen Sync-Cooldown markieren
SyncStateManager.markGlobalSyncStarted(prefs)
viewModelScope.launch {
try {
// Check for unsynced changes
@@ -692,9 +715,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (result.isSuccess && result.syncedCount > 0) {
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
// Silent Sync mit echten Änderungen → trotzdem markCompleted (wird silent behandelt)
// 🆕 v1.8.1 (IMPL_11): Kein Toast bei Silent-Sync
// Das Banner-System respektiert silent=true korrekt (markCompleted → IDLE)
// Toast wurde fälschlicherweise trotzdem angezeigt
SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
_showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount))
loadNotes()
} else if (result.isSuccess) {
Logger.d(TAG, " Auto-sync ($source): No changes")

View File

@@ -1,8 +1,6 @@
package dev.dettmer.simplenotes.ui.main
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -32,7 +30,6 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R

View File

@@ -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}"
}
}

View File

@@ -149,10 +149,9 @@ fun NoteCardCompact(
text = when (note.noteType) {
NoteType.TEXT -> note.content
NoteType.CHECKLIST -> {
note.checklistItems
?.joinToString("\n") { item ->
val prefix = if (item.isChecked) "" else ""
"$prefix ${item.text}"
// 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
note.checklistItems?.let { items ->
generateChecklistPreview(items, note.checklistSortOption)
} ?: ""
}
},

View File

@@ -163,10 +163,9 @@ fun NoteCardGrid(
text = when (note.noteType) {
NoteType.TEXT -> note.content
NoteType.CHECKLIST -> {
note.checklistItems
?.joinToString("\n") { item ->
val prefix = if (item.isChecked) "" else ""
"$prefix ${item.text}"
// 🆕 v1.8.1 (IMPL_03 + IMPL_06): Sortierte Preview mit neuen Emojis
note.checklistItems?.let { items ->
generateChecklistPreview(items, note.checklistSortOption)
} ?: ""
}
},

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ErrorOutline
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
@@ -51,11 +52,13 @@ fun SyncProgressBanner(
// Farbe animiert wechseln je nach State
val isError = progress.phase == SyncPhase.ERROR
val isCompleted = progress.phase == SyncPhase.COMPLETED
val isResult = isError || isCompleted
val isInfo = progress.phase == SyncPhase.INFO // 🆕 v1.8.1 (IMPL_12)
val isResult = isError || isCompleted || isInfo
val backgroundColor by animateColorAsState(
targetValue = when {
isError -> MaterialTheme.colorScheme.errorContainer
isInfo -> MaterialTheme.colorScheme.secondaryContainer // 🆕 v1.8.1 (IMPL_12)
else -> MaterialTheme.colorScheme.primaryContainer
},
label = "bannerColor"
@@ -64,6 +67,7 @@ fun SyncProgressBanner(
val contentColor by animateColorAsState(
targetValue = when {
isError -> MaterialTheme.colorScheme.onErrorContainer
isInfo -> MaterialTheme.colorScheme.onSecondaryContainer // 🆕 v1.8.1 (IMPL_12)
else -> MaterialTheme.colorScheme.onPrimaryContainer
},
label = "bannerContentColor"
@@ -89,7 +93,7 @@ fun SyncProgressBanner(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// Icon: Spinner (aktiv), Checkmark (completed), Error (error)
// Icon: Spinner (aktiv), Checkmark (completed), Error (error), Info (info)
when {
isCompleted -> {
Icon(
@@ -99,6 +103,14 @@ fun SyncProgressBanner(
tint = contentColor
)
}
isInfo -> { // 🆕 v1.8.1 (IMPL_12)
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = contentColor
)
}
isError -> {
Icon(
imageVector = Icons.Filled.ErrorOutline,
@@ -187,5 +199,6 @@ private fun phaseToString(phase: SyncPhase): String {
SyncPhase.IMPORTING_MARKDOWN -> stringResource(R.string.sync_phase_importing_markdown)
SyncPhase.COMPLETED -> stringResource(R.string.sync_phase_completed)
SyncPhase.ERROR -> stringResource(R.string.sync_phase_error)
SyncPhase.INFO -> "" // 🆕 v1.8.1 (IMPL_12): INFO nutzt immer resultMessage
}
}

View File

@@ -82,4 +82,11 @@ object Constants {
// 📋 v1.8.0: Post-Update Changelog
const val KEY_LAST_SHOWN_CHANGELOG_VERSION = "last_shown_changelog_version"
// 🆕 v1.8.1 (IMPL_08): Globaler Sync-Cooldown (über alle Trigger hinweg)
const val KEY_LAST_GLOBAL_SYNC_TIME = "last_global_sync_timestamp"
const val MIN_GLOBAL_SYNC_INTERVAL_MS = 30_000L // 30 Sekunden
// 🆕 v1.8.1 (IMPL_08B): onSave-Sync Worker-Tag (bypassed globalen Cooldown)
const val SYNC_ONSAVE_TAG = "onsave"
}

View File

@@ -38,13 +38,18 @@ class NoteWidget : GlanceAppWidget() {
// Responsive Breakpoints — schmale + breite Spalten
val SIZE_SMALL = DpSize(110.dp, 80.dp) // Schmal+kurz: nur Titel
val SIZE_NARROW_MEDIUM = DpSize(110.dp, 110.dp) // Schmal+mittel: Vorschau
val SIZE_NARROW_SCROLL = DpSize(110.dp, 150.dp) // 🆕 v1.8.1: Schmal+scroll (Standard 3x2)
val SIZE_NARROW_LARGE = DpSize(110.dp, 250.dp) // Schmal+groß: voller Inhalt
val SIZE_WIDE_MEDIUM = DpSize(250.dp, 110.dp) // Breit+mittel: Vorschau
val SIZE_WIDE_SCROLL = DpSize(250.dp, 150.dp) // 🆕 v1.8.1: Breit+scroll (Standard 3x2 breit)
val SIZE_WIDE_LARGE = DpSize(250.dp, 250.dp) // Breit+groß: voller Inhalt
}
override val sizeMode = SizeMode.Responsive(
setOf(SIZE_SMALL, SIZE_NARROW_MEDIUM, SIZE_NARROW_LARGE, SIZE_WIDE_MEDIUM, SIZE_WIDE_LARGE)
setOf(
SIZE_SMALL, SIZE_NARROW_MEDIUM, SIZE_NARROW_SCROLL, SIZE_NARROW_LARGE,
SIZE_WIDE_MEDIUM, SIZE_WIDE_SCROLL, SIZE_WIDE_LARGE
)
)
override val stateDefinition = PreferencesGlanceStateDefinition

View File

@@ -5,6 +5,7 @@ import androidx.glance.GlanceId
import androidx.glance.action.ActionParameters
import androidx.glance.appwidget.action.ActionCallback
import androidx.glance.appwidget.state.updateAppWidgetState
import dev.dettmer.simplenotes.models.ChecklistSortOption
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.Logger
@@ -51,14 +52,32 @@ class ToggleChecklistItemAction : ActionCallback {
} else item
} ?: return
// 🆕 v1.8.1 (IMPL_04): Auto-Sort nach Toggle
// Konsistent mit NoteEditorViewModel.updateChecklistItemChecked
val sortOption = try {
note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
?: ChecklistSortOption.MANUAL
val sortedItems = if (sortOption == ChecklistSortOption.MANUAL ||
sortOption == ChecklistSortOption.UNCHECKED_FIRST) {
val unchecked = updatedItems.filter { !it.isChecked }
val checked = updatedItems.filter { it.isChecked }
(unchecked + checked).mapIndexed { index, item ->
item.copy(order = index)
}
} else {
updatedItems.mapIndexed { index, item -> item.copy(order = index) }
}
val updatedNote = note.copy(
checklistItems = updatedItems,
checklistItems = sortedItems,
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING
)
storage.saveNote(updatedNote)
Logger.d(TAG, "Toggled checklist item '$itemId' in widget")
Logger.d(TAG, "Toggled + auto-sorted checklist item '$itemId' in widget")
// 🐛 FIX: Glance-State ändern um Re-Render zu erzwingen
updateAppWidgetState(context, glanceId) { prefs ->

View File

@@ -38,9 +38,11 @@ import androidx.glance.layout.width
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.ChecklistSortOption
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
import dev.dettmer.simplenotes.ui.main.components.sortChecklistItemsForPreview
/**
* 🆕 v1.8.0: Glance Composable Content für das Notiz-Widget
@@ -52,6 +54,7 @@ import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
// ── Size Classification ──
private val WIDGET_HEIGHT_SMALL_THRESHOLD = 110.dp
private val WIDGET_HEIGHT_SCROLL_THRESHOLD = 150.dp // 🆕 v1.8.1: Scrollbare Ansicht
private val WIDGET_SIZE_MEDIUM_THRESHOLD = 250.dp
// 🆕 v1.8.0: Increased preview lengths for better text visibility
@@ -60,12 +63,40 @@ private const val TEXT_PREVIEW_FULL_LENGTH = 300
private fun DpSize.toSizeClass(): WidgetSizeClass = when {
height < WIDGET_HEIGHT_SMALL_THRESHOLD -> WidgetSizeClass.SMALL
width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_MED
// 🆕 v1.8.1: Neue ScrollView-Schwelle bei 150dp Höhe
width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_HEIGHT_SCROLL_THRESHOLD -> WidgetSizeClass.NARROW_MED
width < WIDGET_SIZE_MEDIUM_THRESHOLD && height < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_SCROLL
width < WIDGET_SIZE_MEDIUM_THRESHOLD -> WidgetSizeClass.NARROW_TALL
height < WIDGET_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
}
/**
* 🆕 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
fun NoteWidgetContent(
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) {
NoteType.TEXT -> TextNoteFullView(note)
NoteType.CHECKLIST -> ChecklistFullView(
NoteType.TEXT -> Box(modifier = contentClickModifier) {
TextNoteFullView(note)
}
NoteType.CHECKLIST -> {
// 🆕 v1.8.1: Locked: Click -> Options | Unlocked: kein Click -> Scroll frei
val checklistBoxModifier = if (isLocked) {
contentClickModifier
} else {
GlanceModifier.fillMaxSize()
}
Box(modifier = checklistBoxModifier) {
ChecklistFullView(
note = note,
isLocked = isLocked,
glanceId = glanceId
)
}
}
}
}
WidgetSizeClass.WIDE_MED -> Box(modifier = contentClickModifier) {
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) {
NoteType.TEXT -> TextNoteFullView(note)
NoteType.CHECKLIST -> ChecklistFullView(
NoteType.TEXT -> Box(modifier = contentClickModifier) {
TextNoteFullView(note)
}
NoteType.CHECKLIST -> {
// 🆕 v1.8.1: Locked: Click -> Options | Unlocked: kein Click -> Scroll frei
val checklistBoxModifier = if (isLocked) {
contentClickModifier
} else {
GlanceModifier.fillMaxSize()
}
Box(modifier = checklistBoxModifier) {
ChecklistFullView(
note = note,
isLocked = isLocked,
glanceId = glanceId
@@ -214,6 +271,8 @@ fun NoteWidgetContent(
}
}
}
}
}
/**
* Optionsleiste — Lock/Unlock + Refresh + Open in App
@@ -370,13 +429,35 @@ private fun ChecklistCompactView(
isLocked: Boolean,
glanceId: GlanceId
) {
val items = note.checklistItems?.sortedBy { it.order } ?: return
// 🆕 v1.8.1 (IMPL_04): Sortierung aus Editor übernehmen
val items = note.checklistItems?.let { rawItems ->
sortChecklistItemsForPreview(rawItems, note.checklistSortOption)
} ?: return
// 🆕 v1.8.1 (IMPL_04): Separator-Logik
val uncheckedCount = items.count { !it.isChecked }
val checkedCount = items.count { it.isChecked }
val sortOption = try {
note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
?: ChecklistSortOption.MANUAL
val showSeparator = (sortOption == ChecklistSortOption.MANUAL ||
sortOption == ChecklistSortOption.UNCHECKED_FIRST) &&
uncheckedCount > 0 && checkedCount > 0
val visibleItems = items.take(maxItems)
val remainingCount = items.size - visibleItems.size
val checkedCount = items.count { it.isChecked }
Column(modifier = GlanceModifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
var separatorShown = false
visibleItems.forEach { item ->
// 🆕 v1.8.1: Separator vor dem ersten checked Item anzeigen
if (showSeparator && !separatorShown && item.isChecked) {
WidgetCheckedItemsSeparator(checkedCount = checkedCount)
separatorShown = true
}
if (isLocked) {
Row(
modifier = GlanceModifier
@@ -385,7 +466,7 @@ private fun ChecklistCompactView(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (item.isChecked) "" else "",
text = if (item.isChecked) "☑️" else "", // 🆕 v1.8.1 (IMPL_06)
style = TextStyle(fontSize = 14.sp)
)
Spacer(modifier = GlanceModifier.width(6.dp))
@@ -443,15 +524,41 @@ private fun ChecklistFullView(
isLocked: Boolean,
glanceId: GlanceId
) {
val items = note.checklistItems?.sortedBy { it.order } ?: return
// 🆕 v1.8.1 (IMPL_04): Sortierung aus Editor übernehmen
val items = note.checklistItems?.let { rawItems ->
sortChecklistItemsForPreview(rawItems, note.checklistSortOption)
} ?: return
// 🆕 v1.8.1 (IMPL_04): Separator-Logik
val uncheckedCount = items.count { !it.isChecked }
val checkedCount = items.count { it.isChecked }
val sortOption = try {
note.checklistSortOption?.let { ChecklistSortOption.valueOf(it) }
} catch (@Suppress("SwallowedException") e: IllegalArgumentException) { null }
?: ChecklistSortOption.MANUAL
val showSeparator = (sortOption == ChecklistSortOption.MANUAL ||
sortOption == ChecklistSortOption.UNCHECKED_FIRST) &&
uncheckedCount > 0 && checkedCount > 0
// 🆕 v1.8.1: Berechne die Gesamtanzahl der Elemente inklusive Separator
val totalItems = items.size + if (showSeparator) 1 else 0
LazyColumn(
modifier = GlanceModifier
.fillMaxSize()
.padding(horizontal = 8.dp)
) {
items(items.size) { index ->
val item = items[index]
items(totalItems) { index ->
// 🆕 v1.8.1: Separator an Position uncheckedCount einfügen
if (showSeparator && index == uncheckedCount) {
WidgetCheckedItemsSeparator(checkedCount = checkedCount)
return@items
}
// Tatsächlichen Item-Index berechnen (nach Separator um 1 verschoben)
val itemIndex = if (showSeparator && index > uncheckedCount) index - 1 else index
val item = items.getOrNull(itemIndex) ?: return@items
if (isLocked) {
Row(
@@ -461,7 +568,7 @@ private fun ChecklistFullView(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = if (item.isChecked) "" else "",
text = if (item.isChecked) "☑️" else "", // 🆕 v1.8.1 (IMPL_06)
style = TextStyle(fontSize = 16.sp)
)
Spacer(modifier = GlanceModifier.width(8.dp))

View File

@@ -4,11 +4,14 @@ package dev.dettmer.simplenotes.widget
* 🆕 v1.8.0: Size classification for responsive Note Widget layouts
*
* Determines which layout variant to use based on widget dimensions.
* 🆕 v1.8.1: Added NARROW_SCROLL and WIDE_SCROLL for scrollable mid-size widgets
*/
enum class WidgetSizeClass {
SMALL, // Nur Titel
NARROW_MED, // Schmal, Vorschau
NARROW_MED, // Schmal, Vorschau (CompactView)
NARROW_SCROLL, // 🆕 v1.8.1: Schmal, scrollbare Liste (150dp+)
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
}

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.ui.editor
import dev.dettmer.simplenotes.models.ChecklistSortOption
import org.junit.Assert.*
import org.junit.Test
@@ -174,4 +175,204 @@ class ChecklistSortingTest {
assertEquals(1, sorted[1].order)
assertEquals(2, sorted[2].order)
}
// ═══════════════════════════════════════════════════════════════════════
// 🆕 v1.8.1 (IMPL_15): Tests für Add-Item Insert-Position
// ═══════════════════════════════════════════════════════════════════════
/**
* Simulates calculateInsertIndexForNewItem() from NoteEditorViewModel.
* Tests the insert position logic for new unchecked items.
*/
private fun calculateInsertIndexForNewItem(
items: List<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)
}
}

View File

@@ -1,8 +1,8 @@
# ⚡ v1.3.1: detekt Configuration
# ⚡ v1.8.1: detekt Configuration
# Pragmatic rules for simple-notes-sync
build:
maxIssues: 100 # Allow existing issues for v1.3.1 release, fix in v1.4.0
maxIssues: 0 # v1.8.1: All issues resolved
excludeCorrectable: false
config:

View File

@@ -276,8 +276,9 @@ Schritt-für-Schritt:
### Daten-Schutz
-**Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion
-**Keine Verschlüsselung** - Klartextformat für Lesbarkeit
- ⚠️ **Sensible Daten?** - Backup-Datei selbst verschlüsseln (z.B. 7-Zip mit Passwort)
-**Optionale Verschlüsselung** _(v1.7.0+)_ - Backup-Dateien mit Passwort schützen
- **Menschenlesbar** - Klartextformat (JSON) wenn unverschlüsselt
- ⚠️ **Sensible Daten?** - Verschlüsselung aktivieren oder externe Tools nutzen (z.B. 7-Zip)
### Empfehlungen
- 🔐 Backup-Dateien in verschlüsseltem Container speichern
@@ -321,4 +322,4 @@ Schritt-für-Schritt:
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown
**Letzte Aktualisierung:** v1.2.1 (2026-01-05)
**Letzte Aktualisierung:** v1.8.1 (2026-02-11)

View File

@@ -276,8 +276,9 @@ Step-by-step:
### Data Protection
-**Locally stored** - No cloud upload without your action
-**No encryption** - Plain text format for readability
- ⚠️ **Sensitive data?** - Encrypt backup file yourself (e.g., 7-Zip with password)
-**Optional encryption** _(v1.7.0+)_ - Password-protect backup files
- **Human-readable** - Plain JSON format when unencrypted
- ⚠️ **Sensitive data?** - Enable encryption or use external tools (e.g., 7-Zip)
### Recommendations
- 🔐 Store backup files in encrypted container
@@ -317,8 +318,8 @@ Step-by-step:
---
**📚 See also:**
- [QUICKSTART.en.md](../QUICKSTART.en.md) - App installation and setup
- [FEATURES.en.md](FEATURES.en.md) - Complete feature list
- [DESKTOP.en.md](DESKTOP.en.md) - Desktop integration with Markdown
- [QUICKSTART.md](../QUICKSTART.md) - App installation and setup
- [FEATURES.md](FEATURES.md) - Complete feature list
- [DESKTOP.md](DESKTOP.md) - Desktop integration with Markdown
**Last update:** v1.2.1 (2026-01-05)
**Last update:** v1.8.1 (2026-02-11)

View File

@@ -48,8 +48,6 @@ git push origin fix/my-bug
## 📱 Installation auf Gerät
## 📱 Installation auf Gerät
### Mit ADB (Empfohlen - sauberes Testing)
```bash
# Gerät verbinden

View File

@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 🔮 Roadmap
### v1.1
- [ ] Suche & Filter
- [ ] Dark Mode
- [ ] Tags/Kategorien
- [ ] Markdown Preview
### v2.0
- [ ] Desktop Client (Flutter)
- [ ] End-to-End Verschlüsselung
- [ ] Shared Notes (Collaboration)
- [ ] Attachment Support
Siehe [UPCOMING.md](UPCOMING.md) für die vollständige Roadmap und geplante Features.
---
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
---
**Letzte Aktualisierung:** 25. Dezember 2025
**Letzte Aktualisierung:** Februar 2026

View File

@@ -541,17 +541,7 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
## 🔮 Roadmap
### v1.1
- [ ] Search & Filter
- [ ] Dark Mode
- [ ] Tags/Categories
- [ ] Markdown Preview
### v2.0
- [ ] Desktop Client (Flutter)
- [ ] End-to-End Encryption
- [ ] Shared Notes (Collaboration)
- [ ] Attachment Support
See [UPCOMING.md](UPCOMING.md) for the full roadmap and planned features.
---
@@ -564,4 +554,4 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0
---
**Last updated:** December 25, 2025
**Last updated:** February 2026

View File

@@ -37,6 +37,50 @@
---
## 📊 Ansichten & Layout _(NEU in v1.7.0+)_
### Darstellungsmodi
-**Listenansicht** - Klassisches Listen-Layout
-**Rasteransicht** _(NEU in v1.7.0)_ - Pinterest-artiges Staggered Grid mit dynamischen Vorschauzeilen
-**Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht wechseln
-**Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
-**Grid als Standard** _(v1.8.0)_ - Neue Installationen starten im Grid-Modus
### Notiz-Sortierung _(NEU in v1.8.0)_
-**Nach Änderungsdatum** - Neueste oder älteste zuerst
-**Nach Erstelldatum** - Nach Erstellungszeitpunkt
-**Nach Titel** - A-Z oder Z-A
-**Nach Typ** - Textnotizen vs. Checklisten
-**Persistente Einstellungen** - Sortier-Option bleibt nach App-Neustart
-**Sortier-Dialog** - Richtungswahl im Hauptbildschirm
### Checklisten-Sortierung _(NEU in v1.8.0)_
-**Manuell** - Eigene Drag & Drop Reihenfolge
-**Alphabetisch** - A-Z Sortierung
-**Offene zuerst** - Unerledigte Items oben
-**Erledigte zuletzt** - Abgehakte Items unten
-**Visueller Trenner** - Zwischen offenen/erledigten Gruppen mit Anzahl
-**Auto-Sortierung** - Neu sortieren beim Abhaken/Öffnen
-**Drag über Grenzen** - Items wechseln Status beim Überqueren des Trenners
---
## 📌 Homescreen-Widgets _(NEU in v1.8.0)_
### Widget-Features
-**Textnotiz-Widget** - Beliebige Notiz auf dem Homescreen anzeigen
-**Checklisten-Widget** - Interaktive Checkboxen mit Sync zum Server
-**5 Größenklassen** - SMALL, NARROW_MED, NARROW_TALL, WIDE_MED, WIDE_TALL
-**Material You Farben** - Dynamische Farben passend zum System-Theme
-**Einstellbare Transparenz** - Hintergrund-Opazität (0-100%)
-**Sperr-Umschalter** - Versehentliche Bearbeitungen verhindern
-**Auto-Aktualisierung** - Updates nach Sync-Abschluss
-**Konfigurations-Activity** - Notiz-Auswahl und Einstellungen
-**Checklisten-Sortierung** _(v1.8.1)_ - Widgets übernehmen Sortier-Option
-**Visuelle Trenner** _(v1.8.1)_ - Zwischen offenen/erledigten Items
---
## 🌍 Mehrsprachigkeit _(NEU in v1.5.0)_
### Unterstützte Sprachen
@@ -129,9 +173,12 @@
### Sync-Mechanismus
-**Upload** - Lokale Änderungen zum Server
-**Download** - Server-Änderungen in App
-**Parallele Downloads** _(NEU in v1.8.0)_ - Bis zu 5 gleichzeitige Downloads
-**Konflikt-Erkennung** - Bei gleichzeitigen Änderungen
-**Konfliktfreies Merging** - Last-Write-Wins via Timestamp
-**Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT
-**Server-Löschungs-Erkennung** _(NEU in v1.8.0)_ - Erkennt auf anderen Geräten gelöschte Notizen
-**Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT, DELETED_ON_SERVER
-**Live Fortschritts-UI** _(NEU in v1.8.0)_ - Phasen-Anzeige mit Upload/Download-Zählern
-**Fehlerbehandlung** - Retry bei Netzwerkproblemen
-**Offline-First** - App funktioniert ohne Server
@@ -140,6 +187,9 @@
-**HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
-**Username/Password** - Basic Authentication
-**Connection Test** - In Einstellungen testen
-**WiFi-Only Sync** _(NEU in v1.7.0)_ - Option nur über WiFi zu synchronisieren
-**VPN-Unterstützung** _(NEU in v1.7.0)_ - Sync funktioniert korrekt über VPN-Tunnels
-**Self-Signed SSL** _(NEU in v1.7.0)_ - Unterstützung für selbstsignierte Zertifikate
-**Server-URL Normalisierung** - Automatisches `/notes/` und `/notes-md/` _(NEU in v1.2.1)_
-**Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/`
@@ -196,11 +246,12 @@
## 🛠️ Technische Details
### Plattform
- **Android 8.0+** (API 26+)
- **Android 7.0+** (API 24+)
- **Target SDK 36** (Android 15)
- **Kotlin** - Moderne Programmiersprache
- **Jetpack Compose** - Deklaratives UI-Framework
- **Material Design 3** - Neueste Design-Richtlinien
- **ViewBinding** - Typ-sichere View-Referenzen
- **Jetpack Glance** _(v1.8.0)_ - Widget-Framework
### Architektur
- **MVVM-Light** - Einfache Architektur
@@ -218,6 +269,7 @@
- **Gson** - JSON Serialization
- **WorkManager** - Background Tasks
- **OkHttp** - HTTP Client (via Sardine)
- **Glance** _(v1.8.0)_ - Widget-Framework
### Build-Varianten
- **Standard** - Universal APK (100% FOSS, keine Google-Dependencies)
@@ -247,22 +299,12 @@
## 🔮 Zukünftige Features
Geplant für kommende Versionen:
Geplant für kommende Versionen siehe [UPCOMING.md](UPCOMING.md) für die vollständige Roadmap.
### v1.4.0 - Checklisten
- **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen
- **Erledigte Items** - Durchstreichen/Abhaken
- **Drag & Drop** - Items neu anordnen
### v1.5.0 - Internationalisierung
- **Mehrsprachigkeit** - Deutsch + Englisch UI
- **Sprachauswahl** - In Einstellungen wählbar
- **Vollständige Übersetzung** - Alle Strings in beiden Sprachen
### v1.6.0 - Modern APIs
- **LocalBroadcastManager ersetzen** - SharedFlow stattdessen
- **PackageInfo Flags** - PackageInfoFlags.of() verwenden
- **Komplexitäts-Refactoring** - Lange Funktionen aufteilen
### v2.0.0 - Legacy Cleanup
- **Veraltete Activities entfernen** - Durch Compose-Varianten ersetzen
- **LocalBroadcastManager → SharedFlow** - Moderne Event-Architektur
- **WebDavSyncService aufteilen** - SyncOrchestrator, NoteUploader, NoteDownloader
---
@@ -305,4 +347,4 @@ A: Ja! Lade die APK direkt von GitHub oder nutze F-Droid.
---
**Letzte Aktualisierung:** v1.3.2 (2026-01-10)
**Letzte Aktualisierung:** v1.8.1 (2026-02-11)

View File

@@ -37,6 +37,50 @@
---
## 📊 Views & Layout _(NEW in v1.7.0+)_
### Display Modes
-**List View** - Classic list layout
-**Grid View** _(NEW in v1.7.0)_ - Pinterest-style staggered grid with dynamic preview lines
-**Layout toggle** - Switch between list and grid in settings
-**Adaptive columns** - 2-3 columns based on screen size
-**Grid as default** _(v1.8.0)_ - New installations default to grid view
### Note Sorting _(NEW in v1.8.0)_
-**Sort by Updated** - Newest or oldest first
-**Sort by Created** - By creation date
-**Sort by Title** - A-Z or Z-A
-**Sort by Type** - Text notes vs checklists
-**Persistent preferences** - Sort option saved across app restarts
-**Sort dialog** - Direction toggle in main screen
### Checklist Sorting _(NEW in v1.8.0)_
-**Manual** - Custom drag & drop order
-**Alphabetical** - A-Z sorting
-**Unchecked First** - Unchecked items on top
-**Checked Last** - Checked items at bottom
-**Visual separator** - Between unchecked/checked groups with count
-**Auto-sort on toggle** - Re-sorts when checking/unchecking items
-**Drag across boundaries** - Items auto-toggle state when crossing separator
---
## 📌 Homescreen Widgets _(NEW in v1.8.0)_
### Widget Features
-**Text note widget** - Display any note on homescreen
-**Checklist widget** - Interactive checkboxes that sync to server
-**5 size classes** - SMALL, NARROW_MED, NARROW_TALL, WIDE_MED, WIDE_TALL
-**Material You colors** - Dynamic colors matching system theme
-**Configurable opacity** - Background transparency (0-100%)
-**Lock toggle** - Prevent accidental edits
-**Auto-refresh** - Updates after sync completion
-**Configuration activity** - Note selection and settings
-**Checklist sorting** _(v1.8.1)_ - Widgets respect saved sort option
-**Visual separators** _(v1.8.1)_ - Between unchecked/checked items
---
## 🌍 Multilingual Support _(NEW in v1.5.0)_
### Supported Languages
@@ -129,9 +173,12 @@
### Sync Mechanism
-**Upload** - Local changes to server
-**Download** - Server changes to app
-**Parallel downloads** _(NEW in v1.8.0)_ - Up to 5 simultaneous downloads
-**Conflict detection** - On simultaneous changes
-**Conflict-free merging** - Last-Write-Wins via timestamp
-**Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT
-**Server deletion detection** _(NEW in v1.8.0)_ - Detects notes deleted on other devices
-**Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT, DELETED_ON_SERVER
-**Live progress UI** _(NEW in v1.8.0)_ - Phase indicators with upload/download counters
-**Error handling** - Retry on network issues
-**Offline-first** - App works without server
@@ -140,6 +187,9 @@
-**HTTP/HTTPS** - HTTP only local, HTTPS for external
-**Username/password** - Basic authentication
-**Connection test** - Test in settings
-**WiFi-only sync** _(NEW in v1.7.0)_ - Option to sync only on WiFi
-**VPN support** _(NEW in v1.7.0)_ - Sync works correctly through VPN tunnels
-**Self-signed SSL** _(NEW in v1.7.0)_ - Support for self-signed certificates
-**Server URL normalization** - Automatic `/notes/` and `/notes-md/` _(NEW in v1.2.1)_
-**Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/`
@@ -196,11 +246,12 @@
## 🛠️ Technical Details
### Platform
- **Android 8.0+** (API 26+)
- **Android 7.0+** (API 24+)
- **Target SDK 36** (Android 15)
- **Kotlin** - Modern programming language
- **Jetpack Compose** - Declarative UI framework
- **Material Design 3** - Latest design guidelines
- **ViewBinding** - Type-safe view references
- **Jetpack Glance** _(v1.8.0)_ - Widget framework
### Architecture
- **MVVM-Light** - Simple architecture
@@ -218,6 +269,7 @@
- **Gson** - JSON serialization
- **WorkManager** - Background tasks
- **OkHttp** - HTTP client (via Sardine)
- **Glance** _(v1.8.0)_ - Widget framework
### Build Variants
- **Standard** - Universal APK (100% FOSS, no Google dependencies)
@@ -247,22 +299,12 @@
## 🔮 Future Features
Planned for upcoming versions:
Planned for upcoming versions see [UPCOMING.md](UPCOMING.md) for the full roadmap.
### v1.4.0 - Checklists
- **Checklist notes** - New note type with checkboxes
- **Completed items** - Strike-through/check off
- **Drag & drop** - Reorder items
### v1.5.0 - Internationalization
- **Multi-language** - German + English UI
- **Language selection** - Selectable in settings
- **Full translation** - All strings in both languages
### v1.6.0 - Modern APIs
- **Replace LocalBroadcastManager** - Use SharedFlow instead
- **PackageInfo Flags** - Use PackageInfoFlags.of()
- **Complexity refactoring** - Split long functions
### v2.0.0 - Legacy Cleanup
- **Remove deprecated Activities** - Replace with Compose equivalents
- **LocalBroadcastManager → SharedFlow** - Modern event architecture
- **WebDavSyncService split** - SyncOrchestrator, NoteUploader, NoteDownloader
---
@@ -305,4 +347,4 @@ A: Yes! Download the APK directly from GitHub or use F-Droid.
---
**Last update:** v1.3.2 (2026-01-10)
**Last update:** v1.8.1 (2026-02-11)

146
docs/SELF_SIGNED_SSL.de.md Normal file
View 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)

View File

@@ -60,28 +60,91 @@
---
## v1.7.0 - Staggered Grid Layout
## v1.7.0 - Grid View, WiFi-Only & VPN ✅
> **Status:** Geplant 📝
> **Status:** Released 🎉 (Januar 2026)
### 🎨 Adaptives Layout
### 🎨 Grid Layout
- **Staggered Grid** - Pinterest-artiges Layout mit `LazyVerticalStaggeredGrid`
- **Intelligente Größen** - Kleine Notizen (kurzer Text, wenige Checklist-Items) kompakt dargestellt
- **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht in Einstellungen wechseln
- **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
- **120 FPS optimiert** - Lazy Loading für flüssiges Scrollen bei vielen Notizen
- **Pinterest-artiges Staggered Grid** - Lückenfreies Layout mit dynamischen Vorschauzeilen
- **Layout-Umschalter** - Zwischen Listen- und Grid-Ansicht wechseln
- **Adaptive Spalten** - 2-3 Spalten basierend auf Bildschirmgröße
### 🔧 Server-Ordner Prüfung
### 📡 Sync-Verbesserungen
- **WebDAV Folder Check** - Prüft ob der Ordner auf dem Server existiert und beschreibbar ist
- **Bessere Fehlermeldungen** - Hilfreiche Hinweise bei Server-Problemen
- **Connection-Test Verbesserung** - Prüft Read/Write Permissions
- **WiFi-Only Sync Toggle** - Nur über WiFi synchronisieren
- **VPN-Unterstützung** - Sync funktioniert korrekt über VPN-Tunnels
- **Self-Signed SSL** - Dokumentation und Unterstützung für selbstsignierte Zertifikate
-**Server-Wechsel-Erkennung** - Alle Notizen auf PENDING zurückgesetzt bei URL-Änderung
### 🔧 Technische Verbesserungen
---
- **Code-Refactoring** - LargeClass Komponenten aufteilen (WebDavSyncService, SettingsActivity)
- **Verbesserte Progress-Dialoge** - Material Design 3 konform
## v1.7.1 - Android 9 Fix & VPN ✅
> **Status:** Released 🎉 (Februar 2026)
-**Android 9 Crash Fix** - `getForegroundInfo()` für WorkManager auf API 28 implementiert
-**VPN-Kompatibilität** - WiFi Socket-Binding erkennt Wireguard VPN-Interfaces
-**SafeSardineWrapper** - Saubere HTTP-Verbindungs-Bereinigung
---
## v1.7.2 - Timestamp & Löschungs-Fixes ✅
> **Status:** Released 🎉 (Februar 2026)
-**Server-mtime als Wahrheitsquelle** - Behebt Timestamp-Probleme mit externen Editoren
-**Deletion Tracker Mutex** - Thread-sichere Batch-Löschungen
-**ISO8601 Timezone-Parsing** - Multi-Format-Unterstützung
-**E-Tag Batch-Caching** - Performance-Verbesserung
-**Memory Leak Prävention** - SafeSardineWrapper mit Closeable
---
## v1.8.0 - Widgets, Sortierung & Erweiterter Sync ✅
> **Status:** Released 🎉 (Februar 2026)
### 📌 Homescreen-Widgets
-**Volles Jetpack Glance Framework** - 5 responsive Größenklassen
-**Interaktive Checklisten** - Checkboxen die zum Server synchronisieren
-**Material You Farben** - Dynamische Farben mit einstellbarer Opazität
-**Sperr-Umschalter** - Versehentliche Bearbeitungen verhindern
-**Konfigurations-Activity** - Notiz-Auswahl und Einstellungen
### 📊 Sortierung
-**Notiz-Sortierung** - Nach Titel, Änderungsdatum, Erstelldatum, Typ
-**Checklisten-Sortierung** - Manuell, alphabetisch, offene zuerst, erledigte zuletzt
-**Visuelle Trenner** - Zwischen offenen/erledigten Gruppen
-**Drag über Grenzen** - Auto-Toggle beim Überqueren des Trenners
### 🔄 Sync-Verbesserungen
-**Parallele Downloads** - Bis zu 5 gleichzeitig (konfigurierbar)
-**Server-Löschungs-Erkennung** - Erkennt auf anderen Clients gelöschte Notizen
-**Live Sync-Fortschritt** - Phasen-Anzeige mit Zählern
-**Sync-Status Legende** - Hilfe-Dialog für alle Sync-Icons
### ✨ UX
-**Post-Update Changelog** - Zeigt lokalisierten Changelog nach Update
-**Grid als Standard** - Neue Installationen starten im Grid-Modus
-**Toast → Banner Migration** - Einheitliches Benachrichtigungssystem
---
## v1.8.1 - Bugfix & Polish ✅
> **Status:** Released 🎉 (Februar 2026)
-**Checklisten-Sortierung Persistenz** - Sortier-Option korrekt wiederhergestellt
-**Widget Scroll Fix** - Scroll funktioniert auf Standard 3×2 Widget-Größe
-**Widget Checklisten-Sortierung** - Widgets übernehmen gespeicherte Sortier-Option
-**Drag Cross-Boundary** - Drag & Drop über Checked/Unchecked-Trenner
-**Sync Rate-Limiting** - Globaler 30s Cooldown zwischen Auto-Syncs
-**Detekt: 0 Issues** - Alle 12 Findings behoben
---
@@ -110,7 +173,6 @@
### 🎨 UI Features
- **Widget** - Schnellzugriff vom Homescreen
- **Kategorien/Tags** - Notizen organisieren
- **Suche** - Volltextsuche in Notizen

View File

@@ -60,28 +60,91 @@
---
## v1.7.0 - Staggered Grid Layout
## v1.7.0 - Grid View, WiFi-Only & VPN ✅
> **Status:** Planned 📝
> **Status:** Released 🎉 (January 2026)
### 🎨 Adaptive Layout
### 🎨 Grid Layout
- **Staggered Grid** - Pinterest-style layout using `LazyVerticalStaggeredGrid`
- **Smart sizing** - Small notes (short text, few checklist items) displayed compactly
- **Layout toggle** - Switch between List and Grid view in settings
- **Adaptive columns** - 2-3 columns based on screen size
- **120 FPS optimized** - Lazy loading for smooth scrolling with many notes
- **Pinterest-style staggered grid** - Gapless layout with dynamic preview lines
- **Layout toggle** - Switch between list and grid in settings
- **Adaptive columns** - 2-3 columns based on screen size
### 🔧 Server Folder Check
### 📡 Sync Improvements
- **WebDAV folder check** - Checks if folder exists and is writable on server
- **Better error messages** - Helpful hints for server problems
- **Connection test improvement** - Checks read/write permissions
- **WiFi-only sync toggle** - Sync only when connected to WiFi
- **VPN support** - Sync works correctly through VPN tunnels
- **Self-signed SSL** - Documentation and support for self-signed certificates
-**Server change detection** - All notes reset to PENDING when server URL changes
### 🔧 Technical Improvements
---
- **Code refactoring** - Split LargeClass components (WebDavSyncService, SettingsActivity)
- **Improved progress dialogs** - Material Design 3 compliant
## v1.7.1 - Android 9 Fix & VPN ✅
> **Status:** Released 🎉 (February 2026)
-**Android 9 crash fix** - Implemented `getForegroundInfo()` for WorkManager on API 28
-**VPN compatibility** - WiFi socket binding detects Wireguard VPN interfaces
-**SafeSardineWrapper** - Proper HTTP connection cleanup
---
## v1.7.2 - Timestamp & Deletion Fixes ✅
> **Status:** Released 🎉 (February 2026)
-**Server mtime as source of truth** - Fixes external editor timestamp issues
-**Deletion tracker mutex** - Thread-safe batch deletes
-**ISO8601 timezone parsing** - Multi-format support
-**E-Tag batch caching** - Performance improvement
-**Memory leak prevention** - SafeSardineWrapper with Closeable
---
## v1.8.0 - Widgets, Sorting & Advanced Sync ✅
> **Status:** Released 🎉 (February 2026)
### 📌 Homescreen Widgets
-**Full Jetpack Glance framework** - 5 responsive size classes
-**Interactive checklists** - Checkboxes that sync to server
-**Material You colors** - Dynamic colors with configurable opacity
-**Lock toggle** - Prevent accidental edits
-**Configuration activity** - Note selection and settings
### 📊 Sorting
-**Note sorting** - By title, date modified, date created, type
-**Checklist sorting** - Manual, alphabetical, unchecked first, checked last
-**Visual separators** - Between unchecked/checked groups
-**Drag across boundaries** - Auto-toggle state on cross-boundary drag
### 🔄 Sync Improvements
-**Parallel downloads** - Up to 5 simultaneous (configurable)
-**Server deletion detection** - Detects notes deleted on other clients
-**Live sync progress** - Phase indicators with counters
-**Sync status legend** - Help dialog explaining all sync icons
### ✨ UX
-**Post-update changelog** - Shows localized changelog on first launch after update
-**Grid as default** - New installations default to grid view
-**Toast → Banner migration** - Unified notification system
---
## v1.8.1 - Bugfix & Polish ✅
> **Status:** Released 🎉 (February 2026)
-**Checklist sort persistence** - Sort option correctly restored when reopening
-**Widget scroll fix** - Scroll works on standard 3×2 widget size
-**Widget checklist sorting** - Widgets apply saved sort option
-**Drag cross-boundary** - Drag & drop across checked/unchecked separator
-**Sync rate-limiting** - Global 30s cooldown between auto-syncs
-**Detekt: 0 issues** - All 12 findings resolved
---
@@ -110,7 +173,6 @@
### 🎨 UI Features
- **Widget** - Quick access from homescreen
- **Categories/Tags** - Organize notes
- **Search** - Full-text search in notes

View File

@@ -5,34 +5,50 @@ Diese Verzeichnisstruktur enthält alle Metadaten für die F-Droid-Veröffentlic
## Struktur
```
fastlane/metadata/android/de-DE/
├── title.txt # App-Name (max 50 Zeichen)
├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
├── full_description.txt # Vollständige Beschreibung (max 4000 Zeichen)
fastlane/metadata/android/
├── de-DE/ # Deutsche Lokalisierung (primär)
│ ├── title.txt # App-Name (max 50 Zeichen)
│ ├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
│ ├── full_description.txt # Vollständige Beschreibung (max 4000 Zeichen)
│ ├── changelogs/
│ │ ├── 1.txt ... 21.txt # Changelogs pro versionCode (max 500 Zeichen!)
│ └── images/
│ └── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
│ ├── 1.png ... 5.png
└── en-US/ # Englische Lokalisierung
├── title.txt
├── short_description.txt
├── full_description.txt
├── changelogs/
── 1.txt # Changelog für Version 1
── 1.txt ... 21.txt
└── images/
└── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
├── 1.png # Hauptansicht (Notizliste)
├── 2.png # Notiz-Editor
├── 3.png # Settings
└── 4.png # Empty State
└── phoneScreenshots/
```
## Wichtige Limits
| Feld | Max. Länge | Hinweis |
|------|-----------|---------|
| `title.txt` | 50 Zeichen | App-Name |
| `short_description.txt` | 80 Zeichen | Kurzbeschreibung |
| `full_description.txt` | 4000 Zeichen | Vollständige Beschreibung |
| `changelogs/*.txt` | **500 Bytes** | Pro versionCode, **Bytes nicht Zeichen!** |
> **Achtung:** Changelogs werden in **Bytes** gemessen! UTF-8 Umlaute (ä, ö, ü) zählen als 2 Bytes.
## Screenshots erstellen
Verwende einen Android Emulator oder physisches Gerät mit:
Verwende ein physisches Gerät oder Emulator mit:
- Material You Theme aktiviert
- Deutsche Sprache
- Deutsche/Englische Sprache je nach Locale
- Screenshots in hoher Auflösung (1080x2400 empfohlen)
### Screenshot-Reihenfolge:
1. **Notizliste** - Mit mehreren Beispiel-Notizen, Sync-Status sichtbar
2. **Editor** - Zeige eine bearbeitete Notiz mit Titel und Inhalt
3. **Settings** - Server-Konfiguration mit erfolgreichem Server-Status
4. **Empty State** - Schöne leere Ansicht mit Material 3 Card
## F-Droid Build-Konfiguration
Die App verwendet den `fdroid` Build-Flavor ohne proprietäre Dependencies.
Siehe `build.gradle.kts` für Details.
Siehe `android/app/build.gradle.kts` für Details.
## Aktuelle Version
- **versionName:** 1.8.1
- **versionCode:** 21

View File

@@ -1,5 +1,5 @@
Unter der Haube haben wir ordentlich aufgeraumt:
Unter der Haube haben wir ordentlich aufgeräumt:
- Verbesserte Sync-Performance durch optimierten Code
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
- Speichereffizientere Datenverarbeitung
- Datenschutz-Hinweis fur Datei-Logging hinzugefugt
- Datenschutz-Hinweis für Datei-Logging hinzugefügt

View 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

View File

@@ -1,12 +1,12 @@
v1.2.2 - Rückwärtskompatibilität für v1.2.0 User
Kritische Fehlerbehebung
• Server-Wiederherstellung findet jetzt ALLE Notizen (Root + /notes/)
• User die von v1.2.0 upgraden verlieren keine Daten mehr
• Alte Notizen aus Root-Ordner werden beim Restore gefunden
• Server-Restore findet jetzt ALLE Notizen (Root + /notes/)
• Upgrade von v1.2.0 ohne Datenverlust
• Alte Notizen aus Root-Ordner werden gefunden
Technische Details
• Dual-Mode Download nur bei Server-Restore aktiv
• Normale Syncs bleiben schnell (scannen nur /notes/)
• Normale Syncs bleiben schnell (nur /notes/)
• Automatische Deduplication verhindert Duplikate
• Sanfte Migration: Neue Uploads gehen in /notes/, alte bleiben lesbar
• Sanfte Migration: Uploads /notes/, alte bleiben lesbar

View File

@@ -1,29 +1,31 @@
Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisation und modernen Features.
Hauptfunktionen:
• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop)
NEU: Raster-Ansicht (Grid View) für Notizen
• Text-Notizen und Checklisten (Tap-to-Check, Drag & Drop, Sortierung)
• Raster- und Listen-Ansicht mit Notizfarben
• Homescreen-Widgets (Quick-Note, Checkliste mit interaktiven Checkboxen)
• Multi-Device Sync (Handy, Tablet, Desktop)
• WebDAV-Synchronisation mit eigenem Server (Nextcloud, ownCloud, etc.)
• Markdown-Export und Import für Desktop-Editoren (Obsidian, VS Code)
NEU: WiFi-only Sync, VPN-Unterstützung, Verschlüsselung für lokale Backups
• WiFi-only Sync, VPN-Unterstützung, parallele Downloads
• Konfigurierbare Sync-Trigger: onSave, onResume, WiFi, periodisch, Boot
• Komplett offline nutzbar
• Keine Werbung, keine Tracker
• Komplett offline nutzbar keine Werbung, keine Tracker
Datenschutz & Sicherheit:
• Alle Daten bleiben bei dir keine Cloud, keine Tracking-Bibliotheken
• Unterstützung für selbstsignierte SSL-Zertifikate (Self-signed SSL)
SHA-256 Hash des Signaturzertifikats in App und Releases sichtbar
• Unterstützung für selbstsignierte SSL-Zertifikate
Verschlüsselte lokale Backups
Synchronisation:
Automatisch oder manuell, optimierte Performance, periodischer Sync optional
Intelligente Konfliktlösung, Lösch-Tracking, Batch-Aktionen
Parallele Downloads (bis zu 5 gleichzeitig)
Live Sync-Fortschritt mit Phasen-Anzeige
• Intelligente Konfliktlösung, Server-Löschungs-Erkennung
• Post-Update Changelog-Dialog
UI & Design:
• Moderne Jetpack Compose Oberfläche
• Material Design 3, Dynamic Colors, Dark Mode
Animationen und Live Sync-Status
Notiz- und Checklisten-Sortierung (Titel, Datum, Farbe, alphabetisch)
Mehrsprachig:
• Deutsch und Englisch, automatische Erkennung, App-Sprachauswahl

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 199 KiB

View 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

View 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

View 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

View File

@@ -1,29 +1,31 @@
Simple Notes Sync is a minimalist note-taking app with WebDAV sync and modern features.
Key Features:
• Text notes and checklists (tap-to-check, drag & drop)
NEW: Grid view for notes
• Text notes and checklists (tap-to-check, drag & drop, sorting)
Grid and list view with note color support
• Homescreen widgets (quick-note, checklist with interactive checkboxes)
• Multi-device sync (phone, tablet, desktop)
• WebDAV sync with your own server (Nextcloud, ownCloud, etc.)
• Markdown export/import for desktop editors (Obsidian, VS Code)
NEW: WiFi-only sync, VPN support, encryption for local backups
• WiFi-only sync, VPN support, parallel downloads
• Configurable sync triggers: onSave, onResume, WiFi, periodic, boot
• Fully usable offline
• No ads, no trackers
• Fully usable offline no ads, no trackers
Privacy & Security:
• Your data stays with you no cloud, no tracking libraries
• Support for self-signed SSL certificates
SHA-256 hash of signing certificate shown in app and releases
Encrypted local backups
Synchronization:
Automatic or manual, optimized performance, optional periodic sync
Smart conflict resolution, deletion tracking, batch actions
Parallel downloads (up to 5 simultaneous)
Live sync progress with phase indicators
• Smart conflict resolution, server deletion detection
• Post-update changelog dialog
UI & Design:
• Modern Jetpack Compose interface
• Material Design 3, dynamic colors, dark mode
Animations and live sync status
Note & checklist sorting (title, date, color, alphabetical)
Multilingual:
• English and German, automatic detection, in-app language selector

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 199 KiB