🐛 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
This commit is contained in:
@@ -17,8 +17,8 @@ android {
|
||||
applicationId = "dev.dettmer.simplenotes"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 5 // 🔥 v1.2.0: Local Backup + Markdown Desktop Integration
|
||||
versionName = "1.2.0" // 🔥 v1.2.0: Backup/Restore + Joplin/Obsidian Support
|
||||
versionCode = 6 // 🐛 v1.2.1: Markdown Initial Export Bugfix
|
||||
versionName = "1.2.1" // 🐛 v1.2.1: Markdown Initial Export Bugfix
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.dettmer.simplenotes
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -548,11 +549,83 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun onMarkdownExportToggled(enabled: Boolean) {
|
||||
prefs.edit().putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled).apply()
|
||||
|
||||
if (enabled) {
|
||||
showToast("Markdown-Export aktiviert - Notizen werden als .md-Dateien exportiert")
|
||||
// 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-Export")
|
||||
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")
|
||||
switchMarkdownExport.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 Einstellung
|
||||
prefs.edit().putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled).apply()
|
||||
|
||||
// Erfolgs-Nachricht
|
||||
showToast("✅ $exportedCount Notizen nach Markdown exportiert")
|
||||
|
||||
} catch (e: Exception) {
|
||||
progressDialog.dismiss()
|
||||
showToast("❌ Export fehlgeschlagen: ${e.message}")
|
||||
|
||||
// Deaktiviere Toggle bei Fehler
|
||||
switchMarkdownExport.isChecked = false
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
// Keine Notizen vorhanden - speichere Einstellung direkt
|
||||
prefs.edit().putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled).apply()
|
||||
showToast("Markdown-Export aktiviert - Notizen werden als .md-Dateien exportiert")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Error toggling markdown export: ${e.message}")
|
||||
showToast("Fehler: ${e.message}")
|
||||
switchMarkdownExport.isChecked = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Deaktivieren - nur Setting speichern
|
||||
prefs.edit().putBoolean(Constants.KEY_MARKDOWN_EXPORT, enabled).apply()
|
||||
showToast("Markdown-Export deaktiviert - nur JSON-Sync aktiv")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
private val storage: NotesStorage
|
||||
private val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private var markdownDirEnsured = false // Cache für Ordner-Existenz
|
||||
|
||||
init {
|
||||
if (BuildConfig.DEBUG) {
|
||||
@@ -189,6 +190,72 @@ class WebDavSyncService(private val context: Context) {
|
||||
return prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt notes/ URL aus Base-URL mit Smart Detection (Task #1.2.1-12)
|
||||
*
|
||||
* Beispiele:
|
||||
* - http://server:8080/ → http://server:8080/notes/
|
||||
* - http://server:8080/notes/ → http://server:8080/notes/
|
||||
* - http://server:8080/notes → http://server:8080/notes/
|
||||
* - http://server:8080/my-path/ → http://server:8080/my-path/notes/
|
||||
*
|
||||
* @param baseUrl Base Server-URL
|
||||
* @return notes/ Ordner-URL (mit trailing /)
|
||||
*/
|
||||
private fun getNotesUrl(baseUrl: String): String {
|
||||
val normalized = baseUrl.trimEnd('/')
|
||||
|
||||
// Wenn URL bereits mit /notes endet → direkt nutzen
|
||||
return if (normalized.endsWith("/notes")) {
|
||||
"$normalized/"
|
||||
} else {
|
||||
"$normalized/notes/"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erzeugt Markdown-Ordner-URL basierend auf getNotesUrl() (Task #1.2.1-14)
|
||||
*
|
||||
* Beispiele:
|
||||
* - http://server:8080/ → http://server:8080/notes-md/
|
||||
* - http://server:8080/notes/ → http://server:8080/notes-md/
|
||||
* - http://server:8080/notes → http://server:8080/notes-md/
|
||||
*
|
||||
* @param baseUrl Base Server-URL
|
||||
* @return Markdown-Ordner-URL (mit trailing /)
|
||||
*/
|
||||
private fun getMarkdownUrl(baseUrl: String): String {
|
||||
val notesUrl = getNotesUrl(baseUrl)
|
||||
val normalized = notesUrl.trimEnd('/')
|
||||
|
||||
// Ersetze /notes mit /notes-md
|
||||
return normalized.replace("/notes", "/notes-md") + "/"
|
||||
}
|
||||
|
||||
/**
|
||||
* Stellt sicher dass notes-md/ Ordner existiert
|
||||
*
|
||||
* Wird beim ersten erfolgreichen Sync aufgerufen (unabhängig von MD-Feature).
|
||||
* Cached in Memory - nur einmal pro App-Session.
|
||||
*/
|
||||
private fun ensureMarkdownDirectoryExists(sardine: Sardine, serverUrl: String) {
|
||||
if (markdownDirEnsured) return
|
||||
|
||||
try {
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
if (!sardine.exists(mdUrl)) {
|
||||
sardine.createDirectory(mdUrl)
|
||||
Logger.d(TAG, "📁 Created notes-md/ directory (for future use)")
|
||||
}
|
||||
|
||||
markdownDirEnsured = true
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to create notes-md/: ${e.message}")
|
||||
// Nicht kritisch - User kann später manuell erstellen
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob lokale Änderungen seit letztem Sync vorhanden sind (v1.1.2)
|
||||
* Performance-Optimierung: Vermeidet unnötige Sync-Operationen
|
||||
@@ -350,20 +417,24 @@ class WebDavSyncService(private val context: Context) {
|
||||
var conflictCount = 0
|
||||
|
||||
Logger.d(TAG, "📍 Step 3: Checking server directory")
|
||||
// Ensure server directory exists
|
||||
// Ensure notes/ directory exists
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
try {
|
||||
Logger.d(TAG, "🔍 Checking if server directory exists...")
|
||||
if (!sardine.exists(serverUrl)) {
|
||||
Logger.d(TAG, "📁 Creating server directory...")
|
||||
sardine.createDirectory(serverUrl)
|
||||
Logger.d(TAG, "🔍 Checking if notes/ directory exists...")
|
||||
if (!sardine.exists(notesUrl)) {
|
||||
Logger.d(TAG, "📁 Creating notes/ directory...")
|
||||
sardine.createDirectory(notesUrl)
|
||||
}
|
||||
Logger.d(TAG, " ✅ Server directory ready")
|
||||
Logger.d(TAG, " ✅ notes/ directory ready")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH checking/creating server directory!", e)
|
||||
Logger.e(TAG, "💥 CRASH checking/creating notes/ directory!", e)
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
|
||||
// Ensure notes-md/ directory exists (for Markdown export)
|
||||
ensureMarkdownDirectoryExists(sardine, serverUrl)
|
||||
|
||||
Logger.d(TAG, "📍 Step 4: Uploading local notes")
|
||||
// Upload local notes
|
||||
try {
|
||||
@@ -448,9 +519,10 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
for (note in localNotes) {
|
||||
try {
|
||||
// 1. JSON-Upload (bestehend, unverändert)
|
||||
// 1. JSON-Upload (Task #1.2.1-13: nutzt getNotesUrl())
|
||||
if (note.syncStatus == SyncStatus.LOCAL_ONLY || note.syncStatus == SyncStatus.PENDING) {
|
||||
val noteUrl = "$serverUrl/${note.id}.json"
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
val noteUrl = "$notesUrl${note.id}.json"
|
||||
val jsonBytes = note.toJson().toByteArray()
|
||||
|
||||
sardine.put(noteUrl, jsonBytes, "application/json")
|
||||
@@ -490,7 +562,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
* @param note Note zum Exportieren
|
||||
*/
|
||||
private fun exportToMarkdown(sardine: Sardine, serverUrl: String, note: Note) {
|
||||
val mdUrl = serverUrl.replace("/notes", "/notes-md")
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
// Erstelle notes-md/ Ordner falls nicht vorhanden
|
||||
if (!sardine.exists(mdUrl)) {
|
||||
@@ -525,6 +597,79 @@ class WebDavSyncService(private val context: Context) {
|
||||
.trim('_', ' ') // Trim Underscores/Spaces
|
||||
}
|
||||
|
||||
/**
|
||||
* Exportiert ALLE lokalen Notizen als Markdown (Initial-Export)
|
||||
*
|
||||
* Wird beim ersten Aktivieren der Desktop-Integration aufgerufen.
|
||||
* Exportiert auch bereits synchronisierte Notizen.
|
||||
*
|
||||
* @return Anzahl exportierter Notizen
|
||||
*/
|
||||
suspend fun exportAllNotesToMarkdown(
|
||||
serverUrl: String,
|
||||
username: String,
|
||||
password: String,
|
||||
onProgress: (current: Int, total: Int) -> Unit = { _, _ -> }
|
||||
): Int = withContext(Dispatchers.IO) {
|
||||
Logger.d(TAG, "🔄 Starting initial Markdown export for all notes...")
|
||||
|
||||
// Erstelle Sardine-Client mit gegebenen Credentials
|
||||
val wifiAddress = getWiFiInetAddress()
|
||||
|
||||
val okHttpClient = if (wifiAddress != null) {
|
||||
Logger.d(TAG, "✅ Using WiFi-bound socket factory")
|
||||
OkHttpClient.Builder()
|
||||
.socketFactory(WiFiSocketFactory(wifiAddress))
|
||||
.build()
|
||||
} else {
|
||||
Logger.d(TAG, "⚠️ Using default OkHttpClient (no WiFi binding)")
|
||||
OkHttpClient.Builder().build()
|
||||
}
|
||||
|
||||
val sardine = OkHttpSardine(okHttpClient).apply {
|
||||
setCredentials(username, password)
|
||||
}
|
||||
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
// Ordner sollte bereits existieren (durch #1.2.1-00), aber Sicherheitscheck
|
||||
ensureMarkdownDirectoryExists(sardine, serverUrl)
|
||||
|
||||
// Hole ALLE lokalen Notizen (inklusive SYNCED)
|
||||
val allNotes = storage.loadAllNotes()
|
||||
val totalCount = allNotes.size
|
||||
var exportedCount = 0
|
||||
|
||||
Logger.d(TAG, "📝 Found $totalCount notes to export")
|
||||
|
||||
allNotes.forEachIndexed { index, note ->
|
||||
try {
|
||||
// Progress-Callback
|
||||
onProgress(index + 1, totalCount)
|
||||
|
||||
// Sanitize Filename
|
||||
val filename = sanitizeFilename(note.title) + ".md"
|
||||
val noteUrl = "$mdUrl/$filename"
|
||||
|
||||
// Konvertiere zu Markdown
|
||||
val mdContent = note.toMarkdown().toByteArray()
|
||||
|
||||
// Upload (überschreibt falls vorhanden)
|
||||
sardine.put(noteUrl, mdContent, "text/markdown")
|
||||
|
||||
exportedCount++
|
||||
Logger.d(TAG, " ✅ Exported [${index + 1}/$totalCount]: ${note.title}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Failed to export ${note.title}: ${e.message}")
|
||||
// Continue mit nächster Note (keine Abbruch bei Einzelfehlern)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Initial export completed: $exportedCount/$totalCount notes")
|
||||
return@withContext exportedCount
|
||||
}
|
||||
|
||||
private data class DownloadResult(
|
||||
val downloadedCount: Int,
|
||||
val conflictCount: Int
|
||||
@@ -535,7 +680,8 @@ class WebDavSyncService(private val context: Context) {
|
||||
var conflictCount = 0
|
||||
|
||||
try {
|
||||
val resources = sardine.list(serverUrl)
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
val resources = sardine.list(notesUrl)
|
||||
|
||||
for (resource in resources) {
|
||||
if (resource.isDirectory || !resource.name.endsWith(".json")) {
|
||||
@@ -611,8 +757,10 @@ class WebDavSyncService(private val context: Context) {
|
||||
|
||||
Logger.d(TAG, "🔄 Starting restore from server...")
|
||||
|
||||
val notesUrl = getNotesUrl(serverUrl)
|
||||
|
||||
// List all files on server
|
||||
val resources = sardine.list(serverUrl)
|
||||
val resources = sardine.list(notesUrl)
|
||||
val jsonFiles = resources.filter {
|
||||
!it.isDirectory && it.name.endsWith(".json")
|
||||
}
|
||||
@@ -624,7 +772,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
// Download and parse each file
|
||||
for (resource in jsonFiles) {
|
||||
try {
|
||||
val fileUrl = serverUrl.trimEnd('/') + "/" + resource.name
|
||||
val fileUrl = notesUrl.trimEnd('/') + "/" + resource.name
|
||||
val content = sardine.get(fileUrl).bufferedReader().use { it.readText() }
|
||||
|
||||
val note = Note.fromJson(content)
|
||||
@@ -697,7 +845,7 @@ class WebDavSyncService(private val context: Context) {
|
||||
val sardine = OkHttpSardine()
|
||||
sardine.setCredentials(username, password)
|
||||
|
||||
val mdUrl = serverUrl.replace("/notes", "/notes-md")
|
||||
val mdUrl = getMarkdownUrl(serverUrl)
|
||||
|
||||
// Check if notes-md/ exists
|
||||
if (!sardine.exists(mdUrl)) {
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:startIconDrawable="@android:drawable/ic_menu_compass"
|
||||
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:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
|
||||
Reference in New Issue
Block a user