9 Commits

Author SHA1 Message Date
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
inventory69
6d135e8f0d fix: Use unique delimiter GHADELIMITER for multiline env vars 2026-01-04 08:28:31 +01:00
inventory69
5d82431bb6 fix: Remove emojis from F-Droid changelogs and fix EOF delimiter
- Removed emojis (🆕 📚) from F-Droid changelogs (better compatibility)
- Changed EOF to CHANGELOG_EOF in workflow (prevents delimiter conflicts)
2026-01-04 02:07:52 +01:00
inventory69
6bb87816f3 Release v1.2.0 - Local Backup & Markdown Desktop Integration
 New Features:
- Local backup/restore system with 3 modes (Merge/Replace/Overwrite)
- Markdown export for desktop access via WebDAV mount
- Dual-format architecture (JSON master + Markdown mirror)
- Settings UI extended with backup & desktop integration sections

📝 Changes:
- Server restore now asks for mode selection (user safety)
- WebDAV mount instructions for Windows/Mac/Linux in README
- Complete CHANGELOG.md with all version history

🔧 Technical:
- BackupManager.kt for complete backup/restore logic
- Note.toMarkdown/fromMarkdown with YAML frontmatter
- ISO8601 timestamps for desktop compatibility
- Last-Write-Wins conflict resolution

📚 Documentation:
- CHANGELOG.md (Keep a Changelog format)
- README updates (removed Joplin/Obsidian, added WebDAV-mount)
- F-Droid changelogs (DE+EN, under 500 chars)
- SYNC_ARCHITECTURE.md in project-docs
- MARKDOWN_DESKTOP_REALITY_CHECK.md strategic plan
- WEB_EDITOR_PLAN_v1.3.0.md for future web editor feature
2026-01-04 01:57:31 +01:00
inventory69
4802c3d979 Update changelog paths, enhance README features, and replace screenshots for v1.1.2 [skip ci] 2025-12-29 10:39:46 +01:00
37 changed files with 5084 additions and 228 deletions

View File

