13 Commits

Author SHA1 Message Date
inventory69
0e96757fab Merge feature/v1.3.2-lint-cleanup into main 2026-01-10 00:57:46 +01:00
inventory69
547c0a1011 v1.3.2: Lint Cleanup & Code Quality
- Complete lint cleanup (Phase 1-7)
- Replace magic numbers with constants
- Remove unused imports/members
- Add Logger.w() for swallowed exceptions
- Custom SyncException for better error handling
- ConstructorParameterNaming with @SerializedName
- ReturnCount & Destructuring with @Suppress
- F-Droid: Add privacy notice for file logging
- Update docs (FEATURES.md, README.md)
- Add fastlane changelogs for versionCode 10
2026-01-10 00:57:28 +01:00
inventory69
b79c0d25e6 [skip ci] fix: simplify workflow for single universal APK per flavor
- Remove APK splits logic from workflow
- Build only universal APKs for both standard and fdroid flavors
- Simplifies release process and fixes F-Droid compatibility
2026-01-09 13:38:43 +01:00
inventory69
cf1142afa2 [skip ci] fix: disable APK splits for F-Droid build
- F-Droid expects single universal APK, not multiple architecture-specific APKs
- Remove splits.abi configuration to fix build error in F-Droid CI
- Pipeline failed with: 'More than one resulting apks found' (armeabi-v7a, arm64-v8a, universal)
- Resolves F-Droid MR #31695 build failure
2026-01-09 13:19:47 +01:00
inventory69
359325bf64 [skip ci] chore: update author information in metadata 2026-01-09 11:33:18 +01:00
inventory69
c7d0f899e7 [skip ci] feat: new app icon with monochrome support & updated descriptions
🎨 New App Icon:
- Fresh adaptive icon design with warm background (#f9e9c8)
- Monochrome icon support for Android 13+ themed icons
- PNG format replacing WebP for better compatibility
- All densities: mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi

📝 Updated Descriptions (EN/DE):
- Added Multi-Device Sync feature
- Added Markdown export for Obsidian/desktop editors
- Added deletion tracking (zombie notes prevention)
- Added E-Tag caching (20x faster checks)
- Added optimized performance (~2-3s sync time)
- Added live sync status indicator
- Added Server-Restore modes (Merge/Replace/Overwrite)

📦 F-Droid Metadata:
- Updated build entries for v1.2.1, v1.2.2, v1.3.0, v1.3.1
- CurrentVersion now 1.3.1 (versionCode 9)
- Prepared for F-Droid merge request submission
2026-01-09 10:55:39 +01:00
inventory69
5121a7b2b8 Fix F-Droid changelogs: Remove emojis and bullet points [skip ci] 2026-01-08 23:18:29 +01:00
inventory69
04664c8920 v1.3.1 - Multi-Device Sync Fix + Performance + Restore Bug Fix
🔧 Fixed:
- Multi-device JSON sync now works (thanks Thomas!)
- Restore from Server skipped files (timestamp bug)
- No duplicate downloads
- First MD sync after export now fast

 Performance:
- JSON sync: 12-14s → 2-3s
- Hybrid timestamp + E-Tag optimization
- Matches Markdown sync speed

 New:
- Sync status UI in MainActivity
- Content-based MD import
- Debug logging improvements
- SyncStateManager for sync coordination

🔧 Technical:
- Clear lastSyncTimestamp on restore
- Clear E-Tag caches on restore
- E-Tag refresh after upload
- Fixed timestamp update after MD export
2026-01-08 23:09:59 +01:00
Inventory69
2a56dd8128 Merge pull request #4 from inventory69/release/v1.3.0
Release v1.3.0: Multi-Device Sync
2026-01-07 12:28:31 +01:00
inventory69
63af7d30dc Release v1.3.0: Multi-Device Sync with Deletion Tracking
New Features:
- Multi-Device Sync with deletion tracking (prevents zombie notes)
- Server deletion via swipe gesture with confirmation dialog
- E-Tag performance optimization (~150ms vs 3s for no-change syncs)
- Markdown Auto-Sync toggle (unified Export + Auto-Import)
- Manual Markdown sync button for performance control
- Server-Restore modes (Merge/Replace/Overwrite)

Technical Implementation:
- DeletionTracker model with JSON persistence
- Intelligent server checks with E-Tag caching
- Deletion-aware download logic
- Two-stage swipe deletion with Material Design dialog
- Automatic Markdown import during sync
- YAML frontmatter scanning for robust file deletion

Thanks to Thomas from Bielefeld for reporting the multi-device sync issue!

Compatible with: v1.2.0-v1.3.0
2026-01-07 12:27:27 +01:00
inventory69
62423f5a5b Release v1.2.2: Backward compatibility for v1.2.0 users
- Added dual-mode download for server restore
- Scans both /notes/ (new) and Root (old v1.2.0) folders
- Normal sync only uses /notes/ for performance
- Fixed URL construction bugs
- Updated F-Droid changelogs
2026-01-05 16:46:07 +01:00
inventory69
9eabc9a5f0 [skip ci] 📚 Docs: Reorganize + Web Editor to v1.3.0
## 📁 Reorganization
- Moved all docs to docs/ folder (FEATURES, BACKUP, DESKTOP, DOCS)
- Updated all cross-references in README.md/en
- Fixed internal links in docs

## �� Corrections
- FEATURES.md: Fixed build variants - both are 100% FOSS (no Google Services)
- Clarified: App is completely FOSS with no proprietary libraries

##  Changes
- Web Editor moved from v1.6.0 to v1.3.0 (earlier implementation)
- Combined with organization features (tags, search, sorting)
2026-01-05 12:43:01 +01:00
inventory69
015b90d56e 🐛 v1.2.1: Markdown Initial Export Bugfix + URL Normalization + GitHub Workflow Fix
## 🐛 Fixed
- Initial Markdown export: Existing notes now exported when Desktop Integration activated
- Markdown directory structure: Files now land correctly in /notes-md/
- JSON URL normalization: Smart detection for both Root-URL and /notes-URL
- GitHub release notes: Fixed language order (DE primary, EN collapsible) and emoji

##  Improved
- Settings UI: Example URL shows /notes instead of /webdav
- Server config: Enter only base URL (app adds /notes/ and /notes-md/ automatically)
- Flexible URL input: Both http://server/ and http://server/notes/ work
- Changelogs: Shortened for F-Droid 500 char limit

## 🔧 Technical
- getNotesUrl() helper with smart /notes/ detection
- getMarkdownUrl() simplified to use getNotesUrl()
- All JSON operations updated to use normalized URLs
- exportAllNotesToMarkdown() with progress callback
- Workflow: Swapped CHANGELOG_DE/EN, replaced broken emoji with 🌍

versionCode: 6
versionName: 1.2.1
2026-01-05 11:46:25 +01:00
96 changed files with 5393 additions and 741 deletions

View File

@@ -61,33 +61,15 @@ jobs:
run: | run: |
mkdir -p apk-output mkdir -p apk-output
# === Standard Flavor (mit Google Services) === # Standard Flavor - Universal APK
# Universal APK (funktioniert auf allen Geraeten) cp android/app/build/outputs/apk/standard/release/app-standard-release.apk \
cp android/app/build/outputs/apk/standard/release/app-standard-universal-release.apk \ apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard.apk
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard-universal.apk
# ARM64 APK (moderne Geräte 2018+) # F-Droid Flavor - Universal APK
cp android/app/build/outputs/apk/standard/release/app-standard-arm64-v8a-release.apk \ cp android/app/build/outputs/apk/fdroid/release/app-fdroid-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard-arm64-v8a.apk apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk
# ARMv7 APK (ältere Geräte) echo "✅ APK-Dateien vorbereitet:"
cp android/app/build/outputs/apk/standard/release/app-standard-armeabi-v7a-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-standard-armeabi-v7a.apk
# === F-Droid Flavor (ohne Google Services) ===
# Universal APK
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-universal-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-universal.apk
# ARM64 APK
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-arm64-v8a-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-arm64-v8a.apk
# ARMv7 APK
cp android/app/build/outputs/apk/fdroid/release/app-fdroid-armeabi-v7a-release.apk \
apk-output/simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-armeabi-v7a.apk
echo "✅ APK-Dateien vorbereitet (Standard + F-Droid):"
ls -lh apk-output/ ls -lh apk-output/
- name: APK-Artefakte hochladen - name: APK-Artefakte hochladen
@@ -138,25 +120,19 @@ jobs:
| Variante | Datei | Info | | Variante | Datei | Info |
|----------|-------|------| |----------|-------|------|
| **🏆 Empfohlen** | `simple-notes-sync-v${{ env.VERSION_NAME }}-standard-universal.apk` | Funktioniert auf allen Android-Geraeten | | **🏆 Empfohlen** | `simple-notes-sync-v${{ env.VERSION_NAME }}-standard.apk` | Standard-Version (funktioniert auf allen Geraeten) |
| Modern (2018+) | `simple-notes-sync-v${{ env.VERSION_NAME }}-standard-arm64-v8a.apk` | Kleinere Dateigröße fuer 64-bit Geräte | | F-Droid | `simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid.apk` | Fuer F-Droid Store |
| Aelter (<2018) | `simple-notes-sync-v${{ env.VERSION_NAME }}-standard-armeabi-v7a.apk` | Fuer 32-bit ARM Geräte |
| F-Droid Universal | `simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-universal.apk` | Fuer F-Droid Store |
| F-Droid ARM64 | `simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-arm64-v8a.apk` | F-Droid 64-bit |
| F-Droid ARMv7 | `simple-notes-sync-v${{ env.VERSION_NAME }}-fdroid-armeabi-v7a.apk` | F-Droid 32-bit |
💡 **Nicht sicher?** → Nimm die **Universal** APK!
--- ---
## 📋 Changelog / Release Notes ## 📋 Changelog / Release Notes
${{ env.CHANGELOG_EN }} ${{ env.CHANGELOG_DE }}
<details> <details>
<summary><EFBFBD>🇪 Deutsche Version (zum Aufklappen)</summary> <summary>🌍 English Version</summary>
${{ env.CHANGELOG_DE }} ${{ env.CHANGELOG_EN }}
</details> </details>

View File

@@ -6,6 +6,241 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
--- ---
## [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<Boolean>` 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 ## [1.2.0] - 2026-01-04
### Added ### Added

View File

@@ -76,7 +76,9 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
| **Password** | (your password from `.env`) | | **Password** | (your password from `.env`) |
| **Gateway SSID** | Name of your WiFi network | | **Gateway SSID** | Name of your WiFi network |
4. **Press "Test connection"** > **💡 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 - ✅ Success? → Continue to step 4
- ❌ Error? → See [Troubleshooting](#troubleshooting) - ❌ Error? → See [Troubleshooting](#troubleshooting)

View File

@@ -76,6 +76,8 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
| **Passwort** | (dein Passwort aus `.env`) | | **Passwort** | (dein Passwort aus `.env`) |
| **Gateway SSID** | Name deines WLAN-Netzwerks | | **Gateway SSID** | Name deines WLAN-Netzwerks |
> **💡 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 4. **"Verbindung testen"** drücken
- ✅ Erfolg? → Weiter zu Schritt 4 - ✅ Erfolg? → Weiter zu Schritt 4
- ❌ Fehler? → Siehe [Troubleshooting](#troubleshooting) - ❌ Fehler? → Siehe [Troubleshooting](#troubleshooting)

View File

@@ -6,7 +6,9 @@
[![Material Design 3](https://img.shields.io/badge/Material-Design%203-green.svg)](https://m3.material.io/) [![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) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Documentation](DOCS.en.md)** · **🚀 [Quick Start](QUICKSTART.en.md)** [<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](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** **🌍 Languages:** [Deutsch](README.md) · **English**
@@ -22,53 +24,27 @@
--- ---
## Features ## ✨ Highlights
### 📝 Notes - 📝 **Offline-first** - Works without internet
* Simple text notes with auto-save - 🔄 **Auto-sync** - Home WiFi only (15/30/60 min)
* Swipe-to-delete with confirmation - 🔒 **Self-hosted** - Your data stays with you (WebDAV)
* Material Design 3 editor - 💾 **Local backup** - Export/Import as JSON file
- 🖥️ **Desktop integration** - Markdown export for VS Code, Typora, etc.
- 🔋 **Battery-friendly** - ~0.2-0.8% per day
- 🎨 **Material Design 3** - Dark mode & dynamic colors
### 💾 Backup & Restore **NEW in v1.2.0** ➡️ **Complete feature list:** [FEATURES.en.md](docs/FEATURES.en.md)
* **Local backup** - Export all notes as JSON file
* **Flexible restore** - 3 modes (Merge, Replace, Overwrite)
* **Automatic safety net** - Auto-backup before every restore
* **Independent from server** - Works completely offline
### 🖥️ Desktop Integration **NEW in v1.2.0**
* **Markdown export** - Notes are automatically exported as `.md` files
* **WebDAV access** - Mount WebDAV as network drive for direct access
* **Editor compatibility** - VS Code, Typora, Notepad++, or any Markdown editor
* **Last-Write-Wins** - Intelligent conflict resolution via timestamps
* **Dual-format** - JSON sync remains master, Markdown is optional mirror
### 🔄 Synchronization
* **Pull-to-refresh** for manual sync
* **Auto-sync** (15/30/60 min) only on home WiFi
* **Smart server check** - No errors on foreign networks
* **Conflict-free merging** - Your changes are never lost
* **6 sync triggers** - Periodic, app-start, WiFi, manual, pull-to-refresh, settings
### 🔒 Privacy & Self-Hosted
* **WebDAV server** (Nextcloud, ownCloud, etc.)
* **Docker setup guide** included in docs
* **Your data stays with you** - No tracking, no cloud
* **HTTP only local** - HTTPS for external servers
* **100% open source** (MIT License)
### 🔋 Performance
* **Battery-friendly** (~0.2-0.8% per day)
* **Offline-first** - Works without internet
* **Dark mode** & dynamic colors
--- ---
## 🚀 Quick Start ## 🚀 Quick Start
### 1. Server Setup ### 1. Server Setup (5 minutes)
```bash ```bash
cd server git clone https://github.com/inventory69/simple-notes-sync.git
cd simple-notes-sync/server
cp .env.example .env cp .env.example .env
# Set password in .env # Set password in .env
docker compose up -d docker compose up -d
@@ -76,114 +52,30 @@ docker compose up -d
➡️ **Details:** [Server Setup Guide](server/README.en.md) ➡️ **Details:** [Server Setup Guide](server/README.en.md)
### 2. App Installation ### 2. App Installation (2 minutes)
1. [Download APK](https://github.com/inventory69/simple-notes-sync/releases/latest) 1. [Download APK](https://github.com/inventory69/simple-notes-sync/releases/latest)
2. Install & open 2. Install & open
3. ⚙️ Settings → Configure server 3. ⚙️ Settings → Configure server:
4. Enable auto-sync - **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! 🎉
➡️ **Details:** [Complete guide](QUICKSTART.en.md) ➡️ **Detailed guide:** [QUICKSTART.en.md](QUICKSTART.en.md)
--- ---
## <EFBFBD> Local Backup & Restore ## 📚 Documentation
### Create Backup
1. **Settings****Backup & Restore**
2. Tap **"📥 Create backup"**
3. Choose location (Downloads, SD card, cloud folder)
4. Done! All notes are saved in a `.json` file
**Filename:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json`
### Restore
1. **Settings****"📤 Restore from file"**
2. Select backup file
3. **Choose restore mode:**
- **Merge** _(Default)_ - Add new notes, keep existing ones
- **Replace** - Delete all and import backup
- **Overwrite duplicates** - Backup wins on ID conflicts
4. Confirm - _Automatic safety backup is created!_
**💡 Tip:** Before every restore, an automatic safety backup is created - your data is safe!
---
## 🖥️ Desktop Integration (WebDAV + Markdown)
### Why Markdown?
The app automatically exports your notes as `.md` files so you can edit them on desktop:
- **JSON remains master** - Primary sync mechanism (reliable, fast)
- **Markdown is mirror** - Additional export for desktop access
- **Dual-format** - Both formats are always in sync
### Setup: WebDAV as Network Drive
**With WebDAV mount ANY Markdown editor works!**
#### Windows:
1. **Open Explorer** → Right-click on "This PC"
2. **"Map network drive"**
3. **Enter WebDAV URL:** `http://YOUR-SERVER:8080/notes-md/`
4. Enter username/password
5. **Done!** - Folder appears as drive (e.g. Z:\)
#### macOS:
1. **Finder** → Menu "Go" → "Connect to Server" (⌘K)
2. **Server Address:** `http://YOUR-SERVER:8080/notes-md/`
3. Enter username/password
4. **Done!** - Folder appears under "Network"
#### Linux:
```bash
# Option 1: GNOME Files / Nautilus
Files → Other Locations → Connect to Server
Server Address: dav://YOUR-SERVER:8080/notes-md/
# Option 2: davfs2 (permanent mount)
sudo apt install davfs2
sudo mount -t davfs http://YOUR-SERVER:8080/notes-md/ /mnt/notes
```
### Workflow:
1. **Enable Markdown export** (App → Settings)
2. **Mount WebDAV** (see above)
3. **Open editor** (VS Code, Typora, Notepad++, etc.)
4. **Edit notes** - Changes are saved directly
5. **"Import Markdown Changes" in app** - Import desktop changes
**Recommended Editors:**
- **VS Code** - Free, powerful, with Markdown preview
- **Typora** - Minimalist, WYSIWYG Markdown
- **Notepad++** - Lightweight, fast
- **iA Writer** - Focused writing
- **VS Code** with WebDAV extension
- **Typora** (local copy)
- **iA Writer** (read/edit only, no auto-sync)
**⚠️ Important:**
- Markdown export is **optional** (toggle in settings)
- JSON sync **always** works - Markdown is additional
- All 6 sync triggers remain unchanged
---
## <20>📚 Documentation
- **[Quick Start Guide](QUICKSTART.en.md)** - Step-by-step guide for end users
- **[Server Setup](server/README.en.md)** - Configure WebDAV server
- **[Complete Docs](DOCS.en.md)** - Features, troubleshooting, build instructions
| 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) |
--- ---
## 🛠️ Development ## 🛠️ Development
@@ -193,24 +85,20 @@ cd android
./gradlew assembleStandardRelease ./gradlew assembleStandardRelease
``` ```
➡️ **Details:** [Build instructions in DOCS.en.md](DOCS.en.md) ➡️ **Build guide:** [DOCS.en.md](docs/DOCS.en.md#-build--deployment)
--- ---
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md)
--- ---
## <EFBFBD> Changelog ## 📄 License
All changes are documented in [CHANGELOG.md](CHANGELOG.md).
---
## <20>📄 License
MIT License - see [LICENSE](LICENSE) MIT License - see [LICENSE](LICENSE)
**v1.2.0** · Built with Kotlin + Material Design 3 ---
**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3

194
README.md
View File

@@ -6,7 +6,9 @@
[![Material Design 3](https://img.shields.io/badge/Material-Design%203-green.svg)](https://m3.material.io/) [![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) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Dokumentation](DOCS.md)** · **🚀 [Quick Start](QUICKSTART.md)** [<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](https://apt.izzysoft.de/fdroid/index/apk/dev.dettmer.simplenotes)
**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Dokumentation](docs/DOCS.md)** · **🚀 [Quick Start](QUICKSTART.md)**
**🌍 Sprachen:** **Deutsch** · [English](README.en.md) **🌍 Sprachen:** **Deutsch** · [English](README.en.md)
@@ -22,53 +24,27 @@
--- ---
## Features ## ✨ Highlights
### 📝 Notizen - 📝 **Offline-First** - Funktioniert ohne Internet
* Einfache Textnotizen mit automatischem Speichern - 🔄 **Auto-Sync** - Nur im Heim-WLAN (15/30/60 Min)
* Swipe-to-Delete mit Bestätigung - 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
* Material Design 3 Editor - 💾 **Lokales Backup** - Export/Import als JSON-Datei
- 🖥️ **Desktop-Integration** - Markdown-Export für VS Code, Typora, etc.
- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
### 💾 Backup & Wiederherstellung **NEU in v1.2.0** ➡️ **Vollständige Feature-Liste:** [FEATURES.md](docs/FEATURES.md)
* **Lokales Backup** - Exportiere alle Notizen als JSON-Datei
* **Flexible Wiederherstellung** - 3 Modi (Zusammenführen, Ersetzen, Überschreiben)
* **Automatisches Sicherheitsnetz** - Auto-Backup vor jeder Wiederherstellung
* **Unabhängig vom Server** - Funktioniert komplett offline
### 🖥️ Desktop-Integration **NEU in v1.2.0**
* **Markdown-Export** - Notizen werden automatisch als `.md` Dateien exportiert
* **WebDAV-Zugriff** - Mounte WebDAV als Netzlaufwerk für direkten Zugriff
* **Editor-Kompatibilität** - VS Code, Typora, Notepad++, oder beliebiger Markdown-Editor
* **Last-Write-Wins** - Intelligente Konfliktauflösung via Zeitstempel
* **Dual-Format** - JSON-Sync bleibt Master, Markdown ist optionaler Mirror
### 🔄 Synchronisation
* **Pull-to-Refresh** für manuellen Sync
* **Auto-Sync** (15/30/60 Min) nur im Heim-WLAN
* **Smart Server-Check** - Keine Fehler in fremden Netzwerken
* **Konfliktfreies Merging** - Deine Änderungen gehen nie verloren
* **6 Sync-Trigger** - Periodic, App-Start, WiFi, Manual, Pull-to-Refresh, Settings
### 🔒 Privacy & Self-Hosted
* **WebDAV-Server** (Nextcloud, ownCloud, etc.)
* **Docker Setup-Anleitung** in den Docs enthalten
* **Deine Daten bleiben bei dir** - Kein Tracking, keine Cloud
* **HTTP nur lokal** - HTTPS für externe Server
* **100% Open Source** (MIT Lizenz)
### 🔋 Performance
* **Akkuschonend** (~0.2-0.8% pro Tag)
* **Offline-First** - Funktioniert ohne Internet
* **Dark Mode** & Dynamic Colors
--- ---
## 🚀 Quick Start ## 🚀 Schnellstart
### 1. Server Setup ### 1. Server Setup (5 Minuten)
```bash ```bash
cd server git clone https://github.com/inventory69/simple-notes-sync.git
cd simple-notes-sync/server
cp .env.example .env cp .env.example .env
# Passwort in .env setzen # Passwort in .env setzen
docker compose up -d docker compose up -d
@@ -76,118 +52,32 @@ docker compose up -d
➡️ **Details:** [Server Setup Guide](server/README.md) ➡️ **Details:** [Server Setup Guide](server/README.md)
### 2. App Installation ### 2. App Installation (2 Minuten)
1. [APK herunterladen](https://github.com/inventory69/simple-notes-sync/releases/latest) 1. [APK herunterladen](https://github.com/inventory69/simple-notes-sync/releases/latest)
2. Installieren & öffnen 2. Installieren & öffnen
3. ⚙️ Einstellungen → Server konfigurieren 3. ⚙️ Einstellungen → Server konfigurieren:
4. Auto-Sync aktivieren - **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! 🎉
➡️ **Details:** [Vollständige Anleitung](QUICKSTART.md) ➡️ **Ausführliche Anleitung:** [QUICKSTART.md](QUICKSTART.md)
--- ---
## <EFBFBD> Lokales Backup & Wiederherstellung ## 📚 Dokumentation
### Backup erstellen | Dokument | Inhalt |
|----------|--------|
1. **Einstellungen****Backup & Wiederherstellung** | **[QUICKSTART.md](QUICKSTART.md)** | Schritt-für-Schritt Installation |
2. Tippe auf **"📥 Backup erstellen"** | **[FEATURES.md](docs/FEATURES.md)** | Vollständige Feature-Liste |
3. Wähle Speicherort (Downloads, SD-Karte, Cloud-Ordner) | **[BACKUP.md](docs/BACKUP.md)** | Backup & Wiederherstellung |
4. Fertig! Alle Notizen sind in einer `.json` Datei gesichert | **[DESKTOP.md](docs/DESKTOP.md)** | Desktop-Integration (Markdown) |
| **[DOCS.md](docs/DOCS.md)** | Technische Details & Troubleshooting |
**Dateiname:** `simplenotes_backup_YYYY-MM-DD_HHmmss.json` | **[CHANGELOG.md](CHANGELOG.md)** | Versionshistorie |
### Wiederherstellen
1. **Einstellungen****"📤 Aus Datei wiederherstellen"**
2. Wähle Backup-Datei
3. **Wiederherstellungs-Modus auswählen:**
- **Zusammenführen** _(Standard)_ - Neue Notizen hinzufügen, bestehende behalten
- **Ersetzen** - Alle löschen und Backup importieren
- **Duplikate überschreiben** - Backup gewinnt bei ID-Konflikten
4. Bestätigen - _Automatisches Sicherheits-Backup wird erstellt!_
**💡 Tipp:** Vor jeder Wiederherstellung wird automatisch ein Auto-Backup erstellt - deine Daten sind sicher!
---
## 🖥️ Desktop-Integration (WebDAV + Markdown)
### Warum Markdown?
Die App exportiert deine Notizen automatisch als `.md` Dateien, damit du sie auf dem Desktop bearbeiten kannst:
- **JSON bleibt Master** - Primärer Sync-Mechanismus (verlässlich, schnell)
- **Markdown ist Mirror** - Zusätzlicher Export für Desktop-Zugriff
- **Dual-Format** - Beide Formate sind immer synchron
### Setup: WebDAV als Netzlaufwerk
**Mit WebDAV-Mount funktioniert JEDER Markdown-Editor!**
#### Windows:
1. **Explorer öffnen** → Rechtsklick auf "Dieser PC"
2. **"Netzlaufwerk verbinden"** wählen
3. **WebDAV-URL eingeben:** `http://DEIN-SERVER:8080/notes-md/`
4. Benutzername/Passwort eingeben
5. **Fertig!** - Ordner erscheint als Laufwerk (z.B. Z:\)
#### macOS:
1. **Finder** → Menü "Gehe zu" → "Mit Server verbinden" (⌘K)
2. **Server-Adresse:** `http://DEIN-SERVER:8080/notes-md/`
3. Benutzername/Passwort eingeben
4. **Fertig!** - Ordner erscheint unter "Netzwerk"
#### Linux:
```bash
# Option 1: GNOME Files / Nautilus
Dateien → Andere Orte → Mit Server verbinden
Server-Adresse: dav://DEIN-SERVER:8080/notes-md/
# Option 2: davfs2 (permanent mount)
sudo apt install davfs2
sudo mount -t davfs http://DEIN-SERVER:8080/notes-md/ /mnt/notes
```
### Workflow:
1. **Markdown-Export aktivieren** (App → Einstellungen)
2. **WebDAV mounten** (siehe oben)
3. **Editor öffnen** (VS Code, Typora, Notepad++, etc.)
4. **Notizen bearbeiten** - Änderungen werden direkt gespeichert
5. **"Import Markdown Changes" in App** - Desktop-Änderungen importieren
**Empfohlene Editoren:**
- **VS Code** - Kostenlos, mächtig, mit Markdown-Preview
- **Typora** - Minimalistisch, WYSIWYG-Markdown
- **Notepad++** - Leichtgewichtig, schnell
- **iA Writer** - Fokussiertes Schreiben
3. Notizen bearbeiten - Änderungen via "Import Markdown Changes" in die App importieren
### Alternative: Direkter Zugriff
Du kannst die `.md` Dateien auch direkt mit jedem Markdown-Editor öffnen:
- **VS Code** mit WebDAV-Extension
- **Typora** (lokale Kopie)
- **iA Writer** (nur lesen/bearbeiten, kein Auto-Sync)
**⚠️ Wichtig:**
- Markdown-Export ist **optional** (in Einstellungen ein/ausschaltbar)
- JSON-Sync funktioniert **immer** - Markdown ist zusätzlich
- Alle 6 Sync-Trigger bleiben unverändert erhalten
---
## <20>📚 Dokumentation
- **[Quick Start Guide](QUICKSTART.md)** - Schritt-für-Schritt Anleitung für Endbenutzer
- **[Server Setup](server/README.md)** - WebDAV Server konfigurieren
- **[Vollständige Docs](DOCS.md)** - Features, Troubleshooting, Build-Anleitung
--- ---
@@ -198,24 +88,20 @@ cd android
./gradlew assembleStandardRelease ./gradlew assembleStandardRelease
``` ```
➡️ **Details:** [Build-Anleitung in DOCS.md](DOCS.md) ➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md#-build--deployment)
--- ---
## 🤝 Contributing ## 🤝 Contributing
Beiträge sind willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) für Details. Beiträge willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md)
--- ---
## <EFBFBD> Changelog ## 📄 Lizenz
Alle Änderungen sind in [CHANGELOG.md](CHANGELOG.md) dokumentiert.
---
## <20>📄 Lizenz
MIT License - siehe [LICENSE](LICENSE) MIT License - siehe [LICENSE](LICENSE)
**v1.2.0** · Gebaut mit Kotlin + Material Design 3 ---
**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3

61
android/.editorconfig Normal file
View File

@@ -0,0 +1,61 @@
# ⚡ v1.3.1: EditorConfig for ktlint
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[*.{kt,kts}]
# ktlint rules
ktlint_code_style = android_studio
ktlint_standard_no-wildcard-imports = disabled
ktlint_standard_package-name = enabled
ktlint_standard_filename = enabled
ktlint_standard_class-naming = enabled
ktlint_standard_function-naming = enabled
ktlint_standard_property-naming = enabled
ktlint_standard_backing-property-naming = enabled
ktlint_standard_enum-entry-name-case = enabled
ktlint_standard_multiline-if-else = enabled
ktlint_standard_no-empty-class-body = enabled
ktlint_standard_no-empty-first-line-in-class-body = enabled
ktlint_standard_blank-line-before-declaration = enabled
ktlint_standard_context-receiver-wrapping = enabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_function-literal = enabled
ktlint_standard_function-type-modifier-spacing = enabled
ktlint_standard_kdoc-wrapping = enabled
ktlint_standard_modifier-list-spacing = enabled
ktlint_standard_no-blank-line-in-list = enabled
ktlint_standard_no-consecutive-comments = disabled
ktlint_standard_no-single-line-block-comment = enabled
ktlint_standard_parameter-list-spacing = enabled
ktlint_standard_parameter-list-wrapping = enabled
ktlint_standard_property-wrapping = enabled
ktlint_standard_spacing-between-function-name-and-opening-parenthesis = enabled
ktlint_standard_statement-wrapping = enabled
ktlint_standard_string-template-indent = disabled
ktlint_standard_try-catch-finally-spacing = enabled
ktlint_standard_type-argument-list-spacing = enabled
ktlint_standard_type-parameter-list-spacing = enabled
ktlint_standard_value-argument-comment = enabled
ktlint_standard_value-parameter-comment = enabled
[*.md]
trim_trailing_whitespace = false
[*.{xml,json}]
indent_size = 2
[*.yml]
indent_size = 2
[Makefile]
indent_style = tab

View File

@@ -1,6 +1,9 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
// ⚡ v1.3.1: ktlint deaktiviert wegen Parser-Problemen, aktivieren in v1.4.0
// alias(libs.plugins.ktlint)
alias(libs.plugins.detekt)
} }
import java.util.Properties import java.util.Properties
@@ -17,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes" applicationId = "dev.dettmer.simplenotes"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 5 // 🔥 v1.2.0: Local Backup + Markdown Desktop Integration versionCode = 10 // 🚀 v1.3.2: Lint-Cleanup "Clean Slate"
versionName = "1.2.0" // 🔥 v1.2.0: Backup/Restore + Joplin/Obsidian Support versionName = "1.3.2" // 🚀 v1.3.2: Code-Qualität-Release (alle einfachen Lint-Issues behoben)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -32,23 +35,15 @@ android {
includeInBundle = false // Also disable for AAB (Google Play) includeInBundle = false // Also disable for AAB (Google Play)
} }
// Enable multiple APKs per ABI for smaller downloads
splits {
abi {
isEnable = true
reset()
include("armeabi-v7a", "arm64-v8a")
isUniversalApk = true // Also generate universal APK
}
}
// Product Flavors for F-Droid and standard builds // Product Flavors for F-Droid and standard builds
// Note: APK splits are disabled to ensure single APK output
flavorDimensions += "distribution" flavorDimensions += "distribution"
productFlavors { productFlavors {
create("fdroid") { create("fdroid") {
dimension = "distribution" dimension = "distribution"
// F-Droid builds have no proprietary dependencies // F-Droid builds have no proprietary dependencies
// All dependencies in this project are already FOSS-compatible // All dependencies in this project are already FOSS-compatible
// No APK splits - F-Droid expects single universal APK
} }
create("standard") { create("standard") {
@@ -75,6 +70,16 @@ android {
} }
buildTypes { buildTypes {
debug {
// ⚡ v1.3.1: Debug-Builds können parallel zur Release-App installiert werden
applicationIdSuffix = ".debug"
versionNameSuffix = "-debug"
isDebuggable = true
// Optionales separates Icon-Label für Debug-Builds
resValue("string", "app_name_debug", "Simple Notes (Debug)")
}
release { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
@@ -144,3 +149,27 @@ fun getBuildDate(): String {
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
return dateFormat.format(Date()) return dateFormat.format(Date())
} }
// ⚡ v1.3.1: ktlint deaktiviert wegen Parser-Problemen
// Aktivieren in v1.4.0 wenn Code-Stil bereinigt wurde
// ktlint {
// android = true
// outputToConsole = true
// ignoreFailures = true
// enableExperimentalRules = false
// filter {
// exclude("**/generated/**")
// exclude("**/build/**")
// }
// }
// ⚡ v1.3.1: detekt-Konfiguration
detekt {
buildUponDefaultConfig = true
allRules = false
config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
baseline = file("$rootDir/config/detekt/baseline.xml")
// Parallel-Verarbeitung für schnellere Checks
parallel = true
}

View File

@@ -21,20 +21,29 @@ import com.google.android.material.color.DynamicColors
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.dettmer.simplenotes.adapters.NotesAdapter import dev.dettmer.simplenotes.adapters.NotesAdapter
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.storage.NotesStorage import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.sync.SyncWorker import dev.dettmer.simplenotes.sync.SyncWorker
import dev.dettmer.simplenotes.utils.NotificationHelper import dev.dettmer.simplenotes.utils.NotificationHelper
import dev.dettmer.simplenotes.utils.showToast import dev.dettmer.simplenotes.utils.showToast
import dev.dettmer.simplenotes.utils.Constants import dev.dettmer.simplenotes.utils.Constants
import android.widget.TextView import android.widget.TextView
import android.widget.CheckBox
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import dev.dettmer.simplenotes.sync.WebDavSyncService import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.sync.SyncStateManager
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import android.view.View
import android.widget.LinearLayout
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -44,9 +53,16 @@ class MainActivity : AppCompatActivity() {
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private lateinit var swipeRefreshLayout: SwipeRefreshLayout private lateinit var swipeRefreshLayout: SwipeRefreshLayout
// 🔄 v1.3.1: Sync Status Banner
private lateinit var syncStatusBanner: LinearLayout
private lateinit var syncStatusText: TextView
private lateinit var adapter: NotesAdapter private lateinit var adapter: NotesAdapter
private val storage by lazy { NotesStorage(this) } private val storage by lazy { NotesStorage(this) }
// Menu reference for sync button state
private var optionsMenu: Menu? = null
// Track pending deletions to prevent flicker when notes reload // Track pending deletions to prevent flicker when notes reload
private val pendingDeletions = mutableSetOf<String>() private val pendingDeletions = mutableSetOf<String>()
@@ -60,6 +76,8 @@ class MainActivity : AppCompatActivity() {
private const val REQUEST_SETTINGS = 1002 private const val REQUEST_SETTINGS = 1002
private const val MIN_AUTO_SYNC_INTERVAL_MS = 60_000L // 1 Minute 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 const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
private const val SYNC_COMPLETED_DELAY_MS = 1500L
private const val ERROR_DISPLAY_DELAY_MS = 3000L
} }
/** /**
@@ -91,9 +109,10 @@ class MainActivity : AppCompatActivity() {
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
// File Logging aktivieren wenn eingestellt // Logger initialisieren und File-Logging aktivieren wenn eingestellt
if (prefs.getBoolean("file_logging_enabled", false)) { Logger.init(this)
Logger.enableFileLogging(this) if (prefs.getBoolean(Constants.KEY_FILE_LOGGING_ENABLED, false)) {
Logger.setFileLoggingEnabled(true)
} }
// Alte Sync-Notifications beim App-Start löschen // Alte Sync-Notifications beim App-Start löschen
@@ -110,6 +129,65 @@ class MainActivity : AppCompatActivity() {
setupFab() setupFab()
loadNotes() loadNotes()
// 🔄 v1.3.1: Observe sync state for UI updates
setupSyncStateObserver()
}
/**
* 🔄 v1.3.1: Beobachtet Sync-Status für UI-Feedback
*/
private fun setupSyncStateObserver() {
SyncStateManager.syncStatus.observe(this) { status ->
when (status.state) {
SyncStateManager.SyncState.SYNCING -> {
// Disable sync controls
setSyncControlsEnabled(false)
// 🔄 v1.3.1: Show sync status banner (ersetzt SwipeRefresh-Animation)
syncStatusText.text = getString(R.string.sync_status_syncing)
syncStatusBanner.visibility = View.VISIBLE
}
SyncStateManager.SyncState.COMPLETED -> {
// Re-enable sync controls
setSyncControlsEnabled(true)
swipeRefreshLayout.isRefreshing = false
// Show completed briefly, then hide
syncStatusText.text = status.message ?: getString(R.string.sync_status_completed)
lifecycleScope.launch {
kotlinx.coroutines.delay(SYNC_COMPLETED_DELAY_MS)
syncStatusBanner.visibility = View.GONE
SyncStateManager.reset()
}
}
SyncStateManager.SyncState.ERROR -> {
// Re-enable sync controls
setSyncControlsEnabled(true)
swipeRefreshLayout.isRefreshing = false
// Show error briefly, then hide
syncStatusText.text = status.message ?: getString(R.string.sync_status_error)
lifecycleScope.launch {
kotlinx.coroutines.delay(ERROR_DISPLAY_DELAY_MS)
syncStatusBanner.visibility = View.GONE
SyncStateManager.reset()
}
}
SyncStateManager.SyncState.IDLE -> {
setSyncControlsEnabled(true)
swipeRefreshLayout.isRefreshing = false
syncStatusBanner.visibility = View.GONE
}
}
}
}
/**
* 🔄 v1.3.1: Aktiviert/deaktiviert Sync-Controls (Button + SwipeRefresh)
*/
private fun setSyncControlsEnabled(enabled: Boolean) {
// Menu Sync-Button
optionsMenu?.findItem(R.id.action_sync)?.isEnabled = enabled
// SwipeRefresh
swipeRefreshLayout.isEnabled = enabled
} }
override fun onResume() { override fun onResume() {
@@ -145,6 +223,12 @@ class MainActivity : AppCompatActivity() {
return return
} }
// 🔄 v1.3.1: Check if sync already running
if (!SyncStateManager.tryStartSync("auto-$source")) {
Logger.d(TAG, "⏭️ Auto-sync ($source): Another sync already in progress")
return
}
Logger.d(TAG, "🔄 Auto-sync triggered ($source)") Logger.d(TAG, "🔄 Auto-sync triggered ($source)")
// Update last sync timestamp // Update last sync timestamp
@@ -157,6 +241,7 @@ class MainActivity : AppCompatActivity() {
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization) // 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
if (!syncService.hasUnsyncedChanges()) { if (!syncService.hasUnsyncedChanges()) {
Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping") Logger.d(TAG, "⏭️ Auto-sync ($source): No unsynced changes - skipping")
SyncStateManager.reset()
return@launch return@launch
} }
@@ -167,6 +252,7 @@ class MainActivity : AppCompatActivity() {
if (!isReachable) { if (!isReachable) {
Logger.d(TAG, "⏭️ Auto-sync ($source): Server not reachable - skipping silently") Logger.d(TAG, "⏭️ Auto-sync ($source): Server not reachable - skipping silently")
SyncStateManager.reset()
return@launch return@launch
} }
@@ -178,6 +264,7 @@ class MainActivity : AppCompatActivity() {
// Feedback abhängig von Source // Feedback abhängig von Source
if (result.isSuccess && result.syncedCount > 0) { if (result.isSuccess && result.syncedCount > 0) {
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes") Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
// onResume: Nur Success-Toast // onResume: Nur Success-Toast
showToast("✅ Gesynct: ${result.syncedCount} Notizen") showToast("✅ Gesynct: ${result.syncedCount} Notizen")
@@ -185,14 +272,17 @@ class MainActivity : AppCompatActivity() {
} else if (result.isSuccess) { } else if (result.isSuccess) {
Logger.d(TAG, " Auto-sync ($source): No changes") Logger.d(TAG, " Auto-sync ($source): No changes")
SyncStateManager.markCompleted()
} else { } else {
Logger.e(TAG, "❌ Auto-sync failed ($source): ${result.errorMessage}") Logger.e(TAG, "❌ Auto-sync failed ($source): ${result.errorMessage}")
SyncStateManager.markError(result.errorMessage)
// Kein Toast - App ist im Hintergrund // Kein Toast - App ist im Hintergrund
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "💥 Auto-sync exception ($source): ${e.message}") Logger.e(TAG, "💥 Auto-sync exception ($source): ${e.message}")
SyncStateManager.markError(e.message)
// Kein Toast - App ist im Hintergrund // Kein Toast - App ist im Hintergrund
} }
} }
@@ -229,6 +319,10 @@ class MainActivity : AppCompatActivity() {
fabAddNote = findViewById(R.id.fabAddNote) fabAddNote = findViewById(R.id.fabAddNote)
toolbar = findViewById(R.id.toolbar) toolbar = findViewById(R.id.toolbar)
swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout) swipeRefreshLayout = findViewById<SwipeRefreshLayout>(R.id.swipeRefreshLayout)
// 🔄 v1.3.1: Sync Status Banner
syncStatusBanner = findViewById(R.id.syncStatusBanner)
syncStatusText = findViewById(R.id.syncStatusText)
} }
private fun setupToolbar() { private fun setupToolbar() {
@@ -256,6 +350,12 @@ class MainActivity : AppCompatActivity() {
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
Logger.d(TAG, "🔄 Pull-to-Refresh triggered - starting manual sync") Logger.d(TAG, "🔄 Pull-to-Refresh triggered - starting manual sync")
// 🔄 v1.3.1: Check if sync already running (Banner zeigt Status)
if (!SyncStateManager.tryStartSync("pullToRefresh")) {
swipeRefreshLayout.isRefreshing = false
return@setOnRefreshListener
}
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
@@ -263,7 +363,7 @@ class MainActivity : AppCompatActivity() {
if (serverUrl.isNullOrEmpty()) { if (serverUrl.isNullOrEmpty()) {
showToast("⚠️ Server noch nicht konfiguriert") showToast("⚠️ Server noch nicht konfiguriert")
swipeRefreshLayout.isRefreshing = false SyncStateManager.reset()
return@launch return@launch
} }
@@ -272,15 +372,13 @@ class MainActivity : AppCompatActivity() {
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization) // 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
if (!syncService.hasUnsyncedChanges()) { if (!syncService.hasUnsyncedChanges()) {
Logger.d(TAG, "⏭️ No unsynced changes, skipping server reachability check") Logger.d(TAG, "⏭️ No unsynced changes, skipping server reachability check")
showToast("Bereits synchronisiert") SyncStateManager.markCompleted("Bereits synchronisiert")
swipeRefreshLayout.isRefreshing = false
return@launch return@launch
} }
// Check if server is reachable // Check if server is reachable
if (!syncService.isServerReachable()) { if (!syncService.isServerReachable()) {
showToast("⚠️ Server nicht erreichbar") SyncStateManager.markError("Server nicht erreichbar")
swipeRefreshLayout.isRefreshing = false
return@launch return@launch
} }
@@ -288,16 +386,14 @@ class MainActivity : AppCompatActivity() {
val result = syncService.syncNotes() val result = syncService.syncNotes()
if (result.isSuccess) { if (result.isSuccess) {
showToast("${result.syncedCount} Notizen synchronisiert") SyncStateManager.markCompleted("${result.syncedCount} Notizen")
loadNotes() loadNotes()
} else { } else {
showToast("❌ Sync fehlgeschlagen: ${result.errorMessage}") SyncStateManager.markError(result.errorMessage)
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "Pull-to-Refresh sync failed", e) Logger.e(TAG, "Pull-to-Refresh sync failed", e)
showToast("❌ Fehler: ${e.message}") SyncStateManager.markError(e.message)
} finally {
swipeRefreshLayout.isRefreshing = false
} }
} }
} }
@@ -320,36 +416,20 @@ class MainActivity : AppCompatActivity() {
): Boolean = false ): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition val position = viewHolder.bindingAdapterPosition
val note = adapter.currentList[position] val swipedNote = adapter.currentList[position]
val notesCopy = adapter.currentList.toMutableList()
// Track pending deletion to prevent flicker // Store original list BEFORE removing note
pendingDeletions.add(note.id) val originalList = adapter.currentList.toList()
// Remove from list immediately for visual feedback // Remove from list for visual feedback (NOT from storage yet!)
notesCopy.removeAt(position) val listWithoutNote = originalList.toMutableList().apply {
adapter.submitList(notesCopy) removeAt(position)
// Show Snackbar with UNDO
Snackbar.make(
recyclerViewNotes,
"Notiz gelöscht",
Snackbar.LENGTH_LONG
).setAction("RÜCKGÄNGIG") {
// UNDO: Remove from pending deletions and restore
pendingDeletions.remove(note.id)
loadNotes()
}.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
if (event != DISMISS_EVENT_ACTION) {
// Snackbar dismissed without UNDO → Actually delete the note
storage.deleteNote(note.id)
pendingDeletions.remove(note.id)
loadNotes()
} }
} adapter.submitList(listWithoutNote)
}).show()
// Show dialog with ability to restore
showServerDeletionDialog(swipedNote, originalList)
} }
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
@@ -361,6 +441,116 @@ class MainActivity : AppCompatActivity() {
itemTouchHelper.attachToRecyclerView(recyclerViewNotes) itemTouchHelper.attachToRecyclerView(recyclerViewNotes)
} }
private fun showServerDeletionDialog(note: Note, originalList: List<Note>) {
val alwaysDeleteFromServer = prefs.getBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, false)
if (alwaysDeleteFromServer) {
// Auto-delete from server without asking
deleteNoteLocally(note, deleteFromServer = true)
return
}
val dialogView = layoutInflater.inflate(R.layout.dialog_server_deletion, null)
val checkboxAlways = dialogView.findViewById<CheckBox>(R.id.checkboxAlwaysDeleteFromServer)
MaterialAlertDialogBuilder(this)
.setTitle("Notiz löschen")
.setMessage("\"${note.title}\" wird lokal gelöscht.\n\nAuch vom Server löschen?")
.setView(dialogView)
.setNeutralButton("Abbrechen") { _, _ ->
// RESTORE: Re-submit original list (note is NOT deleted from storage)
adapter.submitList(originalList)
}
.setOnCancelListener {
// User pressed back - also restore
adapter.submitList(originalList)
}
.setPositiveButton("Nur lokal") { _, _ ->
if (checkboxAlways.isChecked) {
prefs.edit().putBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, false).apply()
}
// NOW actually delete from storage
deleteNoteLocally(note, deleteFromServer = false)
}
.setNegativeButton("Vom Server löschen") { _, _ ->
if (checkboxAlways.isChecked) {
prefs.edit().putBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, true).apply()
}
deleteNoteLocally(note, deleteFromServer = true)
}
.setCancelable(true)
.show()
}
private fun deleteNoteLocally(note: Note, deleteFromServer: Boolean) {
// Track pending deletion to prevent flicker
pendingDeletions.add(note.id)
// Delete from storage
storage.deleteNote(note.id)
// Reload to reflect changes
loadNotes()
// Show Snackbar with UNDO option
val message = if (deleteFromServer) {
"\"${note.title}\" wird lokal und vom Server gelöscht"
} else {
"\"${note.title}\" lokal gelöscht (Server bleibt)"
}
Snackbar.make(recyclerViewNotes, message, Snackbar.LENGTH_LONG)
.setAction("RÜCKGÄNGIG") {
// UNDO: Restore note
storage.saveNote(note)
pendingDeletions.remove(note.id)
loadNotes()
}
.addCallback(object : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
if (event != DISMISS_EVENT_ACTION) {
// Snackbar dismissed without UNDO
pendingDeletions.remove(note.id)
// Delete from server if requested
if (deleteFromServer) {
lifecycleScope.launch {
try {
val webdavService = WebDavSyncService(this@MainActivity)
val success = webdavService.deleteNoteFromServer(note.id)
if (success) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"Vom Server gelöscht",
Toast.LENGTH_SHORT
).show()
}
} else {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"Server-Löschung fehlgeschlagen",
Toast.LENGTH_LONG
).show()
}
}
} catch (e: Exception) {
runOnUiThread {
Toast.makeText(
this@MainActivity,
"Server-Fehler: ${e.message}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
}
}).show()
}
private fun setupFab() { private fun setupFab() {
fabAddNote.setOnClickListener { fabAddNote.setOnClickListener {
openNoteEditor(null) openNoteEditor(null)
@@ -405,6 +595,11 @@ class MainActivity : AppCompatActivity() {
} }
private fun triggerManualSync() { private fun triggerManualSync() {
// 🔄 v1.3.1: Check if sync already running (Banner zeigt Status)
if (!SyncStateManager.tryStartSync("manual")) {
return
}
lifecycleScope.launch { lifecycleScope.launch {
try { try {
// Create sync service // Create sync service
@@ -413,12 +608,10 @@ class MainActivity : AppCompatActivity() {
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization) // 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
if (!syncService.hasUnsyncedChanges()) { if (!syncService.hasUnsyncedChanges()) {
Logger.d(TAG, "⏭️ Manual Sync: No unsynced changes - skipping") Logger.d(TAG, "⏭️ Manual Sync: No unsynced changes - skipping")
showToast("Bereits synchronisiert") SyncStateManager.markCompleted("Bereits synchronisiert")
return@launch return@launch
} }
showToast("Starte Synchronisation...")
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in SyncWorker) // ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in SyncWorker)
val isReachable = withContext(Dispatchers.IO) { val isReachable = withContext(Dispatchers.IO) {
syncService.isServerReachable() syncService.isServerReachable()
@@ -426,7 +619,7 @@ class MainActivity : AppCompatActivity() {
if (!isReachable) { if (!isReachable) {
Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting") Logger.d(TAG, "⏭️ Manual Sync: Server not reachable - aborting")
showToast("Server nicht erreichbar") SyncStateManager.markError("Server nicht erreichbar")
return@launch return@launch
} }
@@ -437,20 +630,21 @@ class MainActivity : AppCompatActivity() {
// Show result // Show result
if (result.isSuccess) { if (result.isSuccess) {
showToast("Sync erfolgreich: ${result.syncedCount} Notizen") SyncStateManager.markCompleted("${result.syncedCount} Notizen")
loadNotes() // Reload notes loadNotes() // Reload notes
} else { } else {
showToast("Sync Fehler: ${result.errorMessage}") SyncStateManager.markError(result.errorMessage)
} }
} catch (e: Exception) { } catch (e: Exception) {
showToast("Sync Fehler: ${e.message}") SyncStateManager.markError(e.message)
} }
} }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu) menuInflater.inflate(R.menu.menu_main, menu)
optionsMenu = menu // 🔄 v1.3.1: Store reference for sync button state
return true return true
} }

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes package dev.dettmer.simplenotes
import android.app.ProgressDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -8,6 +9,7 @@ import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.EditText import android.widget.EditText
import android.widget.RadioButton import android.widget.RadioButton
@@ -23,8 +25,6 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import dev.dettmer.simplenotes.backup.BackupManager import dev.dettmer.simplenotes.backup.BackupManager
@@ -32,11 +32,11 @@ import dev.dettmer.simplenotes.backup.RestoreMode
import dev.dettmer.simplenotes.utils.UrlValidator import dev.dettmer.simplenotes.utils.UrlValidator
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import dev.dettmer.simplenotes.sync.WebDavSyncService import dev.dettmer.simplenotes.sync.WebDavSyncService
import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.sync.NetworkMonitor import dev.dettmer.simplenotes.sync.NetworkMonitor
import dev.dettmer.simplenotes.utils.Constants import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.Logger import dev.dettmer.simplenotes.utils.Logger
import dev.dettmer.simplenotes.utils.showToast import dev.dettmer.simplenotes.utils.showToast
import java.io.File
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -49,6 +49,7 @@ class SettingsActivity : AppCompatActivity() {
private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync" private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync"
private const val GITHUB_PROFILE_URL = "https://github.com/inventory69" private const val GITHUB_PROFILE_URL = "https://github.com/inventory69"
private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE" private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
private const val CONNECTION_TIMEOUT_MS = 3000
} }
private lateinit var textInputLayoutServerUrl: com.google.android.material.textfield.TextInputLayout private lateinit var textInputLayoutServerUrl: com.google.android.material.textfield.TextInputLayout
@@ -56,14 +57,15 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var editTextUsername: EditText private lateinit var editTextUsername: EditText
private lateinit var editTextPassword: EditText private lateinit var editTextPassword: EditText
private lateinit var switchAutoSync: SwitchCompat private lateinit var switchAutoSync: SwitchCompat
private lateinit var switchMarkdownExport: SwitchCompat private lateinit var switchMarkdownAutoSync: SwitchCompat
private lateinit var buttonTestConnection: Button private lateinit var buttonTestConnection: Button
private lateinit var buttonSyncNow: Button private lateinit var buttonSyncNow: Button
private lateinit var buttonCreateBackup: Button private lateinit var buttonCreateBackup: Button
private lateinit var buttonRestoreFromFile: Button private lateinit var buttonRestoreFromFile: Button
private lateinit var buttonRestoreFromServer: Button private lateinit var buttonRestoreFromServer: Button
private lateinit var buttonImportMarkdown: Button private lateinit var buttonManualMarkdownSync: Button
private lateinit var textViewServerStatus: TextView private lateinit var textViewServerStatus: TextView
private lateinit var textViewManualSyncInfo: TextView
// Protocol Selection UI // Protocol Selection UI
private lateinit var protocolRadioGroup: RadioGroup private lateinit var protocolRadioGroup: RadioGroup
@@ -80,6 +82,11 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var cardDeveloperProfile: MaterialCardView private lateinit var cardDeveloperProfile: MaterialCardView
private lateinit var cardLicense: MaterialCardView private lateinit var cardLicense: MaterialCardView
// Debug Section UI
private lateinit var switchFileLogging: com.google.android.material.materialswitch.MaterialSwitch
private lateinit var buttonExportLogs: Button
private lateinit var buttonClearLogs: Button
// Backup Manager // Backup Manager
private val backupManager by lazy { BackupManager(this) } private val backupManager by lazy { BackupManager(this) }
@@ -121,6 +128,7 @@ class SettingsActivity : AppCompatActivity() {
setupListeners() setupListeners()
setupSyncIntervalPicker() setupSyncIntervalPicker()
setupAboutSection() setupAboutSection()
setupDebugSection()
} }
private fun findViews() { private fun findViews() {
@@ -129,14 +137,15 @@ class SettingsActivity : AppCompatActivity() {
editTextUsername = findViewById(R.id.editTextUsername) editTextUsername = findViewById(R.id.editTextUsername)
editTextPassword = findViewById(R.id.editTextPassword) editTextPassword = findViewById(R.id.editTextPassword)
switchAutoSync = findViewById(R.id.switchAutoSync) switchAutoSync = findViewById(R.id.switchAutoSync)
switchMarkdownExport = findViewById(R.id.switchMarkdownExport) switchMarkdownAutoSync = findViewById(R.id.switchMarkdownAutoSync)
buttonTestConnection = findViewById(R.id.buttonTestConnection) buttonTestConnection = findViewById(R.id.buttonTestConnection)
buttonSyncNow = findViewById(R.id.buttonSyncNow) buttonSyncNow = findViewById(R.id.buttonSyncNow)
buttonCreateBackup = findViewById(R.id.buttonCreateBackup) buttonCreateBackup = findViewById(R.id.buttonCreateBackup)
buttonRestoreFromFile = findViewById(R.id.buttonRestoreFromFile) buttonRestoreFromFile = findViewById(R.id.buttonRestoreFromFile)
buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer) buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer)
buttonImportMarkdown = findViewById(R.id.buttonImportMarkdown) buttonManualMarkdownSync = findViewById(R.id.buttonManualMarkdownSync)
textViewServerStatus = findViewById(R.id.textViewServerStatus) textViewServerStatus = findViewById(R.id.textViewServerStatus)
textViewManualSyncInfo = findViewById(R.id.textViewManualSyncInfo)
// Protocol Selection UI // Protocol Selection UI
protocolRadioGroup = findViewById(R.id.protocolRadioGroup) protocolRadioGroup = findViewById(R.id.protocolRadioGroup)
@@ -152,6 +161,11 @@ class SettingsActivity : AppCompatActivity() {
cardGitHubRepo = findViewById(R.id.cardGitHubRepo) cardGitHubRepo = findViewById(R.id.cardGitHubRepo)
cardDeveloperProfile = findViewById(R.id.cardDeveloperProfile) cardDeveloperProfile = findViewById(R.id.cardDeveloperProfile)
cardLicense = findViewById(R.id.cardLicense) cardLicense = findViewById(R.id.cardLicense)
// Debug Section UI
switchFileLogging = findViewById(R.id.switchFileLogging)
buttonExportLogs = findViewById(R.id.buttonExportLogs)
buttonClearLogs = findViewById(R.id.buttonClearLogs)
} }
private fun loadSettings() { private fun loadSettings() {
@@ -179,7 +193,14 @@ class SettingsActivity : AppCompatActivity() {
editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, "")) editTextUsername.setText(prefs.getString(Constants.KEY_USERNAME, ""))
editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, "")) editTextPassword.setText(prefs.getString(Constants.KEY_PASSWORD, ""))
switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false) switchAutoSync.isChecked = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
switchMarkdownExport.isChecked = prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false) // Default: disabled (offline-first)
// Load Markdown Auto-Sync (backward compatible)
val markdownExport = prefs.getBoolean(Constants.KEY_MARKDOWN_EXPORT, false)
val markdownAutoImport = prefs.getBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
val markdownAutoSync = markdownExport && markdownAutoImport
switchMarkdownAutoSync.isChecked = markdownAutoSync
updateMarkdownButtonVisibility()
// Update hint text based on selected protocol // Update hint text based on selected protocol
updateProtocolHint() updateProtocolHint()
@@ -268,17 +289,16 @@ class SettingsActivity : AppCompatActivity() {
showRestoreDialog(RestoreSource.WEBDAV_SERVER, null) showRestoreDialog(RestoreSource.WEBDAV_SERVER, null)
} }
buttonImportMarkdown.setOnClickListener { buttonManualMarkdownSync.setOnClickListener {
saveSettings() performManualMarkdownSync()
importMarkdownChanges()
} }
switchAutoSync.setOnCheckedChangeListener { _, isChecked -> switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
onAutoSyncToggled(isChecked) onAutoSyncToggled(isChecked)
} }
switchMarkdownExport.setOnCheckedChangeListener { _, isChecked -> switchMarkdownAutoSync.setOnCheckedChangeListener { _, isChecked ->
onMarkdownExportToggled(isChecked) onMarkdownAutoSyncToggled(isChecked)
} }
// Clear error when user starts typing again // Clear error when user starts typing again
@@ -303,7 +323,10 @@ class SettingsActivity : AppCompatActivity() {
*/ */
private fun setupSyncIntervalPicker() { private fun setupSyncIntervalPicker() {
// Load current interval from preferences // Load current interval from preferences
val currentInterval = prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES) val currentInterval = prefs.getLong(
Constants.PREF_SYNC_INTERVAL_MINUTES,
Constants.DEFAULT_SYNC_INTERVAL_MINUTES
)
// Set checked radio button based on current interval // Set checked radio button based on current interval
val checkedId = when (currentInterval) { val checkedId = when (currentInterval) {
@@ -376,6 +399,109 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
/**
* Setup Debug section with file logging toggle and export functionality
*/
private fun setupDebugSection() {
// Load current file logging state
val fileLoggingEnabled = prefs.getBoolean(Constants.KEY_FILE_LOGGING_ENABLED, false)
switchFileLogging.isChecked = fileLoggingEnabled
// Update Logger state
Logger.setFileLoggingEnabled(fileLoggingEnabled)
// Toggle file logging
switchFileLogging.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, isChecked).apply()
Logger.setFileLoggingEnabled(isChecked)
if (isChecked) {
showToast("📝 Datei-Logging aktiviert")
Logger.i(TAG, "File logging enabled by user")
} else {
showToast("📝 Datei-Logging deaktiviert")
}
}
// Export logs button
buttonExportLogs.setOnClickListener {
exportAndShareLogs()
}
// Clear logs button
buttonClearLogs.setOnClickListener {
showClearLogsConfirmation()
}
}
/**
* Export logs and share via system share sheet
*/
private fun exportAndShareLogs() {
lifecycleScope.launch {
try {
val logFile = Logger.getLogFile(this@SettingsActivity)
if (logFile == null || !logFile.exists() || logFile.length() == 0L) {
showToast("📭 Keine Logs vorhanden")
return@launch
}
// Create share intent using FileProvider
val logUri = FileProvider.getUriForFile(
this@SettingsActivity,
"${BuildConfig.APPLICATION_ID}.fileprovider",
logFile
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_STREAM, logUri)
putExtra(Intent.EXTRA_SUBJECT, "SimpleNotes Sync Logs")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(shareIntent, "Logs teilen via..."))
Logger.i(TAG, "Logs exported and shared")
} catch (e: Exception) {
Logger.e(TAG, "Failed to export logs", e)
showToast("❌ Fehler beim Exportieren: ${e.message}")
}
}
}
/**
* Show confirmation dialog before clearing logs
*/
private fun showClearLogsConfirmation() {
AlertDialog.Builder(this)
.setTitle("Logs löschen?")
.setMessage("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.")
.setPositiveButton("Löschen") { _, _ ->
clearLogs()
}
.setNegativeButton("Abbrechen", null)
.show()
}
/**
* Clear all log files
*/
private fun clearLogs() {
try {
val cleared = Logger.clearLogFile(this)
if (cleared) {
showToast("🗑️ Logs gelöscht")
} else {
showToast("📭 Keine Logs zum Löschen")
}
} catch (e: Exception) {
Logger.e(TAG, "Failed to clear logs", e)
showToast("❌ Fehler beim Löschen: ${e.message}")
}
}
/** /**
* Opens URL in browser * Opens URL in browser
*/ */
@@ -457,6 +583,14 @@ class SettingsActivity : AppCompatActivity() {
} }
private fun syncNow() { private fun syncNow() {
// 🔄 v1.3.1: Check if sync already running (Button wird deaktiviert)
if (!SyncStateManager.tryStartSync("settings")) {
return
}
// Disable button during sync
buttonSyncNow.isEnabled = false
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val syncService = WebDavSyncService(this@SettingsActivity) val syncService = WebDavSyncService(this@SettingsActivity)
@@ -464,14 +598,16 @@ class SettingsActivity : AppCompatActivity() {
// 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization) // 🔥 v1.1.2: Check if there are unsynced changes first (performance optimization)
if (!syncService.hasUnsyncedChanges()) { if (!syncService.hasUnsyncedChanges()) {
showToast("✅ Bereits synchronisiert") showToast("✅ Bereits synchronisiert")
SyncStateManager.markCompleted()
return@launch return@launch
} }
showToast("Synchronisiere...") showToast("🔄 Synchronisiere...")
// ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in anderen Triggern) // ⭐ WICHTIG: Server-Erreichbarkeits-Check VOR Sync (wie in anderen Triggern)
if (!syncService.isServerReachable()) { if (!syncService.isServerReachable()) {
showToast("⚠️ Server nicht erreichbar") showToast("⚠️ Server nicht erreichbar")
SyncStateManager.markError("Server nicht erreichbar")
checkServerStatus() // Server-Status aktualisieren checkServerStatus() // Server-Status aktualisieren
return@launch return@launch
} }
@@ -480,18 +616,24 @@ class SettingsActivity : AppCompatActivity() {
if (result.isSuccess) { if (result.isSuccess) {
if (result.hasConflicts) { if (result.hasConflicts) {
showToast("Sync abgeschlossen. ${result.conflictCount} Konflikte erkannt!") showToast("Sync abgeschlossen. ${result.conflictCount} Konflikte erkannt!")
} else { } else {
showToast("Erfolgreich! ${result.syncedCount} Notizen synchronisiert") showToast("Erfolgreich! ${result.syncedCount} Notizen synchronisiert")
} }
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
checkServerStatus() // ✅ Server-Status nach Sync aktualisieren checkServerStatus() // ✅ Server-Status nach Sync aktualisieren
} else { } else {
showToast("Sync fehlgeschlagen: ${result.errorMessage}") showToast("Sync fehlgeschlagen: ${result.errorMessage}")
SyncStateManager.markError(result.errorMessage)
checkServerStatus() // ✅ Auch bei Fehler aktualisieren checkServerStatus() // ✅ Auch bei Fehler aktualisieren
} }
} catch (e: Exception) { } catch (e: Exception) {
showToast("Fehler: ${e.message}") showToast("Fehler: ${e.message}")
SyncStateManager.markError(e.message)
checkServerStatus() // ✅ Auch bei Exception aktualisieren checkServerStatus() // ✅ Auch bei Exception aktualisieren
} finally {
// Re-enable button
buttonSyncNow.isEnabled = true
} }
} }
} }
@@ -513,8 +655,8 @@ class SettingsActivity : AppCompatActivity() {
try { try {
val url = URL(serverUrl) val url = URL(serverUrl)
val connection = url.openConnection() as HttpURLConnection val connection = url.openConnection() as HttpURLConnection
connection.connectTimeout = 3000 connection.connectTimeout = CONNECTION_TIMEOUT_MS
connection.readTimeout = 3000 connection.readTimeout = CONNECTION_TIMEOUT_MS
val code = connection.responseCode val code = connection.responseCode
connection.disconnect() connection.disconnect()
code in 200..299 || code == 401 // 401 = Server da, Auth fehlt code in 200..299 || code == 401 // 401 = Server da, Auth fehlt
@@ -547,64 +689,103 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
private fun onMarkdownExportToggled(enabled: Boolean) { private fun onMarkdownAutoSyncToggled(enabled: Boolean) {
prefs.edit().putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled).apply()
if (enabled) { if (enabled) {
showToast("Markdown-Export aktiviert - Notizen werden als .md-Dateien exportiert") // Initial-Export wenn Feature aktiviert wird
} else { lifecycleScope.launch {
showToast("Markdown-Export deaktiviert - nur JSON-Sync aktiv") try {
} val noteStorage = dev.dettmer.simplenotes.storage.NotesStorage(this@SettingsActivity)
val currentNoteCount = noteStorage.loadAllNotes().size
if (currentNoteCount > 0) {
// Zeige Progress-Dialog
val progressDialog = ProgressDialog(this@SettingsActivity).apply {
setTitle("Markdown Auto-Sync")
setMessage("Exportiere Notizen nach Markdown...")
setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
max = currentNoteCount
progress = 0
setCancelable(false)
show()
} }
private fun importMarkdownChanges() { try {
// Prüfen ob Server konfiguriert ist // Hole Server-Daten
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: "" val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
val username = prefs.getString(Constants.KEY_USERNAME, "") ?: "" val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: "" val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) { if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
showToast("Bitte zuerst WebDAV-Server konfigurieren") progressDialog.dismiss()
return showToast("⚠️ Bitte zuerst WebDAV-Server konfigurieren")
switchMarkdownAutoSync.isChecked = false
return@launch
} }
// Import-Dialog mit Warnung // Führe Initial-Export aus
AlertDialog.Builder(this)
.setTitle("Markdown-Import")
.setMessage(
"Importiert Änderungen aus .md-Dateien vom Server.\n\n" +
"⚠️ Bei Konflikten: Last-Write-Wins (neuere Zeitstempel gewinnen)\n\n" +
"Fortfahren?"
)
.setPositiveButton("Importieren") { _, _ ->
performMarkdownImport(serverUrl, username, password)
}
.setNegativeButton("Abbrechen", null)
.show()
}
private fun performMarkdownImport(serverUrl: String, username: String, password: String) {
showToast("Importiere Markdown-Dateien...")
lifecycleScope.launch(Dispatchers.IO) {
try {
val syncService = WebDavSyncService(this@SettingsActivity) val syncService = WebDavSyncService(this@SettingsActivity)
val importCount = syncService.syncMarkdownFiles(serverUrl, username, password) val exportedCount = syncService.exportAllNotesToMarkdown(
serverUrl = serverUrl,
username = username,
password = password,
onProgress = { current, total ->
runOnUiThread {
progressDialog.progress = current
progressDialog.setMessage("Exportiere $current/$total Notizen...")
}
}
)
progressDialog.dismiss()
// Speichere beide Einstellungen
prefs.edit()
.putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled)
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, enabled)
.apply()
updateMarkdownButtonVisibility()
// Erfolgs-Nachricht
showToast("$exportedCount Notizen nach Markdown exportiert")
withContext(Dispatchers.Main) {
if (importCount > 0) {
showToast("$importCount Notizen aus Markdown importiert")
// Benachrichtige MainActivity zum Neuladen
sendBroadcast(Intent("dev.dettmer.simplenotes.NOTES_CHANGED"))
} else {
showToast("Keine Markdown-Änderungen gefunden")
}
}
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { progressDialog.dismiss()
showToast("Import-Fehler: ${e.message}") showToast("❌ Export fehlgeschlagen: ${e.message}")
// Deaktiviere Toggle bei Fehler
switchMarkdownAutoSync.isChecked = false
return@launch
}
} else {
// Keine Notizen vorhanden - speichere Einstellungen direkt
prefs.edit()
.putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled)
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, enabled)
.apply()
updateMarkdownButtonVisibility()
showToast(
"Markdown Auto-Sync aktiviert - " +
"Notizen werden als .md-Dateien exportiert und importiert"
)
}
} catch (e: Exception) {
Logger.e(TAG, "Error toggling markdown auto-sync: ${e.message}")
showToast("Fehler: ${e.message}")
switchMarkdownAutoSync.isChecked = false
} }
} }
} else {
// Deaktivieren - Settings speichern
prefs.edit()
.putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled)
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, enabled)
.apply()
updateMarkdownButtonVisibility()
showToast("Markdown Auto-Sync deaktiviert - nur JSON-Sync aktiv")
} }
} }
@@ -641,11 +822,13 @@ class SettingsActivity : AppCompatActivity() {
intent.data = Uri.parse("package:$packageName") intent.data = Uri.parse("package:$packageName")
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Logger.w(TAG, "Failed to open battery optimization settings: ${e.message}")
// Fallback: Open general battery settings // Fallback: Open general battery settings
try { try {
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
startActivity(intent) startActivity(intent)
} catch (e2: Exception) { } catch (e2: Exception) {
Logger.w(TAG, "Failed to open fallback battery settings: ${e2.message}")
showToast("Bitte Akku-Optimierung manuell deaktivieren") showToast("Bitte Akku-Optimierung manuell deaktivieren")
} }
} }
@@ -664,49 +847,6 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
private fun showRestoreConfirmation() {
android.app.AlertDialog.Builder(this)
.setTitle(R.string.restore_confirmation_title)
.setMessage(R.string.restore_confirmation_message)
.setPositiveButton(R.string.restore_button) { _, _ ->
performRestore()
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun performRestore() {
val progressDialog = android.app.ProgressDialog(this).apply {
setMessage(getString(R.string.restore_progress))
setCancelable(false)
show()
}
CoroutineScope(Dispatchers.Main).launch {
try {
val webdavService = WebDavSyncService(this@SettingsActivity)
val result = withContext(Dispatchers.IO) {
webdavService.restoreFromServer()
}
progressDialog.dismiss()
if (result.isSuccess) {
showToast(getString(R.string.restore_success, result.restoredCount))
// Refresh MainActivity's note list
setResult(RESULT_OK)
} else {
showToast(getString(R.string.restore_error, result.errorMessage))
}
checkServerStatus()
} catch (e: Exception) {
progressDialog.dismiss()
showToast(getString(R.string.restore_error, e.message))
checkServerStatus()
}
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
@@ -769,7 +909,6 @@ class SettingsActivity : AppCompatActivity() {
} }
// Custom View mit Radio Buttons // Custom View mit Radio Buttons
val dialogView = layoutInflater.inflate(android.R.layout.select_dialog_singlechoice, null)
val radioGroup = android.widget.RadioGroup(this).apply { val radioGroup = android.widget.RadioGroup(this).apply {
orientation = android.widget.RadioGroup.VERTICAL orientation = android.widget.RadioGroup.VERTICAL
setPadding(50, 20, 50, 20) setPadding(50, 20, 50, 20)
@@ -778,20 +917,20 @@ class SettingsActivity : AppCompatActivity() {
// Radio Buttons erstellen // Radio Buttons erstellen
val radioMerge = android.widget.RadioButton(this).apply { val radioMerge = android.widget.RadioButton(this).apply {
text = "⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten" text = "⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten"
id = 0 id = android.view.View.generateViewId()
isChecked = true isChecked = true
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
val radioReplace = android.widget.RadioButton(this).apply { val radioReplace = android.widget.RadioButton(this).apply {
text = "⚪ Ersetzen\n → Alle löschen & Backup importieren" text = "⚪ Ersetzen\n → Alle löschen & Backup importieren"
id = 1 id = android.view.View.generateViewId()
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
val radioOverwrite = android.widget.RadioButton(this).apply { val radioOverwrite = android.widget.RadioButton(this).apply {
text = "⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten" text = "⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten"
id = 2 id = android.view.View.generateViewId()
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
@@ -830,8 +969,8 @@ class SettingsActivity : AppCompatActivity() {
.setView(mainLayout) .setView(mainLayout)
.setPositiveButton("Wiederherstellen") { _, _ -> .setPositiveButton("Wiederherstellen") { _, _ ->
val selectedMode = when (radioGroup.checkedRadioButtonId) { val selectedMode = when (radioGroup.checkedRadioButtonId) {
1 -> RestoreMode.REPLACE radioReplace.id -> RestoreMode.REPLACE
2 -> RestoreMode.OVERWRITE_DUPLICATES radioOverwrite.id -> RestoreMode.OVERWRITE_DUPLICATES
else -> RestoreMode.MERGE else -> RestoreMode.MERGE
} }
@@ -862,12 +1001,12 @@ class SettingsActivity : AppCompatActivity() {
progressDialog.dismiss() progressDialog.dismiss()
if (result.success) { if (result.success) {
val message = result.message ?: "Wiederhergestellt: ${result.imported_notes} Notizen" val message = result.message ?: "Wiederhergestellt: ${result.importedNotes} Notizen"
showToast("$message") showToast("$message")
// Refresh MainActivity's note list // Refresh MainActivity's note list
setResult(RESULT_OK) setResult(RESULT_OK)
broadcastNotesChanged() broadcastNotesChanged(result.importedNotes)
} else { } else {
showErrorDialog("Wiederherstellung fehlgeschlagen", result.error ?: "Unbekannter Fehler") showErrorDialog("Wiederherstellung fehlgeschlagen", result.error ?: "Unbekannter Fehler")
} }
@@ -880,12 +1019,7 @@ class SettingsActivity : AppCompatActivity() {
} }
/** /**
* Führt Restore vom Server durch (Task #1.2.0-05b) * Server-Restore mit Restore-Modi (v1.3.0)
* Nutzt neues universelles Dialog-System mit Restore-Modi
*
* HINWEIS: Die alte WebDavSyncService.restoreFromServer() Funktion
* unterstützt noch keine Restore-Modi. Aktuell wird immer REPLACE verwendet.
* TODO: WebDavSyncService.restoreFromServer() erweitern für v1.2.1+
*/ */
private fun performRestoreFromServer(mode: RestoreMode) { private fun performRestoreFromServer(mode: RestoreMode) {
lifecycleScope.launch { lifecycleScope.launch {
@@ -897,7 +1031,6 @@ class SettingsActivity : AppCompatActivity() {
try { try {
Logger.d(TAG, "📥 Restoring from server (mode: $mode)") Logger.d(TAG, "📥 Restoring from server (mode: $mode)")
Logger.w(TAG, "⚠️ Server-Restore nutzt aktuell immer REPLACE Mode (TODO: v1.2.1+)")
// Auto-Backup erstellen (Sicherheitsnetz) // Auto-Backup erstellen (Sicherheitsnetz)
val autoBackupUri = backupManager.createAutoBackup() val autoBackupUri = backupManager.createAutoBackup()
@@ -908,8 +1041,7 @@ class SettingsActivity : AppCompatActivity() {
// Server-Restore durchführen // Server-Restore durchführen
val webdavService = WebDavSyncService(this@SettingsActivity) val webdavService = WebDavSyncService(this@SettingsActivity)
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
// Nutzt alte Funktion (immer REPLACE) webdavService.restoreFromServer(mode) // ✅ Pass mode parameter
webdavService.restoreFromServer()
} }
progressDialog.dismiss() progressDialog.dismiss()
@@ -917,7 +1049,7 @@ class SettingsActivity : AppCompatActivity() {
if (result.isSuccess) { if (result.isSuccess) {
showToast("✅ Wiederhergestellt: ${result.restoredCount} Notizen") showToast("✅ Wiederhergestellt: ${result.restoredCount} Notizen")
setResult(RESULT_OK) setResult(RESULT_OK)
broadcastNotesChanged() broadcastNotesChanged(result.restoredCount)
} else { } else {
showErrorDialog("Wiederherstellung fehlgeschlagen", result.errorMessage ?: "Unbekannter Fehler") showErrorDialog("Wiederherstellung fehlgeschlagen", result.errorMessage ?: "Unbekannter Fehler")
} }
@@ -932,13 +1064,76 @@ class SettingsActivity : AppCompatActivity() {
/** /**
* Sendet Broadcast dass Notizen geändert wurden * Sendet Broadcast dass Notizen geändert wurden
*/ */
private fun broadcastNotesChanged() { private fun broadcastNotesChanged(count: Int = 0) {
val intent = Intent(dev.dettmer.simplenotes.sync.SyncWorker.ACTION_SYNC_COMPLETED) val intent = Intent(dev.dettmer.simplenotes.sync.SyncWorker.ACTION_SYNC_COMPLETED)
intent.putExtra("success", true) intent.putExtra("success", true)
intent.putExtra("syncedCount", 0) intent.putExtra("syncedCount", count)
LocalBroadcastManager.getInstance(this).sendBroadcast(intent) LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
} }
/**
* Updates visibility of manual sync button based on Auto-Sync toggle state
*/
private fun updateMarkdownButtonVisibility() {
val autoSyncEnabled = switchMarkdownAutoSync.isChecked
val visibility = if (autoSyncEnabled) View.GONE else View.VISIBLE
textViewManualSyncInfo.visibility = visibility
buttonManualMarkdownSync.visibility = visibility
}
/**
* Performs manual Markdown sync (Export + Import)
* Called when manual sync button is clicked
*/
private fun performManualMarkdownSync() {
lifecycleScope.launch {
var progressDialog: ProgressDialog? = null
try {
// Validierung
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "")
val username = prefs.getString(Constants.KEY_USERNAME, "")
val password = prefs.getString(Constants.KEY_PASSWORD, "")
if (serverUrl.isNullOrBlank() || username.isNullOrBlank() || password.isNullOrBlank()) {
showToast("⚠️ Bitte zuerst WebDAV-Server konfigurieren")
return@launch
}
// Progress-Dialog
progressDialog = ProgressDialog(this@SettingsActivity).apply {
setTitle("Markdown-Sync")
setMessage("Synchronisiere Markdown-Dateien...")
setCancelable(false)
show()
}
// Sync ausführen
val syncService = dev.dettmer.simplenotes.sync.WebDavSyncService(this@SettingsActivity)
val result = syncService.manualMarkdownSync()
progressDialog.dismiss()
// Erfolgs-Nachricht
val message = "✅ Sync abgeschlossen\n" +
"📤 ${result.exportedCount} exportiert\n" +
"📥 ${result.importedCount} importiert"
showToast(message)
Logger.d(
"SettingsActivity",
"Manual markdown sync: exported=${result.exportedCount}, " +
"imported=${result.importedCount}"
)
} catch (e: Exception) {
progressDialog?.dismiss()
showToast("❌ Sync fehlgeschlagen: ${e.message}")
Logger.e("SettingsActivity", "Manual markdown sync failed", e)
}
}
}
/** /**
* Zeigt Error-Dialog an * Zeigt Error-Dialog an
*/ */

View File

@@ -49,10 +49,10 @@ class BackupManager(private val context: Context) {
Logger.d(TAG, " Found ${allNotes.size} notes to backup") Logger.d(TAG, " Found ${allNotes.size} notes to backup")
val backupData = BackupData( val backupData = BackupData(
backup_version = BACKUP_VERSION, backupVersion = BACKUP_VERSION,
created_at = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
notes_count = allNotes.size, notesCount = allNotes.size,
app_version = BuildConfig.VERSION_NAME, appVersion = BuildConfig.VERSION_NAME,
notes = allNotes notes = allNotes
) )
@@ -65,7 +65,7 @@ class BackupManager(private val context: Context) {
BackupResult( BackupResult(
success = true, success = true,
notes_count = allNotes.size, notesCount = allNotes.size,
message = "Backup erstellt: ${allNotes.size} Notizen" message = "Backup erstellt: ${allNotes.size} Notizen"
) )
@@ -99,10 +99,10 @@ class BackupManager(private val context: Context) {
val allNotes = storage.loadAllNotes() val allNotes = storage.loadAllNotes()
val backupData = BackupData( val backupData = BackupData(
backup_version = BACKUP_VERSION, backupVersion = BACKUP_VERSION,
created_at = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
notes_count = allNotes.size, notesCount = allNotes.size,
app_version = BuildConfig.VERSION_NAME, appVersion = BuildConfig.VERSION_NAME,
notes = allNotes notes = allNotes
) )
@@ -149,7 +149,7 @@ class BackupManager(private val context: Context) {
} }
val backupData = gson.fromJson(jsonString, BackupData::class.java) val backupData = gson.fromJson(jsonString, BackupData::class.java)
Logger.d(TAG, " Backup valid: ${backupData.notes_count} notes, version ${backupData.backup_version}") Logger.d(TAG, " Backup valid: ${backupData.notesCount} notes, version ${backupData.backupVersion}")
// 3. Auto-Backup erstellen (Sicherheitsnetz) // 3. Auto-Backup erstellen (Sicherheitsnetz)
val autoBackupUri = createAutoBackup() val autoBackupUri = createAutoBackup()
@@ -164,7 +164,7 @@ class BackupManager(private val context: Context) {
RestoreMode.OVERWRITE_DUPLICATES -> restoreOverwriteDuplicates(backupData.notes) RestoreMode.OVERWRITE_DUPLICATES -> restoreOverwriteDuplicates(backupData.notes)
} }
Logger.d(TAG, "✅ Restore completed: ${result.imported_notes} imported, ${result.skipped_notes} skipped") Logger.d(TAG, "✅ Restore completed: ${result.importedNotes} imported, ${result.skippedNotes} skipped")
result result
} catch (e: Exception) { } catch (e: Exception) {
@@ -184,10 +184,11 @@ class BackupManager(private val context: Context) {
val backupData = gson.fromJson(jsonString, BackupData::class.java) val backupData = gson.fromJson(jsonString, BackupData::class.java)
// Version kompatibel? // Version kompatibel?
if (backupData.backup_version > BACKUP_VERSION) { if (backupData.backupVersion > BACKUP_VERSION) {
return ValidationResult( return ValidationResult(
isValid = false, isValid = false,
errorMessage = "Backup-Version nicht unterstützt (v${backupData.backup_version} benötigt v${BACKUP_VERSION}+)" errorMessage = "Backup-Version nicht unterstützt " +
"(v${backupData.backupVersion} benötigt v${BACKUP_VERSION}+)"
) )
} }
@@ -238,8 +239,8 @@ class BackupManager(private val context: Context) {
return RestoreResult( return RestoreResult(
success = true, success = true,
imported_notes = newNotes.size, importedNotes = newNotes.size,
skipped_notes = skippedNotes, skippedNotes = skippedNotes,
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen" message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen"
) )
} }
@@ -259,8 +260,8 @@ class BackupManager(private val context: Context) {
return RestoreResult( return RestoreResult(
success = true, success = true,
imported_notes = backupNotes.size, importedNotes = backupNotes.size,
skipped_notes = 0, skippedNotes = 0,
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert" message = "Alle Notizen ersetzt: ${backupNotes.size} importiert"
) )
} }
@@ -283,9 +284,9 @@ class BackupManager(private val context: Context) {
return RestoreResult( return RestoreResult(
success = true, success = true,
imported_notes = newNotes.size, importedNotes = newNotes.size,
skipped_notes = 0, skippedNotes = 0,
overwritten_notes = overwrittenNotes.size, overwrittenNotes = overwrittenNotes.size,
message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben" message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben"
) )
} }
@@ -312,12 +313,17 @@ class BackupManager(private val context: Context) {
/** /**
* Backup-Daten Struktur (JSON) * Backup-Daten Struktur (JSON)
* NOTE: Property names use @SerializedName for JSON compatibility with snake_case
*/ */
data class BackupData( data class BackupData(
val backup_version: Int, @com.google.gson.annotations.SerializedName("backup_version")
val created_at: Long, val backupVersion: Int,
val notes_count: Int, @com.google.gson.annotations.SerializedName("created_at")
val app_version: String, val createdAt: Long,
@com.google.gson.annotations.SerializedName("notes_count")
val notesCount: Int,
@com.google.gson.annotations.SerializedName("app_version")
val appVersion: String,
val notes: List<Note> val notes: List<Note>
) )
@@ -335,7 +341,7 @@ enum class RestoreMode {
*/ */
data class BackupResult( data class BackupResult(
val success: Boolean, val success: Boolean,
val notes_count: Int = 0, val notesCount: Int = 0,
val message: String? = null, val message: String? = null,
val error: String? = null val error: String? = null
) )
@@ -345,9 +351,9 @@ data class BackupResult(
*/ */
data class RestoreResult( data class RestoreResult(
val success: Boolean, val success: Boolean,
val imported_notes: Int = 0, val importedNotes: Int = 0,
val skipped_notes: Int = 0, val skippedNotes: Int = 0,
val overwritten_notes: Int = 0, val overwrittenNotes: Int = 0,
val message: String? = null, val message: String? = null,
val error: String? = null val error: String? = null
) )

View File

@@ -0,0 +1,81 @@
package dev.dettmer.simplenotes.models
import dev.dettmer.simplenotes.utils.Logger
import org.json.JSONArray
import org.json.JSONObject
data class DeletionRecord(
val id: String,
val deletedAt: Long,
val deviceId: String
)
data class DeletionTracker(
val version: Int = 1,
val deletedNotes: MutableList<DeletionRecord> = mutableListOf()
) {
fun addDeletion(noteId: String, deviceId: String) {
if (!deletedNotes.any { it.id == noteId }) {
deletedNotes.add(DeletionRecord(noteId, System.currentTimeMillis(), deviceId))
}
}
fun isDeleted(noteId: String): Boolean {
return deletedNotes.any { it.id == noteId }
}
fun getDeletionTimestamp(noteId: String): Long? {
return deletedNotes.find { it.id == noteId }?.deletedAt
}
fun removeDeletion(noteId: String) {
deletedNotes.removeIf { it.id == noteId }
}
fun toJson(): String {
val jsonObject = JSONObject()
jsonObject.put("version", version)
val notesArray = JSONArray()
for (record in deletedNotes) {
val recordObj = JSONObject()
recordObj.put("id", record.id)
recordObj.put("deletedAt", record.deletedAt)
recordObj.put("deviceId", record.deviceId)
notesArray.put(recordObj)
}
jsonObject.put("deletedNotes", notesArray)
return jsonObject.toString(2) // Pretty print with 2-space indent
}
companion object {
private const val TAG = "DeletionTracker"
fun fromJson(json: String): DeletionTracker? {
return try {
val jsonObject = JSONObject(json)
val version = jsonObject.optInt("version", 1)
val deletedNotes = mutableListOf<DeletionRecord>()
val notesArray = jsonObject.optJSONArray("deletedNotes")
if (notesArray != null) {
for (i in 0 until notesArray.length()) {
val recordObj = notesArray.getJSONObject(i)
val record = DeletionRecord(
id = recordObj.getString("id"),
deletedAt = recordObj.getLong("deletedAt"),
deviceId = recordObj.getString("deviceId")
)
deletedNotes.add(record)
}
}
DeletionTracker(version, deletedNotes)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse DeletionTracker JSON: ${e.message}")
null
}
}
}
}

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.models package dev.dettmer.simplenotes.models
import dev.dettmer.simplenotes.utils.Logger
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -49,11 +50,14 @@ $content
} }
companion object { companion object {
private const val TAG = "Note"
fun fromJson(json: String): Note? { fun fromJson(json: String): Note? {
return try { return try {
val gson = com.google.gson.Gson() val gson = com.google.gson.Gson()
gson.fromJson(json, Note::class.java) gson.fromJson(json, Note::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
null null
} }
} }
@@ -102,6 +106,7 @@ $content
syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
null null
} }
} }
@@ -126,6 +131,7 @@ $content
sdf.timeZone = TimeZone.getTimeZone("UTC") sdf.timeZone = TimeZone.getTimeZone("UTC")
sdf.parse(dateString)?.time ?: System.currentTimeMillis() sdf.parse(dateString)?.time ?: System.currentTimeMillis()
} catch (e: Exception) { } catch (e: Exception) {
Logger.w(TAG, "Failed to parse ISO8601 date '$dateString': ${e.message}")
System.currentTimeMillis() // Fallback System.currentTimeMillis() // Fallback
} }
} }

View File

@@ -1,11 +1,18 @@
package dev.dettmer.simplenotes.storage package dev.dettmer.simplenotes.storage
import android.content.Context import android.content.Context
import dev.dettmer.simplenotes.models.DeletionTracker
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
import dev.dettmer.simplenotes.utils.Logger
import java.io.File import java.io.File
class NotesStorage(private val context: Context) { class NotesStorage(private val context: Context) {
companion object {
private const val TAG = "NotesStorage"
}
private val notesDir: File = File(context.filesDir, "notes").apply { private val notesDir: File = File(context.filesDir, "notes").apply {
if (!exists()) mkdirs() if (!exists()) mkdirs()
} }
@@ -34,19 +41,89 @@ class NotesStorage(private val context: Context) {
fun deleteNote(id: String): Boolean { fun deleteNote(id: String): Boolean {
val file = File(notesDir, "$id.json") val file = File(notesDir, "$id.json")
return file.delete() val deleted = file.delete()
if (deleted) {
Logger.d(TAG, "🗑️ Deleted note: $id")
// Track deletion to prevent zombie notes
val deviceId = DeviceIdGenerator.getDeviceId(context)
trackDeletion(id, deviceId)
}
return deleted
} }
fun deleteAllNotes(): Boolean { fun deleteAllNotes(): Boolean {
return try { return try {
notesDir.listFiles() val notes = loadAllNotes()
?.filter { it.extension == "json" } val deviceId = DeviceIdGenerator.getDeviceId(context)
?.forEach { it.delete() }
for (note in notes) {
deleteNote(note.id) // Uses trackDeletion() automatically
}
Logger.d(TAG, "🗑️ Deleted all notes (${notes.size} notes)")
true true
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "Failed to delete all notes", e)
false false
} }
} }
// === Deletion Tracking ===
private fun getDeletionTrackerFile(): File {
return File(context.filesDir, "deleted_notes.json")
}
fun loadDeletionTracker(): DeletionTracker {
val file = getDeletionTrackerFile()
if (!file.exists()) {
return DeletionTracker()
}
return try {
val json = file.readText()
DeletionTracker.fromJson(json) ?: DeletionTracker()
} catch (e: Exception) {
Logger.e(TAG, "Failed to load deletion tracker", e)
DeletionTracker()
}
}
fun saveDeletionTracker(tracker: DeletionTracker) {
try {
val file = getDeletionTrackerFile()
file.writeText(tracker.toJson())
if (tracker.deletedNotes.size > 1000) {
Logger.w(TAG, "⚠️ Deletion tracker large: ${tracker.deletedNotes.size} entries")
}
Logger.d(TAG, "✅ Deletion tracker saved (${tracker.deletedNotes.size} entries)")
} catch (e: Exception) {
Logger.e(TAG, "Failed to save deletion tracker", e)
}
}
fun trackDeletion(noteId: String, deviceId: String) {
val tracker = loadDeletionTracker()
tracker.addDeletion(noteId, deviceId)
saveDeletionTracker(tracker)
Logger.d(TAG, "📝 Tracked deletion: $noteId")
}
fun isNoteDeleted(noteId: String): Boolean {
val tracker = loadDeletionTracker()
return tracker.isDeleted(noteId)
}
fun clearDeletionTracker() {
saveDeletionTracker(DeletionTracker())
Logger.d(TAG, "🗑️ Deletion tracker cleared")
}
fun getNotesDir(): File = notesDir fun getNotesDir(): File = notesDir
} }

View File

@@ -227,7 +227,11 @@ class NetworkMonitor(private val context: Context) {
if (isWifi) { if (isWifi) {
lastConnectedNetworkId = activeNetwork.toString() lastConnectedNetworkId = activeNetwork.toString()
Logger.d(TAG, " ✅ Initial WiFi network: $lastConnectedNetworkId") Logger.d(TAG, " ✅ Initial WiFi network: $lastConnectedNetworkId")
Logger.d(TAG, " 📡 WiFi already connected at startup - onAvailable() will only trigger on network change") Logger.d(
TAG,
" 📡 WiFi already connected at startup - " +
"onAvailable() will only trigger on network change"
)
} else { } else {
lastConnectedNetworkId = null lastConnectedNetworkId = null
Logger.d(TAG, " ⚠️ Not on WiFi at startup") Logger.d(TAG, " ⚠️ Not on WiFi at startup")
@@ -268,7 +272,7 @@ class NetworkMonitor(private val context: Context) {
connectivityManager.unregisterNetworkCallback(networkCallback) connectivityManager.unregisterNetworkCallback(networkCallback)
Logger.d(TAG, "✅ WiFi monitoring stopped") Logger.d(TAG, "✅ WiFi monitoring stopped")
} catch (e: Exception) { } catch (e: Exception) {
// Already unregistered Logger.w(TAG, "NetworkCallback already unregistered: ${e.message}")
} }
} }
} }

View File

@@ -0,0 +1,129 @@
package dev.dettmer.simplenotes.sync
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import dev.dettmer.simplenotes.utils.Logger
/**
* 🔄 v1.3.1: Zentrale Verwaltung des Sync-Status
*
* Verhindert doppelte Syncs und informiert die UI über den aktuellen Status.
* Thread-safe Singleton mit LiveData für UI-Reaktivität.
*/
object SyncStateManager {
private const val TAG = "SyncStateManager"
/**
* Mögliche Sync-Zustände
*/
enum class SyncState {
IDLE, // Kein Sync aktiv
SYNCING, // Sync läuft gerade
COMPLETED, // Sync erfolgreich abgeschlossen (kurz anzeigen)
ERROR // Sync fehlgeschlagen (kurz anzeigen)
}
/**
* Detaillierte Sync-Informationen für UI
*/
data class SyncStatus(
val state: SyncState = SyncState.IDLE,
val message: String? = null,
val source: String? = null, // "manual", "auto", "pullToRefresh", "background"
val timestamp: Long = System.currentTimeMillis()
)
// Private mutable LiveData
private val _syncStatus = MutableLiveData(SyncStatus())
// Public immutable LiveData für Observer
val syncStatus: LiveData<SyncStatus> = _syncStatus
// Lock für Thread-Sicherheit
private val lock = Any()
/**
* Prüft ob gerade ein Sync läuft
*/
val isSyncing: Boolean
get() = _syncStatus.value?.state == SyncState.SYNCING
/**
* Versucht einen Sync zu starten.
* @return true wenn Sync gestartet werden kann, false wenn bereits einer läuft
*/
fun tryStartSync(source: String): 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")
_syncStatus.postValue(
SyncStatus(
state = SyncState.SYNCING,
message = "Synchronisiere...",
source = source
)
)
return true
}
}
/**
* Markiert Sync als erfolgreich abgeschlossen
*/
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
)
)
}
}
/**
* Markiert Sync als fehlgeschlagen
*/
fun markError(errorMessage: String?) {
synchronized(lock) {
val currentSource = _syncStatus.value?.source
Logger.e(TAG, "❌ Sync failed from: $currentSource - $errorMessage")
_syncStatus.postValue(
SyncStatus(
state = SyncState.ERROR,
message = errorMessage,
source = currentSource
)
)
}
}
/**
* Setzt Status zurück auf IDLE
*/
fun reset() {
synchronized(lock) {
_syncStatus.postValue(SyncStatus())
}
}
/**
* Aktualisiert die Nachricht während des Syncs (z.B. Progress)
*/
fun updateMessage(message: String) {
synchronized(lock) {
val current = _syncStatus.value ?: return
if (current.state == SyncState.SYNCING) {
_syncStatus.postValue(current.copy(message = message))
}
}
}
}

View File

@@ -117,7 +117,11 @@ class SyncWorker(
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Logger.d(TAG, "📍 Step 4: Processing result") Logger.d(TAG, "📍 Step 4: Processing result")
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}") Logger.d(
TAG,
"📦 Sync result: success=${result.isSuccess}, " +
"count=${result.syncedCount}, error=${result.errorMessage}"
)
} }
if (result.isSuccess) { if (result.isSuccess) {

View File

@@ -28,6 +28,7 @@ class WifiSyncReceiver : BroadcastReceiver() {
} }
} }
@Suppress("ReturnCount") // Early returns for WiFi validation checks
private fun isConnectedToHomeWifi(context: Context): Boolean { private fun isConnectedToHomeWifi(context: Context): Boolean {
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false

View File

@@ -23,6 +23,13 @@ object Constants {
const val KEY_MARKDOWN_EXPORT = "markdown_export_enabled" const val KEY_MARKDOWN_EXPORT = "markdown_export_enabled"
const val KEY_MARKDOWN_AUTO_IMPORT = "markdown_auto_import_enabled" const val KEY_MARKDOWN_AUTO_IMPORT = "markdown_auto_import_enabled"
// 🔥 v1.3.0: Performance & Multi-Device Sync
const val KEY_ALWAYS_CHECK_SERVER = "always_check_server"
const val KEY_ALWAYS_DELETE_FROM_SERVER = "always_delete_from_server"
// 🔥 v1.3.1: Debug & Logging
const val KEY_FILE_LOGGING_ENABLED = "file_logging_enabled"
// WorkManager // WorkManager
const val SYNC_WORK_TAG = "notes_sync" const val SYNC_WORK_TAG = "notes_sync"
const val SYNC_DELAY_SECONDS = 5L const val SYNC_DELAY_SECONDS = 5L

View File

@@ -7,6 +7,9 @@ import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private const val DAYS_THRESHOLD = 7L
private const val TRUNCATE_SUFFIX_LENGTH = 3
// Toast Extensions // Toast Extensions
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) { fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show() Toast.makeText(this, message, duration).show()
@@ -27,7 +30,7 @@ fun Long.toReadableTime(): String {
val hours = TimeUnit.MILLISECONDS.toHours(diff) val hours = TimeUnit.MILLISECONDS.toHours(diff)
"Vor $hours Std" "Vor $hours Std"
} }
diff < TimeUnit.DAYS.toMillis(7) -> { diff < TimeUnit.DAYS.toMillis(DAYS_THRESHOLD) -> {
val days = TimeUnit.MILLISECONDS.toDays(diff) val days = TimeUnit.MILLISECONDS.toDays(diff)
"Vor $days Tagen" "Vor $days Tagen"
} }
@@ -41,7 +44,7 @@ fun Long.toReadableTime(): String {
// Truncate long strings // Truncate long strings
fun String.truncate(maxLength: Int): String { fun String.truncate(maxLength: Int): String {
return if (length > maxLength) { return if (length > maxLength) {
substring(0, maxLength - 3) + "..." substring(0, maxLength - TRUNCATE_SUFFIX_LENGTH) + "..."
} else { } else {
this this
} }

View File

@@ -5,7 +5,6 @@ import android.util.Log
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import java.io.File import java.io.File
import java.io.FileWriter import java.io.FileWriter
import java.io.PrintWriter
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -15,10 +14,34 @@ import java.util.*
*/ */
object Logger { object Logger {
private const val MAX_LOG_ENTRIES = 500 // Nur letzte 500 Einträge
private var fileLoggingEnabled = false private var fileLoggingEnabled = false
private var logFile: File? = null private var logFile: File? = null
private var appContext: Context? = null
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
private val maxLogEntries = 500 // Nur letzte 500 Einträge
/**
* Setzt den File-Logging Status (für UI Toggle)
*/
fun setFileLoggingEnabled(enabled: Boolean) {
fileLoggingEnabled = enabled
if (!enabled) {
logFile = null
}
}
/**
* Gibt zurück, ob File-Logging aktiviert ist
*/
fun isFileLoggingEnabled(): Boolean = fileLoggingEnabled
/**
* Initialisiert den Logger mit App-Context
*/
fun init(context: Context) {
appContext = context.applicationContext
}
/** /**
* Aktiviert File-Logging für Debugging * Aktiviert File-Logging für Debugging
@@ -50,11 +73,47 @@ object Logger {
*/ */
fun getLogFile(): File? = logFile fun getLogFile(): File? = logFile
/**
* Gibt Log-Datei mit Context zurück (für SettingsActivity)
*/
fun getLogFile(context: Context): File? {
if (logFile == null && fileLoggingEnabled) {
logFile = File(context.filesDir, "simplenotes_debug.log")
}
return logFile
}
/**
* Löscht die Log-Datei
*/
fun clearLogFile(context: Context): Boolean {
return try {
val file = File(context.filesDir, "simplenotes_debug.log")
if (file.exists()) {
file.delete()
logFile = null
true
} else {
false
}
} catch (e: Exception) {
Log.e("Logger", "Failed to clear log file", e)
false
}
}
/** /**
* Schreibt Log-Eintrag in Datei * Schreibt Log-Eintrag in Datei
*/ */
private fun writeToFile(level: String, tag: String, message: String, throwable: Throwable? = null) { private fun writeToFile(level: String, tag: String, message: String, throwable: Throwable? = null) {
if (!fileLoggingEnabled || logFile == null) return if (!fileLoggingEnabled) return
// Lazy-init logFile mit appContext
if (logFile == null && appContext != null) {
logFile = File(appContext!!.filesDir, "simplenotes_debug.log")
}
if (logFile == null) return
try { try {
val timestamp = dateFormat.format(Date()) val timestamp = dateFormat.format(Date())
@@ -80,13 +139,13 @@ object Logger {
} }
/** /**
* Begrenzt Log-Datei auf maxLogEntries * Begrenzt Log-Datei auf MAX_LOG_ENTRIES
*/ */
private fun trimLogFile() { private fun trimLogFile() {
try { try {
val lines = logFile?.readLines() ?: return val lines = logFile?.readLines() ?: return
if (lines.size > maxLogEntries) { if (lines.size > MAX_LOG_ENTRIES) {
val trimmed = lines.takeLast(maxLogEntries) val trimmed = lines.takeLast(MAX_LOG_ENTRIES)
logFile?.writeText(trimmed.joinToString("\n") + "\n") logFile?.writeText(trimmed.joinToString("\n") + "\n")
} }
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -20,6 +20,7 @@ object NotificationHelper {
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status" private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
private const val NOTIFICATION_ID = 1001 private const val NOTIFICATION_ID = 1001
private const val SYNC_NOTIFICATION_ID = 2 private const val SYNC_NOTIFICATION_ID = 2
private const val AUTO_CANCEL_TIMEOUT_MS = 30_000L
/** /**
* Erstellt Notification Channel (Android 8.0+) * Erstellt Notification Channel (Android 8.0+)
@@ -286,7 +287,7 @@ object NotificationHelper {
Handler(Looper.getMainLooper()).postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
manager.cancel(SYNC_NOTIFICATION_ID) manager.cancel(SYNC_NOTIFICATION_ID)
Logger.d(TAG, "🗑️ Auto-cancelled error notification after 30s timeout") Logger.d(TAG, "🗑️ Auto-cancelled error notification after 30s timeout")
}, 30_000) }, AUTO_CANCEL_TIMEOUT_MS)
} }
/** /**

View File

@@ -0,0 +1,21 @@
package dev.dettmer.simplenotes.utils
/**
* Exception für Sync-spezifische Fehler
*
* Verwendet anstelle von generischen Exceptions für bessere
* Fehlerbehandlung und klarere Fehlermeldungen.
*/
class SyncException(
message: String,
cause: Throwable? = null
) : Exception(message, cause)
/**
* Exception für Validierungsfehler
*
* Verwendet für ungültige Eingaben oder Konfigurationsfehler.
*/
class ValidationException(
message: String
) : IllegalArgumentException(message)

View File

@@ -8,6 +8,16 @@ import java.net.URL
*/ */
object UrlValidator { object UrlValidator {
// RFC 1918 Private IP Ranges
private const val PRIVATE_CLASS_A_FIRST_OCTET = 10
private const val PRIVATE_CLASS_B_FIRST_OCTET = 172
private const val PRIVATE_CLASS_B_SECOND_OCTET_MIN = 16
private const val PRIVATE_CLASS_B_SECOND_OCTET_MAX = 31
private const val PRIVATE_CLASS_C_FIRST_OCTET = 192
private const val PRIVATE_CLASS_C_SECOND_OCTET = 168
private const val LOCALHOST_FIRST_OCTET = 127
private const val OCTET_MAX_VALUE = 255
/** /**
* Prüft ob eine URL eine lokale/private Adresse ist * Prüft ob eine URL eine lokale/private Adresse ist
* Erlaubt: * Erlaubt:
@@ -17,6 +27,7 @@ object UrlValidator {
* - 127.x.x.x (Localhost) * - 127.x.x.x (Localhost)
* - .local domains (mDNS/Bonjour) * - .local domains (mDNS/Bonjour)
*/ */
@Suppress("ReturnCount") // Early returns for validation checks are clearer
fun isLocalUrl(url: String): Boolean { fun isLocalUrl(url: String): Boolean {
return try { return try {
val parsedUrl = URL(url) val parsedUrl = URL(url)
@@ -40,25 +51,29 @@ object UrlValidator {
val octets = match.groupValues.drop(1).map { it.toInt() } val octets = match.groupValues.drop(1).map { it.toInt() }
// Validate octets are in range 0-255 // Validate octets are in range 0-255
if (octets.any { it > 255 }) { if (octets.any { it > OCTET_MAX_VALUE }) {
return false return false
} }
val (o1, o2, o3, o4) = octets // Extract octets individually (destructuring with 4 elements triggers detekt warning)
val o1 = octets[0]
val o2 = octets[1]
// Check RFC 1918 private IP ranges // Check RFC 1918 private IP ranges
return when { return when {
// 10.0.0.0/8 (10.0.0.0 - 10.255.255.255) // 10.0.0.0/8 (10.0.0.0 - 10.255.255.255)
o1 == 10 -> true o1 == PRIVATE_CLASS_A_FIRST_OCTET -> true
// 172.16.0.0/12 (172.16.0.0 - 172.31.255.255) // 172.16.0.0/12 (172.16.0.0 - 172.31.255.255)
o1 == 172 && o2 in 16..31 -> true o1 == PRIVATE_CLASS_B_FIRST_OCTET &&
o2 in PRIVATE_CLASS_B_SECOND_OCTET_MIN..PRIVATE_CLASS_B_SECOND_OCTET_MAX -> true
// 192.168.0.0/16 (192.168.0.0 - 192.168.255.255) // 192.168.0.0/16 (192.168.0.0 - 192.168.255.255)
o1 == 192 && o2 == 168 -> true o1 == PRIVATE_CLASS_C_FIRST_OCTET &&
o2 == PRIVATE_CLASS_C_SECOND_OCTET -> true
// 127.0.0.0/8 (Localhost) // 127.0.0.0/8 (Localhost)
o1 == 127 -> true o1 == LOCALHOST_FIRST_OCTET -> true
else -> false else -> false
} }
@@ -67,7 +82,7 @@ object UrlValidator {
// Not a recognized local address // Not a recognized local address
false false
} catch (e: Exception) { } catch (e: Exception) {
// Invalid URL format Logger.w("UrlValidator", "Failed to parse URL: ${e.message}")
false false
} }
} }

View File

@@ -22,6 +22,39 @@
app:title="@string/app_name" app:title="@string/app_name"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" /> app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
<!-- 🔄 v1.3.1: Sync Status Banner -->
<LinearLayout
android:id="@+id/syncStatusBanner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:background="?attr/colorPrimaryContainer"
android:visibility="gone">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/syncProgressIndicator"
android:layout_width="24dp"
android:layout_height="24dp"
android:indeterminate="true"
app:indicatorSize="24dp"
app:trackThickness="3dp"
app:indicatorColor="?attr/colorOnPrimaryContainer" />
<TextView
android:id="@+id/syncStatusText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="@string/sync_status_syncing"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnPrimaryContainer" />
</LinearLayout>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<!-- SwipeRefreshLayout für Pull-to-Refresh (v1.1.2) --> <!-- SwipeRefreshLayout für Pull-to-Refresh (v1.1.2) -->

View File

@@ -109,7 +109,7 @@
style="@style/Widget.Material3.TextInputLayout.OutlinedBox" style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
app:startIconDrawable="@android:drawable/ic_menu_compass" app:startIconDrawable="@android:drawable/ic_menu_compass"
app:endIconMode="clear_text" app:endIconMode="clear_text"
app:helperText="z.B. http://192.168.0.188:8080/webdav" app:helperText="z.B. http://192.168.0.188:8080/notes"
app:helperTextEnabled="true" app:helperTextEnabled="true"
app:boxCornerRadiusTopStart="12dp" app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusTopEnd="12dp"
@@ -429,11 +429,11 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Markdown Export Toggle --> <!-- Markdown Auto-Sync Toggle (fusioniert Export + Auto-Import) -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginBottom="8dp"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical"> android:gravity="center_vertical">
@@ -441,34 +441,47 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:text="📝 Markdown Export (Desktop-Zugriff)" android:text="🔄 Markdown Auto-Sync"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" /> android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<androidx.appcompat.widget.SwitchCompat <androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchMarkdownExport" android:id="@+id/switchMarkdownAutoSync"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="true" /> android:checked="false" />
</LinearLayout> </LinearLayout>
<!-- Import Markdown Button --> <!-- Auto-Sync Info Text -->
<Button
android:id="@+id/buttonImportMarkdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📥 Markdown-Änderungen importieren"
style="@style/Widget.Material3.Button.TonalButton" />
<!-- Import Info Text -->
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginBottom="16dp"
android:text="Importiert manuelle Änderungen von Desktop-Apps (.md Dateien vom Server)" android:text="Synchronisiert Notizen automatisch als .md Dateien (Upload + Download bei jedem Sync)"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant" /> android:textColor="?attr/colorOnSurfaceVariant" />
<!-- Manual Sync Info (nur sichtbar wenn Auto-Sync OFF) -->
<TextView
android:id="@+id/textViewManualSyncInfo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="Oder synchronisiere Markdown-Dateien manuell:"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnSurface"
android:visibility="gone" />
<!-- Manual Sync Button (nur sichtbar wenn Auto-Sync OFF) -->
<Button
android:id="@+id/buttonManualMarkdownSync"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Markdown synchronisieren"
android:visibility="gone"
style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
@@ -736,6 +749,107 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Material 3 Card: Debug Section -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.Material3.CardView.Elevated"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<!-- Section Header -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Debug &amp; Diagnose"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:layout_marginBottom="16dp" />
<!-- File Logging Toggle -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?attr/colorSurfaceVariant"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📝 Datei-Logging"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:textColor="?attr/colorPrimary"
android:layout_marginBottom="4dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Sync-Logs in Datei speichern"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switchFileLogging"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- F-Droid Privacy Notice -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/file_logging_privacy_notice"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:layout_marginBottom="16dp"
android:paddingStart="4dp"
android:paddingEnd="4dp" />
<!-- Export Logs Button -->
<Button
android:id="@+id/buttonExportLogs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📤 Logs exportieren &amp; teilen"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_marginBottom="8dp" />
<!-- Clear Logs Button -->
<Button
android:id="@+id/buttonClearLogs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🗑️ Logs löschen"
style="@style/Widget.Material3.Button.OutlinedButton" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -15,7 +15,7 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:src="@android:drawable/ic_menu_delete" android:src="@android:drawable/ic_menu_delete"
android:tint="?attr/colorError" app:tint="?attr/colorError"
android:contentDescription="@string/delete" /> android:contentDescription="@string/delete" />
<!-- Title --> <!-- Title -->

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<CheckBox
android:id="@+id/checkboxAlwaysDeleteFromServer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Diese Entscheidung merken"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
</LinearLayout>

View File

@@ -64,7 +64,7 @@
android:layout_height="18dp" android:layout_height="18dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:src="@android:drawable/ic_popup_sync" android:src="@android:drawable/ic_popup_sync"
android:tint="?attr/colorPrimary" app:tint="?attr/colorPrimary"
android:contentDescription="@string/sync_status" /> android:contentDescription="@string/sync_status" />
</LinearLayout> </LinearLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_monochrome"/>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 774 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -4,6 +4,9 @@
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<!-- Adaptive Icon Background -->
<color name="ic_launcher_background">#f9e9c8</color>
<!-- Material 3 Light Theme Colors --> <!-- Material 3 Light Theme Colors -->
<color name="md_theme_light_primary">#6750A4</color> <color name="md_theme_light_primary">#6750A4</color>
<color name="md_theme_light_onPrimary">#FFFFFF</color> <color name="md_theme_light_onPrimary">#FFFFFF</color>

View File

@@ -57,4 +57,13 @@
<string name="restore_progress">Stelle Notizen wieder her…</string> <string name="restore_progress">Stelle Notizen wieder her…</string>
<string name="restore_success">✓ %d Notizen wiederhergestellt</string> <string name="restore_success">✓ %d Notizen wiederhergestellt</string>
<string name="restore_error">Fehler: %s</string> <string name="restore_error">Fehler: %s</string>
<!-- Sync Status Banner (v1.3.1) -->
<string name="sync_status_syncing">Synchronisiere…</string>
<string name="sync_status_completed">Synchronisierung abgeschlossen</string>
<string name="sync_status_error">Synchronisierung fehlgeschlagen</string>
<string name="sync_already_running">Synchronisierung läuft bereits</string>
<!-- Debug/Logging Section (v1.3.2) -->
<string name="file_logging_privacy_notice"> 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.</string>
</resources> </resources>

View File

@@ -2,4 +2,6 @@
plugins { plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.detekt) apply false
} }

View File

@@ -0,0 +1,136 @@
# ⚡ v1.3.1: detekt Configuration
# Pragmatic rules for simple-notes-sync
build:
maxIssues: 100 # Allow existing issues for v1.3.1 release, fix in v1.4.0
excludeCorrectable: false
config:
validation: true
warningsAsErrors: false
comments:
CommentOverPrivateProperty:
active: false
UndocumentedPublicClass:
active: false
UndocumentedPublicFunction:
active: false
complexity:
ComplexCondition:
active: true
threshold: 5
CyclomaticComplexMethod:
active: true
threshold: 15
ignoreSingleWhenExpression: true
LargeClass:
active: true
threshold: 600 # Increased for WebDavSyncService
LongMethod:
active: true
threshold: 80 # Increased for sync methods
LongParameterList:
active: true
functionThreshold: 6
constructorThreshold: 7
NestedBlockDepth:
active: true
threshold: 5
TooManyFunctions:
active: true
thresholdInFiles: 25
thresholdInClasses: 25
thresholdInInterfaces: 20
thresholdInObjects: 20
thresholdInEnums: 10
empty-blocks:
EmptyCatchBlock:
active: true
allowedExceptionNameRegex: "_|(ignore|expected).*"
EmptyFunctionBlock:
active: true
ignoreOverridden: true
exceptions:
SwallowedException:
active: true
ignoredExceptionTypes:
- "InterruptedException"
- "MalformedURLException"
- "NumberFormatException"
- "ParseException"
TooGenericExceptionCaught:
active: true
exceptionNames:
- "Error"
- "Throwable"
allowedExceptionNameRegex: "_|(ignore|expected).*"
naming:
FunctionNaming:
active: true
functionPattern: "[a-zA-Z][a-zA-Z0-9]*"
VariableNaming:
active: true
variablePattern: "[a-z][A-Za-z0-9]*"
PackageNaming:
active: true
packagePattern: "[a-z]+(\\.[a-z][A-Za-z0-9]*)*"
performance:
SpreadOperator:
active: false # Spread operator is fine in most cases
potential-bugs:
CastToNullableType:
active: true
EqualsWithHashCodeExist:
active: true
UnconditionalJumpStatementInLoop:
active: true
style:
ForbiddenComment:
active: true
comments:
- "FIXME:"
- "STOPSHIP:"
allowedPatterns: ""
MagicNumber:
active: true
ignoreNumbers:
- "-1"
- "0"
- "1"
- "2"
- "100"
- "1000"
ignoreHashCodeFunction: true
ignorePropertyDeclaration: true
ignoreLocalVariableDeclaration: true
ignoreAnnotation: true
ignoreEnums: true
ignoreRanges: true
ignoreExtensionFunctions: true
MaxLineLength:
active: true
maxLineLength: 120
excludePackageStatements: true
excludeImportStatements: true
ReturnCount:
active: true
max: 4
excludedFunctions: []
excludeLabeled: true
excludeReturnFromLambda: true
excludeGuardClauses: true
UnusedImports:
active: true
UnusedPrivateMember:
active: true
allowedNames: "_.*"
WildcardImport:
active: false # Allow wildcard imports

View File

@@ -9,6 +9,8 @@ appcompat = "1.6.1"
material = "1.10.0" material = "1.10.0"
activity = "1.8.0" activity = "1.8.0"
constraintlayout = "2.1.4" constraintlayout = "2.1.4"
ktlint = "12.1.0"
detekt = "1.23.4"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -23,4 +25,6 @@ androidx-constraintlayout = { group = "androidx.constraintlayout", name = "const
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" }
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }

324
docs/BACKUP.en.md Normal file
View File

@@ -0,0 +1,324 @@
# 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)

324
docs/BACKUP.md Normal file
View File

@@ -0,0 +1,324 @@
# Backup & Wiederherstellung 💾
**🌍 Languages:** **Deutsch** · [English](BACKUP.en.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)

505
docs/DESKTOP.en.md Normal file
View File

@@ -0,0 +1,505 @@
# 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)

505
docs/DESKTOP.md Normal file
View File

@@ -0,0 +1,505 @@
# Desktop-Integration 🖥️
**🌍 Languages:** **Deutsch** · [English](DESKTOP.en.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)

275
docs/FEATURES.en.md Normal file
View File

@@ -0,0 +1,275 @@
# Complete Feature List 📋
**🌍 Languages:** [Deutsch](FEATURES.md) · **English**
> All features of Simple Notes Sync in detail
---
## 📝 Note Management
### Basic Features
-**Simple text notes** - Focus on content, no distractions
-**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
-**Dual-format** - JSON (master) + Markdown (mirror)
-**Filename sanitization** - Safe filenames from titles
-**Frontmatter metadata** - YAML with ID, timestamps, tags
-**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 binding** - Only in configured home WiFi
-**Battery-friendly** - ~0.2-0.8% per day
-**Smart server check** - No errors on foreign networks
-**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** - When home WiFi connects
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
-**Gateway SSID** - WiFi name for auto-sync
-**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** - Only in home WiFi
-**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)

275
docs/FEATURES.md Normal file
View File

@@ -0,0 +1,275 @@
# Vollständige Feature-Liste 📋
**🌍 Languages:** **Deutsch** · [English](FEATURES.en.md)
> Alle Features von Simple Notes Sync im Detail
---
## 📝 Notiz-Verwaltung
### Basis-Funktionen
-**Einfache Textnotizen** - Fokus auf Inhalt, keine Ablenkung
-**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
### 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
---
## 💾 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
-**Dual-Format** - JSON (Master) + Markdown (Mirror)
-**Dateinamen-Sanitization** - Sichere Dateinamen aus Titeln
-**Frontmatter-Metadata** - YAML mit ID, Timestamps, Tags
-**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
-**WLAN-Bindung** - Nur im konfigurierten Heim-WLAN
-**Akkuschonend** - ~0.2-0.8% pro Tag
-**Smart Server-Check** - Keine Fehler in fremden Netzwerken
-**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** - Wenn Heim-WLAN verbindet
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
-**Gateway SSID** - WLAN-Name für Auto-Sync
-**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
-**WLAN-Only** - Kein Mobile Data Sync
-**Smart Server-Check** - Nur im Heim-WLAN
-**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)

View File

@@ -0,0 +1,5 @@
Unter der Haube haben wir ordentlich aufgeraumt:
- Verbesserte Sync-Performance durch optimierten Code
- Stabilere Fehlerbehandlung bei Verbindungsproblemen
- Speichereffizientere Datenverarbeitung
- Datenschutz-Hinweis fur Datei-Logging hinzugefugt

View File

@@ -0,0 +1,12 @@
v1.2.1 - Initial Export + URL Normalization
Fehlerbehebung
• Bestehende Notizen werden beim Aktivieren der Desktop-Integration exportiert
• Markdown-Dateien landen korrekt im /notes-md/ Ordner
• Vereinfachte Server-Konfiguration: Nur Base-URL eingeben (z.B. http://server:8080/)
• App erstellt automatisch /notes/ und /notes-md/
• Beide URL-Varianten funktionieren: mit und ohne /notes
Verbesserungen
• Beispiel-URL zeigt jetzt /notes statt /webdav
• Progress-Dialog beim Export

View File

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

View File

@@ -0,0 +1,12 @@
v1.3.0 - Multi-Device Sync
NEUE FEATURES:
• Multi-Device Sync mit Deletion Tracking (keine Zombie-Notizen)
• Wisch-Geste zum Server-Löschen (verhindert Duplikate auf anderen Geräten)
• E-Tag Performance-Optimierung (~150ms statt 3s)
• Markdown Auto-Sync Toggle (Export + Import vereint)
• Manueller Markdown-Sync Button
• Server-Wiederherstellung Modi (Merge/Replace/Overwrite)
Dank an Thomas aus Bielefeld!
Kompatibel: v1.2.0-v1.3.0

View File

@@ -0,0 +1,13 @@
v1.3.1 - Multi-Device Sync Fix + Performance
Multi-Device JSON Sync (Danke Thomas!):
- JSON-Dateien syncen jetzt zwischen Geräten
- Funktioniert auch ohne Markdown aktiviert
- Keine doppelten Downloads mehr
Performance-Verbesserungen:
- Sync beschleunigt: 12-14s -> 2-3s
- Erster Sync nach MD-Export jetzt schnell
- JSON erreicht Markdown-Geschwindigkeit
+ Sync-Status-UI, Content MD-Import, Debug-Tools

View File

@@ -4,12 +4,14 @@ HAUPTFUNKTIONEN:
• Einfache Notizen erstellen und bearbeiten • Einfache Notizen erstellen und bearbeiten
• WebDAV-Synchronisation mit eigenem Server • WebDAV-Synchronisation mit eigenem Server
• Multi-Device Sync (Handy, Tablet, Desktop)
• Markdown-Export für Obsidian/Desktop-Editoren
• Automatische Synchronisation im Heim-WLAN • Automatische Synchronisation im Heim-WLAN
• Konfigurierbares Sync-Interval (15/30/60 Minuten) • Konfigurierbares Sync-Interval (15/30/60 Minuten)
• Transparente Batterie-Verbrauchsanzeige • Transparente Batterie-Verbrauchsanzeige
• Material Design 3 mit Dynamic Colors (Android 12+) • Material Design 3 mit Dynamic Colors (Android 12+)
• Swipe-to-Delete mit Bestätigungsdialog • Swipe-to-Delete mit Server-Sync
• Server-Backup & Wiederherstellung • Server-Backup & Wiederherstellung (Merge/Replace/Overwrite)
• Komplett offline nutzbar • Komplett offline nutzbar
• Keine Werbung, keine Tracker • Keine Werbung, keine Tracker
@@ -17,14 +19,23 @@ DATENSCHUTZ:
Deine Daten bleiben bei dir! Die App kommuniziert nur mit deinem eigenen WebDAV-Server. Keine Cloud-Dienste, keine Tracking-Bibliotheken, keine Analysetools. Deine Daten bleiben bei dir! Die App kommuniziert nur mit deinem eigenen WebDAV-Server. Keine Cloud-Dienste, keine Tracking-Bibliotheken, keine Analysetools.
MULTI-DEVICE SYNC:
• Notizen synchronisieren automatisch zwischen allen Geräten
• Lösch-Tracking verhindert "Zombie-Notizen"
• Intelligente Konfliktlösung durch Timestamps
• Markdown-Dateien für Desktop-Bearbeitung (Obsidian, VS Code, etc.)
• Änderungen von Desktop-Editoren werden automatisch importiert
SYNCHRONISATION: SYNCHRONISATION:
• Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.) • Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.)
• Konfigurierbares Interval: 15, 30 oder 60 Minuten • 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) • Gemessener Akkuverbrauch: nur ~0.4% pro Tag (bei 30min)
• Doze Mode optimiert für zuverlässige Background-Syncs • Doze Mode optimiert für zuverlässige Background-Syncs
• Manuelle Synchronisation jederzeit möglich • Manuelle Synchronisation jederzeit möglich
• Konfliktfreie Zusammenführung durch Timestamps
MATERIAL DESIGN 3: MATERIAL DESIGN 3:
@@ -32,6 +43,7 @@ MATERIAL DESIGN 3:
• Dynamic Colors (Material You) auf Android 12+ • Dynamic Colors (Material You) auf Android 12+
• Dark Mode Support • Dark Mode Support
• Intuitive Gesten (Swipe-to-Delete) • Intuitive Gesten (Swipe-to-Delete)
• Live Sync-Status Anzeige
Open Source unter MIT-Lizenz Open Source unter MIT-Lizenz
Quellcode: https://github.com/inventory69/simple-notes-sync Quellcode: https://github.com/inventory69/simple-notes-sync

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,5 @@
Under the hood improvements:
- Improved sync performance through optimized code
- More stable error handling for connection issues
- More memory-efficient data processing
- Added privacy notice for file logging

View File

@@ -0,0 +1,12 @@
v1.2.1 - Initial Export + URL Normalization
Bugfixes
• Existing notes are now exported when Desktop Integration is enabled
• Markdown files now correctly land in /notes-md/ folder
• Simplified server config: Enter only base URL (e.g. http://server:8080/)
• App automatically creates /notes/ and /notes-md/
• Both URL variants work: with and without /notes
Improvements
• Example URL now shows /notes instead of /webdav
• Progress dialog during export

View File

@@ -0,0 +1,12 @@
v1.2.2 - Backward Compatibility for v1.2.0 Users
Critical Bugfix
• Server restore now finds ALL notes (Root + /notes/)
• Users upgrading from v1.2.0 no longer lose data
• Old notes from Root folder are found during restore
Technical Details
• Dual-mode download only active for server restore
• Normal syncs remain fast (scan only /notes/)
• Automatic deduplication prevents duplicates
• Smooth migration: New uploads go to /notes/, old ones remain readable

View File

@@ -0,0 +1,12 @@
v1.3.0 - Multi-Device Sync
NEW FEATURES:
• Multi-Device Sync with deletion tracking (no zombie notes)
• Swipe gesture for server deletion (prevents duplicates on other devices)
• E-Tag performance optimization (~150ms vs 3s)
• Markdown Auto-Sync toggle (unified Export + Import)
• Manual Markdown sync button
• Server restore modes (Merge/Replace/Overwrite)
Thanks to Thomas from Bielefeld!
Compatible: v1.2.0-v1.3.0

View File

@@ -0,0 +1,13 @@
v1.3.1 - Multi-Device Sync Fix + Performance
Multi-Device JSON Sync (Thanks Thomas!):
- JSON files now sync between devices
- Works without Markdown enabled
- No duplicate downloads anymore
Performance Improvements:
- Sync speed: 12-14s -> 2-3s
- First sync after MD export now fast
- JSON matches Markdown speed
+ Sync status UI, content MD import, debug tools

View File

@@ -4,12 +4,14 @@ KEY FEATURES:
• Create and edit simple notes • Create and edit simple notes
• WebDAV synchronization with your own server • WebDAV synchronization with your own server
• Multi-device sync (phone, tablet, desktop)
• Markdown export for Obsidian/desktop editors
• Automatic synchronization on home WiFi • Automatic synchronization on home WiFi
• Configurable sync interval (15/30/60 minutes) • Configurable sync interval (15/30/60 minutes)
• Transparent battery usage display • Transparent battery usage display
• Material Design 3 with Dynamic Colors (Android 12+) • Material Design 3 with Dynamic Colors (Android 12+)
• Swipe-to-delete with confirmation dialog • Swipe-to-delete with server sync
• Server backup & restore • Server backup & restore (Merge/Replace/Overwrite)
• Fully usable offline • Fully usable offline
• No ads, no trackers • No ads, no trackers
@@ -17,14 +19,23 @@ PRIVACY:
Your data stays with you! The app only communicates with your own WebDAV server. No cloud services, no tracking libraries, no analytics tools. Your data stays with you! The app only communicates with your own WebDAV server. No cloud services, no tracking libraries, no analytics tools.
MULTI-DEVICE SYNC:
• Notes sync automatically between all your devices
• Deletion tracking prevents "zombie notes"
• Smart conflict resolution through timestamps
• Markdown files for desktop editing (Obsidian, VS Code, etc.)
• Changes from desktop editors are auto-imported
SYNCHRONIZATION: SYNCHRONIZATION:
• Supports all WebDAV servers (Nextcloud, ownCloud, etc.) • Supports all WebDAV servers (Nextcloud, ownCloud, etc.)
• Configurable interval: 15, 30, or 60 minutes • 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) • Measured battery consumption: only ~0.4% per day (at 30min)
• Doze Mode optimized for reliable background syncs • Doze Mode optimized for reliable background syncs
• Manual synchronization available anytime • Manual synchronization available anytime
• Conflict-free merging through timestamps
MATERIAL DESIGN 3: MATERIAL DESIGN 3:
@@ -32,6 +43,7 @@ MATERIAL DESIGN 3:
• Dynamic Colors (Material You) on Android 12+ • Dynamic Colors (Material You) on Android 12+
• Dark Mode support • Dark Mode support
• Intuitive gestures (Swipe-to-delete) • Intuitive gestures (Swipe-to-delete)
• Live sync status indicator
Open Source under MIT License Open Source under MIT License
Source code: https://github.com/inventory69/simple-notes-sync Source code: https://github.com/inventory69/simple-notes-sync

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,8 +1,8 @@
Categories: Categories:
- Writing - Writing
License: MIT License: MIT
AuthorName: Liq Dettmer AuthorName: inventory69
AuthorEmail: liq@dettmer.dev AuthorEmail: admin@dettmer.dev
AuthorWebSite: https://dettmer.dev AuthorWebSite: https://dettmer.dev
SourceCode: https://github.com/inventory69/simple-notes-sync SourceCode: https://github.com/inventory69/simple-notes-sync
IssueTracker: https://github.com/inventory69/simple-notes-sync/issues IssueTracker: https://github.com/inventory69/simple-notes-sync/issues
@@ -63,7 +63,63 @@ Builds:
scandelete: scandelete:
- android/gradle/wrapper - android/gradle/wrapper
- versionName: 1.2.1
versionCode: 6
commit: v1.2.1
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
- versionName: 1.2.2
versionCode: 7
commit: v1.2.2
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
- versionName: 1.3.0
versionCode: 8
commit: v1.3.0
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
- versionName: 1.3.1
versionCode: 9
commit: v1.3.1
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
AutoUpdateMode: Version AutoUpdateMode: Version
UpdateCheckMode: Tags UpdateCheckMode: Tags
CurrentVersion: 1.2.0 CurrentVersion: 1.3.1
CurrentVersionCode: 5 CurrentVersionCode: 9