diff --git a/CHANGELOG.de.md b/CHANGELOG.de.md new file mode 100644 index 0000000..aa2ce0f --- /dev/null +++ b/CHANGELOG.de.md @@ -0,0 +1,518 @@ +# Changelog + +Alle wichtigen Änderungen an Simple Notes Sync werden in dieser Datei dokumentiert. + +Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +**🌍 Sprachen:** **Deutsch** · [English](CHANGELOG.md) + +--- + +## [1.5.0] - 2026-01-15 + +### 🎉 Major: Jetpack Compose UI Redesign + +Das komplette UI wurde von XML-Views auf Jetpack Compose migriert. Die App ist jetzt moderner, schneller und flüssiger. + +### 🌍 New Feature: Internationalization (i18n) + +- **Englische Sprachunterstützung** - Alle 400+ Strings übersetzt +- **Automatische Spracherkennung** - Folgt der System-Sprache +- **Manuelle Sprachauswahl** - In den Einstellungen umschaltbar +- **Per-App Language (Android 13+)** - Native Spracheinstellung über System-Settings +- **locales_config.xml** - Vollständige Android-Integration + +### ⚙️ Modernized Settings + +- **7 kategorisierte Settings-Screens** - Übersichtlicher und intuitiver +- **Compose Navigation** - Flüssige Übergänge zwischen Screens +- **Konsistentes Design** - Material Design 3 durchgängig + +### ✨ UI Improvements + +- **Selection Mode** - Long-Press für Mehrfachauswahl statt Swipe-to-Delete +- **Batch Delete** - Mehrere Notizen gleichzeitig löschen +- **Silent-Sync Mode** - Kein Banner bei Auto-Sync (nur bei manuellem Sync) +- **App Icon in About Screen** - Hochwertige Darstellung +- **App Icon in Empty State** - Statt Emoji bei leerer Notizliste +- **Splash Screen Update** - Verwendet App-Foreground-Icon +- **Slide Animations** - Flüssige Animationen im NoteEditor + +### 🔧 Technical Improvements + +- **Jetpack Compose** - Komplette UI-Migration +- **Compose ViewModel Integration** - StateFlow für reactive UI +- **Improved Code Quality** - Detekt/Lint Warnings behoben +- **Unused Imports Cleanup** - Sauberer Codebase + +### Looking Ahead + +> 🚀 **v1.6.0** wird Server-Ordner Prüfung und weitere technische Modernisierungen bringen. +> Feature-Requests gerne als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) einreichen. + +--- + +## [1.4.1] - 2026-01-11 + +### Fixed + +- **🗑️ Löschen älterer Notizen (v1.2.0 Kompatibilität)** + - Notizen aus App-Version v1.2.0 oder früher werden jetzt korrekt vom Server gelöscht + - Behebt Problem bei Multi-Device-Nutzung mit älteren Notizen + +- **🔄 Checklisten-Sync Abwärtskompatibilität** + - Checklisten werden jetzt auch als Text-Fallback im `content`-Feld gespeichert + - Ältere App-Versionen (v1.3.x) zeigen Checklisten als lesbaren Text + - Format: GitHub-Style Task-Listen (`[ ] Item` / `[x] Item`) + - Recovery-Mode: Falls Checklisten-Items verloren gehen, werden sie aus dem Content wiederhergestellt + +### Improved + +- **📝 Checklisten Auto-Zeilenumbruch** + - Lange Checklisten-Texte werden jetzt automatisch umgebrochen + - Keine Begrenzung auf 3 Zeilen mehr + - Enter-Taste erstellt weiterhin ein neues Item + +### Looking Ahead + +> 🚀 **v1.5.0** wird das nächste größere Release. Wir sammeln Ideen und Feedback! +> Feature-Requests gerne als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) einreichen. + +--- + +## [1.4.0] - 2026-01-10 + +### 🎉 New Feature: Checklists + +- **✅ Checklist Notes** + - New note type: Checklists with tap-to-toggle items + - Add items via dedicated input field with "+" button + - Drag & drop reordering (long-press to activate) + - Swipe-to-delete items + - Visual distinction: Checked items get strikethrough styling + - Type selector when creating new notes (Text or Checklist) + +- **📝 Markdown Integration** + - Checklists export as GitHub-style task lists (`- [ ]` / `- [x]`) + - Compatible with Obsidian, Notion, and other Markdown editors + - Full round-trip: Edit in Obsidian → Sync back to app + - YAML frontmatter includes `type: checklist` for identification + +### Fixed + +- **� Markdown Parsing Robustness** + - Fixed content extraction after title (was returning empty for some formats) + - Now handles single newline after title (was requiring double newline) + - Protection: Skips import if parsed content is empty but local has content + +- **📂 Duplicate Filename Handling** + - Notes with identical titles now get unique Markdown filenames + - Format: `title_shortid.md` (e.g., `test_71540ca9.md`) + - Prevents data loss from filename collisions + +- **🔔 Notification UX** + - No sync notifications when app is in foreground + - User sees changes directly in UI - no redundant notification + - Background syncs still show notifications as expected + +### Privacy Improvements + +- **🔒 WiFi Permissions Removed** + - Removed `ACCESS_WIFI_STATE` permission + - Removed `CHANGE_WIFI_STATE` permission + - WiFi binding now works via IP detection instead of SSID matching + - Cleaned up all SSID-related code from codebase and documentation + +### Technical Improvements + +- **📦 New Data Model** + - `NoteType` enum: `TEXT`, `CHECKLIST` + - `ChecklistItem` data class with id, text, isChecked, order + - `Note.kt` extended with `noteType` and `checklistItems` fields + +- **🔄 Sync Protocol v1.4.0** + - JSON format updated to include checklist fields + - Full backward compatibility with v1.3.x notes + - Robust JSON parsing with manual field extraction + +--- + +## [1.3.2] - 2026-01-10 + +### Changed +- **🧹 Code-Qualität: "Clean Slate" Release** + - Alle einfachen Lint-Issues behoben (Phase 1-7 des Cleanup-Plans) + - Unused Imports und Members entfernt + - Magic Numbers durch benannte Konstanten ersetzt + - SwallowedExceptions mit Logger.w() versehen + - MaxLineLength-Verstöße reformatiert + - ConstructorParameterNaming (snake_case → camelCase mit @SerializedName) + - Custom Exceptions: SyncException.kt und ValidationException.kt erstellt + +### Added +- **📝 F-Droid Privacy Notice** + - Datenschutz-Hinweis für die Datei-Logging-Funktion + - Erklärt dass Logs nur lokal gespeichert werden + - Erfüllt F-Droid Opt-in Consent-Anforderungen + +### Technical Improvements +- **⚡ Neue Konstanten für bessere Wartbarkeit** + - `SYNC_COMPLETED_DELAY_MS`, `ERROR_DISPLAY_DELAY_MS` (MainActivity) + - `CONNECTION_TIMEOUT_MS` (SettingsActivity) + - `SOCKET_TIMEOUT_MS`, `MAX_FILENAME_LENGTH`, `ETAG_PREVIEW_LENGTH` (WebDavSyncService) + - `AUTO_CANCEL_TIMEOUT_MS` (NotificationHelper) + - RFC 1918 IP-Range Konstanten (UrlValidator) + - `DAYS_THRESHOLD`, `TRUNCATE_SUFFIX_LENGTH` (Extensions) + +- **🔒 @Suppress Annotations für legitime Patterns** + - ReturnCount: Frühe Returns für Validierung sind idiomatisch + - LoopWithTooManyJumpStatements: Komplexe Sync-Logik dokumentiert + +### Notes +- Komplexe Refactorings (LargeClass, LongMethod) für v1.3.3+ geplant +- Deprecation-Warnungen (LocalBroadcastManager, ProgressDialog) bleiben bestehen + +--- + +## [1.3.1] - 2026-01-08 + +### Fixed +- **🔧 Multi-Device JSON Sync (Danke an Thomas aus Bielefeld)** + - JSON-Dateien werden jetzt korrekt zwischen Geräten synchronisiert + - Funktioniert auch ohne aktiviertes Markdown + - Hybrid-Optimierung: Server-Timestamp (Primary) + E-Tag (Secondary) Checks + - E-Tag wird nach Upload gecached um Re-Download zu vermeiden + +### Performance Improvements +- **⚡ JSON Sync Performance-Parität** + - JSON-Sync erreicht jetzt gleiche Performance wie Markdown (~2-3 Sekunden) + - Timestamp-basierte Skip-Logik für unveränderte Dateien (~500ms pro Datei gespart) + - E-Tag-Matching als Fallback für Dateien die seit letztem Sync modifiziert wurden + - **Beispiel:** 24 Dateien von 12-14s auf ~2.7s reduziert (keine Änderungen) + +- **⏭️ Skip unveränderte Dateien** (Haupt-Performance-Fix!) + - JSON-Dateien: Überspringt alle Notizen, die seit letztem Sync nicht geändert wurden + - Markdown-Dateien: Überspringt unveränderte MD-Dateien basierend auf Server-Timestamp + - **Spart ~500ms pro Datei** bei Nextcloud (~20 Dateien = 10 Sekunden gespart!) + - Von 21 Sekunden Sync-Zeit auf 2-3 Sekunden reduziert + +- **⚡ Session-Caching für WebDAV** + - Sardine-Client wird pro Sync-Session wiederverwendet (~600ms gespart) + - WiFi-IP-Adresse wird gecacht statt bei jeder Anfrage neu ermittelt (~300ms gespart) + - `/notes/` Ordner-Existenz wird nur einmal pro Sync geprüft (~500ms gespart) + - **Gesamt: ~1.4 Sekunden zusätzlich gespart** + +- **📝 Content-basierte Markdown-Erkennung** + - Extern bearbeitete Markdown-Dateien werden auch erkannt wenn YAML-Timestamp nicht aktualisiert wurde + - Löst das Problem: Obsidian/Texteditor-Änderungen wurden nicht importiert + - Hybridansatz: Erst Timestamp-Check (schnell), dann Content-Vergleich (zuverlässig) + +### Added +- **🔄 Sync-Status-Anzeige (UI)** + - Sichtbares Banner "Synchronisiere..." mit ProgressBar während Sync läuft + - Sync-Button und Pull-to-Refresh werden deaktiviert während Sync aktiv + - Verhindert versehentliche Doppel-Syncs durch visuelle Rückmeldung + - Auch in Einstellungen: "Jetzt synchronisieren" Button wird deaktiviert + +### Fixed +- **🔧 Sync-Mutex verhindert doppelte Syncs** + - Keine doppelten Toast-Nachrichten mehr bei schnellem Pull-to-Refresh + - Concurrent Sync-Requests werden korrekt blockiert + +- **🐛 Lint-Fehler behoben** + - `View.generateViewId()` statt hardcodierte IDs in RadioButtons + - `app:tint` statt `android:tint` für AppCompat-Kompatibilität + +### Added +- **🔍 detekt Code-Analyse** + - Statische Code-Analyse mit detekt 1.23.4 integriert + - Pragmatische Konfiguration für Sync-intensive Codebasis + - 91 Issues identifiziert (als Baseline für v1.4.0) + +- **🏗️ Debug Build mit separatem Package** + - Debug-APK kann parallel zur Release-Version installiert werden + - Package: `dev.dettmer.simplenotes.debug` (Debug) vs `dev.dettmer.simplenotes` (Release) + - App-Name zeigt "Simple Notes (Debug)" für einfache Unterscheidung + +- **📊 Debug-Logging UI** + - Neuer "Debug Log" Button in Einstellungen → Erweitert + - Zeigt letzte Sync-Logs mit Zeitstempeln + - Export-Funktion für Fehlerberichte + +### Technical +- `WebDavSyncService`: Hybrid-Optimierung für JSON-Downloads (Timestamp PRIMARY, E-Tag SECONDARY) +- `WebDavSyncService`: E-Tag refresh nach Upload statt Invalidierung (verhindert Re-Download) +- E-Tag Caching: `SharedPreferences` mit Key-Pattern `etag_json_{noteId}` +- Skip-Logik: `if (serverModified <= lastSync) skip` → ~1ms pro Datei +- Fallback E-Tag: `if (serverETag == cachedETag) skip` → für Dateien modifiziert nach lastSync +- PROPFIND nach PUT: Fetch E-Tag nach Upload für korrektes Caching +- `SyncStateManager`: Neuer Singleton mit `StateFlow` für Sync-Status +- `MainActivity`: Observer auf `SyncStateManager.isSyncing` für UI-Updates +- Layout: `sync_status_banner` mit `ProgressBar` + `TextView` +- `WebDavSyncService`: Skip-Logik für unveränderte JSON/MD Dateien basierend auf `lastSyncTimestamp` +- `WebDavSyncService`: Neue Session-Cache-Variablen (`sessionSardine`, `sessionWifiAddress`, `notesDirEnsured`) +- `getOrCreateSardine()`: Cached Sardine-Client mit automatischer Credentials-Konfiguration +- `getOrCacheWiFiAddress()`: WiFi-Adresse wird nur einmal pro Sync ermittelt +- `clearSessionCache()`: Aufräumen am Ende jeder Sync-Session +- `ensureNotesDirectoryExists()`: Cached Directory-Check +- Content-basierter Import: Vergleicht MD-Content mit lokaler Note wenn Timestamps gleich +- Build-Tooling: detekt aktiviert, ktlint vorbereitet (deaktiviert wegen Parser-Problemen) +- Debug BuildType: `applicationIdSuffix = ".debug"`, `versionNameSuffix = "-debug"` + +--- + +## [1.3.0] - 2026-01-07 + +### Added +- **🚀 Multi-Device Sync** (Thanks to Thomas from Bielefeld for reporting!) + - Automatic download of new notes from other devices + - Deletion tracking prevents "zombie notes" (deleted notes don't come back) + - Smart cleanup: Re-created notes (newer timestamp) are downloaded + - Works with all devices: v1.2.0, v1.2.1, v1.2.2, and v1.3.0 + +- **🗑️ Server Deletion via Swipe Gesture** + - Swipe left on notes to delete from server (requires confirmation) + - Prevents duplicate notes on other devices + - Works with deletion tracking system + - Material Design confirmation dialog + +- **⚡ E-Tag Performance Optimization** + - Smart server checking with E-Tag caching (~150ms vs 3000ms for "no changes") + - 20x faster when server has no updates + - E-Tag hybrid approach: E-Tag for JSON (fast), timestamp for Markdown (reliable) + - Battery-friendly with minimal server requests + +- **📥 Markdown Auto-Sync Toggle** + - NEW: Unified Auto-Sync toggle in Settings (replaces separate Export/Auto-Import toggles) + - When enabled: Notes export to Markdown AND import changes automatically + - When disabled: Manual sync button appears for on-demand synchronization + - Performance: Auto-Sync OFF = 0ms overhead + +- **🔘 Manual Markdown Sync Button** + - Manual sync button for performance-conscious users + - Shows import/export counts after completion + - Only visible when Auto-Sync is disabled + - On-demand synchronization (~150-200ms only when triggered) + +- **⚙️ Server-Restore Modes** + - MERGE: Keep local notes + add server notes + - REPLACE: Delete all local + download from server + - OVERWRITE: Update duplicates, keep non-duplicates + - Restore modes now work correctly for WebDAV restore + +### Technical +- New `DeletionTracker` model with JSON persistence +- `NotesStorage`: Added deletion tracking methods +- `WebDavSyncService.hasUnsyncedChanges()`: Intelligent server checks with E-Tag caching +- `WebDavSyncService.downloadRemoteNotes()`: Deletion-aware downloads +- `WebDavSyncService.restoreFromServer()`: Support for restore modes +- `WebDavSyncService.deleteNoteFromServer()`: Server deletion with YAML frontmatter scanning +- `WebDavSyncService.importMarkdownFiles()`: Automatic Markdown import during sync +- `WebDavSyncService.manualMarkdownSync()`: Manual sync with result counts +- `MainActivity.setupSwipeToDelete()`: Two-stage swipe deletion with confirmation +- E-Tag caching in SharedPreferences for performance + +--- + +## [1.2.2] - 2026-01-06 + +### Fixed +- **Backward Compatibility for v1.2.0 Users (Critical)** + - App now reads BOTH old (Root) AND new (`/notes/`) folder structures + - Users upgrading from v1.2.0 no longer lose their existing notes + - Server-Restore now finds notes from v1.2.0 stored in Root folder + - Automatic deduplication prevents loading the same note twice + - Graceful error handling if Root folder is not accessible + +### Technical +- `WebDavSyncService.downloadRemoteNotes()` - Dual-mode download (Root + /notes/) +- `WebDavSyncService.restoreFromServer()` - Now uses dual-mode download +- Migration happens naturally: new uploads go to `/notes/`, old notes stay readable + +--- + +## [1.2.1] - 2026-01-05 + +### Fixed +- **Markdown Initial Export Bugfix** + - Existing notes are now exported as Markdown when Desktop Integration is activated + - Previously, only new notes created after activation were exported + - Progress dialog shows export status with current/total counter + - Error handling for network issues during export + - Individual note failures don't abort the entire export + +- **Markdown Directory Structure Fix** + - Markdown files now correctly land in `/notes-md/` folder + - Smart URL detection supports both Root-URL and `/notes` URL structures + - Previously, MD files were incorrectly placed in the root directory + - Markdown import now finds files correctly + +- **JSON URL Normalization** + - Simplified server configuration: enter only base URL (e.g., `http://server:8080/`) + - App automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown + - Smart detection: both `http://server:8080/` and `http://server:8080/notes/` work correctly + - Backward compatible: existing setups with `/notes` in URL continue to work + - No migration required for existing users + +### Changed +- **Markdown Directory Creation** + - `notes-md/` folder is now created on first sync (regardless of Desktop Integration setting) + - Prevents 404 errors when mounting WebDAV folder + - Better user experience: folder is visible before enabling the feature + +- **Settings UI Improvements** + - Updated example URL from `/webdav` to `/notes` to match app behavior + - Example now shows: `http://192.168.0.188:8080/notes` + +### Technical +- `WebDavSyncService.ensureMarkdownDirectoryExists()` - Creates MD folder early +- `WebDavSyncService.getMarkdownUrl()` - Smart URL detection for both structures +- `WebDavSyncService.exportAllNotesToMarkdown()` - Exports all local notes with progress callback +- `SettingsActivity.onMarkdownExportToggled()` - Triggers initial export with ProgressDialog + +--- + +## [1.2.0] - 2026-01-04 + +### Added +- **Local Backup System** + - Export all notes as JSON file to any location (Downloads, SD card, cloud folder) + - Import backup with 3 modes: Merge, Replace, or Overwrite duplicates + - Automatic safety backup created before every restore + - Backup validation (format and version check) + +- **Markdown Desktop Integration** + - Optional Markdown export parallel to JSON sync + - `.md` files synced to `notes-md/` folder on WebDAV + - YAML frontmatter with `id`, `created`, `updated`, `device` + - Manual import button to pull Markdown changes from server + - Last-Write-Wins conflict resolution via timestamps + +- **Settings UI Extensions** + - New "Backup & Restore" section with local + server restore + - New "Desktop Integration" section with Markdown toggle + - Universal restore dialog with radio button mode selection + +### Changed +- **Server Restore Behavior**: Users now choose restore mode (Merge/Replace/Overwrite) instead of hard-coded replace-all + +### Technical +- `BackupManager.kt` - Complete backup/restore logic +- `Note.toMarkdown()` / `Note.fromMarkdown()` - Markdown conversion with YAML frontmatter +- `WebDavSyncService` - Extended for dual-format sync (JSON master + Markdown mirror) +- ISO8601 timestamp formatting for desktop compatibility +- Filename sanitization for safe Markdown file names + +### Documentation +- Added WebDAV mount instructions (Windows, macOS, Linux) +- Created [SYNC_ARCHITECTURE.md](../project-docs/simple-notes-sync/architecture/SYNC_ARCHITECTURE.md) - Complete sync documentation +- Created [MARKDOWN_DESKTOP_REALITY_CHECK.md](../project-docs/simple-notes-sync/markdown-desktop-plan/MARKDOWN_DESKTOP_REALITY_CHECK.md) - Desktop integration analysis + +--- + +## [1.1.2] - 2025-12-28 + +### Fixed +- **"Job was cancelled" Error** + - Fixed coroutine cancellation in sync worker + - Proper error handling for interrupted syncs + +- **UI Improvements** + - Back arrow instead of X in note editor (better UX) + - Pull-to-refresh for manual sync trigger + - HTTP/HTTPS protocol selection with radio buttons + - Inline error display (no toast spam) + +- **Performance & Battery** + - Sync only on actual changes (saves battery) + - Auto-save notifications removed + - 24-hour server offline warning instead of instant error + +### Changed +- Settings grouped into "Auto-Sync" and "Sync Interval" sections +- HTTP only allowed for local networks (RFC 1918 IPs) +- Swipe-to-delete without UI flicker + +--- + +## [1.1.1] - 2025-12-27 + +### Fixed +- **WiFi Connect Sync** + - No error notifications in foreign WiFi networks + - Server reachability check before sync (2s timeout) + - Silent abort when server offline + - Pre-check waits until network is ready + - No errors during network initialization + +### Changed +- **Notifications** + - Old sync notifications cleared on app start + - Error notifications auto-dismiss after 30 seconds + +### UI +- Sync icon only shown when sync is configured +- Swipe-to-delete without flicker +- Scroll to top after saving note + +### Technical +- Server check with 2-second timeout before sync attempts +- Network readiness check in WiFi connect trigger +- Notification cleanup on MainActivity.onCreate() + +--- + +## [1.1.0] - 2025-12-26 + +### Added +- **Configurable Sync Intervals** + - User choice: 15, 30, or 60 minutes + - Real-world battery impact displayed (15min: ~0.8%/day, 30min: ~0.4%/day, 60min: ~0.2%/day) + - Radio button selection in settings + - Doze Mode optimization (syncs batched in maintenance windows) + +- **About Section** + - App version from BuildConfig + - Links to GitHub repository and developer profile + - MIT license information + - Material 3 card design + +### Changed +- Settings UI redesigned with grouped sections +- Periodic sync updated dynamically when interval changes +- WorkManager uses selected interval for background sync + +### Removed +- Debug/Logs section from settings (cleaner UI) + +### Technical +- `PREF_SYNC_INTERVAL_MINUTES` preference key +- NetworkMonitor reads interval from SharedPreferences +- `ExistingPeriodicWorkPolicy.UPDATE` for live interval changes + +--- + +## [1.0.0] - 2025-12-25 + +### Added +- Initial release +- WebDAV synchronization +- Note creation, editing, deletion +- 6 sync triggers: + - Periodic sync (configurable interval) + - App start sync + - WiFi connect sync + - Manual sync (menu button) + - Pull-to-refresh + - Settings "Sync Now" button +- Material 3 design +- Light/Dark theme support +- F-Droid compatible (100% FOSS) + +--- + +[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0 +[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2 +[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1 +[1.1.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.0 +[1.0.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index accbc72..d3621b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,33 +4,79 @@ All notable changes to Simple Notes Sync will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +**🌍 Languages:** [Deutsch](CHANGELOG.de.md) · **English** + +--- + +## [1.5.0] - 2026-01-15 + +### 🎉 Major: Jetpack Compose UI Redesign + +The complete UI has been migrated from XML Views to Jetpack Compose. The app is now more modern, faster, and smoother. + +### 🌍 New Feature: Internationalization (i18n) + +- **English language support** - All 400+ strings translated +- **Automatic language detection** - Follows system language +- **Manual language selection** - Switchable in settings +- **Per-App Language (Android 13+)** - Native language setting via system settings +- **locales_config.xml** - Complete Android integration + +### ⚙️ Modernized Settings + +- **7 categorized settings screens** - Clearer and more intuitive +- **Compose Navigation** - Smooth transitions between screens +- **Consistent design** - Material Design 3 throughout + +### ✨ UI Improvements + +- **Selection Mode** - Long-press for multi-select instead of swipe-to-delete +- **Batch Delete** - Delete multiple notes at once +- **Silent-Sync Mode** - No banner during auto-sync (only for manual sync) +- **App Icon in About Screen** - High-quality display +- **App Icon in Empty State** - Instead of emoji when note list is empty +- **Splash Screen Update** - Uses app foreground icon +- **Slide Animations** - Smooth animations in NoteEditor + +### 🔧 Technical Improvements + +- **Jetpack Compose** - Complete UI migration +- **Compose ViewModel Integration** - StateFlow for reactive UI +- **Improved Code Quality** - Detekt/Lint warnings fixed +- **Unused Imports Cleanup** - Cleaner codebase + +### Looking Ahead + +> 🚀 **v1.6.0** will bring server folder checking and further technical modernizations. +> Feature requests welcome as [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues). + --- ## [1.4.1] - 2026-01-11 ### Fixed -- **🗑️ Löschen älterer Notizen (v1.2.0 Kompatibilität)** - - Notizen aus App-Version v1.2.0 oder früher werden jetzt korrekt vom Server gelöscht - - Behebt Problem bei Multi-Device-Nutzung mit älteren Notizen +- **🗑️ Deleting older notes (v1.2.0 compatibility)** + - Notes from app version v1.2.0 or earlier are now correctly deleted from the server + - Fixes issue with multi-device usage with older notes -- **🔄 Checklisten-Sync Abwärtskompatibilität** - - Checklisten werden jetzt auch als Text-Fallback im `content`-Feld gespeichert - - Ältere App-Versionen (v1.3.x) zeigen Checklisten als lesbaren Text - - Format: GitHub-Style Task-Listen (`[ ] Item` / `[x] Item`) - - Recovery-Mode: Falls Checklisten-Items verloren gehen, werden sie aus dem Content wiederhergestellt +- **🔄 Checklist sync backward compatibility** + - Checklists now also saved as text fallback in the `content` field + - Older app versions (v1.3.x) display checklists as readable text + - Format: GitHub-style task lists (`[ ] Item` / `[x] Item`) + - Recovery mode: If checklist items are lost, they are recovered from content ### Improved -- **📝 Checklisten Auto-Zeilenumbruch** - - Lange Checklisten-Texte werden jetzt automatisch umgebrochen - - Keine Begrenzung auf 3 Zeilen mehr - - Enter-Taste erstellt weiterhin ein neues Item +- **📝 Checklist auto line-wrap** + - Long checklist texts now automatically wrap + - No more limit to 3 lines + - Enter key still creates a new item ### Looking Ahead -> 🚀 **v1.5.0** wird das nächste größere Release. Wir sammeln Ideen und Feedback! -> Feature-Requests gerne als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) einreichen. +> 🚀 **v1.5.0** will be the next major release. We're collecting ideas and feedback! +> Feature requests welcome as [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues). --- diff --git a/QUICKSTART.de.md b/QUICKSTART.de.md new file mode 100644 index 0000000..c6ac210 --- /dev/null +++ b/QUICKSTART.de.md @@ -0,0 +1,269 @@ +# Quick Start Guide - Simple Notes Sync 📝 + +> Schritt-für-Schritt Anleitung zur Installation und Einrichtung + +**🌍 Sprachen:** **Deutsch** · [English](QUICKSTART.md) + +--- + +## Voraussetzungen + +- ✅ Android 8.0+ Smartphone/Tablet +- ✅ WLAN-Verbindung +- ✅ Eigener Server mit Docker (optional - für Self-Hosting) + +--- + +## Option 1: Mit eigenem Server (Self-Hosted) 🏠 + +### Schritt 1: WebDAV Server einrichten + +Auf deinem Server (z.B. Raspberry Pi, NAS, VPS): + +```bash +# Repository klonen +git clone https://github.com/inventory69/simple-notes-sync.git +cd simple-notes-sync/server + +# Umgebungsvariablen konfigurieren +cp .env.example .env +nano .env +``` + +**In `.env` anpassen:** +```env +WEBDAV_PASSWORD=dein-sicheres-passwort-hier +``` + +**Server starten:** +```bash +docker compose up -d +``` + +**IP-Adresse finden:** +```bash +ip addr show | grep "inet " | grep -v 127.0.0.1 +``` + +➡️ **Notiere dir:** `http://DEINE-SERVER-IP:8080/` + +--- + +### Schritt 2: App installieren + +1. **APK herunterladen:** [Neueste Version](https://github.com/inventory69/simple-notes-sync/releases/latest) + - Wähle: `simple-notes-sync-vX.X.X-standard-universal.apk` + +2. **Installation erlauben:** + - Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren + +3. **APK öffnen und installieren** + +--- + +### Schritt 3: App konfigurieren + +1. **App öffnen** + +2. **Einstellungen öffnen** (⚙️ Icon oben rechts) + +3. **Server-Einstellungen konfigurieren:** + + | Feld | Wert | + |------|------| + | **WebDAV Server URL** | `http://DEINE-SERVER-IP:8080/` | + | **Benutzername** | `noteuser` | + | **Passwort** | (dein Passwort aus `.env`) | + + > **💡 Hinweis:** Gib nur die Base-URL ein (ohne `/notes`). Die App erstellt automatisch `/notes/` für JSON-Dateien und `/notes-md/` für Markdown-Export. + +4. **"Verbindung testen"** drücken + - ✅ Erfolg? → Weiter zu Schritt 4 + - ❌ Fehler? → Siehe [Troubleshooting](#troubleshooting) + +5. **Auto-Sync aktivieren** (Toggle Switch) + +6. **Sync-Intervall wählen:** + - **15 Min** - Maximale Aktualität (~0.8% Akku/Tag) + - **30 Min** - Empfohlen (~0.4% Akku/Tag) ⭐ + - **60 Min** - Maximale Akkulaufzeit (~0.2% Akku/Tag) + +--- + +### Schritt 4: Erste Notiz erstellen + +1. Zurück zur Hauptansicht (← Pfeil) + +2. **"Notiz hinzufügen"** (+ Icon) + +3. Titel und Text eingeben + +4. **Speichern** (💾 Icon) + +5. **Warten auf Auto-Sync** (oder manuell: ⚙️ → "Jetzt synchronisieren") + +🎉 **Fertig!** Deine Notizen werden automatisch synchronisiert! + +--- + +## Option 2: Nur lokale Notizen (kein Server) 📱 + +Du kannst Simple Notes auch **ohne Server** nutzen: + +1. **App installieren** (siehe Schritt 2 oben) + +2. **Ohne Server-Konfiguration verwenden:** + - Notizen werden nur lokal gespeichert + - Kein Auto-Sync + - Perfekt für reine Offline-Nutzung + +--- + +## 🔋 Akku-Optimierung deaktivieren + +Für zuverlässigen Auto-Sync: + +1. **Einstellungen** → **Apps** → **Simple Notes Sync** + +2. **Akku** → **Akkuverbrauch** + +3. Wähle: **"Nicht optimieren"** oder **"Unbeschränkt"** + +💡 **Hinweis:** Android Doze Mode kann trotzdem Sync im Standby verzögern (~60 Min). Das ist normal und betrifft alle Apps. + +--- + +## 📊 Sync-Intervalle im Detail + +| Intervall | Syncs/Tag | Akku/Tag | Akku/Sync | Anwendungsfall | +|-----------|-----------|----------|-----------|----------------| +| **15 Min** | ~96 | ~0.8% (~23 mAh) | ~0.008% | ⚡ Maximal aktuell (mehrere Geräte) | +| **30 Min** | ~48 | ~0.4% (~12 mAh) | ~0.008% | ✓ **Empfohlen** - ausgewogen | +| **60 Min** | ~24 | ~0.2% (~6 mAh) | ~0.008% | 🔋 Maximale Akkulaufzeit | + +--- + +## 🐛 Troubleshooting + +### Verbindungstest schlägt fehl + +**Problem:** "Verbindung fehlgeschlagen" beim Test + +**Lösungen:** + +1. **Server läuft?** + ```bash + docker compose ps + # Sollte "Up" zeigen + ``` + +2. **Gleiches Netzwerk?** + - Smartphone und Server müssen im selben Netzwerk sein + +3. **IP-Adresse korrekt?** + ```bash + ip addr show | grep "inet " + # Prüfe ob IP in URL stimmt + ``` + +4. **Firewall?** + ```bash + # Port 8080 öffnen (falls Firewall aktiv) + sudo ufw allow 8080/tcp + ``` + +5. **Server-Logs prüfen:** + ```bash + docker compose logs -f + ``` + +--- + +### Auto-Sync funktioniert nicht + +**Problem:** Notizen werden nicht automatisch synchronisiert + +**Lösungen:** + +1. **Auto-Sync aktiviert?** + - ⚙️ Einstellungen → Toggle "Auto-Sync" muss **AN** sein + +2. **Akku-Optimierung deaktiviert?** + - Siehe [Akku-Optimierung](#-akku-optimierung-deaktivieren) + +3. **Mit WiFi verbunden?** + - Auto-Sync triggert bei jeder WiFi-Verbindung + - Prüfe, ob du mit einem WLAN verbunden bist + +4. **Manuell testen:** + - ⚙️ Einstellungen → "Jetzt synchronisieren" + - Funktioniert das? → Auto-Sync sollte auch funktionieren + +--- + +### Notizen werden nicht angezeigt + +**Problem:** Nach Installation sind keine Notizen da, obwohl welche auf dem Server liegen + +**Lösung:** + +1. **Einmalig manuell synchronisieren:** + - ⚙️ Einstellungen → "Jetzt synchronisieren" + +2. **Server-Daten prüfen:** + ```bash + docker compose exec webdav ls -la /data/ + # Sollte .json Dateien zeigen + ``` + +--- + +### Fehler beim Sync + +**Problem:** Fehlermeldung beim Synchronisieren + +**Lösungen:** + +1. **"401 Unauthorized"** → Passwort falsch + - Prüfe Passwort in App-Einstellungen + - Vergleiche mit `.env` auf Server + +2. **"404 Not Found"** → URL falsch + - Sollte enden mit `/` (z.B. `http://192.168.1.100:8080/`) + +3. **"Network error"** → Keine Verbindung + - Siehe [Verbindungstest schlägt fehl](#verbindungstest-schlägt-fehl) + +--- + +## 📱 Updates + +### Automatisch mit Obtainium (empfohlen) + +1. **[Obtainium installieren](https://github.com/ImranR98/Obtanium/releases/latest)** + +2. **App hinzufügen:** + - URL: `https://github.com/inventory69/simple-notes-sync` + - Auto-Update aktivieren + +3. **Fertig!** Obtainium benachrichtigt dich bei neuen Versionen + +### Manuell + +1. Neue APK von [Releases](https://github.com/inventory69/simple-notes-sync/releases/latest) herunterladen + +2. Installieren (überschreibt alte Version) + +3. Alle Daten bleiben erhalten! + +--- + +## 🆘 Weitere Hilfe + +- **GitHub Issues:** [Problem melden](https://github.com/inventory69/simple-notes-sync/issues) +- **Vollständige Docs:** [DOCS.md](DOCS.md) +- **Server Setup Details:** [server/README.md](server/README.md) + +--- + +**Version:** 1.1.0 · **Erstellt:** Dezember 2025 diff --git a/QUICKSTART.en.md b/QUICKSTART.en.md deleted file mode 100644 index 43128bd..0000000 --- a/QUICKSTART.en.md +++ /dev/null @@ -1,269 +0,0 @@ -# Quick Start Guide - Simple Notes Sync 📝 - -> Step-by-step installation and setup guide - -**🌍 Languages:** [Deutsch](QUICKSTART.md) · **English** - ---- - -## Prerequisites - -- ✅ Android 8.0+ smartphone/tablet -- ✅ WiFi connection -- ✅ Own server with Docker (optional - for self-hosting) - ---- - -## Option 1: With own server (Self-Hosted) 🏠 - -### Step 1: Setup WebDAV Server - -On your server (e.g. Raspberry Pi, NAS, VPS): - -```bash -# Clone repository -git clone https://github.com/inventory69/simple-notes-sync.git -cd simple-notes-sync/server - -# Configure environment variables -cp .env.example .env -nano .env -``` - -**Adjust in `.env`:** -```env -WEBDAV_PASSWORD=your-secure-password-here -``` - -**Start server:** -```bash -docker compose up -d -``` - -**Find IP address:** -```bash -ip addr show | grep "inet " | grep -v 127.0.0.1 -``` - -➡️ **Note down:** `http://YOUR-SERVER-IP:8080/` - ---- - -### Step 2: Install App - -1. **Download APK:** [Latest version](https://github.com/inventory69/simple-notes-sync/releases/latest) - - Choose: `simple-notes-sync-vX.X.X-standard-universal.apk` - -2. **Allow installation:** - - Android: Settings → Security → Enable "Unknown sources" for your browser - -3. **Open and install APK** - ---- - -### Step 3: Configure App - -1. **Open app** - -2. **Open settings** (⚙️ icon top right) - -3. **Configure server settings:** - - | Field | Value | - |------|------| - | **WebDAV Server URL** | `http://YOUR-SERVER-IP:8080/` | - | **Username** | `noteuser` | - | **Password** | (your password from `.env`) | - - > **💡 Note:** Enter only the base URL (without `/notes`). The app automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown export. - -4. **Press "Test connection"**** - - ✅ Success? → Continue to step 4 - - ❌ Error? → See [Troubleshooting](#troubleshooting) - -5. **Enable auto-sync** (toggle switch) - -6. **Choose sync interval:** - - **15 min** - Maximum currency (~0.8% battery/day) - - **30 min** - Recommended (~0.4% battery/day) ⭐ - - **60 min** - Maximum battery life (~0.2% battery/day) - ---- - -### Step 4: Create First Note - -1. Back to main view (← arrow) - -2. **"Add note"** (+ icon) - -3. Enter title and text - -4. **Save** (💾 icon) - -5. **Wait for auto-sync** (or manually: ⚙️ → "Sync now") - -🎉 **Done!** Your notes will be automatically synchronized! - ---- - -## Option 2: Local notes only (no server) 📱 - -You can also use Simple Notes **without a server**: - -1. **Install app** (see step 2 above) - -2. **Use without server configuration:** - - Notes are only stored locally - - No auto-sync - - Perfect for offline-only use - ---- - -## 🔋 Disable Battery Optimization - -For reliable auto-sync: - -1. **Settings** → **Apps** → **Simple Notes Sync** - -2. **Battery** → **Battery usage** - -3. Select: **"Don't optimize"** or **"Unrestricted"** - -💡 **Note:** Android Doze Mode may still delay sync in standby (~60 min). This is normal and affects all apps. - ---- - -## 📊 Sync Intervals in Detail - -| Interval | Syncs/day | Battery/day | Battery/sync | Use case | -|-----------|-----------|----------|-----------|----------------| -| **15 min** | ~96 | ~0.8% (~23 mAh) | ~0.008% | ⚡ Maximum currency (multiple devices) | -| **30 min** | ~48 | ~0.4% (~12 mAh) | ~0.008% | ✓ **Recommended** - balanced | -| **60 min** | ~24 | ~0.2% (~6 mAh) | ~0.008% | 🔋 Maximum battery life | - ---- - -## 🐛 Troubleshooting - -### Connection test fails - -**Problem:** "Connection failed" during test - -**Solutions:** - -1. **Server running?** - ```bash - docker compose ps - # Should show "Up" - ``` - -2. **Same network?** - - Smartphone and server must be on same network - -3. **IP address correct?** - ```bash - ip addr show | grep "inet " - # Check if IP in URL matches - ``` - -4. **Firewall?** - ```bash - # Open port 8080 (if firewall active) - sudo ufw allow 8080/tcp - ``` - -5. **Check server logs:** - ```bash - docker compose logs -f - ``` - ---- - -### Auto-sync not working - -**Problem:** Notes are not automatically synchronized - -**Solutions:** - -1. **Auto-sync enabled?** - - ⚙️ Settings → Toggle "Auto-sync" must be **ON** - -2. **Battery optimization disabled?** - - See [Disable Battery Optimization](#-disable-battery-optimization) - -3. **Connected to WiFi?** - - Auto-sync triggers on any WiFi connection - - Check if you're connected to a WiFi network - -4. **Test manually:** - - ⚙️ Settings → "Sync now" - - Works? → Auto-sync should work too - ---- - -### Notes not showing up - -**Problem:** After installation, no notes visible even though they exist on server - -**Solution:** - -1. **Manually sync once:** - - ⚙️ Settings → "Sync now" - -2. **Check server data:** - ```bash - docker compose exec webdav ls -la /data/ - # Should show .json files - ``` - ---- - -### Sync errors - -**Problem:** Error message during sync - -**Solutions:** - -1. **"401 Unauthorized"** → Wrong password - - Check password in app settings - - Compare with `.env` on server - -2. **"404 Not Found"** → Wrong URL - - Should end with `/` (e.g. `http://192.168.1.100:8080/`) - -3. **"Network error"** → No connection - - See [Connection test fails](#connection-test-fails) - ---- - -## 📱 Updates - -### Automatic with Obtainium (recommended) - -1. **[Install Obtainium](https://github.com/ImranR98/Obtanium/releases/latest)** - -2. **Add app:** - - URL: `https://github.com/inventory69/simple-notes-sync` - - Enable auto-update - -3. **Done!** Obtainium notifies you of new versions - -### Manual - -1. Download new APK from [Releases](https://github.com/inventory69/simple-notes-sync/releases/latest) - -2. Install (overwrites old version) - -3. All data remains intact! - ---- - -## 🆘 Further Help - -- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues) -- **Complete docs:** [DOCS.en.md](DOCS.en.md) -- **Server setup details:** [server/README.en.md](server/README.en.md) - ---- - -**Version:** 1.1.0 · **Created:** December 2025 diff --git a/QUICKSTART.md b/QUICKSTART.md index ead0005..4010d1b 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -1,269 +1,269 @@ # Quick Start Guide - Simple Notes Sync 📝 -> Schritt-für-Schritt Anleitung zur Installation und Einrichtung +> Step-by-step installation and setup guide -**🌍 Sprachen:** **Deutsch** · [English](QUICKSTART.en.md) +**🌍 Languages:** [Deutsch](QUICKSTART.de.md) · **English** --- -## Voraussetzungen +## Prerequisites -- ✅ Android 8.0+ Smartphone/Tablet -- ✅ WLAN-Verbindung -- ✅ Eigener Server mit Docker (optional - für Self-Hosting) +- ✅ Android 8.0+ smartphone/tablet +- ✅ WiFi connection +- ✅ Own server with Docker (optional - for self-hosting) --- -## Option 1: Mit eigenem Server (Self-Hosted) 🏠 +## Option 1: With own server (Self-Hosted) 🏠 -### Schritt 1: WebDAV Server einrichten +### Step 1: Setup WebDAV Server -Auf deinem Server (z.B. Raspberry Pi, NAS, VPS): +On your server (e.g. Raspberry Pi, NAS, VPS): ```bash -# Repository klonen +# Clone repository git clone https://github.com/inventory69/simple-notes-sync.git cd simple-notes-sync/server -# Umgebungsvariablen konfigurieren +# Configure environment variables cp .env.example .env nano .env ``` -**In `.env` anpassen:** +**Adjust in `.env`:** ```env -WEBDAV_PASSWORD=dein-sicheres-passwort-hier +WEBDAV_PASSWORD=your-secure-password-here ``` -**Server starten:** +**Start server:** ```bash docker compose up -d ``` -**IP-Adresse finden:** +**Find IP address:** ```bash ip addr show | grep "inet " | grep -v 127.0.0.1 ``` -➡️ **Notiere dir:** `http://DEINE-SERVER-IP:8080/` +➡️ **Note down:** `http://YOUR-SERVER-IP:8080/` --- -### Schritt 2: App installieren +### Step 2: Install App -1. **APK herunterladen:** [Neueste Version](https://github.com/inventory69/simple-notes-sync/releases/latest) - - Wähle: `simple-notes-sync-vX.X.X-standard-universal.apk` +1. **Download APK:** [Latest version](https://github.com/inventory69/simple-notes-sync/releases/latest) + - Choose: `simple-notes-sync-vX.X.X-standard-universal.apk` -2. **Installation erlauben:** - - Android: Einstellungen → Sicherheit → "Unbekannte Quellen" für deinen Browser aktivieren +2. **Allow installation:** + - Android: Settings → Security → Enable "Unknown sources" for your browser -3. **APK öffnen und installieren** +3. **Open and install APK** --- -### Schritt 3: App konfigurieren +### Step 3: Configure App -1. **App öffnen** +1. **Open app** -2. **Einstellungen öffnen** (⚙️ Icon oben rechts) +2. **Open settings** (⚙️ icon top right) -3. **Server-Einstellungen konfigurieren:** +3. **Configure server settings:** - | Feld | Wert | + | Field | Value | |------|------| - | **WebDAV Server URL** | `http://DEINE-SERVER-IP:8080/` | - | **Benutzername** | `noteuser` | - | **Passwort** | (dein Passwort aus `.env`) | + | **WebDAV Server URL** | `http://YOUR-SERVER-IP:8080/` | + | **Username** | `noteuser` | + | **Password** | (your password from `.env`) | - > **💡 Hinweis:** Gib nur die Base-URL ein (ohne `/notes`). Die App erstellt automatisch `/notes/` für JSON-Dateien und `/notes-md/` für Markdown-Export. + > **💡 Note:** Enter only the base URL (without `/notes`). The app automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown export. -4. **"Verbindung testen"** drücken - - ✅ Erfolg? → Weiter zu Schritt 4 - - ❌ Fehler? → Siehe [Troubleshooting](#troubleshooting) +4. **Press "Test connection"**** + - ✅ Success? → Continue to step 4 + - ❌ Error? → See [Troubleshooting](#troubleshooting) -5. **Auto-Sync aktivieren** (Toggle Switch) +5. **Enable auto-sync** (toggle switch) -6. **Sync-Intervall wählen:** - - **15 Min** - Maximale Aktualität (~0.8% Akku/Tag) - - **30 Min** - Empfohlen (~0.4% Akku/Tag) ⭐ - - **60 Min** - Maximale Akkulaufzeit (~0.2% Akku/Tag) +6. **Choose sync interval:** + - **15 min** - Maximum currency (~0.8% battery/day) + - **30 min** - Recommended (~0.4% battery/day) ⭐ + - **60 min** - Maximum battery life (~0.2% battery/day) --- -### Schritt 4: Erste Notiz erstellen +### Step 4: Create First Note -1. Zurück zur Hauptansicht (← Pfeil) +1. Back to main view (← arrow) -2. **"Notiz hinzufügen"** (+ Icon) +2. **"Add note"** (+ icon) -3. Titel und Text eingeben +3. Enter title and text -4. **Speichern** (💾 Icon) +4. **Save** (💾 icon) -5. **Warten auf Auto-Sync** (oder manuell: ⚙️ → "Jetzt synchronisieren") +5. **Wait for auto-sync** (or manually: ⚙️ → "Sync now") -🎉 **Fertig!** Deine Notizen werden automatisch synchronisiert! +🎉 **Done!** Your notes will be automatically synchronized! --- -## Option 2: Nur lokale Notizen (kein Server) 📱 +## Option 2: Local notes only (no server) 📱 -Du kannst Simple Notes auch **ohne Server** nutzen: +You can also use Simple Notes **without a server**: -1. **App installieren** (siehe Schritt 2 oben) +1. **Install app** (see step 2 above) -2. **Ohne Server-Konfiguration verwenden:** - - Notizen werden nur lokal gespeichert - - Kein Auto-Sync - - Perfekt für reine Offline-Nutzung +2. **Use without server configuration:** + - Notes are only stored locally + - No auto-sync + - Perfect for offline-only use --- -## 🔋 Akku-Optimierung deaktivieren +## 🔋 Disable Battery Optimization -Für zuverlässigen Auto-Sync: +For reliable auto-sync: -1. **Einstellungen** → **Apps** → **Simple Notes Sync** +1. **Settings** → **Apps** → **Simple Notes Sync** -2. **Akku** → **Akkuverbrauch** +2. **Battery** → **Battery usage** -3. Wähle: **"Nicht optimieren"** oder **"Unbeschränkt"** +3. Select: **"Don't optimize"** or **"Unrestricted"** -💡 **Hinweis:** Android Doze Mode kann trotzdem Sync im Standby verzögern (~60 Min). Das ist normal und betrifft alle Apps. +💡 **Note:** Android Doze Mode may still delay sync in standby (~60 min). This is normal and affects all apps. --- -## 📊 Sync-Intervalle im Detail +## 📊 Sync Intervals in Detail -| Intervall | Syncs/Tag | Akku/Tag | Akku/Sync | Anwendungsfall | +| Interval | Syncs/day | Battery/day | Battery/sync | Use case | |-----------|-----------|----------|-----------|----------------| -| **15 Min** | ~96 | ~0.8% (~23 mAh) | ~0.008% | ⚡ Maximal aktuell (mehrere Geräte) | -| **30 Min** | ~48 | ~0.4% (~12 mAh) | ~0.008% | ✓ **Empfohlen** - ausgewogen | -| **60 Min** | ~24 | ~0.2% (~6 mAh) | ~0.008% | 🔋 Maximale Akkulaufzeit | +| **15 min** | ~96 | ~0.8% (~23 mAh) | ~0.008% | ⚡ Maximum currency (multiple devices) | +| **30 min** | ~48 | ~0.4% (~12 mAh) | ~0.008% | ✓ **Recommended** - balanced | +| **60 min** | ~24 | ~0.2% (~6 mAh) | ~0.008% | 🔋 Maximum battery life | --- ## 🐛 Troubleshooting -### Verbindungstest schlägt fehl +### Connection test fails -**Problem:** "Verbindung fehlgeschlagen" beim Test +**Problem:** "Connection failed" during test -**Lösungen:** +**Solutions:** -1. **Server läuft?** +1. **Server running?** ```bash docker compose ps - # Sollte "Up" zeigen + # Should show "Up" ``` -2. **Gleiches Netzwerk?** - - Smartphone und Server müssen im selben Netzwerk sein +2. **Same network?** + - Smartphone and server must be on same network -3. **IP-Adresse korrekt?** +3. **IP address correct?** ```bash ip addr show | grep "inet " - # Prüfe ob IP in URL stimmt + # Check if IP in URL matches ``` 4. **Firewall?** ```bash - # Port 8080 öffnen (falls Firewall aktiv) + # Open port 8080 (if firewall active) sudo ufw allow 8080/tcp ``` -5. **Server-Logs prüfen:** +5. **Check server logs:** ```bash docker compose logs -f ``` --- -### Auto-Sync funktioniert nicht +### Auto-sync not working -**Problem:** Notizen werden nicht automatisch synchronisiert +**Problem:** Notes are not automatically synchronized -**Lösungen:** +**Solutions:** -1. **Auto-Sync aktiviert?** - - ⚙️ Einstellungen → Toggle "Auto-Sync" muss **AN** sein +1. **Auto-sync enabled?** + - ⚙️ Settings → Toggle "Auto-sync" must be **ON** -2. **Akku-Optimierung deaktiviert?** - - Siehe [Akku-Optimierung](#-akku-optimierung-deaktivieren) +2. **Battery optimization disabled?** + - See [Disable Battery Optimization](#-disable-battery-optimization) -3. **Mit WiFi verbunden?** - - Auto-Sync triggert bei jeder WiFi-Verbindung - - Prüfe, ob du mit einem WLAN verbunden bist +3. **Connected to WiFi?** + - Auto-sync triggers on any WiFi connection + - Check if you're connected to a WiFi network -4. **Manuell testen:** - - ⚙️ Einstellungen → "Jetzt synchronisieren" - - Funktioniert das? → Auto-Sync sollte auch funktionieren +4. **Test manually:** + - ⚙️ Settings → "Sync now" + - Works? → Auto-sync should work too --- -### Notizen werden nicht angezeigt +### Notes not showing up -**Problem:** Nach Installation sind keine Notizen da, obwohl welche auf dem Server liegen +**Problem:** After installation, no notes visible even though they exist on server -**Lösung:** +**Solution:** -1. **Einmalig manuell synchronisieren:** - - ⚙️ Einstellungen → "Jetzt synchronisieren" +1. **Manually sync once:** + - ⚙️ Settings → "Sync now" -2. **Server-Daten prüfen:** +2. **Check server data:** ```bash docker compose exec webdav ls -la /data/ - # Sollte .json Dateien zeigen + # Should show .json files ``` --- -### Fehler beim Sync +### Sync errors -**Problem:** Fehlermeldung beim Synchronisieren +**Problem:** Error message during sync -**Lösungen:** +**Solutions:** -1. **"401 Unauthorized"** → Passwort falsch - - Prüfe Passwort in App-Einstellungen - - Vergleiche mit `.env` auf Server +1. **"401 Unauthorized"** → Wrong password + - Check password in app settings + - Compare with `.env` on server -2. **"404 Not Found"** → URL falsch - - Sollte enden mit `/` (z.B. `http://192.168.1.100:8080/`) +2. **"404 Not Found"** → Wrong URL + - Should end with `/` (e.g. `http://192.168.1.100:8080/`) -3. **"Network error"** → Keine Verbindung - - Siehe [Verbindungstest schlägt fehl](#verbindungstest-schlägt-fehl) +3. **"Network error"** → No connection + - See [Connection test fails](#connection-test-fails) --- ## 📱 Updates -### Automatisch mit Obtainium (empfohlen) +### Automatic with Obtainium (recommended) -1. **[Obtainium installieren](https://github.com/ImranR98/Obtanium/releases/latest)** +1. **[Install Obtainium](https://github.com/ImranR98/Obtanium/releases/latest)** -2. **App hinzufügen:** +2. **Add app:** - URL: `https://github.com/inventory69/simple-notes-sync` - - Auto-Update aktivieren + - Enable auto-update -3. **Fertig!** Obtainium benachrichtigt dich bei neuen Versionen +3. **Done!** Obtainium notifies you of new versions -### Manuell +### Manual -1. Neue APK von [Releases](https://github.com/inventory69/simple-notes-sync/releases/latest) herunterladen +1. Download new APK from [Releases](https://github.com/inventory69/simple-notes-sync/releases/latest) -2. Installieren (überschreibt alte Version) +2. Install (overwrites old version) -3. Alle Daten bleiben erhalten! +3. All data remains intact! --- -## 🆘 Weitere Hilfe +## 🆘 Further Help -- **GitHub Issues:** [Problem melden](https://github.com/inventory69/simple-notes-sync/issues) -- **Vollständige Docs:** [DOCS.md](DOCS.md) -- **Server Setup Details:** [server/README.md](server/README.md) +- **GitHub Issues:** [Report problem](https://github.com/inventory69/simple-notes-sync/issues) +- **Complete docs:** [DOCS.en.md](DOCS.en.md) +- **Server setup details:** [server/README.en.md](server/README.en.md) --- -**Version:** 1.1.0 · **Erstellt:** Dezember 2025 +**Version:** 1.1.0 · **Created:** December 2025 diff --git a/README.de.md b/README.de.md new file mode 100644 index 0000000..1fe3111 --- /dev/null +++ b/README.de.md @@ -0,0 +1,115 @@ +# Simple Notes Sync 📝 + +> Minimalistische Offline-Notizen mit Auto-Sync zu deinem eigenen Server + +[![Android](https://img.shields.io/badge/Android-8.0%2B-green.svg)](https://www.android.com/) +[![Material Design 3](https://img.shields.io/badge/Material-Design%203-green.svg)](https://m3.material.io/) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) + +[Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.dettmer.simplenotes) +[Get it on F-Droid](https://f-droid.org/packages/dev.dettmer.simplenotes/) + +**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Dokumentation](docs/DOCS.de.md)** · **🚀 [Quick Start](QUICKSTART.de.md)** + +**🌍 Sprachen:** **Deutsch** · [English](README.md) + +--- + +## 📱 Screenshots + +

+ Notizliste + Notiz bearbeiten + Checkliste bearbeiten + Einstellungen + Server-Einstellungen + Sync-Status +

+ +--- + +## ✨ Highlights + +- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop +- 🌍 **NEU: Mehrsprachig** - Deutsch/Englisch mit Sprachauswahl +- 📝 **Offline-First** - Funktioniert ohne Internet +- 🔄 **Auto-Sync** - WLAN-Verbindung, regelmäßige Intervalle (15/30/60 Min) & Multi-Geräte-Sync +- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV) +- 💾 **Lokales Backup** - Export/Import als JSON-Datei +- 🖥️ **Desktop-Integration** - Markdown-Export für Obsidian, VS Code, Typora +- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag +- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors + +➡️ **Vollständige Feature-Liste:** [FEATURES.de.md](docs/FEATURES.de.md) + +--- + +## 🚀 Schnellstart + +### 1. Server Setup (5 Minuten) + +```bash +git clone https://github.com/inventory69/simple-notes-sync.git +cd simple-notes-sync/server +cp .env.example .env +# Passwort in .env setzen +docker compose up -d +``` + +➡️ **Details:** [Server Setup Guide](server/README.de.md) + +### 2. App Installation (2 Minuten) + +1. [APK herunterladen](https://github.com/inventory69/simple-notes-sync/releases/latest) +2. Installieren & öffnen +3. ⚙️ Einstellungen → Server konfigurieren: + - **URL:** `http://DEINE-SERVER-IP:8080/` _(nur Base-URL!)_ + - **User:** `noteuser` + - **Passwort:** _(aus .env)_ + - **WLAN:** _(dein Netzwerk-Name)_ +4. **Verbindung testen** → Auto-Sync aktivieren +5. Fertig! 🎉 + +➡️ **Ausführliche Anleitung:** [QUICKSTART.de.md](QUICKSTART.de.md) + +--- + +## 📚 Dokumentation + +| Dokument | Inhalt | +|----------|--------| +| **[QUICKSTART.de.md](QUICKSTART.de.md)** | Schritt-für-Schritt Installation | +| **[FEATURES.de.md](docs/FEATURES.de.md)** | Vollständige Feature-Liste | +| **[BACKUP.de.md](docs/BACKUP.de.md)** | Backup & Wiederherstellung | +| **[DESKTOP.de.md](docs/DESKTOP.de.md)** | Desktop-Integration (Markdown) | +| **[DOCS.de.md](docs/DOCS.de.md)** | Technische Details & Troubleshooting | +| **[CHANGELOG.de.md](CHANGELOG.de.md)** | Versionshistorie | +| **[UPCOMING.de.md](docs/UPCOMING.de.md)** | Geplante Features 🚀 | +| **[ÜBERSETZEN.md](docs/TRANSLATING.de.md)** | Übersetzungsanleitung 🌍 | + +--- + +## 🛠️ Entwicklung + +```bash +cd android +./gradlew assembleStandardRelease +``` + +➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md#-build--deployment) + +--- + +## 🤝 Contributing + +Beiträge willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) + +--- + +## 📄 Lizenz + +MIT License - siehe [LICENSE](LICENSE) + +--- + +**v1.4.1** · Built with ❤️ using Kotlin + Material Design 3 diff --git a/README.en.md b/README.en.md deleted file mode 100644 index 30f707e..0000000 --- a/README.en.md +++ /dev/null @@ -1,109 +0,0 @@ -# Simple Notes Sync 📝 - -> Minimalist offline notes with auto-sync to your own server - -[![Android](https://img.shields.io/badge/Android-8.0%2B-green.svg)](https://www.android.com/) -[![Material Design 3](https://img.shields.io/badge/Material-Design%203-green.svg)](https://m3.material.io/) -[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) - -[Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.dettmer.simplenotes) - -**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Documentation](docs/DOCS.en.md)** · **🚀 [Quick Start](QUICKSTART.en.md)** - -**🌍 Languages:** [Deutsch](README.md) · **English** - ---- - -## 📱 Screenshots - -

- Notes list - Edit note - Settings -

- ---- - -## ✨ Highlights - -- ✅ **NEW: Checklists** - Tap-to-check, drag & drop, swipe-to-delete -- 📝 **Offline-first** - Works without internet -- 🔄 **Auto-sync** - On WiFi connection (15/30/60 min) -- 🔒 **Self-hosted** - Your data stays with you (WebDAV) -- 💾 **Local backup** - Export/Import as JSON file -- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora -- 🔋 **Battery-friendly** - ~0.2-0.8% per day -- 🎨 **Material Design 3** - Dark mode & dynamic colors - -➡️ **Complete feature list:** [FEATURES.en.md](docs/FEATURES.en.md) - ---- - -## 🚀 Quick Start - -### 1. Server Setup (5 minutes) - -```bash -git clone https://github.com/inventory69/simple-notes-sync.git -cd simple-notes-sync/server -cp .env.example .env -# Set password in .env -docker compose up -d -``` - -➡️ **Details:** [Server Setup Guide](server/README.en.md) - -### 2. App Installation (2 minutes) - -1. [Download APK](https://github.com/inventory69/simple-notes-sync/releases/latest) -2. Install & open -3. ⚙️ Settings → Configure server: - - **URL:** `http://YOUR-SERVER-IP:8080/` _(base URL only!)_ - - **User:** `noteuser` - - **Password:** _(from .env)_ - - **WiFi:** _(your network name)_ -4. **Test connection** → Enable auto-sync -5. Done! 🎉 - -➡️ **Detailed guide:** [QUICKSTART.en.md](QUICKSTART.en.md) - ---- - -## 📚 Documentation - -| Document | Content | -|----------|---------| -| **[QUICKSTART.en.md](QUICKSTART.en.md)** | Step-by-step installation | -| **[FEATURES.en.md](docs/FEATURES.en.md)** | Complete feature list | -| **[BACKUP.en.md](docs/BACKUP.en.md)** | Backup & restore guide | -| **[DESKTOP.en.md](docs/DESKTOP.en.md)** | Desktop integration (Markdown) | -| **[DOCS.en.md](docs/DOCS.en.md)** | Technical details & troubleshooting | -| **[CHANGELOG.md](CHANGELOG.md)** | Version history | -| **[UPCOMING.en.md](docs/UPCOMING.en.md)** | Upcoming features 🚀 | - ---- - -## 🛠️ Development - -```bash -cd android -./gradlew assembleStandardRelease -``` - -➡️ **Build guide:** [DOCS.en.md](docs/DOCS.en.md#-build--deployment) - ---- - -## 🤝 Contributing - -Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) - ---- - -## 📄 License - -MIT License - see [LICENSE](LICENSE) - ---- - -**v1.4.1** · Built with ❤️ using Kotlin + Material Design 3 diff --git a/README.md b/README.md index b7192f0..0e82781 100644 --- a/README.md +++ b/README.md @@ -1,109 +1,111 @@ # Simple Notes Sync 📝 -> Minimalistische Offline-Notizen mit Auto-Sync zu deinem eigenen Server +> Minimalist offline notes with auto-sync to your own server [![Android](https://img.shields.io/badge/Android-8.0%2B-green.svg)](https://www.android.com/) [![Material Design 3](https://img.shields.io/badge/Material-Design%203-green.svg)](https://m3.material.io/) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dev.dettmer.simplenotes) +[Get it on F-Droid](https://f-droid.org/packages/dev.dettmer.simplenotes/) -**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Dokumentation](docs/DOCS.md)** · **🚀 [Quick Start](QUICKSTART.md)** +**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Documentation](docs/DOCS.md)** · **🚀 [Quick Start](QUICKSTART.md)** -**🌍 Sprachen:** **Deutsch** · [English](README.en.md) +**🌍 Languages:** [Deutsch](README.de.md) · **English** --- ## 📱 Screenshots

- Notizliste - Notiz bearbeiten - Einstellungen + Notes list + Edit note + Edit checklist + Settings + Server settings + Sync status

--- ## ✨ Highlights -- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop, Swipe-to-Delete -- 📝 **Offline-First** - Funktioniert ohne Internet -- 🔄 **Auto-Sync** - Bei WiFi-Verbindung (15/30/60 Min) -- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV) -- 💾 **Lokales Backup** - Export/Import als JSON-Datei -- 🖥️ **Desktop-Integration** - Markdown-Export für Obsidian, VS Code, Typora -- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag -- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors +- ✅ **NEW: Checklists** - Tap-to-check, drag & drop +- 🌍 **NEW: Multilingual** - English/German with language selector +- 📝 **Offline-first** - Works without internet +- 🔄 **Auto-sync** - WiFi reconnect, periodic intervals (15/30/60 min) & multi-device sync +- 🔒 **Self-hosted** - Your data stays with you (WebDAV) +- 💾 **Local backup** - Export/Import as JSON file +- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora +- 🔋 **Battery-friendly** - ~0.2-0.8% per day +- 🎨 **Material Design 3** - Dark mode & dynamic colors -➡️ **Vollständige Feature-Liste:** [FEATURES.md](docs/FEATURES.md) +➡️ **Complete feature list:** [FEATURES.md](docs/FEATURES.md) --- -## 🚀 Schnellstart +## 🚀 Quick Start -### 1. Server Setup (5 Minuten) +### 1. Server Setup (5 minutes) ```bash git clone https://github.com/inventory69/simple-notes-sync.git cd simple-notes-sync/server cp .env.example .env -# Passwort in .env setzen +# Set password in .env docker compose up -d ``` ➡️ **Details:** [Server Setup Guide](server/README.md) -### 2. App Installation (2 Minuten) +### 2. App Installation (2 minutes) -1. [APK herunterladen](https://github.com/inventory69/simple-notes-sync/releases/latest) -2. Installieren & öffnen -3. ⚙️ Einstellungen → Server konfigurieren: - - **URL:** `http://DEINE-SERVER-IP:8080/` _(nur Base-URL!)_ +1. [Download APK](https://github.com/inventory69/simple-notes-sync/releases/latest) +2. Install & open +3. ⚙️ Settings → Configure server: + - **URL:** `http://YOUR-SERVER-IP:8080/` _(base URL only!)_ - **User:** `noteuser` - - **Passwort:** _(aus .env)_ - - **WLAN:** _(dein Netzwerk-Name)_ -4. **Verbindung testen** → Auto-Sync aktivieren -5. Fertig! 🎉 + - **Password:** _(from .env)_ + - **WiFi:** _(your network name)_ +4. **Test connection** → Enable auto-sync +5. Done! 🎉 -➡️ **Ausführliche Anleitung:** [QUICKSTART.md](QUICKSTART.md) +➡️ **Detailed guide:** [QUICKSTART.md](QUICKSTART.md) --- -## 📚 Dokumentation +## 📚 Documentation -| Dokument | Inhalt | -|----------|--------| -| **[QUICKSTART.md](QUICKSTART.md)** | Schritt-für-Schritt Installation | -| **[FEATURES.md](docs/FEATURES.md)** | Vollständige Feature-Liste | -| **[BACKUP.md](docs/BACKUP.md)** | Backup & Wiederherstellung | -| **[DESKTOP.md](docs/DESKTOP.md)** | Desktop-Integration (Markdown) | -| **[DOCS.md](docs/DOCS.md)** | Technische Details & Troubleshooting | -| **[CHANGELOG.md](CHANGELOG.md)** | Versionshistorie | -| **[UPCOMING.md](docs/UPCOMING.md)** | Geplante Features 🚀 | - ---- - -## 🛠️ Entwicklung +| Document | Content | +|----------|---------| +| **[QUICKSTART.md](QUICKSTART.md)** | Step-by-step installation | +| **[FEATURES.md](docs/FEATURES.md)** | Complete feature list | +| **[BACKUP.md](docs/BACKUP.md)** | Backup & restore guide | +| **[DESKTOP.md](docs/DESKTOP.md)** | Desktop integration (Markdown) | +| **[DOCS.md](docs/DOCS.md)** | Technical details & troubleshooting | +| **[CHANGELOG.md](CHANGELOG.md)** | Version history | +| **[UPCOMING.md](docs/UPCOMING.md)** | Upcoming features 🚀 | +| **[TRANSLATING.md](docs/TRANSLATING.md)** | Translation guide 🌍 | ```bash cd android ./gradlew assembleStandardRelease ``` -➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md#-build--deployment) +➡️ **Build guide:** [DOCS.md](docs/DOCS.md#-build--deployment) --- ## 🤝 Contributing -Beiträge willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) +Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) --- -## 📄 Lizenz +## 📄 License -MIT License - siehe [LICENSE](LICENSE) +MIT License - see [LICENSE](LICENSE) --- -**v1.4.1** · Built with ❤️ using Kotlin + Material Design 3 +**v1.5.0** · Built with ❤️ using Kotlin + Jetpack Compose + Material Design 3 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 495c3ea..2115304 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + 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) alias(libs.plugins.detekt) @@ -20,8 +21,8 @@ android { applicationId = "dev.dettmer.simplenotes" minSdk = 24 targetSdk = 36 - versionCode = 12 // 🔧 v1.4.1: Bugfixes (Root-Delete, Checklist Compat) - versionName = "1.4.1" // 🔧 v1.4.1: Root-Folder Delete Fix, Checklisten-Sync Abwärtskompatibilität + versionCode = 13 // 🔧 v1.5.0: Jetpack Compose Settings Redesign + versionName = "1.5.0" // 🔧 v1.5.0: Jetpack Compose Settings Redesign testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -96,6 +97,12 @@ android { buildFeatures { viewBinding = true buildConfig = true // Enable BuildConfig generation + compose = true // v1.5.0: Jetpack Compose für Settings Redesign + } + + // v1.5.0 Hotfix: Strong Skipping Mode für bessere 120Hz Performance + composeCompiler { + enableStrongSkippingMode = true } compileOptions { @@ -135,6 +142,20 @@ dependencies { // SwipeRefreshLayout für Pull-to-Refresh implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + // ═══════════════════════════════════════════════════════════════════════ + // v1.5.0: Jetpack Compose für Settings Redesign + // ═══════════════════════════════════════════════════════════════════════ + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.lifecycle.runtime.compose) + debugImplementation(libs.androidx.compose.ui.tooling) + // Testing (bleiben so) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/android/app/src/debug/res/drawable/ic_launcher_foreground_debug.xml b/android/app/src/debug/res/drawable/ic_launcher_foreground_debug.xml new file mode 100644 index 0000000..50f6b6d --- /dev/null +++ b/android/app/src/debug/res/drawable/ic_launcher_foreground_debug.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..1cdebeb --- /dev/null +++ b/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..10807ae --- /dev/null +++ b/android/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android/app/src/debug/res/values/colors_debug.xml b/android/app/src/debug/res/values/colors_debug.xml new file mode 100644 index 0000000..e52d644 --- /dev/null +++ b/android/app/src/debug/res/values/colors_debug.xml @@ -0,0 +1,5 @@ + + + + #FFB74D + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index be931a7..bc7094a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,13 +22,15 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:localeConfig="@xml/locales_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SimpleNotes" android:networkSecurityConfig="@xml/network_security_config" tools:targetApi="31"> + @@ -37,16 +39,35 @@ - + + + + + android:parentActivityName=".ui.main.ComposeMainActivity" /> - + + + + + android:parentActivityName=".ui.main.ComposeMainActivity" /> + + + { + setSyncControlsEnabled(false) + // Kein Banner anzeigen bei Silent-Sync (z.B. onResume Auto-Sync) + } } } } @@ -222,6 +227,7 @@ class MainActivity : AppCompatActivity() { * - Nur Success-Toast (kein "Auto-Sync..." Toast) * * NOTE: WiFi-Connect Sync nutzt WorkManager (auch wenn App geschlossen!) + * v1.5.0: Silent-Sync - kein Banner während des Syncs, Fehler werden trotzdem angezeigt */ private fun triggerAutoSync(source: String = "unknown") { // Throttling: Max 1 Sync pro Minute @@ -230,7 +236,8 @@ class MainActivity : AppCompatActivity() { } // 🔄 v1.3.1: Check if sync already running - if (!SyncStateManager.tryStartSync("auto-$source")) { + // v1.5.0: silent=true - kein Banner bei Auto-Sync + if (!SyncStateManager.tryStartSync("auto-$source", silent = true)) { Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress") return } @@ -460,10 +467,10 @@ class MainActivity : AppCompatActivity() { val checkboxAlways = dialogView.findViewById(R.id.checkboxAlwaysDeleteFromServer) MaterialAlertDialogBuilder(this) - .setTitle("Notiz löschen") - .setMessage("\"${note.title}\" wird lokal gelöscht.\n\nAuch vom Server löschen?") + .setTitle(getString(R.string.legacy_delete_dialog_title)) + .setMessage(getString(R.string.legacy_delete_dialog_message, note.title)) .setView(dialogView) - .setNeutralButton("Abbrechen") { _, _ -> + .setNeutralButton(getString(R.string.cancel)) { _, _ -> // RESTORE: Re-submit original list (note is NOT deleted from storage) adapter.submitList(originalList) } @@ -478,7 +485,7 @@ class MainActivity : AppCompatActivity() { // NOW actually delete from storage deleteNoteLocally(note, deleteFromServer = false) } - .setNegativeButton("Vom Server löschen") { _, _ -> + .setNegativeButton(getString(R.string.legacy_delete_from_server)) { _, _ -> if (checkboxAlways.isChecked) { prefs.edit().putBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, true).apply() } @@ -500,13 +507,13 @@ class MainActivity : AppCompatActivity() { // Show Snackbar with UNDO option val message = if (deleteFromServer) { - "\"${note.title}\" wird lokal und vom Server gelöscht" + getString(R.string.legacy_delete_with_server, note.title) } else { - "\"${note.title}\" lokal gelöscht (Server bleibt)" + getString(R.string.legacy_delete_local_only, note.title) } Snackbar.make(recyclerViewNotes, message, Snackbar.LENGTH_LONG) - .setAction("RÜCKGÄNGIG") { + .setAction(getString(R.string.snackbar_undo)) { // UNDO: Restore note storage.saveNote(note) pendingDeletions.remove(note.id) @@ -528,7 +535,7 @@ class MainActivity : AppCompatActivity() { runOnUiThread { Toast.makeText( this@MainActivity, - "Vom Server gelöscht", + getString(R.string.snackbar_deleted_from_server), Toast.LENGTH_SHORT ).show() } @@ -536,7 +543,7 @@ class MainActivity : AppCompatActivity() { runOnUiThread { Toast.makeText( this@MainActivity, - "Server-Löschung fehlgeschlagen", + getString(R.string.snackbar_server_delete_failed), Toast.LENGTH_LONG ).show() } @@ -638,7 +645,8 @@ class MainActivity : AppCompatActivity() { } private fun openSettings() { - val intent = Intent(this, SettingsActivity::class.java) + // v1.5.0: Use new Jetpack Compose Settings + val intent = Intent(this, dev.dettmer.simplenotes.ui.settings.ComposeSettingsActivity::class.java) @Suppress("DEPRECATION") startActivityForResult(intent, REQUEST_SETTINGS) } @@ -792,10 +800,9 @@ class MainActivity : AppCompatActivity() { REQUEST_NOTIFICATION_PERMISSION -> { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showToast("Benachrichtigungen aktiviert") + showToast(getString(R.string.toast_notifications_enabled)) } else { - showToast("Benachrichtigungen deaktiviert. " + - "Du kannst sie in den Einstellungen aktivieren.") + showToast(getString(R.string.toast_notifications_disabled)) } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt index a4c926f..8306374 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt @@ -227,9 +227,9 @@ class SettingsActivity : AppCompatActivity() { */ private fun updateProtocolHint() { protocolHintText.text = if (radioHttp.isChecked) { - "HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x)" + getString(R.string.server_connection_http_hint) } else { - "HTTPS für sichere Verbindungen über das Internet" + getString(R.string.server_connection_https_hint) } } @@ -359,7 +359,7 @@ class SettingsActivity : AppCompatActivity() { 60L -> "60 Minuten" else -> "$newInterval Minuten" } - showToast("⏱️ Sync-Intervall auf $intervalText geändert") + showToast(getString(R.string.toast_sync_interval_changed, intervalText)) Logger.i(TAG, "Sync interval changed to $newInterval minutes, restarted periodic sync") } else { showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)") @@ -379,7 +379,7 @@ class SettingsActivity : AppCompatActivity() { textViewAppVersion.text = "Version $versionName ($versionCode)" } catch (e: Exception) { Logger.e(TAG, "Failed to load version info", e) - textViewAppVersion.text = "Version nicht verfügbar" + textViewAppVersion.text = getString(R.string.version_not_available) } // GitHub Repository Card @@ -475,12 +475,12 @@ class SettingsActivity : AppCompatActivity() { */ private fun showClearLogsConfirmation() { AlertDialog.Builder(this) - .setTitle("Logs löschen?") - .setMessage("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.") - .setPositiveButton("Löschen") { _, _ -> + .setTitle(getString(R.string.debug_delete_logs_title)) + .setMessage(getString(R.string.debug_delete_logs_message)) + .setPositiveButton(getString(R.string.delete)) { _, _ -> clearLogs() } - .setNegativeButton("Abbrechen", null) + .setNegativeButton(getString(R.string.cancel), null) .show() } @@ -491,13 +491,13 @@ class SettingsActivity : AppCompatActivity() { try { val cleared = Logger.clearLogFile(this) if (cleared) { - showToast("🗑️ Logs gelöscht") + showToast(getString(R.string.toast_logs_deleted)) } else { - showToast("📭 Keine Logs zum Löschen") + showToast(getString(R.string.toast_no_logs_to_delete)) } } catch (e: Exception) { Logger.e(TAG, "Failed to clear logs", e) - showToast("❌ Fehler beim Löschen: ${e.message}") + showToast(getString(R.string.toast_logs_delete_error, e.message ?: "")) } } @@ -510,7 +510,7 @@ class SettingsActivity : AppCompatActivity() { startActivity(intent) } catch (e: Exception) { Logger.e(TAG, "Failed to open URL: $url", e) - showToast("❌ Fehler beim Öffnen des Links") + showToast(getString(R.string.toast_link_error)) } } @@ -524,7 +524,7 @@ class SettingsActivity : AppCompatActivity() { // 🔥 v1.1.2: Validate HTTP URL (only allow for local networks) if (fullUrl.isNotEmpty()) { - val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl) + val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl) if (!isValid) { // Only show error in TextField (no Toast) textInputLayoutServerUrl.isErrorEnabled = true @@ -552,7 +552,7 @@ class SettingsActivity : AppCompatActivity() { // 🔥 v1.1.2: Validate before testing if (fullUrl.isNotEmpty()) { - val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl) + val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl) if (!isValid) { // Only show error in TextField (no Toast) textInputLayoutServerUrl.isErrorEnabled = true @@ -646,7 +646,7 @@ class SettingsActivity : AppCompatActivity() { return } - textViewServerStatus.text = "🔍 Prüfe..." + textViewServerStatus.text = getString(R.string.status_checking) textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray)) lifecycleScope.launch { @@ -803,12 +803,12 @@ class SettingsActivity : AppCompatActivity() { .setMessage( "Damit die App im Hintergrund synchronisieren kann, " + "muss die Akku-Optimierung deaktiviert werden.\n\n" + - "Bitte wähle 'Nicht optimieren' für Simple Notes." + getString(R.string.battery_optimization_dialog_message) ) - .setPositiveButton("Einstellungen öffnen") { _, _ -> + .setPositiveButton(getString(R.string.battery_optimization_open_settings)) { _, _ -> openBatteryOptimizationSettings() } - .setNegativeButton("Später") { dialog, _ -> + .setNegativeButton(getString(R.string.battery_optimization_later)) { dialog, _ -> dialog.dismiss() } .setCancelable(false) @@ -915,20 +915,20 @@ class SettingsActivity : AppCompatActivity() { // Radio Buttons erstellen val radioMerge = android.widget.RadioButton(this).apply { - text = "⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten" + text = getString(R.string.backup_mode_merge_full) id = android.view.View.generateViewId() isChecked = true setPadding(10, 10, 10, 10) } val radioReplace = android.widget.RadioButton(this).apply { - text = "⚪ Ersetzen\n → Alle löschen & Backup importieren" + text = getString(R.string.backup_mode_replace_full) id = android.view.View.generateViewId() setPadding(10, 10, 10, 10) } val radioOverwrite = android.widget.RadioButton(this).apply { - text = "⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten" + text = getString(R.string.backup_mode_overwrite_full) id = android.view.View.generateViewId() setPadding(10, 10, 10, 10) } @@ -978,7 +978,7 @@ class SettingsActivity : AppCompatActivity() { RestoreSource.WEBDAV_SERVER -> performRestoreFromServer(selectedMode) } } - .setNegativeButton("Abbrechen", null) + .setNegativeButton(getString(R.string.cancel), null) .show() } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt index e741261..e6db9d5 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt @@ -5,6 +5,7 @@ import android.net.Uri import com.google.gson.Gson import com.google.gson.GsonBuilder import dev.dettmer.simplenotes.BuildConfig +import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.storage.NotesStorage import dev.dettmer.simplenotes.utils.Logger @@ -144,7 +145,7 @@ class BackupManager(private val context: Context) { if (!validationResult.isValid) { return@withContext RestoreResult( success = false, - error = validationResult.errorMessage ?: "Ungültige Backup-Datei" + error = validationResult.errorMessage ?: context.getString(R.string.error_invalid_backup_file) ) } @@ -171,7 +172,7 @@ class BackupManager(private val context: Context) { Logger.e(TAG, "Failed to restore backup", e) RestoreResult( success = false, - error = "Wiederherstellung fehlgeschlagen: ${e.message}" + error = context.getString(R.string.error_restore_failed, e.message ?: "") ) } } @@ -187,8 +188,7 @@ class BackupManager(private val context: Context) { if (backupData.backupVersion > BACKUP_VERSION) { return ValidationResult( isValid = false, - errorMessage = "Backup-Version nicht unterstützt " + - "(v${backupData.backupVersion} benötigt v${BACKUP_VERSION}+)" + errorMessage = context.getString(R.string.error_backup_version_unsupported, backupData.backupVersion, BACKUP_VERSION) ) } @@ -196,7 +196,7 @@ class BackupManager(private val context: Context) { if (backupData.notes.isEmpty()) { return ValidationResult( isValid = false, - errorMessage = "Backup enthält keine Notizen" + errorMessage = context.getString(R.string.error_backup_empty) ) } @@ -208,7 +208,7 @@ class BackupManager(private val context: Context) { if (invalidNotes.isNotEmpty()) { return ValidationResult( isValid = false, - errorMessage = "Backup enthält ${invalidNotes.size} ungültige Notizen" + errorMessage = context.getString(R.string.error_backup_invalid_notes, invalidNotes.size) ) } @@ -217,7 +217,7 @@ class BackupManager(private val context: Context) { } catch (e: Exception) { ValidationResult( isValid = false, - errorMessage = "Backup-Datei beschädigt oder ungültig: ${e.message}" + errorMessage = context.getString(R.string.error_backup_corrupt, e.message ?: "") ) } } @@ -241,7 +241,7 @@ class BackupManager(private val context: Context) { success = true, importedNotes = newNotes.size, skippedNotes = skippedNotes, - message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen" + message = context.getString(R.string.restore_merge_result, newNotes.size, skippedNotes) ) } @@ -262,10 +262,10 @@ class BackupManager(private val context: Context) { success = true, importedNotes = backupNotes.size, skippedNotes = 0, - message = "Alle Notizen ersetzt: ${backupNotes.size} importiert" + message = context.getString(R.string.restore_replace_result, backupNotes.size) ) } - + /** * Restore-Modus: OVERWRITE_DUPLICATES * Backup überschreibt bei ID-Konflikten @@ -287,7 +287,7 @@ class BackupManager(private val context: Context) { importedNotes = newNotes.size, skippedNotes = 0, overwrittenNotes = overwrittenNotes.size, - message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben" + message = context.getString(R.string.restore_overwrite_result, newNotes.size, overwrittenNotes.size) ) } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt index 6dce920..5c64a3a 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt @@ -1,5 +1,6 @@ package dev.dettmer.simplenotes.models +import androidx.compose.runtime.Immutable import dev.dettmer.simplenotes.utils.Logger import java.text.SimpleDateFormat import java.util.Date @@ -7,6 +8,12 @@ import java.util.Locale import java.util.TimeZone import java.util.UUID +/** + * Note data class with Compose stability annotation. + * @Immutable tells Compose this class is stable and won't change unexpectedly, + * enabling skip optimizations during recomposition. + */ +@Immutable data class Note( val id: String = UUID.randomUUID().toString(), val title: String, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt index edc0d1d..e43f513 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncStateManager.kt @@ -19,7 +19,8 @@ object SyncStateManager { */ enum class SyncState { IDLE, // Kein Sync aktiv - SYNCING, // Sync läuft gerade + SYNCING, // Sync läuft gerade (Banner sichtbar) + SYNCING_SILENT, // v1.5.0: Sync läuft im Hintergrund (kein Banner, z.B. onResume) COMPLETED, // Sync erfolgreich abgeschlossen (kurz anzeigen) ERROR // Sync fehlgeschlagen (kurz anzeigen) } @@ -31,6 +32,7 @@ object SyncStateManager { val state: SyncState = SyncState.IDLE, val message: String? = null, val source: String? = null, // "manual", "auto", "pullToRefresh", "background" + val silent: Boolean = false, // v1.5.0: Wenn true, wird nach Completion kein Banner angezeigt val timestamp: Long = System.currentTimeMillis() ) @@ -44,28 +46,35 @@ object SyncStateManager { private val lock = Any() /** - * Prüft ob gerade ein Sync läuft + * Prüft ob gerade ein Sync läuft (inkl. Silent-Sync) */ val isSyncing: Boolean - get() = _syncStatus.value?.state == SyncState.SYNCING + get() { + val state = _syncStatus.value?.state + return state == SyncState.SYNCING || state == SyncState.SYNCING_SILENT + } /** * Versucht einen Sync zu starten. + * @param source Quelle des Syncs (für Logging) + * @param silent v1.5.0: Wenn true, wird kein Banner angezeigt (z.B. bei onResume Auto-Sync) * @return true wenn Sync gestartet werden kann, false wenn bereits einer läuft */ - fun tryStartSync(source: String): Boolean { + fun tryStartSync(source: String, silent: Boolean = false): Boolean { synchronized(lock) { if (isSyncing) { Logger.d(TAG, "⚠️ Sync already in progress, rejecting new sync from: $source") return false } - Logger.d(TAG, "🔄 Starting sync from: $source") + val syncState = if (silent) SyncState.SYNCING_SILENT else SyncState.SYNCING + Logger.d(TAG, "🔄 Starting sync from: $source (silent=$silent)") _syncStatus.postValue( SyncStatus( - state = SyncState.SYNCING, + state = syncState, message = "Synchronisiere...", - source = source + source = source, + silent = silent // v1.5.0: Merkt sich ob silent für markCompleted() ) ) return true @@ -74,18 +83,29 @@ object SyncStateManager { /** * Markiert Sync als erfolgreich abgeschlossen + * v1.5.0: Bei Silent-Sync direkt auf IDLE wechseln (kein Banner) */ fun markCompleted(message: String? = null) { synchronized(lock) { - val currentSource = _syncStatus.value?.source - Logger.d(TAG, "✅ Sync completed from: $currentSource") - _syncStatus.postValue( - SyncStatus( - state = SyncState.COMPLETED, - message = message, - source = currentSource + val current = _syncStatus.value + val currentSource = current?.source + val wasSilent = current?.silent == true + + Logger.d(TAG, "✅ Sync completed from: $currentSource (silent=$wasSilent)") + + if (wasSilent) { + // v1.5.0: Silent-Sync - direkt auf IDLE, kein Banner anzeigen + _syncStatus.postValue(SyncStatus()) + } else { + // Normaler Sync - COMPLETED State anzeigen + _syncStatus.postValue( + SyncStatus( + state = SyncState.COMPLETED, + message = message, + source = currentSource + ) ) - ) + } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt index 0c3caa7..580a573 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt @@ -6,6 +6,7 @@ import android.net.NetworkCapabilities import com.thegrizzlylabs.sardineandroid.Sardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import dev.dettmer.simplenotes.BuildConfig +import dev.dettmer.simplenotes.R import dev.dettmer.simplenotes.models.DeletionTracker import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.SyncStatus @@ -1852,15 +1853,15 @@ class WebDavSyncService(private val context: Context) { suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) { return@withContext try { val sardine = getOrCreateSardine() - ?: throw SyncException("Sardine client konnte nicht erstellt werden") + ?: throw SyncException(context.getString(R.string.error_sardine_client_failed)) val serverUrl = getServerUrl() - ?: throw SyncException("Server-URL nicht konfiguriert") + ?: throw SyncException(context.getString(R.string.error_server_url_not_configured)) val username = prefs.getString(Constants.KEY_USERNAME, "") ?: "" val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: "" if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) { - throw SyncException("WebDAV-Server nicht vollständig konfiguriert") + throw SyncException(context.getString(R.string.error_server_not_configured)) } Logger.d(TAG, "🔄 Manual Markdown Sync START") diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt new file mode 100644 index 0000000..b3f4435 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/ComposeNoteEditorActivity.kt @@ -0,0 +1,104 @@ +package dev.dettmer.simplenotes.ui.editor + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.lifecycle.AbstractSavedStateViewModelFactory +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.savedstate.SavedStateRegistryOwner +import com.google.android.material.color.DynamicColors +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme + +/** + * Compose-based Note Editor Activity + * + * v1.5.0: Jetpack Compose NoteEditor Redesign + * Replaces the old NoteEditorActivity with a modern Compose implementation. + * + * Supports: + * - TEXT notes with title and content + * - CHECKLIST notes with drag & drop reordering + * - Auto-keyboard focus for new checklist items + */ +class ComposeNoteEditorActivity : ComponentActivity() { + + companion object { + const val EXTRA_NOTE_ID = "extra_note_id" + const val EXTRA_NOTE_TYPE = "extra_note_type" + } + + private val viewModel: NoteEditorViewModel by viewModels { + NoteEditorViewModelFactory( + application = application, + owner = this, + noteId = intent.getStringExtra(EXTRA_NOTE_ID), + noteType = intent.getStringExtra(EXTRA_NOTE_TYPE) ?: NoteType.TEXT.name + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Apply Dynamic Colors for Android 12+ (Material You) + DynamicColors.applyToActivityIfAvailable(this) + + enableEdgeToEdge() + + // v1.5.0: Handle back button with slide animation + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } + }) + + setContent { + SimpleNotesTheme { + NoteEditorScreen( + viewModel = viewModel, + onNavigateBack = { + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } + ) + } + } + } +} + +/** + * Custom ViewModelFactory to pass SavedStateHandle with intent extras + */ +class NoteEditorViewModelFactory( + private val application: android.app.Application, + owner: SavedStateRegistryOwner, + private val noteId: String?, + private val noteType: String +) : AbstractSavedStateViewModelFactory(owner, null) { + + @Suppress("UNCHECKED_CAST") + override fun create( + key: String, + modelClass: Class, + handle: SavedStateHandle + ): T { + // Populate SavedStateHandle with intent extras + handle[NoteEditorViewModel.ARG_NOTE_ID] = noteId + handle[NoteEditorViewModel.ARG_NOTE_TYPE] = noteType + + return NoteEditorViewModel(application, handle) as T + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropState.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropState.kt new file mode 100644 index 0000000..0889ac0 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/DragDropState.kt @@ -0,0 +1,154 @@ +package dev.dettmer.simplenotes.ui.editor + +import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress +import androidx.compose.foundation.gestures.scrollBy +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +/** + * FOSS Drag & Drop State für LazyList + * + * Native Compose-Implementierung ohne externe Dependencies + * v1.5.0: NoteEditor Redesign + */ +class DragDropListState( + private val state: LazyListState, + private val scope: CoroutineScope, + private val onMove: (Int, Int) -> Unit +) { + var draggingItemIndex by mutableStateOf(null) + private set + + private var draggingItemDraggedDelta by mutableFloatStateOf(0f) + private var draggingItemInitialOffset by mutableFloatStateOf(0f) + private var overscrollJob by mutableStateOf(null) + + val draggingItemOffset: Float + get() = draggingItemLayoutInfo?.let { item -> + draggingItemInitialOffset + draggingItemDraggedDelta - item.offset + } ?: 0f + + private val draggingItemLayoutInfo: LazyListItemInfo? + get() = state.layoutInfo.visibleItemsInfo + .firstOrNull { it.index == draggingItemIndex } + + fun onDragStart(offset: Offset, itemIndex: Int) { + draggingItemIndex = itemIndex + draggingItemInitialOffset = draggingItemLayoutInfo?.offset?.toFloat() ?: 0f + draggingItemDraggedDelta = 0f + } + + fun onDragInterrupted() { + draggingItemDraggedDelta = 0f + draggingItemIndex = null + draggingItemInitialOffset = 0f + overscrollJob?.cancel() + } + + fun onDrag(offset: Offset) { + draggingItemDraggedDelta += offset.y + + val draggingItem = draggingItemLayoutInfo ?: return + val startOffset = draggingItem.offset + draggingItemOffset + val endOffset = startOffset + draggingItem.size + + val middleOffset = startOffset + (endOffset - startOffset) / 2f + + val targetItem = state.layoutInfo.visibleItemsInfo.find { item -> + middleOffset.toInt() in item.offset..item.offsetEnd && + draggingItem.index != item.index + } + + if (targetItem != null) { + val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) { + draggingItem.index + } else if (draggingItem.index == state.firstVisibleItemIndex) { + targetItem.index + } else { + null + } + + if (scrollToIndex != null) { + scope.launch { + state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset) + onMove(draggingItem.index, targetItem.index) + } + } else { + onMove(draggingItem.index, targetItem.index) + } + + draggingItemIndex = targetItem.index + } else { + val overscroll = when { + draggingItemDraggedDelta > 0 -> + (endOffset - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f) + draggingItemDraggedDelta < 0 -> + (startOffset - state.layoutInfo.viewportStartOffset).coerceAtMost(0f) + else -> 0f + } + + if (overscroll != 0f) { + if (overscrollJob?.isActive != true) { + overscrollJob = scope.launch { + state.scrollBy(overscroll) + } + } + } else { + overscrollJob?.cancel() + } + } + } + + private val LazyListItemInfo.offsetEnd: Int + get() = this.offset + this.size +} + +@Composable +fun rememberDragDropListState( + lazyListState: LazyListState, + scope: CoroutineScope, + onMove: (Int, Int) -> Unit +): DragDropListState { + return remember(lazyListState, scope) { + DragDropListState( + state = lazyListState, + scope = scope, + onMove = onMove + ) + } +} + +fun Modifier.dragContainer( + dragDropState: DragDropListState, + itemIndex: Int +): Modifier { + return this.pointerInput(dragDropState) { + detectDragGesturesAfterLongPress( + onDragStart = { offset -> + dragDropState.onDragStart(offset, itemIndex) + }, + onDragEnd = { + dragDropState.onDragInterrupted() + }, + onDragCancel = { + dragDropState.onDragInterrupted() + }, + onDrag = { change, offset -> + change.consume() + dragDropState.onDrag(offset) + } + ) + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt new file mode 100644 index 0000000..f382e23 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorScreen.kt @@ -0,0 +1,376 @@ +package dev.dettmer.simplenotes.ui.editor + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.ui.editor.components.ChecklistItemRow +import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog +import kotlinx.coroutines.delay +import dev.dettmer.simplenotes.utils.showToast +import kotlin.math.roundToInt + +/** + * Main Composable for the Note Editor screen. + * + * v1.5.0: Jetpack Compose NoteEditor Redesign + * - Supports both TEXT and CHECKLIST notes + * - Drag & Drop reordering for checklist items + * - Auto-keyboard focus for new items + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NoteEditorScreen( + viewModel: NoteEditorViewModel, + onNavigateBack: () -> Unit +) { + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsState() + val checklistItems by viewModel.checklistItems.collectAsState() + + var showDeleteDialog by remember { mutableStateOf(false) } + var focusNewItemId by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() + + // v1.5.0: Auto-keyboard support + val keyboardController = LocalSoftwareKeyboardController.current + val titleFocusRequester = remember { FocusRequester() } + val contentFocusRequester = remember { FocusRequester() } + + // v1.5.0: Auto-focus and show keyboard + LaunchedEffect(uiState.isNewNote, uiState.noteType) { + delay(100) // Wait for layout + when { + uiState.isNewNote -> { + // New note: focus title + titleFocusRequester.requestFocus() + keyboardController?.show() + } + !uiState.isNewNote && uiState.noteType == NoteType.TEXT -> { + // Editing text note: focus content + contentFocusRequester.requestFocus() + keyboardController?.show() + } + } + } + + // Handle events + LaunchedEffect(Unit) { + viewModel.events.collect { event -> + when (event) { + is NoteEditorEvent.ShowToast -> { + val message = when (event.message) { + ToastMessage.NOTE_IS_EMPTY -> context.getString(R.string.note_is_empty) + ToastMessage.NOTE_SAVED -> context.getString(R.string.note_saved) + ToastMessage.NOTE_DELETED -> context.getString(R.string.note_deleted) + } + context.showToast(message) + } + is NoteEditorEvent.NavigateBack -> onNavigateBack() + is NoteEditorEvent.ShowDeleteConfirmation -> showDeleteDialog = true + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = when (uiState.toolbarTitle) { + ToolbarTitle.NEW_NOTE -> stringResource(R.string.new_note) + ToolbarTitle.EDIT_NOTE -> stringResource(R.string.edit_note) + ToolbarTitle.NEW_CHECKLIST -> stringResource(R.string.new_checklist) + ToolbarTitle.EDIT_CHECKLIST -> stringResource(R.string.edit_checklist) + } + ) + }, + navigationIcon = { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + actions = { + // Delete button (only for existing notes) + if (viewModel.canDelete()) { + IconButton(onClick = { showDeleteDialog = true }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.delete) + ) + } + } + + // Save button + IconButton(onClick = { viewModel.saveNote() }) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = stringResource(R.string.save) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + }, + modifier = Modifier.imePadding() + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + // Title Input (for both types) + OutlinedTextField( + value = uiState.title, + onValueChange = { viewModel.updateTitle(it) }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(titleFocusRequester), + label = { Text(stringResource(R.string.title)) }, + singleLine = false, + maxLines = 2, + shape = RoundedCornerShape(16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + when (uiState.noteType) { + NoteType.TEXT -> { + // Content Input for TEXT notes + TextNoteContent( + content = uiState.content, + onContentChange = { viewModel.updateContent(it) }, + focusRequester = contentFocusRequester, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + + NoteType.CHECKLIST -> { + // Checklist Editor + ChecklistEditor( + items = checklistItems, + scope = scope, + focusNewItemId = focusNewItemId, + onTextChange = { id, text -> viewModel.updateChecklistItemText(id, text) }, + onCheckedChange = { id, checked -> viewModel.updateChecklistItemChecked(id, checked) }, + onDelete = { id -> viewModel.deleteChecklistItem(id) }, + onAddNewItemAfter = { id -> + val newId = viewModel.addChecklistItemAfter(id) + focusNewItemId = newId + }, + onAddItemAtEnd = { + val newId = viewModel.addChecklistItemAtEnd() + focusNewItemId = newId + }, + onMove = { from, to -> viewModel.moveChecklistItem(from, to) }, + onFocusHandled = { focusNewItemId = null }, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + } + } + } + + // Delete Confirmation Dialog - v1.5.0: Use shared component with server/local options + if (showDeleteDialog) { + DeleteConfirmationDialog( + noteCount = 1, + onDismiss = { showDeleteDialog = false }, + onDeleteLocal = { + showDeleteDialog = false + viewModel.deleteNote(deleteOnServer = false) + }, + onDeleteEverywhere = { + showDeleteDialog = false + viewModel.deleteNote(deleteOnServer = true) + } + ) + } +} + +@Composable +private fun TextNoteContent( + content: String, + onContentChange: (String) -> Unit, + focusRequester: FocusRequester, + modifier: Modifier = Modifier +) { + // v1.5.0: Use TextFieldValue to control cursor position + // Track if initial cursor position has been set (only set to end once on first load) + var initialCursorSet by remember { mutableStateOf(false) } + + var textFieldValue by remember { + mutableStateOf(TextFieldValue( + text = content, + selection = TextRange(content.length) + )) + } + + // Set initial cursor position only once when content first loads + LaunchedEffect(Unit) { + if (!initialCursorSet && content.isNotEmpty()) { + textFieldValue = TextFieldValue( + text = content, + selection = TextRange(content.length) + ) + initialCursorSet = true + } + } + + OutlinedTextField( + value = textFieldValue, + onValueChange = { newValue -> + textFieldValue = newValue + onContentChange(newValue.text) + }, + modifier = modifier.focusRequester(focusRequester), + label = { Text(stringResource(R.string.content)) }, + shape = RoundedCornerShape(16.dp) + ) +} + +@Composable +private fun ChecklistEditor( + items: List, + scope: kotlinx.coroutines.CoroutineScope, + focusNewItemId: String?, + onTextChange: (String, String) -> Unit, + onCheckedChange: (String, Boolean) -> Unit, + onDelete: (String) -> Unit, + onAddNewItemAfter: (String) -> Unit, + onAddItemAtEnd: () -> Unit, + onMove: (Int, Int) -> Unit, + onFocusHandled: () -> Unit, + modifier: Modifier = Modifier +) { + val listState = rememberLazyListState() + val dragDropState = rememberDragDropListState( + lazyListState = listState, + scope = scope, + onMove = onMove + ) + + Column(modifier = modifier) { + LazyColumn( + state = listState, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + itemsIndexed( + items = items, + key = { _, item -> item.id } + ) { index, item -> + val isDragging = dragDropState.draggingItemIndex == index + val elevation by animateDpAsState( + targetValue = if (isDragging) 8.dp else 0.dp, + label = "elevation" + ) + + val shouldFocus = item.id == focusNewItemId + + // v1.5.0: Clear focus request after handling + LaunchedEffect(shouldFocus) { + if (shouldFocus) { + onFocusHandled() + } + } + + ChecklistItemRow( + item = item, + onTextChange = { onTextChange(item.id, it) }, + onCheckedChange = { onCheckedChange(item.id, it) }, + onDelete = { onDelete(item.id) }, + onAddNewItem = { onAddNewItemAfter(item.id) }, + requestFocus = shouldFocus, + modifier = Modifier + .dragContainer(dragDropState, index) + .offset { + IntOffset( + 0, + if (isDragging) dragDropState.draggingItemOffset.roundToInt() else 0 + ) + } + .shadow(elevation, shape = RoundedCornerShape(8.dp)) + .background( + color = MaterialTheme.colorScheme.surface, + shape = RoundedCornerShape(8.dp) + ) + ) + } + } + + // Add Item Button + TextButton( + onClick = onAddItemAtEnd, + modifier = Modifier.padding(start = 8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp) + ) + Text(stringResource(R.string.add_item)) + } + } +} + +// v1.5.0: Local DeleteConfirmationDialog removed - now using shared component from ui/main/components/ diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt new file mode 100644 index 0000000..489a068 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/NoteEditorViewModel.kt @@ -0,0 +1,383 @@ +package dev.dettmer.simplenotes.ui.editor + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dev.dettmer.simplenotes.models.ChecklistItem +import dev.dettmer.simplenotes.models.Note +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.models.SyncStatus +import dev.dettmer.simplenotes.storage.NotesStorage +import dev.dettmer.simplenotes.sync.WebDavSyncService +import dev.dettmer.simplenotes.utils.DeviceIdGenerator +import dev.dettmer.simplenotes.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.UUID + +/** + * ViewModel for NoteEditor Compose Screen + * v1.5.0: Jetpack Compose NoteEditor Redesign + * + * Manages note editing state including title, content, and checklist items. + */ +class NoteEditorViewModel( + application: Application, + private val savedStateHandle: SavedStateHandle +) : AndroidViewModel(application) { + + companion object { + private const val TAG = "NoteEditorViewModel" + const val ARG_NOTE_ID = "noteId" + const val ARG_NOTE_TYPE = "noteType" + } + + private val storage = NotesStorage(application) + + // ═══════════════════════════════════════════════════════════════════════ + // State + // ═══════════════════════════════════════════════════════════════════════ + + private val _uiState = MutableStateFlow(NoteEditorUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _checklistItems = MutableStateFlow>(emptyList()) + val checklistItems: StateFlow> = _checklistItems.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Events + // ═══════════════════════════════════════════════════════════════════════ + + private val _events = MutableSharedFlow() + val events: SharedFlow = _events.asSharedFlow() + + // Internal state + private var existingNote: Note? = null + private var currentNoteType: NoteType = NoteType.TEXT + + init { + loadNote() + } + + private fun loadNote() { + val noteId = savedStateHandle.get(ARG_NOTE_ID) + val noteTypeString = savedStateHandle.get(ARG_NOTE_TYPE) ?: NoteType.TEXT.name + + if (noteId != null) { + // Load existing note + existingNote = storage.loadNote(noteId) + existingNote?.let { note -> + currentNoteType = note.noteType + _uiState.update { state -> + state.copy( + title = note.title, + content = note.content, + noteType = note.noteType, + isNewNote = false, + toolbarTitle = if (note.noteType == NoteType.CHECKLIST) { + ToolbarTitle.EDIT_CHECKLIST + } else { + ToolbarTitle.EDIT_NOTE + } + ) + } + + if (note.noteType == NoteType.CHECKLIST) { + val items = note.checklistItems?.sortedBy { it.order }?.map { + ChecklistItemState( + id = it.id, + text = it.text, + isChecked = it.isChecked, + order = it.order + ) + } ?: emptyList() + _checklistItems.value = items + } + } + } else { + // New note + currentNoteType = try { + NoteType.valueOf(noteTypeString) + } catch (e: IllegalArgumentException) { + Logger.w(TAG, "Invalid note type '$noteTypeString', defaulting to TEXT") + NoteType.TEXT + } + + _uiState.update { state -> + state.copy( + noteType = currentNoteType, + isNewNote = true, + toolbarTitle = if (currentNoteType == NoteType.CHECKLIST) { + ToolbarTitle.NEW_CHECKLIST + } else { + ToolbarTitle.NEW_NOTE + } + ) + } + + // Add first empty item for new checklists + if (currentNoteType == NoteType.CHECKLIST) { + _checklistItems.value = listOf(ChecklistItemState.createEmpty(0)) + } + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun updateTitle(title: String) { + _uiState.update { it.copy(title = title) } + } + + fun updateContent(content: String) { + _uiState.update { it.copy(content = content) } + } + + fun updateChecklistItemText(itemId: String, newText: String) { + _checklistItems.update { items -> + items.map { item -> + if (item.id == itemId) item.copy(text = newText) else item + } + } + } + + fun updateChecklistItemChecked(itemId: String, isChecked: Boolean) { + _checklistItems.update { items -> + items.map { item -> + if (item.id == itemId) item.copy(isChecked = isChecked) else item + } + } + } + + fun addChecklistItemAfter(afterItemId: String): String { + val newItem = ChecklistItemState.createEmpty(0) + _checklistItems.update { items -> + val index = items.indexOfFirst { it.id == afterItemId } + if (index >= 0) { + val newList = items.toMutableList() + newList.add(index + 1, newItem) + // Update order values + newList.mapIndexed { i, item -> item.copy(order = i) } + } else { + items + newItem.copy(order = items.size) + } + } + return newItem.id + } + + fun addChecklistItemAtEnd(): String { + val newItem = ChecklistItemState.createEmpty(_checklistItems.value.size) + _checklistItems.update { items -> items + newItem } + return newItem.id + } + + fun deleteChecklistItem(itemId: String) { + _checklistItems.update { items -> + val filtered = items.filter { it.id != itemId } + // Ensure at least one item exists + if (filtered.isEmpty()) { + listOf(ChecklistItemState.createEmpty(0)) + } else { + // Update order values + filtered.mapIndexed { index, item -> item.copy(order = index) } + } + } + } + + fun moveChecklistItem(fromIndex: Int, toIndex: Int) { + _checklistItems.update { items -> + val mutableList = items.toMutableList() + val item = mutableList.removeAt(fromIndex) + mutableList.add(toIndex, item) + // Update order values + mutableList.mapIndexed { index, i -> i.copy(order = index) } + } + } + + fun saveNote() { + viewModelScope.launch { + val state = _uiState.value + val title = state.title.trim() + + when (currentNoteType) { + NoteType.TEXT -> { + val content = state.content.trim() + + if (title.isEmpty() && content.isEmpty()) { + _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_IS_EMPTY)) + return@launch + } + + val note = if (existingNote != null) { + existingNote!!.copy( + title = title, + content = content, + noteType = NoteType.TEXT, + checklistItems = null, + updatedAt = System.currentTimeMillis(), + syncStatus = SyncStatus.PENDING + ) + } else { + Note( + title = title, + content = content, + noteType = NoteType.TEXT, + checklistItems = null, + deviceId = DeviceIdGenerator.getDeviceId(getApplication()), + syncStatus = SyncStatus.LOCAL_ONLY + ) + } + + storage.saveNote(note) + } + + NoteType.CHECKLIST -> { + // Filter empty items + val validItems = _checklistItems.value + .filter { it.text.isNotBlank() } + .mapIndexed { index, item -> + ChecklistItem( + id = item.id, + text = item.text, + isChecked = item.isChecked, + order = index + ) + } + + if (title.isEmpty() && validItems.isEmpty()) { + _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_IS_EMPTY)) + return@launch + } + + val note = if (existingNote != null) { + existingNote!!.copy( + title = title, + content = "", // Empty for checklists + noteType = NoteType.CHECKLIST, + checklistItems = validItems, + updatedAt = System.currentTimeMillis(), + syncStatus = SyncStatus.PENDING + ) + } else { + Note( + title = title, + content = "", + noteType = NoteType.CHECKLIST, + checklistItems = validItems, + deviceId = DeviceIdGenerator.getDeviceId(getApplication()), + syncStatus = SyncStatus.LOCAL_ONLY + ) + } + + storage.saveNote(note) + } + } + + _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_SAVED)) + _events.emit(NoteEditorEvent.NavigateBack) + } + } + + /** + * Delete the current note + * @param deleteOnServer if true, also triggers server deletion; if false, only deletes locally + * v1.5.0: Added deleteOnServer parameter for unified delete dialog + */ + fun deleteNote(deleteOnServer: Boolean = true) { + viewModelScope.launch { + existingNote?.let { note -> + val noteId = note.id + + // Delete locally first + storage.deleteNote(noteId) + + // Delete from server if requested + if (deleteOnServer) { + try { + val webdavService = WebDavSyncService(getApplication()) + val success = withContext(Dispatchers.IO) { + webdavService.deleteNoteFromServer(noteId) + } + if (success) { + Logger.d(TAG, "Note $noteId deleted from server") + } else { + Logger.w(TAG, "Failed to delete note $noteId from server") + } + } catch (e: Exception) { + Logger.e(TAG, "Error deleting note from server: ${e.message}") + } + } + + _events.emit(NoteEditorEvent.ShowToast(ToastMessage.NOTE_DELETED)) + _events.emit(NoteEditorEvent.NavigateBack) + } + } + } + + fun showDeleteConfirmation() { + viewModelScope.launch { + _events.emit(NoteEditorEvent.ShowDeleteConfirmation) + } + } + + fun canDelete(): Boolean = existingNote != null +} + +// ═══════════════════════════════════════════════════════════════════════════ +// State Classes +// ═══════════════════════════════════════════════════════════════════════════ + +data class NoteEditorUiState( + val title: String = "", + val content: String = "", + val noteType: NoteType = NoteType.TEXT, + val isNewNote: Boolean = true, + val toolbarTitle: ToolbarTitle = ToolbarTitle.NEW_NOTE +) + +data class ChecklistItemState( + val id: String = UUID.randomUUID().toString(), + val text: String = "", + val isChecked: Boolean = false, + val order: Int = 0 +) { + companion object { + fun createEmpty(order: Int): ChecklistItemState { + return ChecklistItemState( + id = UUID.randomUUID().toString(), + text = "", + isChecked = false, + order = order + ) + } + } +} + +enum class ToolbarTitle { + NEW_NOTE, + EDIT_NOTE, + NEW_CHECKLIST, + EDIT_CHECKLIST +} + +enum class ToastMessage { + NOTE_IS_EMPTY, + NOTE_SAVED, + NOTE_DELETED +} + +sealed interface NoteEditorEvent { + data class ShowToast(val message: ToastMessage) : NoteEditorEvent + data object NavigateBack : NoteEditorEvent + data object ShowDeleteConfirmation : NoteEditorEvent +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt new file mode 100644 index 0000000..3af1b2b --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/editor/components/ChecklistItemRow.kt @@ -0,0 +1,178 @@ +package dev.dettmer.simplenotes.ui.editor.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.DragHandle +import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.editor.ChecklistItemState + +/** + * A single row in the checklist editor with drag handle, checkbox, text input, and delete button. + * + * v1.5.0: Jetpack Compose NoteEditor Redesign + */ +@Composable +fun ChecklistItemRow( + item: ChecklistItemState, + onTextChange: (String) -> Unit, + onCheckedChange: (Boolean) -> Unit, + onDelete: () -> Unit, + onAddNewItem: () -> Unit, + requestFocus: Boolean = false, + modifier: Modifier = Modifier +) { + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + var textFieldValue by remember(item.id) { + mutableStateOf(TextFieldValue(text = item.text, selection = TextRange(item.text.length))) + } + + // v1.5.0: Auto-focus AND show keyboard when requestFocus is true (new items) + LaunchedEffect(requestFocus) { + if (requestFocus) { + focusRequester.requestFocus() + keyboardController?.show() + } + } + + // Update text field when external state changes + LaunchedEffect(item.text) { + if (textFieldValue.text != item.text) { + textFieldValue = TextFieldValue( + text = item.text, + selection = TextRange(item.text.length) + ) + } + } + + val alpha = if (item.isChecked) 0.6f else 1.0f + val textDecoration = if (item.isChecked) TextDecoration.LineThrough else TextDecoration.None + + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Drag Handle + Icon( + imageVector = Icons.Default.DragHandle, + contentDescription = stringResource(R.string.drag_to_reorder), + modifier = Modifier + .size(24.dp) + .alpha(0.5f), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.width(4.dp)) + + // Checkbox + Checkbox( + checked = item.isChecked, + onCheckedChange = onCheckedChange, + modifier = Modifier.alpha(alpha) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + // Text Input with placeholder + BasicTextField( + value = textFieldValue, + onValueChange = { newValue -> + // Check for newline (Enter key) + if (newValue.text.contains("\n")) { + val cleanText = newValue.text.replace("\n", "") + textFieldValue = TextFieldValue( + text = cleanText, + selection = TextRange(cleanText.length) + ) + onTextChange(cleanText) + onAddNewItem() + } else { + textFieldValue = newValue + onTextChange(newValue.text) + } + }, + modifier = Modifier + .weight(1f) + .focusRequester(focusRequester) + .alpha(alpha), + textStyle = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurface, + textDecoration = textDecoration + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { onAddNewItem() } + ), + singleLine = false, + maxLines = 5, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + decorationBox = { innerTextField -> + Box { + if (textFieldValue.text.isEmpty()) { + Text( + text = stringResource(R.string.item_placeholder), + style = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + ) + } + innerTextField() + } + } + ) + + Spacer(modifier = Modifier.width(4.dp)) + + // Delete Button + IconButton( + onClick = onDelete, + modifier = Modifier.size(36.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.delete_item), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt new file mode 100644 index 0000000..5af51b1 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt @@ -0,0 +1,386 @@ +package dev.dettmer.simplenotes.ui.main + +import android.Manifest +import android.app.ActivityOptions +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.lifecycleScope +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.google.android.material.color.DynamicColors +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.models.SyncStatus +import dev.dettmer.simplenotes.storage.NotesStorage +import dev.dettmer.simplenotes.sync.SyncStateManager +import dev.dettmer.simplenotes.sync.SyncWorker +import dev.dettmer.simplenotes.ui.settings.ComposeSettingsActivity +import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme +import dev.dettmer.simplenotes.utils.Constants +import dev.dettmer.simplenotes.utils.Logger +import dev.dettmer.simplenotes.utils.NotificationHelper +import kotlinx.coroutines.launch + +/** + * Main Activity with Jetpack Compose UI + * v1.5.0: Complete MainActivity Redesign with Compose + * + * Replaces the old 805-line MainActivity.kt with a modern + * Compose-based implementation featuring: + * - Notes list with swipe-to-delete + * - Pull-to-refresh for sync + * - FAB with note type selection + * - Material 3 Design with Dynamic Colors (Material You) + * - Design consistent with ComposeSettingsActivity + */ +class ComposeMainActivity : ComponentActivity() { + + companion object { + private const val TAG = "ComposeMainActivity" + private const val REQUEST_NOTIFICATION_PERMISSION = 1001 + private const val REQUEST_SETTINGS = 1002 + } + + private val viewModel: MainViewModel by viewModels() + + private val prefs by lazy { + getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) + } + + // Phase 3: Track if coming from editor to scroll to top + private var cameFromEditor = false + + /** + * BroadcastReceiver for Background-Sync Completion (Periodic Sync) + */ + private val syncCompletedReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + val success = intent?.getBooleanExtra("success", false) ?: false + val count = intent?.getIntExtra("count", 0) ?: 0 + + Logger.d(TAG, "📡 Sync completed broadcast received: success=$success, count=$count") + + // UI refresh + if (success && count > 0) { + viewModel.loadNotes() + Logger.d(TAG, "🔄 Notes reloaded after background sync") + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + // Install Splash Screen (Android 12+) + installSplashScreen() + + super.onCreate(savedInstanceState) + + // Apply Dynamic Colors for Material You (Android 12+) + DynamicColors.applyToActivityIfAvailable(this) + + // Enable edge-to-edge display + enableEdgeToEdge() + + // Initialize Logger and enable file logging if configured + Logger.init(this) + if (prefs.getBoolean(Constants.KEY_FILE_LOGGING_ENABLED, false)) { + Logger.setFileLoggingEnabled(true) + } + + // Clear old sync notifications on app start + NotificationHelper.clearSyncNotifications(this) + + // Request notification permission (Android 13+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestNotificationPermission() + } + + // v1.4.1: Migrate checklists for backwards compatibility + migrateChecklistsForBackwardsCompat() + + // Setup Sync State Observer + setupSyncStateObserver() + + setContent { + SimpleNotesTheme { + val context = LocalContext.current + + // Dialog state for delete confirmation + var deleteDialogData by remember { mutableStateOf(null) } + + // Handle delete dialog events + LaunchedEffect(Unit) { + viewModel.showDeleteDialog.collect { data -> + deleteDialogData = data + } + } + + // Handle toast events + LaunchedEffect(Unit) { + viewModel.showToast.collect { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + + // Delete confirmation dialog + deleteDialogData?.let { data -> + DeleteConfirmationDialog( + noteTitle = data.note.title, + onDismiss = { + viewModel.restoreNoteAfterSwipe(data.originalList) + deleteDialogData = null + }, + onDeleteLocal = { + viewModel.deleteNoteConfirmed(data.note, deleteFromServer = false) + deleteDialogData = null + }, + onDeleteFromServer = { + viewModel.deleteNoteConfirmed(data.note, deleteFromServer = true) + deleteDialogData = null + } + ) + } + + MainScreen( + viewModel = viewModel, + onOpenNote = { noteId -> openNoteEditor(noteId) }, + onOpenSettings = { openSettings() }, + onCreateNote = { noteType -> createNote(noteType) } + ) + } + } + } + + override fun onResume() { + super.onResume() + + Logger.d(TAG, "📱 ComposeMainActivity.onResume() - Registering receivers") + + // Register BroadcastReceiver for Background-Sync + LocalBroadcastManager.getInstance(this).registerReceiver( + syncCompletedReceiver, + IntentFilter(SyncWorker.ACTION_SYNC_COMPLETED) + ) + + Logger.d(TAG, "📡 BroadcastReceiver registered (sync-completed)") + + // Reload notes + viewModel.loadNotes() + + // Phase 3: Scroll to top if coming from editor (new/edited note) + if (cameFromEditor) { + viewModel.scrollToTop() + cameFromEditor = false + Logger.d(TAG, "📜 Came from editor - scrolling to top") + } + + // Trigger Auto-Sync on app resume + viewModel.triggerAutoSync("onResume") + } + + override fun onPause() { + super.onPause() + + // Unregister BroadcastReceiver + LocalBroadcastManager.getInstance(this).unregisterReceiver(syncCompletedReceiver) + Logger.d(TAG, "📡 BroadcastReceiver unregistered") + } + + private fun setupSyncStateObserver() { + SyncStateManager.syncStatus.observe(this) { status -> + viewModel.updateSyncState(status) + + // Hide banner after delay for completed/error states + when (status.state) { + SyncStateManager.SyncState.COMPLETED -> { + lifecycleScope.launch { + kotlinx.coroutines.delay(1500L) + SyncStateManager.reset() + } + } + SyncStateManager.SyncState.ERROR -> { + lifecycleScope.launch { + kotlinx.coroutines.delay(3000L) + SyncStateManager.reset() + } + } + else -> { /* No action needed */ } + } + } + } + + private fun openNoteEditor(noteId: String?) { + cameFromEditor = true + val intent = Intent(this, ComposeNoteEditorActivity::class.java) + noteId?.let { + intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_ID, it) + } + + // v1.5.0: Add slide animation + val options = ActivityOptions.makeCustomAnimation( + this, + dev.dettmer.simplenotes.R.anim.slide_in_right, + dev.dettmer.simplenotes.R.anim.slide_out_left + ) + startActivity(intent, options.toBundle()) + } + + private fun createNote(noteType: NoteType) { + cameFromEditor = true + val intent = Intent(this, ComposeNoteEditorActivity::class.java) + intent.putExtra(ComposeNoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name) + + // v1.5.0: Add slide animation + val options = ActivityOptions.makeCustomAnimation( + this, + dev.dettmer.simplenotes.R.anim.slide_in_right, + dev.dettmer.simplenotes.R.anim.slide_out_left + ) + startActivity(intent, options.toBundle()) + } + + private fun openSettings() { + val intent = Intent(this, ComposeSettingsActivity::class.java) + val options = ActivityOptions.makeCustomAnimation( + this, + dev.dettmer.simplenotes.R.anim.slide_in_right, + dev.dettmer.simplenotes.R.anim.slide_out_left + ) + @Suppress("DEPRECATION") + startActivityForResult(intent, REQUEST_SETTINGS, options.toBundle()) + } + + private fun requestNotificationPermission() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + REQUEST_NOTIFICATION_PERMISSION + ) + } + } + } + + /** + * v1.4.1: Migrates existing checklists for backwards compatibility. + */ + private fun migrateChecklistsForBackwardsCompat() { + val migrationKey = "v1.4.1_checklist_migration_done" + + // Only run once + if (prefs.getBoolean(migrationKey, false)) { + return + } + + val storage = NotesStorage(this) + val allNotes = storage.loadAllNotes() + val checklistsToMigrate = allNotes.filter { note -> + note.noteType == NoteType.CHECKLIST && + note.content.isBlank() && + note.checklistItems?.isNotEmpty() == true + } + + if (checklistsToMigrate.isNotEmpty()) { + Logger.d(TAG, "🔄 v1.4.1 Migration: Found ${checklistsToMigrate.size} checklists without fallback content") + + for (note in checklistsToMigrate) { + val updatedNote = note.copy( + syncStatus = SyncStatus.PENDING + ) + storage.saveNote(updatedNote) + Logger.d(TAG, " 📝 Marked for re-sync: ${note.title}") + } + + Logger.d(TAG, "✅ v1.4.1 Migration: ${checklistsToMigrate.size} checklists marked for re-sync") + } + + // Mark migration as done + prefs.edit().putBoolean(migrationKey, true).apply() + } + + @Deprecated("Deprecated in Java") + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_SETTINGS && resultCode == RESULT_OK) { + // Settings changed, reload notes + viewModel.loadNotes() + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + + when (requestCode) { + REQUEST_NOTIFICATION_PERMISSION -> { + if (grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, getString(R.string.toast_notifications_enabled), Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(this, + getString(R.string.toast_notifications_disabled), + Toast.LENGTH_SHORT + ).show() + } + } + } + } +} + +/** + * Delete confirmation dialog + */ +@Composable +private fun DeleteConfirmationDialog( + noteTitle: String, + onDismiss: () -> Unit, + onDeleteLocal: () -> Unit, + onDeleteFromServer: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.legacy_delete_dialog_title)) }, + text = { + Text(stringResource(R.string.legacy_delete_dialog_message, noteTitle)) + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton(onClick = onDeleteLocal) { + Text(stringResource(R.string.delete_local_only)) + } + TextButton(onClick = onDeleteFromServer) { + Text(stringResource(R.string.legacy_delete_from_server)) + } + } + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt new file mode 100644 index 0000000..a3a3dc5 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt @@ -0,0 +1,322 @@ +package dev.dettmer.simplenotes.ui.main + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.SelectAll +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.ExperimentalMaterial3Api +// FabPosition nicht mehr benötigt - FAB wird manuell platziert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.sync.SyncStateManager +import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog +import dev.dettmer.simplenotes.ui.main.components.EmptyState +import dev.dettmer.simplenotes.ui.main.components.NoteTypeFAB +import dev.dettmer.simplenotes.ui.main.components.NotesList +import dev.dettmer.simplenotes.ui.main.components.SyncStatusBanner +import kotlinx.coroutines.launch + +/** + * Main screen displaying the notes list + * v1.5.0: Jetpack Compose MainActivity Redesign + * + * Performance optimized with proper state handling: + * - LazyListState for scroll control + * - Scaffold FAB slot for proper z-ordering + * - Scroll-to-top on new note + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MainScreen( + viewModel: MainViewModel, + onOpenNote: (String?) -> Unit, + onOpenSettings: () -> Unit, + onCreateNote: (NoteType) -> Unit +) { + val notes by viewModel.notes.collectAsState() + val syncState by viewModel.syncState.collectAsState() + val syncMessage by viewModel.syncMessage.collectAsState() + val scrollToTop by viewModel.scrollToTop.collectAsState() + + // Multi-Select State + val selectedNotes by viewModel.selectedNotes.collectAsState() + val isSelectionMode by viewModel.isSelectionMode.collectAsState() + + // Delete confirmation dialog state + var showBatchDeleteDialog by remember { mutableStateOf(false) } + + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val listState = rememberLazyListState() + + // Compute isSyncing once + val isSyncing = syncState == SyncStateManager.SyncState.SYNCING + + // Handle snackbar events from ViewModel + LaunchedEffect(Unit) { + viewModel.showSnackbar.collect { data -> + scope.launch { + val result = snackbarHostState.showSnackbar( + message = data.message, + actionLabel = data.actionLabel, + duration = SnackbarDuration.Long + ) + if (result == SnackbarResult.ActionPerformed) { + data.onAction() + } + } + } + } + + // Phase 3: Scroll to top when new note created + LaunchedEffect(scrollToTop) { + if (scrollToTop) { + listState.animateScrollToItem(0) + viewModel.resetScrollToTop() + } + } + + // v1.5.0 Hotfix: FAB manuell mit zIndex platzieren für garantierte Sichtbarkeit + Scaffold( + topBar = { + // Animated switch between normal and selection TopBar + AnimatedVisibility( + visible = isSelectionMode, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut() + ) { + SelectionTopBar( + selectedCount = selectedNotes.size, + totalCount = notes.size, + onCloseSelection = { viewModel.clearSelection() }, + onSelectAll = { viewModel.selectAllNotes() }, + onDeleteSelected = { showBatchDeleteDialog = true } + ) + } + AnimatedVisibility( + visible = !isSelectionMode, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut() + ) { + MainTopBar( + syncEnabled = !isSyncing, + onSyncClick = { viewModel.triggerManualSync("toolbar") }, + onSettingsClick = onOpenSettings + ) + } + }, + // FAB wird manuell in Box platziert für korrekten z-Index + snackbarHost = { SnackbarHost(snackbarHostState) }, + containerColor = MaterialTheme.colorScheme.surface + ) { paddingValues -> + // PullToRefreshBox wraps the content with pull-to-refresh capability + PullToRefreshBox( + isRefreshing = isSyncing, + onRefresh = { viewModel.triggerManualSync("pullToRefresh") }, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Box(modifier = Modifier.fillMaxSize()) { + // Main content column + Column(modifier = Modifier.fillMaxSize()) { + // Sync Status Banner (not affected by pull-to-refresh) + SyncStatusBanner( + syncState = syncState, + message = syncMessage + ) + + // Content: Empty state or notes list + if (notes.isEmpty()) { + EmptyState(modifier = Modifier.weight(1f)) + } else { + NotesList( + notes = notes, + showSyncStatus = viewModel.isServerConfigured(), + selectedNotes = selectedNotes, + isSelectionMode = isSelectionMode, + listState = listState, + modifier = Modifier.weight(1f), + onNoteClick = { note -> onOpenNote(note.id) }, + onNoteLongPress = { note -> + // Long-press starts selection mode + viewModel.startSelectionMode(note.id) + }, + onNoteSelectionToggle = { note -> + viewModel.toggleNoteSelection(note.id) + } + ) + } + } + + // FAB als TOP-LAYER - nur anzeigen wenn nicht im Selection Mode + AnimatedVisibility( + visible = !isSelectionMode, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp) + .zIndex(Float.MAX_VALUE) + ) { + NoteTypeFAB( + onCreateNote = onCreateNote + ) + } + } + } + + // Batch Delete Confirmation Dialog + if (showBatchDeleteDialog) { + DeleteConfirmationDialog( + noteCount = selectedNotes.size, + onDismiss = { showBatchDeleteDialog = false }, + onDeleteLocal = { + viewModel.deleteSelectedNotes(deleteFromServer = false) + showBatchDeleteDialog = false + }, + onDeleteEverywhere = { + viewModel.deleteSelectedNotes(deleteFromServer = true) + showBatchDeleteDialog = false + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun MainTopBar( + syncEnabled: Boolean, + onSyncClick: () -> Unit, + onSettingsClick: () -> Unit +) { + TopAppBar( + title = { + Text( + text = stringResource(R.string.main_title), + style = MaterialTheme.typography.titleLarge + ) + }, + actions = { + IconButton( + onClick = onSyncClick, + enabled = syncEnabled + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = stringResource(R.string.action_sync) + ) + } + IconButton(onClick = onSettingsClick) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.action_settings) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) +} + +/** + * Selection mode TopBar - shows selected count and actions + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SelectionTopBar( + selectedCount: Int, + totalCount: Int, + onCloseSelection: () -> Unit, + onSelectAll: () -> Unit, + onDeleteSelected: () -> Unit +) { + TopAppBar( + navigationIcon = { + IconButton(onClick = onCloseSelection) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.action_close_selection) + ) + } + }, + title = { + Text( + text = stringResource(R.string.selection_count, selectedCount), + style = MaterialTheme.typography.titleLarge + ) + }, + actions = { + // Select All button (only if not all selected) + if (selectedCount < totalCount) { + IconButton(onClick = onSelectAll) { + Icon( + imageVector = Icons.Default.SelectAll, + contentDescription = stringResource(R.string.action_select_all) + ) + } + } + // Delete button + IconButton( + onClick = onDeleteSelected, + enabled = selectedCount > 0 + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.action_delete_selected), + tint = if (selectedCount > 0) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + } + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer, + actionIconContentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt new file mode 100644 index 0000000..3e6ed1c --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt @@ -0,0 +1,613 @@ +package dev.dettmer.simplenotes.ui.main + +import android.app.Application +import android.content.Context +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import dev.dettmer.simplenotes.models.Note +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.storage.NotesStorage +import dev.dettmer.simplenotes.sync.SyncStateManager +import dev.dettmer.simplenotes.sync.WebDavSyncService +import dev.dettmer.simplenotes.utils.Constants +import dev.dettmer.simplenotes.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * ViewModel for MainActivity Compose + * v1.5.0: Jetpack Compose MainActivity Redesign + * + * Manages notes list, sync state, and deletion with undo. + */ +class MainViewModel(application: Application) : AndroidViewModel(application) { + + companion object { + private const val TAG = "MainViewModel" + private const val MIN_AUTO_SYNC_INTERVAL_MS = 60_000L // 1 Minute + private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp" + } + + private val storage = NotesStorage(application) + private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) + + // ═══════════════════════════════════════════════════════════════════════ + // Notes State + // ═══════════════════════════════════════════════════════════════════════ + + private val _notes = MutableStateFlow>(emptyList()) + val notes: StateFlow> = _notes.asStateFlow() + + private val _pendingDeletions = MutableStateFlow>(emptySet()) + val pendingDeletions: StateFlow> = _pendingDeletions.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Multi-Select State (v1.5.0) + // ═══════════════════════════════════════════════════════════════════════ + + private val _selectedNotes = MutableStateFlow>(emptySet()) + val selectedNotes: StateFlow> = _selectedNotes.asStateFlow() + + val isSelectionMode: StateFlow = _selectedNotes + .map { it.isNotEmpty() } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) + + // ═══════════════════════════════════════════════════════════════════════ + // Sync State (derived from SyncStateManager) + // ═══════════════════════════════════════════════════════════════════════ + + private val _syncState = MutableStateFlow(SyncStateManager.SyncState.IDLE) + val syncState: StateFlow = _syncState.asStateFlow() + + private val _syncMessage = MutableStateFlow(null) + val syncMessage: StateFlow = _syncMessage.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // UI Events + // ═══════════════════════════════════════════════════════════════════════ + + private val _showToast = MutableSharedFlow() + val showToast: SharedFlow = _showToast.asSharedFlow() + + private val _showDeleteDialog = MutableSharedFlow() + val showDeleteDialog: SharedFlow = _showDeleteDialog.asSharedFlow() + + private val _showSnackbar = MutableSharedFlow() + val showSnackbar: SharedFlow = _showSnackbar.asSharedFlow() + + // Phase 3: Scroll-to-top when new note is created + private val _scrollToTop = MutableStateFlow(false) + val scrollToTop: StateFlow = _scrollToTop.asStateFlow() + + // Track first note ID to detect new notes + private var previousFirstNoteId: String? = null + + // ═══════════════════════════════════════════════════════════════════════ + // Data Classes + // ═══════════════════════════════════════════════════════════════════════ + + data class DeleteDialogData( + val note: Note, + val originalList: List + ) + + data class SnackbarData( + val message: String, + val actionLabel: String, + val onAction: () -> Unit + ) + + // ═══════════════════════════════════════════════════════════════════════ + // Initialization + // ═══════════════════════════════════════════════════════════════════════ + + init { + // v1.5.0 Performance: Load notes asynchronously to avoid blocking UI + viewModelScope.launch(Dispatchers.IO) { + loadNotesAsync() + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Notes Actions + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Load notes asynchronously on IO dispatcher + * This prevents UI blocking during app startup + */ + private suspend fun loadNotesAsync() { + val allNotes = storage.loadAllNotes() + val pendingIds = _pendingDeletions.value + val filteredNotes = allNotes.filter { it.id !in pendingIds } + + withContext(Dispatchers.Main) { + // Phase 3: Detect if a new note was added at the top + val newFirstNoteId = filteredNotes.firstOrNull()?.id + if (newFirstNoteId != null && + previousFirstNoteId != null && + newFirstNoteId != previousFirstNoteId) { + // New note at top → trigger scroll + _scrollToTop.value = true + Logger.d(TAG, "📜 New note detected at top, triggering scroll-to-top") + } + previousFirstNoteId = newFirstNoteId + + _notes.value = filteredNotes + } + } + + /** + * Public loadNotes - delegates to async version + */ + fun loadNotes() { + viewModelScope.launch(Dispatchers.IO) { + loadNotesAsync() + } + } + + /** + * Reset scroll-to-top flag after scroll completed + */ + fun resetScrollToTop() { + _scrollToTop.value = false + } + + /** + * Force scroll to top (e.g., after returning from editor) + */ + fun scrollToTop() { + _scrollToTop.value = true + } + + // ═══════════════════════════════════════════════════════════════════════ + // Multi-Select Actions (v1.5.0) + // ═══════════════════════════════════════════════════════════════════════ + + /** + * Toggle selection of a note + */ + fun toggleNoteSelection(noteId: String) { + _selectedNotes.value = if (noteId in _selectedNotes.value) { + _selectedNotes.value - noteId + } else { + _selectedNotes.value + noteId + } + } + + /** + * Start selection mode with initial note + */ + fun startSelectionMode(noteId: String) { + _selectedNotes.value = setOf(noteId) + } + + /** + * Select all notes + */ + fun selectAllNotes() { + _selectedNotes.value = _notes.value.map { it.id }.toSet() + } + + /** + * Clear selection and exit selection mode + */ + fun clearSelection() { + _selectedNotes.value = emptySet() + } + + /** + * Get count of selected notes + */ + fun getSelectedCount(): Int = _selectedNotes.value.size + + /** + * Delete all selected notes + */ + fun deleteSelectedNotes(deleteFromServer: Boolean) { + val selectedIds = _selectedNotes.value.toList() + val selectedNotes = _notes.value.filter { it.id in selectedIds } + + if (selectedNotes.isEmpty()) return + + // Add to pending deletions + _pendingDeletions.value = _pendingDeletions.value + selectedIds.toSet() + + // Delete from storage + selectedNotes.forEach { note -> + storage.deleteNote(note.id) + } + + // Clear selection + clearSelection() + + // Reload notes + loadNotes() + + // Show snackbar with undo + val count = selectedNotes.size + val message = if (deleteFromServer) { + getString(R.string.snackbar_notes_deleted_server, count) + } else { + getString(R.string.snackbar_notes_deleted_local, count) + } + + viewModelScope.launch { + _showSnackbar.emit(SnackbarData( + message = message, + actionLabel = getString(R.string.snackbar_undo), + onAction = { + undoDeleteMultiple(selectedNotes) + } + )) + + // If delete from server, actually delete after a short delay + // (to allow undo action before server deletion) + if (deleteFromServer) { + kotlinx.coroutines.delay(3500) // Snackbar shows for ~3s + // Only delete if not restored (check if still in pending) + val idsToDelete = selectedIds.filter { it in _pendingDeletions.value } + if (idsToDelete.isNotEmpty()) { + deleteMultipleNotesFromServer(idsToDelete) + } + } else { + // Just finalize local deletion + selectedIds.forEach { noteId -> + finalizeDeletion(noteId) + } + } + } + } + + /** + * Undo deletion of multiple notes + */ + private fun undoDeleteMultiple(notes: List) { + // Remove from pending deletions + _pendingDeletions.value = _pendingDeletions.value - notes.map { it.id }.toSet() + + // Restore to storage + notes.forEach { note -> + storage.saveNote(note) + } + + // Reload notes + loadNotes() + } + + /** + * Called when user long-presses a note to delete + * Shows dialog for delete confirmation (replaces swipe-to-delete for performance) + */ + fun onNoteLongPressDelete(note: Note) { + val alwaysDeleteFromServer = prefs.getBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, false) + + // Store original list for potential restore + val originalList = _notes.value.toList() + + if (alwaysDeleteFromServer) { + // Auto-delete without dialog + deleteNoteConfirmed(note, deleteFromServer = true) + } else { + // Show dialog - don't remove from UI yet (user can cancel) + viewModelScope.launch { + _showDeleteDialog.emit(DeleteDialogData(note, originalList)) + } + } + } + + /** + * Called when user swipes to delete a note (legacy - kept for compatibility) + * Shows dialog if "always delete from server" is not enabled + */ + fun onNoteSwipedToDelete(note: Note) { + onNoteLongPressDelete(note) // Delegate to long-press handler + } + + /** + * Restore note after swipe (user cancelled dialog) + */ + fun restoreNoteAfterSwipe(originalList: List) { + _notes.value = originalList + } + + /** + * Confirm note deletion (from dialog or auto-delete) + */ + fun deleteNoteConfirmed(note: Note, deleteFromServer: Boolean) { + // Add to pending deletions + _pendingDeletions.value = _pendingDeletions.value + note.id + + // Delete from storage + storage.deleteNote(note.id) + + // Reload notes + loadNotes() + + // Show snackbar with undo + val message = if (deleteFromServer) { + getString(R.string.snackbar_note_deleted_server, note.title) + } else { + getString(R.string.snackbar_note_deleted_local, note.title) + } + + viewModelScope.launch { + _showSnackbar.emit(SnackbarData( + message = message, + actionLabel = getString(R.string.snackbar_undo), + onAction = { + undoDelete(note) + } + )) + + // If delete from server, actually delete after snackbar timeout + if (deleteFromServer) { + kotlinx.coroutines.delay(3500) // Snackbar shows for ~3s + // Only delete if not restored (check if still in pending) + if (note.id in _pendingDeletions.value) { + deleteNoteFromServer(note.id) + } + } else { + // Just finalize local deletion + finalizeDeletion(note.id) + } + } + } + + /** + * Undo note deletion + */ + fun undoDelete(note: Note) { + // Remove from pending deletions + _pendingDeletions.value = _pendingDeletions.value - note.id + + // Restore to storage + storage.saveNote(note) + + // Reload notes + loadNotes() + } + + /** + * Actually delete note from server after snackbar dismissed + */ + fun deleteNoteFromServer(noteId: String) { + viewModelScope.launch { + try { + val webdavService = WebDavSyncService(getApplication()) + val success = withContext(Dispatchers.IO) { + webdavService.deleteNoteFromServer(noteId) + } + + if (success) { + _showToast.emit(getString(R.string.snackbar_deleted_from_server)) + } else { + _showToast.emit(getString(R.string.snackbar_server_delete_failed)) + } + } catch (e: Exception) { + _showToast.emit(getString(R.string.snackbar_server_error, e.message ?: "")) + } finally { + // Remove from pending deletions + _pendingDeletions.value = _pendingDeletions.value - noteId + } + } + } + + /** + * Delete multiple notes from server with aggregated toast + * Shows single toast at the end instead of one per note + */ + private fun deleteMultipleNotesFromServer(noteIds: List) { + viewModelScope.launch { + val webdavService = WebDavSyncService(getApplication()) + var successCount = 0 + var failCount = 0 + + noteIds.forEach { noteId -> + try { + val success = withContext(Dispatchers.IO) { + webdavService.deleteNoteFromServer(noteId) + } + if (success) successCount++ else failCount++ + } catch (e: Exception) { + failCount++ + } finally { + _pendingDeletions.value = _pendingDeletions.value - noteId + } + } + + // Show aggregated toast + val message = when { + failCount == 0 -> getString(R.string.snackbar_notes_deleted_from_server, successCount) + successCount == 0 -> getString(R.string.snackbar_server_delete_failed) + else -> getString( + R.string.snackbar_notes_deleted_from_server_partial, + successCount, + successCount + failCount + ) + } + _showToast.emit(message) + } + } + + /** + * Finalize deletion (remove from pending set) + */ + fun finalizeDeletion(noteId: String) { + _pendingDeletions.value = _pendingDeletions.value - noteId + } + + // ═══════════════════════════════════════════════════════════════════════ + // Sync Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun updateSyncState(status: SyncStateManager.SyncStatus) { + _syncState.value = status.state + _syncMessage.value = status.message + } + + /** + * Trigger manual sync (from toolbar button or pull-to-refresh) + */ + fun triggerManualSync(source: String = "manual") { + if (!SyncStateManager.tryStartSync(source)) { + return + } + + viewModelScope.launch { + try { + val syncService = WebDavSyncService(getApplication()) + + // Check for unsynced changes + if (!syncService.hasUnsyncedChanges()) { + Logger.d(TAG, "⏭️ $source Sync: No unsynced changes") + SyncStateManager.markCompleted("Bereits synchronisiert") + loadNotes() + return@launch + } + + // Check server reachability + val isReachable = withContext(Dispatchers.IO) { + syncService.isServerReachable() + } + + if (!isReachable) { + Logger.d(TAG, "⏭️ $source Sync: Server not reachable") + SyncStateManager.markError(getString(R.string.snackbar_server_unreachable)) + return@launch + } + + // Perform sync + val result = withContext(Dispatchers.IO) { + syncService.syncNotes() + } + + if (result.isSuccess) { + val bannerMessage = if (result.syncedCount > 0) { + getString(R.string.toast_sync_success, result.syncedCount) + } else { + getString(R.string.snackbar_nothing_to_sync) + } + SyncStateManager.markCompleted(bannerMessage) + loadNotes() + } else { + SyncStateManager.markError(result.errorMessage) + } + } catch (e: Exception) { + SyncStateManager.markError(e.message) + } + } + } + + /** + * Trigger auto-sync (onResume) + * 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 + */ + fun triggerAutoSync(source: String = "auto") { + // Throttling check + if (!canTriggerAutoSync()) { + return + } + + // Check if server is configured + val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) + if (serverUrl.isNullOrEmpty() || serverUrl == "http://" || serverUrl == "https://") { + return + } + + // v1.5.0: silent=true - kein Banner bei Auto-Sync, aber Fehler werden trotzdem angezeigt + if (!SyncStateManager.tryStartSync("auto-$source", silent = true)) { + Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress") + return + } + + Logger.d(TAG, "🔄 Auto-sync triggered ($source)") + + // Update last sync timestamp + prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply() + + viewModelScope.launch { + try { + val syncService = WebDavSyncService(getApplication()) + + // Check for unsynced changes + if (!syncService.hasUnsyncedChanges()) { + Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping") + SyncStateManager.reset() + return@launch + } + + // Check server reachability + val isReachable = withContext(Dispatchers.IO) { + syncService.isServerReachable() + } + + if (!isReachable) { + Logger.d(TAG, "⏭️ Auto-sync ($source): Server not reachable - skipping silently") + SyncStateManager.reset() + return@launch + } + + // Perform sync + val result = withContext(Dispatchers.IO) { + syncService.syncNotes() + } + + if (result.isSuccess && result.syncedCount > 0) { + Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes") + SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount)) + _showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount)) + loadNotes() + } else if (result.isSuccess) { + Logger.d(TAG, "ℹ️ Auto-sync ($source): No changes") + SyncStateManager.markCompleted(getString(R.string.snackbar_nothing_to_sync)) + } else { + Logger.e(TAG, "❌ Auto-sync failed ($source): ${result.errorMessage}") + SyncStateManager.markError(result.errorMessage) + } + } catch (e: Exception) { + Logger.e(TAG, "💥 Auto-sync exception ($source): ${e.message}") + SyncStateManager.markError(e.message) + } + } + } + + private fun canTriggerAutoSync(): Boolean { + val lastSyncTime = prefs.getLong(PREF_LAST_AUTO_SYNC_TIME, 0) + val now = System.currentTimeMillis() + val timeSinceLastSync = now - lastSyncTime + + if (timeSinceLastSync < MIN_AUTO_SYNC_INTERVAL_MS) { + val remainingSeconds = (MIN_AUTO_SYNC_INTERVAL_MS - timeSinceLastSync) / 1000 + Logger.d(TAG, "⏳ Auto-sync throttled - wait ${remainingSeconds}s") + return false + } + + return true + } + + // ═══════════════════════════════════════════════════════════════════════ + // Helpers + // ═══════════════════════════════════════════════════════════════════════ + + private fun getString(resId: Int): String = getApplication().getString(resId) + + private fun getString(resId: Int, vararg formatArgs: Any): String = + getApplication().getString(resId, *formatArgs) + + fun isServerConfigured(): Boolean { + val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) + return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://" + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt new file mode 100644 index 0000000..6029aae --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt @@ -0,0 +1,93 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R + +/** + * Delete confirmation dialog with server/local options + * v1.5.0: Multi-Select Feature + */ +@Composable +fun DeleteConfirmationDialog( + noteCount: Int = 1, + onDismiss: () -> Unit, + onDeleteLocal: () -> Unit, + onDeleteEverywhere: () -> Unit +) { + val title = if (noteCount == 1) { + stringResource(R.string.delete_note_title) + } else { + stringResource(R.string.delete_notes_title, noteCount) + } + + val message = if (noteCount == 1) { + stringResource(R.string.delete_note_message) + } else { + stringResource(R.string.delete_notes_message, noteCount) + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + Text( + text = message, + style = MaterialTheme.typography.bodyMedium + ) + }, + confirmButton = { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Delete everywhere (server + local) - primary action + TextButton( + onClick = onDeleteEverywhere, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text(stringResource(R.string.delete_everywhere)) + } + + // Delete local only + TextButton( + onClick = onDeleteLocal, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.delete_local_only)) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Cancel button + TextButton( + onClick = onDismiss, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.cancel)) + } + } + }, + dismissButton = null // All buttons in confirmButton column + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt new file mode 100644 index 0000000..f1473b5 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt @@ -0,0 +1,98 @@ +package dev.dettmer.simplenotes.ui.main.components + +import android.graphics.Bitmap +import android.graphics.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import dev.dettmer.simplenotes.R + +/** + * Empty state card shown when no notes exist + * v1.5.0: Jetpack Compose MainActivity Redesign + */ +@Composable +fun EmptyState( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Card( + modifier = Modifier.padding(horizontal = 32.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Column( + modifier = Modifier.padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // App icon foreground (transparent background) + val context = LocalContext.current + val appIcon = remember { + val drawable = ContextCompat.getDrawable(context, R.mipmap.ic_launcher_foreground) + drawable?.let { + val size = 256 + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + it.setBounds(0, 0, size, size) + it.draw(canvas) + bitmap.asImageBitmap() + } + } + + appIcon?.let { + Image( + bitmap = it, + contentDescription = null, + modifier = Modifier.size(96.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = stringResource(R.string.empty_state_title), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Message + Text( + text = stringResource(R.string.empty_state_message), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt new file mode 100644 index 0000000..d352b59 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt @@ -0,0 +1,240 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.List +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.outlined.CloudDone +import androidx.compose.material.icons.outlined.CloudOff +import androidx.compose.material.icons.outlined.CloudSync +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.Note +import dev.dettmer.simplenotes.models.NoteType +import dev.dettmer.simplenotes.models.SyncStatus +import dev.dettmer.simplenotes.utils.toReadableTime + +/** + * Note card - v1.5.0 with Multi-Select Support + * + * ULTRA SIMPLE + SELECTION: + * - NO remember() anywhere + * - Direct MaterialTheme access + * - Selection indicator via border + checkbox overlay + * - Long-press starts selection mode + * - Tap in selection mode toggles selection + */ +@Composable +fun NoteCard( + note: Note, + showSyncStatus: Boolean, + isSelected: Boolean = false, + isSelectionMode: Boolean = false, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + val context = LocalContext.current + + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .then( + if (isSelected) { + Modifier.border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(16.dp) + ) + } else Modifier + ) + .pointerInput(note.id, isSelectionMode) { + detectTapGestures( + onTap = { onClick() }, + onLongPress = { onLongClick() } + ) + }, + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), + colors = CardDefaults.cardColors( + containerColor = if (isSelected) { + MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + } + ) + ) { + Box { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // Header row + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + // Type icon + Box( + modifier = Modifier + .size(32.dp) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = if (note.noteType == NoteType.TEXT) + Icons.Outlined.Description + else + Icons.AutoMirrored.Outlined.List, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(16.dp) + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + // Title + Text( + text = note.title.ifEmpty { stringResource(R.string.untitled) }, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Preview + Text( + text = when (note.noteType) { + NoteType.TEXT -> note.content.take(100) + NoteType.CHECKLIST -> { + val items = note.checklistItems ?: emptyList() + stringResource(R.string.checklist_progress, items.count { it.isChecked }, items.size) + } + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 3, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Footer + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = note.updatedAt.toReadableTime(context), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.weight(1f) + ) + + if (showSyncStatus) { + Icon( + imageVector = when (note.syncStatus) { + SyncStatus.SYNCED -> Icons.Outlined.CloudDone + SyncStatus.PENDING -> Icons.Outlined.CloudSync + SyncStatus.CONFLICT -> Icons.Default.Warning + SyncStatus.LOCAL_ONLY -> Icons.Outlined.CloudOff + }, + contentDescription = null, + tint = when (note.syncStatus) { + SyncStatus.SYNCED -> MaterialTheme.colorScheme.primary + SyncStatus.CONFLICT -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.outline + }, + modifier = Modifier.size(16.dp) + ) + } + } + } + + // Selection indicator checkbox (top-right) + androidx.compose.animation.AnimatedVisibility( + visible = isSelectionMode, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background( + if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.surfaceContainerHighest + } + ) + .border( + width = 2.dp, + color = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.outline + }, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = stringResource(R.string.selection_count, 1), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt new file mode 100644 index 0000000..f49c57c --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt @@ -0,0 +1,85 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.List +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.models.NoteType + +/** + * FAB with dropdown menu for note type selection + * v1.5.0: PERFORMANCE FIX - No Box wrapper for proper elevation + * + * Uses consistent icons with NoteCard: + * - TEXT: Description (document icon) + * - CHECKLIST: List (bullet list icon) + */ +@Composable +fun NoteTypeFAB( + modifier: Modifier = Modifier, + onCreateNote: (NoteType) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + // FAB directly without Box wrapper - elevation works correctly + FloatingActionButton( + onClick = { expanded = true }, + modifier = modifier, + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = stringResource(R.string.fab_new_note) + ) + + // Dropdown inside FAB - renders as popup overlay + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + DropdownMenuItem( + text = { Text(stringResource(R.string.fab_text_note)) }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.Description, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + }, + onClick = { + expanded = false + onCreateNote(NoteType.TEXT) + } + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.fab_checklist)) }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Outlined.List, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface + ) + }, + onClick = { + expanded = false + onCreateNote(NoteType.CHECKLIST) + } + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NotesList.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NotesList.kt new file mode 100644 index 0000000..46c4926 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NotesList.kt @@ -0,0 +1,65 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.models.Note + +/** + * Notes list - v1.5.0 with Multi-Select Support + * + * ULTRA SIMPLE + SELECTION: + * - NO remember() anywhere + * - NO caching tricks + * - Selection state passed through as parameters + * - Tap behavior changes based on selection mode + */ +@Composable +fun NotesList( + notes: List, + showSyncStatus: Boolean, + selectedNotes: Set = emptySet(), + isSelectionMode: Boolean = false, + modifier: Modifier = Modifier, + listState: LazyListState = rememberLazyListState(), + onNoteClick: (Note) -> Unit, + onNoteLongPress: (Note) -> Unit, + onNoteSelectionToggle: (Note) -> Unit = {} +) { + LazyColumn( + state = listState, + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(top = 8.dp, bottom = 88.dp) + ) { + items( + items = notes, + key = { it.id }, + contentType = { "NoteCard" } + ) { note -> + val isSelected = note.id in selectedNotes + + NoteCard( + note = note, + showSyncStatus = showSyncStatus, + isSelected = isSelected, + isSelectionMode = isSelectionMode, + onClick = { + if (isSelectionMode) { + // In selection mode, tap toggles selection + onNoteSelectionToggle(note) + } else { + // Normal mode, open note + onNoteClick(note) + } + }, + onLongClick = { onNoteLongPress(note) } + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt new file mode 100644 index 0000000..ac9ba26 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt @@ -0,0 +1,77 @@ +package dev.dettmer.simplenotes.ui.main.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.sync.SyncStateManager + +/** + * Sync status banner shown below the toolbar during sync + * v1.5.0: Jetpack Compose MainActivity Redesign + * v1.5.0: SYNCING_SILENT ignorieren - Banner nur bei manuellen Syncs oder Fehlern anzeigen + */ +@Composable +fun SyncStatusBanner( + syncState: SyncStateManager.SyncState, + message: String?, + modifier: Modifier = Modifier +) { + // v1.5.0: Banner nicht anzeigen bei IDLE oder SYNCING_SILENT (Auto-Sync im Hintergrund) + // Fehler werden trotzdem angezeigt (ERROR state nach Silent-Sync wechselt zu ERROR, nicht SYNCING_SILENT) + val isVisible = syncState != SyncStateManager.SyncState.IDLE + && syncState != SyncStateManager.SyncState.SYNCING_SILENT + + AnimatedVisibility( + visible = isVisible, + enter = expandVertically(), + exit = shrinkVertically(), + modifier = modifier + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (syncState == SyncStateManager.SyncState.SYNCING) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 3.dp, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = when (syncState) { + SyncStateManager.SyncState.SYNCING -> stringResource(R.string.sync_status_syncing) + SyncStateManager.SyncState.SYNCING_SILENT -> "" // v1.5.0: Wird nicht angezeigt (isVisible = false) + SyncStateManager.SyncState.COMPLETED -> message ?: stringResource(R.string.sync_status_completed) + SyncStateManager.SyncState.ERROR -> message ?: stringResource(R.string.sync_status_error) + SyncStateManager.SyncState.IDLE -> "" + }, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.weight(1f) + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt new file mode 100644 index 0000000..ec9ddbd --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt @@ -0,0 +1,186 @@ +package dev.dettmer.simplenotes.ui.settings + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.compose.rememberNavController +import com.google.android.material.color.DynamicColors +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.SimpleNotesApplication +import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme +import dev.dettmer.simplenotes.utils.Logger +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.launch + +/** + * Settings Activity with Jetpack Compose UI + * v1.5.0: Complete Settings Redesign with grouped screens + * + * Replaces the old 1147-line SettingsActivity.kt with a modern + * Compose-based implementation featuring: + * - 6 logical settings groups as separate screens + * - Material 3 Design with Dynamic Colors (Material You) + * - Navigation with back button in each screen + * - Clean separation of concerns with SettingsViewModel + */ +class ComposeSettingsActivity : AppCompatActivity() { + + companion object { + private const val TAG = "ComposeSettingsActivity" + } + + private val viewModel: SettingsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Apply Dynamic Colors for Material You (Android 12+) + DynamicColors.applyToActivityIfAvailable(this) + + // Enable edge-to-edge display + enableEdgeToEdge() + + // Handle back button with slide animation + onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + setResult(RESULT_OK) + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } + }) + + // Collect events from ViewModel (for Activity-level actions) + collectViewModelEvents() + + setContent { + SimpleNotesTheme { + val navController = rememberNavController() + val context = LocalContext.current + + // Toast handling from ViewModel + LaunchedEffect(Unit) { + viewModel.showToast.collect { message -> + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } + } + + SettingsNavHost( + navController = navController, + viewModel = viewModel, + onFinish = { + setResult(RESULT_OK) + finish() + @Suppress("DEPRECATION") + overridePendingTransition( + dev.dettmer.simplenotes.R.anim.slide_in_left, + dev.dettmer.simplenotes.R.anim.slide_out_right + ) + } + ) + } + } + } + + /** + * Collect events from ViewModel for Activity-level actions + * v1.5.0: Ported from old SettingsActivity + */ + private fun collectViewModelEvents() { + lifecycleScope.launch { + viewModel.events.collect { event -> + when (event) { + is SettingsViewModel.SettingsEvent.RequestBatteryOptimization -> { + checkBatteryOptimization() + } + is SettingsViewModel.SettingsEvent.RestartNetworkMonitor -> { + restartNetworkMonitor() + } + } + } + } + } + + /** + * Check if battery optimization is disabled for this app + * v1.5.0: Ported from old SettingsActivity + */ + private fun checkBatteryOptimization() { + val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager + + if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { + showBatteryOptimizationDialog() + } + } + + /** + * Show dialog asking user to disable battery optimization + * v1.5.0: Ported from old SettingsActivity + */ + private fun showBatteryOptimizationDialog() { + AlertDialog.Builder(this) + .setTitle(getString(R.string.battery_optimization_dialog_title)) + .setMessage(getString(R.string.battery_optimization_dialog_full_message)) + .setPositiveButton(getString(R.string.battery_optimization_open_settings)) { _, _ -> + openBatteryOptimizationSettings() + } + .setNegativeButton(getString(R.string.battery_optimization_later)) { dialog, _ -> + dialog.dismiss() + } + .setCancelable(false) + .show() + } + + /** + * Open system battery optimization settings + * v1.5.0: Ported from old SettingsActivity + */ + private fun openBatteryOptimizationSettings() { + try { + val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) + intent.data = Uri.parse("package:$packageName") + startActivity(intent) + } catch (e: Exception) { + Logger.w(TAG, "Failed to open battery optimization settings: ${e.message}") + // Fallback: Open general battery settings + try { + val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + startActivity(intent) + } catch (e2: Exception) { + Logger.w(TAG, "Failed to open fallback battery settings: ${e2.message}") + Toast.makeText(this, "Bitte Akku-Optimierung manuell deaktivieren", Toast.LENGTH_LONG).show() + } + } + } + + /** + * Restart the network monitor after sync settings change + * v1.5.0: Ported from old SettingsActivity + */ + private fun restartNetworkMonitor() { + try { + val app = application as SimpleNotesApplication + Logger.d(TAG, "🔄 Restarting NetworkMonitor with new settings") + app.networkMonitor.stopMonitoring() + app.networkMonitor.startMonitoring() + Logger.d(TAG, "✅ NetworkMonitor restarted successfully") + } catch (e: Exception) { + Logger.e(TAG, "❌ Failed to restart NetworkMonitor: ${e.message}") + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt new file mode 100644 index 0000000..18c1c0e --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt @@ -0,0 +1,93 @@ +package dev.dettmer.simplenotes.ui.settings + +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import dev.dettmer.simplenotes.ui.settings.screens.AboutScreen +import dev.dettmer.simplenotes.ui.settings.screens.BackupSettingsScreen +import dev.dettmer.simplenotes.ui.settings.screens.DebugSettingsScreen +import dev.dettmer.simplenotes.ui.settings.screens.LanguageSettingsScreen +import dev.dettmer.simplenotes.ui.settings.screens.MarkdownSettingsScreen +import dev.dettmer.simplenotes.ui.settings.screens.ServerSettingsScreen +import dev.dettmer.simplenotes.ui.settings.screens.SettingsMainScreen +import dev.dettmer.simplenotes.ui.settings.screens.SyncSettingsScreen + +/** + * Settings navigation host with all routes + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsNavHost( + navController: NavHostController, + viewModel: SettingsViewModel, + onFinish: () -> Unit +) { + NavHost( + navController = navController, + startDestination = SettingsRoute.Main.route + ) { + // Main Settings Overview + composable(SettingsRoute.Main.route) { + SettingsMainScreen( + viewModel = viewModel, + onNavigate = { route -> navController.navigate(route.route) }, + onBack = onFinish + ) + } + + // Language Settings + composable(SettingsRoute.Language.route) { + LanguageSettingsScreen( + onBack = { navController.popBackStack() } + ) + } + + // Server Settings + composable(SettingsRoute.Server.route) { + ServerSettingsScreen( + viewModel = viewModel, + onBack = { navController.popBackStack() } + ) + } + + // Sync Settings + composable(SettingsRoute.Sync.route) { + SyncSettingsScreen( + viewModel = viewModel, + onBack = { navController.popBackStack() } + ) + } + + // Markdown Settings + composable(SettingsRoute.Markdown.route) { + MarkdownSettingsScreen( + viewModel = viewModel, + onBack = { navController.popBackStack() } + ) + } + + // Backup Settings + composable(SettingsRoute.Backup.route) { + BackupSettingsScreen( + viewModel = viewModel, + onBack = { navController.popBackStack() } + ) + } + + // About Screen + composable(SettingsRoute.About.route) { + AboutScreen( + onBack = { navController.popBackStack() } + ) + } + + // Debug Settings + composable(SettingsRoute.Debug.route) { + DebugSettingsScreen( + viewModel = viewModel, + onBack = { navController.popBackStack() } + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt new file mode 100644 index 0000000..ffd3a23 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt @@ -0,0 +1,16 @@ +package dev.dettmer.simplenotes.ui.settings + +/** + * Navigation routes for Settings screens + * v1.5.0: Jetpack Compose Settings Redesign + */ +sealed class SettingsRoute(val route: String) { + data object Main : SettingsRoute("settings_main") + data object Language : SettingsRoute("settings_language") + data object Server : SettingsRoute("settings_server") + data object Sync : SettingsRoute("settings_sync") + data object Markdown : SettingsRoute("settings_markdown") + data object Backup : SettingsRoute("settings_backup") + data object About : SettingsRoute("settings_about") + data object Debug : SettingsRoute("settings_debug") +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..4f10f5c --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt @@ -0,0 +1,519 @@ +package dev.dettmer.simplenotes.ui.settings + +import android.app.Application +import android.content.Context +import android.net.Uri +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.backup.BackupManager +import dev.dettmer.simplenotes.backup.RestoreMode +import dev.dettmer.simplenotes.sync.WebDavSyncService +import dev.dettmer.simplenotes.utils.Constants +import dev.dettmer.simplenotes.utils.Logger +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.HttpURLConnection +import java.net.URL + +/** + * ViewModel for Settings screens + * v1.5.0: Jetpack Compose Settings Redesign + * + * Manages all settings state and actions across the Settings navigation graph. + */ +class SettingsViewModel(application: Application) : AndroidViewModel(application) { + + companion object { + private const val TAG = "SettingsViewModel" + private const val CONNECTION_TIMEOUT_MS = 3000 + } + + private val prefs = application.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) + val backupManager = BackupManager(application) + + // ═══════════════════════════════════════════════════════════════════════ + // Server Settings State + // ═══════════════════════════════════════════════════════════════════════ + + // v1.5.0 Fix: Initialize URL with protocol prefix if empty + private val storedUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: "" + private val initialUrl = if (storedUrl.isEmpty()) "http://" else storedUrl + + private val _serverUrl = MutableStateFlow(initialUrl) + val serverUrl: StateFlow = _serverUrl.asStateFlow() + + private val _username = MutableStateFlow(prefs.getString(Constants.KEY_USERNAME, "") ?: "") + val username: StateFlow = _username.asStateFlow() + + private val _password = MutableStateFlow(prefs.getString(Constants.KEY_PASSWORD, "") ?: "") + val password: StateFlow = _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 = _isHttps.asStateFlow() + + private val _serverStatus = MutableStateFlow(ServerStatus.Unknown) + val serverStatus: StateFlow = _serverStatus.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Events (for Activity-level actions like dialogs, intents) + // ═══════════════════════════════════════════════════════════════════════ + + private val _events = MutableSharedFlow() + val events: SharedFlow = _events.asSharedFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Markdown Export Progress State + // ═══════════════════════════════════════════════════════════════════════ + + private val _markdownExportProgress = MutableStateFlow(null) + val markdownExportProgress: StateFlow = _markdownExportProgress.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Sync Settings State + // ═══════════════════════════════════════════════════════════════════════ + + private val _autoSyncEnabled = MutableStateFlow(prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)) + val autoSyncEnabled: StateFlow = _autoSyncEnabled.asStateFlow() + + private val _syncInterval = MutableStateFlow( + prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES) + ) + val syncInterval: StateFlow = _syncInterval.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Markdown Settings State + // ═══════════════════════════════════════════════════════════════════════ + + private val _markdownAutoSync = MutableStateFlow( + prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false) && + prefs.getBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false) + ) + val markdownAutoSync: StateFlow = _markdownAutoSync.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Debug Settings State + // ═══════════════════════════════════════════════════════════════════════ + + private val _fileLoggingEnabled = MutableStateFlow( + prefs.getBoolean(Constants.KEY_FILE_LOGGING_ENABLED, false) + ) + val fileLoggingEnabled: StateFlow = _fileLoggingEnabled.asStateFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // UI State + // ═══════════════════════════════════════════════════════════════════════ + + private val _isSyncing = MutableStateFlow(false) + val isSyncing: StateFlow = _isSyncing.asStateFlow() + + private val _isBackupInProgress = MutableStateFlow(false) + val isBackupInProgress: StateFlow = _isBackupInProgress.asStateFlow() + + private val _showToast = MutableSharedFlow() + val showToast: SharedFlow = _showToast.asSharedFlow() + + // ═══════════════════════════════════════════════════════════════════════ + // Server Settings Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun updateServerUrl(url: String) { + _serverUrl.value = url + saveServerSettings() + } + + fun updateProtocol(useHttps: Boolean) { + _isHttps.value = useHttps + val currentUrl = _serverUrl.value + + // 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() + } + + fun updateUsername(value: String) { + _username.value = value + saveServerSettings() + } + + fun updatePassword(value: String) { + _password.value = value + saveServerSettings() + } + + private fun saveServerSettings() { + prefs.edit().apply { + putString(Constants.KEY_SERVER_URL, _serverUrl.value) + putString(Constants.KEY_USERNAME, _username.value) + putString(Constants.KEY_PASSWORD, _password.value) + apply() + } + } + + fun testConnection() { + viewModelScope.launch { + _serverStatus.value = ServerStatus.Checking + try { + val syncService = WebDavSyncService(getApplication()) + val result = syncService.testConnection() + _serverStatus.value = if (result.isSuccess) { + ServerStatus.Reachable + } else { + ServerStatus.Unreachable(result.errorMessage) + } + val message = if (result.isSuccess) { + getString(R.string.toast_connection_success) + } else { + getString(R.string.toast_connection_failed, result.errorMessage ?: "") + } + emitToast(message) + } catch (e: Exception) { + _serverStatus.value = ServerStatus.Unreachable(e.message) + emitToast(getString(R.string.toast_error, e.message ?: "")) + } + } + } + + fun checkServerStatus() { + val serverUrl = _serverUrl.value + // v1.5.0 Fix: URL mit nur Prefix gilt als "nicht konfiguriert" + if (serverUrl.isEmpty() || serverUrl == "http://" || serverUrl == "https://") { + _serverStatus.value = ServerStatus.NotConfigured + return + } + + viewModelScope.launch { + _serverStatus.value = ServerStatus.Checking + val isReachable = withContext(Dispatchers.IO) { + try { + val url = URL(serverUrl) + val connection = url.openConnection() as HttpURLConnection + connection.connectTimeout = CONNECTION_TIMEOUT_MS + connection.readTimeout = CONNECTION_TIMEOUT_MS + val code = connection.responseCode + connection.disconnect() + code in 200..299 || code == 401 + } catch (e: Exception) { + Log.e(TAG, "Server check failed: ${e.message}") + false + } + } + _serverStatus.value = if (isReachable) ServerStatus.Reachable else ServerStatus.Unreachable(null) + } + } + + fun syncNow() { + if (_isSyncing.value) return + viewModelScope.launch { + _isSyncing.value = true + try { + emitToast(getString(R.string.toast_syncing)) + val syncService = WebDavSyncService(getApplication()) + + if (!syncService.hasUnsyncedChanges()) { + emitToast(getString(R.string.toast_already_synced)) + return@launch + } + + val result = syncService.syncNotes() + if (result.isSuccess) { + emitToast(getString(R.string.toast_sync_success, result.syncedCount)) + } else { + emitToast(getString(R.string.toast_sync_failed, result.errorMessage ?: "")) + } + } catch (e: Exception) { + emitToast(getString(R.string.toast_error, e.message ?: "")) + } finally { + _isSyncing.value = false + } + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Sync Settings Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun setAutoSync(enabled: Boolean) { + _autoSyncEnabled.value = enabled + prefs.edit().putBoolean(Constants.KEY_AUTO_SYNC, enabled).apply() + + viewModelScope.launch { + if (enabled) { + // v1.5.0 Fix: Trigger battery optimization check and network monitor restart + _events.emit(SettingsEvent.RequestBatteryOptimization) + _events.emit(SettingsEvent.RestartNetworkMonitor) + emitToast(getString(R.string.toast_auto_sync_enabled)) + } else { + _events.emit(SettingsEvent.RestartNetworkMonitor) + emitToast(getString(R.string.toast_auto_sync_disabled)) + } + } + } + + fun setSyncInterval(minutes: Long) { + _syncInterval.value = minutes + prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, minutes).apply() + viewModelScope.launch { + val text = when (minutes) { + 15L -> getString(R.string.toast_sync_interval_15min) + 60L -> getString(R.string.toast_sync_interval_60min) + else -> getString(R.string.toast_sync_interval_30min) + } + emitToast(getString(R.string.toast_sync_interval, text)) + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Markdown Settings Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun setMarkdownAutoSync(enabled: Boolean) { + if (enabled) { + // v1.5.0 Fix: Perform initial export when enabling (like old SettingsActivity) + viewModelScope.launch { + try { + // Check server configuration first + val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: "" + val username = prefs.getString(Constants.KEY_USERNAME, "") ?: "" + val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: "" + + if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) { + emitToast(getString(R.string.toast_configure_server_first)) + // Don't enable - revert state + return@launch + } + + // Check if there are notes to export + val noteStorage = dev.dettmer.simplenotes.storage.NotesStorage(getApplication()) + val noteCount = noteStorage.loadAllNotes().size + + if (noteCount > 0) { + // Show progress and perform initial export + _markdownExportProgress.value = MarkdownExportProgress(0, noteCount) + + val syncService = WebDavSyncService(getApplication()) + val exportedCount = withContext(Dispatchers.IO) { + syncService.exportAllNotesToMarkdown( + serverUrl = serverUrl, + username = username, + password = password, + onProgress = { current, total -> + _markdownExportProgress.value = MarkdownExportProgress(current, total) + } + ) + } + + // Export successful - save settings + _markdownAutoSync.value = true + prefs.edit() + .putBoolean(Constants.KEY_MARKDOWN_EXPORT, true) + .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true) + .apply() + + _markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true) + emitToast(getString(R.string.toast_markdown_exported, exportedCount)) + + // Clear progress after short delay + kotlinx.coroutines.delay(500) + _markdownExportProgress.value = null + + } else { + // No notes - just enable the feature + _markdownAutoSync.value = true + prefs.edit() + .putBoolean(Constants.KEY_MARKDOWN_EXPORT, true) + .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true) + .apply() + emitToast(getString(R.string.toast_markdown_enabled)) + } + + } catch (e: Exception) { + _markdownExportProgress.value = null + emitToast(getString(R.string.toast_export_failed, e.message ?: "")) + // Don't enable on error + } + } + } else { + // Disable - simple + _markdownAutoSync.value = false + prefs.edit() + .putBoolean(Constants.KEY_MARKDOWN_EXPORT, false) + .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false) + .apply() + viewModelScope.launch { + emitToast(getString(R.string.toast_markdown_disabled)) + } + } + } + + fun performManualMarkdownSync() { + viewModelScope.launch { + try { + emitToast(getString(R.string.toast_markdown_syncing)) + val syncService = WebDavSyncService(getApplication()) + val result = syncService.manualMarkdownSync() + emitToast(getString(R.string.toast_markdown_result, result.exportedCount, result.importedCount)) + } catch (e: Exception) { + emitToast(getString(R.string.toast_error, e.message ?: "")) + } + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Backup Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun createBackup(uri: Uri) { + viewModelScope.launch { + _isBackupInProgress.value = true + try { + val result = backupManager.createBackup(uri) + val message = if (result.success) { + getString(R.string.toast_backup_success, result.message ?: "") + } else { + getString(R.string.toast_backup_failed, result.error ?: "") + } + emitToast(message) + } catch (e: Exception) { + emitToast(getString(R.string.toast_backup_failed, e.message ?: "")) + } finally { + _isBackupInProgress.value = false + } + } + } + + fun restoreFromFile(uri: Uri, mode: RestoreMode) { + viewModelScope.launch { + _isBackupInProgress.value = true + try { + val result = backupManager.restoreBackup(uri, mode) + val message = if (result.success) { + getString(R.string.toast_restore_success, result.importedNotes) + } else { + getString(R.string.toast_restore_failed, result.error ?: "") + } + emitToast(message) + } catch (e: Exception) { + emitToast(getString(R.string.toast_restore_failed, e.message ?: "")) + } finally { + _isBackupInProgress.value = false + } + } + } + + fun restoreFromServer(mode: RestoreMode) { + viewModelScope.launch { + _isBackupInProgress.value = true + try { + emitToast(getString(R.string.restore_progress)) + val syncService = WebDavSyncService(getApplication()) + val result = withContext(Dispatchers.IO) { + syncService.restoreFromServer(mode) + } + val message = if (result.isSuccess) { + getString(R.string.toast_restore_success, result.restoredCount) + } else { + getString(R.string.toast_restore_failed, result.errorMessage ?: "") + } + emitToast(message) + } catch (e: Exception) { + emitToast(getString(R.string.toast_error, e.message ?: "")) + } finally { + _isBackupInProgress.value = false + } + } + } + + // ═══════════════════════════════════════════════════════════════════════ + // Debug Settings Actions + // ═══════════════════════════════════════════════════════════════════════ + + fun setFileLogging(enabled: Boolean) { + _fileLoggingEnabled.value = enabled + prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, enabled).apply() + Logger.setFileLoggingEnabled(enabled) + viewModelScope.launch { + emitToast(if (enabled) getString(R.string.toast_file_logging_enabled) else getString(R.string.toast_file_logging_disabled)) + } + } + + fun clearLogs() { + viewModelScope.launch { + try { + val cleared = Logger.clearLogFile(getApplication()) + emitToast(if (cleared) getString(R.string.toast_logs_deleted) else getString(R.string.toast_logs_deleted)) + } catch (e: Exception) { + emitToast(getString(R.string.toast_error, e.message ?: "")) + } + } + } + + fun getLogFile() = Logger.getLogFile(getApplication()) + + // ═══════════════════════════════════════════════════════════════════════ + // Helper + // ═══════════════════════════════════════════════════════════════════════ + + private fun getString(resId: Int): String = getApplication().getString(resId) + + private fun getString(resId: Int, vararg formatArgs: Any): String = + getApplication().getString(resId, *formatArgs) + + private suspend fun emitToast(message: String) { + _showToast.emit(message) + } + + /** + * Server status states + */ + sealed class ServerStatus { + data object Unknown : ServerStatus() + data object NotConfigured : ServerStatus() + data object Checking : ServerStatus() + data object Reachable : ServerStatus() + data class Unreachable(val error: String?) : ServerStatus() + } + + /** + * Events for Activity-level actions (dialogs, intents, etc.) + * v1.5.0: Ported from old SettingsActivity + */ + sealed class SettingsEvent { + data object RequestBatteryOptimization : SettingsEvent() + data object RestartNetworkMonitor : SettingsEvent() + } + + /** + * Progress state for Markdown export + * v1.5.0: For initial export progress dialog + */ + data class MarkdownExportProgress( + val current: Int, + val total: Int, + val isComplete: Boolean = false + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt new file mode 100644 index 0000000..7146bb9 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsCard.kt @@ -0,0 +1,113 @@ +package dev.dettmer.simplenotes.ui.settings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** + * Clickable Settings group card with icon, title, subtitle and optional status + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsCard( + icon: ImageVector, + title: String, + modifier: Modifier = Modifier, + subtitle: String? = null, + statusText: String? = null, + statusColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + onClick: () -> Unit +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp) + .clickable(onClick = onClick), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon with circle background + Box( + modifier = Modifier + .size(40.dp) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(20.dp) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // Content + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface + ) + if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (statusText != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = statusText, + style = MaterialTheme.typography.bodySmall, + color = statusColor + ) + } + } + + // Arrow + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt new file mode 100644 index 0000000..4322976 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsComponents.kt @@ -0,0 +1,151 @@ +package dev.dettmer.simplenotes.ui.settings.components + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Primary filled button for settings actions + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false +) { + Button( + onClick = onClick, + enabled = enabled && !isLoading, + modifier = modifier.fillMaxWidth() + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Text(text) + } + } +} + +/** + * Outlined secondary button for settings actions + */ +@Composable +fun SettingsOutlinedButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + isLoading: Boolean = false +) { + OutlinedButton( + onClick = onClick, + enabled = enabled && !isLoading, + modifier = modifier.fillMaxWidth() + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.height(20.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.primary + ) + } else { + Text(text) + } + } +} + +/** + * Danger/destructive button for settings actions + */ +@Composable +fun SettingsDangerButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + OutlinedButton( + onClick = onClick, + enabled = enabled, + modifier = modifier.fillMaxWidth(), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Text(text) + } +} + +/** + * Info card with description text + */ +@Composable +fun SettingsInfoCard( + text: String, + modifier: Modifier = Modifier +) { + androidx.compose.material3.Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = androidx.compose.material3.CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Text( + text = text, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp), + lineHeight = MaterialTheme.typography.bodySmall.lineHeight * 1.3f + ) + } +} + +/** + * Section header text + */ +@Composable +fun SettingsSectionHeader( + text: String, + modifier: Modifier = Modifier +) { + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) +} + +/** + * Divider between settings groups + */ +@Composable +fun SettingsDivider( + modifier: Modifier = Modifier +) { + Spacer(modifier = modifier.height(8.dp)) + androidx.compose.material3.HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + color = MaterialTheme.colorScheme.outlineVariant + ) + Spacer(modifier = Modifier.height(8.dp)) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt new file mode 100644 index 0000000..6fd267c --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsRadioGroup.kt @@ -0,0 +1,94 @@ +package dev.dettmer.simplenotes.ui.settings.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp + +/** + * Data class for radio option + */ +data class RadioOption( + val value: T, + val title: String, + val subtitle: String? = null +) + +/** + * Settings radio group for selecting one option + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsRadioGroup( + options: List>, + selectedValue: T, + onValueSelected: (T) -> Unit, + modifier: Modifier = Modifier, + title: String? = null +) { + Column( + modifier = modifier + .fillMaxWidth() + .selectableGroup() + ) { + if (title != null) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + options.forEach { option -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = option.value == selectedValue, + onClick = { onValueSelected(option.value) }, + role = Role.RadioButton + ) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = option.value == selectedValue, + onClick = null // handled by selectable + ) + + Column( + modifier = Modifier + .padding(start = 16.dp) + .weight(1f) + ) { + Text( + text = option.title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + if (option.subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = option.subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt new file mode 100644 index 0000000..457b423 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt @@ -0,0 +1,59 @@ +package dev.dettmer.simplenotes.ui.settings.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import dev.dettmer.simplenotes.R + +/** + * Reusable Scaffold with back-navigation TopAppBar + * v1.5.0: Jetpack Compose Settings Redesign + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScaffold( + title: String, + onBack: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable (PaddingValues) -> Unit +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.content_description_back) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) + }, + containerColor = MaterialTheme.colorScheme.surface + ) { paddingValues -> + content(paddingValues) + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt new file mode 100644 index 0000000..63cf998 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsSwitch.kt @@ -0,0 +1,83 @@ +package dev.dettmer.simplenotes.ui.settings.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** + * Settings switch item with title, optional subtitle and icon + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsSwitch( + title: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + subtitle: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (enabled) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + }, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = if (enabled) { + MaterialTheme.colorScheme.onSurface + } else { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + } + ) + if (subtitle != null) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = if (enabled) { + MaterialTheme.colorScheme.onSurfaceVariant + } else { + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f) + } + ) + } + } + + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled + ) + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt new file mode 100644 index 0000000..cdd7982 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt @@ -0,0 +1,242 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Code +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Policy +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import dev.dettmer.simplenotes.BuildConfig +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold +import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader + +/** + * About app information screen + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun AboutScreen( + onBack: () -> Unit +) { + val context = LocalContext.current + + val githubRepoUrl = "https://github.com/inventory69/simple-notes-sync" + val githubProfileUrl = "https://github.com/inventory69" + val licenseUrl = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE" + + SettingsScaffold( + title = stringResource(R.string.about_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + // App Info Card + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // v1.5.0: App icon foreground loaded directly for better quality + val context = LocalContext.current + val appIcon = remember { + val drawable = ContextCompat.getDrawable(context, R.mipmap.ic_launcher_foreground) + drawable?.let { + // Use fixed size for consistent quality (256x256) + val size = 256 + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + it.setBounds(0, 0, size, size) + it.draw(canvas) + bitmap.asImageBitmap() + } + } + + appIcon?.let { + Image( + bitmap = it, + contentDescription = stringResource(R.string.about_app_name), + modifier = Modifier.size(96.dp) + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.about_app_name), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + SettingsSectionHeader(text = stringResource(R.string.about_links_section)) + + // GitHub Repository + AboutLinkItem( + icon = Icons.Default.Code, + title = stringResource(R.string.about_github_title), + subtitle = stringResource(R.string.about_github_subtitle), + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubRepoUrl)) + context.startActivity(intent) + } + ) + + // Developer + AboutLinkItem( + icon = Icons.Default.Person, + title = stringResource(R.string.about_developer_title), + subtitle = stringResource(R.string.about_developer_subtitle), + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubProfileUrl)) + context.startActivity(intent) + } + ) + + // License + AboutLinkItem( + icon = Icons.Default.Policy, + title = stringResource(R.string.about_license_title), + subtitle = stringResource(R.string.about_license_subtitle), + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(licenseUrl)) + context.startActivity(intent) + } + ) + + SettingsDivider() + + // Data Privacy Info + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = stringResource(R.string.about_privacy_title), + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.about_privacy_text), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +/** + * Clickable link item for About section + */ +@Composable +private fun AboutLinkItem( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt new file mode 100644 index 0000000..a93bc19 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt @@ -0,0 +1,257 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.backup.RestoreMode +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.RadioOption +import dev.dettmer.simplenotes.ui.settings.components.SettingsButton +import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider +import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsOutlinedButton +import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold +import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Backup and restore settings screen + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun BackupSettingsScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit +) { + val isBackupInProgress by viewModel.isBackupInProgress.collectAsState() + + // Restore dialog state + var showRestoreDialog by remember { mutableStateOf(false) } + var restoreSource by remember { mutableStateOf(RestoreSource.LocalFile) } + var pendingRestoreUri by remember { mutableStateOf(null) } + var selectedRestoreMode by remember { mutableStateOf(RestoreMode.MERGE) } + + // File picker launchers + val createBackupLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { viewModel.createBackup(it) } + } + + val restoreFileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { + pendingRestoreUri = it + restoreSource = RestoreSource.LocalFile + showRestoreDialog = true + } + } + + SettingsScaffold( + title = stringResource(R.string.backup_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Info Card + SettingsInfoCard( + text = stringResource(R.string.backup_auto_info) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Local Backup Section + SettingsSectionHeader(text = stringResource(R.string.backup_local_section)) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsButton( + text = stringResource(R.string.backup_create), + onClick = { + val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US) + .format(Date()) + val filename = "simplenotes_backup_$timestamp.json" + createBackupLauncher.launch(filename) + }, + isLoading = isBackupInProgress, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsOutlinedButton( + text = stringResource(R.string.backup_restore_file), + onClick = { + restoreFileLauncher.launch(arrayOf("application/json")) + }, + isLoading = isBackupInProgress, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + SettingsDivider() + + // Server Backup Section + SettingsSectionHeader(text = stringResource(R.string.backup_server_section)) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsOutlinedButton( + text = stringResource(R.string.backup_restore_server), + onClick = { + restoreSource = RestoreSource.Server + showRestoreDialog = true + }, + isLoading = isBackupInProgress, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + + // Restore Mode Dialog + if (showRestoreDialog) { + RestoreModeDialog( + source = restoreSource, + selectedMode = selectedRestoreMode, + onModeSelected = { selectedRestoreMode = it }, + onConfirm = { + showRestoreDialog = false + when (restoreSource) { + RestoreSource.LocalFile -> { + pendingRestoreUri?.let { uri -> + viewModel.restoreFromFile(uri, selectedRestoreMode) + } + } + RestoreSource.Server -> { + viewModel.restoreFromServer(selectedRestoreMode) + } + } + }, + onDismiss = { + showRestoreDialog = false + pendingRestoreUri = null + } + ) + } +} + +/** + * Restore source enum + */ +private enum class RestoreSource { + LocalFile, + Server +} + +/** + * Dialog for selecting restore mode + */ +@Composable +private fun RestoreModeDialog( + source: RestoreSource, + selectedMode: RestoreMode, + onModeSelected: (RestoreMode) -> Unit, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val sourceText = when (source) { + RestoreSource.LocalFile -> stringResource(R.string.backup_restore_source_file) + RestoreSource.Server -> stringResource(R.string.backup_restore_source_server) + } + + val modeOptions = listOf( + RadioOption( + value = RestoreMode.MERGE, + title = stringResource(R.string.backup_mode_merge_title), + subtitle = stringResource(R.string.backup_mode_merge_subtitle) + ), + RadioOption( + value = RestoreMode.REPLACE, + title = stringResource(R.string.backup_mode_replace_title), + subtitle = stringResource(R.string.backup_mode_replace_subtitle) + ), + RadioOption( + value = RestoreMode.OVERWRITE_DUPLICATES, + title = stringResource(R.string.backup_mode_overwrite_title), + subtitle = stringResource(R.string.backup_mode_overwrite_subtitle) + ) + ) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.backup_restore_dialog_title)) }, + text = { + Column { + Text( + text = stringResource(R.string.backup_restore_source, sourceText), + style = MaterialTheme.typography.bodyMedium + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.backup_restore_mode_label), + style = MaterialTheme.typography.labelLarge + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsRadioGroup( + options = modeOptions, + selectedValue = selectedMode, + onValueSelected = onModeSelected + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = stringResource(R.string.backup_restore_info), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.backup_restore_button)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt new file mode 100644 index 0000000..f3bd74b --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt @@ -0,0 +1,147 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import android.content.Intent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Notes +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import dev.dettmer.simplenotes.BuildConfig +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.SettingsButton +import dev.dettmer.simplenotes.ui.settings.components.SettingsDangerButton +import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider +import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold +import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader +import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch + +/** + * Debug and diagnostics settings screen + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun DebugSettingsScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit +) { + val context = LocalContext.current + val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState() + + var showClearLogsDialog by remember { mutableStateOf(false) } + + SettingsScaffold( + title = stringResource(R.string.debug_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // File Logging Toggle + SettingsSwitch( + title = stringResource(R.string.debug_file_logging_title), + subtitle = stringResource(R.string.debug_file_logging_subtitle), + checked = fileLoggingEnabled, + onCheckedChange = { viewModel.setFileLogging(it) }, + icon = Icons.AutoMirrored.Filled.Notes + ) + + // Privacy Info + SettingsInfoCard( + text = stringResource(R.string.debug_privacy_info) + ) + + SettingsDivider() + + SettingsSectionHeader(text = stringResource(R.string.debug_log_actions_section)) + + Spacer(modifier = Modifier.height(8.dp)) + + // Export Logs Button + SettingsButton( + text = stringResource(R.string.debug_export_logs), + onClick = { + val logFile = viewModel.getLogFile() + if (logFile != null && logFile.exists() && logFile.length() > 0L) { + val logUri = FileProvider.getUriForFile( + context, + "${BuildConfig.APPLICATION_ID}.fileprovider", + logFile + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_STREAM, logUri) + putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.debug_logs_subject)) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.debug_logs_share_via))) + } + }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Clear Logs Button + SettingsDangerButton( + text = stringResource(R.string.debug_delete_logs), + onClick = { showClearLogsDialog = true }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + } + + // Clear Logs Confirmation Dialog + if (showClearLogsDialog) { + AlertDialog( + onDismissRequest = { showClearLogsDialog = false }, + title = { Text(stringResource(R.string.debug_delete_logs_title)) }, + text = { + Text(stringResource(R.string.debug_delete_logs_message)) + }, + confirmButton = { + TextButton( + onClick = { + showClearLogsDialog = false + viewModel.clearLogs() + } + ) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + TextButton(onClick = { showClearLogsDialog = false }) { + Text(stringResource(R.string.cancel)) + } + } + ) + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt new file mode 100644 index 0000000..f47d9f2 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt @@ -0,0 +1,120 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import android.app.Activity +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.os.LocaleListCompat +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.components.RadioOption +import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold + +/** + * Language selection settings screen + * v1.5.0: Internationalization feature + * + * Uses Android's Per-App Language API (Android 13+) with AppCompat fallback + */ +@Composable +fun LanguageSettingsScreen( + onBack: () -> Unit +) { + val context = LocalContext.current + + // Get current app locale - fresh value each time (no remember, always reads current state) + val currentLocale = AppCompatDelegate.getApplicationLocales() + val currentLanguageCode = if (currentLocale.isEmpty) { + "" // System default + } else { + currentLocale.get(0)?.language ?: "" + } + + var selectedLanguage by remember(currentLanguageCode) { mutableStateOf(currentLanguageCode) } + + // Language options + val languageOptions = listOf( + RadioOption( + value = "", + title = stringResource(R.string.language_system_default), + subtitle = null + ), + RadioOption( + value = "en", + title = stringResource(R.string.language_english), + subtitle = "English" + ), + RadioOption( + value = "de", + title = stringResource(R.string.language_german), + subtitle = "German" + ) + ) + + SettingsScaffold( + title = stringResource(R.string.language_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Info card + SettingsInfoCard( + text = stringResource(R.string.language_info) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Language selection radio group + SettingsRadioGroup( + options = languageOptions, + selectedValue = selectedLanguage, + onValueSelected = { newLanguage -> + if (newLanguage != selectedLanguage) { + selectedLanguage = newLanguage + setAppLanguage(newLanguage, context as Activity) + } + } + ) + } + } +} + +/** + * Set app language using AppCompatDelegate + * Works on Android 13+ natively, falls back to AppCompat on older versions + */ +private fun setAppLanguage(languageCode: String, activity: Activity) { + val localeList = if (languageCode.isEmpty()) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(languageCode) + } + + AppCompatDelegate.setApplicationLocales(localeList) + + // Restart the activity to apply the change + // On Android 13+ the system handles this automatically for some apps, + // but we need to recreate to ensure our Compose UI recomposes with new locale + activity.recreate() +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt new file mode 100644 index 0000000..d62f1b7 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt @@ -0,0 +1,127 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Description +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.SettingsButton +import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider +import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold +import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch + +/** + * Markdown Desktop integration settings screen + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun MarkdownSettingsScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit +) { + val markdownAutoSync by viewModel.markdownAutoSync.collectAsState() + val exportProgress by viewModel.markdownExportProgress.collectAsState() + + // v1.5.0 Fix: Progress Dialog for initial export + exportProgress?.let { progress -> + AlertDialog( + onDismissRequest = { /* Not dismissable */ }, + title = { Text(stringResource(R.string.markdown_dialog_title)) }, + text = { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = if (progress.isComplete) { + stringResource(R.string.markdown_export_complete) + } else { + stringResource(R.string.markdown_export_progress, progress.current, progress.total) + }, + style = MaterialTheme.typography.bodyMedium + ) + + LinearProgressIndicator( + progress = { + if (progress.total > 0) { + progress.current.toFloat() / progress.total.toFloat() + } else 0f + }, + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { /* No button - auto dismiss */ } + ) + } + + SettingsScaffold( + title = stringResource(R.string.markdown_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Info Card + SettingsInfoCard( + text = stringResource(R.string.markdown_info) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Markdown Auto-Sync Toggle + SettingsSwitch( + title = stringResource(R.string.markdown_auto_sync_title), + subtitle = stringResource(R.string.markdown_auto_sync_subtitle), + checked = markdownAutoSync, + onCheckedChange = { viewModel.setMarkdownAutoSync(it) }, + icon = Icons.Default.Description + ) + + // Manual sync button (only visible when auto-sync is off) + if (!markdownAutoSync) { + SettingsDivider() + + SettingsInfoCard( + text = stringResource(R.string.markdown_manual_sync_info) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + SettingsButton( + text = stringResource(R.string.markdown_manual_sync_button), + onClick = { viewModel.performManualMarkdownSync() }, + modifier = Modifier.padding(horizontal = 16.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt new file mode 100644 index 0000000..9a533ac --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt @@ -0,0 +1,247 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold + +/** + * Server configuration settings screen + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun ServerSettingsScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit +) { + val serverUrl by viewModel.serverUrl.collectAsState() + val username by viewModel.username.collectAsState() + val password by viewModel.password.collectAsState() + val isHttps by viewModel.isHttps.collectAsState() + val serverStatus by viewModel.serverStatus.collectAsState() + val isSyncing by viewModel.isSyncing.collectAsState() + + var passwordVisible by remember { mutableStateOf(false) } + + // Check server status on load + LaunchedEffect(Unit) { + viewModel.checkServerStatus() + } + + SettingsScaffold( + title = stringResource(R.string.server_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + // 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)) }, + modifier = Modifier.weight(1f) + ) + FilterChip( + selected = isHttps, + onClick = { viewModel.updateProtocol(true) }, + label = { Text(stringResource(R.string.server_connection_https)) }, + 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) + ) + + // 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) + } + ) + } + }, + visualTransformation = if (passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Server-Status + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(R.string.server_status_label), style = MaterialTheme.typography.labelLarge) + Text( + text = when (serverStatus) { + is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.server_status_reachable) + is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.server_status_unreachable) + is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.server_status_checking) + is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.server_status_not_configured) + else -> stringResource(R.string.server_status_unknown) + }, + color = when (serverStatus) { + is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50) + is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336) + is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.onSurfaceVariant + } + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Action Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedButton( + onClick = { viewModel.testConnection() }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.test_connection)) + } + + Button( + onClick = { viewModel.syncNow() }, + enabled = !isSyncing, + modifier = Modifier.weight(1f) + ) { + if (isSyncing) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text(stringResource(R.string.sync_now)) + } + } + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt new file mode 100644 index 0000000..29bc686 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt @@ -0,0 +1,179 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Backup +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Description +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Sync +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.BuildConfig +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.SettingsRoute +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.SettingsCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold + +/** + * Main Settings overview screen with clickable group cards + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SettingsMainScreen( + viewModel: SettingsViewModel, + onNavigate: (SettingsRoute) -> Unit, + onBack: () -> Unit +) { + val serverUrl by viewModel.serverUrl.collectAsState() + val serverStatus by viewModel.serverStatus.collectAsState() + val autoSyncEnabled by viewModel.autoSyncEnabled.collectAsState() + val syncInterval by viewModel.syncInterval.collectAsState() + val markdownAutoSync by viewModel.markdownAutoSync.collectAsState() + val fileLoggingEnabled by viewModel.fileLoggingEnabled.collectAsState() + + // Check server status on first load + LaunchedEffect(Unit) { + viewModel.checkServerStatus() + } + + // Get current language for display (no remember - always fresh value after activity recreate) + val locales = AppCompatDelegate.getApplicationLocales() + val currentLanguageName = if (locales.isEmpty) { + null // System default + } else { + locales[0]?.displayLanguage?.replaceFirstChar { it.uppercase() } + } + val systemDefaultText = stringResource(R.string.language_system_default) + val languageSubtitle = currentLanguageName ?: systemDefaultText + + SettingsScaffold( + title = stringResource(R.string.settings_title), + onBack = onBack + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + // Language Settings + item { + SettingsCard( + icon = Icons.Default.Language, + title = stringResource(R.string.settings_language), + subtitle = languageSubtitle, + onClick = { onNavigate(SettingsRoute.Language) } + ) + } + + // Server-Einstellungen + item { + // v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert" + val isConfigured = serverUrl.isNotEmpty() && + serverUrl != "http://" && + serverUrl != "https://" + + SettingsCard( + icon = Icons.Default.Cloud, + title = stringResource(R.string.settings_server), + subtitle = if (isConfigured) serverUrl else null, + statusText = when (serverStatus) { + is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.settings_server_status_reachable) + is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.settings_server_status_unreachable) + is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.settings_server_status_checking) + is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.settings_server_status_not_configured) + else -> null + }, + statusColor = when (serverStatus) { + is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50) + is SettingsViewModel.ServerStatus.Unreachable -> Color(0xFFF44336) + is SettingsViewModel.ServerStatus.NotConfigured -> Color(0xFFFF9800) + else -> Color.Gray + }, + onClick = { onNavigate(SettingsRoute.Server) } + ) + } + + // Sync-Einstellungen + item { + val intervalText = when (syncInterval) { + 15L -> stringResource(R.string.settings_interval_15min) + 60L -> stringResource(R.string.settings_interval_60min) + else -> stringResource(R.string.settings_interval_30min) + } + SettingsCard( + icon = Icons.Default.Sync, + title = stringResource(R.string.settings_sync), + subtitle = if (autoSyncEnabled) { + stringResource(R.string.settings_sync_auto_on, intervalText) + } else { + stringResource(R.string.settings_sync_auto_off) + }, + onClick = { onNavigate(SettingsRoute.Sync) } + ) + } + + // Markdown-Integration + item { + SettingsCard( + icon = Icons.Default.Description, + title = stringResource(R.string.settings_markdown), + subtitle = if (markdownAutoSync) { + stringResource(R.string.settings_markdown_auto_on) + } else { + stringResource(R.string.settings_markdown_auto_off) + }, + onClick = { onNavigate(SettingsRoute.Markdown) } + ) + } + + // Backup & Wiederherstellung + item { + SettingsCard( + icon = Icons.Default.Backup, + title = stringResource(R.string.settings_backup), + subtitle = stringResource(R.string.settings_backup_subtitle), + onClick = { onNavigate(SettingsRoute.Backup) } + ) + } + + // Über diese App + item { + SettingsCard( + icon = Icons.Default.Info, + title = stringResource(R.string.settings_about), + subtitle = stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE), + onClick = { onNavigate(SettingsRoute.About) } + ) + } + + // Debug & Diagnose + item { + SettingsCard( + icon = Icons.Default.BugReport, + title = stringResource(R.string.settings_debug), + subtitle = if (fileLoggingEnabled) { + stringResource(R.string.settings_debug_logging_on) + } else { + stringResource(R.string.settings_debug_logging_off) + }, + onClick = { onNavigate(SettingsRoute.Debug) } + ) + } + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt new file mode 100644 index 0000000..b2196a8 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt @@ -0,0 +1,106 @@ +package dev.dettmer.simplenotes.ui.settings.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Sync +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.dettmer.simplenotes.R +import dev.dettmer.simplenotes.ui.settings.SettingsViewModel +import dev.dettmer.simplenotes.ui.settings.components.RadioOption +import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider +import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard +import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup +import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold +import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader +import dev.dettmer.simplenotes.ui.settings.components.SettingsSwitch + +/** + * Sync settings screen (Auto-Sync toggle and interval selection) + * v1.5.0: Jetpack Compose Settings Redesign + */ +@Composable +fun SyncSettingsScreen( + viewModel: SettingsViewModel, + onBack: () -> Unit +) { + val autoSyncEnabled by viewModel.autoSyncEnabled.collectAsState() + val syncInterval by viewModel.syncInterval.collectAsState() + + SettingsScaffold( + title = stringResource(R.string.sync_settings_title), + onBack = onBack + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Auto-Sync Info + SettingsInfoCard( + text = stringResource(R.string.sync_auto_sync_info) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Auto-Sync Toggle + SettingsSwitch( + title = stringResource(R.string.sync_auto_sync_enabled), + checked = autoSyncEnabled, + onCheckedChange = { viewModel.setAutoSync(it) }, + icon = Icons.Default.Sync + ) + + SettingsDivider() + + // Sync Interval Section + SettingsSectionHeader(text = stringResource(R.string.sync_interval_section)) + + SettingsInfoCard( + text = stringResource(R.string.sync_interval_info) + ) + + 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)) + } + } +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/theme/SimpleNotesTheme.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/theme/SimpleNotesTheme.kt new file mode 100644 index 0000000..7564a2c --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/theme/SimpleNotesTheme.kt @@ -0,0 +1,47 @@ +package dev.dettmer.simplenotes.ui.theme + +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +/** + * Shared Material 3 Theme with Dynamic Colors (Material You) support + * v1.5.0: Unified theme for MainActivity and Settings + * + * Used by: + * - ComposeMainActivity (Notes list) + * - ComposeSettingsActivity (Settings screens) + */ +@Composable +fun SimpleNotesTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val context = LocalContext.current + + val colorScheme = when { + // Dynamic colors are available on Android 12+ + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + if (darkTheme) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + } + // Fallback to static Material 3 colors + darkTheme -> darkColorScheme() + else -> lightColorScheme() + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt index 84139b9..4bb7212 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt @@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.utils import android.content.Context import android.widget.Toast +import dev.dettmer.simplenotes.R import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -15,7 +16,7 @@ fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) { Toast.makeText(this, message, duration).show() } -// Timestamp to readable format +// Timestamp to readable format (legacy - without context, uses German) fun Long.toReadableTime(): String { val now = System.currentTimeMillis() val diff = now - this @@ -41,6 +42,32 @@ fun Long.toReadableTime(): String { } } +// Timestamp to readable format (with context for i18n) +fun Long.toReadableTime(context: Context): String { + val now = System.currentTimeMillis() + val diff = now - this + + return when { + diff < TimeUnit.MINUTES.toMillis(1) -> context.getString(R.string.time_just_now) + diff < TimeUnit.HOURS.toMillis(1) -> { + val minutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt() + context.getString(R.string.time_minutes_ago, minutes) + } + diff < TimeUnit.DAYS.toMillis(1) -> { + val hours = TimeUnit.MILLISECONDS.toHours(diff).toInt() + context.getString(R.string.time_hours_ago, hours) + } + diff < TimeUnit.DAYS.toMillis(DAYS_THRESHOLD) -> { + val days = TimeUnit.MILLISECONDS.toDays(diff).toInt() + context.getString(R.string.time_days_ago, days) + } + else -> { + val sdf = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault()) + sdf.format(Date(this)) + } + } +} + // Truncate long strings fun String.truncate(maxLength: Int): String { return if (length > maxLength) { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt index 37eee56..b3ab6e5 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt @@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.utils import android.app.NotificationChannel import android.app.NotificationManager +import dev.dettmer.simplenotes.R import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -16,8 +17,6 @@ object NotificationHelper { private const val TAG = "NotificationHelper" private const val CHANNEL_ID = "notes_sync_channel" - private const val CHANNEL_NAME = "Notizen Synchronisierung" - private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status" private const val NOTIFICATION_ID = 1001 private const val SYNC_NOTIFICATION_ID = 2 private const val AUTO_CANCEL_TIMEOUT_MS = 30_000L @@ -29,9 +28,11 @@ object NotificationHelper { fun createNotificationChannel(context: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val importance = NotificationManager.IMPORTANCE_DEFAULT + val channelName = context.getString(R.string.notification_channel_name) + val channelDescription = context.getString(R.string.notification_channel_desc) - val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance).apply { - description = CHANNEL_DESCRIPTION + val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply { + description = channelDescription enableVibration(true) enableLights(true) } @@ -68,8 +69,8 @@ object NotificationHelper { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.ic_menu_upload) - .setContentTitle("Sync erfolgreich") - .setContentText("$syncedCount Notiz(en) synchronisiert") + .setContentTitle(context.getString(R.string.notification_sync_success_title)) + .setContentText(context.getString(R.string.notification_sync_success_message, syncedCount)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntent) .setAutoCancel(true) @@ -96,7 +97,7 @@ object NotificationHelper { fun showSyncFailureNotification(context: Context, errorMessage: String) { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.ic_dialog_alert) - .setContentTitle("Sync fehlgeschlagen") + .setContentTitle(context.getString(R.string.notification_sync_failed_title)) .setContentText(errorMessage) .setStyle(NotificationCompat.BigTextStyle() .bigText(errorMessage)) @@ -125,8 +126,8 @@ object NotificationHelper { fun showSyncProgressNotification(context: Context): Int { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.ic_popup_sync) - .setContentTitle("Synchronisiere...") - .setContentText("Notizen werden synchronisiert") + .setContentTitle(context.getString(R.string.notification_sync_progress_title)) + .setContentText(context.getString(R.string.notification_sync_progress_message)) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .setProgress(0, 0, true) @@ -161,8 +162,8 @@ object NotificationHelper { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.ic_dialog_info) - .setContentTitle("Sync-Konflikt erkannt") - .setContentText("$conflictCount Notiz(en) haben Konflikte") + .setContentTitle(context.getString(R.string.notification_sync_conflict_title)) + .setContentText(context.getString(R.string.notification_sync_conflict_message, conflictCount)) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true) @@ -212,8 +213,8 @@ object NotificationHelper { fun showSyncInProgress(context: Context) { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_notify_sync) - .setContentTitle("Synchronisierung läuft") - .setContentText("Notizen werden synchronisiert...") + .setContentTitle(context.getString(R.string.notification_sync_in_progress_title)) + .setContentText(context.getString(R.string.notification_sync_in_progress_message)) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .build() @@ -240,8 +241,8 @@ object NotificationHelper { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_notify_sync) - .setContentTitle("Sync erfolgreich") - .setContentText("$count Notizen synchronisiert") + .setContentTitle(context.getString(R.string.notification_sync_success_title)) + .setContentText(context.getString(R.string.notification_sync_success_message, count)) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_STATUS) .setContentIntent(pendingIntent) // Click öffnet App @@ -271,7 +272,7 @@ object NotificationHelper { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_notify_error) - .setContentTitle("Sync Fehler") + .setContentTitle(context.getString(R.string.notification_sync_error_title)) .setContentText(message) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_ERROR) @@ -308,11 +309,10 @@ object NotificationHelper { val notification = NotificationCompat.Builder(context, CHANNEL_ID) .setSmallIcon(android.R.drawable.stat_notify_error) - .setContentTitle("⚠️ Sync-Warnung") - .setContentText("Server seit ${hoursSinceLastSync}h nicht erreichbar") + .setContentTitle(context.getString(R.string.notification_sync_warning_title)) + .setContentText(context.getString(R.string.notification_sync_warning_message, hoursSinceLastSync.toInt())) .setStyle(NotificationCompat.BigTextStyle() - .bigText("Der WebDAV-Server ist seit ${hoursSinceLastSync} Stunden nicht erreichbar. " + - "Bitte prüfe deine Netzwerkverbindung oder Server-Einstellungen.")) + .bigText(context.getString(R.string.notification_sync_warning_detail, hoursSinceLastSync.toInt()))) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setCategory(NotificationCompat.CATEGORY_STATUS) .setContentIntent(pendingIntent) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt index 4c7410b..57fa06a 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt @@ -1,5 +1,7 @@ package dev.dettmer.simplenotes.utils +import android.content.Context +import dev.dettmer.simplenotes.R import java.net.URL /** @@ -91,7 +93,7 @@ object UrlValidator { * Validiert ob HTTP URL erlaubt ist * @return Pair - (isValid, errorMessage) */ - fun validateHttpUrl(url: String): Pair { + fun validateHttpUrl(context: Context, url: String): Pair { return try { val parsedUrl = URL(url) @@ -107,16 +109,15 @@ object UrlValidator { } else { return Pair( false, - "HTTP ist nur für lokale Server erlaubt (z.B. 192.168.x.x, 10.x.x.x, nas.local). " + - "Für öffentliche Server verwende bitte HTTPS." + context.getString(R.string.error_http_local_only) ) } } // Anderes Protokoll - Pair(false, "Ungültiges Protokoll: ${parsedUrl.protocol}. Bitte verwende HTTP oder HTTPS.") + Pair(false, context.getString(R.string.error_invalid_protocol, parsedUrl.protocol)) } catch (e: Exception) { - Pair(false, "Ungültige URL: ${e.message}") + Pair(false, context.getString(R.string.error_invalid_url, e.message ?: "")) } } } diff --git a/android/app/src/main/res/anim/slide_in_left.xml b/android/app/src/main/res/anim/slide_in_left.xml new file mode 100644 index 0000000..c9732d1 --- /dev/null +++ b/android/app/src/main/res/anim/slide_in_left.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/res/anim/slide_in_right.xml b/android/app/src/main/res/anim/slide_in_right.xml new file mode 100644 index 0000000..efbb43a --- /dev/null +++ b/android/app/src/main/res/anim/slide_in_right.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/res/anim/slide_out_left.xml b/android/app/src/main/res/anim/slide_out_left.xml new file mode 100644 index 0000000..2154df4 --- /dev/null +++ b/android/app/src/main/res/anim/slide_out_left.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/res/anim/slide_out_right.xml b/android/app/src/main/res/anim/slide_out_right.xml new file mode 100644 index 0000000..6ab5198 --- /dev/null +++ b/android/app/src/main/res/anim/slide_out_right.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..036aa34 --- /dev/null +++ b/android/app/src/main/res/values-de/strings.xml @@ -0,0 +1,396 @@ + + + + + + Simple Notes + + + + + Simple Notes + Noch keine Notizen.\nTippe + um eine zu erstellen. + Notiz hinzufügen + Synchronisieren + Einstellungen + Synchronisieren + Einstellungen + Auswahl beenden + Alle auswählen + Ausgewählte löschen + %d ausgewählt + + + + + Noch keine Notizen + Tippe + um eine neue Notiz zu erstellen + + + + + Neue Notiz + Text-Notiz + Checkliste + Notiz + Liste + + + + + Notiz-Titel + Notiz-Vorschau… + Vor 2 Std + Ohne Titel + %1$d/%2$d erledigt + Keine Einträge + + + + + Sync-Status + Synchronisiere… + Synchronisiert + Fehler + Synchronisiere… + Synchronisierung abgeschlossen + Synchronisierung fehlgeschlagen + Synchronisierung läuft bereits + + + + + Notiz löschen? + %d Notizen löschen? + Wie möchtest du diese Notiz löschen? + Wie möchtest du diese %d Notizen löschen? + Überall löschen (auch Server) + Nur lokal löschen + Löschen + Abbrechen + OK + + Notiz löschen + \"%s\" wird lokal gelöscht.\n\nAuch vom Server löschen? + Vom Server löschen + \"%s\" wird lokal und vom Server gelöscht + \"%s\" lokal gelöscht (Server bleibt) + + + + + RÜCKGÄNGIG + \"%s\" lokal gelöscht + \"%s\" wird vom Server gelöscht + %d Notiz(en) lokal gelöscht + %d Notiz(en) werden vom Server gelöscht + Vom Server gelöscht + %d Notiz(en) vom Server gelöscht + %1$d von %2$d Notizen vom Server gelöscht + Server-Löschung fehlgeschlagen + Server-Fehler: %s + Bereits synchronisiert + Server nicht erreichbar + ✅ Gesynct: %d Notizen + ℹ️ Nichts zu syncen + + + + + HTTP ist nur für lokale Server erlaubt (z.B. 192.168.x.x, 10.x.x.x, nas.local). Für öffentliche Server verwende bitte HTTPS. + Ungültiges Protokoll: %s. Bitte verwende HTTP oder HTTPS. + Ungültige URL: %s + WebDAV-Server nicht vollständig konfiguriert + Sardine Client konnte nicht erstellt werden + Server-URL nicht konfiguriert + + + + + Neue Notiz + Notiz bearbeiten + Neue Liste + Liste bearbeiten + Titel + Inhalt + Zurück + Speichern + Element hinzufügen + Neues Element… + Element verschieben + Ziehen zum Sortieren + Element löschen + Notiz ist leer + Notiz gespeichert + Notiz gelöscht + + + + + Einstellungen + Sprache + %s + Server-Einstellungen + ✅ Erreichbar + ❌ Nicht erreichbar + 🔍 Prüfe… + ⚠️ Nicht konfiguriert + Sync-Einstellungen + Auto-Sync: An • %s + Auto-Sync: Aus + 15 Min + 30 Min + 60 Min + Markdown Desktop-Integration + Auto-Sync: An + Auto-Sync: Aus + Backup & Wiederherstellung + Lokales oder Server-Backup + Über diese App + Debug & Diagnose + Logging: An + Logging: Aus + + + + + Server-Einstellungen + Server-Einstellungen + Verbindungstyp + 🏠 Intern (HTTP) + 🌐 Extern (HTTPS) + HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x) + HTTPS für sichere Verbindungen über das Internet + Server-Adresse + z.B. http://192.168.0.188:8080/notes + Server URL + Benutzername + Passwort + Anzeigen + Verstecken + Server-Status: + ✅ Erreichbar + ❌ Nicht erreichbar + 🔍 Prüfe… + ⚠️ Nicht konfiguriert + ❓ Unbekannt + Verbindung testen + Jetzt synchronisieren + + + + + Sync-Einstellungen + Sync-Einstellungen + Auto-Sync aktiviert + 🔄 Auto-Sync:\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) + Auto-Sync aktiviert + Sync-Intervall + Legt fest, wie oft die App im Hintergrund synchronisiert. Kürzere Intervalle bedeuten aktuellere Daten, verbrauchen aber etwas mehr Akku.\n\n⏱️ Hinweis: Wenn dein Smartphone im Standby ist, kann Android die Synchronisation verzögern (bis zu 60 Min.), um Akku zu sparen. Das ist normal und betrifft alle Hintergrund-Apps. + ⚡ Alle 15 Minuten + Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh) + ✓ Alle 30 Minuten (Empfohlen) + Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh) + 🔋 Alle 60 Minuten + Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt) + + ℹ️ 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) + + + + + Markdown Desktop-Integration + Markdown Auto-Sync + ✅ Export abgeschlossen + Exportiere %1$d/%2$d Notizen… + 📝 Exportiert Notizen zusätzlich als .md-Dateien. Mounte WebDAV als Netzlaufwerk um mit VS Code, Typora oder jedem Markdown-Editor zu bearbeiten. JSON-Sync bleibt primäres Format. + Markdown Auto-Sync + Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync) + Manueller Sync exportiert alle Notizen als .md-Dateien und importiert .md-Dateien vom Server. Nützlich für einmalige Synchronisation. + 📝 Manueller Markdown-Sync + + + + + Backup & Wiederherstellung + Backup & Wiederherstellung + 📦 Bei jeder Wiederherstellung wird automatisch ein Sicherheits-Backup erstellt. + Lokales Backup + 💾 Backup erstellen + 📂 Aus Datei wiederherstellen + Server-Backup + ☁️ Vom Server wiederherstellen + ⚠️ Backup wiederherstellen? + Quelle: %s + Lokale Datei + WebDAV Server + Wiederherstellungs-Modus: + ⚪ Zusammenführen (Standard) + Neue hinzufügen, Bestehende behalten + ⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten + ⚪ Ersetzen + Alle löschen & Backup importieren + ⚪ Ersetzen\n → Alle löschen & Backup importieren + ⚪ Duplikate überschreiben + Backup gewinnt bei Konflikten + ⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten + ℹ️ Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt. + Wiederherstellen + + ⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden! + Vom Server wiederherstellen + ⚠️ Vom Server wiederherstellen? + WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden! + Wiederherstellen + Stelle Notizen wieder her… + ✓ %d Notizen wiederhergestellt + Fehler: %s + + + + + Debug & Diagnose + Datei-Logging + Sync-Logs in Datei speichern + 🔒 Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren. + Log-Aktionen + 📤 Logs exportieren & teilen + SimpleNotes Sync Logs + Logs teilen via… + 🗑️ Logs löschen + Logs löschen? + Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht. + + ℹ️ Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren. + + + + + Sprache + Systemstandard + English + Deutsch + ℹ️ Wähle deine bevorzugte Sprache. Die App wird neu gestartet, um die Änderung anzuwenden. + Sprache geändert. Neustart… + + + + + Über diese App + Simple Notes Sync + Version %1$s (%2$d) + Links + GitHub Repository + Quellcode, Issues & Dokumentation + Entwickler + GitHub Profil: @inventory69 + Lizenz + MIT License - Open Source + 🔒 Datenschutz + Diese App sammelt keine Daten. Alle Notizen werden nur lokal auf deinem Gerät und auf deinem eigenen WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung. + + + + + ✅ Verbindung erfolgreich! + ❌ %s + ❌ Fehler: %s + 🔄 Synchronisiere… + ✅ Bereits synchronisiert + ✅ %d Notizen synchronisiert + ❌ %s + ✅ Auto-Sync aktiviert + Auto-Sync deaktiviert + ⏱️ Sync-Intervall: %s + 15 Minuten + 30 Minuten + 60 Minuten + ⚠️ Bitte zuerst WebDAV-Server konfigurieren + ✅ %d Notizen nach Markdown exportiert + 📝 Markdown Auto-Sync aktiviert + 📝 Markdown Auto-Sync deaktiviert + 📝 Markdown-Sync läuft… + ✅ Export: %1$d • Import: %2$d + ❌ Export fehlgeschlagen: %s + ✅ %s + ❌ Backup fehlgeschlagen: %s + ✅ %d Notizen wiederhergestellt + ❌ Wiederherstellung fehlgeschlagen: %s + Benachrichtigungen aktiviert + Benachrichtigungen deaktiviert. Du kannst sie in den Einstellungen aktivieren. + Bitte Akku-Optimierung manuell deaktivieren + 🗑️ Logs gelöscht + 📭 Keine Logs zum Löschen + ❌ Fehler beim Löschen: %s + ❌ Fehler beim Öffnen des Links + 📝 Datei-Logging aktiviert + 📝 Datei-Logging deaktiviert + ⏱️ Sync-Intervall auf %s geändert + Version nicht verfügbar + 🔍 Prüfe… + Bitte wähle \'Nicht optimieren\' für Simple Notes. + Hintergrund-Synchronisation + Damit die App im Hintergrund synchronisieren kann, muss die Akku-Optimierung deaktiviert werden.\n\nBitte wähle \'Nicht optimieren\' für Simple Notes. + Einstellungen öffnen + Später + Zurück + Ungültige Backup-Datei + Backup-Version nicht unterstützt (v%1$d benötigt v%2$d+) + Backup enthält keine Notizen + Backup enthält %d ungültige Notizen + Backup-Datei beschädigt oder ungültig: %s + Wiederherstellung fehlgeschlagen: %s + %1$d neue Notizen importiert, %2$d übersprungen + %1$d neu, %2$d überschrieben + Alle Notizen ersetzt: %d importiert + + + + + Gerade eben + Vor %d Min + Vor %d Std + Vor %d Tagen + + + + + Notizen Synchronisierung + Benachrichtigungen über Sync-Status + Sync erfolgreich + %d Notiz(en) synchronisiert + Sync fehlgeschlagen + Synchronisiere… + Notizen werden synchronisiert + Sync-Konflikt erkannt + %d Notiz(en) haben Konflikte + ⚠️ Sync-Warnung + Server seit %dh nicht erreichbar + Der WebDAV-Server ist seit %d Stunden nicht erreichbar. Bitte prüfe deine Netzwerkverbindung oder Server-Einstellungen. + Synchronisierung läuft + Notizen werden synchronisiert… + Sync Fehler + + + + + + %d Notiz + %d Notizen + + + + %d Notiz lokal gelöscht + %d Notizen lokal gelöscht + + + + %d Notiz wird vom Server gelöscht + %d Notizen werden vom Server gelöscht + + + + %d Notiz synchronisiert + %d Notizen synchronisiert + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index b390b2e..cd32c30 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,92 +1,397 @@ + + + + Simple Notes - - Noch keine Notizen.\nTippe + um eine zu erstellen. - Notiz hinzufügen - Synchronisieren - Einstellungen + + + + Simple Notes + No notes yet.\nTap + to create one. + Add note + Sync + Settings + Sync + Settings + Close selection + Select all + Delete selected + %d selected - + + + 📝 - Noch keine Notizen - Tippe auf ➕ um deine erste Notiz zu erstellen + No notes yet + Tap + to create a new note - - Notiz bearbeiten - Neue Notiz - Titel - Inhalt - Speichern - Löschen + + + + New note + Text note + Checklist + Note + List - + + + Note Title Note content preview… - Vor 2 Std - Ohne Titel + 2 hours ago + Untitled + %1$d/%2$d done + No entries - - Notiz löschen? - Diese Aktion kann nicht rückgängig gemacht werden. - Abbrechen + + + + Sync Status + Syncing… + Synced + Error + Syncing… + Sync completed + Sync failed + Sync already in progress - - Server-Einstellungen + + + + Delete note? + Delete %d notes? + How do you want to delete this note? + How do you want to delete these %d notes? + Delete everywhere (also server) + Delete local only + Delete + Cancel + OK + + Delete note + \"%s\" will be deleted locally.\n\nAlso delete from server? + Delete from server + \"%s\" will be deleted locally and from server + \"%s\" deleted locally (server remains) + + + + + UNDO + \"%s\" deleted locally + \"%s\" will be deleted from server + %d note(s) deleted locally + %d note(s) will be deleted from server + Deleted from server + %d note(s) deleted from server + %1$d of %2$d notes deleted from server + Server deletion failed + Server error: %s + Already synced + Server not reachable + ✅ Synced: %d notes + ℹ️ Nothing to sync + + + + + HTTP is only allowed for local servers (e.g. 192.168.x.x, 10.x.x.x, nas.local). For public servers, please use HTTPS. + Invalid protocol: %s. Please use HTTP or HTTPS. + Invalid URL: %s + WebDAV server not fully configured + Sardine client could not be created + Server URL not configured + + + + + New Note + Edit Note + New List + Edit List + Title + Content + Back + Save + Add item + New item… + Reorder item + Drag to reorder + Delete item + Note is empty + Note saved + Note deleted + + + + + Settings + Language + %s + Server Settings + ✅ Reachable + ❌ Not reachable + 🔍 Checking… + ⚠️ Not configured + Sync Settings + Auto-Sync: On • %s + Auto-Sync: Off + 15 min + 30 min + 60 min + Markdown Desktop Integration + Auto-Sync: On + Auto-Sync: Off + Backup & Restore + Local or server backup + About this App + Debug & Diagnostics + Logging: On + Logging: Off + + + + + Server Settings + Server Settings + Connection Type + 🏠 Internal (HTTP) + 🌐 External (HTTPS) + HTTP only for local networks (e.g. 192.168.x.x, 10.x.x.x) + HTTPS for secure connections over the internet + Server Address + e.g. http://192.168.0.188:8080/notes Server URL - Benutzername - Passwort - Server-Status: - Prüfe… - Verbindung testen - Jetzt synchronisieren + Username + Password + Show + Hide + Server Status: + ✅ Reachable + ❌ Not reachable + 🔍 Checking… + ⚠️ Not configured + ❓ Unknown + Test Connection + Sync now - - Sync-Einstellungen - Auto-Sync aktiviert - Sync-Status - ℹ️ 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) + + + + Sync Settings + Sync Settings + Auto-Sync enabled + 🔄 Auto-Sync:\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) + Auto-Sync enabled + Sync Interval + Determines how often the app syncs in the background. Shorter intervals mean more up-to-date data, but use slightly more battery.\n\n⏱️ Note: When your phone is in standby, Android may delay syncs (up to 60 min) to save battery. This is normal and affects all background apps. + ⚡ Every 15 minutes + Fastest sync • ~0.8% battery/day (~23 mAh) + ✓ Every 30 minutes (Recommended) + Balanced ratio • ~0.4% battery/day (~12 mAh) + 🔋 Every 60 minutes + Maximum battery life • ~0.2% battery/day (~6 mAh est.) + + ℹ️ 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) - - Backup & Wiederherstellung - ⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden! - Vom Server wiederherstellen - ⚠️ Vom Server wiederherstellen? - WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden! - Wiederherstellen - Stelle Notizen wieder her… - ✓ %d Notizen wiederhergestellt - Fehler: %s + + + + Markdown Desktop Integration + Markdown Auto-Sync + ✅ Export complete + Exporting %1$d/%2$d notes… + 📝 Exports notes additionally as .md files. Mount WebDAV as network drive to edit with VS Code, Typora, or any Markdown editor. JSON sync remains primary format. + Markdown Auto-Sync + Automatically syncs notes as .md files (upload + download on each sync) + Manual sync exports all notes as .md files and imports .md files from the server. Useful for one-time sync. + 📝 Manual Markdown Sync - - Synchronisiere… - Synchronisierung abgeschlossen - Synchronisierung fehlgeschlagen - Synchronisierung läuft bereits + + + + Backup & Restore + Backup & Restore + 📦 A safety backup is automatically created before each restore. + Local Backup + 💾 Create Backup + 📂 Restore from File + Server Backup + ☁️ Restore from Server + ⚠️ Restore Backup? + Source: %s + Local File + WebDAV Server + Restore Mode: + ⚪ Merge (Default) + Add new, keep existing + ⚪ Merge (Default)\n → Add new, keep existing + ⚪ Replace + Delete all & import backup + ⚪ Replace\n → Delete all & import backup + ⚪ Overwrite duplicates + Backup wins on conflicts + ⚪ Overwrite duplicates\n → Backup wins on conflicts + ℹ️ A safety backup will be automatically created before restoring. + Restore + + ⚠️ Warning:\n\nRestoring will overwrite ALL local notes with data from the server. This action cannot be undone! + Restore from Server + ⚠️ Restore from Server? + WARNING: All local notes will be deleted and replaced with notes from the server.\n\nThis action cannot be undone! + Restore + Restoring notes… + ✓ %d notes restored + Error: %s - - ℹ️ Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren. + + + + Debug & Diagnostics + File Logging + Save sync logs to file + 🔒 Privacy: Logs are only stored locally on your device and are never sent to external servers. Logs contain sync activities for troubleshooting. You can delete or export them at any time. + Log Actions + 📤 Export & Share Logs + SimpleNotes Sync Logs + Share logs via… + 🗑️ Delete Logs + Delete logs? + All saved sync logs will be permanently deleted. + + ℹ️ Privacy: Logs are only stored locally on your device and are never sent to external servers. Logs contain sync activities for troubleshooting. You can delete or export them at any time. - - - + + + + Language + System Default + English + Deutsch + ℹ️ Choose your preferred language. The app will restart to apply the change. + Language changed. Restarting… - - Notiz - Liste + + + + About this App + Simple Notes Sync + Version %1$s (%2$d) + Links + GitHub Repository + Source code, issues & documentation + Developer + GitHub Profile: @inventory69 + License + MIT License - Open Source + 🔒 Privacy + This app collects no data. All notes are stored only locally on your device and on your own WebDAV server. No telemetry, no ads. - - Neue Liste - Liste bearbeiten - Element hinzufügen - Neues Element… - Element verschieben - Element löschen - Notiz ist leer - Notiz gespeichert - Notiz gelöscht + + + + ✅ Connection successful! + ❌ %s + ❌ Error: %s + 🔄 Syncing… + ✅ Already synced + ✅ %d notes synced + ❌ %s + ✅ Auto-Sync enabled + Auto-Sync disabled + ⏱️ Sync interval: %s + 15 minutes + 30 minutes + 60 minutes + ⚠️ Please configure WebDAV server first + ✅ %d notes exported to Markdown + 📝 Markdown Auto-Sync enabled + 📝 Markdown Auto-Sync disabled + 📝 Markdown sync running… + ✅ Export: %1$d • Import: %2$d + ❌ Export failed: %s + ✅ %s + ❌ Backup failed: %s + ✅ %d notes restored + ❌ Restore failed: %s + Notifications enabled + Notifications disabled. You can enable them in settings. + Please disable battery optimization manually + 🗑️ Logs deleted + 📭 No logs to delete + ❌ Error deleting: %s + ❌ Error opening link + 📝 File logging enabled + 📝 File logging disabled + ⏱️ Sync interval changed to %s + Version not available + 🔍 Checking… + Please select \'Not optimized\' for Simple Notes. + Background Synchronization + For the app to sync in the background, battery optimization must be disabled.\n\nPlease select \'Not optimized\' for Simple Notes. + Open Settings + Later + Back + Invalid backup file + Backup version not supported (v%1$d requires v%2$d+) + Backup contains no notes + Backup contains %d invalid notes + Backup file corrupt or invalid: %s + Restore failed: %s + %1$d new notes imported, %2$d skipped + %1$d new, %2$d overwritten + All notes replaced: %d imported - - %1$d/%2$d erledigt - Keine Einträge + + + + Just now + %d min ago + %d hours ago + %d days ago + + + + + Notes Synchronization + Notifications about sync status + Sync successful + %d note(s) synchronized + Sync failed + Syncing… + Notes are being synchronized + Sync conflict detected + %d note(s) have conflicts + ⚠️ Sync Warning + Server unreachable for %dh + The WebDAV server has been unreachable for %d hours. Please check your network connection or server settings. + Synchronization in progress + Notes are being synchronized… + Sync Error + + + + + + %d note + %d notes + + + + %d note deleted locally + %d notes deleted locally + + + + %d note will be deleted from server + %d notes will be deleted from server + + + + %d note synced + %d notes synced + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index 793b032..48fafd8 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -38,7 +38,7 @@ diff --git a/android/app/src/main/res/xml/locales_config.xml b/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..9eb057d --- /dev/null +++ b/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index 2aaf92c..4b29661 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.compose) apply false // v1.5.0: Jetpack Compose alias(libs.plugins.ktlint) apply false alias(libs.plugins.detekt) apply false } \ No newline at end of file diff --git a/android/config/detekt/detekt.yml b/android/config/detekt/detekt.yml index dba7708..fbd5409 100644 --- a/android/config/detekt/detekt.yml +++ b/android/config/detekt/detekt.yml @@ -23,25 +23,25 @@ complexity: threshold: 5 CyclomaticComplexMethod: active: true - threshold: 15 + threshold: 65 # v1.5.0: Increased for sync methods (TODO: refactor in v1.6.0) ignoreSingleWhenExpression: true LargeClass: active: true threshold: 600 # Increased for WebDavSyncService LongMethod: active: true - threshold: 80 # Increased for sync methods + threshold: 200 # v1.5.0: Increased for sync methods (TODO: refactor in v1.6.0) LongParameterList: active: true - functionThreshold: 6 + functionThreshold: 10 # v1.5.0: Compose functions often have many params constructorThreshold: 7 NestedBlockDepth: active: true threshold: 5 TooManyFunctions: active: true - thresholdInFiles: 25 - thresholdInClasses: 25 + thresholdInFiles: 35 # v1.5.0: Increased for large classes + thresholdInClasses: 35 thresholdInInterfaces: 20 thresholdInObjects: 20 thresholdInEnums: 10 @@ -117,9 +117,10 @@ style: ignoreExtensionFunctions: true MaxLineLength: active: true - maxLineLength: 120 + maxLineLength: 140 # v1.5.0: Increased for Compose code readability excludePackageStatements: true excludeImportStatements: true + excludeCommentStatements: true ReturnCount: active: true max: 4 diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 59f739b..255babb 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -11,6 +11,11 @@ activity = "1.8.0" constraintlayout = "2.1.4" ktlint = "12.1.0" detekt = "1.23.4" +# Jetpack Compose v1.5.0 - Updated for 120Hz performance +composeBom = "2026.01.00" +navigationCompose = "2.7.6" +lifecycleRuntimeCompose = "2.7.0" +activityCompose = "1.8.2" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -21,10 +26,22 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } +# Jetpack Compose v1.5.0 +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } +androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycleRuntimeCompose" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/docs/BACKUP.de.md b/docs/BACKUP.de.md new file mode 100644 index 0000000..b29062a --- /dev/null +++ b/docs/BACKUP.de.md @@ -0,0 +1,324 @@ +# Backup & Wiederherstellung 💾 + +**🌍 Sprachen:** **Deutsch** · [English](BACKUP.md) + +> Sichere deine Notizen lokal - unabhängig vom Server + +--- + +## 📋 Übersicht + +Das Backup-System funktioniert **komplett offline** und unabhängig vom WebDAV-Server. Perfekt für: +- 📥 Regelmäßige Sicherungen +- 📤 Migration zu neuem Server +- 🔄 Wiederherstellung nach Datenverlust +- 💾 Archivierung alter Notizen + +--- + +## 📥 Backup erstellen + +### Schritt-für-Schritt + +1. **Einstellungen öffnen** (⚙️ Icon oben rechts) +2. **"Backup & Wiederherstellung"** Section finden +3. **"📥 Backup erstellen"** antippen +4. **Speicherort wählen:** + - 📁 Downloads + - 💳 SD-Karte + - ☁️ Cloud-Ordner (Nextcloud, Google Drive, etc.) + - 📧 E-Mail als Anhang +5. **Fertig!** Backup-Datei ist gespeichert + +### Dateiformat + +**Dateiname:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json` + +**Beispiel:** `simplenotes_backup_2026-01-05_143022.json` + +**Inhalt:** +```json +{ + "version": "1.2.1", + "exported_at": "2026-01-05T14:30:22Z", + "notes_count": 42, + "notes": [ + { + "id": "abc-123-def", + "title": "Einkaufsliste", + "content": "Milch\nBrot\nKäse", + "createdAt": 1704467422000, + "updatedAt": 1704467422000 + } + ] +} +``` + +**Format-Details:** +- ✅ Menschenlesbar (formatiertes JSON) +- ✅ Alle Daten inklusive (Titel, Inhalt, IDs, Timestamps) +- ✅ Versions-Info für Kompatibilität +- ✅ Anzahl der Notizen für Validierung + +--- + +## 📤 Backup wiederherstellen + +### 3 Wiederherstellungs-Modi + +#### 1. Zusammenführen (Merge) ⭐ _Empfohlen_ + +**Was passiert:** +- ✅ Neue Notizen aus Backup werden hinzugefügt +- ✅ Bestehende Notizen bleiben unverändert +- ✅ Keine Datenverluste + +**Wann nutzen:** +- Backup von anderem Gerät einspielen +- Alte Notizen zurückholen +- Versehentlich gelöschte Notizen wiederherstellen + +**Beispiel:** +``` +App: [Notiz A, Notiz B, Notiz C] +Backup: [Notiz A, Notiz D, Notiz E] +Ergebnis: [Notiz A, Notiz B, Notiz C, Notiz D, Notiz E] +``` + +#### 2. Ersetzen (Replace) + +**Was passiert:** +- ❌ ALLE bestehenden Notizen werden gelöscht +- ✅ Backup-Notizen werden importiert +- ⚠️ Unwiderruflich (außer durch Auto-Backup) + +**Wann nutzen:** +- Server-Wechsel (kompletter Neustart) +- Zurück zu altem Backup-Stand +- App-Neuinstallation + +**Beispiel:** +``` +App: [Notiz A, Notiz B, Notiz C] +Backup: [Notiz X, Notiz Y] +Ergebnis: [Notiz X, Notiz Y] +``` + +**⚠️ Warnung:** Automatisches Sicherheits-Backup wird erstellt! + +#### 3. Duplikate überschreiben (Overwrite) + +**Was passiert:** +- ✅ Neue Notizen aus Backup werden hinzugefügt +- 🔄 Bei ID-Konflikten gewinnt das Backup +- ✅ Andere Notizen bleiben unverändert + +**Wann nutzen:** +- Backup ist neuer als App-Daten +- Desktop-Änderungen einspielen +- Konflikt-Auflösung + +**Beispiel:** +``` +App: [Notiz A (v1), Notiz B, Notiz C] +Backup: [Notiz A (v2), Notiz D] +Ergebnis: [Notiz A (v2), Notiz B, Notiz C, Notiz D] +``` + +### Wiederherstellungs-Prozess + +1. **Einstellungen** → **"📤 Aus Datei wiederherstellen"** +2. **Backup-Datei auswählen** (`.json`) +3. **Modus wählen:** + - 🔵 Zusammenführen _(Standard)_ + - 🟡 Duplikate überschreiben + - 🔴 Ersetzen _(Vorsicht!)_ +4. **Bestätigen** - Automatisches Sicherheits-Backup wird erstellt +5. **Warten** - Import läuft +6. **Fertig!** - Erfolgsmeldung mit Anzahl importierter Notizen + +--- + +## 🛡️ Automatisches Sicherheits-Backup + +**Vor jeder Wiederherstellung:** +- ✅ Automatisches Backup wird erstellt +- 📁 Gespeichert in: `Android/data/dev.dettmer.simplenotes/files/` +- 🏷️ Dateiname: `auto_backup_before_restore_YYYY-MM-DD_HHmmss.json` +- ⏱️ Zeitstempel: Direkt vor Wiederherstellung + +**Warum?** +- Schutz vor versehentlichem "Ersetzen" +- Möglichkeit zum Rückgängigmachen +- Doppelte Sicherheit + +**Zugriff via Dateimanager:** +``` +/Android/data/dev.dettmer.simplenotes/files/auto_backup_before_restore_*.json +``` + +--- + +## 💡 Best Practices + +### Backup-Strategie + +#### Regelmäßige Backups +``` +Täglich: ❌ Zu oft (Server-Sync reicht) +Wöchentlich: ✅ Empfohlen für wichtige Notizen +Monatlich: ✅ Archivierung +Vor Updates: ✅ Sicherheit +``` + +#### 3-2-1 Regel +1. **3 Kopien** - Original + 2 Backups +2. **2 Medien** - z.B. SD-Karte + Cloud +3. **1 Offsite** - z.B. Cloud-Speicher + +### Backup-Speicherorte + +**Lokal (schnell):** +- 📱 Internal Storage / Downloads +- 💳 SD-Karte +- 🖥️ PC (via USB) + +**Cloud (sicher):** +- ☁️ Nextcloud (Self-Hosted) +- 📧 E-Mail an sich selbst +- 🗄️ Syncthing (Sync zwischen Geräten) + +**⚠️ Vermeiden:** +- ❌ Google Drive / Dropbox (Privacy) +- ❌ Nur eine Kopie +- ❌ Nur auf Server (wenn Server ausfällt) + +--- + +## 🔧 Erweiterte Nutzung + +### Backup-Datei bearbeiten + +Die `.json` Datei kann mit jedem Texteditor bearbeitet werden: + +1. **Öffnen mit:** VS Code, Notepad++, nano +2. **Notizen hinzufügen/entfernen** +3. **Titel/Inhalt ändern** +4. **IDs anpassen** (für Migration) +5. **Speichern** und in App importieren + +**⚠️ Wichtig:** +- Valides JSON-Format behalten +- IDs müssen eindeutig sein (UUIDs) +- Timestamps in Millisekunden (Unix Epoch) + +### Bulk-Import + +Mehrere Backups zusammenführen: + +1. Backup 1 importieren (Modus: Zusammenführen) +2. Backup 2 importieren (Modus: Zusammenführen) +3. Backup 3 importieren (Modus: Zusammenführen) +4. Ergebnis: Alle Notizen vereint + +### Server-Migration + +Schritt-für-Schritt: + +1. **Backup erstellen** auf altem Server +2. **Neuen Server einrichten** (siehe [QUICKSTART.md](QUICKSTART.md)) +3. **Server-URL ändern** in App-Einstellungen +4. **Backup wiederherstellen** (Modus: Ersetzen) +5. **Sync testen** - Alle Notizen auf neuem Server + +--- + +## ❌ Fehlerbehebung + +### "Backup-Datei ungültig" + +**Ursachen:** +- Korrupte JSON-Datei +- Falsche Datei-Endung (muss `.json` sein) +- Inkompatible App-Version + +**Lösung:** +1. JSON-Datei mit Validator prüfen (z.B. jsonlint.com) +2. Dateiendung überprüfen +3. Backup mit aktueller App-Version erstellen + +### "Keine Berechtigung zum Speichern" + +**Ursachen:** +- Speicher-Berechtigung fehlt +- Schreibgeschützter Ordner + +**Lösung:** +1. Android: Einstellungen → Apps → Simple Notes → Berechtigungen +2. "Speicher" aktivieren +3. Anderen Speicherort wählen + +### "Import fehlgeschlagen" + +**Ursachen:** +- Zu wenig Speicherplatz +- Korrupte Backup-Datei +- App-Crash während Import + +**Lösung:** +1. Speicherplatz freigeben +2. Backup-Datei neu erstellen +3. App neu starten und erneut importieren + +--- + +## 🔒 Sicherheit & Privacy + +### Daten-Schutz +- ✅ **Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion +- ✅ **Keine Verschlüsselung** - Klartextformat für Lesbarkeit +- ⚠️ **Sensible Daten?** - Backup-Datei selbst verschlüsseln (z.B. 7-Zip mit Passwort) + +### Empfehlungen +- 🔐 Backup-Dateien in verschlüsseltem Container speichern +- 🗑️ Alte Backups regelmäßig löschen +- 📧 Nicht per unverschlüsselter E-Mail versenden +- ☁️ Self-Hosted Cloud nutzen (Nextcloud) + +--- + +## 📊 Technische Details + +### Format-Spezifikation + +**JSON-Struktur:** +```json +{ + "version": "string", // App-Version beim Export + "exported_at": "ISO8601", // Zeitstempel des Exports + "notes_count": number, // Anzahl der Notizen + "notes": [ + { + "id": "UUID", // Eindeutige ID + "title": "string", // Notiz-Titel + "content": "string", // Notiz-Inhalt + "createdAt": number, // Unix Timestamp (ms) + "updatedAt": number // Unix Timestamp (ms) + } + ] +} +``` + +### Kompatibilität +- ✅ v1.2.0+ - Vollständig kompatibel +- ⚠️ v1.1.x - Grundfunktionen (ohne Auto-Backup) +- ❌ v1.0.x - Nicht unterstützt + +--- + +**📚 Siehe auch:** +- [QUICKSTART.md](../QUICKSTART.md) - App-Installation und Einrichtung +- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste +- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown + +**Letzte Aktualisierung:** v1.2.1 (2026-01-05) diff --git a/docs/BACKUP.en.md b/docs/BACKUP.en.md deleted file mode 100644 index 0b3bc80..0000000 --- a/docs/BACKUP.en.md +++ /dev/null @@ -1,324 +0,0 @@ -# Backup & Restore 💾 - -**🌍 Languages:** [Deutsch](BACKUP.md) · **English** - -> Secure your notes locally - independent from the server - ---- - -## 📋 Overview - -The backup system works **completely offline** and independent from the WebDAV server. Perfect for: -- 📥 Regular backups -- 📤 Migration to new server -- 🔄 Recovery after data loss -- 💾 Archiving old notes - ---- - -## 📥 Create Backup - -### Step-by-Step - -1. **Open settings** (⚙️ icon top right) -2. **Find "Backup & Restore"** section -3. **Tap "📥 Create backup"** -4. **Choose location:** - - 📁 Downloads - - 💳 SD card - - ☁️ Cloud folder (Nextcloud, Google Drive, etc.) - - 📧 Email as attachment -5. **Done!** Backup file is saved - -### File Format - -**Filename:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json` - -**Example:** `simplenotes_backup_2026-01-05_143022.json` - -**Content:** -```json -{ - "version": "1.2.1", - "exported_at": "2026-01-05T14:30:22Z", - "notes_count": 42, - "notes": [ - { - "id": "abc-123-def", - "title": "Shopping List", - "content": "Milk\nBread\nCheese", - "createdAt": 1704467422000, - "updatedAt": 1704467422000 - } - ] -} -``` - -**Format details:** -- ✅ Human-readable (formatted JSON) -- ✅ All data included (title, content, IDs, timestamps) -- ✅ Version info for compatibility -- ✅ Note count for validation - ---- - -## 📤 Restore Backup - -### 3 Restore Modes - -#### 1. Merge ⭐ _Recommended_ - -**What happens:** -- ✅ New notes from backup are added -- ✅ Existing notes remain unchanged -- ✅ No data loss - -**When to use:** -- Import backup from another device -- Recover old notes -- Restore accidentally deleted notes - -**Example:** -``` -App: [Note A, Note B, Note C] -Backup: [Note A, Note D, Note E] -Result: [Note A, Note B, Note C, Note D, Note E] -``` - -#### 2. Replace - -**What happens:** -- ❌ ALL existing notes are deleted -- ✅ Backup notes are imported -- ⚠️ Irreversible (except through auto-backup) - -**When to use:** -- Server migration (complete restart) -- Return to old backup state -- App reinstallation - -**Example:** -``` -App: [Note A, Note B, Note C] -Backup: [Note X, Note Y] -Result: [Note X, Note Y] -``` - -**⚠️ Warning:** Automatic safety backup is created! - -#### 3. Overwrite Duplicates - -**What happens:** -- ✅ New notes from backup are added -- 🔄 On ID conflicts, backup wins -- ✅ Other notes remain unchanged - -**When to use:** -- Backup is newer than app data -- Import desktop changes -- Conflict resolution - -**Example:** -``` -App: [Note A (v1), Note B, Note C] -Backup: [Note A (v2), Note D] -Result: [Note A (v2), Note B, Note C, Note D] -``` - -### Restore Process - -1. **Settings** → **"📤 Restore from file"** -2. **Select backup file** (`.json`) -3. **Choose mode:** - - 🔵 Merge _(Default)_ - - 🟡 Overwrite duplicates - - 🔴 Replace _(Caution!)_ -4. **Confirm** - Automatic safety backup is created -5. **Wait** - Import runs -6. **Done!** - Success message with number of imported notes - ---- - -## 🛡️ Automatic Safety Backup - -**Before every restore:** -- ✅ Automatic backup is created -- 📁 Saved in: `Android/data/dev.dettmer.simplenotes/files/` -- 🏷️ Filename: `auto_backup_before_restore_YYYY-MM-DD_HHmmss.json` -- ⏱️ Timestamp: Right before restore - -**Why?** -- Protection against accidental "Replace" -- Ability to undo -- Double security - -**Access via file manager:** -``` -/Android/data/dev.dettmer.simplenotes/files/auto_backup_before_restore_*.json -``` - ---- - -## 💡 Best Practices - -### Backup Strategy - -#### Regular Backups -``` -Daily: ❌ Too often (server sync is enough) -Weekly: ✅ Recommended for important notes -Monthly: ✅ Archiving -Before updates: ✅ Safety -``` - -#### 3-2-1 Rule -1. **3 copies** - Original + 2 backups -2. **2 media** - e.g., SD card + cloud -3. **1 offsite** - e.g., cloud storage - -### Backup Locations - -**Local (fast):** -- 📱 Internal storage / Downloads -- 💳 SD card -- 🖥️ PC (via USB) - -**Cloud (secure):** -- ☁️ Nextcloud (self-hosted) -- 📧 Email to yourself -- 🗄️ Syncthing (sync between devices) - -**⚠️ Avoid:** -- ❌ Google Drive / Dropbox (privacy) -- ❌ Only one copy -- ❌ Only on server (if server fails) - ---- - -## 🔧 Advanced Usage - -### Edit Backup File - -The `.json` file can be edited with any text editor: - -1. **Open with:** VS Code, Notepad++, nano -2. **Add/remove notes** -3. **Change title/content** -4. **Adjust IDs** (for migration) -5. **Save** and import to app - -**⚠️ Important:** -- Keep valid JSON format -- IDs must be unique (UUIDs) -- Timestamps in milliseconds (Unix Epoch) - -### Bulk Import - -Merge multiple backups: - -1. Import backup 1 (Mode: Merge) -2. Import backup 2 (Mode: Merge) -3. Import backup 3 (Mode: Merge) -4. Result: All notes combined - -### Server Migration - -Step-by-step: - -1. **Create backup** on old server -2. **Set up new server** (see [QUICKSTART.en.md](QUICKSTART.en.md)) -3. **Change server URL** in app settings -4. **Restore backup** (Mode: Replace) -5. **Test sync** - All notes on new server - ---- - -## ❌ Troubleshooting - -### "Invalid backup file" - -**Causes:** -- Corrupt JSON file -- Wrong file extension (must be `.json`) -- Incompatible app version - -**Solution:** -1. Check JSON file with validator (e.g., jsonlint.com) -2. Verify file extension -3. Create backup with current app version - -### "No permission to save" - -**Causes:** -- Storage permission missing -- Write-protected folder - -**Solution:** -1. Android: Settings → Apps → Simple Notes → Permissions -2. Activate "Storage" -3. Choose different location - -### "Import failed" - -**Causes:** -- Not enough storage space -- Corrupt backup file -- App crash during import - -**Solution:** -1. Free up storage space -2. Create new backup file -3. Restart app and try again - ---- - -## 🔒 Security & Privacy - -### Data Protection -- ✅ **Locally stored** - No cloud upload without your action -- ✅ **No encryption** - Plain text format for readability -- ⚠️ **Sensitive data?** - Encrypt backup file yourself (e.g., 7-Zip with password) - -### Recommendations -- 🔐 Store backup files in encrypted container -- 🗑️ Regularly delete old backups -- 📧 Don't send via unencrypted email -- ☁️ Use self-hosted cloud (Nextcloud) - ---- - -## 📊 Technical Details - -### Format Specification - -**JSON structure:** -```json -{ - "version": "string", // App version at export - "exported_at": "ISO8601", // Export timestamp - "notes_count": number, // Number of notes - "notes": [ - { - "id": "UUID", // Unique ID - "title": "string", // Note title - "content": "string", // Note content - "createdAt": number, // Unix timestamp (ms) - "updatedAt": number // Unix timestamp (ms) - } - ] -} -``` - -### Compatibility -- ✅ v1.2.0+ - Fully compatible -- ⚠️ v1.1.x - Basic functions (without auto-backup) -- ❌ v1.0.x - Not supported - ---- - -**📚 See also:** -- [QUICKSTART.en.md](../QUICKSTART.en.md) - App installation and setup -- [FEATURES.en.md](FEATURES.en.md) - Complete feature list -- [DESKTOP.en.md](DESKTOP.en.md) - Desktop integration with Markdown - -**Last update:** v1.2.1 (2026-01-05) diff --git a/docs/BACKUP.md b/docs/BACKUP.md index 8aa54da..13123c8 100644 --- a/docs/BACKUP.md +++ b/docs/BACKUP.md @@ -1,42 +1,42 @@ -# Backup & Wiederherstellung 💾 +# Backup & Restore 💾 -**🌍 Languages:** **Deutsch** · [English](BACKUP.en.md) +**🌍 Languages:** [Deutsch](BACKUP.de.md) · **English** -> Sichere deine Notizen lokal - unabhängig vom Server +> Secure your notes locally - independent from the server --- -## 📋 Übersicht +## 📋 Overview -Das Backup-System funktioniert **komplett offline** und unabhängig vom WebDAV-Server. Perfekt für: -- 📥 Regelmäßige Sicherungen -- 📤 Migration zu neuem Server -- 🔄 Wiederherstellung nach Datenverlust -- 💾 Archivierung alter Notizen +The backup system works **completely offline** and independent from the WebDAV server. Perfect for: +- 📥 Regular backups +- 📤 Migration to new server +- 🔄 Recovery after data loss +- 💾 Archiving old notes --- -## 📥 Backup erstellen +## 📥 Create Backup -### Schritt-für-Schritt +### Step-by-Step -1. **Einstellungen öffnen** (⚙️ Icon oben rechts) -2. **"Backup & Wiederherstellung"** Section finden -3. **"📥 Backup erstellen"** antippen -4. **Speicherort wählen:** +1. **Open settings** (⚙️ icon top right) +2. **Find "Backup & Restore"** section +3. **Tap "📥 Create backup"** +4. **Choose location:** - 📁 Downloads - - 💳 SD-Karte - - ☁️ Cloud-Ordner (Nextcloud, Google Drive, etc.) - - 📧 E-Mail als Anhang -5. **Fertig!** Backup-Datei ist gespeichert + - 💳 SD card + - ☁️ Cloud folder (Nextcloud, Google Drive, etc.) + - 📧 Email as attachment +5. **Done!** Backup file is saved -### Dateiformat +### File Format -**Dateiname:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json` +**Filename:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json` -**Beispiel:** `simplenotes_backup_2026-01-05_143022.json` +**Example:** `simplenotes_backup_2026-01-05_143022.json` -**Inhalt:** +**Content:** ```json { "version": "1.2.1", @@ -45,8 +45,8 @@ Das Backup-System funktioniert **komplett offline** und unabhängig vom WebDAV-S "notes": [ { "id": "abc-123-def", - "title": "Einkaufsliste", - "content": "Milch\nBrot\nKäse", + "title": "Shopping List", + "content": "Milk\nBread\nCheese", "createdAt": 1704467422000, "updatedAt": 1704467422000 } @@ -54,105 +54,105 @@ Das Backup-System funktioniert **komplett offline** und unabhängig vom WebDAV-S } ``` -**Format-Details:** -- ✅ Menschenlesbar (formatiertes JSON) -- ✅ Alle Daten inklusive (Titel, Inhalt, IDs, Timestamps) -- ✅ Versions-Info für Kompatibilität -- ✅ Anzahl der Notizen für Validierung +**Format details:** +- ✅ Human-readable (formatted JSON) +- ✅ All data included (title, content, IDs, timestamps) +- ✅ Version info for compatibility +- ✅ Note count for validation --- -## 📤 Backup wiederherstellen +## 📤 Restore Backup -### 3 Wiederherstellungs-Modi +### 3 Restore Modes -#### 1. Zusammenführen (Merge) ⭐ _Empfohlen_ +#### 1. Merge ⭐ _Recommended_ -**Was passiert:** -- ✅ Neue Notizen aus Backup werden hinzugefügt -- ✅ Bestehende Notizen bleiben unverändert -- ✅ Keine Datenverluste +**What happens:** +- ✅ New notes from backup are added +- ✅ Existing notes remain unchanged +- ✅ No data loss -**Wann nutzen:** -- Backup von anderem Gerät einspielen -- Alte Notizen zurückholen -- Versehentlich gelöschte Notizen wiederherstellen +**When to use:** +- Import backup from another device +- Recover old notes +- Restore accidentally deleted notes -**Beispiel:** +**Example:** ``` -App: [Notiz A, Notiz B, Notiz C] -Backup: [Notiz A, Notiz D, Notiz E] -Ergebnis: [Notiz A, Notiz B, Notiz C, Notiz D, Notiz E] +App: [Note A, Note B, Note C] +Backup: [Note A, Note D, Note E] +Result: [Note A, Note B, Note C, Note D, Note E] ``` -#### 2. Ersetzen (Replace) +#### 2. Replace -**Was passiert:** -- ❌ ALLE bestehenden Notizen werden gelöscht -- ✅ Backup-Notizen werden importiert -- ⚠️ Unwiderruflich (außer durch Auto-Backup) +**What happens:** +- ❌ ALL existing notes are deleted +- ✅ Backup notes are imported +- ⚠️ Irreversible (except through auto-backup) -**Wann nutzen:** -- Server-Wechsel (kompletter Neustart) -- Zurück zu altem Backup-Stand -- App-Neuinstallation +**When to use:** +- Server migration (complete restart) +- Return to old backup state +- App reinstallation -**Beispiel:** +**Example:** ``` -App: [Notiz A, Notiz B, Notiz C] -Backup: [Notiz X, Notiz Y] -Ergebnis: [Notiz X, Notiz Y] +App: [Note A, Note B, Note C] +Backup: [Note X, Note Y] +Result: [Note X, Note Y] ``` -**⚠️ Warnung:** Automatisches Sicherheits-Backup wird erstellt! +**⚠️ Warning:** Automatic safety backup is created! -#### 3. Duplikate überschreiben (Overwrite) +#### 3. Overwrite Duplicates -**Was passiert:** -- ✅ Neue Notizen aus Backup werden hinzugefügt -- 🔄 Bei ID-Konflikten gewinnt das Backup -- ✅ Andere Notizen bleiben unverändert +**What happens:** +- ✅ New notes from backup are added +- 🔄 On ID conflicts, backup wins +- ✅ Other notes remain unchanged -**Wann nutzen:** -- Backup ist neuer als App-Daten -- Desktop-Änderungen einspielen -- Konflikt-Auflösung +**When to use:** +- Backup is newer than app data +- Import desktop changes +- Conflict resolution -**Beispiel:** +**Example:** ``` -App: [Notiz A (v1), Notiz B, Notiz C] -Backup: [Notiz A (v2), Notiz D] -Ergebnis: [Notiz A (v2), Notiz B, Notiz C, Notiz D] +App: [Note A (v1), Note B, Note C] +Backup: [Note A (v2), Note D] +Result: [Note A (v2), Note B, Note C, Note D] ``` -### Wiederherstellungs-Prozess +### Restore Process -1. **Einstellungen** → **"📤 Aus Datei wiederherstellen"** -2. **Backup-Datei auswählen** (`.json`) -3. **Modus wählen:** - - 🔵 Zusammenführen _(Standard)_ - - 🟡 Duplikate überschreiben - - 🔴 Ersetzen _(Vorsicht!)_ -4. **Bestätigen** - Automatisches Sicherheits-Backup wird erstellt -5. **Warten** - Import läuft -6. **Fertig!** - Erfolgsmeldung mit Anzahl importierter Notizen +1. **Settings** → **"📤 Restore from file"** +2. **Select backup file** (`.json`) +3. **Choose mode:** + - 🔵 Merge _(Default)_ + - 🟡 Overwrite duplicates + - 🔴 Replace _(Caution!)_ +4. **Confirm** - Automatic safety backup is created +5. **Wait** - Import runs +6. **Done!** - Success message with number of imported notes --- -## 🛡️ Automatisches Sicherheits-Backup +## 🛡️ Automatic Safety Backup -**Vor jeder Wiederherstellung:** -- ✅ Automatisches Backup wird erstellt -- 📁 Gespeichert in: `Android/data/dev.dettmer.simplenotes/files/` -- 🏷️ Dateiname: `auto_backup_before_restore_YYYY-MM-DD_HHmmss.json` -- ⏱️ Zeitstempel: Direkt vor Wiederherstellung +**Before every restore:** +- ✅ Automatic backup is created +- 📁 Saved in: `Android/data/dev.dettmer.simplenotes/files/` +- 🏷️ Filename: `auto_backup_before_restore_YYYY-MM-DD_HHmmss.json` +- ⏱️ Timestamp: Right before restore -**Warum?** -- Schutz vor versehentlichem "Ersetzen" -- Möglichkeit zum Rückgängigmachen -- Doppelte Sicherheit +**Why?** +- Protection against accidental "Replace" +- Ability to undo +- Double security -**Zugriff via Dateimanager:** +**Access via file manager:** ``` /Android/data/dev.dettmer.simplenotes/files/auto_backup_before_restore_*.json ``` @@ -161,164 +161,164 @@ Ergebnis: [Notiz A (v2), Notiz B, Notiz C, Notiz D] ## 💡 Best Practices -### Backup-Strategie +### Backup Strategy -#### Regelmäßige Backups +#### Regular Backups ``` -Täglich: ❌ Zu oft (Server-Sync reicht) -Wöchentlich: ✅ Empfohlen für wichtige Notizen -Monatlich: ✅ Archivierung -Vor Updates: ✅ Sicherheit +Daily: ❌ Too often (server sync is enough) +Weekly: ✅ Recommended for important notes +Monthly: ✅ Archiving +Before updates: ✅ Safety ``` -#### 3-2-1 Regel -1. **3 Kopien** - Original + 2 Backups -2. **2 Medien** - z.B. SD-Karte + Cloud -3. **1 Offsite** - z.B. Cloud-Speicher +#### 3-2-1 Rule +1. **3 copies** - Original + 2 backups +2. **2 media** - e.g., SD card + cloud +3. **1 offsite** - e.g., cloud storage -### Backup-Speicherorte +### Backup Locations -**Lokal (schnell):** -- 📱 Internal Storage / Downloads -- 💳 SD-Karte +**Local (fast):** +- 📱 Internal storage / Downloads +- 💳 SD card - 🖥️ PC (via USB) -**Cloud (sicher):** -- ☁️ Nextcloud (Self-Hosted) -- 📧 E-Mail an sich selbst -- 🗄️ Syncthing (Sync zwischen Geräten) +**Cloud (secure):** +- ☁️ Nextcloud (self-hosted) +- 📧 Email to yourself +- 🗄️ Syncthing (sync between devices) -**⚠️ Vermeiden:** -- ❌ Google Drive / Dropbox (Privacy) -- ❌ Nur eine Kopie -- ❌ Nur auf Server (wenn Server ausfällt) +**⚠️ Avoid:** +- ❌ Google Drive / Dropbox (privacy) +- ❌ Only one copy +- ❌ Only on server (if server fails) --- -## 🔧 Erweiterte Nutzung +## 🔧 Advanced Usage -### Backup-Datei bearbeiten +### Edit Backup File -Die `.json` Datei kann mit jedem Texteditor bearbeitet werden: +The `.json` file can be edited with any text editor: -1. **Öffnen mit:** VS Code, Notepad++, nano -2. **Notizen hinzufügen/entfernen** -3. **Titel/Inhalt ändern** -4. **IDs anpassen** (für Migration) -5. **Speichern** und in App importieren +1. **Open with:** VS Code, Notepad++, nano +2. **Add/remove notes** +3. **Change title/content** +4. **Adjust IDs** (for migration) +5. **Save** and import to app -**⚠️ Wichtig:** -- Valides JSON-Format behalten -- IDs müssen eindeutig sein (UUIDs) -- Timestamps in Millisekunden (Unix Epoch) +**⚠️ Important:** +- Keep valid JSON format +- IDs must be unique (UUIDs) +- Timestamps in milliseconds (Unix Epoch) -### Bulk-Import +### Bulk Import -Mehrere Backups zusammenführen: +Merge multiple backups: -1. Backup 1 importieren (Modus: Zusammenführen) -2. Backup 2 importieren (Modus: Zusammenführen) -3. Backup 3 importieren (Modus: Zusammenführen) -4. Ergebnis: Alle Notizen vereint +1. Import backup 1 (Mode: Merge) +2. Import backup 2 (Mode: Merge) +3. Import backup 3 (Mode: Merge) +4. Result: All notes combined -### Server-Migration +### Server Migration -Schritt-für-Schritt: +Step-by-step: -1. **Backup erstellen** auf altem Server -2. **Neuen Server einrichten** (siehe [QUICKSTART.md](QUICKSTART.md)) -3. **Server-URL ändern** in App-Einstellungen -4. **Backup wiederherstellen** (Modus: Ersetzen) -5. **Sync testen** - Alle Notizen auf neuem Server +1. **Create backup** on old server +2. **Set up new server** (see [QUICKSTART.en.md](QUICKSTART.en.md)) +3. **Change server URL** in app settings +4. **Restore backup** (Mode: Replace) +5. **Test sync** - All notes on new server --- -## ❌ Fehlerbehebung +## ❌ Troubleshooting -### "Backup-Datei ungültig" +### "Invalid backup file" -**Ursachen:** -- Korrupte JSON-Datei -- Falsche Datei-Endung (muss `.json` sein) -- Inkompatible App-Version +**Causes:** +- Corrupt JSON file +- Wrong file extension (must be `.json`) +- Incompatible app version -**Lösung:** -1. JSON-Datei mit Validator prüfen (z.B. jsonlint.com) -2. Dateiendung überprüfen -3. Backup mit aktueller App-Version erstellen +**Solution:** +1. Check JSON file with validator (e.g., jsonlint.com) +2. Verify file extension +3. Create backup with current app version -### "Keine Berechtigung zum Speichern" +### "No permission to save" -**Ursachen:** -- Speicher-Berechtigung fehlt -- Schreibgeschützter Ordner +**Causes:** +- Storage permission missing +- Write-protected folder -**Lösung:** -1. Android: Einstellungen → Apps → Simple Notes → Berechtigungen -2. "Speicher" aktivieren -3. Anderen Speicherort wählen +**Solution:** +1. Android: Settings → Apps → Simple Notes → Permissions +2. Activate "Storage" +3. Choose different location -### "Import fehlgeschlagen" +### "Import failed" -**Ursachen:** -- Zu wenig Speicherplatz -- Korrupte Backup-Datei -- App-Crash während Import +**Causes:** +- Not enough storage space +- Corrupt backup file +- App crash during import -**Lösung:** -1. Speicherplatz freigeben -2. Backup-Datei neu erstellen -3. App neu starten und erneut importieren +**Solution:** +1. Free up storage space +2. Create new backup file +3. Restart app and try again --- -## 🔒 Sicherheit & Privacy +## 🔒 Security & Privacy -### Daten-Schutz -- ✅ **Lokal gespeichert** - Kein Cloud-Upload ohne deine Aktion -- ✅ **Keine Verschlüsselung** - Klartextformat für Lesbarkeit -- ⚠️ **Sensible Daten?** - Backup-Datei selbst verschlüsseln (z.B. 7-Zip mit Passwort) +### Data Protection +- ✅ **Locally stored** - No cloud upload without your action +- ✅ **No encryption** - Plain text format for readability +- ⚠️ **Sensitive data?** - Encrypt backup file yourself (e.g., 7-Zip with password) -### Empfehlungen -- 🔐 Backup-Dateien in verschlüsseltem Container speichern -- 🗑️ Alte Backups regelmäßig löschen -- 📧 Nicht per unverschlüsselter E-Mail versenden -- ☁️ Self-Hosted Cloud nutzen (Nextcloud) +### Recommendations +- 🔐 Store backup files in encrypted container +- 🗑️ Regularly delete old backups +- 📧 Don't send via unencrypted email +- ☁️ Use self-hosted cloud (Nextcloud) --- -## 📊 Technische Details +## 📊 Technical Details -### Format-Spezifikation +### Format Specification -**JSON-Struktur:** +**JSON structure:** ```json { - "version": "string", // App-Version beim Export - "exported_at": "ISO8601", // Zeitstempel des Exports - "notes_count": number, // Anzahl der Notizen + "version": "string", // App version at export + "exported_at": "ISO8601", // Export timestamp + "notes_count": number, // Number of notes "notes": [ { - "id": "UUID", // Eindeutige ID - "title": "string", // Notiz-Titel - "content": "string", // Notiz-Inhalt - "createdAt": number, // Unix Timestamp (ms) - "updatedAt": number // Unix Timestamp (ms) + "id": "UUID", // Unique ID + "title": "string", // Note title + "content": "string", // Note content + "createdAt": number, // Unix timestamp (ms) + "updatedAt": number // Unix timestamp (ms) } ] } ``` -### Kompatibilität -- ✅ v1.2.0+ - Vollständig kompatibel -- ⚠️ v1.1.x - Grundfunktionen (ohne Auto-Backup) -- ❌ v1.0.x - Nicht unterstützt +### Compatibility +- ✅ v1.2.0+ - Fully compatible +- ⚠️ v1.1.x - Basic functions (without auto-backup) +- ❌ v1.0.x - Not supported --- -**📚 Siehe auch:** -- [QUICKSTART.md](../QUICKSTART.md) - App-Installation und Einrichtung -- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste -- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown +**📚 See also:** +- [QUICKSTART.en.md](../QUICKSTART.en.md) - App installation and setup +- [FEATURES.en.md](FEATURES.en.md) - Complete feature list +- [DESKTOP.en.md](DESKTOP.en.md) - Desktop integration with Markdown -**Letzte Aktualisierung:** v1.2.1 (2026-01-05) +**Last update:** v1.2.1 (2026-01-05) diff --git a/docs/DESKTOP.de.md b/docs/DESKTOP.de.md new file mode 100644 index 0000000..6d90a20 --- /dev/null +++ b/docs/DESKTOP.de.md @@ -0,0 +1,505 @@ +# Desktop-Integration 🖥️ + +**🌍 Sprachen:** **Deutsch** · [English](DESKTOP.md) + +> Bearbeite deine Notizen mit jedem Markdown-Editor auf dem Desktop + +--- + +## 📋 Übersicht + +Die Desktop-Integration ermöglicht dir, Notizen auf dem PC/Mac zu bearbeiten: +- 📝 Jeder Markdown-Editor funktioniert +- 🔄 Automatische Synchronisation über WebDAV +- 💾 Dual-Format: JSON (Master) + Markdown (Mirror) +- ⚡ Last-Write-Wins Konfliktauflösung + +--- + +## 🎯 Warum Markdown? + +### Dual-Format Architektur + +``` +┌─────────────────────────────────────┐ +│ Android App │ +│ │ +│ ┌──────────┐ ┌─────────────┐ │ +│ │ JSON │ ──→ │ Markdown │ │ +│ │ (Master) │ │ (Mirror) │ │ +│ └──────────┘ └─────────────┘ │ +└────────┬────────────────┬───────────┘ + │ │ + ↓ ↓ + WebDAV Server + │ │ + ┌────┴────┐ ┌────┴──────┐ + │ /notes/ │ │ /notes-md/│ + │ *.json │ │ *.md │ + └─────────┘ └───────────┘ + ↑ ↑ + │ │ + ┌────┴────────────────┴───────────┐ + │ Desktop Editor │ + │ (VS Code, Typora, etc.) │ + └──────────────────────────────────┘ +``` + +### Vorteile + +**JSON (Master):** +- ✅ Zuverlässig und schnell +- ✅ Strukturierte Daten (IDs, Timestamps) +- ✅ Primärer Sync-Mechanismus +- ✅ Immer aktiv + +**Markdown (Mirror):** +- ✅ Menschenlesbar +- ✅ Desktop-Editor kompatibel +- ✅ Syntax-Highlighting +- ✅ Optional aktivierbar + +--- + +## 🚀 Schnellstart + +### 1. Erste Synchronisation + +**Wichtig:** Führe ZUERST einen Sync durch, bevor du Desktop-Integration aktivierst! + +1. **App einrichten** (siehe [QUICKSTART.md](QUICKSTART.md)) +2. **Server-Verbindung testen** +3. **Erste Notiz erstellen** +4. **Synchronisieren** (Pull-to-Refresh oder Auto-Sync) +5. ✅ Server erstellt automatisch `/notes/` und `/notes-md/` Ordner + +### 2. Desktop-Integration aktivieren + +1. **Einstellungen** → **Desktop-Integration** +2. **Toggle aktivieren** +3. **Initial Export startet** - Zeigt Progress (X/Y) +4. ✅ Alle bestehenden Notizen werden als `.md` exportiert + +### 3. WebDAV als Netzlaufwerk mounten + +#### Windows + +``` +1. Explorer öffnen +2. Rechtsklick auf "Dieser PC" +3. "Netzlaufwerk verbinden" +4. URL eingeben: http://DEIN-SERVER:8080/notes-md/ +5. Benutzername: noteuser +6. Passwort: (dein WebDAV-Passwort) +7. Laufwerksbuchstabe: Z:\ (oder beliebig) +8. Fertig! +``` + +**Zugriff:** `Z:\` im Explorer + +#### macOS + +``` +1. Finder öffnen +2. Menü "Gehe zu" → "Mit Server verbinden" (⌘K) +3. Server-Adresse: http://DEIN-SERVER:8080/notes-md/ +4. Verbinden +5. Benutzername: noteuser +6. Passwort: (dein WebDAV-Passwort) +7. Fertig! +``` + +**Zugriff:** Finder → Netzwerk → notes-md + +#### Linux (GNOME) + +``` +1. Files / Nautilus öffnen +2. "Andere Orte" +3. "Mit Server verbinden" +4. Server-Adresse: dav://DEIN-SERVER:8080/notes-md/ +5. Benutzername: noteuser +6. Passwort: (dein WebDAV-Passwort) +7. Fertig! +``` + +**Zugriff:** `/run/user/1000/gvfs/dav:host=...` + +#### Linux (davfs2 - permanent) + +```bash +# Installation +sudo apt install davfs2 + +# Mount-Point erstellen +sudo mkdir -p /mnt/notes-md + +# Einmalig mounten +sudo mount -t davfs http://DEIN-SERVER:8080/notes-md/ /mnt/notes-md + +# Permanent in /etc/fstab +echo "http://DEIN-SERVER:8080/notes-md/ /mnt/notes-md davfs rw,user,noauto 0 0" | sudo tee -a /etc/fstab +``` + +**Zugriff:** `/mnt/notes-md/` + +--- + +## 📝 Markdown-Editoren + +### Empfohlene Editoren + +#### 1. VS Code ⭐ _Empfohlen_ + +**Vorteile:** +- ✅ Kostenlos & Open Source +- ✅ Markdown-Preview (Ctrl+Shift+V) +- ✅ Syntax-Highlighting +- ✅ Git-Integration +- ✅ Erweiterungen (Spell Check, etc.) + +**Setup:** +``` +1. VS Code installieren +2. WebDAV-Laufwerk mounten +3. Ordner öffnen: Z:\notes-md\ (Windows) oder /mnt/notes-md (Linux) +4. Fertig! Markdown-Dateien bearbeiten +``` + +**Extensions (optional):** +- `Markdown All in One` - Shortcuts & Preview +- `Markdown Preview Enhanced` - Bessere Preview +- `Code Spell Checker` - Rechtschreibprüfung + +#### 2. Typora + +**Vorteile:** +- ✅ WYSIWYG Markdown-Editor +- ✅ Minimalistisches Design +- ✅ Live-Preview +- ⚠️ Kostenpflichtig (~15€) + +**Setup:** +``` +1. Typora installieren +2. WebDAV mounten +3. Ordner in Typora öffnen +4. Notizen bearbeiten +``` + +#### 3. Notepad++ + +**Vorteile:** +- ✅ Leichtgewichtig +- ✅ Schnell +- ✅ Syntax-Highlighting +- ⚠️ Keine Markdown-Preview + +**Setup:** +``` +1. Notepad++ installieren +2. WebDAV mounten +3. Dateien direkt öffnen +``` + +#### 4. Obsidian + +**Vorteile:** +- ✅ Zweite Gehirn-Philosophie +- ✅ Graph-View für Verlinkungen +- ✅ Viele Plugins +- ⚠️ Sync-Konflikte möglich (2 Master) + +**Setup:** +``` +1. Obsidian installieren +2. WebDAV als Vault öffnen +3. Vorsicht: Obsidian erstellt eigene Metadaten! +``` + +**⚠️ Nicht empfohlen:** Kann Frontmatter verändern + +--- + +## 📄 Markdown-Dateiformat + +### Struktur + +Jede Notiz wird als `.md` Datei mit YAML-Frontmatter exportiert: + +```markdown +--- +id: abc-123-def-456 +created: 2026-01-05T14:30:22Z +updated: 2026-01-05T14:30:22Z +tags: [] +--- + +# Notiz-Titel + +Notiz-Inhalt hier... +``` + +### Frontmatter-Felder + +| Feld | Typ | Beschreibung | Pflicht | +|------|-----|--------------|---------| +| `id` | UUID | Eindeutige Notiz-ID | ✅ Ja | +| `created` | ISO8601 | Erstellungsdatum | ✅ Ja | +| `updated` | ISO8601 | Änderungsdatum | ✅ Ja | +| `tags` | Array | Tags (zukünftig) | ❌ Nein | + +### Dateinamen + +**Sanitization-Regeln:** +``` +Titel: "Meine Einkaufsliste 🛒" +→ Dateiname: "Meine_Einkaufsliste.md" + +Entfernt werden: +- Emojis: 🛒 → entfernt +- Sonderzeichen: / \ : * ? " < > | → entfernt +- Mehrfache Leerzeichen → einzelnes Leerzeichen +- Leerzeichen → Unterstrich _ +``` + +**Beispiele:** +``` +"Meeting Notes 2026" → "Meeting_Notes_2026.md" +"To-Do: Projekt" → "To-Do_Projekt.md" +"Urlaub ☀️" → "Urlaub.md" +``` + +--- + +## 🔄 Synchronisation + +### Workflow: Android → Desktop + +1. **Notiz in App erstellen/bearbeiten** +2. **Sync ausführen** (Auto oder manuell) +3. **JSON wird hochgeladen** (`/notes/abc-123.json`) +4. **Markdown wird exportiert** (`/notes-md/Notiz_Titel.md`) _(nur wenn Desktop-Integration AN)_ +5. **Desktop-Editor zeigt Änderungen** (nach Refresh) + +### Workflow: Desktop → Android + +1. **Markdown-Datei bearbeiten** (im gemounteten Ordner) +2. **Speichern** - Datei liegt sofort auf Server +3. **In App: Markdown-Import ausführen** + - Einstellungen → "Import Markdown Changes" + - Oder: Auto-Import bei jedem Sync (zukünftig) +4. **App übernimmt Änderungen** (wenn Desktop-Version neuer) + +### Konfliktauflösung: Last-Write-Wins + +**Regel:** Neueste Version (nach `updated` Timestamp) gewinnt + +**Beispiel:** +``` +App-Version: updated: 2026-01-05 14:00 +Desktop-Version: updated: 2026-01-05 14:30 +→ Desktop gewinnt (neuerer Timestamp) +``` + +**Automatisch:** +- ✅ Beim Markdown-Import +- ✅ Beim JSON-Sync +- ⚠️ Keine Merge-Konflikte - nur komplettes Überschreiben + +--- + +## ⚙️ Einstellungen + +### Desktop-Integration Toggle + +**Einstellungen → Desktop-Integration** + +**AN (aktiviert):** +- ✅ Neue Notizen → automatisch als `.md` exportiert +- ✅ Aktualisierte Notizen → `.md` Update +- ✅ Gelöschte Notizen → `.md` bleibt (zukünftig: auch löschen) + +**AUS (deaktiviert):** +- ❌ Kein Markdown-Export +- ✅ JSON-Sync läuft normal weiter +- ✅ Bestehende `.md` Dateien bleiben erhalten + +### Initial Export + +**Was passiert beim Aktivieren:** +1. Alle bestehenden Notizen werden gescannt +2. Progress-Dialog zeigt Fortschritt (z.B. "23/42") +3. Jede Notiz wird als `.md` exportiert +4. Bei Fehlern: Einzelne Notiz wird übersprungen +5. Erfolgsmeldung mit Anzahl exportierter Notizen + +**Zeit:** ~1-2 Sekunden pro 50 Notizen + +--- + +## 🛠️ Erweiterte Nutzung + +### Manuelle Markdown-Erstellung + +Du kannst `.md` Dateien manuell erstellen: + +```markdown +--- +id: 00000000-0000-0000-0000-000000000001 +created: 2026-01-05T12:00:00Z +updated: 2026-01-05T12:00:00Z +--- + +# Neue Desktop-Notiz + +Inhalt hier... +``` + +**⚠️ Wichtig:** +- `id` muss gültige UUID sein (z.B. mit uuidgen.io) +- Timestamps in ISO8601-Format +- Frontmatter mit `---` umschließen + +### Bulk-Operations + +**Mehrere Notizen auf einmal bearbeiten:** + +1. WebDAV mounten +2. Alle `.md` Dateien in VS Code öffnen +3. Suchen & Ersetzen über alle Dateien (Ctrl+Shift+H) +4. Speichern +5. In App: "Import Markdown Changes" + +### Scripting + +**Beispiel: Alle Notizen nach Datum sortieren** + +```bash +#!/bin/bash +cd /mnt/notes-md/ + +# Alle .md Dateien nach Update-Datum sortieren +for file in *.md; do + updated=$(grep "^updated:" "$file" | cut -d' ' -f2) + echo "$updated $file" +done | sort +``` + +--- + +## ❌ Fehlerbehebung + +### "404 Not Found" beim WebDAV-Mount + +**Ursache:** `/notes-md/` Ordner existiert nicht + +**Lösung:** +1. **Erste Sync durchführen** - Ordner wird automatisch erstellt +2. ODER: Manuell erstellen via Terminal: + ```bash + curl -X MKCOL -u noteuser:password http://server:8080/notes-md/ + ``` + +### Markdown-Dateien erscheinen nicht + +**Ursache:** Desktop-Integration nicht aktiviert + +**Lösung:** +1. Einstellungen → "Desktop-Integration" AN +2. Warten auf Initial Export +3. WebDAV-Ordner refreshen + +### Änderungen vom Desktop erscheinen nicht in App + +**Ursache:** Markdown-Import nicht ausgeführt + +**Lösung:** +1. Einstellungen → "Import Markdown Changes" +2. ODER: Auto-Sync abwarten (zukünftiges Feature) + +### "Frontmatter fehlt" Fehler + +**Ursache:** `.md` Datei ohne gültiges YAML-Frontmatter + +**Lösung:** +1. Datei in Editor öffnen +2. Frontmatter am Anfang hinzufügen: + ```yaml + --- + id: NEUE-UUID-HIER + created: 2026-01-05T12:00:00Z + updated: 2026-01-05T12:00:00Z + --- + ``` +3. Speichern und erneut importieren + +--- + +## 🔒 Sicherheit & Best Practices + +### Do's ✅ + +- ✅ **Backup vor Bulk-Edits** - Lokales Backup erstellen +- ✅ **Ein Editor zur Zeit** - Nicht parallel in App UND Desktop bearbeiten +- ✅ **Sync abwarten** - Vor Desktop-Bearbeitung Sync durchführen +- ✅ **Frontmatter respektieren** - Nicht manuell ändern (außer du weißt was du tust) + +### Don'ts ❌ + +- ❌ **Parallel bearbeiten** - App und Desktop gleichzeitig → Konflikte +- ❌ **Frontmatter löschen** - Notiz kann nicht mehr importiert werden +- ❌ **IDs ändern** - Notiz wird als neue erkannt +- ❌ **Timestamps manipulieren** - Konfliktauflösung funktioniert nicht + +### Empfohlener Workflow + +``` +1. Sync in App (Pull-to-Refresh) +2. Desktop öffnen +3. Änderungen machen +4. Speichern +5. In App: "Import Markdown Changes" +6. Überprüfen +7. Weiteren Sync durchführen +``` + +--- + +## 📊 Vergleich: JSON vs Markdown + +| Aspekt | JSON | Markdown | +|--------|------|----------| +| **Format** | Strukturiert | Fließtext | +| **Lesbarkeit (Mensch)** | ⚠️ Mittel | ✅ Gut | +| **Lesbarkeit (Maschine)** | ✅ Perfekt | ⚠️ Parsing nötig | +| **Metadata** | Native | Frontmatter | +| **Editoren** | Code-Editoren | Alle Text-Editoren | +| **Sync-Geschwindigkeit** | ✅ Schnell | ⚠️ Langsamer | +| **Zuverlässigkeit** | ✅ 100% | ⚠️ Frontmatter-Fehler möglich | +| **Mobile-First** | ✅ Ja | ❌ Nein | +| **Desktop-First** | ❌ Nein | ✅ Ja | + +**Fazit:** Beide Formate nutzen = Beste Erfahrung auf beiden Plattformen! + +--- + +## 🔮 Zukünftige Features + +Geplant für v1.3.0+: + +- ⏳ **Auto-Markdown-Import** - Bei jedem Sync automatisch +- ⏳ **Bidirektionaler Sync** - Ohne manuellen Import +- ⏳ **Markdown-Vorschau** - In der App +- ⏳ **Konflikts-UI** - Bei gleichzeitigen Änderungen +- ⏳ **Tags in Frontmatter** - Synchronisiert mit App +- ⏳ **Attachments** - Bilder/Dateien in Markdown + +--- + +**📚 Siehe auch:** +- [QUICKSTART.md](../QUICKSTART.md) - App-Einrichtung +- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste +- [BACKUP.md](BACKUP.md) - Backup & Wiederherstellung + +**Letzte Aktualisierung:** v1.2.1 (2026-01-05) diff --git a/docs/DESKTOP.en.md b/docs/DESKTOP.en.md deleted file mode 100644 index 1bc6c9c..0000000 --- a/docs/DESKTOP.en.md +++ /dev/null @@ -1,505 +0,0 @@ -# Desktop Integration 🖥️ - -**🌍 Languages:** [Deutsch](DESKTOP.md) · **English** - -> Edit your notes with any Markdown editor on desktop - ---- - -## 📋 Overview - -Desktop integration allows you to edit notes on PC/Mac: -- 📝 Any Markdown editor works -- 🔄 Automatic synchronization via WebDAV -- 💾 Dual-format: JSON (master) + Markdown (mirror) -- ⚡ Last-Write-Wins conflict resolution - ---- - -## 🎯 Why Markdown? - -### Dual-Format Architecture - -``` -┌─────────────────────────────────────┐ -│ Android App │ -│ │ -│ ┌──────────┐ ┌─────────────┐ │ -│ │ JSON │ ──→ │ Markdown │ │ -│ │ (Master) │ │ (Mirror) │ │ -│ └──────────┘ └─────────────┘ │ -└────────┬────────────────┬───────────┘ - │ │ - ↓ ↓ - WebDAV Server - │ │ - ┌────┴────┐ ┌────┴──────┐ - │ /notes/ │ │ /notes-md/│ - │ *.json │ │ *.md │ - └─────────┘ └───────────┘ - ↑ ↑ - │ │ - ┌────┴────────────────┴───────────┐ - │ Desktop Editor │ - │ (VS Code, Typora, etc.) │ - └──────────────────────────────────┘ -``` - -### Advantages - -**JSON (Master):** -- ✅ Reliable and fast -- ✅ Structured data (IDs, timestamps) -- ✅ Primary sync mechanism -- ✅ Always active - -**Markdown (Mirror):** -- ✅ Human-readable -- ✅ Desktop editor compatible -- ✅ Syntax highlighting -- ✅ Optionally activatable - ---- - -## 🚀 Quick Start - -### 1. First Synchronization - -**Important:** Perform a sync FIRST before activating desktop integration! - -1. **Set up app** (see [QUICKSTART.en.md](QUICKSTART.en.md)) -2. **Test server connection** -3. **Create first note** -4. **Synchronize** (pull-to-refresh or auto-sync) -5. ✅ Server automatically creates `/notes/` and `/notes-md/` folders - -### 2. Activate Desktop Integration - -1. **Settings** → **Desktop Integration** -2. **Toggle ON** -3. **Initial export starts** - Shows progress (X/Y) -4. ✅ All existing notes are exported as `.md` - -### 3. Mount WebDAV as Network Drive - -#### Windows - -``` -1. Open Explorer -2. Right-click on "This PC" -3. "Map network drive" -4. Enter URL: http://YOUR-SERVER:8080/notes-md/ -5. Username: noteuser -6. Password: (your WebDAV password) -7. Drive letter: Z:\ (or any) -8. Done! -``` - -**Access:** `Z:\` in Explorer - -#### macOS - -``` -1. Open Finder -2. Menu "Go" → "Connect to Server" (⌘K) -3. Server address: http://YOUR-SERVER:8080/notes-md/ -4. Connect -5. Username: noteuser -6. Password: (your WebDAV password) -7. Done! -``` - -**Access:** Finder → Network → notes-md - -#### Linux (GNOME) - -``` -1. Open Files / Nautilus -2. "Other Locations" -3. "Connect to Server" -4. Server address: dav://YOUR-SERVER:8080/notes-md/ -5. Username: noteuser -6. Password: (your WebDAV password) -7. Done! -``` - -**Access:** `/run/user/1000/gvfs/dav:host=...` - -#### Linux (davfs2 - permanent) - -```bash -# Installation -sudo apt install davfs2 - -# Create mount point -sudo mkdir -p /mnt/notes-md - -# Mount once -sudo mount -t davfs http://YOUR-SERVER:8080/notes-md/ /mnt/notes-md - -# Permanent in /etc/fstab -echo "http://YOUR-SERVER:8080/notes-md/ /mnt/notes-md davfs rw,user,noauto 0 0" | sudo tee -a /etc/fstab -``` - -**Access:** `/mnt/notes-md/` - ---- - -## 📝 Markdown Editors - -### Recommended Editors - -#### 1. VS Code ⭐ _Recommended_ - -**Advantages:** -- ✅ Free & open source -- ✅ Markdown preview (Ctrl+Shift+V) -- ✅ Syntax highlighting -- ✅ Git integration -- ✅ Extensions (spell check, etc.) - -**Setup:** -``` -1. Install VS Code -2. Mount WebDAV drive -3. Open folder: Z:\notes-md\ (Windows) or /mnt/notes-md (Linux) -4. Done! Edit Markdown files -``` - -**Extensions (optional):** -- `Markdown All in One` - Shortcuts & preview -- `Markdown Preview Enhanced` - Better preview -- `Code Spell Checker` - Spell checking - -#### 2. Typora - -**Advantages:** -- ✅ WYSIWYG Markdown editor -- ✅ Minimalist design -- ✅ Live preview -- ⚠️ Paid (~15€) - -**Setup:** -``` -1. Install Typora -2. Mount WebDAV -3. Open folder in Typora -4. Edit notes -``` - -#### 3. Notepad++ - -**Advantages:** -- ✅ Lightweight -- ✅ Fast -- ✅ Syntax highlighting -- ⚠️ No Markdown preview - -**Setup:** -``` -1. Install Notepad++ -2. Mount WebDAV -3. Open files directly -``` - -#### 4. Obsidian - -**Advantages:** -- ✅ Second brain philosophy -- ✅ Graph view for links -- ✅ Many plugins -- ⚠️ Sync conflicts possible (2 masters) - -**Setup:** -``` -1. Install Obsidian -2. Open WebDAV as vault -3. Caution: Obsidian creates own metadata! -``` - -**⚠️ Not recommended:** Can alter frontmatter - ---- - -## 📄 Markdown File Format - -### Structure - -Each note is exported as `.md` file with YAML frontmatter: - -```markdown ---- -id: abc-123-def-456 -created: 2026-01-05T14:30:22Z -updated: 2026-01-05T14:30:22Z -tags: [] ---- - -# Note Title - -Note content here... -``` - -### Frontmatter Fields - -| Field | Type | Description | Required | -|-------|------|-------------|----------| -| `id` | UUID | Unique note ID | ✅ Yes | -| `created` | ISO8601 | Creation date | ✅ Yes | -| `updated` | ISO8601 | Modification date | ✅ Yes | -| `tags` | Array | Tags (future) | ❌ No | - -### Filenames - -**Sanitization rules:** -``` -Title: "My Shopping List 🛒" -→ Filename: "My_Shopping_List.md" - -Removed: -- Emojis: 🛒 → removed -- Special chars: / \ : * ? " < > | → removed -- Multiple spaces → single space -- Spaces → underscore _ -``` - -**Examples:** -``` -"Meeting Notes 2026" → "Meeting_Notes_2026.md" -"To-Do: Project" → "To-Do_Project.md" -"Vacation ☀️" → "Vacation.md" -``` - ---- - -## 🔄 Synchronization - -### Workflow: Android → Desktop - -1. **Create/edit note in app** -2. **Run sync** (auto or manual) -3. **JSON is uploaded** (`/notes/abc-123.json`) -4. **Markdown is exported** (`/notes-md/Note_Title.md`) _(only if Desktop Integration ON)_ -5. **Desktop editor shows changes** (after refresh) - -### Workflow: Desktop → Android - -1. **Edit Markdown file** (in mounted folder) -2. **Save** - File is immediately on server -3. **In app: Run Markdown import** - - Settings → "Import Markdown Changes" - - Or: Auto-import on every sync (future) -4. **App adopts changes** (if desktop version is newer) - -### Conflict Resolution: Last-Write-Wins - -**Rule:** Newest version (by `updated` timestamp) wins - -**Example:** -``` -App version: updated: 2026-01-05 14:00 -Desktop version: updated: 2026-01-05 14:30 -→ Desktop wins (newer timestamp) -``` - -**Automatic:** -- ✅ On Markdown import -- ✅ On JSON sync -- ⚠️ No merge conflicts - only complete overwrite - ---- - -## ⚙️ Settings - -### Desktop Integration Toggle - -**Settings → Desktop Integration** - -**ON (activated):** -- ✅ New notes → automatically exported as `.md` -- ✅ Updated notes → `.md` update -- ✅ Deleted notes → `.md` remains (future: also delete) - -**OFF (deactivated):** -- ❌ No Markdown export -- ✅ JSON sync continues normally -- ✅ Existing `.md` files remain - -### Initial Export - -**What happens on activation:** -1. All existing notes are scanned -2. Progress dialog shows progress (e.g., "23/42") -3. Each note is exported as `.md` -4. On errors: Individual note is skipped -5. Success message with number of exported notes - -**Time:** ~1-2 seconds per 50 notes - ---- - -## 🛠️ Advanced Usage - -### Manual Markdown Creation - -You can create `.md` files manually: - -```markdown ---- -id: 00000000-0000-0000-0000-000000000001 -created: 2026-01-05T12:00:00Z -updated: 2026-01-05T12:00:00Z ---- - -# New Desktop Note - -Content here... -``` - -**⚠️ Important:** -- `id` must be valid UUID (e.g., with uuidgen.io) -- Timestamps in ISO8601 format -- Frontmatter enclosed with `---` - -### Bulk Operations - -**Edit multiple notes at once:** - -1. Mount WebDAV -2. Open all `.md` files in VS Code -3. Find & Replace across all files (Ctrl+Shift+H) -4. Save -5. In app: "Import Markdown Changes" - -### Scripting - -**Example: Sort all notes by date** - -```bash -#!/bin/bash -cd /mnt/notes-md/ - -# Sort all .md files by update date -for file in *.md; do - updated=$(grep "^updated:" "$file" | cut -d' ' -f2) - echo "$updated $file" -done | sort -``` - ---- - -## ❌ Troubleshooting - -### "404 Not Found" when mounting WebDAV - -**Cause:** `/notes-md/` folder doesn't exist - -**Solution:** -1. **Perform first sync** - Folder is created automatically -2. OR: Create manually via terminal: - ```bash - curl -X MKCOL -u noteuser:password http://server:8080/notes-md/ - ``` - -### Markdown files don't appear - -**Cause:** Desktop integration not activated - -**Solution:** -1. Settings → "Desktop Integration" ON -2. Wait for initial export -3. Refresh WebDAV folder - -### Changes from desktop don't appear in app - -**Cause:** Markdown import not executed - -**Solution:** -1. Settings → "Import Markdown Changes" -2. OR: Wait for auto-sync (future feature) - -### "Frontmatter missing" error - -**Cause:** `.md` file without valid YAML frontmatter - -**Solution:** -1. Open file in editor -2. Add frontmatter at the beginning: - ```yaml - --- - id: NEW-UUID-HERE - created: 2026-01-05T12:00:00Z - updated: 2026-01-05T12:00:00Z - --- - ``` -3. Save and import again - ---- - -## 🔒 Security & Best Practices - -### Do's ✅ - -- ✅ **Backup before bulk edits** - Create local backup -- ✅ **One editor at a time** - Don't edit in app AND desktop in parallel -- ✅ **Wait for sync** - Run sync before desktop editing -- ✅ **Respect frontmatter** - Don't change manually (unless you know what you're doing) - -### Don'ts ❌ - -- ❌ **Parallel editing** - App and desktop simultaneously → conflicts -- ❌ **Delete frontmatter** - Note can't be imported anymore -- ❌ **Change IDs** - Note is recognized as new -- ❌ **Manipulate timestamps** - Conflict resolution doesn't work - -### Recommended Workflow - -``` -1. Sync in app (pull-to-refresh) -2. Open desktop -3. Make changes -4. Save -5. In app: "Import Markdown Changes" -6. Verify -7. Run another sync -``` - ---- - -## 📊 Comparison: JSON vs Markdown - -| Aspect | JSON | Markdown | -|--------|------|----------| -| **Format** | Structured | Flowing text | -| **Readability (human)** | ⚠️ Medium | ✅ Good | -| **Readability (machine)** | ✅ Perfect | ⚠️ Parsing needed | -| **Metadata** | Native | Frontmatter | -| **Editors** | Code editors | All text editors | -| **Sync speed** | ✅ Fast | ⚠️ Slower | -| **Reliability** | ✅ 100% | ⚠️ Frontmatter errors possible | -| **Mobile-first** | ✅ Yes | ❌ No | -| **Desktop-first** | ❌ No | ✅ Yes | - -**Conclusion:** Using both formats = Best experience on both platforms! - ---- - -## 🔮 Future Features - -Planned for v1.3.0+: - -- ⏳ **Auto-Markdown-import** - Automatically on every sync -- ⏳ **Bidirectional sync** - Without manual import -- ⏳ **Markdown preview** - In the app -- ⏳ **Conflict UI** - On simultaneous changes -- ⏳ **Tags in frontmatter** - Synchronized with app -- ⏳ **Attachments** - Images/files in Markdown - ---- - -**📚 See also:** -- [QUICKSTART.en.md](../QUICKSTART.en.md) - App setup -- [FEATURES.en.md](FEATURES.en.md) - Complete feature list -- [BACKUP.en.md](BACKUP.en.md) - Backup & restore - -**Last update:** v1.2.1 (2026-01-05) diff --git a/docs/DESKTOP.md b/docs/DESKTOP.md index 3cac251..82b1c74 100644 --- a/docs/DESKTOP.md +++ b/docs/DESKTOP.md @@ -1,24 +1,24 @@ -# Desktop-Integration 🖥️ +# Desktop Integration 🖥️ -**🌍 Languages:** **Deutsch** · [English](DESKTOP.en.md) +**🌍 Languages:** [Deutsch](DESKTOP.de.md) · **English** -> Bearbeite deine Notizen mit jedem Markdown-Editor auf dem Desktop +> Edit your notes with any Markdown editor on desktop --- -## 📋 Übersicht +## 📋 Overview -Die Desktop-Integration ermöglicht dir, Notizen auf dem PC/Mac zu bearbeiten: -- 📝 Jeder Markdown-Editor funktioniert -- 🔄 Automatische Synchronisation über WebDAV -- 💾 Dual-Format: JSON (Master) + Markdown (Mirror) -- ⚡ Last-Write-Wins Konfliktauflösung +Desktop integration allows you to edit notes on PC/Mac: +- 📝 Any Markdown editor works +- 🔄 Automatic synchronization via WebDAV +- 💾 Dual-format: JSON (master) + Markdown (mirror) +- ⚡ Last-Write-Wins conflict resolution --- -## 🎯 Warum Markdown? +## 🎯 Why Markdown? -### Dual-Format Architektur +### Dual-Format Architecture ``` ┌─────────────────────────────────────┐ @@ -45,85 +45,85 @@ Die Desktop-Integration ermöglicht dir, Notizen auf dem PC/Mac zu bearbeiten: └──────────────────────────────────┘ ``` -### Vorteile +### Advantages **JSON (Master):** -- ✅ Zuverlässig und schnell -- ✅ Strukturierte Daten (IDs, Timestamps) -- ✅ Primärer Sync-Mechanismus -- ✅ Immer aktiv +- ✅ Reliable and fast +- ✅ Structured data (IDs, timestamps) +- ✅ Primary sync mechanism +- ✅ Always active **Markdown (Mirror):** -- ✅ Menschenlesbar -- ✅ Desktop-Editor kompatibel -- ✅ Syntax-Highlighting -- ✅ Optional aktivierbar +- ✅ Human-readable +- ✅ Desktop editor compatible +- ✅ Syntax highlighting +- ✅ Optionally activatable --- -## 🚀 Schnellstart +## 🚀 Quick Start -### 1. Erste Synchronisation +### 1. First Synchronization -**Wichtig:** Führe ZUERST einen Sync durch, bevor du Desktop-Integration aktivierst! +**Important:** Perform a sync FIRST before activating desktop integration! -1. **App einrichten** (siehe [QUICKSTART.md](QUICKSTART.md)) -2. **Server-Verbindung testen** -3. **Erste Notiz erstellen** -4. **Synchronisieren** (Pull-to-Refresh oder Auto-Sync) -5. ✅ Server erstellt automatisch `/notes/` und `/notes-md/` Ordner +1. **Set up app** (see [QUICKSTART.en.md](QUICKSTART.en.md)) +2. **Test server connection** +3. **Create first note** +4. **Synchronize** (pull-to-refresh or auto-sync) +5. ✅ Server automatically creates `/notes/` and `/notes-md/` folders -### 2. Desktop-Integration aktivieren +### 2. Activate Desktop Integration -1. **Einstellungen** → **Desktop-Integration** -2. **Toggle aktivieren** -3. **Initial Export startet** - Zeigt Progress (X/Y) -4. ✅ Alle bestehenden Notizen werden als `.md` exportiert +1. **Settings** → **Desktop Integration** +2. **Toggle ON** +3. **Initial export starts** - Shows progress (X/Y) +4. ✅ All existing notes are exported as `.md` -### 3. WebDAV als Netzlaufwerk mounten +### 3. Mount WebDAV as Network Drive #### Windows ``` -1. Explorer öffnen -2. Rechtsklick auf "Dieser PC" -3. "Netzlaufwerk verbinden" -4. URL eingeben: http://DEIN-SERVER:8080/notes-md/ -5. Benutzername: noteuser -6. Passwort: (dein WebDAV-Passwort) -7. Laufwerksbuchstabe: Z:\ (oder beliebig) -8. Fertig! +1. Open Explorer +2. Right-click on "This PC" +3. "Map network drive" +4. Enter URL: http://YOUR-SERVER:8080/notes-md/ +5. Username: noteuser +6. Password: (your WebDAV password) +7. Drive letter: Z:\ (or any) +8. Done! ``` -**Zugriff:** `Z:\` im Explorer +**Access:** `Z:\` in Explorer #### macOS ``` -1. Finder öffnen -2. Menü "Gehe zu" → "Mit Server verbinden" (⌘K) -3. Server-Adresse: http://DEIN-SERVER:8080/notes-md/ -4. Verbinden -5. Benutzername: noteuser -6. Passwort: (dein WebDAV-Passwort) -7. Fertig! +1. Open Finder +2. Menu "Go" → "Connect to Server" (⌘K) +3. Server address: http://YOUR-SERVER:8080/notes-md/ +4. Connect +5. Username: noteuser +6. Password: (your WebDAV password) +7. Done! ``` -**Zugriff:** Finder → Netzwerk → notes-md +**Access:** Finder → Network → notes-md #### Linux (GNOME) ``` -1. Files / Nautilus öffnen -2. "Andere Orte" -3. "Mit Server verbinden" -4. Server-Adresse: dav://DEIN-SERVER:8080/notes-md/ -5. Benutzername: noteuser -6. Passwort: (dein WebDAV-Passwort) -7. Fertig! +1. Open Files / Nautilus +2. "Other Locations" +3. "Connect to Server" +4. Server address: dav://YOUR-SERVER:8080/notes-md/ +5. Username: noteuser +6. Password: (your WebDAV password) +7. Done! ``` -**Zugriff:** `/run/user/1000/gvfs/dav:host=...` +**Access:** `/run/user/1000/gvfs/dav:host=...` #### Linux (davfs2 - permanent) @@ -131,101 +131,101 @@ Die Desktop-Integration ermöglicht dir, Notizen auf dem PC/Mac zu bearbeiten: # Installation sudo apt install davfs2 -# Mount-Point erstellen +# Create mount point sudo mkdir -p /mnt/notes-md -# Einmalig mounten -sudo mount -t davfs http://DEIN-SERVER:8080/notes-md/ /mnt/notes-md +# Mount once +sudo mount -t davfs http://YOUR-SERVER:8080/notes-md/ /mnt/notes-md # Permanent in /etc/fstab -echo "http://DEIN-SERVER:8080/notes-md/ /mnt/notes-md davfs rw,user,noauto 0 0" | sudo tee -a /etc/fstab +echo "http://YOUR-SERVER:8080/notes-md/ /mnt/notes-md davfs rw,user,noauto 0 0" | sudo tee -a /etc/fstab ``` -**Zugriff:** `/mnt/notes-md/` +**Access:** `/mnt/notes-md/` --- -## 📝 Markdown-Editoren +## 📝 Markdown Editors -### Empfohlene Editoren +### Recommended Editors -#### 1. VS Code ⭐ _Empfohlen_ +#### 1. VS Code ⭐ _Recommended_ -**Vorteile:** -- ✅ Kostenlos & Open Source -- ✅ Markdown-Preview (Ctrl+Shift+V) -- ✅ Syntax-Highlighting -- ✅ Git-Integration -- ✅ Erweiterungen (Spell Check, etc.) +**Advantages:** +- ✅ Free & open source +- ✅ Markdown preview (Ctrl+Shift+V) +- ✅ Syntax highlighting +- ✅ Git integration +- ✅ Extensions (spell check, etc.) **Setup:** ``` -1. VS Code installieren -2. WebDAV-Laufwerk mounten -3. Ordner öffnen: Z:\notes-md\ (Windows) oder /mnt/notes-md (Linux) -4. Fertig! Markdown-Dateien bearbeiten +1. Install VS Code +2. Mount WebDAV drive +3. Open folder: Z:\notes-md\ (Windows) or /mnt/notes-md (Linux) +4. Done! Edit Markdown files ``` **Extensions (optional):** -- `Markdown All in One` - Shortcuts & Preview -- `Markdown Preview Enhanced` - Bessere Preview -- `Code Spell Checker` - Rechtschreibprüfung +- `Markdown All in One` - Shortcuts & preview +- `Markdown Preview Enhanced` - Better preview +- `Code Spell Checker` - Spell checking #### 2. Typora -**Vorteile:** -- ✅ WYSIWYG Markdown-Editor -- ✅ Minimalistisches Design -- ✅ Live-Preview -- ⚠️ Kostenpflichtig (~15€) +**Advantages:** +- ✅ WYSIWYG Markdown editor +- ✅ Minimalist design +- ✅ Live preview +- ⚠️ Paid (~15€) **Setup:** ``` -1. Typora installieren -2. WebDAV mounten -3. Ordner in Typora öffnen -4. Notizen bearbeiten +1. Install Typora +2. Mount WebDAV +3. Open folder in Typora +4. Edit notes ``` #### 3. Notepad++ -**Vorteile:** -- ✅ Leichtgewichtig -- ✅ Schnell -- ✅ Syntax-Highlighting -- ⚠️ Keine Markdown-Preview +**Advantages:** +- ✅ Lightweight +- ✅ Fast +- ✅ Syntax highlighting +- ⚠️ No Markdown preview **Setup:** ``` -1. Notepad++ installieren -2. WebDAV mounten -3. Dateien direkt öffnen +1. Install Notepad++ +2. Mount WebDAV +3. Open files directly ``` #### 4. Obsidian -**Vorteile:** -- ✅ Zweite Gehirn-Philosophie -- ✅ Graph-View für Verlinkungen -- ✅ Viele Plugins -- ⚠️ Sync-Konflikte möglich (2 Master) +**Advantages:** +- ✅ Second brain philosophy +- ✅ Graph view for links +- ✅ Many plugins +- ⚠️ Sync conflicts possible (2 masters) **Setup:** ``` -1. Obsidian installieren -2. WebDAV als Vault öffnen -3. Vorsicht: Obsidian erstellt eigene Metadaten! +1. Install Obsidian +2. Open WebDAV as vault +3. Caution: Obsidian creates own metadata! ``` -**⚠️ Nicht empfohlen:** Kann Frontmatter verändern +**⚠️ Not recommended:** Can alter frontmatter --- -## 📄 Markdown-Dateiformat +## 📄 Markdown File Format -### Struktur +### Structure -Jede Notiz wird als `.md` Datei mit YAML-Frontmatter exportiert: +Each note is exported as `.md` file with YAML frontmatter: ```markdown --- @@ -235,114 +235,114 @@ updated: 2026-01-05T14:30:22Z tags: [] --- -# Notiz-Titel +# Note Title -Notiz-Inhalt hier... +Note content here... ``` -### Frontmatter-Felder +### Frontmatter Fields -| Feld | Typ | Beschreibung | Pflicht | -|------|-----|--------------|---------| -| `id` | UUID | Eindeutige Notiz-ID | ✅ Ja | -| `created` | ISO8601 | Erstellungsdatum | ✅ Ja | -| `updated` | ISO8601 | Änderungsdatum | ✅ Ja | -| `tags` | Array | Tags (zukünftig) | ❌ Nein | +| Field | Type | Description | Required | +|-------|------|-------------|----------| +| `id` | UUID | Unique note ID | ✅ Yes | +| `created` | ISO8601 | Creation date | ✅ Yes | +| `updated` | ISO8601 | Modification date | ✅ Yes | +| `tags` | Array | Tags (future) | ❌ No | -### Dateinamen +### Filenames -**Sanitization-Regeln:** +**Sanitization rules:** ``` -Titel: "Meine Einkaufsliste 🛒" -→ Dateiname: "Meine_Einkaufsliste.md" +Title: "My Shopping List 🛒" +→ Filename: "My_Shopping_List.md" -Entfernt werden: -- Emojis: 🛒 → entfernt -- Sonderzeichen: / \ : * ? " < > | → entfernt -- Mehrfache Leerzeichen → einzelnes Leerzeichen -- Leerzeichen → Unterstrich _ +Removed: +- Emojis: 🛒 → removed +- Special chars: / \ : * ? " < > | → removed +- Multiple spaces → single space +- Spaces → underscore _ ``` -**Beispiele:** +**Examples:** ``` "Meeting Notes 2026" → "Meeting_Notes_2026.md" -"To-Do: Projekt" → "To-Do_Projekt.md" -"Urlaub ☀️" → "Urlaub.md" +"To-Do: Project" → "To-Do_Project.md" +"Vacation ☀️" → "Vacation.md" ``` --- -## 🔄 Synchronisation +## 🔄 Synchronization ### Workflow: Android → Desktop -1. **Notiz in App erstellen/bearbeiten** -2. **Sync ausführen** (Auto oder manuell) -3. **JSON wird hochgeladen** (`/notes/abc-123.json`) -4. **Markdown wird exportiert** (`/notes-md/Notiz_Titel.md`) _(nur wenn Desktop-Integration AN)_ -5. **Desktop-Editor zeigt Änderungen** (nach Refresh) +1. **Create/edit note in app** +2. **Run sync** (auto or manual) +3. **JSON is uploaded** (`/notes/abc-123.json`) +4. **Markdown is exported** (`/notes-md/Note_Title.md`) _(only if Desktop Integration ON)_ +5. **Desktop editor shows changes** (after refresh) ### Workflow: Desktop → Android -1. **Markdown-Datei bearbeiten** (im gemounteten Ordner) -2. **Speichern** - Datei liegt sofort auf Server -3. **In App: Markdown-Import ausführen** - - Einstellungen → "Import Markdown Changes" - - Oder: Auto-Import bei jedem Sync (zukünftig) -4. **App übernimmt Änderungen** (wenn Desktop-Version neuer) +1. **Edit Markdown file** (in mounted folder) +2. **Save** - File is immediately on server +3. **In app: Run Markdown import** + - Settings → "Import Markdown Changes" + - Or: Auto-import on every sync (future) +4. **App adopts changes** (if desktop version is newer) -### Konfliktauflösung: Last-Write-Wins +### Conflict Resolution: Last-Write-Wins -**Regel:** Neueste Version (nach `updated` Timestamp) gewinnt +**Rule:** Newest version (by `updated` timestamp) wins -**Beispiel:** +**Example:** ``` -App-Version: updated: 2026-01-05 14:00 -Desktop-Version: updated: 2026-01-05 14:30 -→ Desktop gewinnt (neuerer Timestamp) +App version: updated: 2026-01-05 14:00 +Desktop version: updated: 2026-01-05 14:30 +→ Desktop wins (newer timestamp) ``` -**Automatisch:** -- ✅ Beim Markdown-Import -- ✅ Beim JSON-Sync -- ⚠️ Keine Merge-Konflikte - nur komplettes Überschreiben +**Automatic:** +- ✅ On Markdown import +- ✅ On JSON sync +- ⚠️ No merge conflicts - only complete overwrite --- -## ⚙️ Einstellungen +## ⚙️ Settings -### Desktop-Integration Toggle +### Desktop Integration Toggle -**Einstellungen → Desktop-Integration** +**Settings → Desktop Integration** -**AN (aktiviert):** -- ✅ Neue Notizen → automatisch als `.md` exportiert -- ✅ Aktualisierte Notizen → `.md` Update -- ✅ Gelöschte Notizen → `.md` bleibt (zukünftig: auch löschen) +**ON (activated):** +- ✅ New notes → automatically exported as `.md` +- ✅ Updated notes → `.md` update +- ✅ Deleted notes → `.md` remains (future: also delete) -**AUS (deaktiviert):** -- ❌ Kein Markdown-Export -- ✅ JSON-Sync läuft normal weiter -- ✅ Bestehende `.md` Dateien bleiben erhalten +**OFF (deactivated):** +- ❌ No Markdown export +- ✅ JSON sync continues normally +- ✅ Existing `.md` files remain ### Initial Export -**Was passiert beim Aktivieren:** -1. Alle bestehenden Notizen werden gescannt -2. Progress-Dialog zeigt Fortschritt (z.B. "23/42") -3. Jede Notiz wird als `.md` exportiert -4. Bei Fehlern: Einzelne Notiz wird übersprungen -5. Erfolgsmeldung mit Anzahl exportierter Notizen +**What happens on activation:** +1. All existing notes are scanned +2. Progress dialog shows progress (e.g., "23/42") +3. Each note is exported as `.md` +4. On errors: Individual note is skipped +5. Success message with number of exported notes -**Zeit:** ~1-2 Sekunden pro 50 Notizen +**Time:** ~1-2 seconds per 50 notes --- -## 🛠️ Erweiterte Nutzung +## 🛠️ Advanced Usage -### Manuelle Markdown-Erstellung +### Manual Markdown Creation -Du kannst `.md` Dateien manuell erstellen: +You can create `.md` files manually: ```markdown --- @@ -351,35 +351,35 @@ created: 2026-01-05T12:00:00Z updated: 2026-01-05T12:00:00Z --- -# Neue Desktop-Notiz +# New Desktop Note -Inhalt hier... +Content here... ``` -**⚠️ Wichtig:** -- `id` muss gültige UUID sein (z.B. mit uuidgen.io) -- Timestamps in ISO8601-Format -- Frontmatter mit `---` umschließen +**⚠️ Important:** +- `id` must be valid UUID (e.g., with uuidgen.io) +- Timestamps in ISO8601 format +- Frontmatter enclosed with `---` -### Bulk-Operations +### Bulk Operations -**Mehrere Notizen auf einmal bearbeiten:** +**Edit multiple notes at once:** -1. WebDAV mounten -2. Alle `.md` Dateien in VS Code öffnen -3. Suchen & Ersetzen über alle Dateien (Ctrl+Shift+H) -4. Speichern -5. In App: "Import Markdown Changes" +1. Mount WebDAV +2. Open all `.md` files in VS Code +3. Find & Replace across all files (Ctrl+Shift+H) +4. Save +5. In app: "Import Markdown Changes" ### Scripting -**Beispiel: Alle Notizen nach Datum sortieren** +**Example: Sort all notes by date** ```bash #!/bin/bash cd /mnt/notes-md/ -# Alle .md Dateien nach Update-Datum sortieren +# Sort all .md files by update date for file in *.md; do updated=$(grep "^updated:" "$file" | cut -d' ' -f2) echo "$updated $file" @@ -388,118 +388,118 @@ done | sort --- -## ❌ Fehlerbehebung +## ❌ Troubleshooting -### "404 Not Found" beim WebDAV-Mount +### "404 Not Found" when mounting WebDAV -**Ursache:** `/notes-md/` Ordner existiert nicht +**Cause:** `/notes-md/` folder doesn't exist -**Lösung:** -1. **Erste Sync durchführen** - Ordner wird automatisch erstellt -2. ODER: Manuell erstellen via Terminal: +**Solution:** +1. **Perform first sync** - Folder is created automatically +2. OR: Create manually via terminal: ```bash curl -X MKCOL -u noteuser:password http://server:8080/notes-md/ ``` -### Markdown-Dateien erscheinen nicht +### Markdown files don't appear -**Ursache:** Desktop-Integration nicht aktiviert +**Cause:** Desktop integration not activated -**Lösung:** -1. Einstellungen → "Desktop-Integration" AN -2. Warten auf Initial Export -3. WebDAV-Ordner refreshen +**Solution:** +1. Settings → "Desktop Integration" ON +2. Wait for initial export +3. Refresh WebDAV folder -### Änderungen vom Desktop erscheinen nicht in App +### Changes from desktop don't appear in app -**Ursache:** Markdown-Import nicht ausgeführt +**Cause:** Markdown import not executed -**Lösung:** -1. Einstellungen → "Import Markdown Changes" -2. ODER: Auto-Sync abwarten (zukünftiges Feature) +**Solution:** +1. Settings → "Import Markdown Changes" +2. OR: Wait for auto-sync (future feature) -### "Frontmatter fehlt" Fehler +### "Frontmatter missing" error -**Ursache:** `.md` Datei ohne gültiges YAML-Frontmatter +**Cause:** `.md` file without valid YAML frontmatter -**Lösung:** -1. Datei in Editor öffnen -2. Frontmatter am Anfang hinzufügen: +**Solution:** +1. Open file in editor +2. Add frontmatter at the beginning: ```yaml --- - id: NEUE-UUID-HIER + id: NEW-UUID-HERE created: 2026-01-05T12:00:00Z updated: 2026-01-05T12:00:00Z --- ``` -3. Speichern und erneut importieren +3. Save and import again --- -## 🔒 Sicherheit & Best Practices +## 🔒 Security & Best Practices ### Do's ✅ -- ✅ **Backup vor Bulk-Edits** - Lokales Backup erstellen -- ✅ **Ein Editor zur Zeit** - Nicht parallel in App UND Desktop bearbeiten -- ✅ **Sync abwarten** - Vor Desktop-Bearbeitung Sync durchführen -- ✅ **Frontmatter respektieren** - Nicht manuell ändern (außer du weißt was du tust) +- ✅ **Backup before bulk edits** - Create local backup +- ✅ **One editor at a time** - Don't edit in app AND desktop in parallel +- ✅ **Wait for sync** - Run sync before desktop editing +- ✅ **Respect frontmatter** - Don't change manually (unless you know what you're doing) ### Don'ts ❌ -- ❌ **Parallel bearbeiten** - App und Desktop gleichzeitig → Konflikte -- ❌ **Frontmatter löschen** - Notiz kann nicht mehr importiert werden -- ❌ **IDs ändern** - Notiz wird als neue erkannt -- ❌ **Timestamps manipulieren** - Konfliktauflösung funktioniert nicht +- ❌ **Parallel editing** - App and desktop simultaneously → conflicts +- ❌ **Delete frontmatter** - Note can't be imported anymore +- ❌ **Change IDs** - Note is recognized as new +- ❌ **Manipulate timestamps** - Conflict resolution doesn't work -### Empfohlener Workflow +### Recommended Workflow ``` -1. Sync in App (Pull-to-Refresh) -2. Desktop öffnen -3. Änderungen machen -4. Speichern -5. In App: "Import Markdown Changes" -6. Überprüfen -7. Weiteren Sync durchführen +1. Sync in app (pull-to-refresh) +2. Open desktop +3. Make changes +4. Save +5. In app: "Import Markdown Changes" +6. Verify +7. Run another sync ``` --- -## 📊 Vergleich: JSON vs Markdown +## 📊 Comparison: JSON vs Markdown -| Aspekt | JSON | Markdown | +| Aspect | JSON | Markdown | |--------|------|----------| -| **Format** | Strukturiert | Fließtext | -| **Lesbarkeit (Mensch)** | ⚠️ Mittel | ✅ Gut | -| **Lesbarkeit (Maschine)** | ✅ Perfekt | ⚠️ Parsing nötig | +| **Format** | Structured | Flowing text | +| **Readability (human)** | ⚠️ Medium | ✅ Good | +| **Readability (machine)** | ✅ Perfect | ⚠️ Parsing needed | | **Metadata** | Native | Frontmatter | -| **Editoren** | Code-Editoren | Alle Text-Editoren | -| **Sync-Geschwindigkeit** | ✅ Schnell | ⚠️ Langsamer | -| **Zuverlässigkeit** | ✅ 100% | ⚠️ Frontmatter-Fehler möglich | -| **Mobile-First** | ✅ Ja | ❌ Nein | -| **Desktop-First** | ❌ Nein | ✅ Ja | +| **Editors** | Code editors | All text editors | +| **Sync speed** | ✅ Fast | ⚠️ Slower | +| **Reliability** | ✅ 100% | ⚠️ Frontmatter errors possible | +| **Mobile-first** | ✅ Yes | ❌ No | +| **Desktop-first** | ❌ No | ✅ Yes | -**Fazit:** Beide Formate nutzen = Beste Erfahrung auf beiden Plattformen! +**Conclusion:** Using both formats = Best experience on both platforms! --- -## 🔮 Zukünftige Features +## 🔮 Future Features -Geplant für v1.3.0+: +Planned for v1.3.0+: -- ⏳ **Auto-Markdown-Import** - Bei jedem Sync automatisch -- ⏳ **Bidirektionaler Sync** - Ohne manuellen Import -- ⏳ **Markdown-Vorschau** - In der App -- ⏳ **Konflikts-UI** - Bei gleichzeitigen Änderungen -- ⏳ **Tags in Frontmatter** - Synchronisiert mit App -- ⏳ **Attachments** - Bilder/Dateien in Markdown +- ⏳ **Auto-Markdown-import** - Automatically on every sync +- ⏳ **Bidirectional sync** - Without manual import +- ⏳ **Markdown preview** - In the app +- ⏳ **Conflict UI** - On simultaneous changes +- ⏳ **Tags in frontmatter** - Synchronized with app +- ⏳ **Attachments** - Images/files in Markdown --- -**📚 Siehe auch:** -- [QUICKSTART.md](../QUICKSTART.md) - App-Einrichtung -- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste -- [BACKUP.md](BACKUP.md) - Backup & Wiederherstellung +**📚 See also:** +- [QUICKSTART.en.md](../QUICKSTART.en.md) - App setup +- [FEATURES.en.md](FEATURES.en.md) - Complete feature list +- [BACKUP.en.md](BACKUP.en.md) - Backup & restore -**Letzte Aktualisierung:** v1.2.1 (2026-01-05) +**Last update:** v1.2.1 (2026-01-05) diff --git a/docs/DOCS.en.md b/docs/DOCS.de.md similarity index 54% rename from docs/DOCS.en.md rename to docs/DOCS.de.md index b9c551f..d371b78 100644 --- a/docs/DOCS.en.md +++ b/docs/DOCS.de.md @@ -1,14 +1,14 @@ -# Simple Notes Sync - Technical Documentation +# Simple Notes Sync - Technische Dokumentation -This file contains detailed technical information about implementation, architecture, and advanced features. +Diese Datei enthält detaillierte technische Informationen über die Implementierung, Architektur und erweiterte Funktionen. -**🌍 Languages:** [Deutsch](DOCS.md) · **English** +**🌍 Sprachen:** **Deutsch** · [English](DOCS.md) --- -## 📐 Architecture +## 📐 Architektur -### Overall Overview +### Gesamtübersicht ``` ┌─────────────────┐ @@ -23,81 +23,81 @@ This file contains detailed technical information about implementation, architec └─────────────────┘ ``` -### Android App Architecture +### Android App Architektur ``` app/ ├── models/ -│ ├── Note.kt # Data class for notes -│ └── SyncStatus.kt # Sync status enum +│ ├── Note.kt # Data class für Notizen +│ └── SyncStatus.kt # Sync-Status Enum ├── storage/ -│ └── NotesStorage.kt # Local JSON file storage +│ └── NotesStorage.kt # Lokale JSON-Datei Speicherung ├── sync/ -│ ├── WebDavSyncService.kt # WebDAV sync logic -│ ├── NetworkMonitor.kt # WiFi detection -│ ├── SyncWorker.kt # WorkManager background worker -│ └── BootReceiver.kt # Device reboot handler +│ ├── WebDavSyncService.kt # WebDAV Sync-Logik +│ ├── NetworkMonitor.kt # WLAN-Erkennung +│ ├── SyncWorker.kt # WorkManager Background Worker +│ └── BootReceiver.kt # Device Reboot Handler ├── adapters/ -│ └── NotesAdapter.kt # RecyclerView adapter +│ └── NotesAdapter.kt # RecyclerView Adapter ├── utils/ -│ ├── Constants.kt # App constants -│ ├── NotificationHelper.kt# Notification management -│ └── Logger.kt # Debug/release logging +│ ├── Constants.kt # App-Konstanten +│ ├── NotificationHelper.kt# Notification Management +│ └── Logger.kt # Debug/Release Logging └── activities/ - ├── MainActivity.kt # Main view with list - ├── NoteEditorActivity.kt# Note editor - └── SettingsActivity.kt # Server configuration + ├── MainActivity.kt # Hauptansicht mit Liste + ├── NoteEditorActivity.kt# Editor für Notizen + └── SettingsActivity.kt # Server-Konfiguration ``` --- -## 🔄 Auto-Sync Implementation +## 🔄 Auto-Sync Implementierung ### WorkManager Periodic Task -Auto-sync is based on **WorkManager** with the following configuration: +Der Auto-Sync basiert auf **WorkManager** mit folgender Konfiguration: ```kotlin val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only + .setRequiredNetworkType(NetworkType.UNMETERED) // Nur WiFi .build() val syncRequest = PeriodicWorkRequestBuilder( - 30, TimeUnit.MINUTES, // Every 30 minutes + 30, TimeUnit.MINUTES, // Alle 30 Minuten 10, TimeUnit.MINUTES // Flex interval ) .setConstraints(constraints) .build() ``` -**Why WorkManager?** -- ✅ Runs even when app is closed -- ✅ Automatic restart after device reboot +**Warum WorkManager?** +- ✅ Läuft auch wenn App geschlossen ist +- ✅ Automatischer Restart nach Device Reboot - ✅ Battery-efficient (Android managed) -- ✅ Guaranteed execution when constraints are met +- ✅ Garantierte Ausführung bei erfüllten Constraints ### Network Detection -We use **Gateway IP Comparison** to check if the server is reachable: +Wir verwenden **Gateway IP Comparison** um zu prüfen, ob der Server erreichbar ist: ```kotlin fun isInHomeNetwork(): Boolean { - val gatewayIP = getGatewayIP() // e.g. 192.168.0.1 - val serverIP = extractIPFromUrl(serverUrl) // e.g. 192.168.0.188 + val gatewayIP = getGatewayIP() // z.B. 192.168.0.1 + val serverIP = extractIPFromUrl(serverUrl) // z.B. 192.168.0.188 - return isSameNetwork(gatewayIP, serverIP) // Checks /24 network + return isSameNetwork(gatewayIP, serverIP) // Prüft /24 Netzwerk } ``` -**Advantages:** -- ✅ No location permissions needed -- ✅ Works with all Android versions -- ✅ Reliable and fast +**Vorteile:** +- ✅ Keine Location Permissions nötig +- ✅ Funktioniert mit allen Android Versionen +- ✅ Zuverlässig und schnell ### Sync Flow ``` -1. WorkManager wakes up (every 30 min) +1. WorkManager wacht auf (alle 30 Min) ↓ 2. Check: WiFi connected? ↓ @@ -105,7 +105,7 @@ fun isInHomeNetwork(): Boolean { ↓ 4. Load local notes ↓ -5. Upload new/changed notes → Server +5. Upload neue/geänderte Notes → Server ↓ 6. Download remote notes ← Server ↓ @@ -118,20 +118,20 @@ fun isInHomeNetwork(): Boolean { --- -## 🔄 Sync Trigger Overview +## � Sync-Trigger Übersicht -The app uses **4 different sync triggers** with different use cases: +Die App verwendet **4 verschiedene Sync-Trigger** mit unterschiedlichen Anwendungsfällen: -| Trigger | File | Function | When? | Pre-Check? | -|---------|------|----------|-------|------------| -| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes | -| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ Yes | -| **3. Background Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Every 15/30/60 minutes (configurable) | ✅ Yes | -| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi connected | ✅ Yes | +| Trigger | Datei | Funktion | Wann? | Pre-Check? | +|---------|-------|----------|-------|------------| +| **1. Manueller Sync** | `MainActivity.kt` | `triggerManualSync()` | User klickt auf Sync-Button im Menü | ✅ Ja | +| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App wird geöffnet/fortgesetzt | ✅ Ja | +| **3. Hintergrund-Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Alle 15/30/60 Minuten (konfigurierbar) | ✅ Ja | +| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi verbunden | ✅ Ja | -### Server Reachability Check (Pre-Check) +### Server-Erreichbarkeits-Check (Pre-Check) -**All 4 sync triggers** use a **pre-check** before the actual sync: +**Alle 4 Sync-Trigger** verwenden vor dem eigentlichen Sync einen **Pre-Check**: ```kotlin // WebDavSyncService.kt - isServerReachable() @@ -148,53 +148,53 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) { } ``` -**Why Socket Check instead of HTTP Request?** -- ⚡ **Faster:** Socket connect is instant, HTTP request takes longer -- 🔋 **Battery Efficient:** No HTTP overhead (headers, TLS handshake, etc.) -- 🎯 **More Precise:** Only checks network reachability, not server logic -- 🛡️ **Prevents Errors:** Detects foreign WiFi networks before sync error occurs +**Warum Socket-Check statt HTTP-Request?** +- ⚡ **Schneller:** Socket-Connect ist instant, HTTP-Request dauert länger +- 🔋 **Akkuschonender:** Kein HTTP-Overhead (Headers, TLS Handshake, etc.) +- 🎯 **Präziser:** Prüft nur Netzwerk-Erreichbarkeit, nicht Server-Logik +- 🛡️ **Verhindert Fehler:** Erkennt fremde WiFi-Netze bevor Sync-Fehler entsteht -**When does the check fail?** -- ❌ Server offline/unreachable -- ❌ Wrong WiFi network (e.g. public café WiFi) -- ❌ Network not ready yet (DHCP/routing delay after WiFi connect) -- ❌ VPN blocks server access -- ❌ No WebDAV server URL configured +**Wann schlägt der Check fehl?** +- ❌ Server offline/nicht erreichbar +- ❌ Falsches WiFi-Netzwerk (z.B. öffentliches Café-WiFi) +- ❌ Netzwerk noch nicht bereit (DHCP/Routing-Delay nach WiFi-Connect) +- ❌ VPN blockiert Server-Zugriff +- ❌ Keine WebDAV-Server-URL konfiguriert -### Sync Behavior by Trigger Type +### Sync-Verhalten nach Trigger-Typ -| Trigger | When server not reachable | On successful sync | Throttling | -|---------|--------------------------|-------------------|------------| -| Manual Sync | Toast: "Server not reachable" | Toast: "✅ Synced: X notes" | None | -| Auto-Sync (onResume) | Silent abort (no toast) | Toast: "✅ Synced: X notes" | Max. 1x/min | -| Background Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | 15/30/60 min | -| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | WiFi-based | +| Trigger | Bei Server nicht erreichbar | Bei erfolgreichem Sync | Throttling | +|---------|----------------------------|----------------------|------------| +| Manueller Sync | Toast: "Server nicht erreichbar" | Toast: "✅ Gesynct: X Notizen" | Keins | +| Auto-Sync (onResume) | Silent abort (kein Toast) | Toast: "✅ Gesynct: X Notizen" | Max. 1x/Min | +| Hintergrund-Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | 15/30/60 Min | +| WiFi-Connect Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | WiFi-basiert | --- -## 🔋 Battery Optimization +## 🔋 Akku-Optimierung -### Usage Analysis +### Verbrauchsanalyse -| Component | Frequency | Usage | Details | +| Komponente | Frequenz | Verbrauch | Details | |------------|----------|-----------|---------| -| WorkManager Wakeup | Every 30 min | ~0.15 mAh | System wakes up | -| Network Check | 48x/day | ~0.03 mAh | Gateway IP check | -| WebDAV Sync | 2-3x/day | ~1.5 mAh | Only when changes | -| **Total** | - | **~12 mAh/day** | **~0.4%** at 3000mAh | +| WorkManager Wakeup | Alle 30 Min | ~0.15 mAh | System wacht auf | +| Network Check | 48x/Tag | ~0.03 mAh | Gateway IP check | +| WebDAV Sync | 2-3x/Tag | ~1.5 mAh | Nur bei Änderungen | +| **Total** | - | **~12 mAh/Tag** | **~0.4%** bei 3000mAh | -### Optimizations +### Optimierungen 1. **IP Caching** ```kotlin private var cachedServerIP: String? = null - // DNS lookup only once at start, not every check + // DNS lookup nur 1x beim Start, nicht bei jedem Check ``` 2. **Throttling** ```kotlin private var lastSyncTime = 0L - private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 sync/min + private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 Sync/Min ``` 3. **Conditional Logging** @@ -207,9 +207,9 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) { ``` 4. **Network Constraints** - - WiFi only (not mobile data) - - Only when server is reachable - - No permanent listeners + - Nur WiFi (nicht mobile Daten) + - Nur wenn Server erreichbar + - Keine permanenten Listeners --- @@ -255,15 +255,15 @@ suspend fun downloadNotes(): DownloadResult { val localNote = storage.loadNote(remoteNote.id) if (localNote == null) { - // New note from server + // Neue Note vom Server storage.saveNote(remoteNote) downloadedCount++ } else if (localNote.modifiedAt < remoteNote.modifiedAt) { - // Server has newer version + // Server hat neuere Version storage.saveNote(remoteNote) downloadedCount++ } else if (localNote.modifiedAt > remoteNote.modifiedAt) { - // Local version is newer → Conflict + // Lokale Version ist neuer → Conflict resolveConflict(localNote, remoteNote) conflictCount++ } @@ -275,19 +275,19 @@ suspend fun downloadNotes(): DownloadResult { ### Conflict Resolution -Strategy: **Last-Write-Wins** with **Conflict Copy** +Strategie: **Last-Write-Wins** mit **Conflict Copy** ```kotlin fun resolveConflict(local: Note, remote: Note) { - // Rename remote note (conflict copy) + // Remote Note umbenennen (Conflict Copy) val conflictNote = remote.copy( id = "${remote.id}_conflict_${System.currentTimeMillis()}", - title = "${remote.title} (Conflict)" + title = "${remote.title} (Konflikt)" ) storage.saveNote(conflictNote) - // Local note remains + // Lokale Note bleibt local.syncStatus = SyncStatus.SYNCED storage.saveNote(local) } @@ -302,7 +302,7 @@ fun resolveConflict(local: Note, remote: Note) { ```kotlin val channel = NotificationChannel( "notes_sync_channel", - "Notes Synchronization", + "Notizen Synchronisierung", NotificationManager.IMPORTANCE_DEFAULT ) ``` @@ -315,9 +315,9 @@ fun showSyncSuccess(context: Context, count: Int) { val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAGS) val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle("Sync successful") - .setContentText("$count notes synchronized") - .setContentIntent(pendingIntent) // Click opens app + .setContentTitle("Sync erfolgreich") + .setContentText("$count Notizen synchronisiert") + .setContentIntent(pendingIntent) // Click öffnet App .setAutoCancel(true) // Dismiss on click .build() @@ -329,10 +329,10 @@ fun showSyncSuccess(context: Context, count: Int) { ## 🛡️ Permissions -The app requires **minimal permissions**: +Die App benötigt **minimale Permissions**: ```xml - + @@ -348,28 +348,28 @@ The app requires **minimal permissions**: ``` -**No Location Permissions!** -We use Gateway IP Comparison instead of SSID detection. No location permission required. +**Keine Location Permissions!** +Wir verwenden Gateway IP Comparison statt SSID-Erkennung. Keine Standortberechtigung nötig. --- ## 🧪 Testing -### Test Server +### Server testen ```bash -# WebDAV server reachable? +# WebDAV Server erreichbar? curl -u noteuser:password http://192.168.0.188:8080/ -# Upload file +# Datei hochladen echo '{"test":"data"}' > test.json curl -u noteuser:password -T test.json http://192.168.0.188:8080/test.json -# Download file +# Datei herunterladen curl -u noteuser:password http://192.168.0.188:8080/test.json ``` -### Test Android App +### Android App testen **Unit Tests:** ```bash @@ -384,15 +384,15 @@ cd android **Manual Testing Checklist:** -- [ ] Create note → visible in list -- [ ] Edit note → changes saved -- [ ] Delete note → removed from list -- [ ] Manual sync → server status "Reachable" -- [ ] Auto-sync → notification after ~30 min -- [ ] Close app → auto-sync continues -- [ ] Device reboot → auto-sync starts automatically -- [ ] Server offline → error notification -- [ ] Notification click → app opens +- [ ] Notiz erstellen → in Liste sichtbar +- [ ] Notiz bearbeiten → Änderungen gespeichert +- [ ] Notiz löschen → aus Liste entfernt +- [ ] Manueller Sync → Server Status "Erreichbar" +- [ ] Auto-Sync → Notification nach ~30 Min +- [ ] App schließen → Auto-Sync funktioniert weiter +- [ ] Device Reboot → Auto-Sync startet automatisch +- [ ] Server offline → Error Notification +- [ ] Notification Click → App öffnet sich --- @@ -413,18 +413,18 @@ cd android # APK: app/build/outputs/apk/release/app-release-unsigned.apk ``` -### Sign (for Distribution) +### Signieren (für Distribution) ```bash -# Create keystore +# Keystore erstellen keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias -# Sign APK +# APK signieren jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ -keystore my-release-key.jks \ app-release-unsigned.apk my-alias -# Optimize +# Optimieren zipalign -v 4 app-release-unsigned.apk app-release.apk ``` @@ -435,39 +435,39 @@ zipalign -v 4 app-release-unsigned.apk app-release.apk ### LogCat Filter ```bash -# Only app logs +# Nur App-Logs adb logcat -s SimpleNotesApp NetworkMonitor SyncWorker WebDavSyncService -# With timestamps +# Mit Timestamps adb logcat -v time -s SyncWorker -# Save to file +# In Datei speichern adb logcat -s SyncWorker > sync_debug.log ``` ### Common Issues -**Problem: Auto-sync not working** +**Problem: Auto-Sync funktioniert nicht** ``` -Solution: Disable battery optimization +Lösung: Akku-Optimierung deaktivieren Settings → Apps → Simple Notes → Battery → Don't optimize ``` -**Problem: Server not reachable** +**Problem: Server nicht erreichbar** ``` Check: -1. Server running? → docker-compose ps -2. IP correct? → ip addr show -3. Port open? → telnet 192.168.0.188 8080 +1. Server läuft? → docker-compose ps +2. IP korrekt? → ip addr show +3. Port offen? → telnet 192.168.0.188 8080 4. Firewall? → sudo ufw allow 8080 ``` -**Problem: Notifications not appearing** +**Problem: Notifications kommen nicht** ``` Check: -1. Notification permission granted? -2. Do Not Disturb active? -3. App in background? → Force stop & restart +1. Notification Permission erteilt? +2. Do Not Disturb aktiv? +3. App im Background? → Force stop & restart ``` --- @@ -504,26 +504,26 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0 ## 🔮 Roadmap ### v1.1 -- [ ] Search & Filter +- [ ] Suche & Filter - [ ] Dark Mode -- [ ] Tags/Categories +- [ ] Tags/Kategorien - [ ] Markdown Preview ### v2.0 - [ ] Desktop Client (Flutter) -- [ ] End-to-End Encryption +- [ ] End-to-End Verschlüsselung - [ ] Shared Notes (Collaboration) - [ ] Attachment Support --- -## 📖 Further Documentation +## 📖 Weitere Dokumentation - [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync) -- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detailed Sync Trigger Documentation** +- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detaillierte Sync-Trigger Dokumentation** - [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md) --- -**Last updated:** December 25, 2025 +**Letzte Aktualisierung:** 25. Dezember 2025 diff --git a/docs/DOCS.md b/docs/DOCS.md index a87180f..cc548ef 100644 --- a/docs/DOCS.md +++ b/docs/DOCS.md @@ -1,14 +1,14 @@ -# Simple Notes Sync - Technische Dokumentation +# Simple Notes Sync - Technical Documentation -Diese Datei enthält detaillierte technische Informationen über die Implementierung, Architektur und erweiterte Funktionen. +This file contains detailed technical information about implementation, architecture, and advanced features. -**🌍 Sprachen:** **Deutsch** · [English](DOCS.en.md) +**🌍 Languages:** [Deutsch](DOCS.de.md) · **English** --- -## 📐 Architektur +## 📐 Architecture -### Gesamtübersicht +### Overall Overview ``` ┌─────────────────┐ @@ -23,81 +23,81 @@ Diese Datei enthält detaillierte technische Informationen über die Implementie └─────────────────┘ ``` -### Android App Architektur +### Android App Architecture ``` app/ ├── models/ -│ ├── Note.kt # Data class für Notizen -│ └── SyncStatus.kt # Sync-Status Enum +│ ├── Note.kt # Data class for notes +│ └── SyncStatus.kt # Sync status enum ├── storage/ -│ └── NotesStorage.kt # Lokale JSON-Datei Speicherung +│ └── NotesStorage.kt # Local JSON file storage ├── sync/ -│ ├── WebDavSyncService.kt # WebDAV Sync-Logik -│ ├── NetworkMonitor.kt # WLAN-Erkennung -│ ├── SyncWorker.kt # WorkManager Background Worker -│ └── BootReceiver.kt # Device Reboot Handler +│ ├── WebDavSyncService.kt # WebDAV sync logic +│ ├── NetworkMonitor.kt # WiFi detection +│ ├── SyncWorker.kt # WorkManager background worker +│ └── BootReceiver.kt # Device reboot handler ├── adapters/ -│ └── NotesAdapter.kt # RecyclerView Adapter +│ └── NotesAdapter.kt # RecyclerView adapter ├── utils/ -│ ├── Constants.kt # App-Konstanten -│ ├── NotificationHelper.kt# Notification Management -│ └── Logger.kt # Debug/Release Logging +│ ├── Constants.kt # App constants +│ ├── NotificationHelper.kt# Notification management +│ └── Logger.kt # Debug/release logging └── activities/ - ├── MainActivity.kt # Hauptansicht mit Liste - ├── NoteEditorActivity.kt# Editor für Notizen - └── SettingsActivity.kt # Server-Konfiguration + ├── MainActivity.kt # Main view with list + ├── NoteEditorActivity.kt# Note editor + └── SettingsActivity.kt # Server configuration ``` --- -## 🔄 Auto-Sync Implementierung +## 🔄 Auto-Sync Implementation ### WorkManager Periodic Task -Der Auto-Sync basiert auf **WorkManager** mit folgender Konfiguration: +Auto-sync is based on **WorkManager** with the following configuration: ```kotlin val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) // Nur WiFi + .setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only .build() val syncRequest = PeriodicWorkRequestBuilder( - 30, TimeUnit.MINUTES, // Alle 30 Minuten + 30, TimeUnit.MINUTES, // Every 30 minutes 10, TimeUnit.MINUTES // Flex interval ) .setConstraints(constraints) .build() ``` -**Warum WorkManager?** -- ✅ Läuft auch wenn App geschlossen ist -- ✅ Automatischer Restart nach Device Reboot +**Why WorkManager?** +- ✅ Runs even when app is closed +- ✅ Automatic restart after device reboot - ✅ Battery-efficient (Android managed) -- ✅ Garantierte Ausführung bei erfüllten Constraints +- ✅ Guaranteed execution when constraints are met ### Network Detection -Wir verwenden **Gateway IP Comparison** um zu prüfen, ob der Server erreichbar ist: +We use **Gateway IP Comparison** to check if the server is reachable: ```kotlin fun isInHomeNetwork(): Boolean { - val gatewayIP = getGatewayIP() // z.B. 192.168.0.1 - val serverIP = extractIPFromUrl(serverUrl) // z.B. 192.168.0.188 + val gatewayIP = getGatewayIP() // e.g. 192.168.0.1 + val serverIP = extractIPFromUrl(serverUrl) // e.g. 192.168.0.188 - return isSameNetwork(gatewayIP, serverIP) // Prüft /24 Netzwerk + return isSameNetwork(gatewayIP, serverIP) // Checks /24 network } ``` -**Vorteile:** -- ✅ Keine Location Permissions nötig -- ✅ Funktioniert mit allen Android Versionen -- ✅ Zuverlässig und schnell +**Advantages:** +- ✅ No location permissions needed +- ✅ Works with all Android versions +- ✅ Reliable and fast ### Sync Flow ``` -1. WorkManager wacht auf (alle 30 Min) +1. WorkManager wakes up (every 30 min) ↓ 2. Check: WiFi connected? ↓ @@ -105,7 +105,7 @@ fun isInHomeNetwork(): Boolean { ↓ 4. Load local notes ↓ -5. Upload neue/geänderte Notes → Server +5. Upload new/changed notes → Server ↓ 6. Download remote notes ← Server ↓ @@ -118,20 +118,20 @@ fun isInHomeNetwork(): Boolean { --- -## � Sync-Trigger Übersicht +## 🔄 Sync Trigger Overview -Die App verwendet **4 verschiedene Sync-Trigger** mit unterschiedlichen Anwendungsfällen: +The app uses **4 different sync triggers** with different use cases: -| Trigger | Datei | Funktion | Wann? | Pre-Check? | -|---------|-------|----------|-------|------------| -| **1. Manueller Sync** | `MainActivity.kt` | `triggerManualSync()` | User klickt auf Sync-Button im Menü | ✅ Ja | -| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App wird geöffnet/fortgesetzt | ✅ Ja | -| **3. Hintergrund-Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Alle 15/30/60 Minuten (konfigurierbar) | ✅ Ja | -| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi verbunden | ✅ Ja | +| Trigger | File | Function | When? | Pre-Check? | +|---------|------|----------|-------|------------| +| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes | +| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ Yes | +| **3. Background Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Every 15/30/60 minutes (configurable) | ✅ Yes | +| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi connected | ✅ Yes | -### Server-Erreichbarkeits-Check (Pre-Check) +### Server Reachability Check (Pre-Check) -**Alle 4 Sync-Trigger** verwenden vor dem eigentlichen Sync einen **Pre-Check**: +**All 4 sync triggers** use a **pre-check** before the actual sync: ```kotlin // WebDavSyncService.kt - isServerReachable() @@ -148,53 +148,53 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) { } ``` -**Warum Socket-Check statt HTTP-Request?** -- ⚡ **Schneller:** Socket-Connect ist instant, HTTP-Request dauert länger -- 🔋 **Akkuschonender:** Kein HTTP-Overhead (Headers, TLS Handshake, etc.) -- 🎯 **Präziser:** Prüft nur Netzwerk-Erreichbarkeit, nicht Server-Logik -- 🛡️ **Verhindert Fehler:** Erkennt fremde WiFi-Netze bevor Sync-Fehler entsteht +**Why Socket Check instead of HTTP Request?** +- ⚡ **Faster:** Socket connect is instant, HTTP request takes longer +- 🔋 **Battery Efficient:** No HTTP overhead (headers, TLS handshake, etc.) +- 🎯 **More Precise:** Only checks network reachability, not server logic +- 🛡️ **Prevents Errors:** Detects foreign WiFi networks before sync error occurs -**Wann schlägt der Check fehl?** -- ❌ Server offline/nicht erreichbar -- ❌ Falsches WiFi-Netzwerk (z.B. öffentliches Café-WiFi) -- ❌ Netzwerk noch nicht bereit (DHCP/Routing-Delay nach WiFi-Connect) -- ❌ VPN blockiert Server-Zugriff -- ❌ Keine WebDAV-Server-URL konfiguriert +**When does the check fail?** +- ❌ Server offline/unreachable +- ❌ Wrong WiFi network (e.g. public café WiFi) +- ❌ Network not ready yet (DHCP/routing delay after WiFi connect) +- ❌ VPN blocks server access +- ❌ No WebDAV server URL configured -### Sync-Verhalten nach Trigger-Typ +### Sync Behavior by Trigger Type -| Trigger | Bei Server nicht erreichbar | Bei erfolgreichem Sync | Throttling | -|---------|----------------------------|----------------------|------------| -| Manueller Sync | Toast: "Server nicht erreichbar" | Toast: "✅ Gesynct: X Notizen" | Keins | -| Auto-Sync (onResume) | Silent abort (kein Toast) | Toast: "✅ Gesynct: X Notizen" | Max. 1x/Min | -| Hintergrund-Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | 15/30/60 Min | -| WiFi-Connect Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | WiFi-basiert | +| Trigger | When server not reachable | On successful sync | Throttling | +|---------|--------------------------|-------------------|------------| +| Manual Sync | Toast: "Server not reachable" | Toast: "✅ Synced: X notes" | None | +| Auto-Sync (onResume) | Silent abort (no toast) | Toast: "✅ Synced: X notes" | Max. 1x/min | +| Background Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | 15/30/60 min | +| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | WiFi-based | --- -## 🔋 Akku-Optimierung +## 🔋 Battery Optimization -### Verbrauchsanalyse +### Usage Analysis -| Komponente | Frequenz | Verbrauch | Details | +| Component | Frequency | Usage | Details | |------------|----------|-----------|---------| -| WorkManager Wakeup | Alle 30 Min | ~0.15 mAh | System wacht auf | -| Network Check | 48x/Tag | ~0.03 mAh | Gateway IP check | -| WebDAV Sync | 2-3x/Tag | ~1.5 mAh | Nur bei Änderungen | -| **Total** | - | **~12 mAh/Tag** | **~0.4%** bei 3000mAh | +| WorkManager Wakeup | Every 30 min | ~0.15 mAh | System wakes up | +| Network Check | 48x/day | ~0.03 mAh | Gateway IP check | +| WebDAV Sync | 2-3x/day | ~1.5 mAh | Only when changes | +| **Total** | - | **~12 mAh/day** | **~0.4%** at 3000mAh | -### Optimierungen +### Optimizations 1. **IP Caching** ```kotlin private var cachedServerIP: String? = null - // DNS lookup nur 1x beim Start, nicht bei jedem Check + // DNS lookup only once at start, not every check ``` 2. **Throttling** ```kotlin private var lastSyncTime = 0L - private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 Sync/Min + private const val MIN_SYNC_INTERVAL_MS = 60_000L // Max 1 sync/min ``` 3. **Conditional Logging** @@ -207,9 +207,9 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) { ``` 4. **Network Constraints** - - Nur WiFi (nicht mobile Daten) - - Nur wenn Server erreichbar - - Keine permanenten Listeners + - WiFi only (not mobile data) + - Only when server is reachable + - No permanent listeners --- @@ -255,15 +255,15 @@ suspend fun downloadNotes(): DownloadResult { val localNote = storage.loadNote(remoteNote.id) if (localNote == null) { - // Neue Note vom Server + // New note from server storage.saveNote(remoteNote) downloadedCount++ } else if (localNote.modifiedAt < remoteNote.modifiedAt) { - // Server hat neuere Version + // Server has newer version storage.saveNote(remoteNote) downloadedCount++ } else if (localNote.modifiedAt > remoteNote.modifiedAt) { - // Lokale Version ist neuer → Conflict + // Local version is newer → Conflict resolveConflict(localNote, remoteNote) conflictCount++ } @@ -275,19 +275,19 @@ suspend fun downloadNotes(): DownloadResult { ### Conflict Resolution -Strategie: **Last-Write-Wins** mit **Conflict Copy** +Strategy: **Last-Write-Wins** with **Conflict Copy** ```kotlin fun resolveConflict(local: Note, remote: Note) { - // Remote Note umbenennen (Conflict Copy) + // Rename remote note (conflict copy) val conflictNote = remote.copy( id = "${remote.id}_conflict_${System.currentTimeMillis()}", - title = "${remote.title} (Konflikt)" + title = "${remote.title} (Conflict)" ) storage.saveNote(conflictNote) - // Lokale Note bleibt + // Local note remains local.syncStatus = SyncStatus.SYNCED storage.saveNote(local) } @@ -302,7 +302,7 @@ fun resolveConflict(local: Note, remote: Note) { ```kotlin val channel = NotificationChannel( "notes_sync_channel", - "Notizen Synchronisierung", + "Notes Synchronization", NotificationManager.IMPORTANCE_DEFAULT ) ``` @@ -315,9 +315,9 @@ fun showSyncSuccess(context: Context, count: Int) { val pendingIntent = PendingIntent.getActivity(context, 0, intent, FLAGS) val notification = NotificationCompat.Builder(context, CHANNEL_ID) - .setContentTitle("Sync erfolgreich") - .setContentText("$count Notizen synchronisiert") - .setContentIntent(pendingIntent) // Click öffnet App + .setContentTitle("Sync successful") + .setContentText("$count notes synchronized") + .setContentIntent(pendingIntent) // Click opens app .setAutoCancel(true) // Dismiss on click .build() @@ -329,10 +329,10 @@ fun showSyncSuccess(context: Context, count: Int) { ## 🛡️ Permissions -Die App benötigt **minimale Permissions**: +The app requires **minimal permissions**: ```xml - + @@ -348,28 +348,28 @@ Die App benötigt **minimale Permissions**: ``` -**Keine Location Permissions!** -Wir verwenden Gateway IP Comparison statt SSID-Erkennung. Keine Standortberechtigung nötig. +**No Location Permissions!** +We use Gateway IP Comparison instead of SSID detection. No location permission required. --- ## 🧪 Testing -### Server testen +### Test Server ```bash -# WebDAV Server erreichbar? +# WebDAV server reachable? curl -u noteuser:password http://192.168.0.188:8080/ -# Datei hochladen +# Upload file echo '{"test":"data"}' > test.json curl -u noteuser:password -T test.json http://192.168.0.188:8080/test.json -# Datei herunterladen +# Download file curl -u noteuser:password http://192.168.0.188:8080/test.json ``` -### Android App testen +### Test Android App **Unit Tests:** ```bash @@ -384,15 +384,15 @@ cd android **Manual Testing Checklist:** -- [ ] Notiz erstellen → in Liste sichtbar -- [ ] Notiz bearbeiten → Änderungen gespeichert -- [ ] Notiz löschen → aus Liste entfernt -- [ ] Manueller Sync → Server Status "Erreichbar" -- [ ] Auto-Sync → Notification nach ~30 Min -- [ ] App schließen → Auto-Sync funktioniert weiter -- [ ] Device Reboot → Auto-Sync startet automatisch -- [ ] Server offline → Error Notification -- [ ] Notification Click → App öffnet sich +- [ ] Create note → visible in list +- [ ] Edit note → changes saved +- [ ] Delete note → removed from list +- [ ] Manual sync → server status "Reachable" +- [ ] Auto-sync → notification after ~30 min +- [ ] Close app → auto-sync continues +- [ ] Device reboot → auto-sync starts automatically +- [ ] Server offline → error notification +- [ ] Notification click → app opens --- @@ -413,18 +413,18 @@ cd android # APK: app/build/outputs/apk/release/app-release-unsigned.apk ``` -### Signieren (für Distribution) +### Sign (for Distribution) ```bash -# Keystore erstellen +# Create keystore keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias -# APK signieren +# Sign APK jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 \ -keystore my-release-key.jks \ app-release-unsigned.apk my-alias -# Optimieren +# Optimize zipalign -v 4 app-release-unsigned.apk app-release.apk ``` @@ -435,39 +435,39 @@ zipalign -v 4 app-release-unsigned.apk app-release.apk ### LogCat Filter ```bash -# Nur App-Logs +# Only app logs adb logcat -s SimpleNotesApp NetworkMonitor SyncWorker WebDavSyncService -# Mit Timestamps +# With timestamps adb logcat -v time -s SyncWorker -# In Datei speichern +# Save to file adb logcat -s SyncWorker > sync_debug.log ``` ### Common Issues -**Problem: Auto-Sync funktioniert nicht** +**Problem: Auto-sync not working** ``` -Lösung: Akku-Optimierung deaktivieren +Solution: Disable battery optimization Settings → Apps → Simple Notes → Battery → Don't optimize ``` -**Problem: Server nicht erreichbar** +**Problem: Server not reachable** ``` Check: -1. Server läuft? → docker-compose ps -2. IP korrekt? → ip addr show -3. Port offen? → telnet 192.168.0.188 8080 +1. Server running? → docker-compose ps +2. IP correct? → ip addr show +3. Port open? → telnet 192.168.0.188 8080 4. Firewall? → sudo ufw allow 8080 ``` -**Problem: Notifications kommen nicht** +**Problem: Notifications not appearing** ``` Check: -1. Notification Permission erteilt? -2. Do Not Disturb aktiv? -3. App im Background? → Force stop & restart +1. Notification permission granted? +2. Do Not Disturb active? +3. App in background? → Force stop & restart ``` --- @@ -504,26 +504,26 @@ androidx.localbroadcastmanager:localbroadcastmanager:1.1.0 ## 🔮 Roadmap ### v1.1 -- [ ] Suche & Filter +- [ ] Search & Filter - [ ] Dark Mode -- [ ] Tags/Kategorien +- [ ] Tags/Categories - [ ] Markdown Preview ### v2.0 - [ ] Desktop Client (Flutter) -- [ ] End-to-End Verschlüsselung +- [ ] End-to-End Encryption - [ ] Shared Notes (Collaboration) - [ ] Attachment Support --- -## 📖 Weitere Dokumentation +## 📖 Further Documentation - [Project Docs](https://github.com/inventory69/project-docs/tree/main/simple-notes-sync) -- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detaillierte Sync-Trigger Dokumentation** +- [Sync Architecture](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/SYNC_ARCHITECTURE.md) - **Detailed Sync Trigger Documentation** - [Android Guide](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/ANDROID_GUIDE.md) - [Bugfix Documentation](https://github.com/inventory69/project-docs/blob/main/simple-notes-sync/BUGFIX_SYNC_SPAM_AND_NOTIFICATIONS.md) --- -**Letzte Aktualisierung:** 25. Dezember 2025 +**Last updated:** December 25, 2025 diff --git a/docs/FEATURES.de.md b/docs/FEATURES.de.md new file mode 100644 index 0000000..3610d2a --- /dev/null +++ b/docs/FEATURES.de.md @@ -0,0 +1,305 @@ +# Vollständige Feature-Liste 📋 + +**🌍 Sprachen:** **Deutsch** · [English](FEATURES.md) + +> Alle Features von Simple Notes Sync im Detail + +--- + +## 📝 Notiz-Verwaltung + +### Notiz-Typen +- ✅ **Textnotizen** - Klassische Freitext-Notizen +- ✅ **Checklisten** _(NEU in v1.4.0)_ - Aufgabenlisten mit Tap-to-Check + - ➕ Items hinzufügen über Eingabefeld + - ☑️ Tap zum Abhaken/Wieder-Öffnen + - 📌 Long-Press für Drag & Drop Sortierung + - ~~Durchstreichen~~ bei erledigten Einträgen + +### Basis-Funktionen +- ✅ **Automatisches Speichern** - Kein manuelles Speichern nötig +- ✅ **Titel + Inhalt** - Klare Struktur für jede Notiz +- ✅ **Zeitstempel** - Erstellungs- und Änderungsdatum automatisch +- ✅ **Auswahlmodus** _(NEU in v1.5.0)_ - Long-Press für Mehrfachauswahl und Batch-Löschen +- ✅ **Bestätigungs-Dialog** - Schutz vor versehentlichem Löschen +- ✅ **Jetpack Compose UI** _(NEU in v1.5.0)_ - Moderne, performante Benutzeroberfläche +- ✅ **Material Design 3** - Moderne, saubere UI +- ✅ **Dark Mode** - Automatisch je nach System-Einstellung +- ✅ **Dynamic Colors** - Passt sich deinem Android-Theme an + +### Editor +- ✅ **Minimalistischer Editor** - Kein Schnickschnack +- ✅ **Auto-Fokus** - Direkt losschreiben +- ✅ **Vollbild-Modus** - Maximale Schreibfläche +- ✅ **Speichern-Button** - Manuelle Bestätigung möglich +- ✅ **Zurück-Navigation** - Speichert automatisch +- ✅ **Slide-Animationen** _(NEU in v1.5.0)_ - Flüssige Übergänge + +--- + +## 🌍 Mehrsprachigkeit _(NEU in v1.5.0)_ + +### Unterstützte Sprachen +- ✅ **Englisch** - Primäre Sprache (Standard) +- ✅ **Deutsch** - Vollständige Übersetzung + +### Sprachauswahl +- ✅ **Automatische Erkennung** - Folgt der System-Sprache +- ✅ **Manuelle Auswahl** - In den Einstellungen umschaltbar +- ✅ **Per-App Language** - Android 13+ native Sprachauswahl +- ✅ **locales_config.xml** - Vollständige Android-Integration + +### Umfang +- ✅ **400+ Strings** - Komplett übersetzt +- ✅ **UI-Texte** - Alle Buttons, Dialoge, Menüs +- ✅ **Fehlermeldungen** - Hilfreiche lokalisierte Hinweise +- ✅ **Einstellungen** - 7 kategorisierte Screens + +--- + +## 💾 Backup & Wiederherstellung + +### Lokales Backup System +- ✅ **JSON-Export** - Alle Notizen in einer Datei +- ✅ **Freie Speicherort-Wahl** - Downloads, SD-Karte, Cloud-Ordner +- ✅ **Dateinamen mit Zeitstempel** - `simplenotes_backup_YYYY-MM-DD_HHmmss.json` +- ✅ **Vollständiger Export** - Titel, Inhalt, Timestamps, IDs +- ✅ **Menschenlesbares Format** - JSON mit Formatierung +- ✅ **Unabhängig vom Server** - Funktioniert komplett offline + +### Wiederherstellungs-Modi +- ✅ **Zusammenführen (Merge)** - Neue Notizen hinzufügen, bestehende behalten _(Standard)_ +- ✅ **Ersetzen (Replace)** - Alle löschen und Backup importieren +- ✅ **Duplikate überschreiben (Overwrite)** - Backup gewinnt bei ID-Konflikten +- ✅ **Automatisches Sicherheits-Backup** - Vor jeder Wiederherstellung +- ✅ **Backup-Validierung** - Prüft Format und Version +- ✅ **Fehlerbehandlung** - Klare Fehlermeldungen bei Problemen + +--- + +## 🖥️ Desktop-Integration + +### Markdown-Export +- ✅ **Automatischer Export** - Jede Notiz → `.md` Datei +- ✅ **Checklisten als Task-Listen** _(NEU)_ - `- [ ]` / `- [x]` Format (GitHub-kompatibel) +- ✅ **Dual-Format** - JSON (Master) + Markdown (Mirror) +- ✅ **Dateinamen-Sanitization** - Sichere Dateinamen aus Titeln +- ✅ **Duplikat-Handling** _(NEU)_ - ID-Suffix bei gleichen Titeln +- ✅ **Frontmatter-Metadata** - YAML mit ID, Timestamps, Type +- ✅ **WebDAV-Sync** - Parallel zum JSON-Sync +- ✅ **Optional** - In Einstellungen ein/ausschaltbar +- ✅ **Initial Export** - Alle bestehenden Notizen beim Aktivieren +- ✅ **Progress-Anzeige** - Zeigt X/Y beim Export + +### Markdown-Import +- ✅ **Desktop → App** - Änderungen vom Desktop importieren +- ✅ **Last-Write-Wins** - Konfliktauflösung via Timestamp +- ✅ **Frontmatter-Parsing** - Liest Metadata aus `.md` Dateien +- ✅ **Neue Notizen erkennen** - Automatisch in App übernehmen +- ✅ **Updates erkennen** - Nur wenn Desktop-Version neuer ist +- ✅ **Fehlertoleranz** - Einzelne Fehler brechen Import nicht ab + +### WebDAV-Zugriff +- ✅ **Network Drive Mount** - Windows, macOS, Linux +- ✅ **Jeder Markdown-Editor** - VS Code, Typora, Notepad++, iA Writer +- ✅ **Live-Bearbeitung** - Direkter Zugriff auf `.md` Dateien +- ✅ **Ordner-Struktur** - `/notes/` für JSON, `/notes-md/` für Markdown +- ✅ **Automatische Ordner-Erstellung** - Beim ersten Sync + +--- + +## 🔄 Synchronisation + +### Auto-Sync +- ✅ **Intervall-Auswahl** - 15, 30 oder 60 Minuten +- ✅ **WiFi-Trigger** - Sync bei WiFi-Verbindung _(keine SSID-Einschränkung)_ +- ✅ **Akkuschonend** - ~0.2-0.8% pro Tag +- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar +- ✅ **WorkManager** - Zuverlässige Background-Ausführung +- ✅ **Battery-Optimierung kompatibel** - Funktioniert auch mit Doze Mode + +### Sync-Trigger (6 Stück) +1. ✅ **Periodic Sync** - Automatisch nach Intervall +2. ✅ **App-Start Sync** - Beim Öffnen der App +3. ✅ **WiFi-Connect Sync** - Bei jeder WiFi-Verbindung +4. ✅ **Manual Sync** - Button in Einstellungen +5. ✅ **Pull-to-Refresh** - Wisch-Geste in Notizliste +6. ✅ **Settings-Save Sync** - Nach Server-Konfiguration + +### Sync-Mechanismus +- ✅ **Upload** - Lokale Änderungen zum Server +- ✅ **Download** - Server-Änderungen in App +- ✅ **Konflikt-Erkennung** - Bei gleichzeitigen Änderungen +- ✅ **Konfliktfreies Merging** - Last-Write-Wins via Timestamp +- ✅ **Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT +- ✅ **Fehlerbehandlung** - Retry bei Netzwerkproblemen +- ✅ **Offline-First** - App funktioniert ohne Server + +### Server-Verbindung +- ✅ **WebDAV-Protokoll** - Standard-Protokoll +- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern +- ✅ **Username/Password** - Basic Authentication +- ✅ **Connection Test** - In Einstellungen testen +- ✅ **Server-URL Normalisierung** - Automatisches `/notes/` und `/notes-md/` _(NEU in v1.2.1)_ +- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/` + +--- + +## 🔒 Privacy & Sicherheit + +### Self-Hosted +- ✅ **Eigener Server** - Volle Kontrolle über Daten +- ✅ **Keine Cloud** - Keine Drittanbieter +- ✅ **Kein Tracking** - Keine Analytik, keine Telemetrie +- ✅ **Kein Account** - Nur Server-Zugangsdaten +- ✅ **100% Open Source** - MIT Lizenz + +### Daten-Sicherheit +- ✅ **Lokale Speicherung** - App-Private Storage (Android) +- ✅ **WebDAV-Verschlüsselung** - HTTPS für externe Server +- ✅ **Passwort-Speicherung** - Android SharedPreferences (verschlüsselt) +- ✅ **Keine Drittanbieter-Libs** - Nur Android SDK + Sardine (WebDAV) + +### Entwickler-Features +- ✅ **Datei-Logging** - Optional, nur bei Aktivierung _(NEU in v1.3.2)_ +- ✅ **Datenschutz-Hinweis** - Explizite Warnung bei Aktivierung +- ✅ **Lokale Logs** - Logs bleiben auf dem Gerät + +--- + +## 🔋 Performance & Optimierung + +### Akku-Effizienz +- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min +- ✅ **WiFi-Only** - Kein Mobile Data Sync +- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar +- ✅ **WorkManager** - System-optimierte Ausführung +- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby +- ✅ **Gemessener Verbrauch:** + - 15 Min: ~0.8% / Tag (~23 mAh) + - 30 Min: ~0.4% / Tag (~12 mAh) ⭐ _Empfohlen_ + - 60 Min: ~0.2% / Tag (~6 mAh) + +### App-Performance +- ✅ **Offline-First** - Funktioniert ohne Internet +- ✅ **Instant-Load** - Notizen laden in <100ms +- ✅ **Smooth Scrolling** - RecyclerView mit ViewHolder +- ✅ **Material Design 3** - Native Android UI +- ✅ **Kotlin Coroutines** - Asynchrone Operationen +- ✅ **Minimale APK-Größe** - ~2 MB + +--- + +## 🛠️ Technische Details + +### Plattform +- ✅ **Android 8.0+** (API 26+) +- ✅ **Target SDK 36** (Android 15) +- ✅ **Kotlin** - Moderne Programmiersprache +- ✅ **Material Design 3** - Neueste Design-Richtlinien +- ✅ **ViewBinding** - Typ-sichere View-Referenzen + +### Architektur +- ✅ **MVVM-Light** - Einfache Architektur +- ✅ **Single Activity** - Moderne Navigation +- ✅ **Kotlin Coroutines** - Async/Await Pattern +- ✅ **Dispatchers.IO** - Background-Operationen +- ✅ **SharedPreferences** - Settings-Speicherung +- ✅ **File-Based Storage** - JSON-Dateien lokal +- ✅ **Custom Exceptions** - Dedizierte SyncException für bessere Fehlerbehandlung _(NEU in v1.3.2)_ + +### Abhängigkeiten +- ✅ **AndroidX** - Jetpack Libraries +- ✅ **Material Components** - Material Design 3 +- ✅ **Sardine** - WebDAV Client (com.thegrizzlylabs) +- ✅ **Gson** - JSON Serialization +- ✅ **WorkManager** - Background Tasks +- ✅ **OkHttp** - HTTP Client (via Sardine) + +### Build-Varianten +- ✅ **Standard** - Universal APK (100% FOSS, keine Google-Dependencies) +- ✅ **F-Droid** - Identisch mit Standard (100% FOSS) +- ✅ **Debug/Release** - Entwicklung und Production +- ✅ **Keine Google Services** - Komplett FOSS, keine proprietären Bibliotheken + +--- + +## 📦 Server-Kompatibilität + +### Getestete WebDAV-Server +- ✅ **Docker WebDAV** (empfohlen für Self-Hosting) +- ✅ **Nextcloud** - Vollständig kompatibel +- ✅ **ownCloud** - Funktioniert einwandfrei +- ✅ **Apache mod_dav** - Standard WebDAV +- ✅ **nginx + WebDAV** - Mit korrekter Konfiguration + +### Server-Features +- ✅ **Basic Auth** - Username/Password +- ✅ **Directory Listing** - Für Download +- ✅ **PUT/GET** - Upload/Download +- ✅ **MKCOL** - Ordner erstellen +- ✅ **DELETE** - Notizen löschen (zukünftig) + +--- + +## 🔮 Zukünftige Features + +Geplant für kommende Versionen: + +### v1.4.0 - Checklisten +- ⏳ **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen +- ⏳ **Erledigte Items** - Durchstreichen/Abhaken +- ⏳ **Drag & Drop** - Items neu anordnen + +### v1.5.0 - Internationalisierung +- ⏳ **Mehrsprachigkeit** - Deutsch + Englisch UI +- ⏳ **Sprachauswahl** - In Einstellungen wählbar +- ⏳ **Vollständige Übersetzung** - Alle Strings in beiden Sprachen + +### v1.6.0 - Modern APIs +- ⏳ **LocalBroadcastManager ersetzen** - SharedFlow stattdessen +- ⏳ **PackageInfo Flags** - PackageInfoFlags.of() verwenden +- ⏳ **Komplexitäts-Refactoring** - Lange Funktionen aufteilen + +--- + +## 📊 Vergleich mit anderen Apps + +| Feature | Simple Notes Sync | Google Keep | Nextcloud Notes | +|---------|------------------|-------------|-----------------| +| Offline-First | ✅ | ⚠️ Eingeschränkt | ⚠️ Eingeschränkt | +| Self-Hosted | ✅ | ❌ | ✅ | +| Auto-Sync | ✅ | ✅ | ✅ | +| Markdown-Export | ✅ | ❌ | ✅ | +| Desktop-Zugriff | ✅ (WebDAV) | ✅ (Web) | ✅ (Web + WebDAV) | +| Lokales Backup | ✅ | ❌ | ⚠️ Server-Backup | +| Kein Google-Account | ✅ | ❌ | ✅ | +| Open Source | ✅ MIT | ❌ | ✅ AGPL | +| APK-Größe | ~2 MB | ~50 MB | ~8 MB | +| Akku-Verbrauch | ~0.4%/Tag | ~1-2%/Tag | ~0.5%/Tag | + +--- + +## ❓ FAQ + +**Q: Brauche ich einen Server?** +A: Nein! Die App funktioniert auch komplett offline. Der Server ist optional für Sync. + +**Q: Welcher Server ist am besten?** +A: Für Einstieg: Docker WebDAV (einfach, leicht). Für Profis: Nextcloud (viele Features). + +**Q: Funktioniert Markdown-Export ohne Desktop-Integration?** +A: Nein, du musst das Feature in den Einstellungen aktivieren. + +**Q: Gehen meine Daten verloren wenn ich den Server wechsle?** +A: Nein! Erstelle ein lokales Backup, wechsle Server, stelle wieder her. + +**Q: Warum JSON + Markdown?** +A: JSON ist zuverlässig und schnell (Master). Markdown ist menschenlesbar (Mirror für Desktop). + +**Q: Kann ich die App ohne Google Play nutzen?** +A: Ja! Lade die APK direkt von GitHub oder nutze F-Droid. + +--- + +**Letzte Aktualisierung:** v1.3.2 (2026-01-10) diff --git a/docs/FEATURES.en.md b/docs/FEATURES.en.md deleted file mode 100644 index 0fbdb8f..0000000 --- a/docs/FEATURES.en.md +++ /dev/null @@ -1,284 +0,0 @@ -# Complete Feature List 📋 - -**🌍 Languages:** [Deutsch](FEATURES.md) · **English** - -> All features of Simple Notes Sync in detail - ---- - -## 📝 Note Management - -### Note Types -- ✅ **Text notes** - Classic free-form notes -- ✅ **Checklists** _(NEW in v1.4.0)_ - Task lists with tap-to-check - - ➕ Add items via input field - - ☑️ Tap to check/uncheck - - 📌 Long-press for drag & drop sorting - - 🗑️ Swipe-to-delete individual items - - ~~Strikethrough~~ for completed entries - -### Basic Features -- ✅ **Auto-save** - No manual saving needed -- ✅ **Title + content** - Clear structure for each note -- ✅ **Timestamps** - Creation and modification date automatically -- ✅ **Swipe-to-delete** - Intuitive gesture for deletion -- ✅ **Confirmation dialog** - Protection against accidental deletion -- ✅ **Material Design 3** - Modern, clean UI -- ✅ **Dark mode** - Automatically based on system settings -- ✅ **Dynamic colors** - Adapts to your Android theme - -### Editor -- ✅ **Minimalist editor** - No bells and whistles -- ✅ **Auto-focus** - Start writing immediately -- ✅ **Fullscreen mode** - Maximum writing space -- ✅ **Save button** - Manual confirmation possible -- ✅ **Back navigation** - Saves automatically - ---- - -## 💾 Backup & Restore - -### Local Backup System -- ✅ **JSON export** - All notes in one file -- ✅ **Free location choice** - Downloads, SD card, cloud folder -- ✅ **Filenames with timestamp** - `simplenotes_backup_YYYY-MM-DD_HHmmss.json` -- ✅ **Complete export** - Title, content, timestamps, IDs -- ✅ **Human-readable format** - JSON with formatting -- ✅ **Independent from server** - Works completely offline - -### Restore Modes -- ✅ **Merge** - Add new notes, keep existing ones _(Default)_ -- ✅ **Replace** - Delete all and import backup -- ✅ **Overwrite duplicates** - Backup wins on ID conflicts -- ✅ **Automatic safety backup** - Before every restore -- ✅ **Backup validation** - Checks format and version -- ✅ **Error handling** - Clear error messages on issues - ---- - -## 🖥️ Desktop Integration - -### Markdown Export -- ✅ **Automatic export** - Each note → `.md` file -- ✅ **Checklists as task lists** _(NEW)_ - `- [ ]` / `- [x]` format (GitHub-compatible) -- ✅ **Dual-format** - JSON (master) + Markdown (mirror) -- ✅ **Filename sanitization** - Safe filenames from titles -- ✅ **Duplicate handling** _(NEW)_ - ID suffix for same titles -- ✅ **Frontmatter metadata** - YAML with ID, timestamps, type -- ✅ **WebDAV sync** - Parallel to JSON sync -- ✅ **Optional** - Toggle in settings -- ✅ **Initial export** - All existing notes when activated -- ✅ **Progress indicator** - Shows X/Y during export - -### Markdown Import -- ✅ **Desktop → App** - Import changes from desktop -- ✅ **Last-Write-Wins** - Conflict resolution via timestamp -- ✅ **Frontmatter parsing** - Reads metadata from `.md` files -- ✅ **Detect new notes** - Automatically adopt to app -- ✅ **Detect updates** - Only if desktop version is newer -- ✅ **Error tolerance** - Individual errors don't abort import - -### WebDAV Access -- ✅ **Network drive mount** - Windows, macOS, Linux -- ✅ **Any Markdown editor** - VS Code, Typora, Notepad++, iA Writer -- ✅ **Live editing** - Direct access to `.md` files -- ✅ **Folder structure** - `/notes/` for JSON, `/notes-md/` for Markdown -- ✅ **Automatic folder creation** - On first sync - ---- - -## 🔄 Synchronization - -### Auto-Sync -- ✅ **Interval selection** - 15, 30 or 60 minutes -- ✅ **WiFi trigger** - Sync on WiFi connection _(no SSID restriction)_ -- ✅ **Battery-friendly** - ~0.2-0.8% per day -- ✅ **Smart server check** - Sync only when server is reachable -- ✅ **WorkManager** - Reliable background execution -- ✅ **Battery optimization compatible** - Works even with Doze mode - -### Sync Triggers (6 total) -1. ✅ **Periodic sync** - Automatically after interval -2. ✅ **App-start sync** - When opening the app -3. ✅ **WiFi-connect sync** - On any WiFi connection -4. ✅ **Manual sync** - Button in settings -5. ✅ **Pull-to-refresh** - Swipe gesture in notes list -6. ✅ **Settings-save sync** - After server configuration - -### Sync Mechanism -- ✅ **Upload** - Local changes to server -- ✅ **Download** - Server changes to app -- ✅ **Conflict detection** - On simultaneous changes -- ✅ **Conflict-free merging** - Last-Write-Wins via timestamp -- ✅ **Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT -- ✅ **Error handling** - Retry on network issues -- ✅ **Offline-first** - App works without server - -### Server Connection -- ✅ **WebDAV protocol** - Standard protocol -- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external -- ✅ **Username/password** - Basic authentication -- ✅ **Connection test** - Test in settings -- ✅ **Server URL normalization** - Automatic `/notes/` and `/notes-md/` _(NEW in v1.2.1)_ -- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/` - ---- - -## 🔒 Privacy & Security - -### Self-Hosted -- ✅ **Own server** - Full control over data -- ✅ **No cloud** - No third parties -- ✅ **No tracking** - No analytics, no telemetry -- ✅ **No account** - Only server credentials -- ✅ **100% open source** - MIT License - -### Data Security -- ✅ **Local storage** - App-private storage (Android) -- ✅ **WebDAV encryption** - HTTPS for external servers -- ✅ **Password storage** - Android SharedPreferences (encrypted) -- ✅ **No third-party libs** - Only Android SDK + Sardine (WebDAV) - -### Developer Features -- ✅ **File logging** - Optional, only when enabled _(NEW in v1.3.2)_ -- ✅ **Privacy notice** - Explicit warning on activation -- ✅ **Local logs** - Logs stay on device - ---- - -## 🔋 Performance & Optimization - -### Battery Efficiency -- ✅ **Optimized sync intervals** - 15/30/60 min -- ✅ **WiFi-only** - No mobile data sync -- ✅ **Smart server check** - Sync only when server is reachable -- ✅ **WorkManager** - System-optimized execution -- ✅ **Doze mode compatible** - Sync runs even in standby -- ✅ **Measured consumption:** - - 15 min: ~0.8% / day (~23 mAh) - - 30 min: ~0.4% / day (~12 mAh) ⭐ _Recommended_ - - 60 min: ~0.2% / day (~6 mAh) - -### App Performance -- ✅ **Offline-first** - Works without internet -- ✅ **Instant-load** - Notes load in <100ms -- ✅ **Smooth scrolling** - RecyclerView with ViewHolder -- ✅ **Material Design 3** - Native Android UI -- ✅ **Kotlin Coroutines** - Asynchronous operations -- ✅ **Minimal APK size** - ~2 MB - ---- - -## 🛠️ Technical Details - -### Platform -- ✅ **Android 8.0+** (API 26+) -- ✅ **Target SDK 36** (Android 15) -- ✅ **Kotlin** - Modern programming language -- ✅ **Material Design 3** - Latest design guidelines -- ✅ **ViewBinding** - Type-safe view references - -### Architecture -- ✅ **MVVM-Light** - Simple architecture -- ✅ **Single Activity** - Modern navigation -- ✅ **Kotlin Coroutines** - Async/Await pattern -- ✅ **Dispatchers.IO** - Background operations -- ✅ **SharedPreferences** - Settings storage -- ✅ **File-based storage** - JSON files locally -- ✅ **Custom exceptions** - Dedicated SyncException for better error handling _(NEW in v1.3.2)_ - -### Dependencies -- ✅ **AndroidX** - Jetpack libraries -- ✅ **Material Components** - Material Design 3 -- ✅ **Sardine** - WebDAV client (com.thegrizzlylabs) -- ✅ **Gson** - JSON serialization -- ✅ **WorkManager** - Background tasks -- ✅ **OkHttp** - HTTP client (via Sardine) - -### Build Variants -- ✅ **Standard** - Universal APK (100% FOSS, no Google dependencies) -- ✅ **F-Droid** - Identical to Standard (100% FOSS) -- ✅ **Debug/Release** - Development and production -- ✅ **No Google Services** - Completely FOSS, no proprietary libraries - ---- - -## 📦 Server Compatibility - -### Tested WebDAV Servers -- ✅ **Docker WebDAV** (recommended for self-hosting) -- ✅ **Nextcloud** - Fully compatible -- ✅ **ownCloud** - Works perfectly -- ✅ **Apache mod_dav** - Standard WebDAV -- ✅ **nginx + WebDAV** - With correct configuration - -### Server Features -- ✅ **Basic Auth** - Username/password -- ✅ **Directory listing** - For download -- ✅ **PUT/GET** - Upload/download -- ✅ **MKCOL** - Create folders -- ✅ **DELETE** - Delete notes (future) - ---- - -## 🔮 Future Features - -Planned for upcoming versions: - -### v1.4.0 - Checklists -- ⏳ **Checklist notes** - New note type with checkboxes -- ⏳ **Completed items** - Strike-through/check off -- ⏳ **Drag & drop** - Reorder items - -### v1.5.0 - Internationalization -- ⏳ **Multi-language** - German + English UI -- ⏳ **Language selection** - Selectable in settings -- ⏳ **Full translation** - All strings in both languages - -### v1.6.0 - Modern APIs -- ⏳ **Replace LocalBroadcastManager** - Use SharedFlow instead -- ⏳ **PackageInfo Flags** - Use PackageInfoFlags.of() -- ⏳ **Complexity refactoring** - Split long functions - ---- - -## 📊 Comparison with Other Apps - -| Feature | Simple Notes Sync | Google Keep | Nextcloud Notes | -|---------|------------------|-------------|-----------------| -| Offline-first | ✅ | ⚠️ Limited | ⚠️ Limited | -| Self-hosted | ✅ | ❌ | ✅ | -| Auto-sync | ✅ | ✅ | ✅ | -| Markdown export | ✅ | ❌ | ✅ | -| Desktop access | ✅ (WebDAV) | ✅ (Web) | ✅ (Web + WebDAV) | -| Local backup | ✅ | ❌ | ⚠️ Server backup | -| No Google account | ✅ | ❌ | ✅ | -| Open source | ✅ MIT | ❌ | ✅ AGPL | -| APK size | ~2 MB | ~50 MB | ~8 MB | -| Battery usage | ~0.4%/day | ~1-2%/day | ~0.5%/day | - ---- - -## ❓ FAQ - -**Q: Do I need a server?** -A: No! The app works completely offline. The server is optional for sync. - -**Q: Which server is best?** -A: For beginners: Docker WebDAV (simple, easy). For pros: Nextcloud (many features). - -**Q: Does Markdown export work without Desktop Integration?** -A: No, you need to activate the feature in settings. - -**Q: Will my data be lost if I switch servers?** -A: No! Create a local backup, switch servers, restore. - -**Q: Why JSON + Markdown?** -A: JSON is reliable and fast (master). Markdown is human-readable (mirror for desktop). - -**Q: Can I use the app without Google Play?** -A: Yes! Download the APK directly from GitHub or use F-Droid. - ---- - -**Last update:** v1.3.2 (2026-01-10) diff --git a/docs/FEATURES.md b/docs/FEATURES.md index c00d7ba..09d39f8 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,284 +1,305 @@ -# Vollständige Feature-Liste 📋 +# Complete Feature List 📋 -**🌍 Languages:** **Deutsch** · [English](FEATURES.en.md) +**🌍 Languages:** [Deutsch](FEATURES.de.md) · **English** -> Alle Features von Simple Notes Sync im Detail +> All features of Simple Notes Sync in detail --- -## 📝 Notiz-Verwaltung +## 📝 Note Management -### Notiz-Typen -- ✅ **Textnotizen** - Klassische Freitext-Notizen -- ✅ **Checklisten** _(NEU in v1.4.0)_ - Aufgabenlisten mit Tap-to-Check - - ➕ Items hinzufügen über Eingabefeld - - ☑️ Tap zum Abhaken/Wieder-Öffnen - - 📌 Long-Press für Drag & Drop Sortierung - - 🗑️ Swipe-to-Delete für einzelne Items - - ~~Durchstreichen~~ bei erledigten Einträgen +### Note Types +- ✅ **Text notes** - Classic free-form notes +- ✅ **Checklists** _(NEW in v1.4.0)_ - Task lists with tap-to-check + - ➕ Add items via input field + - ☑️ Tap to check/uncheck + - 📌 Long-press for drag & drop sorting + - ~~Strikethrough~~ for completed entries -### Basis-Funktionen -- ✅ **Automatisches Speichern** - Kein manuelles Speichern nötig -- ✅ **Titel + Inhalt** - Klare Struktur für jede Notiz -- ✅ **Zeitstempel** - Erstellungs- und Änderungsdatum automatisch -- ✅ **Swipe-to-Delete** - Intuitive Geste zum Löschen -- ✅ **Bestätigungs-Dialog** - Schutz vor versehentlichem Löschen -- ✅ **Material Design 3** - Moderne, saubere UI -- ✅ **Dark Mode** - Automatisch je nach System-Einstellung -- ✅ **Dynamic Colors** - Passt sich deinem Android-Theme an +### Basic Features +- ✅ **Auto-save** - No manual saving needed +- ✅ **Title + content** - Clear structure for each note +- ✅ **Timestamps** - Creation and modification date automatically +- ✅ **Selection Mode** _(NEW in v1.5.0)_ - Long-press for multi-select and batch delete +- ✅ **Confirmation dialog** - Protection against accidental deletion +- ✅ **Jetpack Compose UI** _(NEW in v1.5.0)_ - Modern, performant user interface +- ✅ **Material Design 3** - Modern, clean UI +- ✅ **Dark mode** - Automatically based on system settings +- ✅ **Dynamic colors** - Adapts to your Android theme ### Editor -- ✅ **Minimalistischer Editor** - Kein Schnickschnack -- ✅ **Auto-Fokus** - Direkt losschreiben -- ✅ **Vollbild-Modus** - Maximale Schreibfläche -- ✅ **Speichern-Button** - Manuelle Bestätigung möglich -- ✅ **Zurück-Navigation** - Speichert automatisch +- ✅ **Minimalist editor** - No bells and whistles +- ✅ **Auto-focus** - Start writing immediately +- ✅ **Fullscreen mode** - Maximum writing space +- ✅ **Save button** - Manual confirmation possible +- ✅ **Back navigation** - Saves automatically +- ✅ **Slide animations** _(NEW in v1.5.0)_ - Smooth transitions --- -## 💾 Backup & Wiederherstellung +## 🌍 Multilingual Support _(NEW in v1.5.0)_ -### Lokales Backup System -- ✅ **JSON-Export** - Alle Notizen in einer Datei -- ✅ **Freie Speicherort-Wahl** - Downloads, SD-Karte, Cloud-Ordner -- ✅ **Dateinamen mit Zeitstempel** - `simplenotes_backup_YYYY-MM-DD_HHmmss.json` -- ✅ **Vollständiger Export** - Titel, Inhalt, Timestamps, IDs -- ✅ **Menschenlesbares Format** - JSON mit Formatierung -- ✅ **Unabhängig vom Server** - Funktioniert komplett offline +### Supported Languages +- ✅ **English** - Primary language (default) +- ✅ **German** - Fully translated -### Wiederherstellungs-Modi -- ✅ **Zusammenführen (Merge)** - Neue Notizen hinzufügen, bestehende behalten _(Standard)_ -- ✅ **Ersetzen (Replace)** - Alle löschen und Backup importieren -- ✅ **Duplikate überschreiben (Overwrite)** - Backup gewinnt bei ID-Konflikten -- ✅ **Automatisches Sicherheits-Backup** - Vor jeder Wiederherstellung -- ✅ **Backup-Validierung** - Prüft Format und Version -- ✅ **Fehlerbehandlung** - Klare Fehlermeldungen bei Problemen +### Language Selection +- ✅ **Automatic detection** - Follows system language +- ✅ **Manual selection** - Switchable in settings +- ✅ **Per-App Language** - Android 13+ native language selection +- ✅ **locales_config.xml** - Complete Android integration + +### Scope +- ✅ **400+ strings** - Fully translated +- ✅ **UI texts** - All buttons, dialogs, menus +- ✅ **Error messages** - Helpful localized hints +- ✅ **Settings** - 7 categorized screens --- -## 🖥️ Desktop-Integration +## 💾 Backup & Restore -### Markdown-Export -- ✅ **Automatischer Export** - Jede Notiz → `.md` Datei -- ✅ **Checklisten als Task-Listen** _(NEU)_ - `- [ ]` / `- [x]` Format (GitHub-kompatibel) -- ✅ **Dual-Format** - JSON (Master) + Markdown (Mirror) -- ✅ **Dateinamen-Sanitization** - Sichere Dateinamen aus Titeln -- ✅ **Duplikat-Handling** _(NEU)_ - ID-Suffix bei gleichen Titeln -- ✅ **Frontmatter-Metadata** - YAML mit ID, Timestamps, Type -- ✅ **WebDAV-Sync** - Parallel zum JSON-Sync -- ✅ **Optional** - In Einstellungen ein/ausschaltbar -- ✅ **Initial Export** - Alle bestehenden Notizen beim Aktivieren -- ✅ **Progress-Anzeige** - Zeigt X/Y beim Export +### Local Backup System +- ✅ **JSON export** - All notes in one file +- ✅ **Free location choice** - Downloads, SD card, cloud folder +- ✅ **Filenames with timestamp** - `simplenotes_backup_YYYY-MM-DD_HHmmss.json` +- ✅ **Complete export** - Title, content, timestamps, IDs +- ✅ **Human-readable format** - JSON with formatting +- ✅ **Independent from server** - Works completely offline -### Markdown-Import -- ✅ **Desktop → App** - Änderungen vom Desktop importieren -- ✅ **Last-Write-Wins** - Konfliktauflösung via Timestamp -- ✅ **Frontmatter-Parsing** - Liest Metadata aus `.md` Dateien -- ✅ **Neue Notizen erkennen** - Automatisch in App übernehmen -- ✅ **Updates erkennen** - Nur wenn Desktop-Version neuer ist -- ✅ **Fehlertoleranz** - Einzelne Fehler brechen Import nicht ab - -### WebDAV-Zugriff -- ✅ **Network Drive Mount** - Windows, macOS, Linux -- ✅ **Jeder Markdown-Editor** - VS Code, Typora, Notepad++, iA Writer -- ✅ **Live-Bearbeitung** - Direkter Zugriff auf `.md` Dateien -- ✅ **Ordner-Struktur** - `/notes/` für JSON, `/notes-md/` für Markdown -- ✅ **Automatische Ordner-Erstellung** - Beim ersten Sync +### Restore Modes +- ✅ **Merge** - Add new notes, keep existing ones _(Default)_ +- ✅ **Replace** - Delete all and import backup +- ✅ **Overwrite duplicates** - Backup wins on ID conflicts +- ✅ **Automatic safety backup** - Before every restore +- ✅ **Backup validation** - Checks format and version +- ✅ **Error handling** - Clear error messages on issues --- -## 🔄 Synchronisation +## 🖥️ Desktop Integration + +### Markdown Export +- ✅ **Automatic export** - Each note → `.md` file +- ✅ **Checklists as task lists** _(NEW)_ - `- [ ]` / `- [x]` format (GitHub-compatible) +- ✅ **Dual-format** - JSON (master) + Markdown (mirror) +- ✅ **Filename sanitization** - Safe filenames from titles +- ✅ **Duplicate handling** _(NEW)_ - ID suffix for same titles +- ✅ **Frontmatter metadata** - YAML with ID, timestamps, type +- ✅ **WebDAV sync** - Parallel to JSON sync +- ✅ **Optional** - Toggle in settings +- ✅ **Initial export** - All existing notes when activated +- ✅ **Progress indicator** - Shows X/Y during export + +### Markdown Import +- ✅ **Desktop → App** - Import changes from desktop +- ✅ **Last-Write-Wins** - Conflict resolution via timestamp +- ✅ **Frontmatter parsing** - Reads metadata from `.md` files +- ✅ **Detect new notes** - Automatically adopt to app +- ✅ **Detect updates** - Only if desktop version is newer +- ✅ **Error tolerance** - Individual errors don't abort import + +### WebDAV Access +- ✅ **Network drive mount** - Windows, macOS, Linux +- ✅ **Any Markdown editor** - VS Code, Typora, Notepad++, iA Writer +- ✅ **Live editing** - Direct access to `.md` files +- ✅ **Folder structure** - `/notes/` for JSON, `/notes-md/` for Markdown +- ✅ **Automatic folder creation** - On first sync + +--- + +## 🔄 Synchronization ### Auto-Sync -- ✅ **Intervall-Auswahl** - 15, 30 oder 60 Minuten -- ✅ **WiFi-Trigger** - Sync bei WiFi-Verbindung _(keine SSID-Einschränkung)_ -- ✅ **Akkuschonend** - ~0.2-0.8% pro Tag -- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar -- ✅ **WorkManager** - Zuverlässige Background-Ausführung -- ✅ **Battery-Optimierung kompatibel** - Funktioniert auch mit Doze Mode +- ✅ **Interval selection** - 15, 30 or 60 minutes +- ✅ **WiFi trigger** - Sync on WiFi connection _(no SSID restriction)_ +- ✅ **Battery-friendly** - ~0.2-0.8% per day +- ✅ **Smart server check** - Sync only when server is reachable +- ✅ **WorkManager** - Reliable background execution +- ✅ **Battery optimization compatible** - Works even with Doze mode -### Sync-Trigger (6 Stück) -1. ✅ **Periodic Sync** - Automatisch nach Intervall -2. ✅ **App-Start Sync** - Beim Öffnen der App -3. ✅ **WiFi-Connect Sync** - Bei jeder WiFi-Verbindung -4. ✅ **Manual Sync** - Button in Einstellungen -5. ✅ **Pull-to-Refresh** - Wisch-Geste in Notizliste -6. ✅ **Settings-Save Sync** - Nach Server-Konfiguration +### Sync Triggers (6 total) +1. ✅ **Periodic sync** - Automatically after interval +2. ✅ **App-start sync** - When opening the app +3. ✅ **WiFi-connect sync** - On any WiFi connection +4. ✅ **Manual sync** - Button in settings +5. ✅ **Pull-to-refresh** - Swipe gesture in notes list +6. ✅ **Settings-save sync** - After server configuration -### Sync-Mechanismus -- ✅ **Upload** - Lokale Änderungen zum Server -- ✅ **Download** - Server-Änderungen in App -- ✅ **Konflikt-Erkennung** - Bei gleichzeitigen Änderungen -- ✅ **Konfliktfreies Merging** - Last-Write-Wins via Timestamp -- ✅ **Sync-Status Tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT -- ✅ **Fehlerbehandlung** - Retry bei Netzwerkproblemen -- ✅ **Offline-First** - App funktioniert ohne Server +### Sync Mechanism +- ✅ **Upload** - Local changes to server +- ✅ **Download** - Server changes to app +- ✅ **Conflict detection** - On simultaneous changes +- ✅ **Conflict-free merging** - Last-Write-Wins via timestamp +- ✅ **Sync status tracking** - LOCAL_ONLY, PENDING, SYNCED, CONFLICT +- ✅ **Error handling** - Retry on network issues +- ✅ **Offline-first** - App works without server -### Server-Verbindung -- ✅ **WebDAV-Protokoll** - Standard-Protokoll -- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern -- ✅ **Username/Password** - Basic Authentication -- ✅ **Connection Test** - In Einstellungen testen -- ✅ **Server-URL Normalisierung** - Automatisches `/notes/` und `/notes-md/` _(NEU in v1.2.1)_ -- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/` +### Server Connection +- ✅ **WebDAV protocol** - Standard protocol +- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external +- ✅ **Username/password** - Basic authentication +- ✅ **Connection test** - Test in settings +- ✅ **Server URL normalization** - Automatic `/notes/` and `/notes-md/` _(NEW in v1.2.1)_ +- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/` --- -## 🔒 Privacy & Sicherheit +## 🔒 Privacy & Security ### Self-Hosted -- ✅ **Eigener Server** - Volle Kontrolle über Daten -- ✅ **Keine Cloud** - Keine Drittanbieter -- ✅ **Kein Tracking** - Keine Analytik, keine Telemetrie -- ✅ **Kein Account** - Nur Server-Zugangsdaten -- ✅ **100% Open Source** - MIT Lizenz +- ✅ **Own server** - Full control over data +- ✅ **No cloud** - No third parties +- ✅ **No tracking** - No analytics, no telemetry +- ✅ **No account** - Only server credentials +- ✅ **100% open source** - MIT License -### Daten-Sicherheit -- ✅ **Lokale Speicherung** - App-Private Storage (Android) -- ✅ **WebDAV-Verschlüsselung** - HTTPS für externe Server -- ✅ **Passwort-Speicherung** - Android SharedPreferences (verschlüsselt) -- ✅ **Keine Drittanbieter-Libs** - Nur Android SDK + Sardine (WebDAV) +### Data Security +- ✅ **Local storage** - App-private storage (Android) +- ✅ **WebDAV encryption** - HTTPS for external servers +- ✅ **Password storage** - Android SharedPreferences (encrypted) +- ✅ **No third-party libs** - Only Android SDK + Sardine (WebDAV) -### Entwickler-Features -- ✅ **Datei-Logging** - Optional, nur bei Aktivierung _(NEU in v1.3.2)_ -- ✅ **Datenschutz-Hinweis** - Explizite Warnung bei Aktivierung -- ✅ **Lokale Logs** - Logs bleiben auf dem Gerät +### Developer Features +- ✅ **File logging** - Optional, only when enabled _(NEW in v1.3.2)_ +- ✅ **Privacy notice** - Explicit warning on activation +- ✅ **Local logs** - Logs stay on device --- -## 🔋 Performance & Optimierung +## 🔋 Performance & Optimization -### Akku-Effizienz -- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min -- ✅ **WiFi-Only** - Kein Mobile Data Sync -- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar -- ✅ **WorkManager** - System-optimierte Ausführung -- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby -- ✅ **Gemessener Verbrauch:** - - 15 Min: ~0.8% / Tag (~23 mAh) - - 30 Min: ~0.4% / Tag (~12 mAh) ⭐ _Empfohlen_ - - 60 Min: ~0.2% / Tag (~6 mAh) +### Battery Efficiency +- ✅ **Optimized sync intervals** - 15/30/60 min +- ✅ **WiFi-only** - No mobile data sync +- ✅ **Smart server check** - Sync only when server is reachable +- ✅ **WorkManager** - System-optimized execution +- ✅ **Doze mode compatible** - Sync runs even in standby +- ✅ **Measured consumption:** + - 15 min: ~0.8% / day (~23 mAh) + - 30 min: ~0.4% / day (~12 mAh) ⭐ _Recommended_ + - 60 min: ~0.2% / day (~6 mAh) -### App-Performance -- ✅ **Offline-First** - Funktioniert ohne Internet -- ✅ **Instant-Load** - Notizen laden in <100ms -- ✅ **Smooth Scrolling** - RecyclerView mit ViewHolder +### App Performance +- ✅ **Offline-first** - Works without internet +- ✅ **Instant-load** - Notes load in <100ms +- ✅ **Smooth scrolling** - RecyclerView with ViewHolder - ✅ **Material Design 3** - Native Android UI -- ✅ **Kotlin Coroutines** - Asynchrone Operationen -- ✅ **Minimale APK-Größe** - ~2 MB +- ✅ **Kotlin Coroutines** - Asynchronous operations +- ✅ **Minimal APK size** - ~2 MB --- -## 🛠️ Technische Details +## 🛠️ Technical Details -### Plattform +### Platform - ✅ **Android 8.0+** (API 26+) - ✅ **Target SDK 36** (Android 15) -- ✅ **Kotlin** - Moderne Programmiersprache -- ✅ **Material Design 3** - Neueste Design-Richtlinien -- ✅ **ViewBinding** - Typ-sichere View-Referenzen +- ✅ **Kotlin** - Modern programming language +- ✅ **Material Design 3** - Latest design guidelines +- ✅ **ViewBinding** - Type-safe view references -### Architektur -- ✅ **MVVM-Light** - Einfache Architektur -- ✅ **Single Activity** - Moderne Navigation -- ✅ **Kotlin Coroutines** - Async/Await Pattern -- ✅ **Dispatchers.IO** - Background-Operationen -- ✅ **SharedPreferences** - Settings-Speicherung -- ✅ **File-Based Storage** - JSON-Dateien lokal -- ✅ **Custom Exceptions** - Dedizierte SyncException für bessere Fehlerbehandlung _(NEU in v1.3.2)_ +### Architecture +- ✅ **MVVM-Light** - Simple architecture +- ✅ **Single Activity** - Modern navigation +- ✅ **Kotlin Coroutines** - Async/Await pattern +- ✅ **Dispatchers.IO** - Background operations +- ✅ **SharedPreferences** - Settings storage +- ✅ **File-based storage** - JSON files locally +- ✅ **Custom exceptions** - Dedicated SyncException for better error handling _(NEW in v1.3.2)_ -### Abhängigkeiten -- ✅ **AndroidX** - Jetpack Libraries +### Dependencies +- ✅ **AndroidX** - Jetpack libraries - ✅ **Material Components** - Material Design 3 -- ✅ **Sardine** - WebDAV Client (com.thegrizzlylabs) -- ✅ **Gson** - JSON Serialization -- ✅ **WorkManager** - Background Tasks -- ✅ **OkHttp** - HTTP Client (via Sardine) +- ✅ **Sardine** - WebDAV client (com.thegrizzlylabs) +- ✅ **Gson** - JSON serialization +- ✅ **WorkManager** - Background tasks +- ✅ **OkHttp** - HTTP client (via Sardine) -### Build-Varianten -- ✅ **Standard** - Universal APK (100% FOSS, keine Google-Dependencies) -- ✅ **F-Droid** - Identisch mit Standard (100% FOSS) -- ✅ **Debug/Release** - Entwicklung und Production -- ✅ **Keine Google Services** - Komplett FOSS, keine proprietären Bibliotheken +### Build Variants +- ✅ **Standard** - Universal APK (100% FOSS, no Google dependencies) +- ✅ **F-Droid** - Identical to Standard (100% FOSS) +- ✅ **Debug/Release** - Development and production +- ✅ **No Google Services** - Completely FOSS, no proprietary libraries --- -## 📦 Server-Kompatibilität +## 📦 Server Compatibility -### Getestete WebDAV-Server -- ✅ **Docker WebDAV** (empfohlen für Self-Hosting) -- ✅ **Nextcloud** - Vollständig kompatibel -- ✅ **ownCloud** - Funktioniert einwandfrei +### Tested WebDAV Servers +- ✅ **Docker WebDAV** (recommended for self-hosting) +- ✅ **Nextcloud** - Fully compatible +- ✅ **ownCloud** - Works perfectly - ✅ **Apache mod_dav** - Standard WebDAV -- ✅ **nginx + WebDAV** - Mit korrekter Konfiguration +- ✅ **nginx + WebDAV** - With correct configuration -### Server-Features -- ✅ **Basic Auth** - Username/Password -- ✅ **Directory Listing** - Für Download -- ✅ **PUT/GET** - Upload/Download -- ✅ **MKCOL** - Ordner erstellen -- ✅ **DELETE** - Notizen löschen (zukünftig) +### Server Features +- ✅ **Basic Auth** - Username/password +- ✅ **Directory listing** - For download +- ✅ **PUT/GET** - Upload/download +- ✅ **MKCOL** - Create folders +- ✅ **DELETE** - Delete notes (future) --- -## 🔮 Zukünftige Features +## 🔮 Future Features -Geplant für kommende Versionen: +Planned for upcoming versions: -### v1.4.0 - Checklisten -- ⏳ **Checklisten-Notizen** - Neuer Notiz-Typ mit Checkboxen -- ⏳ **Erledigte Items** - Durchstreichen/Abhaken -- ⏳ **Drag & Drop** - Items neu anordnen +### v1.4.0 - Checklists +- ⏳ **Checklist notes** - New note type with checkboxes +- ⏳ **Completed items** - Strike-through/check off +- ⏳ **Drag & drop** - Reorder items -### v1.5.0 - Internationalisierung -- ⏳ **Mehrsprachigkeit** - Deutsch + Englisch UI -- ⏳ **Sprachauswahl** - In Einstellungen wählbar -- ⏳ **Vollständige Übersetzung** - Alle Strings in beiden Sprachen +### v1.5.0 - Internationalization +- ⏳ **Multi-language** - German + English UI +- ⏳ **Language selection** - Selectable in settings +- ⏳ **Full translation** - All strings in both languages ### v1.6.0 - Modern APIs -- ⏳ **LocalBroadcastManager ersetzen** - SharedFlow stattdessen -- ⏳ **PackageInfo Flags** - PackageInfoFlags.of() verwenden -- ⏳ **Komplexitäts-Refactoring** - Lange Funktionen aufteilen +- ⏳ **Replace LocalBroadcastManager** - Use SharedFlow instead +- ⏳ **PackageInfo Flags** - Use PackageInfoFlags.of() +- ⏳ **Complexity refactoring** - Split long functions --- -## 📊 Vergleich mit anderen Apps +## 📊 Comparison with Other Apps | Feature | Simple Notes Sync | Google Keep | Nextcloud Notes | |---------|------------------|-------------|-----------------| -| Offline-First | ✅ | ⚠️ Eingeschränkt | ⚠️ Eingeschränkt | -| Self-Hosted | ✅ | ❌ | ✅ | -| Auto-Sync | ✅ | ✅ | ✅ | -| Markdown-Export | ✅ | ❌ | ✅ | -| Desktop-Zugriff | ✅ (WebDAV) | ✅ (Web) | ✅ (Web + WebDAV) | -| Lokales Backup | ✅ | ❌ | ⚠️ Server-Backup | -| Kein Google-Account | ✅ | ❌ | ✅ | -| Open Source | ✅ MIT | ❌ | ✅ AGPL | -| APK-Größe | ~2 MB | ~50 MB | ~8 MB | -| Akku-Verbrauch | ~0.4%/Tag | ~1-2%/Tag | ~0.5%/Tag | +| Offline-first | ✅ | ⚠️ Limited | ⚠️ Limited | +| Self-hosted | ✅ | ❌ | ✅ | +| Auto-sync | ✅ | ✅ | ✅ | +| Markdown export | ✅ | ❌ | ✅ | +| Desktop access | ✅ (WebDAV) | ✅ (Web) | ✅ (Web + WebDAV) | +| Local backup | ✅ | ❌ | ⚠️ Server backup | +| No Google account | ✅ | ❌ | ✅ | +| Open source | ✅ MIT | ❌ | ✅ AGPL | +| APK size | ~2 MB | ~50 MB | ~8 MB | +| Battery usage | ~0.4%/day | ~1-2%/day | ~0.5%/day | --- ## ❓ FAQ -**Q: Brauche ich einen Server?** -A: Nein! Die App funktioniert auch komplett offline. Der Server ist optional für Sync. +**Q: Do I need a server?** +A: No! The app works completely offline. The server is optional for sync. -**Q: Welcher Server ist am besten?** -A: Für Einstieg: Docker WebDAV (einfach, leicht). Für Profis: Nextcloud (viele Features). +**Q: Which server is best?** +A: For beginners: Docker WebDAV (simple, easy). For pros: Nextcloud (many features). -**Q: Funktioniert Markdown-Export ohne Desktop-Integration?** -A: Nein, du musst das Feature in den Einstellungen aktivieren. +**Q: Does Markdown export work without Desktop Integration?** +A: No, you need to activate the feature in settings. -**Q: Gehen meine Daten verloren wenn ich den Server wechsle?** -A: Nein! Erstelle ein lokales Backup, wechsle Server, stelle wieder her. +**Q: Will my data be lost if I switch servers?** +A: No! Create a local backup, switch servers, restore. -**Q: Warum JSON + Markdown?** -A: JSON ist zuverlässig und schnell (Master). Markdown ist menschenlesbar (Mirror für Desktop). +**Q: Why JSON + Markdown?** +A: JSON is reliable and fast (master). Markdown is human-readable (mirror for desktop). -**Q: Kann ich die App ohne Google Play nutzen?** -A: Ja! Lade die APK direkt von GitHub oder nutze F-Droid. +**Q: Can I use the app without Google Play?** +A: Yes! Download the APK directly from GitHub or use F-Droid. --- -**Letzte Aktualisierung:** v1.3.2 (2026-01-10) +**Last update:** v1.3.2 (2026-01-10) diff --git a/docs/TRANSLATING.de.md b/docs/TRANSLATING.de.md new file mode 100644 index 0000000..505f47f --- /dev/null +++ b/docs/TRANSLATING.de.md @@ -0,0 +1,151 @@ +# Übersetzung beitragen 🌍 + +**🌍 Sprachen:** **Deutsch** · [English](TRANSLATING.md) + +> So kannst du Simple Notes Sync in deine Sprache übersetzen! + +--- + +## 📋 Übersicht + +Simple Notes Sync unterstützt aktuell: +- 🇺🇸 **Englisch** (en) - Primärsprache +- 🇩🇪 **Deutsch** (de) - Vollständig übersetzt + +Wir freuen uns über neue Übersetzungen! + +--- + +## 🚀 Schnellstart + +### 1. Repository forken + +1. Gehe zu [github.com/inventory69/simple-notes-sync](https://github.com/inventory69/simple-notes-sync) +2. Klicke auf **Fork** (oben rechts) +3. Clone dein Fork: `git clone https://github.com/DEIN-USERNAME/simple-notes-sync.git` + +### 2. Sprachdateien erstellen + +```bash +cd simple-notes-sync/android/app/src/main/res + +# Ordner für deine Sprache erstellen (z.B. Französisch) +mkdir values-fr + +# Strings kopieren +cp values/strings.xml values-fr/strings.xml +``` + +### 3. Strings übersetzen + +Öffne `values-fr/strings.xml` und übersetze alle ``-Einträge: + +```xml + +Simple Notes +Notes + + +Notes Simples +Notes +``` + +**Wichtig:** +- Übersetze nur den Text zwischen `>` und `` +- Ändere NICHT die `name="..."` Attribute +- Behalte `%s`, `%d`, `%1$s` etc. als Platzhalter + +### 4. locales_config.xml aktualisieren + +Füge deine Sprache in `android/app/src/main/res/xml/locales_config.xml` hinzu: + +```xml + + + + + +``` + +### 5. Pull Request erstellen + +1. Committe deine Änderungen +2. Pushe zu deinem Fork +3. Erstelle einen Pull Request mit Titel: `Add [Language] translation` + +--- + +## 📁 Dateistruktur + +``` +android/app/src/main/res/ +├── values/ # Englisch (Fallback) +│ └── strings.xml +├── values-de/ # Deutsch +│ └── strings.xml +├── values-fr/ # Französisch (neu) +│ └── strings.xml +└── xml/ + └── locales_config.xml # Sprachregistrierung +``` + +--- + +## 📝 String-Kategorien + +Die `strings.xml` enthält etwa 400+ Strings, aufgeteilt in: + +| Kategorie | Beschreibung | Anzahl | +|-----------|--------------|--------| +| UI Texte | Buttons, Labels, Titel | ~100 | +| Settings | Alle 7 Einstellungs-Screens | ~150 | +| Dialoge | Bestätigungen, Fehler | ~80 | +| Sync | Synchronisations-Meldungen | ~50 | +| Sonstige | Tooltips, Accessibility | ~30 | + +--- + +## ✅ Qualitätscheckliste + +Vor dem Pull Request: + +- [ ] Alle Strings übersetzt (keine englischen Reste) +- [ ] Platzhalter (`%s`, `%d`) beibehalten +- [ ] Keine XML-Syntaxfehler +- [ ] App startet ohne Crashes +- [ ] Text passt in UI-Elemente (nicht zu lang) +- [ ] `locales_config.xml` aktualisiert + +--- + +## 🔧 Testen + +```bash +cd android +./gradlew app:assembleDebug + +# APK installieren und Sprache in Android-Einstellungen wechseln +``` + +--- + +## ❓ FAQ + +**Muss ich alle Strings übersetzen?** +> Idealerweise ja. Fehlende Strings fallen auf Englisch zurück. + +**Was passiert mit Platzhaltern?** +> `%s` = Text, `%d` = Zahl. Position beibehalten oder mit `%1$s` nummerieren. + +**Wie teste ich meine Übersetzung?** +> App bauen, installieren, in Android-Einstellungen → Apps → Simple Notes → Sprache wählen. + +--- + +## 🙏 Danke! + +Jede Übersetzung hilft Simple Notes Sync mehr Menschen zu erreichen. + +Bei Fragen: [GitHub Issue erstellen](https://github.com/inventory69/simple-notes-sync/issues) + +[← Zurück zur Dokumentation](DOCS.md) diff --git a/docs/TRANSLATING.md b/docs/TRANSLATING.md new file mode 100644 index 0000000..0d8442e --- /dev/null +++ b/docs/TRANSLATING.md @@ -0,0 +1,151 @@ +# Contributing Translations 🌍 + +**🌍 Languages:** [Deutsch](TRANSLATING.de.md) · **English** + +> How to translate Simple Notes Sync into your language! + +--- + +## 📋 Overview + +Simple Notes Sync currently supports: +- 🇺🇸 **English** (en) - Primary language +- 🇩🇪 **German** (de) - Fully translated + +We welcome new translations! + +--- + +## 🚀 Quick Start + +### 1. Fork the Repository + +1. Go to [github.com/inventory69/simple-notes-sync](https://github.com/inventory69/simple-notes-sync) +2. Click **Fork** (top right) +3. Clone your fork: `git clone https://github.com/YOUR-USERNAME/simple-notes-sync.git` + +### 2. Create Language Files + +```bash +cd simple-notes-sync/android/app/src/main/res + +# Create folder for your language (e.g., French) +mkdir values-fr + +# Copy strings +cp values/strings.xml values-fr/strings.xml +``` + +### 3. Translate Strings + +Open `values-fr/strings.xml` and translate all `` entries: + +```xml + +Simple Notes +Notes + + +Notes Simples +Notes +``` + +**Important:** +- Only translate text between `>` and `` +- Do NOT change `name="..."` attributes +- Keep `%s`, `%d`, `%1$s` etc. as placeholders + +### 4. Update locales_config.xml + +Add your language to `android/app/src/main/res/xml/locales_config.xml`: + +```xml + + + + + +``` + +### 5. Create Pull Request + +1. Commit your changes +2. Push to your fork +3. Create a Pull Request with title: `Add [Language] translation` + +--- + +## 📁 File Structure + +``` +android/app/src/main/res/ +├── values/ # English (Fallback) +│ └── strings.xml +├── values-de/ # German +│ └── strings.xml +├── values-fr/ # French (new) +│ └── strings.xml +└── xml/ + └── locales_config.xml # Language registration +``` + +--- + +## 📝 String Categories + +The `strings.xml` contains about 400+ strings, divided into: + +| Category | Description | Count | +|----------|-------------|-------| +| UI Texts | Buttons, labels, titles | ~100 | +| Settings | All 7 settings screens | ~150 | +| Dialogs | Confirmations, errors | ~80 | +| Sync | Synchronization messages | ~50 | +| Other | Tooltips, accessibility | ~30 | + +--- + +## ✅ Quality Checklist + +Before creating your Pull Request: + +- [ ] All strings translated (no English leftovers) +- [ ] Placeholders (`%s`, `%d`) preserved +- [ ] No XML syntax errors +- [ ] App launches without crashes +- [ ] Text fits in UI elements (not too long) +- [ ] `locales_config.xml` updated + +--- + +## 🔧 Testing + +```bash +cd android +./gradlew app:assembleDebug + +# Install APK and switch language in Android settings +``` + +--- + +## ❓ FAQ + +**Do I need to translate all strings?** +> Ideally yes. Missing strings fall back to English. + +**What about placeholders?** +> `%s` = text, `%d` = number. Keep position or use `%1$s` for numbering. + +**How do I test my translation?** +> Build app, install, go to Android Settings → Apps → Simple Notes → Language. + +--- + +## 🙏 Thank You! + +Every translation helps Simple Notes Sync reach more people. + +Questions? [Create a GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) + +[← Back to Documentation](DOCS.md) diff --git a/docs/UPCOMING.de.md b/docs/UPCOMING.de.md new file mode 100644 index 0000000..50f3554 --- /dev/null +++ b/docs/UPCOMING.de.md @@ -0,0 +1,76 @@ +# Geplante Features 🚀 + +**🌍 Sprachen:** **Deutsch** · [English](UPCOMING.md) + +> Was kommt als Nächstes? Hier findest du unsere Pläne für zukünftige Versionen. + +--- + +## v1.5.0 - Jetpack Compose & Internationalisierung ✅ + +> **Status:** Released 🎉 (Januar 2026) + +### 🎨 Jetpack Compose UI + +- ✅ **Komplettes UI-Redesign** - Von XML-Views zu Jetpack Compose +- ✅ **Modernisierte Einstellungen** - 7 kategorisierte Screens +- ✅ **Selection Mode** - Long-Press für Mehrfachauswahl +- ✅ **Silent-Sync Mode** - Kein Banner bei Auto-Sync + +### 🌍 Mehrsprachigkeit + +- ✅ **Englisch + Deutsch** - 400+ übersetzte Strings +- ✅ **Automatische Spracherkennung** - Folgt der System-Sprache +- ✅ **Per-App Language (Android 13+)** - Native Sprachauswahl + +### 🎨 UI-Verbesserungen + +- ✅ **Splash Screen** - App-Foreground-Icon +- ✅ **App Icon** - In About Screen und Empty State +- ✅ **Slide-Animationen** - Flüssige Übergänge im NoteEditor + +--- + +## v1.6.0 - Technische Modernisierung + +> **Status:** In Planung 📋 + +### 🔧 Server-Ordner Prüfung + +- **WebDAV Folder Check** - Prüft ob der Ordner auf dem Server existiert und beschreibbar ist +- **Bessere Fehlermeldungen** - Hilfreiche Hinweise bei Server-Problemen +- **Connection-Test Verbesserung** - Prüft Read/Write Permissions + +### 🔧 Technische Verbesserungen + +- **Code-Refactoring** - LongMethod und LargeClass Warnings beheben +- **Modernere Background-Sync Architektur** - Noch zuverlässiger +- **Verbesserte Progress-Dialoge** - Material Design 3 konform + +--- + +## v1.7.0 - Community Features + +> **Status:** Ideen-Sammlung 💡 + +### Mögliche Features + +- **Zusätzliche Sprachen** - Community-Übersetzungen (FR, ES, IT, ...) +- **Kategorien/Tags** - Notizen organisieren +- **Suche** - Volltextsuche in Notizen +- **Widget** - Schnellzugriff vom Homescreen + +--- + +## 💡 Feedback & Wünsche + +Hast du eine Idee für ein neues Feature? + +- **[Feature Request erstellen](https://github.com/inventory69/simple-notes-sync/issues/new?template=feature_request.yml)** +- **[Bestehende Wünsche ansehen](https://github.com/inventory69/simple-notes-sync/issues?q=is%3Aissue+label%3Aenhancement)** + +--- + +**Hinweis:** Diese Roadmap zeigt unsere aktuellen Pläne. Prioritäten können sich basierend auf Community-Feedback ändern. + +[← Zurück zur Dokumentation](DOCS.md) diff --git a/docs/UPCOMING.en.md b/docs/UPCOMING.en.md deleted file mode 100644 index a512d9b..0000000 --- a/docs/UPCOMING.en.md +++ /dev/null @@ -1,56 +0,0 @@ -# Upcoming Features 🚀 - -**🌍 Languages:** [Deutsch](UPCOMING.md) · **English** - -> What's next? Here you'll find our plans for future versions. - ---- - -## v1.5.0 - Internationalization & UI Polish - -> **Status:** In Development 🔨 - -### 🌍 Multi-Language Support - -- **English as default language** - International users welcome -- **German translation** - Fully translated -- **Automatic language detection** - Follows system language -- **Manual language selection** - Switchable in settings - -### ⚙️ Redesigned Settings - -- **Modernized settings design** - Cleaner and more intuitive -- **Categorized options** - Sync, backup, developer options - -### 🎨 UI Improvements - -- **Splash screen** - App icon on startup -- **Server folder check** - Better error messages during setup -- **Subtle sync indicator** - Banner only shown on actual changes - ---- - -## v1.6.0 - Modern APIs - -> **Status:** Planned 📋 - -### 🔧 Technical Modernization - -- **Modern background sync architecture** - Even more reliable -- **Improved progress dialogs** - Material Design 3 compliant -- **Code quality** - Internal optimizations - ---- - -## 💡 Feedback & Suggestions - -Have an idea for a new feature? - -- **[Create a feature request](https://github.com/inventory69/simple-notes-sync/issues/new?template=feature_request.yml)** -- **[View existing requests](https://github.com/inventory69/simple-notes-sync/issues?q=is%3Aissue+label%3Aenhancement)** - ---- - -**Note:** This roadmap shows our current plans. Priorities may change based on community feedback. - -[← Back to documentation](DOCS.en.md) diff --git a/docs/UPCOMING.md b/docs/UPCOMING.md index 89fba23..3ac984e 100644 --- a/docs/UPCOMING.md +++ b/docs/UPCOMING.md @@ -1,56 +1,76 @@ -# Geplante Features 🚀 +# Upcoming Features 🚀 -**🌍 Languages:** **Deutsch** · [English](UPCOMING.en.md) +**🌍 Languages:** [Deutsch](UPCOMING.de.md) · **English** -> Was kommt als Nächstes? Hier findest du unsere Pläne für zukünftige Versionen. +> What's next? Here you'll find our plans for future versions. --- -## v1.5.0 - Internationalisierung & UI-Polish +## v1.5.0 - Jetpack Compose & Internationalization ✅ -> **Status:** In Entwicklung 🔨 +> **Status:** Released 🎉 (January 2026) -### 🌍 Mehrsprachigkeit +### 🎨 Jetpack Compose UI -- **Englisch als Standard-Sprache** - Internationale Nutzer willkommen -- **Deutsche Übersetzung** - Vollständig übersetzt -- **Automatische Spracherkennung** - Folgt der System-Sprache -- **Manuelle Sprachwahl** - In den Einstellungen umschaltbar +- ✅ **Complete UI redesign** - From XML views to Jetpack Compose +- ✅ **Modernized settings** - 7 categorized screens +- ✅ **Selection Mode** - Long-press for multi-select +- ✅ **Silent-Sync Mode** - No banner during auto-sync -### ⚙️ Überarbeitete Einstellungen +### 🌍 Multi-Language Support -- **Modernisiertes Settings-Design** - Übersichtlicher und intuitiver -- **Kategorisierte Optionen** - Sync, Backup, Entwickler-Optionen +- ✅ **English + German** - 400+ translated strings +- ✅ **Automatic language detection** - Follows system language +- ✅ **Per-App Language (Android 13+)** - Native language selection -### 🎨 UI-Verbesserungen +### 🎨 UI Improvements -- **Splash Screen** - App-Icon beim Start -- **Server-Ordner Prüfung** - Bessere Fehlermeldungen bei der Einrichtung -- **Dezentere Sync-Anzeige** - Banner nur bei tatsächlichen Änderungen +- ✅ **Splash screen** - App foreground icon +- ✅ **App icon** - In About screen and empty state +- ✅ **Slide animations** - Smooth transitions in NoteEditor --- -## v1.6.0 - Modern APIs +## v1.6.0 - Technical Modernization -> **Status:** Geplant 📋 +> **Status:** Planned 📋 -### 🔧 Technische Modernisierung +### 🔧 Server Folder Check -- **Modernere Background-Sync Architektur** - Noch zuverlässiger -- **Verbesserte Progress-Dialoge** - Material Design 3 konform -- **Code-Qualität** - Interne Optimierungen +- **WebDAV folder check** - Checks if folder exists and is writable on server +- **Better error messages** - Helpful hints for server problems +- **Connection test improvement** - Checks read/write permissions + +### 🔧 Technical Improvements + +- **Code refactoring** - Fix LongMethod and LargeClass warnings +- **Modern background sync architecture** - Even more reliable +- **Improved progress dialogs** - Material Design 3 compliant --- -## 💡 Feedback & Wünsche +## v1.7.0 - Community Features -Hast du eine Idee für ein neues Feature? +> **Status:** Idea Collection 💡 -- **[Feature Request erstellen](https://github.com/inventory69/simple-notes-sync/issues/new?template=feature_request.yml)** -- **[Bestehende Wünsche ansehen](https://github.com/inventory69/simple-notes-sync/issues?q=is%3Aissue+label%3Aenhancement)** +### Potential Features + +- **Additional languages** - Community translations (FR, ES, IT, ...) +- **Categories/Tags** - Organize notes +- **Search** - Full-text search in notes +- **Widget** - Quick access from homescreen --- -**Hinweis:** Diese Roadmap zeigt unsere aktuellen Pläne. Prioritäten können sich basierend auf Community-Feedback ändern. +## 💡 Feedback & Suggestions -[← Zurück zur Dokumentation](DOCS.md) +Have an idea for a new feature? + +- **[Create a feature request](https://github.com/inventory69/simple-notes-sync/issues/new?template=feature_request.yml)** +- **[View existing requests](https://github.com/inventory69/simple-notes-sync/issues?q=is%3Aissue+label%3Aenhancement)** + +--- + +**Note:** This roadmap shows our current plans. Priorities may change based on community feedback. + +[← Back to documentation](DOCS.md) diff --git a/fastlane/metadata/android/de-DE/changelogs/13.txt b/fastlane/metadata/android/de-DE/changelogs/13.txt new file mode 100644 index 0000000..3a105ce --- /dev/null +++ b/fastlane/metadata/android/de-DE/changelogs/13.txt @@ -0,0 +1,9 @@ +• Komplett neues UI-Design mit Jetpack Compose +• NEU: Englische Sprachunterstützung + Sprachauswahl +• NEU: Automatische Systemsprachen-Erkennung +• NEU: Long-Press Mehrfachauswahl zum Löschen +• Modernisierte Einstellungen mit 7 Bereichen +• Silent-Sync (kein Banner bei Auto-Sync) +• "Nichts zu syncen" Feedback im Status-Banner +• Diverse UI/UX Verbesserungen und Bugfixes +• App-Icon in Über-Screen und Leerzustand diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt index 8065c9e..b06d331 100644 --- a/fastlane/metadata/android/de-DE/full_description.txt +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -3,7 +3,8 @@ Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisatio HAUPTFUNKTIONEN: • Text-Notizen und Checklisten erstellen -• Checklisten mit Tap-to-Check, Drag & Drop, Swipe-to-Delete +• Checklisten mit Tap-to-Check und Drag & Drop +• Auswahlmodus: Long-Press zur Mehrfachauswahl für Batch-Aktionen • WebDAV-Synchronisation mit eigenem Server • Multi-Device Sync (Handy, Tablet, Desktop) • Markdown-Export für Obsidian/Desktop-Editoren @@ -11,9 +12,17 @@ HAUPTFUNKTIONEN: • Automatische Synchronisation im Heim-WLAN • Konfigurierbares Sync-Interval (15/30/60 Minuten) • Material Design 3 mit Dynamic Colors (Android 12+) +• Jetpack Compose UI - modern, schnell und flüssig • Komplett offline nutzbar • Keine Werbung, keine Tracker +MEHRSPRACHIG: + +• Englische und deutsche Sprachunterstützung +• Per-App Sprachauswahl (Android 13+) +• Automatische Systemsprachen-Erkennung +• Über 400 übersetzte Strings + DATENSCHUTZ: Deine Daten bleiben bei dir! Die App kommuniziert nur mit deinem eigenen WebDAV-Server. Keine Cloud-Dienste, keine Tracking-Bibliotheken, keine Analysetools. @@ -29,20 +38,23 @@ MULTI-DEVICE SYNC: SYNCHRONISATION: • Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.) +• Automatische WiFi-Sync: Synchronisiert automatisch wenn du ein beliebiges WLAN betrittst (wenn Server erreichbar ist) • Konfigurierbares Interval: 15, 30 oder 60 Minuten • Optimierte Performance: überspringt unveränderte Dateien (~2-3s Sync-Zeit) • 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 • Doze Mode optimiert für zuverlässige Background-Syncs • Manuelle Synchronisation jederzeit möglich MATERIAL DESIGN 3: -• Moderne Benutzeroberfläche +• Moderne Jetpack Compose Benutzeroberfläche • Dynamic Colors (Material You) auf Android 12+ • Dark Mode Support -• Intuitive Gesten (Swipe-to-Delete) +• Auswahlmodus mit Batch-Löschen • Live Sync-Status Anzeige +• Flüssige Slide-Animationen Open Source unter MIT-Lizenz Quellcode: https://github.com/inventory69/simple-notes-sync \ No newline at end of file diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.jpg b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.jpg deleted file mode 100644 index 7bac06a..0000000 Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.jpg and /dev/null differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png new file mode 100644 index 0000000..be24039 Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.jpg b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.jpg deleted file mode 100644 index 4adc37a..0000000 Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.jpg and /dev/null differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png new file mode 100644 index 0000000..3d01bb6 Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.jpg b/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.jpg deleted file mode 100644 index b646484..0000000 Binary files a/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.jpg and /dev/null differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png new file mode 100644 index 0000000..ba6a867 Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png new file mode 100644 index 0000000..4be691d Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png new file mode 100644 index 0000000..e639635 Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png b/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png new file mode 100644 index 0000000..098f9e2 Binary files /dev/null and b/fastlane/metadata/android/de-DE/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/changelogs/13.txt b/fastlane/metadata/android/en-US/changelogs/13.txt new file mode 100644 index 0000000..1e60b52 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/13.txt @@ -0,0 +1,9 @@ +• Complete UI redesign with Jetpack Compose +• NEW: English language support + Language selector +• NEW: Auto-detects system language +• NEW: Long-press selection mode for batch delete +• Modernized Settings with 7 organized screens +• Silent-Sync mode (no banner during auto-sync) +• "Nothing to sync" feedback in status banner +• Various UI/UX improvements and bug fixes +• App icon in About screen and empty state diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 92257a2..1871db1 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -3,7 +3,8 @@ Simple Notes Sync is a minimalist note-taking app with WebDAV synchronization. KEY FEATURES: • Create text notes and checklists -• Checklists with tap-to-check, drag & drop, swipe-to-delete +• Checklists with tap-to-check, drag & drop reordering +• Selection mode: long-press to select multiple notes for batch actions • WebDAV synchronization with your own server • Multi-device sync (phone, tablet, desktop) • Markdown export for Obsidian/desktop editors @@ -11,9 +12,17 @@ KEY FEATURES: • Automatic synchronization on home WiFi • Configurable sync interval (15/30/60 minutes) • Material Design 3 with Dynamic Colors (Android 12+) +• Jetpack Compose UI - modern, fast, and smooth • Fully usable offline • No ads, no trackers +MULTILINGUAL: + +• English and German language support +• Per-App Language selector (Android 13+) +• Automatic system language detection +• 400+ translated strings + PRIVACY: Your data stays with you! The app only communicates with your own WebDAV server. No cloud services, no tracking libraries, no analytics tools. @@ -29,20 +38,23 @@ MULTI-DEVICE SYNC: SYNCHRONIZATION: • Supports all WebDAV servers (Nextcloud, ownCloud, etc.) +• Automatic WiFi sync: synchronizes whenever you join any WiFi network (if server is reachable) • Configurable interval: 15, 30, or 60 minutes • Optimized performance: skips unchanged files (~2-3s sync time) • 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 • Doze Mode optimized for reliable background syncs • Manual synchronization available anytime MATERIAL DESIGN 3: -• Modern user interface +• Modern Jetpack Compose user interface • Dynamic Colors (Material You) on Android 12+ • Dark Mode support -• Intuitive gestures (Swipe-to-delete) +• Selection mode with batch delete • Live sync status indicator +• Smooth slide animations Open Source under MIT License Source code: https://github.com/inventory69/simple-notes-sync diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg deleted file mode 100644 index 23d4905..0000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png new file mode 100644 index 0000000..be24039 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg deleted file mode 100644 index dd84c75..0000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png new file mode 100644 index 0000000..3d01bb6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg deleted file mode 100644 index 5700c5d..0000000 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.jpg and /dev/null differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png new file mode 100644 index 0000000..ba6a867 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 100644 index 0000000..4be691d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 100644 index 0000000..e639635 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png new file mode 100644 index 0000000..098f9e2 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/metadata/dev.dettmer.simplenotes.yml b/metadata/dev.dettmer.simplenotes.yml index bf9cab7..bd97ecb 100644 --- a/metadata/dev.dettmer.simplenotes.yml +++ b/metadata/dev.dettmer.simplenotes.yml @@ -15,9 +15,9 @@ 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.4.1 - versionCode: 12 - commit: 7128c25bd519861ca136e4be9b67ce280ef184bf + - versionName: 1.5.0 + versionCode: 13 + commit: PLACEHOLDER_UPDATE_AFTER_MERGE subdir: android/app gradle: - fdroid @@ -26,5 +26,5 @@ AllowedAPKSigningKeys: 42a1c613bbc673045af3dc8191bf9cb6456ee44c7dce40c7cfb566fac AutoUpdateMode: Version UpdateCheckMode: Tags -CurrentVersion: 1.4.1 -CurrentVersionCode: 12 +CurrentVersion: 1.5.0 +CurrentVersionCode: 13