@@ -104,24 +104,22 @@ jobs:
- name: F-Droid Changelogs lesen - name: F-Droid Changelogs lesen
run: | run: |
# Lese deutsche Changelog (Hauptsprache) # Lese deutsche Changelog (Hauptsprache) - Use printf to ensure proper formatting
if [ -f "android/fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then if [ -f "fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then
{ CHANGELOG_CONTENT=$(cat "fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt")
echo 'CHANGELOG_DE<<EOF' echo "CHANGELOG_DE<<GHADELIMITER" >> $GITHUB_ENV
cat "android/fastlane/metadata/android/de-DE/changelogs/${{ env.BUILD_NUMBER }}.txt" echo "$CHANGELOG_CONTENT" >> $GITHUB_ENV
echo 'EOF' echo "GHADELIMITER" >> $GITHUB_ENV
} >> $GITHUB_ENV
else else
echo "CHANGELOG_DE=Keine deutschen Release Notes verfügbar." >> $GITHUB_ENV echo "CHANGELOG_DE=Keine deutschen Release Notes verfügbar." >> $GITHUB_ENV
fi fi
# Lese englische Changelog (optional) # Lese englische Changelog (optional)
if [ -f "android/fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then if [ -f "fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt" ]; then
{ CHANGELOG_CONTENT_EN=$(cat "fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt")
echo 'CHANGELOG_EN<<EOF' echo "CHANGELOG_EN<<GHADELIMITER" >> $GITHUB_ENV
cat "android/fastlane/metadata/android/en-US/changelogs/${{ env.BUILD_NUMBER }}.txt" echo "$CHANGELOG_CONTENT_EN" >> $GITHUB_ENV
echo 'EOF' echo "GHADELIMITER" >> $GITHUB_ENV
} >> $GITHUB_ENV
else else
echo "CHANGELOG_EN=" >> $GITHUB_ENV echo "CHANGELOG_EN=" >> $GITHUB_ENV
fi fi
@@ -153,12 +151,12 @@ jobs:
## 📋 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>

263
CHANGELOG.md Normal file
View File

@@ -0,0 +1,263 @@
# Changelog
All notable changes to Simple Notes Sync will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [1.3.0] - 2026-01-07
### Added
- **🚀 Multi-Device Sync** (Thanks to Thomas from Bielefeld for reporting!)
- Automatic download of new notes from other devices
- Deletion tracking prevents "zombie notes" (deleted notes don't come back)
- Smart cleanup: Re-created notes (newer timestamp) are downloaded
- Works with all devices: v1.2.0, v1.2.1, v1.2.2, and v1.3.0
- **🗑️ Server Deletion via Swipe Gesture**
- Swipe left on notes to delete from server (requires confirmation)
- Prevents duplicate notes on other devices
- Works with deletion tracking system
- Material Design confirmation dialog
- **⚡ E-Tag Performance Optimization**
- Smart server checking with E-Tag caching (~150ms vs 3000ms for "no changes")
- 20x faster when server has no updates
- E-Tag hybrid approach: E-Tag for JSON (fast), timestamp for Markdown (reliable)
- Battery-friendly with minimal server requests
- **📥 Markdown Auto-Sync Toggle**
- NEW: Unified Auto-Sync toggle in Settings (replaces separate Export/Auto-Import toggles)
- When enabled: Notes export to Markdown AND import changes automatically
- When disabled: Manual sync button appears for on-demand synchronization
- Performance: Auto-Sync OFF = 0ms overhead
- **🔘 Manual Markdown Sync Button**
- Manual sync button for performance-conscious users
- Shows import/export counts after completion
- Only visible when Auto-Sync is disabled
- On-demand synchronization (~150-200ms only when triggered)
- **⚙️ Server-Restore Modes**
- MERGE: Keep local notes + add server notes
- REPLACE: Delete all local + download from server
- OVERWRITE: Update duplicates, keep non-duplicates
- Restore modes now work correctly for WebDAV restore
### Technical
- New `DeletionTracker` model with JSON persistence
- `NotesStorage`: Added deletion tracking methods
- `WebDavSyncService.hasUnsyncedChanges()`: Intelligent server checks with E-Tag caching
- `WebDavSyncService.downloadRemoteNotes()`: Deletion-aware downloads
- `WebDavSyncService.restoreFromServer()`: Support for restore modes
- `WebDavSyncService.deleteNoteFromServer()`: Server deletion with YAML frontmatter scanning
- `WebDavSyncService.importMarkdownFiles()`: Automatic Markdown import during sync
- `WebDavSyncService.manualMarkdownSync()`: Manual sync with result counts
- `MainActivity.setupSwipeToDelete()`: Two-stage swipe deletion with confirmation
- E-Tag caching in SharedPreferences for performance
---
## [1.2.2] - 2026-01-06
### Fixed
- **Backward Compatibility for v1.2.0 Users (Critical)**
- App now reads BOTH old (Root) AND new (`/notes/`) folder structures
- Users upgrading from v1.2.0 no longer lose their existing notes
- Server-Restore now finds notes from v1.2.0 stored in Root folder
- Automatic deduplication prevents loading the same note twice
- Graceful error handling if Root folder is not accessible
### Technical
- `WebDavSyncService.downloadRemoteNotes()` - Dual-mode download (Root + /notes/)
- `WebDavSyncService.restoreFromServer()` - Now uses dual-mode download
- Migration happens naturally: new uploads go to `/notes/`, old notes stay readable
---
## [1.2.1] - 2026-01-05
### Fixed
- **Markdown Initial Export Bugfix**
- Existing notes are now exported as Markdown when Desktop Integration is activated
- Previously, only new notes created after activation were exported
- Progress dialog shows export status with current/total counter
- Error handling for network issues during export
- Individual note failures don't abort the entire export
- **Markdown Directory Structure Fix**
- Markdown files now correctly land in `/notes-md/` folder
- Smart URL detection supports both Root-URL and `/notes` URL structures
- Previously, MD files were incorrectly placed in the root directory
- Markdown import now finds files correctly
- **JSON URL Normalization**
- Simplified server configuration: enter only base URL (e.g., `http://server:8080/`)
- App automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown
- Smart detection: both `http://server:8080/` and `http://server:8080/notes/` work correctly
- Backward compatible: existing setups with `/notes` in URL continue to work
- No migration required for existing users
### Changed
- **Markdown Directory Creation**
- `notes-md/` folder is now created on first sync (regardless of Desktop Integration setting)
- Prevents 404 errors when mounting WebDAV folder
- Better user experience: folder is visible before enabling the feature
- **Settings UI Improvements**
- Updated example URL from `/webdav` to `/notes` to match app behavior
- Example now shows: `http://192.168.0.188:8080/notes`
### Technical
- `WebDavSyncService.ensureMarkdownDirectoryExists()` - Creates MD folder early
- `WebDavSyncService.getMarkdownUrl()` - Smart URL detection for both structures
- `WebDavSyncService.exportAllNotesToMarkdown()` - Exports all local notes with progress callback
- `SettingsActivity.onMarkdownExportToggled()` - Triggers initial export with ProgressDialog
---
## [1.2.0] - 2026-01-04
### Added
- **Local Backup System**
- Export all notes as JSON file to any location (Downloads, SD card, cloud folder)
- Import backup with 3 modes: Merge, Replace, or Overwrite duplicates
- Automatic safety backup created before every restore
- Backup validation (format and version check)
- **Markdown Desktop Integration**
- Optional Markdown export parallel to JSON sync
- `.md` files synced to `notes-md/` folder on WebDAV
- YAML frontmatter with `id`, `created`, `updated`, `device`
- Manual import button to pull Markdown changes from server
- Last-Write-Wins conflict resolution via timestamps
- **Settings UI Extensions**
- New "Backup & Restore" section with local + server restore
- New "Desktop Integration" section with Markdown toggle
- Universal restore dialog with radio button mode selection
### Changed
- **Server Restore Behavior**: Users now choose restore mode (Merge/Replace/Overwrite) instead of hard-coded replace-all
### Technical
- `BackupManager.kt` - Complete backup/restore logic
- `Note.toMarkdown()` / `Note.fromMarkdown()` - Markdown conversion with YAML frontmatter
- `WebDavSyncService` - Extended for dual-format sync (JSON master + Markdown mirror)
- ISO8601 timestamp formatting for desktop compatibility
- Filename sanitization for safe Markdown file names
### Documentation
- Added WebDAV mount instructions (Windows, macOS, Linux)
- Created [SYNC_ARCHITECTURE.md](../project-docs/simple-notes-sync/architecture/SYNC_ARCHITECTURE.md) - Complete sync documentation
- Created [MARKDOWN_DESKTOP_REALITY_CHECK.md](../project-docs/simple-notes-sync/markdown-desktop-plan/MARKDOWN_DESKTOP_REALITY_CHECK.md) - Desktop integration analysis
---
## [1.1.2] - 2025-12-28
### Fixed
- **"Job was cancelled" Error**
- Fixed coroutine cancellation in sync worker
- Proper error handling for interrupted syncs
- **UI Improvements**
- Back arrow instead of X in note editor (better UX)
- Pull-to-refresh for manual sync trigger
- HTTP/HTTPS protocol selection with radio buttons
- Inline error display (no toast spam)
- **Performance & Battery**
- Sync only on actual changes (saves battery)
- Auto-save notifications removed
- 24-hour server offline warning instead of instant error
### Changed
- Settings grouped into "Auto-Sync" and "Sync Interval" sections
- HTTP only allowed for local networks (RFC 1918 IPs)
- Swipe-to-delete without UI flicker
---
## [1.1.1] - 2025-12-27
### Fixed
- **WiFi Connect Sync**
- No error notifications in foreign WiFi networks
- Server reachability check before sync (2s timeout)
- Silent abort when server offline
- Pre-check waits until network is ready
- No errors during network initialization
### Changed
- **Notifications**
- Old sync notifications cleared on app start
- Error notifications auto-dismiss after 30 seconds
### UI
- Sync icon only shown when sync is configured
- Swipe-to-delete without flicker
- Scroll to top after saving note
### Technical
- Server check with 2-second timeout before sync attempts
- Network readiness check in WiFi connect trigger
- Notification cleanup on MainActivity.onCreate()
---
## [1.1.0] - 2025-12-26
### Added
- **Configurable Sync Intervals**
- User choice: 15, 30, or 60 minutes
- Real-world battery impact displayed (15min: ~0.8%/day, 30min: ~0.4%/day, 60min: ~0.2%/day)
- Radio button selection in settings
- Doze Mode optimization (syncs batched in maintenance windows)
- **About Section**
- App version from BuildConfig
- Links to GitHub repository and developer profile
- MIT license information
- Material 3 card design
### Changed
- Settings UI redesigned with grouped sections
- Periodic sync updated dynamically when interval changes
- WorkManager uses selected interval for background sync
### Removed
- Debug/Logs section from settings (cleaner UI)
### Technical
- `PREF_SYNC_INTERVAL_MINUTES` preference key
- NetworkMonitor reads interval from SharedPreferences
- `ExistingPeriodicWorkPolicy.UPDATE` for live interval changes
---
## [1.0.0] - 2025-12-25
### Added
- Initial release
- WebDAV synchronization
- Note creation, editing, deletion
- 6 sync triggers:
- Periodic sync (configurable interval)
- App start sync
- WiFi connect sync
- Manual sync (menu button)
- Pull-to-refresh
- Settings "Sync Now" button
- Material 3 design
- Light/Dark theme support
- F-Droid compatible (100% FOSS)
---
[1.2.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.2.0
[1.1.2]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.2
[1.1.1]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.1
[1.1.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.1.0
[1.0.0]: https://github.com/inventory69/simple-notes-sync/releases/tag/v1.0.0

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,42 +24,27 @@
--- ---
## Features ## ✨ Highlights
### 📝 Notes - 📝 **Offline-first** - Works without internet
- Create and edit simple text notes - 🔄 **Auto-sync** - Home WiFi only (15/30/60 min)
- Automatic save - 🔒 **Self-hosted** - Your data stays with you (WebDAV)
- Swipe-to-delete with confirmation - 💾 **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
### 🔄 Synchronization ➡️ **Complete feature list:** [FEATURES.en.md](docs/FEATURES.en.md)
- Auto-sync (15/30/60 min intervals)
- WiFi-based - Sync on home WiFi connection
- Server check (2s timeout) - No errors in foreign networks
- Conflict-free merging via timestamps
### 🏠 Self-Hosted & Privacy
- WebDAV server (Nextcloud, ownCloud, etc.)
- Your data stays with you - No tracking, no analytics
- 100% Open Source (MIT license)
### 🔋 Performance
- Battery-friendly (~0.2-0.8% per day)
- Doze Mode optimized
- Offline-first - All features work without internet
### 🎨 Material Design 3
- Dynamic Colors (Material You)
- Dark Mode
- Modern, intuitive UI
--- ---
## 🚀 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
@@ -65,23 +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)
--- ---
## 📚 Documentation ## 📚 Documentation
- **[Quick Start Guide](QUICKSTART.en.md)** - Step-by-step guide for end users | Document | Content |
- **[Server Setup](server/README.en.md)** - Configure WebDAV server |----------|---------|
- **[Complete Docs](DOCS.en.md)** - Features, troubleshooting, build instructions | **[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
@@ -91,13 +85,13 @@ 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)
--- ---
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details. Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md)
--- ---
@@ -105,4 +99,6 @@ Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
MIT License - see [LICENSE](LICENSE) MIT License - see [LICENSE](LICENSE)
**v1.1.1** · Built with Kotlin + Material Design 3 ---
**v1.2.1** · Built with ❤️ using Kotlin + Material Design 3

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,42 +24,27 @@
--- ---
## Features ## ✨ Highlights
### 📝 Notizen - 📝 **Offline-First** - Funktioniert ohne Internet
- Einfache Textnotizen erstellen und bearbeiten - 🔄 **Auto-Sync** - Nur im Heim-WLAN (15/30/60 Min)
- Automatisches Speichern - 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
- Swipe-to-Delete mit Bestätigung - 💾 **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
### 🔄 Synchronisation ➡️ **Vollständige Feature-Liste:** [FEATURES.md](docs/FEATURES.md)
- Auto-Sync (15/30/60 Min Intervalle)
- WiFi-basiert - Sync bei Heim-WLAN-Verbindung
- Server-Check (2s Timeout) - Keine Fehler in fremden Netzwerken
- Konfliktfreies Merging via Timestamps
### 🏠 Self-Hosted & Privacy
- WebDAV-Server (Nextcloud, ownCloud, etc.)
- Deine Daten bei dir - Kein Tracking, keine Analytics
- 100% Open Source (MIT Lizenz)
### 🔋 Performance
- Akkuschonend (~0.2-0.8% pro Tag)
- Doze Mode optimiert
- Offline-First - Alle Features ohne Internet
### 🎨 Material Design 3
- Dynamic Colors (Material You)
- Dark Mode
- Moderne, intuitive UI
--- ---
## 🚀 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
@@ -65,22 +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)
--- ---
## 📚 Dokumentation ## 📚 Dokumentation
- **[Quick Start Guide](QUICKSTART.md)** - Schritt-für-Schritt Anleitung für Endbenutzer | Dokument | Inhalt |
- **[Server Setup](server/README.md)** - WebDAV Server konfigurieren |----------|--------|
- **[Vollständige Docs](DOCS.md)** - Features, Troubleshooting, Build-Anleitung | **[QUICKSTART.md](QUICKSTART.md)** | Schritt-für-Schritt Installation |
| **[FEATURES.md](docs/FEATURES.md)** | Vollständige Feature-Liste |
| **[BACKUP.md](docs/BACKUP.md)** | Backup & Wiederherstellung |
| **[DESKTOP.md](docs/DESKTOP.md)** | Desktop-Integration (Markdown) |
| **[DOCS.md](docs/DOCS.md)** | Technische Details & Troubleshooting |
| **[CHANGELOG.md](CHANGELOG.md)** | Versionshistorie |
--- ---
@@ -91,13 +88,13 @@ cd android
./gradlew assembleStandardRelease ./gradlew assembleStandardRelease
``` ```
➡️ **Details:** [Build-Anleitung in DOCS.md](DOCS.md) ➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md)
--- ---
## 🤝 Contributing ## 🤝 Contributing
Beiträge sind willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) für Details. Beiträge willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md)
--- ---
@@ -105,4 +102,6 @@ Beiträge sind willkommen! Siehe [CONTRIBUTING.md](CONTRIBUTING.md) für Details
MIT License - siehe [LICENSE](LICENSE) MIT License - siehe [LICENSE](LICENSE)
**v1.1.1** · Gebaut mit Kotlin + Material Design 3 ---
**v1.2.1** · Built with ❤️ using Kotlin + Material Design 3

