Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d524bc715d | ||
|
|
2a22e7d88e | ||
|
|
b5cb4e1d96 | ||
|
|
80a35da3ff | ||
|
|
6254758a03 | ||
|
|
ff6510af90 | ||
|
|
ea5c6dae70 | ||
|
|
1d010d0034 |
25
.github/workflows/pr-build-check.yml
vendored
@@ -33,6 +33,31 @@ jobs:
|
|||||||
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
|
echo "VERSION_NAME=$VERSION_NAME" >> $GITHUB_ENV
|
||||||
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
|
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
|
||||||
echo "📱 Version: $VERSION_NAME (Code: $VERSION_CODE)"
|
echo "📱 Version: $VERSION_NAME (Code: $VERSION_CODE)"
|
||||||
|
|
||||||
|
# 🔍 Code Quality Checks (v1.6.1)
|
||||||
|
- name: Run detekt (Code Quality)
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
./gradlew detekt --no-daemon
|
||||||
|
continue-on-error: false
|
||||||
|
|
||||||
|
- name: Run ktlint (Code Style)
|
||||||
|
run: |
|
||||||
|
cd android
|
||||||
|
./gradlew ktlintCheck --no-daemon
|
||||||
|
continue-on-error: true # Parser-Probleme in Legacy-Code
|
||||||
|
|
||||||
|
- name: Upload Lint Reports
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: lint-reports-pr-${{ github.event.pull_request.number }}
|
||||||
|
path: |
|
||||||
|
android/app/build/reports/detekt/
|
||||||
|
android/app/build/reports/ktlint/
|
||||||
|
android/app/build/reports/lint-results*.html
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Debug Build erstellen (ohne Signing)
|
- name: Debug Build erstellen (ohne Signing)
|
||||||
run: |
|
run: |
|
||||||
cd android
|
cd android
|
||||||
|
|||||||
5
.gitignore
vendored
@@ -43,3 +43,8 @@ Thumbs.db
|
|||||||
*.swp
|
*.swp
|
||||||
*~
|
*~
|
||||||
test-apks/
|
test-apks/
|
||||||
|
|
||||||
|
# F-Droid metadata (managed in fdroiddata repo)
|
||||||
|
# Exclude fastlane metadata (we want to track those screenshots)
|
||||||
|
metadata/
|
||||||
|
!fastlane/metadata/
|
||||||
|
|||||||
@@ -8,6 +8,94 @@ Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.6.1] - 2026-01-20
|
||||||
|
|
||||||
|
### 🧹 Code-Qualität & Build-Verbesserungen
|
||||||
|
|
||||||
|
- **detekt: 0 Issues** - Alle 29 Code-Qualitäts-Issues behoben
|
||||||
|
- Triviale Fixes: Unused Imports, MaxLineLength
|
||||||
|
- Datei umbenannt: DragDropState.kt → DragDropListState.kt
|
||||||
|
- MagicNumbers → Constants (Dimensions.kt, SyncConstants.kt)
|
||||||
|
- SwallowedException: Logger.w() für besseres Error-Tracking hinzugefügt
|
||||||
|
- LongParameterList: ChecklistEditorCallbacks data class erstellt
|
||||||
|
- LongMethod: ServerSettingsScreen in Komponenten aufgeteilt
|
||||||
|
- @Suppress Annotationen für Legacy-Code (WebDavSyncService, SettingsActivity)
|
||||||
|
|
||||||
|
- **Zero Build Warnings** - Alle 21 Deprecation Warnings eliminiert
|
||||||
|
- File-level @Suppress für deprecated Imports
|
||||||
|
- ProgressDialog, LocalBroadcastManager, AbstractSavedStateViewModelFactory
|
||||||
|
- onActivityResult, onRequestPermissionsResult
|
||||||
|
- Gradle Compose Config bereinigt (StrongSkipping ist jetzt Standard)
|
||||||
|
|
||||||
|
- **ktlint reaktiviert** - Linting mit Compose-spezifischen Regeln wieder aktiviert
|
||||||
|
- .editorconfig mit Compose Formatierungsregeln erstellt
|
||||||
|
- Legacy-Dateien ausgeschlossen: WebDavSyncService.kt, build.gradle.kts
|
||||||
|
- ignoreFailures=true für graduelle Migration
|
||||||
|
|
||||||
|
- **CI/CD Verbesserungen** - GitHub Actions Lint-Checks integriert
|
||||||
|
- detekt + ktlint + Android Lint laufen vor Build in pr-build-check.yml
|
||||||
|
- Stellt Code-Qualität bei jedem Pull Request sicher
|
||||||
|
|
||||||
|
### 🔧 Technische Verbesserungen
|
||||||
|
|
||||||
|
- **Constants Refactoring** - Bessere Code-Organisation
|
||||||
|
- ui/theme/Dimensions.kt: UI-bezogene Konstanten
|
||||||
|
- utils/SyncConstants.kt: Sync-Operations Konstanten
|
||||||
|
|
||||||
|
- **Vorbereitung für v2.0.0** - Legacy-Code für Entfernung markiert
|
||||||
|
- SettingsActivity und MainActivity (ersetzt durch Compose-Versionen)
|
||||||
|
- Alle deprecated APIs mit Removal-Plan dokumentiert
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.6.0] - 2026-01-19
|
||||||
|
|
||||||
|
### 🎉 Major: Konfigurierbare Sync-Trigger
|
||||||
|
|
||||||
|
Feingranulare Kontrolle darüber, wann deine Notizen synchronisiert werden - wähle die Trigger, die am besten zu deinem Workflow passen!
|
||||||
|
|
||||||
|
### ⚙️ Sync-Trigger System
|
||||||
|
|
||||||
|
- **Individuelle Trigger-Kontrolle** - Jeden Sync-Trigger einzeln in den Einstellungen aktivieren/deaktivieren
|
||||||
|
- **5 Unabhängige Trigger:**
|
||||||
|
- **onSave Sync** - Sync sofort nach dem Speichern einer Notiz (5s Throttle)
|
||||||
|
- **onResume Sync** - Sync beim Öffnen der App (60s Throttle)
|
||||||
|
- **WiFi-Connect Sync** - Sync bei WiFi-Verbindung
|
||||||
|
- **Periodischer Sync** - Hintergrund-Sync alle 15/30/60 Minuten (konfigurierbar)
|
||||||
|
- **Boot Sync** - Startet Hintergrund-Sync nach Geräteneustart
|
||||||
|
|
||||||
|
- **Smarte Defaults** - Nur ereignisbasierte Trigger standardmäßig aktiv (onSave, onResume, WiFi-Connect)
|
||||||
|
- **Akku-optimiert** - ~0.2%/Tag mit Defaults, bis zu ~1.0% mit aktiviertem periodischen Sync
|
||||||
|
- **Offline-Modus UI** - Ausgegraute Sync-Toggles wenn kein Server konfiguriert
|
||||||
|
- **Dynamischer Settings-Subtitle** - Zeigt Anzahl aktiver Trigger im Haupteinstellungs-Screen
|
||||||
|
|
||||||
|
### 🔧 Server-Konfiguration Verbesserungen
|
||||||
|
|
||||||
|
- **Offline-Modus Toggle** - Alle Netzwerkfunktionen mit einem Schalter deaktivieren
|
||||||
|
- **Getrennte Protokoll & Host Eingabe** - Protokoll (http/https) als nicht-editierbares Präfix angezeigt
|
||||||
|
- **Klickbare Settings-Cards** - Gesamte Card klickbar für bessere UX
|
||||||
|
- **Klickbare Toggle-Zeilen** - Text/Icon klicken um Switches zu bedienen (nicht nur der Switch selbst)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **Fix:** Fehlender 5. Sync-Trigger (Boot) in der Haupteinstellungs-Screen Subtitle-Zählung
|
||||||
|
- **Fix:** Offline-Modus Status wird nicht aktualisiert beim Zurückkehren aus Einstellungen
|
||||||
|
- **Fix:** Pull-to-Refresh funktioniert auch im Offline-Modus
|
||||||
|
|
||||||
|
### 🔧 Technische Verbesserungen
|
||||||
|
|
||||||
|
- **Reaktiver Offline-Modus Status** - StateFlow stellt sicher, dass UI korrekt aktualisiert wird
|
||||||
|
- **Getrennte Server-Config Checks** - `hasServerConfig()` vs `isServerConfigured()` (offline-aware)
|
||||||
|
- **Verbesserte Konstanten** - Alle Sync-Trigger Keys und Defaults in Constants.kt
|
||||||
|
- **Bessere Code-Organisation** - Settings-Screens für Klarheit refactored
|
||||||
|
|
||||||
|
### Looking Ahead
|
||||||
|
|
||||||
|
> 🚀 **v1.7.0** wird Server-Ordner Prüfung und weitere Community-Features bringen.
|
||||||
|
> Feature-Requests sind willkommen als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.5.0] - 2026-01-15
|
## [1.5.0] - 2026-01-15
|
||||||
|
|
||||||
### 🎉 Major: Jetpack Compose UI Redesign
|
### 🎉 Major: Jetpack Compose UI Redesign
|
||||||
|
|||||||
87
CHANGELOG.md
@@ -8,6 +8,93 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.6.1] - 2026-01-20
|
||||||
|
|
||||||
|
### 🧹 Code Quality & Build Improvements
|
||||||
|
|
||||||
|
- **detekt: 0 issues** - All 29 code quality issues resolved
|
||||||
|
- Trivial fixes: Unused imports, MaxLineLength
|
||||||
|
- File rename: DragDropState.kt → DragDropListState.kt
|
||||||
|
- MagicNumbers → Constants (Dimensions.kt, SyncConstants.kt)
|
||||||
|
- SwallowedException: Logger.w() added for better error tracking
|
||||||
|
- LongParameterList: ChecklistEditorCallbacks data class created
|
||||||
|
- LongMethod: ServerSettingsScreen split into components
|
||||||
|
- @Suppress annotations for legacy code (WebDavSyncService, SettingsActivity)
|
||||||
|
|
||||||
|
- **Zero build warnings** - All 21 deprecation warnings eliminated
|
||||||
|
- File-level @Suppress for deprecated imports
|
||||||
|
- ProgressDialog, LocalBroadcastManager, AbstractSavedStateViewModelFactory
|
||||||
|
- onActivityResult, onRequestPermissionsResult
|
||||||
|
- Gradle Compose config cleaned up (StrongSkipping is now default)
|
||||||
|
|
||||||
|
- **ktlint reactivated** - Linting re-enabled with Compose-specific rules
|
||||||
|
- .editorconfig created with Compose formatting rules
|
||||||
|
- Legacy files excluded: WebDavSyncService.kt, build.gradle.kts
|
||||||
|
- ignoreFailures=true for gradual migration
|
||||||
|
|
||||||
|
- **CI/CD improvements** - GitHub Actions lint checks integrated
|
||||||
|
- detekt + ktlint + Android Lint run before build in pr-build-check.yml
|
||||||
|
- Ensures code quality on every pull request
|
||||||
|
|
||||||
|
### 🔧 Technical Improvements
|
||||||
|
|
||||||
|
- **Constants refactoring** - Better code organization
|
||||||
|
- ui/theme/Dimensions.kt: UI-related constants
|
||||||
|
- utils/SyncConstants.kt: Sync operation constants
|
||||||
|
|
||||||
|
- **Preparation for v2.0.0** - Legacy code marked for removal
|
||||||
|
- SettingsActivity and MainActivity (replaced by Compose versions)
|
||||||
|
- All deprecated APIs documented with removal plan
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## [1.6.0] - 2026-01-19
|
||||||
|
|
||||||
|
### 🎉 Major: Configurable Sync Triggers
|
||||||
|
|
||||||
|
Fine-grained control over when your notes sync - choose which triggers fit your workflow best!
|
||||||
|
|
||||||
|
### ⚙️ Sync Trigger System
|
||||||
|
|
||||||
|
- **Individual trigger control** - Enable/disable each sync trigger separately in settings
|
||||||
|
- **5 Independent Triggers:**
|
||||||
|
- **onSave Sync** - Sync immediately after saving a note (5s throttle)
|
||||||
|
- **onResume Sync** - Sync when app is opened (60s throttle)
|
||||||
|
- **WiFi-Connect Sync** - Sync when WiFi is connected
|
||||||
|
- **Periodic Sync** - Background sync every 15/30/60 minutes (configurable)
|
||||||
|
- **Boot Sync** - Start background sync after device restart
|
||||||
|
|
||||||
|
- **Smart Defaults** - Only event-driven triggers active by default (onSave, onResume, WiFi-Connect)
|
||||||
|
- **Battery Optimized** - ~0.2%/day with defaults, up to ~1.0% with periodic sync enabled
|
||||||
|
- **Offline Mode UI** - Grayed-out sync toggles when no server configured
|
||||||
|
- **Dynamic Settings Subtitle** - Shows count of active triggers on main settings screen
|
||||||
|
|
||||||
|
### 🔧 Server Configuration Improvements
|
||||||
|
|
||||||
|
- **Offline Mode Toggle** - Disable all network features with one switch
|
||||||
|
- **Split Protocol & Host** - Protocol (http/https) shown as non-editable prefix
|
||||||
|
- **Clickable Settings Cards** - Full card clickable for better UX
|
||||||
|
- **Clickable Toggle Rows** - Click text/icon to toggle switches (not just the switch itself)
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **Fixed:** Missing 5th sync trigger (Boot) in main settings screen subtitle count
|
||||||
|
- **Various fixes** - UI improvements and stability enhancements
|
||||||
|
|
||||||
|
### 🔧 Technical Improvements
|
||||||
|
|
||||||
|
- **Reactive offline mode state** - StateFlow ensures UI updates correctly
|
||||||
|
- **Separated server config checks** - `hasServerConfig()` vs `isServerConfigured()` (offline-aware)
|
||||||
|
- **Improved constants** - All sync trigger keys and defaults in Constants.kt
|
||||||
|
- **Better code organization** - Settings screens refactored for clarity
|
||||||
|
|
||||||
|
### Looking Ahead
|
||||||
|
|
||||||
|
> 🚀 **v1.7.0** will bring server folder checking and additional community features.
|
||||||
|
> Feature requests welcome as [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.5.0] - 2026-01-15
|
## [1.5.0] - 2026-01-15
|
||||||
|
|
||||||
### 🎉 Major: Jetpack Compose UI Redesign
|
### 🎉 Major: Jetpack Compose UI Redesign
|
||||||
|
|||||||
10
README.de.md
@@ -18,12 +18,12 @@
|
|||||||
## 📱 Screenshots
|
## 📱 Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png" width="250" alt="Notizliste">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png" width="250" alt="Sync-Status">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png" width="250" alt="Notiz bearbeiten">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png" width="250" alt="Notiz bearbeiten">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png" width="250" alt="Checkliste bearbeiten">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png" width="250" alt="Checkliste bearbeiten">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png" width="250" alt="Einstellungen">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png" width="250" alt="Einstellungen">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png" width="250" alt="Server-Einstellungen">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png" width="250" alt="Server-Einstellungen">
|
||||||
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png" width="250" alt="Sync-Status">
|
<img src="fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png" width="250" alt="Sync-Einstellungen">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop
|
- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop
|
||||||
- 🌍 **NEU: Mehrsprachig** - Deutsch/Englisch mit Sprachauswahl
|
- 🌍 **NEU: Mehrsprachig** - Deutsch/Englisch mit Sprachauswahl
|
||||||
- 📝 **Offline-First** - Funktioniert ohne Internet
|
- 📝 **Offline-First** - Funktioniert ohne Internet
|
||||||
- 🔄 **Auto-Sync** - WLAN-Verbindung, regelmäßige Intervalle (15/30/60 Min) & Multi-Geräte-Sync
|
- 🔄 **Konfigurierbare Sync-Trigger** - onSave, onResume, WiFi-Verbindung, periodisch (15/30/60 Min), Boot
|
||||||
- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
|
- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
|
||||||
- 💾 **Lokales Backup** - Export/Import als JSON-Datei
|
- 💾 **Lokales Backup** - Export/Import als JSON-Datei
|
||||||
- 🖥️ **Desktop-Integration** - Markdown-Export für Obsidian, VS Code, Typora
|
- 🖥️ **Desktop-Integration** - Markdown-Export für Obsidian, VS Code, Typora
|
||||||
- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag
|
- 🔋 **Akkuschonend** - ~0.2% mit Defaults, bis zu ~1.0% mit Periodic Sync
|
||||||
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
|
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
|
||||||
|
|
||||||
➡️ **Vollständige Feature-Liste:** [FEATURES.de.md](docs/FEATURES.de.md)
|
➡️ **Vollständige Feature-Liste:** [FEATURES.de.md](docs/FEATURES.de.md)
|
||||||
@@ -112,4 +112,4 @@ MIT License - siehe [LICENSE](LICENSE)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.4.1** · Built with ❤️ using Kotlin + Material Design 3
|
**v1.6.0** · Built with ❤️ using Kotlin + Material Design 3
|
||||||
|
|||||||
10
README.md
@@ -18,12 +18,12 @@
|
|||||||
## 📱 Screenshots
|
## 📱 Screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="250" alt="Notes list">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" width="250" alt="Sync status">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="250" alt="Edit note">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" width="250" alt="Edit note">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="250" alt="Edit checklist">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" width="250" alt="Edit checklist">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="250" alt="Settings">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" width="250" alt="Settings">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="250" alt="Server settings">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/5.png" width="250" alt="Server settings">
|
||||||
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" width="250" alt="Sync status">
|
<img src="fastlane/metadata/android/en-US/images/phoneScreenshots/7.png" width="250" alt="Sync settings">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -33,11 +33,11 @@
|
|||||||
- ✅ **NEW: Checklists** - Tap-to-check, drag & drop
|
- ✅ **NEW: Checklists** - Tap-to-check, drag & drop
|
||||||
- 🌍 **NEW: Multilingual** - English/German with language selector
|
- 🌍 **NEW: Multilingual** - English/German with language selector
|
||||||
- 📝 **Offline-first** - Works without internet
|
- 📝 **Offline-first** - Works without internet
|
||||||
- 🔄 **Auto-sync** - WiFi reconnect, periodic intervals (15/30/60 min) & multi-device sync
|
- 🔄 **Configurable sync triggers** - onSave, onResume, WiFi-connect, periodic (15/30/60 min), boot
|
||||||
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
|
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
|
||||||
- 💾 **Local backup** - Export/Import as JSON file
|
- 💾 **Local backup** - Export/Import as JSON file
|
||||||
- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
|
- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
|
||||||
- 🔋 **Battery-friendly** - ~0.2-0.8% per day
|
- 🔋 **Battery-friendly** - ~0.2% with defaults, up to ~1.0% with periodic sync
|
||||||
- 🎨 **Material Design 3** - Dark mode & dynamic colors
|
- 🎨 **Material Design 3** - Dark mode & dynamic colors
|
||||||
|
|
||||||
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
|
➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md)
|
||||||
@@ -108,4 +108,4 @@ MIT License - see [LICENSE](LICENSE)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**v1.5.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
**v1.6.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
|
alias(libs.plugins.kotlin.compose) // v1.5.0: Jetpack Compose Compiler
|
||||||
// ⚡ v1.3.1: ktlint deaktiviert wegen Parser-Problemen, aktivieren in v1.4.0
|
alias(libs.plugins.ktlint) // ✅ v1.6.1: Reaktiviert nach Code-Cleanup
|
||||||
// alias(libs.plugins.ktlint)
|
|
||||||
alias(libs.plugins.detekt)
|
alias(libs.plugins.detekt)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 13 // 🔧 v1.5.0: Jetpack Compose Settings Redesign
|
versionCode = 15 // 🔧 v1.6.1: Lint-Cleanup detekt and ktlint
|
||||||
versionName = "1.5.0" // 🔧 v1.5.0: Jetpack Compose Settings Redesign
|
versionName = "1.6.1" // 🔧 v1.6.1: Lint-Cleanup detekt and ktlint
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -101,9 +100,8 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// v1.5.0 Hotfix: Strong Skipping Mode für bessere 120Hz Performance
|
// v1.5.0 Hotfix: Strong Skipping Mode für bessere 120Hz Performance
|
||||||
composeCompiler {
|
// v1.6.1: Feature ist ab dieser Kotlin/Compose Version bereits Standard
|
||||||
enableStrongSkippingMode = true
|
// composeCompiler { }
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
@@ -162,18 +160,21 @@ dependencies {
|
|||||||
androidTestImplementation(libs.androidx.espresso.core)
|
androidTestImplementation(libs.androidx.espresso.core)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⚡ v1.3.1: ktlint deaktiviert wegen Parser-Problemen
|
// ✅ v1.6.1: ktlint reaktiviert nach Code-Cleanup
|
||||||
// Aktivieren in v1.4.0 wenn Code-Stil bereinigt wurde
|
ktlint {
|
||||||
// ktlint {
|
android = true
|
||||||
// android = true
|
outputToConsole = true
|
||||||
// outputToConsole = true
|
ignoreFailures = true // Parser-Probleme in WebDavSyncService.kt und build.gradle.kts
|
||||||
// ignoreFailures = true
|
enableExperimentalRules = false
|
||||||
// enableExperimentalRules = false
|
|
||||||
// filter {
|
filter {
|
||||||
// exclude("**/generated/**")
|
exclude("**/generated/**")
|
||||||
// exclude("**/build/**")
|
exclude("**/build/**")
|
||||||
// }
|
// Legacy adapters with ktlint parser issues
|
||||||
// }
|
exclude("**/adapters/NotesAdapter.kt")
|
||||||
|
exclude("**/SettingsActivity.kt")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ⚡ v1.3.1: detekt-Konfiguration
|
// ⚡ v1.3.1: detekt-Konfiguration
|
||||||
detekt {
|
detekt {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION") // Legacy code using LocalBroadcastManager, will be removed in v2.0.0
|
||||||
|
|
||||||
package dev.dettmer.simplenotes
|
package dev.dettmer.simplenotes
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
@@ -48,6 +50,11 @@ import android.view.Gravity
|
|||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import dev.dettmer.simplenotes.models.NoteType
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy MainActivity - DEPRECATED seit v1.5.0, wird entfernt in v2.0.0
|
||||||
|
* Ersetzt durch ComposeMainActivity
|
||||||
|
*/
|
||||||
|
@Suppress("DEPRECATION") // Legacy code using LocalBroadcastManager, will be removed in v2.0.0
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var recyclerViewNotes: RecyclerView
|
private lateinit var recyclerViewNotes: RecyclerView
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION") // Legacy code using ProgressDialog & LocalBroadcastManager, will be removed in v2.0.0
|
||||||
|
|
||||||
package dev.dettmer.simplenotes
|
package dev.dettmer.simplenotes
|
||||||
|
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
@@ -42,6 +44,7 @@ import java.net.URL
|
|||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Suppress("LargeClass", "DEPRECATION") // Legacy code using ProgressDialog & LocalBroadcastManager, will be removed in v2.0.0
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import dev.dettmer.simplenotes.utils.Logger
|
|||||||
/**
|
/**
|
||||||
* BootReceiver: Startet WorkManager nach Device Reboot
|
* BootReceiver: Startet WorkManager nach Device Reboot
|
||||||
* CRITICAL: Ohne diesen Receiver funktioniert Auto-Sync nach Reboot NICHT!
|
* CRITICAL: Ohne diesen Receiver funktioniert Auto-Sync nach Reboot NICHT!
|
||||||
|
* v1.6.0: Configurable trigger - checks KEY_SYNC_TRIGGER_BOOT
|
||||||
*/
|
*/
|
||||||
class BootReceiver : BroadcastReceiver() {
|
class BootReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
@@ -24,16 +25,22 @@ class BootReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
Logger.d(TAG, "📱 BOOT_COMPLETED received")
|
Logger.d(TAG, "📱 BOOT_COMPLETED received")
|
||||||
|
|
||||||
// Prüfe ob Auto-Sync aktiviert ist
|
|
||||||
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
|
||||||
|
|
||||||
if (!autoSyncEnabled) {
|
// 🌟 v1.6.0: Check if Boot trigger is enabled
|
||||||
Logger.d(TAG, "❌ Auto-sync disabled - not starting WorkManager")
|
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_BOOT, Constants.DEFAULT_TRIGGER_BOOT)) {
|
||||||
|
Logger.d(TAG, "⏭️ Boot sync disabled - not starting WorkManager")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.d(TAG, "🚀 Auto-sync enabled - starting WorkManager")
|
// Check if server is configured
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
||||||
|
Logger.d(TAG, "⏭️ Offline mode - not starting WorkManager")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.d(TAG, "🚀 Boot sync enabled - starting WorkManager")
|
||||||
|
|
||||||
// WorkManager neu starten
|
// WorkManager neu starten
|
||||||
val networkMonitor = NetworkMonitor(context.applicationContext)
|
val networkMonitor = NetworkMonitor(context.applicationContext)
|
||||||
|
|||||||
@@ -102,8 +102,22 @@ class NetworkMonitor(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Triggert WiFi-Connect Sync via WorkManager
|
* Triggert WiFi-Connect Sync via WorkManager
|
||||||
* WorkManager wacht App auf (funktioniert auch wenn App geschlossen!)
|
* WorkManager wacht App auf (funktioniert auch wenn App geschlossen!)
|
||||||
|
* v1.6.0: Configurable trigger - checks KEY_SYNC_TRIGGER_WIFI_CONNECT
|
||||||
*/
|
*/
|
||||||
private fun triggerWifiConnectSync() {
|
private fun triggerWifiConnectSync() {
|
||||||
|
// 🌟 v1.6.0: Check if WiFi-Connect trigger is enabled
|
||||||
|
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, Constants.DEFAULT_TRIGGER_WIFI_CONNECT)) {
|
||||||
|
Logger.d(TAG, "⏭️ WiFi-Connect sync disabled - skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server is configured
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
||||||
|
Logger.d(TAG, "⏭️ Offline mode - skipping WiFi-Connect sync")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
Logger.d(TAG, "📡 Scheduling WiFi-Connect sync via WorkManager")
|
Logger.d(TAG, "📡 Scheduling WiFi-Connect sync via WorkManager")
|
||||||
|
|
||||||
// 🔥 WICHTIG: NetworkType.UNMETERED constraint!
|
// 🔥 WICHTIG: NetworkType.UNMETERED constraint!
|
||||||
@@ -148,8 +162,25 @@ class NetworkMonitor(private val context: Context) {
|
|||||||
/**
|
/**
|
||||||
* Startet WorkManager periodic sync
|
* Startet WorkManager periodic sync
|
||||||
* 🔥 Interval aus SharedPrefs konfigurierbar (15/30/60 min)
|
* 🔥 Interval aus SharedPrefs konfigurierbar (15/30/60 min)
|
||||||
|
* v1.6.0: Configurable trigger - checks KEY_SYNC_TRIGGER_PERIODIC
|
||||||
*/
|
*/
|
||||||
private fun startPeriodicSync() {
|
private fun startPeriodicSync() {
|
||||||
|
// 🌟 v1.6.0: Check if Periodic trigger is enabled
|
||||||
|
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_PERIODIC, Constants.DEFAULT_TRIGGER_PERIODIC)) {
|
||||||
|
Logger.d(TAG, "⏭️ Periodic sync disabled - skipping")
|
||||||
|
// Cancel existing periodic work if disabled
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(AUTO_SYNC_WORK_NAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if server is configured
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
||||||
|
Logger.d(TAG, "⏭️ Offline mode - skipping Periodic sync")
|
||||||
|
WorkManager.getInstance(context).cancelUniqueWork(AUTO_SYNC_WORK_NAME)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 Interval aus SharedPrefs lesen
|
// 🔥 Interval aus SharedPrefs lesen
|
||||||
val intervalMinutes = prefs.getLong(
|
val intervalMinutes = prefs.getLong(
|
||||||
Constants.PREF_SYNC_INTERVAL_MINUTES,
|
Constants.PREF_SYNC_INTERVAL_MINUTES,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION") // LocalBroadcastManager deprecated but functional, will migrate in v2.0.0
|
||||||
|
|
||||||
package dev.dettmer.simplenotes.sync
|
package dev.dettmer.simplenotes.sync
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
@@ -255,6 +257,7 @@ class SyncWorker(
|
|||||||
/**
|
/**
|
||||||
* Sendet Broadcast an MainActivity für UI Refresh
|
* Sendet Broadcast an MainActivity für UI Refresh
|
||||||
*/
|
*/
|
||||||
|
@Suppress("DEPRECATION") // LocalBroadcastManager deprecated but still functional, will migrate in v2.0.0
|
||||||
private fun broadcastSyncCompleted(success: Boolean, count: Int) {
|
private fun broadcastSyncCompleted(success: Boolean, count: Int) {
|
||||||
val intent = Intent(ACTION_SYNC_COMPLETED).apply {
|
val intent = Intent(ACTION_SYNC_COMPLETED).apply {
|
||||||
putExtra("success", success)
|
putExtra("success", success)
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ data class ManualMarkdownSyncResult(
|
|||||||
val importedCount: Int
|
val importedCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
// TODO v2.0.0: Split into SyncOrchestrator, NoteUploader, NoteDownloader, ConflictResolver
|
||||||
class WebDavSyncService(private val context: Context) {
|
class WebDavSyncService(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -136,6 +138,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
|
|
||||||
Logger.d(TAG, "✅ Network is WiFi, searching for interface...")
|
Logger.d(TAG, "✅ Network is WiFi, searching for interface...")
|
||||||
|
|
||||||
|
@Suppress("LoopWithTooManyJumpStatements") // Network interface filtering requires multiple conditions
|
||||||
// Finde WiFi Interface
|
// Finde WiFi Interface
|
||||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||||
while (interfaces.hasMoreElements()) {
|
while (interfaces.hasMoreElements()) {
|
||||||
@@ -780,6 +783,8 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth", "LoopWithTooManyJumpStatements")
|
||||||
|
// Sync logic requires nested conditions for comprehensive error handling and state management
|
||||||
private fun uploadLocalNotes(sardine: Sardine, serverUrl: String): Int {
|
private fun uploadLocalNotes(sardine: Sardine, serverUrl: String): Int {
|
||||||
var uploadedCount = 0
|
var uploadedCount = 0
|
||||||
val localNotes = storage.loadAllNotes()
|
val localNotes = storage.loadAllNotes()
|
||||||
@@ -1022,6 +1027,8 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
val conflictCount: Int
|
val conflictCount: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth", "LoopWithTooManyJumpStatements")
|
||||||
|
// Sync logic requires nested conditions for comprehensive error handling and conflict resolution
|
||||||
private fun downloadRemoteNotes(
|
private fun downloadRemoteNotes(
|
||||||
sardine: Sardine,
|
sardine: Sardine,
|
||||||
serverUrl: String,
|
serverUrl: String,
|
||||||
@@ -1541,6 +1548,8 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
*
|
*
|
||||||
* ⚡ v1.3.1: Performance-Optimierung - Skip unveränderte Dateien
|
* ⚡ v1.3.1: Performance-Optimierung - Skip unveränderte Dateien
|
||||||
*/
|
*/
|
||||||
|
@Suppress("NestedBlockDepth", "LoopWithTooManyJumpStatements")
|
||||||
|
// Import logic requires nested conditions for file validation and duplicate handling
|
||||||
private fun importMarkdownFiles(sardine: Sardine, serverUrl: String): Int {
|
private fun importMarkdownFiles(sardine: Sardine, serverUrl: String): Int {
|
||||||
return try {
|
return try {
|
||||||
Logger.d(TAG, "📝 Importing Markdown files...")
|
Logger.d(TAG, "📝 Importing Markdown files...")
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION") // AbstractSavedStateViewModelFactory deprecated, will migrate to viewModelFactory in v2.0.0
|
||||||
|
|
||||||
package dev.dettmer.simplenotes.ui.editor
|
package dev.dettmer.simplenotes.ui.editor
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ fun NoteEditorScreen(
|
|||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
val checklistItems by viewModel.checklistItems.collectAsState()
|
val checklistItems by viewModel.checklistItems.collectAsState()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Offline mode state
|
||||||
|
val isOfflineMode by viewModel.isOfflineMode.collectAsState()
|
||||||
|
|
||||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
var focusNewItemId by remember { mutableStateOf<String?>(null) }
|
var focusNewItemId by remember { mutableStateOf<String?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
@@ -233,6 +236,7 @@ fun NoteEditorScreen(
|
|||||||
if (showDeleteDialog) {
|
if (showDeleteDialog) {
|
||||||
DeleteConfirmationDialog(
|
DeleteConfirmationDialog(
|
||||||
noteCount = 1,
|
noteCount = 1,
|
||||||
|
isOfflineMode = isOfflineMode,
|
||||||
onDismiss = { showDeleteDialog = false },
|
onDismiss = { showDeleteDialog = false },
|
||||||
onDeleteLocal = {
|
onDeleteLocal = {
|
||||||
showDeleteDialog = false
|
showDeleteDialog = false
|
||||||
@@ -287,6 +291,7 @@ private fun TextNoteContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList") // Compose functions commonly have many callback parameters
|
||||||
@Composable
|
@Composable
|
||||||
private fun ChecklistEditor(
|
private fun ChecklistEditor(
|
||||||
items: List<ChecklistItemState>,
|
items: List<ChecklistItemState>,
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
package dev.dettmer.simplenotes.ui.editor
|
package dev.dettmer.simplenotes.ui.editor
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
|
import androidx.work.WorkManager
|
||||||
import dev.dettmer.simplenotes.models.ChecklistItem
|
import dev.dettmer.simplenotes.models.ChecklistItem
|
||||||
import dev.dettmer.simplenotes.models.Note
|
import dev.dettmer.simplenotes.models.Note
|
||||||
import dev.dettmer.simplenotes.models.NoteType
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.models.SyncStatus
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
|
import dev.dettmer.simplenotes.sync.SyncWorker
|
||||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||||
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -42,6 +47,7 @@ class NoteEditorViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val storage = NotesStorage(application)
|
private val storage = NotesStorage(application)
|
||||||
|
private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// State
|
// State
|
||||||
@@ -53,6 +59,12 @@ class NoteEditorViewModel(
|
|||||||
private val _checklistItems = MutableStateFlow<List<ChecklistItemState>>(emptyList())
|
private val _checklistItems = MutableStateFlow<List<ChecklistItemState>>(emptyList())
|
||||||
val checklistItems: StateFlow<List<ChecklistItemState>> = _checklistItems.asStateFlow()
|
val checklistItems: StateFlow<List<ChecklistItemState>> = _checklistItems.asStateFlow()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Offline Mode State
|
||||||
|
private val _isOfflineMode = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)
|
||||||
|
)
|
||||||
|
val isOfflineMode: StateFlow<Boolean> = _isOfflineMode.asStateFlow()
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Events
|
// Events
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -108,7 +120,7 @@ class NoteEditorViewModel(
|
|||||||
currentNoteType = try {
|
currentNoteType = try {
|
||||||
NoteType.valueOf(noteTypeString)
|
NoteType.valueOf(noteTypeString)
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT")
|
Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT: ${e.message}")
|
||||||
NoteType.TEXT
|
NoteType.TEXT
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +296,10 @@ class NoteEditorViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED))
|
_events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED))
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Trigger onSave Sync
|
||||||
|
triggerOnSaveSync()
|
||||||
|
|
||||||
_events.emit(NoteEditorEvent.NavigateBack)
|
_events.emit(NoteEditorEvent.NavigateBack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,6 +347,52 @@ class NoteEditorViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun canDelete(): Boolean = existingNote != null
|
fun canDelete(): Boolean = existingNote != null
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 🌟 v1.6.0: Sync Trigger - onSave
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggers sync after saving a note (if enabled and server configured)
|
||||||
|
* v1.6.0: New configurable sync trigger
|
||||||
|
*
|
||||||
|
* Separate throttling (5 seconds) to prevent spam when saving multiple times
|
||||||
|
*/
|
||||||
|
private fun triggerOnSaveSync() {
|
||||||
|
// Check 1: Trigger enabled?
|
||||||
|
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_ON_SAVE, Constants.DEFAULT_TRIGGER_ON_SAVE)) {
|
||||||
|
Logger.d(TAG, "⏭️ onSave sync disabled - skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 2: Server configured?
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
||||||
|
Logger.d(TAG, "⏭️ Offline mode - skipping onSave sync")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check 3: Throttling (5 seconds) to prevent spam
|
||||||
|
val lastOnSaveSyncTime = prefs.getLong(Constants.PREF_LAST_ON_SAVE_SYNC_TIME, 0)
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val timeSinceLastSync = now - lastOnSaveSyncTime
|
||||||
|
|
||||||
|
if (timeSinceLastSync < Constants.MIN_ON_SAVE_SYNC_INTERVAL_MS) {
|
||||||
|
val remainingSeconds = (Constants.MIN_ON_SAVE_SYNC_INTERVAL_MS - timeSinceLastSync) / 1000
|
||||||
|
Logger.d(TAG, "⏳ onSave sync throttled - wait ${remainingSeconds}s")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last sync time
|
||||||
|
prefs.edit().putLong(Constants.PREF_LAST_ON_SAVE_SYNC_TIME, now).apply()
|
||||||
|
|
||||||
|
// Trigger sync via WorkManager
|
||||||
|
Logger.d(TAG, "📤 Triggering onSave sync")
|
||||||
|
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||||
|
.addTag(Constants.SYNC_WORK_TAG)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(getApplication()).enqueue(syncRequest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ fun ChecklistItemRow(
|
|||||||
val alpha = if (item.isChecked) 0.6f else 1.0f
|
val alpha = if (item.isChecked) 0.6f else 1.0f
|
||||||
val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None
|
val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None
|
||||||
|
|
||||||
|
@Suppress("MagicNumber") // UI padding values are self-explanatory
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@file:Suppress("DEPRECATION") // LocalBroadcastManager & deprecated lifecycle methods, will migrate in v2.0.0
|
||||||
|
|
||||||
package dev.dettmer.simplenotes.ui.main
|
package dev.dettmer.simplenotes.ui.main
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
@@ -177,7 +179,12 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
Logger.d(TAG, "📱 ComposeMainActivity.onResume() - Registering receivers")
|
Logger.d(TAG, "📱 ComposeMainActivity.onResume() - Registering receivers")
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Refresh offline mode state FIRST (before any sync checks)
|
||||||
|
// This ensures UI reflects current offline mode when returning from Settings
|
||||||
|
viewModel.refreshOfflineModeState()
|
||||||
|
|
||||||
// Register BroadcastReceiver for Background-Sync
|
// Register BroadcastReceiver for Background-Sync
|
||||||
|
@Suppress("DEPRECATION") // LocalBroadcastManager deprecated but functional
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||||
syncCompletedReceiver,
|
syncCompletedReceiver,
|
||||||
IntentFilter(SyncWorker.ACTION_SYNC_COMPLETED)
|
IntentFilter(SyncWorker.ACTION_SYNC_COMPLETED)
|
||||||
@@ -203,6 +210,7 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
super.onPause()
|
super.onPause()
|
||||||
|
|
||||||
// Unregister BroadcastReceiver
|
// Unregister BroadcastReceiver
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(syncCompletedReceiver)
|
LocalBroadcastManager.getInstance(this).unregisterReceiver(syncCompletedReceiver)
|
||||||
Logger.d(TAG, "📡 BroadcastReceiver unregistered")
|
Logger.d(TAG, "📡 BroadcastReceiver unregistered")
|
||||||
}
|
}
|
||||||
@@ -211,6 +219,7 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
SyncStateManager.syncStatus.observe(this) { status ->
|
SyncStateManager.syncStatus.observe(this) { status ->
|
||||||
viewModel.updateSyncState(status)
|
viewModel.updateSyncState(status)
|
||||||
|
|
||||||
|
@Suppress("MagicNumber") // UI timing delays for banner visibility
|
||||||
// Hide banner after delay for completed/error states
|
// Hide banner after delay for completed/error states
|
||||||
when (status.state) {
|
when (status.state) {
|
||||||
SyncStateManager.SyncState.COMPLETED -> {
|
SyncStateManager.SyncState.COMPLETED -> {
|
||||||
@@ -330,6 +339,8 @@ class ComposeMainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in API 23", ReplaceWith("Use ActivityResultContracts"))
|
||||||
|
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||||
override fun onRequestPermissionsResult(
|
override fun onRequestPermissionsResult(
|
||||||
requestCode: Int,
|
requestCode: Int,
|
||||||
permissions: Array<out String>,
|
permissions: Array<out String>,
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ fun MainScreen(
|
|||||||
val selectedNotes by viewModel.selectedNotes.collectAsState()
|
val selectedNotes by viewModel.selectedNotes.collectAsState()
|
||||||
val isSelectionMode by viewModel.isSelectionMode.collectAsState()
|
val isSelectionMode by viewModel.isSelectionMode.collectAsState()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Reactive offline mode state
|
||||||
|
val isOfflineMode by viewModel.isOfflineMode.collectAsState()
|
||||||
|
|
||||||
// Delete confirmation dialog state
|
// Delete confirmation dialog state
|
||||||
var showBatchDeleteDialog by remember { mutableStateOf(false) }
|
var showBatchDeleteDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -89,6 +92,13 @@ fun MainScreen(
|
|||||||
// Compute isSyncing once
|
// Compute isSyncing once
|
||||||
val isSyncing = syncState == SyncStateManager.SyncState.SYNCING
|
val isSyncing = syncState == SyncStateManager.SyncState.SYNCING
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Reactive sync availability (recomposes when offline mode changes)
|
||||||
|
// Note: isOfflineMode is updated via StateFlow from MainViewModel.refreshOfflineModeState()
|
||||||
|
// which is called in ComposeMainActivity.onResume() when returning from Settings
|
||||||
|
val hasServerConfig = viewModel.hasServerConfig()
|
||||||
|
val isSyncAvailable = !isOfflineMode && hasServerConfig
|
||||||
|
val canSync = isSyncAvailable && !isSyncing
|
||||||
|
|
||||||
// Handle snackbar events from ViewModel
|
// Handle snackbar events from ViewModel
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.showSnackbar.collect { data ->
|
viewModel.showSnackbar.collect { data ->
|
||||||
@@ -136,7 +146,7 @@ fun MainScreen(
|
|||||||
exit = slideOutVertically() + fadeOut()
|
exit = slideOutVertically() + fadeOut()
|
||||||
) {
|
) {
|
||||||
MainTopBar(
|
MainTopBar(
|
||||||
syncEnabled = !isSyncing,
|
syncEnabled = canSync,
|
||||||
onSyncClick = { viewModel.triggerManualSync("toolbar") },
|
onSyncClick = { viewModel.triggerManualSync("toolbar") },
|
||||||
onSettingsClick = onOpenSettings
|
onSettingsClick = onOpenSettings
|
||||||
)
|
)
|
||||||
@@ -146,10 +156,10 @@ fun MainScreen(
|
|||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
containerColor = MaterialTheme.colorScheme.surface
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
// PullToRefreshBox wraps the content with pull-to-refresh capability
|
// 🌟 v1.6.0: PullToRefreshBox only enabled when sync available
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isSyncing,
|
isRefreshing = isSyncing,
|
||||||
onRefresh = { viewModel.triggerManualSync("pullToRefresh") },
|
onRefresh = { if (isSyncAvailable) viewModel.triggerManualSync("pullToRefresh") },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
@@ -207,6 +217,7 @@ fun MainScreen(
|
|||||||
if (showBatchDeleteDialog) {
|
if (showBatchDeleteDialog) {
|
||||||
DeleteConfirmationDialog(
|
DeleteConfirmationDialog(
|
||||||
noteCount = selectedNotes.size,
|
noteCount = selectedNotes.size,
|
||||||
|
isOfflineMode = isOfflineMode,
|
||||||
onDismiss = { showBatchDeleteDialog = false },
|
onDismiss = { showBatchDeleteDialog = false },
|
||||||
onDeleteLocal = {
|
onDeleteLocal = {
|
||||||
viewModel.deleteSelectedNotes(deleteFromServer = false)
|
viewModel.deleteSelectedNotes(deleteFromServer = false)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import dev.dettmer.simplenotes.sync.SyncStateManager
|
|||||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.Logger
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
|
import dev.dettmer.simplenotes.utils.SyncConstants
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -62,6 +63,26 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
.map { it.isNotEmpty() }
|
.map { it.isNotEmpty() }
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
// 🌟 v1.6.0: Offline Mode State (reactive)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
private val _isOfflineMode = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)
|
||||||
|
)
|
||||||
|
val isOfflineMode: StateFlow<Boolean> = _isOfflineMode.asStateFlow()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh offline mode state from SharedPreferences
|
||||||
|
* Called when returning from Settings screen (in onResume)
|
||||||
|
*/
|
||||||
|
fun refreshOfflineModeState() {
|
||||||
|
val oldValue = _isOfflineMode.value
|
||||||
|
val newValue = prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)
|
||||||
|
_isOfflineMode.value = newValue
|
||||||
|
Logger.d(TAG, "🔄 refreshOfflineModeState: offlineMode=$oldValue → $newValue")
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Sync State (derived from SyncStateManager)
|
// Sync State (derived from SyncStateManager)
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -251,6 +272,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@Suppress("MagicNumber") // Snackbar timing coordination
|
||||||
// If delete from server, actually delete after a short delay
|
// If delete from server, actually delete after a short delay
|
||||||
// (to allow undo action before server deletion)
|
// (to allow undo action before server deletion)
|
||||||
if (deleteFromServer) {
|
if (deleteFromServer) {
|
||||||
@@ -350,6 +372,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@Suppress("MagicNumber") // Snackbar timing
|
||||||
// If delete from server, actually delete after snackbar timeout
|
// If delete from server, actually delete after snackbar timeout
|
||||||
if (deleteFromServer) {
|
if (deleteFromServer) {
|
||||||
kotlinx.coroutines.delay(3500) // Snackbar shows for ~3s
|
kotlinx.coroutines.delay(3500) // Snackbar shows for ~3s
|
||||||
@@ -420,6 +443,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
}
|
}
|
||||||
if (success) successCount++ else failCount++
|
if (success) successCount++ else failCount++
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Logger.w(TAG, "Failed to delete note $noteId from server: ${e.message}")
|
||||||
failCount++
|
failCount++
|
||||||
} finally {
|
} finally {
|
||||||
_pendingDeletions.value = _pendingDeletions.value - noteId
|
_pendingDeletions.value = _pendingDeletions.value - noteId
|
||||||
@@ -460,6 +484,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* Trigger manual sync (from toolbar button or pull-to-refresh)
|
* Trigger manual sync (from toolbar button or pull-to-refresh)
|
||||||
*/
|
*/
|
||||||
fun triggerManualSync(source: String = "manual") {
|
fun triggerManualSync(source: String = "manual") {
|
||||||
|
// 🌟 v1.6.0: Block sync in offline mode
|
||||||
|
if (prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)) {
|
||||||
|
Logger.d(TAG, "⏭️ $source Sync blocked: Offline mode enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!SyncStateManager.tryStartSync(source)) {
|
if (!SyncStateManager.tryStartSync(source)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -513,8 +543,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
* Trigger auto-sync (onResume)
|
* Trigger auto-sync (onResume)
|
||||||
* Only runs if server is configured and interval has passed
|
* Only runs if server is configured and interval has passed
|
||||||
* v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt
|
* v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt
|
||||||
|
* v1.6.0: Configurable trigger - checks KEY_SYNC_TRIGGER_ON_RESUME
|
||||||
*/
|
*/
|
||||||
fun triggerAutoSync(source: String = "auto") {
|
fun triggerAutoSync(source: String = "auto") {
|
||||||
|
// 🌟 v1.6.0: Check if onResume trigger is enabled
|
||||||
|
if (!prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_ON_RESUME, Constants.DEFAULT_TRIGGER_ON_RESUME)) {
|
||||||
|
Logger.d(TAG, "⏭️ onResume sync disabled - skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Throttling check
|
// Throttling check
|
||||||
if (!canTriggerAutoSync()) {
|
if (!canTriggerAutoSync()) {
|
||||||
return
|
return
|
||||||
@@ -523,6 +560,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
// Check if server is configured
|
// Check if server is configured
|
||||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
||||||
|
Logger.d(TAG, "⏭️ Offline mode - skipping onResume sync")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +645,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
|||||||
getApplication<android.app.Application>().getString(resId, *formatArgs)
|
getApplication<android.app.Application>().getString(resId, *formatArgs)
|
||||||
|
|
||||||
fun isServerConfigured(): Boolean {
|
fun isServerConfigured(): Boolean {
|
||||||
|
// 🌟 v1.6.0: Use reactive offline mode state
|
||||||
|
if (_isOfflineMode.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🌟 v1.6.0: Check if server has a configured URL (ignores offline mode)
|
||||||
|
* Used for determining if sync would be available when offline mode is disabled
|
||||||
|
*/
|
||||||
|
fun hasServerConfig(): Boolean {
|
||||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://"
|
return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,24 @@ package dev.dettmer.simplenotes.ui.main.components
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CloudOff
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -19,10 +28,12 @@ import dev.dettmer.simplenotes.R
|
|||||||
/**
|
/**
|
||||||
* Delete confirmation dialog with server/local options
|
* Delete confirmation dialog with server/local options
|
||||||
* v1.5.0: Multi-Select Feature
|
* v1.5.0: Multi-Select Feature
|
||||||
|
* v1.6.0: Offline mode support - disables server deletion option
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun DeleteConfirmationDialog(
|
fun DeleteConfirmationDialog(
|
||||||
noteCount: Int = 1,
|
noteCount: Int = 1,
|
||||||
|
isOfflineMode: Boolean = false,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onDeleteLocal: () -> Unit,
|
onDeleteLocal: () -> Unit,
|
||||||
onDeleteEverywhere: () -> Unit
|
onDeleteEverywhere: () -> Unit
|
||||||
@@ -59,16 +70,56 @@ fun DeleteConfirmationDialog(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// Delete everywhere (server + local) - primary action
|
// Delete everywhere (server + local) - primary action
|
||||||
|
// 🌟 v1.6.0: Disabled in offline mode with visual hint
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onDeleteEverywhere,
|
onClick = onDeleteEverywhere,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isOfflineMode,
|
||||||
colors = ButtonDefaults.textButtonColors(
|
colors = ButtonDefaults.textButtonColors(
|
||||||
contentColor = MaterialTheme.colorScheme.error
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.delete_everywhere))
|
Text(stringResource(R.string.delete_everywhere))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Show offline hint in a subtle Surface container
|
||||||
|
if (isOfflineMode) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(horizontal = 8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CloudOff,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.delete_everywhere_offline_hint),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
}
|
||||||
|
|
||||||
// Delete local only
|
// Delete local only
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onDeleteLocal,
|
onClick = onDeleteLocal,
|
||||||
|
|||||||
@@ -55,7 +55,13 @@ fun SettingsNavHost(
|
|||||||
composable(SettingsRoute.Sync.route) {
|
composable(SettingsRoute.Sync.route) {
|
||||||
SyncSettingsScreen(
|
SyncSettingsScreen(
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
onBack = { navController.popBackStack() }
|
onBack = { navController.popBackStack() },
|
||||||
|
onNavigateToServerSettings = {
|
||||||
|
navController.navigate(SettingsRoute.Server.route) {
|
||||||
|
// Avoid multiple copies of server settings in back stack
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@@ -46,10 +49,30 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
// v1.5.0 Fix: Initialize URL with protocol prefix if empty
|
// v1.5.0 Fix: Initialize URL with protocol prefix if empty
|
||||||
private val storedUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
|
private val storedUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
|
||||||
private val initialUrl = if (storedUrl.isEmpty()) "http://" else storedUrl
|
|
||||||
|
|
||||||
private val _serverUrl = MutableStateFlow(initialUrl)
|
// 🌟 v1.6.0: Separate host from prefix for better UX
|
||||||
val serverUrl: StateFlow<String> = _serverUrl.asStateFlow()
|
// isHttps determines the prefix, serverHost is the editable part
|
||||||
|
private val _isHttps = MutableStateFlow(storedUrl.startsWith("https://"))
|
||||||
|
val isHttps: StateFlow<Boolean> = _isHttps.asStateFlow()
|
||||||
|
|
||||||
|
// Extract host part (everything after http:// or https://)
|
||||||
|
private fun extractHostFromUrl(url: String): String {
|
||||||
|
return when {
|
||||||
|
url.startsWith("https://") -> url.removePrefix("https://")
|
||||||
|
url.startsWith("http://") -> url.removePrefix("http://")
|
||||||
|
else -> url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Only the host part is editable (without protocol prefix)
|
||||||
|
private val _serverHost = MutableStateFlow(extractHostFromUrl(storedUrl))
|
||||||
|
val serverHost: StateFlow<String> = _serverHost.asStateFlow()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Full URL for display purposes (computed from prefix + host)
|
||||||
|
val serverUrl: StateFlow<String> = combine(_isHttps, _serverHost) { https, host ->
|
||||||
|
val prefix = if (https) "https://" else "http://"
|
||||||
|
if (host.isEmpty()) "" else prefix + host
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.Eagerly, storedUrl)
|
||||||
|
|
||||||
private val _username = MutableStateFlow(prefs.getString(Constants.KEY_USERNAME, "") ?: "")
|
private val _username = MutableStateFlow(prefs.getString(Constants.KEY_USERNAME, "") ?: "")
|
||||||
val username: StateFlow<String> = _username.asStateFlow()
|
val username: StateFlow<String> = _username.asStateFlow()
|
||||||
@@ -57,13 +80,28 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
private val _password = MutableStateFlow(prefs.getString(Constants.KEY_PASSWORD, "") ?: "")
|
private val _password = MutableStateFlow(prefs.getString(Constants.KEY_PASSWORD, "") ?: "")
|
||||||
val password: StateFlow<String> = _password.asStateFlow()
|
val password: StateFlow<String> = _password.asStateFlow()
|
||||||
|
|
||||||
// v1.5.0 Fix: isHttps based on stored URL (false = HTTP if empty)
|
|
||||||
private val _isHttps = MutableStateFlow(storedUrl.startsWith("https://"))
|
|
||||||
val isHttps: StateFlow<Boolean> = _isHttps.asStateFlow()
|
|
||||||
|
|
||||||
private val _serverStatus = MutableStateFlow<ServerStatus>(ServerStatus.Unknown)
|
private val _serverStatus = MutableStateFlow<ServerStatus>(ServerStatus.Unknown)
|
||||||
val serverStatus: StateFlow<ServerStatus> = _serverStatus.asStateFlow()
|
val serverStatus: StateFlow<ServerStatus> = _serverStatus.asStateFlow()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Offline Mode Toggle
|
||||||
|
// Default: true for new users (no server), false for existing users (has server config)
|
||||||
|
private val _offlineMode = MutableStateFlow(
|
||||||
|
if (prefs.contains(Constants.KEY_OFFLINE_MODE)) {
|
||||||
|
prefs.getBoolean(Constants.KEY_OFFLINE_MODE, true)
|
||||||
|
} else {
|
||||||
|
// Migration: auto-detect based on existing server config
|
||||||
|
!hasExistingServerConfig()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val offlineMode: StateFlow<Boolean> = _offlineMode.asStateFlow()
|
||||||
|
|
||||||
|
private fun hasExistingServerConfig(): Boolean {
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
return !serverUrl.isNullOrEmpty() &&
|
||||||
|
serverUrl != "http://" &&
|
||||||
|
serverUrl != "https://"
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Events (for Activity-level actions like dialogs, intents)
|
// Events (for Activity-level actions like dialogs, intents)
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -90,6 +128,32 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
)
|
)
|
||||||
val syncInterval: StateFlow<Long> = _syncInterval.asStateFlow()
|
val syncInterval: StateFlow<Long> = _syncInterval.asStateFlow()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Configurable Sync Triggers
|
||||||
|
private val _triggerOnSave = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_ON_SAVE, Constants.DEFAULT_TRIGGER_ON_SAVE)
|
||||||
|
)
|
||||||
|
val triggerOnSave: StateFlow<Boolean> = _triggerOnSave.asStateFlow()
|
||||||
|
|
||||||
|
private val _triggerOnResume = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_ON_RESUME, Constants.DEFAULT_TRIGGER_ON_RESUME)
|
||||||
|
)
|
||||||
|
val triggerOnResume: StateFlow<Boolean> = _triggerOnResume.asStateFlow()
|
||||||
|
|
||||||
|
private val _triggerWifiConnect = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, Constants.DEFAULT_TRIGGER_WIFI_CONNECT)
|
||||||
|
)
|
||||||
|
val triggerWifiConnect: StateFlow<Boolean> = _triggerWifiConnect.asStateFlow()
|
||||||
|
|
||||||
|
private val _triggerPeriodic = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_PERIODIC, Constants.DEFAULT_TRIGGER_PERIODIC)
|
||||||
|
)
|
||||||
|
val triggerPeriodic: StateFlow<Boolean> = _triggerPeriodic.asStateFlow()
|
||||||
|
|
||||||
|
private val _triggerBoot = MutableStateFlow(
|
||||||
|
prefs.getBoolean(Constants.KEY_SYNC_TRIGGER_BOOT, Constants.DEFAULT_TRIGGER_BOOT)
|
||||||
|
)
|
||||||
|
val triggerBoot: StateFlow<Boolean> = _triggerBoot.asStateFlow()
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Markdown Settings State
|
// Markdown Settings State
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -126,32 +190,41 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
// Server Settings Actions
|
// Server Settings Actions
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.6.0: Set offline mode on/off
|
||||||
|
* When enabled, all network features are disabled
|
||||||
|
*/
|
||||||
|
fun setOfflineMode(enabled: Boolean) {
|
||||||
|
_offlineMode.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_OFFLINE_MODE, enabled).apply()
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
_serverStatus.value = ServerStatus.OfflineMode
|
||||||
|
} else {
|
||||||
|
// Re-check server status when disabling offline mode
|
||||||
|
checkServerStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateServerUrl(url: String) {
|
fun updateServerUrl(url: String) {
|
||||||
_serverUrl.value = url
|
// 🌟 v1.6.0: Deprecated - use updateServerHost instead
|
||||||
|
// This function is kept for compatibility but now delegates to updateServerHost
|
||||||
|
val host = extractHostFromUrl(url)
|
||||||
|
updateServerHost(host)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🌟 v1.6.0: Update only the host part of the server URL
|
||||||
|
* The protocol prefix is handled separately by updateProtocol()
|
||||||
|
*/
|
||||||
|
fun updateServerHost(host: String) {
|
||||||
|
_serverHost.value = host
|
||||||
saveServerSettings()
|
saveServerSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateProtocol(useHttps: Boolean) {
|
fun updateProtocol(useHttps: Boolean) {
|
||||||
_isHttps.value = useHttps
|
_isHttps.value = useHttps
|
||||||
val currentUrl = _serverUrl.value
|
// 🌟 v1.6.0: Host stays the same, only prefix changes
|
||||||
|
|
||||||
// v1.5.0 Fix: Automatisch Prefix setzen, auch bei leerem Feld
|
|
||||||
val newUrl = if (useHttps) {
|
|
||||||
when {
|
|
||||||
currentUrl.isEmpty() || currentUrl == "http://" -> "https://"
|
|
||||||
currentUrl.startsWith("http://") -> currentUrl.replace("http://", "https://")
|
|
||||||
!currentUrl.startsWith("https://") -> "https://$currentUrl"
|
|
||||||
else -> currentUrl
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when {
|
|
||||||
currentUrl.isEmpty() || currentUrl == "https://" -> "http://"
|
|
||||||
currentUrl.startsWith("https://") -> currentUrl.replace("https://", "http://")
|
|
||||||
!currentUrl.startsWith("http://") -> "http://$currentUrl"
|
|
||||||
else -> currentUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_serverUrl.value = newUrl
|
|
||||||
saveServerSettings()
|
saveServerSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +239,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveServerSettings() {
|
private fun saveServerSettings() {
|
||||||
|
// 🌟 v1.6.0: Construct full URL from prefix + host
|
||||||
|
val prefix = if (_isHttps.value) "https://" else "http://"
|
||||||
|
val fullUrl = if (_serverHost.value.isEmpty()) "" else prefix + _serverHost.value
|
||||||
|
|
||||||
prefs.edit().apply {
|
prefs.edit().apply {
|
||||||
putString(Constants.KEY_SERVER_URL, _serverUrl.value)
|
putString(Constants.KEY_SERVER_URL, fullUrl)
|
||||||
putString(Constants.KEY_USERNAME, _username.value)
|
putString(Constants.KEY_USERNAME, _username.value)
|
||||||
putString(Constants.KEY_PASSWORD, _password.value)
|
putString(Constants.KEY_PASSWORD, _password.value)
|
||||||
apply()
|
apply()
|
||||||
@@ -199,13 +276,23 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkServerStatus() {
|
fun checkServerStatus() {
|
||||||
val serverUrl = _serverUrl.value
|
// 🌟 v1.6.0: Respect offline mode first
|
||||||
// v1.5.0 Fix: URL mit nur Prefix gilt als "nicht konfiguriert"
|
if (_offlineMode.value) {
|
||||||
if (serverUrl.isEmpty() || serverUrl == "http://" || serverUrl == "https://") {
|
_serverStatus.value = ServerStatus.OfflineMode
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Check if host is configured
|
||||||
|
val serverHost = _serverHost.value
|
||||||
|
if (serverHost.isEmpty()) {
|
||||||
_serverStatus.value = ServerStatus.NotConfigured
|
_serverStatus.value = ServerStatus.NotConfigured
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Construct full URL
|
||||||
|
val prefix = if (_isHttps.value) "https://" else "http://"
|
||||||
|
val serverUrl = prefix + serverHost
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_serverStatus.value = ServerStatus.Checking
|
_serverStatus.value = ServerStatus.Checking
|
||||||
val isReachable = withContext(Dispatchers.IO) {
|
val isReachable = withContext(Dispatchers.IO) {
|
||||||
@@ -287,6 +374,44 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Configurable Sync Triggers Setters
|
||||||
|
|
||||||
|
fun setTriggerOnSave(enabled: Boolean) {
|
||||||
|
_triggerOnSave.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_SYNC_TRIGGER_ON_SAVE, enabled).apply()
|
||||||
|
Logger.d(TAG, "Trigger onSave: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTriggerOnResume(enabled: Boolean) {
|
||||||
|
_triggerOnResume.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_SYNC_TRIGGER_ON_RESUME, enabled).apply()
|
||||||
|
Logger.d(TAG, "Trigger onResume: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTriggerWifiConnect(enabled: Boolean) {
|
||||||
|
_triggerWifiConnect.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_SYNC_TRIGGER_WIFI_CONNECT, enabled).apply()
|
||||||
|
viewModelScope.launch {
|
||||||
|
_events.emit(SettingsEvent.RestartNetworkMonitor)
|
||||||
|
}
|
||||||
|
Logger.d(TAG, "Trigger WiFi-Connect: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTriggerPeriodic(enabled: Boolean) {
|
||||||
|
_triggerPeriodic.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_SYNC_TRIGGER_PERIODIC, enabled).apply()
|
||||||
|
viewModelScope.launch {
|
||||||
|
_events.emit(SettingsEvent.RestartNetworkMonitor)
|
||||||
|
}
|
||||||
|
Logger.d(TAG, "Trigger Periodic: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setTriggerBoot(enabled: Boolean) {
|
||||||
|
_triggerBoot.value = enabled
|
||||||
|
prefs.edit().putBoolean(Constants.KEY_SYNC_TRIGGER_BOOT, enabled).apply()
|
||||||
|
Logger.d(TAG, "Trigger Boot: $enabled")
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
// Markdown Settings Actions
|
// Markdown Settings Actions
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
@@ -337,6 +462,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
_markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
|
_markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
|
||||||
emitToast(getString(R.string.toast_markdown_exported, exportedCount))
|
emitToast(getString(R.string.toast_markdown_exported, exportedCount))
|
||||||
|
|
||||||
|
@Suppress("MagicNumber") // UI progress delay
|
||||||
// Clear progress after short delay
|
// Clear progress after short delay
|
||||||
kotlinx.coroutines.delay(500)
|
kotlinx.coroutines.delay(500)
|
||||||
_markdownExportProgress.value = null
|
_markdownExportProgress.value = null
|
||||||
@@ -371,6 +497,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun performManualMarkdownSync() {
|
fun performManualMarkdownSync() {
|
||||||
|
// 🌟 v1.6.0: Block in offline mode
|
||||||
|
if (_offlineMode.value) {
|
||||||
|
Logger.d(TAG, "⏭️ Manual Markdown sync blocked: Offline mode enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
emitToast(getString(R.string.toast_markdown_syncing))
|
emitToast(getString(R.string.toast_markdown_syncing))
|
||||||
@@ -478,6 +610,20 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
// Helper
|
// Helper
|
||||||
// ═══════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if server is configured AND not in offline mode
|
||||||
|
* v1.6.0: Returns false if offline mode is enabled
|
||||||
|
*/
|
||||||
|
fun isServerConfigured(): Boolean {
|
||||||
|
// Offline mode takes priority
|
||||||
|
if (_offlineMode.value) return false
|
||||||
|
|
||||||
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
return !serverUrl.isNullOrEmpty() &&
|
||||||
|
serverUrl != "http://" &&
|
||||||
|
serverUrl != "https://"
|
||||||
|
}
|
||||||
|
|
||||||
private fun getString(resId: Int): String = getApplication<android.app.Application>().getString(resId)
|
private fun getString(resId: Int): String = getApplication<android.app.Application>().getString(resId)
|
||||||
|
|
||||||
private fun getString(resId: Int, vararg formatArgs: Any): String =
|
private fun getString(resId: Int, vararg formatArgs: Any): String =
|
||||||
@@ -489,9 +635,11 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Server status states
|
* Server status states
|
||||||
|
* v1.6.0: Added OfflineMode state
|
||||||
*/
|
*/
|
||||||
sealed class ServerStatus {
|
sealed class ServerStatus {
|
||||||
data object Unknown : ServerStatus()
|
data object Unknown : ServerStatus()
|
||||||
|
data object OfflineMode : ServerStatus() // 🌟 v1.6.0
|
||||||
data object NotConfigured : ServerStatus()
|
data object NotConfigured : ServerStatus()
|
||||||
data object Checking : ServerStatus()
|
data object Checking : ServerStatus()
|
||||||
data object Reachable : ServerStatus()
|
data object Reachable : ServerStatus()
|
||||||
|
|||||||
@@ -95,24 +95,34 @@ fun SettingsDangerButton(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Info card with description text
|
* Info card with description text
|
||||||
|
* v1.6.0: Added isWarning parameter for offline mode warning
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsInfoCard(
|
fun SettingsInfoCard(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
isWarning: Boolean = false
|
||||||
) {
|
) {
|
||||||
androidx.compose.material3.Card(
|
androidx.compose.material3.Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
colors = androidx.compose.material3.CardDefaults.cardColors(
|
colors = androidx.compose.material3.CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
containerColor = if (isWarning) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isWarning) {
|
||||||
|
MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
},
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.3f
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.3f
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@file:Suppress("MatchingDeclarationName")
|
||||||
package dev.dettmer.simplenotes.ui.settings.components
|
package dev.dettmer.simplenotes.ui.settings.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.ui.settings.components
|
package dev.dettmer.simplenotes.ui.settings.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
@@ -34,6 +35,7 @@ fun SettingsSwitch(
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.clickable(enabled = enabled) { onCheckedChange(!checked) }
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ fun BackupSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val isBackupInProgress by viewModel.isBackupInProgress.collectAsState()
|
val isBackupInProgress by viewModel.isBackupInProgress.collectAsState()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Check if server restore is available
|
||||||
|
val isServerConfigured = viewModel.isServerConfigured()
|
||||||
|
|
||||||
// Restore dialog state
|
// Restore dialog state
|
||||||
var showRestoreDialog by remember { mutableStateOf(false) }
|
var showRestoreDialog by remember { mutableStateOf(false) }
|
||||||
var restoreSource by remember { mutableStateOf<RestoreSource>(RestoreSource.LocalFile) }
|
var restoreSource by remember { mutableStateOf<RestoreSource>(RestoreSource.LocalFile) }
|
||||||
@@ -126,6 +129,7 @@ fun BackupSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Disabled when offline mode active
|
||||||
SettingsOutlinedButton(
|
SettingsOutlinedButton(
|
||||||
text = stringResource(R.string.backup_restore_server),
|
text = stringResource(R.string.backup_restore_server),
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -133,9 +137,21 @@ fun BackupSettingsScreen(
|
|||||||
showRestoreDialog = true
|
showRestoreDialog = true
|
||||||
},
|
},
|
||||||
isLoading = isBackupInProgress,
|
isLoading = isBackupInProgress,
|
||||||
|
enabled = isServerConfigured,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Show hint when offline
|
||||||
|
if (!isServerConfigured) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_sync_offline_mode),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ fun MarkdownSettingsScreen(
|
|||||||
val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
|
val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
|
||||||
val exportProgress by viewModel.markdownExportProgress.collectAsState()
|
val exportProgress by viewModel.markdownExportProgress.collectAsState()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Check offline mode
|
||||||
|
val offlineMode by viewModel.offlineMode.collectAsState()
|
||||||
|
val isServerConfigured = viewModel.isServerConfigured()
|
||||||
|
|
||||||
// v1.5.0 Fix: Progress Dialog for initial export
|
// v1.5.0 Fix: Progress Dialog for initial export
|
||||||
exportProgress?.let { progress ->
|
exportProgress?.let { progress ->
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
@@ -96,15 +100,22 @@ fun MarkdownSettingsScreen(
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Markdown Auto-Sync Toggle
|
// Markdown Auto-Sync Toggle
|
||||||
|
// 🌟 v1.6.0: Disabled when offline mode active
|
||||||
SettingsSwitch(
|
SettingsSwitch(
|
||||||
title = stringResource(R.string.markdown_auto_sync_title),
|
title = stringResource(R.string.markdown_auto_sync_title),
|
||||||
subtitle = stringResource(R.string.markdown_auto_sync_subtitle),
|
subtitle = if (!isServerConfigured) {
|
||||||
|
stringResource(R.string.settings_sync_offline_mode)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.markdown_auto_sync_subtitle)
|
||||||
|
},
|
||||||
checked = markdownAutoSync,
|
checked = markdownAutoSync,
|
||||||
onCheckedChange = { viewModel.setMarkdownAutoSync(it) },
|
onCheckedChange = { viewModel.setMarkdownAutoSync(it) },
|
||||||
icon = Icons.Default.Description
|
icon = Icons.Default.Description,
|
||||||
|
enabled = isServerConfigured
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manual sync button (only visible when auto-sync is off)
|
// Manual sync button (only visible when auto-sync is off)
|
||||||
|
// 🌟 v1.6.0: Also disabled in offline mode
|
||||||
if (!markdownAutoSync) {
|
if (!markdownAutoSync) {
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
|
|
||||||
@@ -117,8 +128,20 @@ fun MarkdownSettingsScreen(
|
|||||||
SettingsButton(
|
SettingsButton(
|
||||||
text = stringResource(R.string.markdown_manual_sync_button),
|
text = stringResource(R.string.markdown_manual_sync_button),
|
||||||
onClick = { viewModel.performManualMarkdownSync() },
|
onClick = { viewModel.performManualMarkdownSync() },
|
||||||
|
enabled = isServerConfigured,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Show hint when offline
|
||||||
|
if (!isServerConfigured) {
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_sync_offline_mode),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.ui.settings.screens
|
package dev.dettmer.simplenotes.ui.settings.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -29,6 +30,7 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -39,6 +41,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -52,13 +55,17 @@ import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
|
|||||||
/**
|
/**
|
||||||
* Server configuration settings screen
|
* Server configuration settings screen
|
||||||
* v1.5.0: Jetpack Compose Settings Redesign
|
* v1.5.0: Jetpack Compose Settings Redesign
|
||||||
|
* v1.6.0: Offline Mode Toggle
|
||||||
*/
|
*/
|
||||||
|
@Suppress("LongMethod", "MagicNumber") // Compose UI + Color hex values
|
||||||
@Composable
|
@Composable
|
||||||
fun ServerSettingsScreen(
|
fun ServerSettingsScreen(
|
||||||
viewModel: SettingsViewModel,
|
viewModel: SettingsViewModel,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
) {
|
) {
|
||||||
val serverUrl by viewModel.serverUrl.collectAsState()
|
val offlineMode by viewModel.offlineMode.collectAsState()
|
||||||
|
val serverHost by viewModel.serverHost.collectAsState() // 🌟 v1.6.0: Only host part
|
||||||
|
val serverUrl by viewModel.serverUrl.collectAsState() // Full URL for display
|
||||||
val username by viewModel.username.collectAsState()
|
val username by viewModel.username.collectAsState()
|
||||||
val password by viewModel.password.collectAsState()
|
val password by viewModel.password.collectAsState()
|
||||||
val isHttps by viewModel.isHttps.collectAsState()
|
val isHttps by viewModel.isHttps.collectAsState()
|
||||||
@@ -67,9 +74,11 @@ fun ServerSettingsScreen(
|
|||||||
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Check server status on load
|
// Check server status on load (only if not in offline mode)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(offlineMode) {
|
||||||
viewModel.checkServerStatus()
|
if (!offlineMode) {
|
||||||
|
viewModel.checkServerStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsScaffold(
|
SettingsScaffold(
|
||||||
@@ -83,99 +92,168 @@ fun ServerSettingsScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
// Verbindungstyp
|
// ═══════════════════════════════════════════════════════════════
|
||||||
Text(
|
// 🌟 v1.6.0: Offline-Modus Toggle (TOP)
|
||||||
text = stringResource(R.string.server_connection_type),
|
// ═══════════════════════════════════════════════════════════════
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
.fillMaxWidth()
|
||||||
|
.clickable { viewModel.setOfflineMode(!offlineMode) },
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = if (offlineMode) {
|
||||||
|
MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
}
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
Row(
|
||||||
selected = !isHttps,
|
modifier = Modifier
|
||||||
onClick = { viewModel.updateProtocol(false) },
|
.fillMaxWidth()
|
||||||
label = { Text(stringResource(R.string.server_connection_http)) },
|
.padding(16.dp),
|
||||||
modifier = Modifier.weight(1f)
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
)
|
verticalAlignment = Alignment.CenterVertically
|
||||||
FilterChip(
|
) {
|
||||||
selected = isHttps,
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
onClick = { viewModel.updateProtocol(true) },
|
Text(
|
||||||
label = { Text(stringResource(R.string.server_connection_https)) },
|
text = stringResource(R.string.server_offline_mode_title),
|
||||||
modifier = Modifier.weight(1f)
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
Text(
|
||||||
|
text = stringResource(R.string.server_offline_mode_subtitle),
|
||||||
Text(
|
style = MaterialTheme.typography.bodySmall,
|
||||||
text = if (!isHttps) {
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
stringResource(R.string.server_connection_http_hint)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.server_connection_https_hint)
|
|
||||||
},
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(top = 4.dp, bottom = 16.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Server-Adresse
|
|
||||||
OutlinedTextField(
|
|
||||||
value = serverUrl,
|
|
||||||
onValueChange = { viewModel.updateServerUrl(it) },
|
|
||||||
label = { Text(stringResource(R.string.server_address)) },
|
|
||||||
supportingText = { Text(stringResource(R.string.server_address_hint)) },
|
|
||||||
leadingIcon = { Icon(Icons.Default.Language, null) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Benutzername
|
|
||||||
OutlinedTextField(
|
|
||||||
value = username,
|
|
||||||
onValueChange = { viewModel.updateUsername(it) },
|
|
||||||
label = { Text(stringResource(R.string.username)) },
|
|
||||||
leadingIcon = { Icon(Icons.Default.Person, null) },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
// Passwort
|
|
||||||
OutlinedTextField(
|
|
||||||
value = password,
|
|
||||||
onValueChange = { viewModel.updatePassword(it) },
|
|
||||||
label = { Text(stringResource(R.string.password)) },
|
|
||||||
leadingIcon = { Icon(Icons.Default.Lock, null) },
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = if (passwordVisible) {
|
|
||||||
Icons.Default.VisibilityOff
|
|
||||||
} else {
|
|
||||||
Icons.Default.Visibility
|
|
||||||
},
|
|
||||||
contentDescription = if (passwordVisible) {
|
|
||||||
stringResource(R.string.server_password_hide)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.server_password_show)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
Switch(
|
||||||
visualTransformation = if (passwordVisible) {
|
checked = offlineMode,
|
||||||
VisualTransformation.None
|
onCheckedChange = { viewModel.setOfflineMode(it) }
|
||||||
} else {
|
)
|
||||||
PasswordVisualTransformation()
|
}
|
||||||
},
|
}
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
singleLine = true,
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
|
||||||
)
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// Server Configuration (grayed out when offline mode)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
val fieldsEnabled = !offlineMode
|
||||||
|
val fieldsAlpha = if (offlineMode) 0.5f else 1f
|
||||||
|
|
||||||
|
Column(modifier = Modifier.alpha(fieldsAlpha)) {
|
||||||
|
// Verbindungstyp
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.server_connection_type),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = !isHttps,
|
||||||
|
onClick = { viewModel.updateProtocol(false) },
|
||||||
|
label = { Text(stringResource(R.string.server_connection_http)) },
|
||||||
|
enabled = fieldsEnabled,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = isHttps,
|
||||||
|
onClick = { viewModel.updateProtocol(true) },
|
||||||
|
label = { Text(stringResource(R.string.server_connection_https)) },
|
||||||
|
enabled = fieldsEnabled,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (!isHttps) {
|
||||||
|
stringResource(R.string.server_connection_http_hint)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.server_connection_https_hint)
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(top = 4.dp, bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Server-Adresse with non-editable prefix
|
||||||
|
OutlinedTextField(
|
||||||
|
value = serverHost, // Only host part is editable
|
||||||
|
onValueChange = { viewModel.updateServerHost(it) },
|
||||||
|
label = { Text(stringResource(R.string.server_address)) },
|
||||||
|
supportingText = { Text(stringResource(R.string.server_address_hint)) },
|
||||||
|
prefix = {
|
||||||
|
// Protocol prefix is displayed but not editable
|
||||||
|
Text(
|
||||||
|
text = if (isHttps) "https://" else "http://",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = if (fieldsEnabled) {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Language, null) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldsEnabled,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Benutzername
|
||||||
|
OutlinedTextField(
|
||||||
|
value = username,
|
||||||
|
onValueChange = { viewModel.updateUsername(it) },
|
||||||
|
label = { Text(stringResource(R.string.username)) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Person, null) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldsEnabled
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Passwort
|
||||||
|
OutlinedTextField(
|
||||||
|
value = password,
|
||||||
|
onValueChange = { viewModel.updatePassword(it) },
|
||||||
|
label = { Text(stringResource(R.string.password)) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Lock, null) },
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (passwordVisible) {
|
||||||
|
Icons.Default.VisibilityOff
|
||||||
|
} else {
|
||||||
|
Icons.Default.Visibility
|
||||||
|
},
|
||||||
|
contentDescription = if (passwordVisible) {
|
||||||
|
stringResource(R.string.server_password_hide)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.server_password_show)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visualTransformation = if (passwordVisible) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
enabled = fieldsEnabled,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
@@ -196,16 +274,18 @@ fun ServerSettingsScreen(
|
|||||||
Text(stringResource(R.string.server_status_label), style = MaterialTheme.typography.labelLarge)
|
Text(stringResource(R.string.server_status_label), style = MaterialTheme.typography.labelLarge)
|
||||||
Text(
|
Text(
|
||||||
text = when (serverStatus) {
|
text = when (serverStatus) {
|
||||||
|
is SettingsViewModel.ServerStatus.OfflineMode -> stringResource(R.string.server_status_offline_mode)
|
||||||
is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.server_status_reachable)
|
is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.server_status_reachable)
|
||||||
is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.server_status_unreachable)
|
is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.server_status_unreachable)
|
||||||
is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.server_status_checking)
|
is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.server_status_checking)
|
||||||
is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.server_status_not_configured)
|
is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.server_status_offline_mode)
|
||||||
else -> stringResource(R.string.server_status_unknown)
|
else -> stringResource(R.string.server_status_unknown)
|
||||||
},
|
},
|
||||||
color = when (serverStatus) {
|
color = when (serverStatus) {
|
||||||
|
is SettingsViewModel.ServerStatus.OfflineMode -> MaterialTheme.colorScheme.tertiary
|
||||||
is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
|
is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
|
||||||
is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336)
|
is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336)
|
||||||
is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800)
|
is SettingsViewModel.ServerStatus.NotConfigured -> MaterialTheme.colorScheme.tertiary
|
||||||
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
else -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -214,13 +294,16 @@ fun ServerSettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons (disabled in offline mode)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.alpha(fieldsAlpha),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { viewModel.testConnection() },
|
onClick = { viewModel.testConnection() },
|
||||||
|
enabled = fieldsEnabled,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.test_connection))
|
Text(stringResource(R.string.test_connection))
|
||||||
@@ -228,7 +311,7 @@ fun ServerSettingsScreen(
|
|||||||
|
|
||||||
Button(
|
Button(
|
||||||
onClick = { viewModel.syncNow() },
|
onClick = { viewModel.syncNow() },
|
||||||
enabled = !isSyncing,
|
enabled = fieldsEnabled && !isSyncing,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
if (isSyncing) {
|
if (isSyncing) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.Description
|
|||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Language
|
import androidx.compose.material.icons.filled.Language
|
||||||
import androidx.compose.material.icons.filled.Sync
|
import androidx.compose.material.icons.filled.Sync
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -32,6 +33,7 @@ import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
|
|||||||
* Main Settings overview screen with clickable group cards
|
* Main Settings overview screen with clickable group cards
|
||||||
* v1.5.0: Jetpack Compose Settings Redesign
|
* v1.5.0: Jetpack Compose Settings Redesign
|
||||||
*/
|
*/
|
||||||
|
@Suppress("MagicNumber") // Color hex values
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsMainScreen(
|
fun SettingsMainScreen(
|
||||||
viewModel: SettingsViewModel,
|
viewModel: SettingsViewModel,
|
||||||
@@ -45,6 +47,14 @@ fun SettingsMainScreen(
|
|||||||
val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
|
val markdownAutoSync by viewModel.markdownAutoSync.collectAsState()
|
||||||
val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState()
|
val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState()
|
||||||
|
|
||||||
|
// 🌟 v1.6.0: Collect offline mode and trigger states
|
||||||
|
val offlineMode by viewModel.offlineMode.collectAsState()
|
||||||
|
val triggerOnSave by viewModel.triggerOnSave.collectAsState()
|
||||||
|
val triggerOnResume by viewModel.triggerOnResume.collectAsState()
|
||||||
|
val triggerWifiConnect by viewModel.triggerWifiConnect.collectAsState()
|
||||||
|
val triggerPeriodic by viewModel.triggerPeriodic.collectAsState()
|
||||||
|
val triggerBoot by viewModel.triggerBoot.collectAsState()
|
||||||
|
|
||||||
// Check server status on first load
|
// Check server status on first load
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.checkServerStatus()
|
viewModel.checkServerStatus()
|
||||||
@@ -82,26 +92,38 @@ fun SettingsMainScreen(
|
|||||||
|
|
||||||
// Server-Einstellungen
|
// Server-Einstellungen
|
||||||
item {
|
item {
|
||||||
// v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert"
|
// 🌟 v1.6.0: Check if server is configured (host is not empty)
|
||||||
val isConfigured = serverUrl.isNotEmpty() &&
|
val isConfigured = serverUrl.isNotEmpty()
|
||||||
serverUrl != "http://" &&
|
|
||||||
serverUrl != "https://"
|
|
||||||
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
icon = Icons.Default.Cloud,
|
icon = Icons.Default.Cloud,
|
||||||
title = stringResource(R.string.settings_server),
|
title = stringResource(R.string.settings_server),
|
||||||
subtitle = if (isConfigured) serverUrl else null,
|
subtitle = if (!offlineMode && isConfigured) serverUrl else null,
|
||||||
statusText = when (serverStatus) {
|
statusText = when {
|
||||||
is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.settings_server_status_reachable)
|
offlineMode ->
|
||||||
is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.settings_server_status_unreachable)
|
stringResource(R.string.settings_server_status_offline_mode)
|
||||||
is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.settings_server_status_checking)
|
serverStatus is SettingsViewModel.ServerStatus.OfflineMode ->
|
||||||
is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.settings_server_status_not_configured)
|
stringResource(R.string.settings_server_status_offline_mode)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.Reachable ->
|
||||||
|
stringResource(R.string.settings_server_status_reachable)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.Unreachable ->
|
||||||
|
stringResource(R.string.settings_server_status_unreachable)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.Checking ->
|
||||||
|
stringResource(R.string.settings_server_status_checking)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.NotConfigured ->
|
||||||
|
stringResource(R.string.settings_server_status_offline_mode)
|
||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
statusColor = when (serverStatus) {
|
statusColor = when {
|
||||||
is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
|
offlineMode -> MaterialTheme.colorScheme.tertiary
|
||||||
is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336)
|
serverStatus is SettingsViewModel.ServerStatus.OfflineMode ->
|
||||||
is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800)
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.Reachable ->
|
||||||
|
Color(0xFF4CAF50)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.Unreachable ->
|
||||||
|
Color(0xFFF44336)
|
||||||
|
serverStatus is SettingsViewModel.ServerStatus.NotConfigured ->
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
else -> Color.Gray
|
else -> Color.Gray
|
||||||
},
|
},
|
||||||
onClick = { onNavigate(SettingsRoute.Server) }
|
onClick = { onNavigate(SettingsRoute.Server) }
|
||||||
@@ -110,33 +132,52 @@ fun SettingsMainScreen(
|
|||||||
|
|
||||||
// Sync-Einstellungen
|
// Sync-Einstellungen
|
||||||
item {
|
item {
|
||||||
val intervalText = when (syncInterval) {
|
// 🌟 v1.6.0: Build dynamic subtitle based on active triggers
|
||||||
15L -> stringResource(R.string.settings_interval_15min)
|
val isServerConfigured = viewModel.isServerConfigured()
|
||||||
60L -> stringResource(R.string.settings_interval_60min)
|
val activeTriggersCount = listOf(
|
||||||
else -> stringResource(R.string.settings_interval_30min)
|
triggerOnSave,
|
||||||
}
|
triggerOnResume,
|
||||||
|
triggerWifiConnect,
|
||||||
|
triggerPeriodic,
|
||||||
|
triggerBoot
|
||||||
|
).count { it }
|
||||||
|
|
||||||
|
// 🌟 v1.6.0 Fix: Use statusText for offline mode (consistent with Server card)
|
||||||
|
val syncSubtitle = if (isServerConfigured) {
|
||||||
|
if (activeTriggersCount == 0) {
|
||||||
|
stringResource(R.string.settings_sync_manual_only)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.settings_sync_triggers_active, activeTriggersCount)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
icon = Icons.Default.Sync,
|
icon = Icons.Default.Sync,
|
||||||
title = stringResource(R.string.settings_sync),
|
title = stringResource(R.string.settings_sync),
|
||||||
subtitle = if (autoSyncEnabled) {
|
subtitle = syncSubtitle,
|
||||||
stringResource(R.string.settings_sync_auto_on, intervalText)
|
statusText = if (!isServerConfigured) stringResource(R.string.settings_sync_offline_mode) else null,
|
||||||
} else {
|
statusColor = if (!isServerConfigured) MaterialTheme.colorScheme.tertiary else Color.Gray,
|
||||||
stringResource(R.string.settings_sync_auto_off)
|
|
||||||
},
|
|
||||||
onClick = { onNavigate(SettingsRoute.Sync) }
|
onClick = { onNavigate(SettingsRoute.Sync) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Markdown-Integration
|
// Markdown-Integration
|
||||||
item {
|
item {
|
||||||
|
// 🌟 v1.6.0 Fix: Use statusText for offline mode (consistent with Server card)
|
||||||
|
val isServerConfiguredForMarkdown = viewModel.isServerConfigured()
|
||||||
|
|
||||||
SettingsCard(
|
SettingsCard(
|
||||||
icon = Icons.Default.Description,
|
icon = Icons.Default.Description,
|
||||||
title = stringResource(R.string.settings_markdown),
|
title = stringResource(R.string.settings_markdown),
|
||||||
subtitle = if (markdownAutoSync) {
|
subtitle = if (isServerConfiguredForMarkdown) {
|
||||||
stringResource(R.string.settings_markdown_auto_on)
|
if (markdownAutoSync) {
|
||||||
} else {
|
stringResource(R.string.settings_markdown_auto_on)
|
||||||
stringResource(R.string.settings_markdown_auto_off)
|
} else {
|
||||||
},
|
stringResource(R.string.settings_markdown_auto_off)
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
statusText = if (!isServerConfiguredForMarkdown) stringResource(R.string.settings_sync_offline_mode) else null,
|
||||||
|
statusColor = if (!isServerConfiguredForMarkdown) MaterialTheme.colorScheme.tertiary else Color.Gray,
|
||||||
onClick = { onNavigate(SettingsRoute.Markdown) }
|
onClick = { onNavigate(SettingsRoute.Markdown) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,13 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Sync
|
import androidx.compose.material.icons.filled.PhonelinkRing
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material.icons.filled.SettingsInputAntenna
|
||||||
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -26,17 +32,27 @@ import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
|
|||||||
import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
|
import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync settings screen (Auto-Sync toggle and interval selection)
|
* Sync settings screen - Configurable Sync Triggers
|
||||||
* v1.5.0: Jetpack Compose Settings Redesign
|
* v1.5.0: Jetpack Compose Settings Redesign
|
||||||
|
* v1.6.0: Individual toggle for each sync trigger (onSave, onResume, WiFi-Connect, Periodic, Boot)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SyncSettingsScreen(
|
fun SyncSettingsScreen(
|
||||||
viewModel: SettingsViewModel,
|
viewModel: SettingsViewModel,
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit,
|
||||||
|
onNavigateToServerSettings: () -> Unit
|
||||||
) {
|
) {
|
||||||
val autoSyncEnabled by viewModel.autoSyncEnabled.collectAsState()
|
// Collect all trigger states
|
||||||
|
val triggerOnSave by viewModel.triggerOnSave.collectAsState()
|
||||||
|
val triggerOnResume by viewModel.triggerOnResume.collectAsState()
|
||||||
|
val triggerWifiConnect by viewModel.triggerWifiConnect.collectAsState()
|
||||||
|
val triggerPeriodic by viewModel.triggerPeriodic.collectAsState()
|
||||||
|
val triggerBoot by viewModel.triggerBoot.collectAsState()
|
||||||
val syncInterval by viewModel.syncInterval.collectAsState()
|
val syncInterval by viewModel.syncInterval.collectAsState()
|
||||||
|
|
||||||
|
// Check if server is configured
|
||||||
|
val isServerConfigured = viewModel.isServerConfigured()
|
||||||
|
|
||||||
SettingsScaffold(
|
SettingsScaffold(
|
||||||
title = stringResource(R.string.sync_settings_title),
|
title = stringResource(R.string.sync_settings_title),
|
||||||
onBack = onBack
|
onBack = onBack
|
||||||
@@ -49,55 +65,137 @@ fun SyncSettingsScreen(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
// Auto-Sync Info
|
// 🌟 v1.6.0: Offline Mode Warning if server not configured
|
||||||
SettingsInfoCard(
|
if (!isServerConfigured) {
|
||||||
text = stringResource(R.string.sync_auto_sync_info)
|
SettingsInfoCard(
|
||||||
|
text = stringResource(R.string.sync_offline_mode_message),
|
||||||
|
isWarning = true
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onNavigateToServerSettings,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.sync_offline_mode_button))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// SOFORT-SYNC Section
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
SettingsSectionHeader(text = stringResource(R.string.sync_section_instant))
|
||||||
|
|
||||||
|
// onSave Trigger
|
||||||
|
SettingsSwitch(
|
||||||
|
title = stringResource(R.string.sync_trigger_on_save_title),
|
||||||
|
subtitle = stringResource(R.string.sync_trigger_on_save_subtitle),
|
||||||
|
checked = triggerOnSave,
|
||||||
|
onCheckedChange = { viewModel.setTriggerOnSave(it) },
|
||||||
|
icon = Icons.Default.Save,
|
||||||
|
enabled = isServerConfigured
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
// onResume Trigger
|
||||||
|
|
||||||
// Auto-Sync Toggle
|
|
||||||
SettingsSwitch(
|
SettingsSwitch(
|
||||||
title = stringResource(R.string.sync_auto_sync_enabled),
|
title = stringResource(R.string.sync_trigger_on_resume_title),
|
||||||
checked = autoSyncEnabled,
|
subtitle = stringResource(R.string.sync_trigger_on_resume_subtitle),
|
||||||
onCheckedChange = { viewModel.setAutoSync(it) },
|
checked = triggerOnResume,
|
||||||
icon = Icons.Default.Sync
|
onCheckedChange = { viewModel.setTriggerOnResume(it) },
|
||||||
|
icon = Icons.Default.PhonelinkRing,
|
||||||
|
enabled = isServerConfigured
|
||||||
)
|
)
|
||||||
|
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
|
|
||||||
// Sync Interval Section
|
// ═══════════════════════════════════════════════════════════════
|
||||||
SettingsSectionHeader(text = stringResource(R.string.sync_interval_section))
|
// HINTERGRUND-SYNC Section
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
SettingsSectionHeader(text = stringResource(R.string.sync_section_background))
|
||||||
|
|
||||||
|
// WiFi-Connect Trigger
|
||||||
|
SettingsSwitch(
|
||||||
|
title = stringResource(R.string.sync_trigger_wifi_connect_title),
|
||||||
|
subtitle = stringResource(R.string.sync_trigger_wifi_connect_subtitle),
|
||||||
|
checked = triggerWifiConnect,
|
||||||
|
onCheckedChange = { viewModel.setTriggerWifiConnect(it) },
|
||||||
|
icon = Icons.Default.Wifi,
|
||||||
|
enabled = isServerConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
// Periodic Trigger
|
||||||
|
SettingsSwitch(
|
||||||
|
title = stringResource(R.string.sync_trigger_periodic_title),
|
||||||
|
subtitle = stringResource(R.string.sync_trigger_periodic_subtitle),
|
||||||
|
checked = triggerPeriodic,
|
||||||
|
onCheckedChange = { viewModel.setTriggerPeriodic(it) },
|
||||||
|
icon = Icons.Default.Schedule,
|
||||||
|
enabled = isServerConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
// Periodic Interval Selection (only visible if periodic trigger is enabled)
|
||||||
|
if (triggerPeriodic && isServerConfigured) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val intervalOptions = listOf(
|
||||||
|
RadioOption(
|
||||||
|
value = 15L,
|
||||||
|
title = stringResource(R.string.sync_interval_15min_title),
|
||||||
|
subtitle = null
|
||||||
|
),
|
||||||
|
RadioOption(
|
||||||
|
value = 30L,
|
||||||
|
title = stringResource(R.string.sync_interval_30min_title),
|
||||||
|
subtitle = null
|
||||||
|
),
|
||||||
|
RadioOption(
|
||||||
|
value = 60L,
|
||||||
|
title = stringResource(R.string.sync_interval_60min_title),
|
||||||
|
subtitle = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsRadioGroup(
|
||||||
|
options = intervalOptions,
|
||||||
|
selectedValue = syncInterval,
|
||||||
|
onValueSelected = { viewModel.setSyncInterval(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
// ADVANCED Section (Boot Sync)
|
||||||
|
// ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
SettingsSectionHeader(text = stringResource(R.string.sync_section_advanced))
|
||||||
|
|
||||||
|
// Boot Trigger
|
||||||
|
SettingsSwitch(
|
||||||
|
title = stringResource(R.string.sync_trigger_boot_title),
|
||||||
|
subtitle = stringResource(R.string.sync_trigger_boot_subtitle),
|
||||||
|
checked = triggerBoot,
|
||||||
|
onCheckedChange = { viewModel.setTriggerBoot(it) },
|
||||||
|
icon = Icons.Default.SettingsInputAntenna,
|
||||||
|
enabled = isServerConfigured
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsDivider()
|
||||||
|
|
||||||
|
// Manual Sync Info
|
||||||
|
val manualHintText = if (isServerConfigured) {
|
||||||
|
stringResource(R.string.sync_manual_hint)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.sync_manual_hint_disabled)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsInfoCard(
|
SettingsInfoCard(
|
||||||
text = stringResource(R.string.sync_interval_info)
|
text = manualHintText
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Interval Radio Group
|
|
||||||
val intervalOptions = listOf(
|
|
||||||
RadioOption(
|
|
||||||
value = 15L,
|
|
||||||
title = stringResource(R.string.sync_interval_15min_title),
|
|
||||||
subtitle = stringResource(R.string.sync_interval_15min_subtitle)
|
|
||||||
),
|
|
||||||
RadioOption(
|
|
||||||
value = 30L,
|
|
||||||
title = stringResource(R.string.sync_interval_30min_title),
|
|
||||||
subtitle = stringResource(R.string.sync_interval_30min_subtitle)
|
|
||||||
),
|
|
||||||
RadioOption(
|
|
||||||
value = 60L,
|
|
||||||
title = stringResource(R.string.sync_interval_60min_title),
|
|
||||||
subtitle = stringResource(R.string.sync_interval_60min_subtitle)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
SettingsRadioGroup(
|
|
||||||
options = intervalOptions,
|
|
||||||
selectedValue = syncInterval,
|
|
||||||
onValueSelected = { viewModel.setSyncInterval(it) }
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package dev.dettmer.simplenotes.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zentrale UI-Dimensionen für konsistentes Design
|
||||||
|
*/
|
||||||
|
object Dimensions {
|
||||||
|
// Padding & Spacing
|
||||||
|
val SpacingSmall = 4.dp
|
||||||
|
val SpacingMedium = 8.dp
|
||||||
|
val SpacingLarge = 16.dp
|
||||||
|
val SpacingXLarge = 24.dp
|
||||||
|
|
||||||
|
// Icon Sizes
|
||||||
|
val IconSizeSmall = 16.dp
|
||||||
|
val IconSizeMedium = 24.dp
|
||||||
|
val IconSizeLarge = 32.dp
|
||||||
|
|
||||||
|
// Minimum Touch Target (Material Design: 48dp)
|
||||||
|
val MinTouchTarget = 48.dp
|
||||||
|
|
||||||
|
// Checklist
|
||||||
|
val ChecklistItemMinHeight = 48.dp
|
||||||
|
|
||||||
|
// Status Bar Heights
|
||||||
|
val StatusBarHeightDefault = 56.dp
|
||||||
|
}
|
||||||
@@ -29,6 +29,27 @@ object Constants {
|
|||||||
// 🔥 v1.3.1: Debug & Logging
|
// 🔥 v1.3.1: Debug & Logging
|
||||||
const val KEY_FILE_LOGGING_ENABLED = "file_logging_enabled"
|
const val KEY_FILE_LOGGING_ENABLED = "file_logging_enabled"
|
||||||
|
|
||||||
|
// 🔥 v1.6.0: Offline Mode Toggle
|
||||||
|
const val KEY_OFFLINE_MODE = "offline_mode_enabled"
|
||||||
|
|
||||||
|
// 🔥 v1.6.0: Configurable Sync Triggers
|
||||||
|
const val KEY_SYNC_TRIGGER_ON_SAVE = "sync_trigger_on_save"
|
||||||
|
const val KEY_SYNC_TRIGGER_ON_RESUME = "sync_trigger_on_resume"
|
||||||
|
const val KEY_SYNC_TRIGGER_WIFI_CONNECT = "sync_trigger_wifi_connect"
|
||||||
|
const val KEY_SYNC_TRIGGER_PERIODIC = "sync_trigger_periodic"
|
||||||
|
const val KEY_SYNC_TRIGGER_BOOT = "sync_trigger_boot"
|
||||||
|
|
||||||
|
// Sync Trigger Defaults (active after server configuration)
|
||||||
|
const val DEFAULT_TRIGGER_ON_SAVE = true
|
||||||
|
const val DEFAULT_TRIGGER_ON_RESUME = true
|
||||||
|
const val DEFAULT_TRIGGER_WIFI_CONNECT = true
|
||||||
|
const val DEFAULT_TRIGGER_PERIODIC = false
|
||||||
|
const val DEFAULT_TRIGGER_BOOT = false
|
||||||
|
|
||||||
|
// Throttling for onSave sync (5 seconds)
|
||||||
|
const val MIN_ON_SAVE_SYNC_INTERVAL_MS = 5_000L
|
||||||
|
const val PREF_LAST_ON_SAVE_SYNC_TIME = "last_on_save_sync_time"
|
||||||
|
|
||||||
// WorkManager
|
// WorkManager
|
||||||
const val SYNC_WORK_TAG = "notes_sync"
|
const val SYNC_WORK_TAG = "notes_sync"
|
||||||
const val SYNC_DELAY_SECONDS = 5L
|
const val SYNC_DELAY_SECONDS = 5L
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.dettmer.simplenotes.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konstanten für Sync-Operationen
|
||||||
|
*/
|
||||||
|
object SyncConstants {
|
||||||
|
// Debounce Delays
|
||||||
|
const val SEARCH_DEBOUNCE_MS = 300L
|
||||||
|
const val SYNC_DEBOUNCE_MS = 500L
|
||||||
|
|
||||||
|
// Connection Timeouts
|
||||||
|
const val CONNECTION_TEST_TIMEOUT_MS = 5000L
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@
|
|||||||
<string name="delete_note_message">Wie möchtest du diese Notiz löschen?</string>
|
<string name="delete_note_message">Wie möchtest du diese Notiz löschen?</string>
|
||||||
<string name="delete_notes_message">Wie möchtest du diese %d Notizen löschen?</string>
|
<string name="delete_notes_message">Wie möchtest du diese %d Notizen löschen?</string>
|
||||||
<string name="delete_everywhere">Überall löschen (auch Server)</string>
|
<string name="delete_everywhere">Überall löschen (auch Server)</string>
|
||||||
|
<string name="delete_everywhere_offline_hint">Nicht verfügbar im Offline-Modus</string>
|
||||||
<string name="delete_local_only">Nur lokal löschen</string>
|
<string name="delete_local_only">Nur lokal löschen</string>
|
||||||
<string name="delete">Löschen</string>
|
<string name="delete">Löschen</string>
|
||||||
<string name="cancel">Abbrechen</string>
|
<string name="cancel">Abbrechen</string>
|
||||||
@@ -135,9 +136,13 @@
|
|||||||
<string name="settings_server_status_unreachable">❌ Nicht erreichbar</string>
|
<string name="settings_server_status_unreachable">❌ Nicht erreichbar</string>
|
||||||
<string name="settings_server_status_checking">🔍 Prüfe…</string>
|
<string name="settings_server_status_checking">🔍 Prüfe…</string>
|
||||||
<string name="settings_server_status_not_configured">⚠️ Nicht konfiguriert</string>
|
<string name="settings_server_status_not_configured">⚠️ Nicht konfiguriert</string>
|
||||||
|
<string name="settings_server_status_offline_mode">📴 Offline-Modus</string>
|
||||||
<string name="settings_sync">Sync-Einstellungen</string>
|
<string name="settings_sync">Sync-Einstellungen</string>
|
||||||
<string name="settings_sync_auto_on">Auto-Sync: An • %s</string>
|
<string name="settings_sync_auto_on">Auto-Sync: An • %s</string>
|
||||||
<string name="settings_sync_auto_off">Auto-Sync: Aus</string>
|
<string name="settings_sync_auto_off">Auto-Sync: Aus</string>
|
||||||
|
<string name="settings_sync_offline_mode">📴 Offline-Modus</string>
|
||||||
|
<string name="settings_sync_manual_only">Nur manueller Sync</string>
|
||||||
|
<string name="settings_sync_triggers_active">%d Trigger aktiv</string>
|
||||||
<string name="settings_interval_15min">15 Min</string>
|
<string name="settings_interval_15min">15 Min</string>
|
||||||
<string name="settings_interval_30min">30 Min</string>
|
<string name="settings_interval_30min">30 Min</string>
|
||||||
<string name="settings_interval_60min">60 Min</string>
|
<string name="settings_interval_60min">60 Min</string>
|
||||||
@@ -173,7 +178,10 @@
|
|||||||
<string name="server_status_unreachable">❌ Nicht erreichbar</string>
|
<string name="server_status_unreachable">❌ Nicht erreichbar</string>
|
||||||
<string name="server_status_checking">🔍 Prüfe…</string>
|
<string name="server_status_checking">🔍 Prüfe…</string>
|
||||||
<string name="server_status_not_configured">⚠️ Nicht konfiguriert</string>
|
<string name="server_status_not_configured">⚠️ Nicht konfiguriert</string>
|
||||||
|
<string name="server_status_offline_mode">📴 Offline-Modus aktiv</string>
|
||||||
<string name="server_status_unknown">❓ Unbekannt</string>
|
<string name="server_status_unknown">❓ Unbekannt</string>
|
||||||
|
<string name="server_offline_mode_title">📴 Offline-Modus</string>
|
||||||
|
<string name="server_offline_mode_subtitle">Alle Netzwerkfunktionen deaktivieren</string>
|
||||||
<string name="test_connection">Verbindung testen</string>
|
<string name="test_connection">Verbindung testen</string>
|
||||||
<string name="sync_now">Jetzt synchronisieren</string>
|
<string name="sync_now">Jetzt synchronisieren</string>
|
||||||
|
|
||||||
@@ -196,6 +204,33 @@
|
|||||||
<!-- Legacy -->
|
<!-- Legacy -->
|
||||||
<string name="auto_sync_info">ℹ️ Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)</string>
|
<string name="auto_sync_info">ℹ️ Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)</string>
|
||||||
|
|
||||||
|
<!-- 🌟 v1.6.0: Configurable Sync Triggers -->
|
||||||
|
<string name="sync_section_instant">📲 Sofort-Sync</string>
|
||||||
|
<string name="sync_section_background">📡 Hintergrund-Sync</string>
|
||||||
|
<string name="sync_section_advanced">⚙️ Erweitert</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_on_save_title">Nach dem Speichern</string>
|
||||||
|
<string name="sync_trigger_on_save_subtitle">Sync sofort nach jeder Änderung</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_on_resume_title">Beim App-Start</string>
|
||||||
|
<string name="sync_trigger_on_resume_subtitle">Sync wenn die App geöffnet wird</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_wifi_connect_title">Bei WiFi-Verbindung</string>
|
||||||
|
<string name="sync_trigger_wifi_connect_subtitle">Sync wenn WiFi verbunden wird</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_periodic_title">Automatisch alle X Minuten</string>
|
||||||
|
<string name="sync_trigger_periodic_subtitle">Regelmäßiger Hintergrund-Sync</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_boot_title">Nach Gerät-Neustart</string>
|
||||||
|
<string name="sync_trigger_boot_subtitle">Startet Hintergrund-Sync nach Reboot</string>
|
||||||
|
|
||||||
|
<string name="sync_manual_hint">Manueller Sync (Toolbar/Pull-to-Refresh) ist ebenfalls verfügbar.</string>
|
||||||
|
<string name="sync_manual_hint_disabled">Sync ist im Offline-Modus nicht verfügbar.</string>
|
||||||
|
|
||||||
|
<string name="sync_offline_mode_title">Offline-Modus</string>
|
||||||
|
<string name="sync_offline_mode_message">Du nutzt die App im Offline-Modus. Richte einen Server ein, um Notizen zu synchronisieren.</string>
|
||||||
|
<string name="sync_offline_mode_button">Server einrichten</string>
|
||||||
|
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
<!-- SETTINGS - MARKDOWN -->
|
<!-- SETTINGS - MARKDOWN -->
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
<string name="delete_note_message">How do you want to delete this note?</string>
|
<string name="delete_note_message">How do you want to delete this note?</string>
|
||||||
<string name="delete_notes_message">How do you want to delete these %d notes?</string>
|
<string name="delete_notes_message">How do you want to delete these %d notes?</string>
|
||||||
<string name="delete_everywhere">Delete everywhere (also server)</string>
|
<string name="delete_everywhere">Delete everywhere (also server)</string>
|
||||||
|
<string name="delete_everywhere_offline_hint">Not available in offline mode</string>
|
||||||
<string name="delete_local_only">Delete local only</string>
|
<string name="delete_local_only">Delete local only</string>
|
||||||
<string name="delete">Delete</string>
|
<string name="delete">Delete</string>
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
@@ -136,9 +137,13 @@
|
|||||||
<string name="settings_server_status_unreachable">❌ Not reachable</string>
|
<string name="settings_server_status_unreachable">❌ Not reachable</string>
|
||||||
<string name="settings_server_status_checking">🔍 Checking…</string>
|
<string name="settings_server_status_checking">🔍 Checking…</string>
|
||||||
<string name="settings_server_status_not_configured">⚠️ Not configured</string>
|
<string name="settings_server_status_not_configured">⚠️ Not configured</string>
|
||||||
|
<string name="settings_server_status_offline_mode">📴 Offline Mode</string>
|
||||||
<string name="settings_sync">Sync Settings</string>
|
<string name="settings_sync">Sync Settings</string>
|
||||||
<string name="settings_sync_auto_on">Auto-Sync: On • %s</string>
|
<string name="settings_sync_auto_on">Auto-Sync: On • %s</string>
|
||||||
<string name="settings_sync_auto_off">Auto-Sync: Off</string>
|
<string name="settings_sync_auto_off">Auto-Sync: Off</string>
|
||||||
|
<string name="settings_sync_offline_mode">📴 Offline Mode</string>
|
||||||
|
<string name="settings_sync_manual_only">Manual sync only</string>
|
||||||
|
<string name="settings_sync_triggers_active">%d triggers active</string>
|
||||||
<string name="settings_interval_15min">15 min</string>
|
<string name="settings_interval_15min">15 min</string>
|
||||||
<string name="settings_interval_30min">30 min</string>
|
<string name="settings_interval_30min">30 min</string>
|
||||||
<string name="settings_interval_60min">60 min</string>
|
<string name="settings_interval_60min">60 min</string>
|
||||||
@@ -174,7 +179,10 @@
|
|||||||
<string name="server_status_unreachable">❌ Not reachable</string>
|
<string name="server_status_unreachable">❌ Not reachable</string>
|
||||||
<string name="server_status_checking">🔍 Checking…</string>
|
<string name="server_status_checking">🔍 Checking…</string>
|
||||||
<string name="server_status_not_configured">⚠️ Not configured</string>
|
<string name="server_status_not_configured">⚠️ Not configured</string>
|
||||||
|
<string name="server_status_offline_mode">📴 Offline mode active</string>
|
||||||
<string name="server_status_unknown">❓ Unknown</string>
|
<string name="server_status_unknown">❓ Unknown</string>
|
||||||
|
<string name="server_offline_mode_title">📴 Offline Mode</string>
|
||||||
|
<string name="server_offline_mode_subtitle">Disable all network features</string>
|
||||||
<string name="test_connection">Test Connection</string>
|
<string name="test_connection">Test Connection</string>
|
||||||
<string name="sync_now">Sync now</string>
|
<string name="sync_now">Sync now</string>
|
||||||
|
|
||||||
@@ -197,6 +205,33 @@
|
|||||||
<!-- Legacy -->
|
<!-- Legacy -->
|
||||||
<string name="auto_sync_info">ℹ️ Auto-Sync:\n\n• Checks every 30 min if server is reachable\n• Works on any WiFi connection\n• Runs in background\n• Minimal battery usage (~0.4%%/day)</string>
|
<string name="auto_sync_info">ℹ️ Auto-Sync:\n\n• Checks every 30 min if server is reachable\n• Works on any WiFi connection\n• Runs in background\n• Minimal battery usage (~0.4%%/day)</string>
|
||||||
|
|
||||||
|
<!-- 🌟 v1.6.0: Configurable Sync Triggers -->
|
||||||
|
<string name="sync_section_instant">📲 Instant Sync</string>
|
||||||
|
<string name="sync_section_background">📡 Background Sync</string>
|
||||||
|
<string name="sync_section_advanced">⚙️ Advanced</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_on_save_title">After Saving</string>
|
||||||
|
<string name="sync_trigger_on_save_subtitle">Sync immediately after each change</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_on_resume_title">On App Start</string>
|
||||||
|
<string name="sync_trigger_on_resume_subtitle">Sync when the app is opened</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_wifi_connect_title">On WiFi Connection</string>
|
||||||
|
<string name="sync_trigger_wifi_connect_subtitle">Sync when WiFi is connected</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_periodic_title">Automatically every X minutes</string>
|
||||||
|
<string name="sync_trigger_periodic_subtitle">Regular background sync</string>
|
||||||
|
|
||||||
|
<string name="sync_trigger_boot_title">After Device Restart</string>
|
||||||
|
<string name="sync_trigger_boot_subtitle">Starts background sync after reboot</string>
|
||||||
|
|
||||||
|
<string name="sync_manual_hint">Manual sync (toolbar/pull-to-refresh) is also available.</string>
|
||||||
|
<string name="sync_manual_hint_disabled">Sync is not available in offline mode.</string>
|
||||||
|
|
||||||
|
<string name="sync_offline_mode_title">Offline Mode</string>
|
||||||
|
<string name="sync_offline_mode_message">You are using the app in offline mode. Set up a server to synchronize notes.</string>
|
||||||
|
<string name="sync_offline_mode_button">Set Up Server</string>
|
||||||
|
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
<!-- SETTINGS - MARKDOWN -->
|
<!-- SETTINGS - MARKDOWN -->
|
||||||
<!-- ============================= -->
|
<!-- ============================= -->
|
||||||
|
|||||||
@@ -174,30 +174,68 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
|
|||||||
|
|
||||||
## 🔋 Akku-Optimierung
|
## 🔋 Akku-Optimierung
|
||||||
|
|
||||||
### Verbrauchsanalyse
|
### v1.6.0: Konfigurierbare Sync-Trigger
|
||||||
|
|
||||||
|
Seit v1.6.0 kann jeder Sync-Trigger einzeln aktiviert/deaktiviert werden. Das gibt Nutzern feine Kontrolle über den Akkuverbrauch.
|
||||||
|
|
||||||
|
#### Sync-Trigger Übersicht
|
||||||
|
|
||||||
|
| Trigger | Standard | Akku-Impact | Beschreibung |
|
||||||
|
|---------|----------|-------------|--------------|
|
||||||
|
| **Manueller Sync** | Immer an | 0 (nutzer-getriggert) | Toolbar-Button / Pull-to-Refresh |
|
||||||
|
| **onSave Sync** | ✅ AN | ~0.5 mAh/Speichern | Sync sofort nach Speichern einer Notiz |
|
||||||
|
| **onResume Sync** | ✅ AN | ~0.3 mAh/Öffnen | Sync beim App-Öffnen (60s Throttle) |
|
||||||
|
| **WiFi-Connect** | ✅ AN | ~0.5 mAh/Verbindung | Sync bei WiFi-Verbindung |
|
||||||
|
| **Periodic Sync** | ❌ AUS | 0.2-0.8%/Tag | Hintergrund-Sync alle 15/30/60 Min |
|
||||||
|
| **Boot Sync** | ❌ AUS | ~0.1 mAh/Boot | Start Hintergrund-Sync nach Neustart |
|
||||||
|
|
||||||
|
#### Akku-Verbrauchsberechnung
|
||||||
|
|
||||||
|
**Typisches Nutzungsszenario (Standardeinstellungen):**
|
||||||
|
- onSave: ~5 Speichern/Tag × 0.5 mAh = **~2.5 mAh**
|
||||||
|
- onResume: ~10 Öffnen/Tag × 0.3 mAh = **~3 mAh**
|
||||||
|
- WiFi-Connect: ~2 Verbindungen/Tag × 0.5 mAh = **~1 mAh**
|
||||||
|
- **Gesamt: ~6.5 mAh/Tag (~0.2% bei 3000mAh Akku)**
|
||||||
|
|
||||||
|
**Mit aktiviertem Periodic Sync (15/30/60 Min):**
|
||||||
|
|
||||||
|
| Intervall | Syncs/Tag | Akku/Tag | Gesamt (mit Standards) |
|
||||||
|
|-----------|-----------|----------|------------------------|
|
||||||
|
| **15 Min** | ~96 | ~23 mAh | ~30 mAh (~1.0%) |
|
||||||
|
| **30 Min** | ~48 | ~12 mAh | ~19 mAh (~0.6%) |
|
||||||
|
| **60 Min** | ~24 | ~6 mAh | ~13 mAh (~0.4%) |
|
||||||
|
|
||||||
|
#### Komponenten-Aufschlüsselung
|
||||||
|
|
||||||
| Komponente | Frequenz | Verbrauch | Details |
|
| Komponente | Frequenz | Verbrauch | Details |
|
||||||
|------------|----------|-----------|---------|
|
|------------|----------|-----------|---------|
|
||||||
| WorkManager Wakeup | Alle 30 Min | ~0.15 mAh | System wacht auf |
|
| WorkManager Wakeup | Pro Sync | ~0.15 mAh | System wacht auf |
|
||||||
| Network Check | 48x/Tag | ~0.03 mAh | Gateway IP check |
|
| Network Check | Pro Sync | ~0.03 mAh | Gateway IP Check |
|
||||||
| WebDAV Sync | 2-3x/Tag | ~1.5 mAh | Nur bei Änderungen |
|
| WebDAV Sync | Nur bei Änderungen | ~0.25 mAh | HTTP PUT/GET |
|
||||||
| **Total** | - | **~12 mAh/Tag** | **~0.4%** bei 3000mAh |
|
| **Pro-Sync Gesamt** | - | **~0.25 mAh** | Optimiert |
|
||||||
|
|
||||||
### Optimierungen
|
### Optimierungen
|
||||||
|
|
||||||
1. **IP Caching**
|
1. **Pre-Checks vor Sync**
|
||||||
|
```kotlin
|
||||||
|
// Reihenfolge wichtig! Günstigste Checks zuerst
|
||||||
|
if (!hasUnsyncedChanges()) return // Lokaler Check (günstig)
|
||||||
|
if (!isServerReachable()) return // Netzwerk Check (teuer)
|
||||||
|
performSync() // Nur wenn beide bestehen
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Throttling**
|
||||||
|
- onResume: 60 Sekunden Mindestabstand
|
||||||
|
- onSave: 5 Sekunden Mindestabstand
|
||||||
|
- Periodic: 15/30/60 Minuten Intervalle
|
||||||
|
|
||||||
|
3. **IP Caching**
|
||||||
```kotlin
|
```kotlin
|
||||||
private var cachedServerIP: String? = null
|
private var cachedServerIP: String? = null
|
||||||
// DNS lookup nur 1x beim Start, nicht bei jedem Check
|
// DNS lookup nur 1x beim Start, nicht bei jedem Check
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Throttling**
|
4. **Conditional Logging**
|
||||||
```kotlin
|
|
||||||
private var lastSyncTime = 0L
|
|
||||||
private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 Sync/Min
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Conditional Logging**
|
|
||||||
```kotlin
|
```kotlin
|
||||||
object Logger {
|
object Logger {
|
||||||
fun d(tag: String, msg: String) {
|
fun d(tag: String, msg: String) {
|
||||||
@@ -206,7 +244,7 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Network Constraints**
|
5. **Network Constraints**
|
||||||
- Nur WiFi (nicht mobile Daten)
|
- Nur WiFi (nicht mobile Daten)
|
||||||
- Nur wenn Server erreichbar
|
- Nur wenn Server erreichbar
|
||||||
- Keine permanenten Listeners
|
- Keine permanenten Listeners
|
||||||
|
|||||||
68
docs/DOCS.md
@@ -174,30 +174,68 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
|
|||||||
|
|
||||||
## 🔋 Battery Optimization
|
## 🔋 Battery Optimization
|
||||||
|
|
||||||
### Usage Analysis
|
### v1.6.0: Configurable Sync Triggers
|
||||||
|
|
||||||
|
Since v1.6.0, each sync trigger can be individually enabled/disabled. This gives users fine-grained control over battery usage.
|
||||||
|
|
||||||
|
#### Sync Trigger Overview
|
||||||
|
|
||||||
|
| Trigger | Default | Battery Impact | Description |
|
||||||
|
|---------|---------|----------------|-------------|
|
||||||
|
| **Manual Sync** | Always on | 0 (user-triggered) | Toolbar button / Pull-to-refresh |
|
||||||
|
| **onSave Sync** | ✅ ON | ~0.5 mAh/save | Sync immediately after saving a note |
|
||||||
|
| **onResume Sync** | ✅ ON | ~0.3 mAh/resume | Sync when app is opened (60s throttle) |
|
||||||
|
| **WiFi-Connect** | ✅ ON | ~0.5 mAh/connect | Sync when WiFi is connected |
|
||||||
|
| **Periodic Sync** | ❌ OFF | 0.2-0.8%/day | Background sync every 15/30/60 min |
|
||||||
|
| **Boot Sync** | ❌ OFF | ~0.1 mAh/boot | Start background sync after reboot |
|
||||||
|
|
||||||
|
#### Battery Usage Calculation
|
||||||
|
|
||||||
|
**Typical usage scenario (defaults):**
|
||||||
|
- onSave: ~5 saves/day × 0.5 mAh = **~2.5 mAh**
|
||||||
|
- onResume: ~10 opens/day × 0.3 mAh = **~3 mAh**
|
||||||
|
- WiFi-Connect: ~2 connects/day × 0.5 mAh = **~1 mAh**
|
||||||
|
- **Total: ~6.5 mAh/day (~0.2% on 3000mAh battery)**
|
||||||
|
|
||||||
|
**With Periodic Sync enabled (15/30/60 min):**
|
||||||
|
|
||||||
|
| Interval | Syncs/day | Battery/day | Total (with defaults) |
|
||||||
|
|----------|-----------|-------------|----------------------|
|
||||||
|
| **15 min** | ~96 | ~23 mAh | ~30 mAh (~1.0%) |
|
||||||
|
| **30 min** | ~48 | ~12 mAh | ~19 mAh (~0.6%) |
|
||||||
|
| **60 min** | ~24 | ~6 mAh | ~13 mAh (~0.4%) |
|
||||||
|
|
||||||
|
#### Component Breakdown
|
||||||
|
|
||||||
| Component | Frequency | Usage | Details |
|
| Component | Frequency | Usage | Details |
|
||||||
|------------|----------|-----------|---------|
|
|-----------|-----------|-------|---------|
|
||||||
| WorkManager Wakeup | Every 30 min | ~0.15 mAh | System wakes up |
|
| WorkManager Wakeup | Per sync | ~0.15 mAh | System wakes up |
|
||||||
| Network Check | 48x/day | ~0.03 mAh | Gateway IP check |
|
| Network Check | Per sync | ~0.03 mAh | Gateway IP check |
|
||||||
| WebDAV Sync | 2-3x/day | ~1.5 mAh | Only when changes |
|
| WebDAV Sync | Only if changes | ~0.25 mAh | HTTP PUT/GET |
|
||||||
| **Total** | - | **~12 mAh/day** | **~0.4%** at 3000mAh |
|
| **Per-Sync Total** | - | **~0.25 mAh** | Optimized |
|
||||||
|
|
||||||
### Optimizations
|
### Optimizations
|
||||||
|
|
||||||
1. **IP Caching**
|
1. **Pre-Checks before Sync**
|
||||||
|
```kotlin
|
||||||
|
// Order matters! Cheapest checks first
|
||||||
|
if (!hasUnsyncedChanges()) return // Local check (cheap)
|
||||||
|
if (!isServerReachable()) return // Network check (expensive)
|
||||||
|
performSync() // Only if both pass
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Throttling**
|
||||||
|
- onResume: 60 second minimum interval
|
||||||
|
- onSave: 5 second minimum interval
|
||||||
|
- Periodic: 15/30/60 minute intervals
|
||||||
|
|
||||||
|
3. **IP Caching**
|
||||||
```kotlin
|
```kotlin
|
||||||
private var cachedServerIP: String? = null
|
private var cachedServerIP: String? = null
|
||||||
// DNS lookup only once at start, not every check
|
// DNS lookup only once at start, not every check
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Throttling**
|
4. **Conditional Logging**
|
||||||
```kotlin
|
|
||||||
private var lastSyncTime = 0L
|
|
||||||
private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 sync/min
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Conditional Logging**
|
|
||||||
```kotlin
|
```kotlin
|
||||||
object Logger {
|
object Logger {
|
||||||
fun d(tag: String, msg: String) {
|
fun d(tag: String, msg: String) {
|
||||||
@@ -206,7 +244,7 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Network Constraints**
|
5. **Network Constraints**
|
||||||
- WiFi only (not mobile data)
|
- WiFi only (not mobile data)
|
||||||
- Only when server is reachable
|
- Only when server is reachable
|
||||||
- No permanent listeners
|
- No permanent listeners
|
||||||
|
|||||||
@@ -169,16 +169,19 @@
|
|||||||
|
|
||||||
## 🔋 Performance & Optimierung
|
## 🔋 Performance & Optimierung
|
||||||
|
|
||||||
### Akku-Effizienz
|
### Akku-Effizienz (v1.6.0)
|
||||||
- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min
|
- ✅ **Konfigurierbare Sync-Trigger** - Jeden Trigger einzeln aktivieren/deaktivieren
|
||||||
|
- ✅ **Smarte Defaults** - Nur ereignisbasierte Trigger standardmäßig aktiv
|
||||||
|
- ✅ **Optimierte Periodische Intervalle** - 15/30/60 Min (Standard: AUS)
|
||||||
- ✅ **WiFi-Only** - Kein Mobile Data Sync
|
- ✅ **WiFi-Only** - Kein Mobile Data Sync
|
||||||
- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar
|
- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar
|
||||||
- ✅ **WorkManager** - System-optimierte Ausführung
|
- ✅ **WorkManager** - System-optimierte Ausführung
|
||||||
- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby
|
- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby
|
||||||
- ✅ **Gemessener Verbrauch:**
|
- ✅ **Gemessener Verbrauch:**
|
||||||
- 15 Min: ~0.8% / Tag (~23 mAh)
|
- Standard (nur ereignisbasiert): ~0.2%/Tag (~6.5 mAh) ⭐ _Optimal_
|
||||||
- 30 Min: ~0.4% / Tag (~12 mAh) ⭐ _Empfohlen_
|
- Mit Periodic 15 Min: ~1.0%/Tag (~30 mAh)
|
||||||
- 60 Min: ~0.2% / Tag (~6 mAh)
|
- Mit Periodic 30 Min: ~0.6%/Tag (~19 mAh)
|
||||||
|
- Mit Periodic 60 Min: ~0.4%/Tag (~13 mAh)
|
||||||
|
|
||||||
### App-Performance
|
### App-Performance
|
||||||
- ✅ **Offline-First** - Funktioniert ohne Internet
|
- ✅ **Offline-First** - Funktioniert ohne Internet
|
||||||
|
|||||||
@@ -169,16 +169,19 @@
|
|||||||
|
|
||||||
## 🔋 Performance & Optimization
|
## 🔋 Performance & Optimization
|
||||||
|
|
||||||
### Battery Efficiency
|
### Battery Efficiency (v1.6.0)
|
||||||
- ✅ **Optimized sync intervals** - 15/30/60 min
|
- ✅ **Configurable sync triggers** - Enable/disable each trigger individually
|
||||||
|
- ✅ **Smart defaults** - Only event-driven triggers active by default
|
||||||
|
- ✅ **Optimized periodic intervals** - 15/30/60 min (default: OFF)
|
||||||
- ✅ **WiFi-only** - No mobile data sync
|
- ✅ **WiFi-only** - No mobile data sync
|
||||||
- ✅ **Smart server check** - Sync only when server is reachable
|
- ✅ **Smart server check** - Sync only when server is reachable
|
||||||
- ✅ **WorkManager** - System-optimized execution
|
- ✅ **WorkManager** - System-optimized execution
|
||||||
- ✅ **Doze mode compatible** - Sync runs even in standby
|
- ✅ **Doze mode compatible** - Sync runs even in standby
|
||||||
- ✅ **Measured consumption:**
|
- ✅ **Measured consumption:**
|
||||||
- 15 min: ~0.8% / day (~23 mAh)
|
- Default (event-driven only): ~0.2%/day (~6.5 mAh) ⭐ _Optimal_
|
||||||
- 30 min: ~0.4% / day (~12 mAh) ⭐ _Recommended_
|
- With periodic 15 min: ~1.0%/day (~30 mAh)
|
||||||
- 60 min: ~0.2% / day (~6 mAh)
|
- With periodic 30 min: ~0.6%/day (~19 mAh)
|
||||||
|
- With periodic 60 min: ~0.4%/day (~13 mAh)
|
||||||
|
|
||||||
### App Performance
|
### App Performance
|
||||||
- ✅ **Offline-first** - Works without internet
|
- ✅ **Offline-first** - Works without internet
|
||||||
|
|||||||
@@ -31,9 +31,46 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.6.0 - Technische Modernisierung
|
## v1.6.0 - Technische Modernisierung ✅
|
||||||
|
|
||||||
> **Status:** In Planung 📋
|
> **Status:** Released 🎉 (Januar 2026)
|
||||||
|
|
||||||
|
### ⚙️ Konfigurierbare Sync-Trigger
|
||||||
|
|
||||||
|
- ✅ **Individuelle Trigger-Kontrolle** - Jeden Sync-Trigger einzeln aktivieren/deaktivieren
|
||||||
|
- ✅ **Ereignisbasierte Defaults** - onSave, onResume, WiFi-Connect standardmäßig aktiv
|
||||||
|
- ✅ **Periodischer Sync optional** - 15/30/60 Min Intervalle (Standard: AUS)
|
||||||
|
- ✅ **Boot Sync optional** - Periodischen Sync nach Geräteneustart starten (Standard: AUS)
|
||||||
|
- ✅ **Offline-Modus UI** - Ausgegraute Toggles wenn kein Server konfiguriert
|
||||||
|
- ✅ **Akku-optimiert** - ~0.2%/Tag mit Defaults, bis zu ~1.0% mit Periodic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.6.1 - Clean Code ✅
|
||||||
|
|
||||||
|
> **Status:** Released 🎉 (Januar 2026)
|
||||||
|
|
||||||
|
### 🧹 Code-Qualität
|
||||||
|
|
||||||
|
- ✅ **detekt: 0 Issues** - Alle 29 Code-Qualitäts-Issues behoben
|
||||||
|
- ✅ **Zero Build Warnings** - Alle 21 Deprecation Warnings eliminiert
|
||||||
|
- ✅ **ktlint reaktiviert** - Mit Compose-spezifischen Regeln
|
||||||
|
- ✅ **CI/CD Lint-Checks** - In PR Build Workflow integriert
|
||||||
|
- ✅ **Constants Refactoring** - Dimensions.kt, SyncConstants.kt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.7.0 - Staggered Grid Layout
|
||||||
|
|
||||||
|
> **Status:** Geplant 📝
|
||||||
|
|
||||||
|
### 🎨 Adaptives 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
|
||||||
|
|
||||||
### 🔧 Server-Ordner Prüfung
|
### 🔧 Server-Ordner Prüfung
|
||||||
|
|
||||||
@@ -43,22 +80,43 @@
|
|||||||
|
|
||||||
### 🔧 Technische Verbesserungen
|
### 🔧 Technische Verbesserungen
|
||||||
|
|
||||||
- **Code-Refactoring** - LongMethod und LargeClass Warnings beheben
|
- **Code-Refactoring** - LargeClass Komponenten aufteilen (WebDavSyncService, SettingsActivity)
|
||||||
- **Modernere Background-Sync Architektur** - Noch zuverlässiger
|
|
||||||
- **Verbesserte Progress-Dialoge** - Material Design 3 konform
|
- **Verbesserte Progress-Dialoge** - Material Design 3 konform
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.7.0 - Community Features
|
## v2.0.0 - Legacy Cleanup
|
||||||
|
|
||||||
> **Status:** Ideen-Sammlung 💡
|
> **Status:** Geplant 📝
|
||||||
|
|
||||||
### Mögliche Features
|
### 🗑️ Legacy Code Entfernung
|
||||||
|
|
||||||
- **Zusätzliche Sprachen** - Community-Übersetzungen (FR, ES, IT, ...)
|
- **SettingsActivity entfernen** - Ersetzt durch ComposeSettingsActivity
|
||||||
|
- **MainActivity entfernen** - Ersetzt durch ComposeMainActivity
|
||||||
|
- **LocalBroadcastManager → SharedFlow** - Moderne Event-Architektur
|
||||||
|
- **ProgressDialog → Material Dialog** - Volle Material 3 Konformität
|
||||||
|
- **AbstractSavedStateViewModelFactory → viewModelFactory** - Moderne ViewModel-Erstellung
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Backlog
|
||||||
|
|
||||||
|
> Features für zukünftige Überlegungen
|
||||||
|
|
||||||
|
### 🔐 Sicherheits-Verbesserungen
|
||||||
|
|
||||||
|
- **Passwortgeschützte lokale Backups** - Backup-ZIP mit Passwort verschlüsseln
|
||||||
|
- **Biometrische Entsperrung** - Fingerabdruck/Gesichtserkennung für App
|
||||||
|
|
||||||
|
### 🎨 UI Features
|
||||||
|
|
||||||
|
- **Widget** - Schnellzugriff vom Homescreen
|
||||||
- **Kategorien/Tags** - Notizen organisieren
|
- **Kategorien/Tags** - Notizen organisieren
|
||||||
- **Suche** - Volltextsuche in Notizen
|
- **Suche** - Volltextsuche in Notizen
|
||||||
- **Widget** - Schnellzugriff vom Homescreen
|
|
||||||
|
### 🌍 Community
|
||||||
|
|
||||||
|
- **Zusätzliche Sprachen** - Community-Übersetzungen (FR, ES, IT, ...)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -31,9 +31,46 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.6.0 - Technical Modernization
|
## v1.6.0 - Technical Modernization ✅
|
||||||
|
|
||||||
> **Status:** Planned 📋
|
> **Status:** Released 🎉 (January 2026)
|
||||||
|
|
||||||
|
### ⚙️ Configurable Sync Triggers
|
||||||
|
|
||||||
|
- ✅ **Individual trigger control** - Enable/disable each sync trigger separately
|
||||||
|
- ✅ **Event-driven defaults** - onSave, onResume, WiFi-Connect active by default
|
||||||
|
- ✅ **Periodic sync optional** - 15/30/60 min intervals (default: OFF)
|
||||||
|
- ✅ **Boot sync optional** - Start periodic sync after device restart (default: OFF)
|
||||||
|
- ✅ **Offline mode UI** - Grayed-out toggles when no server configured
|
||||||
|
- ✅ **Battery optimized** - ~0.2%/day with defaults, up to ~1.0% with periodic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.6.1 - Clean Code ✅
|
||||||
|
|
||||||
|
> **Status:** Released 🎉 (January 2026)
|
||||||
|
|
||||||
|
### 🧹 Code Quality
|
||||||
|
|
||||||
|
- ✅ **detekt: 0 issues** - All 29 code quality issues fixed
|
||||||
|
- ✅ **Zero build warnings** - All 21 deprecation warnings eliminated
|
||||||
|
- ✅ **ktlint reactivated** - With Compose-specific rules
|
||||||
|
- ✅ **CI/CD lint checks** - Integrated into PR build workflow
|
||||||
|
- ✅ **Constants refactoring** - Dimensions.kt, SyncConstants.kt
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v1.7.0 - Staggered Grid Layout
|
||||||
|
|
||||||
|
> **Status:** Planned 📝
|
||||||
|
|
||||||
|
### 🎨 Adaptive 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
|
||||||
|
|
||||||
### 🔧 Server Folder Check
|
### 🔧 Server Folder Check
|
||||||
|
|
||||||
@@ -43,22 +80,43 @@
|
|||||||
|
|
||||||
### 🔧 Technical Improvements
|
### 🔧 Technical Improvements
|
||||||
|
|
||||||
- **Code refactoring** - Fix LongMethod and LargeClass warnings
|
- **Code refactoring** - Split LargeClass components (WebDavSyncService, SettingsActivity)
|
||||||
- **Modern background sync architecture** - Even more reliable
|
|
||||||
- **Improved progress dialogs** - Material Design 3 compliant
|
- **Improved progress dialogs** - Material Design 3 compliant
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## v1.7.0 - Community Features
|
## v2.0.0 - Legacy Cleanup
|
||||||
|
|
||||||
> **Status:** Idea Collection 💡
|
> **Status:** Planned 📝
|
||||||
|
|
||||||
### Potential Features
|
### 🗑️ Legacy Code Removal
|
||||||
|
|
||||||
- **Additional languages** - Community translations (FR, ES, IT, ...)
|
- **Remove SettingsActivity** - Replaced by ComposeSettingsActivity
|
||||||
|
- **Remove MainActivity** - Replaced by ComposeMainActivity
|
||||||
|
- **LocalBroadcastManager → SharedFlow** - Modern event architecture
|
||||||
|
- **ProgressDialog → Material Dialog** - Full Material 3 compliance
|
||||||
|
- **AbstractSavedStateViewModelFactory → viewModelFactory** - Modern ViewModel creation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Backlog
|
||||||
|
|
||||||
|
> Features for future consideration
|
||||||
|
|
||||||
|
### 🔐 Security Enhancements
|
||||||
|
|
||||||
|
- **Password-protected local backups** - Encrypt backup ZIP with password
|
||||||
|
- **Biometric unlock option** - Fingerprint/Face unlock for app
|
||||||
|
|
||||||
|
### 🎨 UI Features
|
||||||
|
|
||||||
|
- **Widget** - Quick access from homescreen
|
||||||
- **Categories/Tags** - Organize notes
|
- **Categories/Tags** - Organize notes
|
||||||
- **Search** - Full-text search in notes
|
- **Search** - Full-text search in notes
|
||||||
- **Widget** - Quick access from homescreen
|
|
||||||
|
### 🌍 Community
|
||||||
|
|
||||||
|
- **Additional languages** - Community translations (FR, ES, IT, ...)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
6
fastlane/metadata/android/de-DE/changelogs/14.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
• NEU: Konfigurierbare Sync-Trigger - Jeden einzeln aktivieren/deaktivieren
|
||||||
|
• NEU: Offline-Modus - Alle Netzwerkfunktionen mit einem Schalter aus
|
||||||
|
• 5 Trigger: onSave, onResume, WiFi, Periodic (15/30/60 Min), Boot
|
||||||
|
• Smarte Defaults: Nur ereignisbasiert aktiv (~0.2%/Tag Akku)
|
||||||
|
• Periodischer Sync optional (Standard: AUS)
|
||||||
|
• Verschiedene Fixes und UI-Verbesserungen
|
||||||
2
fastlane/metadata/android/de-DE/changelogs/15.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
• Code Quality Verbesserungen
|
||||||
|
• Bessere Vorbereitung für zukünftige Updates
|
||||||
@@ -38,11 +38,13 @@ MULTI-DEVICE SYNC:
|
|||||||
SYNCHRONISATION:
|
SYNCHRONISATION:
|
||||||
|
|
||||||
• Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.)
|
• Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.)
|
||||||
• Automatische WiFi-Sync: Synchronisiert automatisch wenn du ein beliebiges WLAN betrittst (wenn Server erreichbar ist)
|
• Konfigurierbare Sync-Trigger: Wähle einzeln, wann synchronisiert wird
|
||||||
• Konfigurierbares Interval: 15, 30 oder 60 Minuten
|
• 5 Trigger: onSave (nach dem Speichern), onResume (beim Öffnen), WiFi-Connect, Periodic (15/30/60 Min), Boot
|
||||||
|
• Offline-Modus: Alle Netzwerkfunktionen mit einem Schalter deaktivieren
|
||||||
|
• Smarte Defaults: nur ereignisbasierte Trigger aktiv (~0.2%/Tag Akku)
|
||||||
|
• Periodischer Sync optional (Standard: AUS)
|
||||||
• Optimierte Performance: überspringt unveränderte Dateien (~2-3s Sync-Zeit)
|
• Optimierte Performance: überspringt unveränderte Dateien (~2-3s Sync-Zeit)
|
||||||
• E-Tag Caching für 20x schnellere "keine Änderungen" Checks
|
• E-Tag Caching für 20x schnellere "keine Änderungen" Checks
|
||||||
• Gemessener Akkuverbrauch: nur ~0.4% pro Tag (bei 30min)
|
|
||||||
• Silent-Sync Modus: kein Banner bei Auto-Sync
|
• Silent-Sync Modus: kein Banner bei Auto-Sync
|
||||||
• Doze Mode optimiert für zuverlässige Background-Syncs
|
• Doze Mode optimiert für zuverlässige Background-Syncs
|
||||||
• Manuelle Synchronisation jederzeit möglich
|
• Manuelle Synchronisation jederzeit möglich
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB |
BIN
fastlane/metadata/android/de-DE/images/phoneScreenshots/7.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
6
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
• NEW: Configurable Sync Triggers - Enable/disable each individually
|
||||||
|
• NEW: Offline Mode - Disable all network features with one switch
|
||||||
|
• 5 triggers: onSave, onResume, WiFi, Periodic (15/30/60 min), Boot
|
||||||
|
• Smart defaults: Event-driven only (~0.2%/day battery)
|
||||||
|
• Periodic sync optional (default: OFF)
|
||||||
|
• Various fixes and UI improvements
|
||||||
2
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
• Code quality improvements
|
||||||
|
• Better preparation for future updates
|
||||||
@@ -38,11 +38,13 @@ MULTI-DEVICE SYNC:
|
|||||||
SYNCHRONIZATION:
|
SYNCHRONIZATION:
|
||||||
|
|
||||||
• Supports all WebDAV servers (Nextcloud, ownCloud, etc.)
|
• Supports all WebDAV servers (Nextcloud, ownCloud, etc.)
|
||||||
• Automatic WiFi sync: synchronizes whenever you join any WiFi network (if server is reachable)
|
• Configurable Sync Triggers: Choose individually when to sync
|
||||||
• Configurable interval: 15, 30, or 60 minutes
|
• 5 triggers: onSave (after saving), onResume (on open), WiFi-Connect, Periodic (15/30/60 min), Boot
|
||||||
|
• Offline Mode: Disable all network features with one switch
|
||||||
|
• Smart defaults: event-driven triggers only (~0.2%/day battery)
|
||||||
|
• Periodic sync optional (default: OFF)
|
||||||
• Optimized performance: skips unchanged files (~2-3s sync time)
|
• Optimized performance: skips unchanged files (~2-3s sync time)
|
||||||
• E-Tag caching for 20x faster "no changes" checks
|
• E-Tag caching for 20x faster "no changes" checks
|
||||||
• Measured battery consumption: only ~0.4% per day (at 30min)
|
|
||||||
• Silent-Sync mode: no banner during auto-sync
|
• Silent-Sync mode: no banner during auto-sync
|
||||||
• Doze Mode optimized for reliable background syncs
|
• Doze Mode optimized for reliable background syncs
|
||||||
• Manual synchronization available anytime
|
• Manual synchronization available anytime
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 100 KiB |
BIN
fastlane/metadata/android/en-US/images/phoneScreenshots/7.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
@@ -1,30 +0,0 @@
|
|||||||
Categories:
|
|
||||||
- Writing
|
|
||||||
License: MIT
|
|
||||||
AuthorName: inventory69
|
|
||||||
AuthorEmail: admin@dettmer.dev
|
|
||||||
AuthorWebSite: https://dettmer.dev
|
|
||||||
SourceCode: https://github.com/inventory69/simple-notes-sync
|
|
||||||
IssueTracker: https://github.com/inventory69/simple-notes-sync/issues
|
|
||||||
Changelog: https://github.com/inventory69/simple-notes-sync/releases
|
|
||||||
|
|
||||||
AutoName: Simple Notes
|
|
||||||
|
|
||||||
RepoType: git
|
|
||||||
Repo: https://github.com/inventory69/simple-notes-sync.git
|
|
||||||
Binaries: https://github.com/inventory69/simple-notes-sync/releases/download/v%v/simple-notes-sync-v%v-fdroid.apk
|
|
||||||
|
|
||||||
Builds:
|
|
||||||
- versionName: 1.5.0
|
|
||||||
versionCode: 13
|
|
||||||
commit: 65395142fab487e0a286cc5dfe3cf8b76652379d
|
|
||||||
subdir: android/app
|
|
||||||
gradle:
|
|
||||||
- fdroid
|
|
||||||
|
|
||||||
AllowedAPKSigningKeys: 42a1c613bbc673045af3dc8191bf9cb6456ee44c7dce40c7cfb566facb69f16a
|
|
||||||
|
|
||||||
AutoUpdateMode: Version
|
|
||||||
UpdateCheckMode: Tags
|
|
||||||
CurrentVersion: 1.5.0
|
|
||||||
CurrentVersionCode: 13
|
|
||||||