2 Commits

Author SHA1 Message Date
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
15 changed files with 210 additions and 95 deletions

View File

@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
## [1.2.2] - TBD
### 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

View File

@@ -6,7 +6,7 @@
[![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)
**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Documentation](DOCS.en.md)** · **🚀 [Quick Start](QUICKSTART.en.md)**
**📱 [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**
@@ -32,7 +32,7 @@
- 🔋 **Battery-friendly** - ~0.2-0.8% per day
- 🎨 **Material Design 3** - Dark mode & dynamic colors
➡️ **Complete feature list:** [FEATURES.en.md](FEATURES.en.md)
➡️ **Complete feature list:** [FEATURES.en.md](docs/FEATURES.en.md)
---
@@ -71,12 +71,9 @@ docker compose up -d
| Document | Content |
|----------|---------|
| **[QUICKSTART.en.md](QUICKSTART.en.md)** | Step-by-step installation |
| **[FEATURES.en.md](FEATURES.en.md)** | Complete feature list |
| **[BACKUP.en.md](BACKUP.en.md)** | Backup & restore guide |
| **[DESKTOP.en.md](DESKTOP.en.md)** | Desktop integration (Markdown) |
| **[DOCS.en.md](DOCS.en.md)** | Technical details & troubleshooting |
| **[CHANGELOG.md](CHANGELOG.md)** | Version history |
| **[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
@@ -86,7 +83,7 @@ cd android
./gradlew assembleStandardRelease
```
➡️ **Build guide:** [DOCS.en.md](DOCS.en.md)
➡️ **Build guide:** [DOCS.en.md](docs/DOCS.en.md)
---

View File

@@ -6,7 +6,7 @@
[![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)
**📱 [APK Download](https://github.com/inventory69/simple-notes-sync/releases/latest)** · **📖 [Dokumentation](DOCS.md)** · **🚀 [Quick Start](QUICKSTART.md)**
**📱 [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)
@@ -32,7 +32,7 @@
- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
➡️ **Vollständige Feature-Liste:** [FEATURES.md](FEATURES.md)
➡️ **Vollständige Feature-Liste:** [FEATURES.md](docs/FEATURES.md)
---
@@ -71,10 +71,10 @@ docker compose up -d
| Dokument | Inhalt |
|----------|--------|
| **[QUICKSTART.md](QUICKSTART.md)** | Schritt-für-Schritt Installation |
| **[FEATURES.md](FEATURES.md)** | Vollständige Feature-Liste |
| **[BACKUP.md](BACKUP.md)** | Backup & Wiederherstellung |
| **[DESKTOP.md](DESKTOP.md)** | Desktop-Integration (Markdown) |
| **[DOCS.md](DOCS.md)** | Technische Details & Troubleshooting |
| **[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 |
---
@@ -86,7 +86,7 @@ cd android
./gradlew assembleStandardRelease
```
➡️ **Build-Anleitung:** [DOCS.md](DOCS.md)
➡️ **Build-Anleitung:** [DOCS.md](docs/DOCS.md)
---

View File

@@ -17,8 +17,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
versionCode = 6 // 🐛 v1.2.1: Markdown Initial Export Bugfix
versionName = "1.2.1" // 🐛 v1.2.1: Markdown Initial Export Bugfix
versionCode = 7 // 🔧 v1.2.2: Backward compatibility for v1.2.0 migration
versionName = "1.2.2" // 🔧 v1.2.2: Dual-mode download (Root + /notes/)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -675,12 +675,22 @@ class WebDavSyncService(private val context: Context) {
val conflictCount: Int
)
private fun downloadRemoteNotes(sardine: Sardine, serverUrl: String): DownloadResult {
private fun downloadRemoteNotes(
sardine: Sardine,
serverUrl: String,
includeRootFallback: Boolean = false // 🆕 v1.2.2: Only for restore from server
): DownloadResult {
var downloadedCount = 0
var conflictCount = 0
val processedIds = mutableSetOf<String>() // 🆕 v1.2.2: Track already loaded notes
try {
// 🆕 PHASE 1: Download from /notes/ (new structure v1.2.1+)
val notesUrl = getNotesUrl(serverUrl)
Logger.d(TAG, "🔍 Phase 1: Checking /notes/ at: $notesUrl")
if (sardine.exists(notesUrl)) {
Logger.d(TAG, " ✅ /notes/ exists, scanning...")
val resources = sardine.list(notesUrl)
for (resource in resources) {
@@ -688,10 +698,13 @@ class WebDavSyncService(private val context: Context) {
continue
}
val noteUrl = resource.href.toString()
// 🔧 Fix: Build full URL instead of using href directly
val noteUrl = notesUrl.trimEnd('/') + "/" + resource.name
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
val remoteNote = Note.fromJson(jsonContent) ?: continue
processedIds.add(remoteNote.id) // 🆕 Mark as processed
val localNote = storage.loadNote(remoteNote.id)
when {
@@ -699,6 +712,7 @@ class WebDavSyncService(private val context: Context) {
// New note from server
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
downloadedCount++
Logger.d(TAG, " ✅ Downloaded from /notes/: ${remoteNote.id}")
}
localNote.updatedAt < remoteNote.updatedAt -> {
// Remote is newer
@@ -710,14 +724,95 @@ class WebDavSyncService(private val context: Context) {
// Safe to overwrite
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
downloadedCount++
Logger.d(TAG, " ✅ Updated from /notes/: ${remoteNote.id}")
}
}
}
}
} catch (e: Exception) {
// Log error but don't fail entire sync
Logger.d(TAG, " 📊 Phase 1 complete: $downloadedCount notes from /notes/")
} else {
Logger.w(TAG, " ⚠️ /notes/ does not exist, skipping Phase 1")
}
// 🆕 PHASE 2: BACKWARD-COMPATIBILITY - Download from Root (old structure v1.2.0)
// ⚠️ ONLY for restore from server! Normal sync should NOT scan Root
if (includeRootFallback) {
val rootUrl = serverUrl.trimEnd('/')
Logger.d(TAG, "🔍 Phase 2: Checking ROOT at: $rootUrl (Restore mode)")
try {
val rootResources = sardine.list(rootUrl)
Logger.d(TAG, " 📂 Found ${rootResources.size} resources in ROOT")
val oldNotes = rootResources.filter { resource ->
!resource.isDirectory &&
resource.name.endsWith(".json") &&
!resource.path.contains("/notes/") && // Not from /notes/ subdirectory
!resource.path.contains("/notes-md/") // Not from /notes-md/
}
Logger.d(TAG, " 🔎 Filtered to ${oldNotes.size} .json files (excluding /notes/ and /notes-md/)")
if (oldNotes.isNotEmpty()) {
Logger.w(TAG, "⚠️ Found ${oldNotes.size} notes in ROOT (old v1.2.0 structure)")
for (resource in oldNotes) {
// 🔧 Fix: Build full URL instead of using href directly
val noteUrl = rootUrl.trimEnd('/') + "/" + resource.name
Logger.d(TAG, " 📄 Processing: ${resource.name} from ${resource.path}")
val jsonContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
val remoteNote = Note.fromJson(jsonContent) ?: continue
// Skip if already loaded from /notes/
if (processedIds.contains(remoteNote.id)) {
Logger.d(TAG, " ⏭️ Skipping ${remoteNote.id} (already loaded from /notes/)")
continue
}
processedIds.add(remoteNote.id)
val localNote = storage.loadNote(remoteNote.id)
when {
localNote == null -> {
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
downloadedCount++
Logger.d(TAG, " ✅ Downloaded from ROOT: ${remoteNote.id}")
}
localNote.updatedAt < remoteNote.updatedAt -> {
if (localNote.syncStatus == SyncStatus.PENDING) {
storage.saveNote(localNote.copy(syncStatus = SyncStatus.CONFLICT))
conflictCount++
} else {
storage.saveNote(remoteNote.copy(syncStatus = SyncStatus.SYNCED))
downloadedCount++
Logger.d(TAG, " ✅ Updated from ROOT: ${remoteNote.id}")
}
}
else -> {
// Local is newer - do nothing
Logger.d(TAG, " ⏭️ Local is newer: ${remoteNote.id}")
}
}
}
Logger.d(TAG, " 📊 Phase 2 complete: downloaded ${oldNotes.size} notes from ROOT")
} else {
Logger.d(TAG, " No old notes found in ROOT")
}
} catch (e: Exception) {
Logger.e(TAG, "⚠️ Failed to scan ROOT directory: ${e.message}", e)
Logger.e(TAG, " Stack trace: ${e.stackTraceToString()}")
// Not fatal - new users may not have root access
}
} else {
Logger.d(TAG, "⏭️ Skipping Phase 2 (Root scan) - only enabled for restore from server")
}
} catch (e: Exception) {
Logger.e(TAG, "❌ downloadRemoteNotes failed", e)
}
Logger.d(TAG, "📊 Total download result: $downloadedCount notes, $conflictCount conflicts")
return DownloadResult(downloadedCount, conflictCount)
}
@@ -757,38 +852,18 @@ class WebDavSyncService(private val context: Context) {
Logger.d(TAG, "🔄 Starting restore from server...")
val notesUrl = getNotesUrl(serverUrl)
// Clear local storage FIRST
Logger.d(TAG, "🗑️ Clearing local storage...")
storage.deleteAllNotes()
// List all files on server
val resources = sardine.list(notesUrl)
val jsonFiles = resources.filter {
!it.isDirectory && it.name.endsWith(".json")
}
// 🆕 v1.2.2: Use downloadRemoteNotes() with Root fallback enabled
val result = downloadRemoteNotes(
sardine = sardine,
serverUrl = serverUrl,
includeRootFallback = true // ✅ Enable backward compatibility for restore
)
Logger.d(TAG, "📂 Found ${jsonFiles.size} files on server")
val restoredNotes = mutableListOf<Note>()
// Download and parse each file
for (resource in jsonFiles) {
try {
val fileUrl = notesUrl.trimEnd('/') + "/" + resource.name
val content = sardine.get(fileUrl).bufferedReader().use { it.readText() }
val note = Note.fromJson(content)
if (note != null) {
restoredNotes.add(note)
Logger.d(TAG, "✅ Downloaded: ${note.title}")
} else {
Logger.e(TAG, "❌ Failed to parse ${resource.name}: Note.fromJson returned null")
}
} catch (e: Exception) {
Logger.e(TAG, "❌ Failed to download ${resource.name}", e)
// Continue with other files
}
}
if (restoredNotes.isEmpty()) {
if (result.downloadedCount == 0) {
return@withContext RestoreResult(
isSuccess = false,
errorMessage = "Keine Notizen auf Server gefunden",
@@ -796,22 +871,14 @@ class WebDavSyncService(private val context: Context) {
)
}
// Clear local storage
Logger.d(TAG, "🗑️ Clearing local storage...")
storage.deleteAllNotes()
saveLastSyncTimestamp()
// Save all restored notes
Logger.d(TAG, "💾 Saving ${restoredNotes.size} notes...")
restoredNotes.forEach { note ->
storage.saveNote(note.copy(syncStatus = SyncStatus.SYNCED))
}
Logger.d(TAG, "✅ Restore completed: ${restoredNotes.size} notes")
Logger.d(TAG, "✅ Restore completed: ${result.downloadedCount} notes")
RestoreResult(
isSuccess = true,
errorMessage = null,
restoredCount = restoredNotes.size
restoredCount = result.downloadedCount
)
} catch (e: Exception) {

View File

@@ -317,7 +317,7 @@ Step-by-step:
---
**📚 See also:**
- [QUICKSTART.en.md](QUICKSTART.en.md) - App installation and setup
- [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

View File

@@ -317,7 +317,7 @@ Schritt-für-Schritt:
---
**📚 Siehe auch:**
- [QUICKSTART.md](QUICKSTART.md) - App-Installation und Einrichtung
- [QUICKSTART.md](../QUICKSTART.md) - App-Installation und Einrichtung
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
- [DESKTOP.md](DESKTOP.md) - Desktop-Integration mit Markdown

View File

@@ -498,7 +498,7 @@ Planned for v1.3.0+:
---
**📚 See also:**
- [QUICKSTART.en.md](QUICKSTART.en.md) - App setup
- [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

View File

@@ -498,7 +498,7 @@ Geplant für v1.3.0+:
---
**📚 Siehe auch:**
- [QUICKSTART.md](QUICKSTART.md) - App-Einrichtung
- [QUICKSTART.md](../QUICKSTART.md) - App-Einrichtung
- [FEATURES.md](FEATURES.md) - Vollständige Feature-Liste
- [BACKUP.md](BACKUP.md) - Backup & Wiederherstellung

View File

@@ -181,9 +181,10 @@
- ✅ **OkHttp** - HTTP client (via Sardine)
### Build Variants
- ✅ **Standard** - Google Play version (with Google services prepared)
- ✅ **F-Droid** - FOSS version (no Google dependencies)
- ✅ **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
---
@@ -209,7 +210,11 @@
Planned for upcoming versions (see [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
### v1.3.0 - Advanced Organization
### 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

View File

@@ -181,9 +181,10 @@
- ✅ **OkHttp** - HTTP Client (via Sardine)
### Build-Varianten
- ✅ **Standard** - Google Play Version (mit Google-Services vorbereitet)
- ✅ **F-Droid** - FOSS Version (keine Google-Dependencies)
- ✅ **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
---
@@ -209,7 +210,11 @@
Geplant für kommende Versionen (siehe [TODO.md](project-docs/simple-notes-sync/planning/TODO.md)):
### v1.3.0 - Erweiterte Organisation
### 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

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.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