View File

@@ -17,8 +17,8 @@ android {
applicationId = "dev.dettmer.simplenotes" applicationId = "dev.dettmer.simplenotes"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 4 // 🔥 v1.1.2: UX Fixes + CancellationException Handling versionCode = 8 // 🚀 v1.3.0: Multi-Device Sync with deletion tracking
versionName = "1.1.2" // 🔥 v1.1.2: Better UX + Job Cancellation Fix versionName = "1.3.0" // 🚀 v1.3.0: Multi-Device Sync, E-Tag caching, Markdown auto-import
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -21,13 +21,19 @@ 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
@@ -320,36 +326,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 +351,104 @@ 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)

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,11 +9,13 @@ 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
import android.widget.RadioGroup import android.widget.RadioGroup
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SwitchCompat import androidx.appcompat.widget.SwitchCompat
@@ -26,6 +29,8 @@ import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.coroutines.CoroutineScope 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.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
@@ -53,10 +58,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 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 buttonRestoreFromFile: Button
private lateinit var buttonRestoreFromServer: Button private lateinit var buttonRestoreFromServer: 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
@@ -73,6 +83,22 @@ class SettingsActivity : AppCompatActivity() {
private lateinit var cardDeveloperProfile: MaterialCardView private lateinit var cardDeveloperProfile: MaterialCardView
private lateinit var cardLicense: MaterialCardView private lateinit var cardLicense: MaterialCardView
// Backup Manager
private val backupManager by lazy { BackupManager(this) }
// Activity Result Launchers
private val createBackupLauncher = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { createBackup(it) }
}
private val restoreBackupLauncher = registerForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { showRestoreDialog(RestoreSource.LOCAL_FILE, it) }
}
private val prefs by lazy { private val prefs by lazy {
getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE) getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE)
} }
@@ -106,10 +132,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)
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)
buttonRestoreFromFile = findViewById(R.id.buttonRestoreFromFile)
buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer) buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer)
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)
@@ -153,6 +184,14 @@ class SettingsActivity : AppCompatActivity() {
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)
// 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()
@@ -223,15 +262,35 @@ class SettingsActivity : AppCompatActivity() {
syncNow() syncNow()
} }
buttonCreateBackup.setOnClickListener {
// Dateiname mit Timestamp
val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
.format(java.util.Date())
val filename = "simplenotes_backup_$timestamp.json"
createBackupLauncher.launch(filename)
}
buttonRestoreFromFile.setOnClickListener {
restoreBackupLauncher.launch(arrayOf("application/json"))
}
buttonRestoreFromServer.setOnClickListener { buttonRestoreFromServer.setOnClickListener {
saveSettings() saveSettings()
showRestoreConfirmation() showRestoreDialog(RestoreSource.WEBDAV_SERVER, null)
}
buttonManualMarkdownSync.setOnClickListener {
performManualMarkdownSync()
} }
switchAutoSync.setOnCheckedChangeListener { _, isChecked -> switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
onAutoSyncToggled(isChecked) onAutoSyncToggled(isChecked)
} }
switchMarkdownAutoSync.setOnCheckedChangeListener { _, isChecked ->
onMarkdownAutoSyncToggled(isChecked)
}
// Clear error when user starts typing again // Clear error when user starts typing again
editTextServerUrl.addTextChangedListener(object : android.text.TextWatcher { editTextServerUrl.addTextChangedListener(object : android.text.TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
@@ -498,6 +557,103 @@ class SettingsActivity : AppCompatActivity() {
} }
} }
private fun onMarkdownAutoSyncToggled(enabled: Boolean) {
if (enabled) {
// Initial-Export wenn Feature aktiviert wird
lifecycleScope.launch {
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()
}
try {
// Hole Server-Daten
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, "") ?: ""
val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
progressDialog.dismiss()
showToast("⚠️ Bitte zuerst WebDAV-Server konfigurieren")
switchMarkdownAutoSync.isChecked = false
return@launch
}
// Führe Initial-Export aus
val syncService = WebDavSyncService(this@SettingsActivity)
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")
} catch (e: Exception) {
progressDialog.dismiss()
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")
}
}
private fun checkBatteryOptimization() { private fun checkBatteryOptimization() {
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
val packageName = packageName val packageName = packageName
@@ -612,4 +768,281 @@ class SettingsActivity : AppCompatActivity() {
super.onPause() super.onPause()
saveSettings() saveSettings()
} }
// ========================================
// BACKUP & RESTORE FUNCTIONS (v1.2.0)
// ========================================
/**
* Restore-Quelle (Lokale Datei oder WebDAV Server)
*/
private enum class RestoreSource {
LOCAL_FILE,
WEBDAV_SERVER
}
/**
* Erstellt Backup (Task #1.2.0-04)
*/
private fun createBackup(uri: Uri) {
lifecycleScope.launch {
try {
Logger.d(TAG, "📦 Creating backup...")
val result = backupManager.createBackup(uri)
if (result.success) {
showToast("${result.message}")
} else {
showErrorDialog("Backup fehlgeschlagen", result.error ?: "Unbekannter Fehler")
}
} catch (e: Exception) {
Logger.e(TAG, "Failed to create backup", e)
showErrorDialog("Backup fehlgeschlagen", e.message ?: "Unbekannter Fehler")
}
}
}
/**
* Universeller Restore-Dialog für beide Quellen (Task #1.2.0-05 + #1.2.0-05b)
*
* @param source Lokale Datei oder WebDAV Server
* @param fileUri URI der lokalen Datei (nur für LOCAL_FILE)
*/
private fun showRestoreDialog(source: RestoreSource, fileUri: Uri?) {
val sourceText = when (source) {
RestoreSource.LOCAL_FILE -> "Lokale Datei"
RestoreSource.WEBDAV_SERVER -> "WebDAV Server"
}
// Custom View mit Radio Buttons
val dialogView = layoutInflater.inflate(android.R.layout.select_dialog_singlechoice, null)
val radioGroup = android.widget.RadioGroup(this).apply {
orientation = android.widget.RadioGroup.VERTICAL
setPadding(50, 20, 50, 20)
}
// Radio Buttons erstellen
val radioMerge = android.widget.RadioButton(this).apply {
text = "⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten"
id = 0
isChecked = true
setPadding(10, 10, 10, 10)
}
val radioReplace = android.widget.RadioButton(this).apply {
text = "⚪ Ersetzen\n → Alle löschen & Backup importieren"
id = 1
setPadding(10, 10, 10, 10)
}
val radioOverwrite = android.widget.RadioButton(this).apply {
text = "⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten"
id = 2
setPadding(10, 10, 10, 10)
}
radioGroup.addView(radioMerge)
radioGroup.addView(radioReplace)
radioGroup.addView(radioOverwrite)
// Hauptlayout
val mainLayout = android.widget.LinearLayout(this).apply {
orientation = android.widget.LinearLayout.VERTICAL
setPadding(50, 30, 50, 30)
}
// Info Text
val infoText = android.widget.TextView(this).apply {
text = "Quelle: $sourceText\n\nWiederherstellungs-Modus:"
textSize = 16f
setPadding(0, 0, 0, 20)
}
// Hinweis Text
val hintText = android.widget.TextView(this).apply {
text = "\n Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt."
textSize = 14f
setTypeface(null, android.graphics.Typeface.ITALIC)
setPadding(0, 20, 0, 0)
}
mainLayout.addView(infoText)
mainLayout.addView(radioGroup)
mainLayout.addView(hintText)
// Dialog erstellen
AlertDialog.Builder(this)
.setTitle("⚠️ Backup wiederherstellen?")
.setView(mainLayout)
.setPositiveButton("Wiederherstellen") { _, _ ->
val selectedMode = when (radioGroup.checkedRadioButtonId) {
1 -> RestoreMode.REPLACE
2 -> RestoreMode.OVERWRITE_DUPLICATES
else -> RestoreMode.MERGE
}
when (source) {
RestoreSource.LOCAL_FILE -> fileUri?.let { performRestoreFromFile(it, selectedMode) }
RestoreSource.WEBDAV_SERVER -> performRestoreFromServer(selectedMode)
}
}
.setNegativeButton("Abbrechen", null)
.show()
}
/**
* Führt Restore aus lokaler Datei durch (Task #1.2.0-05)
*/
private fun performRestoreFromFile(uri: Uri, mode: RestoreMode) {
lifecycleScope.launch {
val progressDialog = android.app.ProgressDialog(this@SettingsActivity).apply {
setMessage("Wiederherstellen...")
setCancelable(false)
show()
}
try {
Logger.d(TAG, "📥 Restoring from file: $uri (mode: $mode)")
val result = backupManager.restoreBackup(uri, mode)
progressDialog.dismiss()
if (result.success) {
val message = result.message ?: "Wiederhergestellt: ${result.imported_notes} Notizen"
showToast("$message")
// Refresh MainActivity's note list
setResult(RESULT_OK)
broadcastNotesChanged(result.imported_notes)
} else {
showErrorDialog("Wiederherstellung fehlgeschlagen", result.error ?: "Unbekannter Fehler")
}
} catch (e: Exception) {
progressDialog.dismiss()
Logger.e(TAG, "Failed to restore from file", e)
showErrorDialog("Wiederherstellung fehlgeschlagen", e.message ?: "Unbekannter Fehler")
}
}
}
/**
* Server-Restore mit Restore-Modi (v1.3.0)
*/
private fun performRestoreFromServer(mode: RestoreMode) {
lifecycleScope.launch {
val progressDialog = android.app.ProgressDialog(this@SettingsActivity).apply {
setMessage("Wiederherstellen vom Server...")
setCancelable(false)
show()
}
try {
Logger.d(TAG, "📥 Restoring from server (mode: $mode)")
// Auto-Backup erstellen (Sicherheitsnetz)
val autoBackupUri = backupManager.createAutoBackup()
if (autoBackupUri == null) {
Logger.w(TAG, "⚠️ Auto-backup failed, but continuing with restore")
}
// Server-Restore durchführen
val webdavService = WebDavSyncService(this@SettingsActivity)
val result = withContext(Dispatchers.IO) {
webdavService.restoreFromServer(mode) // ✅ Pass mode parameter
}
progressDialog.dismiss()
if (result.isSuccess) {
showToast("✅ Wiederhergestellt: ${result.restoredCount} Notizen")
setResult(RESULT_OK)
broadcastNotesChanged(result.restoredCount)
} else {
showErrorDialog("Wiederherstellung fehlgeschlagen", result.errorMessage ?: "Unbekannter Fehler")
}
} catch (e: Exception) {
progressDialog.dismiss()
Logger.e(TAG, "Failed to restore from server", e)
showErrorDialog("Wiederherstellung fehlgeschlagen", e.message ?: "Unbekannter Fehler")
}
}
}
/**
* Sendet Broadcast dass Notizen geändert wurden
*/
private fun broadcastNotesChanged(count: Int = 0) {
val intent = Intent(dev.dettmer.simplenotes.sync.SyncWorker.ACTION_SYNC_COMPLETED)
intent.putExtra("success", true)
intent.putExtra("syncedCount", count)
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
*/
private fun showErrorDialog(title: String, message: String) {
AlertDialog.Builder(this)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK", null)
.show()
}
} }

View File

@@ -0,0 +1,361 @@
package dev.dettmer.simplenotes.backup
import android.content.Context
import android.net.Uri
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* BackupManager: Lokale Backup & Restore Funktionalität
*
* Features:
* - Backup aller Notizen in JSON-Datei
* - Restore mit 3 Modi (Merge, Replace, Overwrite Duplicates)
* - Auto-Backup vor Restore (Sicherheitsnetz)
* - Backup-Validierung
*/
class BackupManager(private val context: Context) {
companion object {
private const val TAG = "BackupManager"
private const val BACKUP_VERSION = 1
private const val AUTO_BACKUP_DIR = "auto_backups"
private const val AUTO_BACKUP_RETENTION_DAYS = 7
}
private val storage = NotesStorage(context)
private val gson: Gson = GsonBuilder().setPrettyPrinting().create()
/**
* Erstellt Backup aller Notizen
*
* @param uri Output-URI (via Storage Access Framework)
* @return BackupResult mit Erfolg/Fehler Info
*/
suspend fun createBackup(uri: Uri): BackupResult = withContext(Dispatchers.IO) {
return@withContext try {
Logger.d(TAG, "📦 Creating backup to: $uri")
val allNotes = storage.loadAllNotes()
Logger.d(TAG, " Found ${allNotes.size} notes to backup")
val backupData = BackupData(
backup_version = BACKUP_VERSION,
created_at = System.currentTimeMillis(),
notes_count = allNotes.size,
app_version = BuildConfig.VERSION_NAME,
notes = allNotes
)
val jsonString = gson.toJson(backupData)
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
outputStream.write(jsonString.toByteArray())
Logger.d(TAG, "✅ Backup created successfully")
}
BackupResult(
success = true,
notes_count = allNotes.size,
message = "Backup erstellt: ${allNotes.size} Notizen"
)
} catch (e: Exception) {
Logger.e(TAG, "Failed to create backup", e)
BackupResult(
success = false,
error = "Backup fehlgeschlagen: ${e.message}"
)
}
}
/**
* Erstellt automatisches Backup (vor Restore)
* Gespeichert in app-internem Storage
*
* @return Uri des Auto-Backups oder null bei Fehler
*/
suspend fun createAutoBackup(): Uri? = withContext(Dispatchers.IO) {
return@withContext try {
val autoBackupDir = File(context.filesDir, AUTO_BACKUP_DIR).apply {
if (!exists()) mkdirs()
}
val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
.format(Date())
val filename = "auto_backup_before_restore_$timestamp.json"
val file = File(autoBackupDir, filename)
Logger.d(TAG, "📦 Creating auto-backup: ${file.absolutePath}")
val allNotes = storage.loadAllNotes()
val backupData = BackupData(
backup_version = BACKUP_VERSION,
created_at = System.currentTimeMillis(),
notes_count = allNotes.size,
app_version = BuildConfig.VERSION_NAME,
notes = allNotes
)
file.writeText(gson.toJson(backupData))
// Cleanup alte Auto-Backups
cleanupOldAutoBackups(autoBackupDir)
Logger.d(TAG, "✅ Auto-backup created: ${file.absolutePath}")
Uri.fromFile(file)
} catch (e: Exception) {
Logger.e(TAG, "Failed to create auto-backup", e)
null
}
}
/**
* Stellt Notizen aus Backup wieder her
*
* @param uri Backup-Datei URI
* @param mode Wiederherstellungs-Modus (Merge/Replace/Overwrite)
* @return RestoreResult mit Details
*/
suspend fun restoreBackup(uri: Uri, mode: RestoreMode): RestoreResult = withContext(Dispatchers.IO) {
return@withContext try {
Logger.d(TAG, "📥 Restoring backup from: $uri (mode: $mode)")
// 1. Backup-Datei lesen
val jsonString = context.contentResolver.openInputStream(uri)?.use { inputStream ->
inputStream.bufferedReader().use { it.readText() }
} ?: return@withContext RestoreResult(
success = false,
error = "Datei konnte nicht gelesen werden"
)
// 2. Backup validieren & parsen
val validationResult = validateBackup(jsonString)
if (!validationResult.isValid) {
return@withContext RestoreResult(
success = false,
error = validationResult.errorMessage ?: "Ungültige Backup-Datei"
)
}
val backupData = gson.fromJson(jsonString, BackupData::class.java)
Logger.d(TAG, " Backup valid: ${backupData.notes_count} notes, version ${backupData.backup_version}")
// 3. Auto-Backup erstellen (Sicherheitsnetz)
val autoBackupUri = createAutoBackup()
if (autoBackupUri == null) {
Logger.w(TAG, "⚠️ Auto-backup failed, but continuing with restore")
}
// 4. Restore durchführen (je nach Modus)
val result = when (mode) {
RestoreMode.MERGE -> restoreMerge(backupData.notes)
RestoreMode.REPLACE -> restoreReplace(backupData.notes)
RestoreMode.OVERWRITE_DUPLICATES -> restoreOverwriteDuplicates(backupData.notes)
}
Logger.d(TAG, "✅ Restore completed: ${result.imported_notes} imported, ${result.skipped_notes} skipped")
result
} catch (e: Exception) {
Logger.e(TAG, "Failed to restore backup", e)
RestoreResult(
success = false,
error = "Wiederherstellung fehlgeschlagen: ${e.message}"
)
}
}
/**
* Validiert Backup-Datei
*/
private fun validateBackup(jsonString: String): ValidationResult {
return try {
val backupData = gson.fromJson(jsonString, BackupData::class.java)
// Version kompatibel?
if (backupData.backup_version > BACKUP_VERSION) {
return ValidationResult(
isValid = false,
errorMessage = "Backup-Version nicht unterstützt (v${backupData.backup_version} benötigt v${BACKUP_VERSION}+)"
)
}
// Notizen-Array vorhanden?
if (backupData.notes.isEmpty()) {
return ValidationResult(
isValid = false,
errorMessage = "Backup enthält keine Notizen"
)
}
// Alle Notizen haben ID, title, content?
val invalidNotes = backupData.notes.filter { note ->
note.id.isBlank() || note.title.isBlank()
}
if (invalidNotes.isNotEmpty()) {
return ValidationResult(
isValid = false,
errorMessage = "Backup enthält ${invalidNotes.size} ungültige Notizen"
)
}
ValidationResult(isValid = true)
} catch (e: Exception) {
ValidationResult(
isValid = false,
errorMessage = "Backup-Datei beschädigt oder ungültig: ${e.message}"
)
}
}
/**
* Restore-Modus: MERGE
* Fügt neue Notizen hinzu, behält bestehende
*/
private fun restoreMerge(backupNotes: List<Note>): RestoreResult {
val existingNotes = storage.loadAllNotes()
val existingIds = existingNotes.map { it.id }.toSet()
val newNotes = backupNotes.filter { it.id !in existingIds }
val skippedNotes = backupNotes.size - newNotes.size
newNotes.forEach { note ->
storage.saveNote(note)
}
return RestoreResult(
success = true,
imported_notes = newNotes.size,
skipped_notes = skippedNotes,
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen"
)
}
/**
* Restore-Modus: REPLACE
* Löscht alle bestehenden Notizen, importiert Backup
*/
private fun restoreReplace(backupNotes: List<Note>): RestoreResult {
// Alle bestehenden Notizen löschen
storage.deleteAllNotes()
// Backup-Notizen importieren
backupNotes.forEach { note ->
storage.saveNote(note)
}
return RestoreResult(
success = true,
imported_notes = backupNotes.size,
skipped_notes = 0,
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert"
)
}
/**
* Restore-Modus: OVERWRITE_DUPLICATES
* Backup überschreibt bei ID-Konflikten
*/
private fun restoreOverwriteDuplicates(backupNotes: List<Note>): RestoreResult {
val existingNotes = storage.loadAllNotes()
val existingIds = existingNotes.map { it.id }.toSet()
val newNotes = backupNotes.filter { it.id !in existingIds }
val overwrittenNotes = backupNotes.filter { it.id in existingIds }
// Alle Backup-Notizen speichern (überschreibt automatisch)
backupNotes.forEach { note ->
storage.saveNote(note)
}
return RestoreResult(
success = true,
imported_notes = newNotes.size,
skipped_notes = 0,
overwritten_notes = overwrittenNotes.size,
message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben"
)
}
/**
* Löscht Auto-Backups älter als RETENTION_DAYS
*/
private fun cleanupOldAutoBackups(autoBackupDir: File) {
try {
val retentionTimeMs = AUTO_BACKUP_RETENTION_DAYS * 24 * 60 * 60 * 1000L
val cutoffTime = System.currentTimeMillis() - retentionTimeMs
autoBackupDir.listFiles()?.forEach { file ->
if (file.lastModified() < cutoffTime) {
Logger.d(TAG, "🗑️ Deleting old auto-backup: ${file.name}")
file.delete()
}
}
} catch (e: Exception) {
Logger.e(TAG, "Failed to cleanup old backups", e)
}
}
}
/**
* Backup-Daten Struktur (JSON)
*/
data class BackupData(
val backup_version: Int,
val created_at: Long,
val notes_count: Int,
val app_version: String,
val notes: List<Note>
)
/**
* Wiederherstellungs-Modi
*/
enum class RestoreMode {
MERGE, // Bestehende + Neue (Standard)
REPLACE, // Alles löschen + Importieren
OVERWRITE_DUPLICATES // Backup überschreibt bei ID-Konflikten
}
/**
* Backup-Ergebnis
*/
data class BackupResult(
val success: Boolean,
val notes_count: Int = 0,
val message: String? = null,
val error: String? = null
)
/**
* Restore-Ergebnis
*/
data class RestoreResult(
val success: Boolean,
val imported_notes: Int = 0,
val skipped_notes: Int = 0,
val overwritten_notes: Int = 0,
val message: String? = null,
val error: String? = null
)
/**
* Validierungs-Ergebnis
*/
data class ValidationResult(
val isValid: Boolean,
val errorMessage: String? = null
)

View File

@@ -0,0 +1,77 @@
package dev.dettmer.simplenotes.models
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 {
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) {
null
}
}
}
}

View File

@@ -1,5 +1,9 @@
package dev.dettmer.simplenotes.models package dev.dettmer.simplenotes.models
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.UUID import java.util.UUID
data class Note( data class Note(
@@ -25,6 +29,25 @@ data class Note(
""".trimIndent() """.trimIndent()
} }
/**
* Konvertiert Note zu Markdown mit YAML Frontmatter (Task #1.2.0-08)
* Format kompatibel mit Obsidian, Joplin, Typora
*/
fun toMarkdown(): String {
return """
---
id: $id
created: ${formatISO8601(createdAt)}
updated: ${formatISO8601(updatedAt)}
device: $deviceId
---
# $title
$content
""".trimIndent()
}
companion object { companion object {
fun fromJson(json: String): Note? { fun fromJson(json: String): Note? {
return try { return try {
@@ -34,6 +57,78 @@ data class Note(
null null
} }
} }
/**
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
*
* @param md Markdown-String mit YAML Frontmatter
* @return Note-Objekt oder null bei Parse-Fehler
*/
fun fromMarkdown(md: String): Note? {
return try {
// Parse YAML Frontmatter + Markdown Content
val frontmatterRegex = Regex("^---\\n(.+?)\\n---\\n(.*)$", RegexOption.DOT_MATCHES_ALL)
val match = frontmatterRegex.find(md) ?: return null
val yamlBlock = match.groupValues[1]
val contentBlock = match.groupValues[2]
// Parse YAML (einfach per String-Split für MVP)
val metadata = yamlBlock.lines()
.mapNotNull { line ->
val parts = line.split(":", limit = 2)
if (parts.size == 2) {
parts[0].trim() to parts[1].trim()
} else null
}.toMap()
// Extract title from first # heading
val title = contentBlock.lines()
.firstOrNull { it.startsWith("# ") }
?.removePrefix("# ")?.trim() ?: "Untitled"
// Extract content (everything after heading)
val content = contentBlock
.substringAfter("# $title\n\n", "")
.trim()
Note(
id = metadata["id"] ?: UUID.randomUUID().toString(),
title = title,
content = content,
createdAt = parseISO8601(metadata["created"] ?: ""),
updatedAt = parseISO8601(metadata["updated"] ?: ""),
deviceId = metadata["device"] ?: "desktop",
syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert
)
} catch (e: Exception) {
null
}
}
/**
* Formatiert Timestamp zu ISO8601 (Task #1.2.0-10)
* Format: 2024-12-21T18:00:00Z (UTC)
*/
private fun formatISO8601(timestamp: Long): String {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
return sdf.format(Date(timestamp))
}
/**
* Parst ISO8601 zurück zu Timestamp (Task #1.2.0-10)
* Fallback: Aktueller Timestamp bei Fehler
*/
private fun parseISO8601(dateString: String): Long {
return try {
val sdf = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US)
sdf.timeZone = TimeZone.getTimeZone("UTC")
sdf.parse(dateString)?.time ?: System.currentTimeMillis()
} catch (e: Exception) {
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

@@ -19,6 +19,14 @@ object Constants {
const val PREF_SYNC_INTERVAL_MINUTES = "sync_interval_minutes" const val PREF_SYNC_INTERVAL_MINUTES = "sync_interval_minutes"
const val DEFAULT_SYNC_INTERVAL_MINUTES = 30L const val DEFAULT_SYNC_INTERVAL_MINUTES = 30L
// 🔥 v1.2.0: Markdown Export/Import
const val KEY_MARKDOWN_EXPORT = "markdown_export_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"
// 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

@@ -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"
@@ -387,6 +387,105 @@
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Material 3 Card: Markdown Desktop-Integration (v1.2.0) -->
<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="Markdown Desktop-Integration"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:layout_marginBottom="12dp" />
<!-- Info Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:text=" Exportiert Notizen zusätzlich als .md Dateien. Mounte WebDAV als Netzlaufwerk um mit VS Code, Typora oder jedem Markdown-Editor zu bearbeiten. JSON-Sync bleibt primäres Format."
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnPrimaryContainer"
android:lineSpacingMultiplier="1.3" />
</com.google.android.material.card.MaterialCardView>
<!-- Markdown Auto-Sync Toggle (fusioniert Export + Auto-Import) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🔄 Markdown Auto-Sync"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/switchMarkdownAutoSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false" />
</LinearLayout>
<!-- Auto-Sync Info Text -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="Synchronisiert Notizen automatisch als .md Dateien (Upload + Download bei jedem Sync)"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
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>
</com.google.android.material.card.MaterialCardView>
<!-- Material 3 Card: Backup & Restore --> <!-- Material 3 Card: Backup & Restore -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -409,12 +508,12 @@
android:textAppearance="@style/TextAppearance.Material3.TitleMedium" android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:layout_marginBottom="12dp" /> android:layout_marginBottom="12dp" />
<!-- Warning Info Card --> <!-- Info Card (anstatt Warning) -->
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
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="16dp"
app:cardBackgroundColor="?attr/colorErrorContainer" app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardCornerRadius="12dp" app:cardCornerRadius="12dp"
app:cardElevation="0dp"> app:cardElevation="0dp">
@@ -422,19 +521,61 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="16dp" android:padding="16dp"
android:text="@string/backup_restore_warning" android:text=" Bei jeder Wiederherstellung wird automatisch ein Sicherheits-Backup erstellt."
android:textAppearance="@style/TextAppearance.Material3.BodySmall" android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnErrorContainer" android:textColor="?attr/colorOnPrimaryContainer"
android:lineSpacingMultiplier="1.3" /> android:lineSpacingMultiplier="1.3" />
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<!-- Restore Button --> <!-- Lokales Backup Sektion -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Lokales Backup"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:layout_marginBottom="12dp" />
<!-- Backup erstellen Button -->
<Button
android:id="@+id/buttonCreateBackup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📥 Backup erstellen"
android:layout_marginBottom="8dp"
style="@style/Widget.Material3.Button.TonalButton" />
<!-- Aus Datei wiederherstellen Button -->
<Button
android:id="@+id/buttonRestoreFromFile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📤 Aus Datei wiederherstellen"
android:layout_marginBottom="16dp"
style="@style/Widget.Material3.Button.TonalButton" />
<!-- Divider -->
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/colorOutline"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" />
<!-- Server-Backup Sektion -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Server-Backup"
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
android:layout_marginBottom="12dp" />
<!-- Vom Server wiederherstellen Button -->
<Button <Button
android:id="@+id/buttonRestoreFromServer" android:id="@+id/buttonRestoreFromServer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/restore_from_server" android:text="🔄 Vom Server wiederherstellen"
style="@style/Widget.Material3.Button.TonalButton" /> style="@style/Widget.Material3.Button.TonalButton" />
</LinearLayout> </LinearLayout>

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>

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)

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

@@ -0,0 +1,274 @@
# 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)
---
## 🔋 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
### 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 (see [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
### v1.3.0 - Web Editor & Organization
- **Browser-based editor** - Edit notes in web browser
- **WebDAV access via browser** - No mount needed
- **Mobile-optimized** - Responsive design
- **Offline-capable** - Progressive Web App (PWA)
- **Tags/labels** - Categorize notes
- **Search** - Full-text search in all notes
- **Sorting** - By date, title, tags
- **Filter** - Filter by tags
### v1.4.0 - Sharing & Export
- **Share note** - Via share intent
- **Export single note** - As .txt or .md
- **Import from text** - Via share intent
### v1.5.0 - Advanced Editor Features
- **Markdown preview** - In-app rendering
- **Checklists** - TODO lists in notes
- **Syntax highlighting** - For code snippets
---
## 📊 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.2.1 (2026-01-05)

274
docs/FEATURES.md Normal file
View File

@@ -0,0 +1,274 @@
# 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)
---
## 🔋 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
### 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 (siehe [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
### v1.3.0 - Web Editor & Organisation
- **Browser-basierter Editor** - Notizen im Webbrowser bearbeiten
- **WebDAV-Zugriff via Browser** - Kein Mount nötig
- **Mobile-optimiert** - Responsive Design
- **Offline-fähig** - Progressive Web App (PWA)
- **Tags/Labels** - Kategorisierung von Notizen
- **Suche** - Volltextsuche in allen Notizen
- **Sortierung** - Nach Datum, Titel, Tags
- **Filter** - Nach Tags filtern
### v1.4.0 - Sharing & Export
- **Notiz teilen** - Via Share-Intent
- **Einzelne Notiz exportieren** - Als .txt oder .md
- **Import von Text** - Via Share-Intent
### v1.5.0 - Erweiterte Editor-Features
- **Markdown-Vorschau** - In-App Rendering
- **Checklisten** - TODO-Listen in Notizen
- **Syntax-Highlighting** - Für Code-Snippets
---
## 📊 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.2.1 (2026-01-05)

View File

@@ -0,0 +1,13 @@
v1.2.0 - Backup & Desktop-Integration
Lokales Backup/Restore
• Exportiere alle Notizen als JSON
• 3 Wiederherstellungs-Modi (Merge/Replace/Overwrite)
• Auto-Backup vor Restore
Markdown Desktop-Integration (optional)
• .md Export für Desktop-Editoren (WebDAV-Mount)
• Last-Write-Wins Sync
• Manueller Import
Sync-Architektur Doku

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -0,0 +1,13 @@
v1.2.0 - Backup & Desktop Integration
Local Backup/Restore
• Export all notes as JSON
• 3 restore modes (Merge/Replace/Overwrite)
• Auto-backup before restore
Markdown Desktop Integration (optional)
• .md export for desktop editors (WebDAV mount)
• Last-Write-Wins sync
• Manual import
Sync architecture docs

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

@@ -47,7 +47,23 @@ Builds:
scandelete: scandelete:
- android/gradle/wrapper - android/gradle/wrapper
- versionName: 1.2.0
versionCode: 5
commit: v1.2.0
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
srclibs:
- reproducible-apk-tools@v0.2.8
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
AutoUpdateMode: Version AutoUpdateMode: Version
UpdateCheckMode: Tags UpdateCheckMode: Tags
CurrentVersion: 1.1.1 CurrentVersion: 1.2.0
CurrentVersionCode: 3 CurrentVersionCode: 5