feat(v1.4.1): Bugfixes + Checklist auto line wrap
Fixed: - Delete notes from older app versions (v1.2.0 compatibility) - Checklist sync backwards compatibility (v1.3.x) - Fallback content in GitHub task list format - Recovery mode for lost checklistItems Improved: - Checklist auto line wrap (no maxLines limit) - Enter key creates new item (via TextWatcher) Metadata: - Changelogs for versionCode 12 - IzzyOnDroid metadata updated
This commit is contained in:
@@ -131,6 +131,9 @@ class MainActivity : AppCompatActivity() {
|
||||
setupRecyclerView()
|
||||
setupFab()
|
||||
|
||||
// v1.4.1: Migrate checklists for backwards compatibility
|
||||
migrateChecklistsForBackwardsCompat()
|
||||
|
||||
loadNotes()
|
||||
|
||||
// 🔄 v1.3.1: Observe sync state for UI updates
|
||||
@@ -730,6 +733,54 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.1: Migriert bestehende Checklisten für Abwärtskompatibilität.
|
||||
*
|
||||
* Problem: v1.4.0 Checklisten haben leeren "content", was auf älteren
|
||||
* App-Versionen (v1.3.x) als leere Notiz angezeigt wird.
|
||||
*
|
||||
* Lösung: Alle Checklisten ohne Fallback-Content als PENDING markieren,
|
||||
* damit sie beim nächsten Sync mit Fallback-Content hochgeladen werden.
|
||||
*
|
||||
* TODO: Diese Migration kann entfernt werden, sobald v1.4.0 nicht mehr
|
||||
* im Umlauf ist (ca. 6 Monate nach v1.4.1 Release, also ~Juli 2026).
|
||||
* Tracking: https://github.com/inventory69/simple-notes-sync/issues/XXX
|
||||
*/
|
||||
private fun migrateChecklistsForBackwardsCompat() {
|
||||
val migrationKey = "v1.4.1_checklist_migration_done"
|
||||
|
||||
// Nur einmal ausführen
|
||||
if (prefs.getBoolean(migrationKey, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
val allNotes = storage.loadAllNotes()
|
||||
val checklistsToMigrate = allNotes.filter { note ->
|
||||
note.noteType == NoteType.CHECKLIST &&
|
||||
note.content.isBlank() &&
|
||||
note.checklistItems?.isNotEmpty() == true
|
||||
}
|
||||
|
||||
if (checklistsToMigrate.isNotEmpty()) {
|
||||
Logger.d(TAG, "🔄 v1.4.1 Migration: Found ${checklistsToMigrate.size} checklists without fallback content")
|
||||
|
||||
for (note in checklistsToMigrate) {
|
||||
// Als PENDING markieren, damit beim nächsten Sync der Fallback-Content
|
||||
// generiert und hochgeladen wird
|
||||
val updatedNote = note.copy(
|
||||
syncStatus = dev.dettmer.simplenotes.models.SyncStatus.PENDING
|
||||
)
|
||||
storage.saveNote(updatedNote)
|
||||
Logger.d(TAG, " 📝 Marked for re-sync: ${note.title}")
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ v1.4.1 Migration: ${checklistsToMigrate.size} checklists marked for re-sync")
|
||||
}
|
||||
|
||||
// Migration als erledigt markieren
|
||||
prefs.edit().putBoolean(migrationKey, true).apply()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
|
||||
@@ -3,12 +3,10 @@ package dev.dettmer.simplenotes.adapters
|
||||
import android.graphics.Paint
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
@@ -55,33 +53,31 @@ class ChecklistEditorAdapter(
|
||||
editText.setText(item.text)
|
||||
updateStrikethrough(item.isChecked)
|
||||
|
||||
// TextWatcher für Änderungen
|
||||
// v1.4.1: TextWatcher für Änderungen + Enter-Erkennung für neues Item
|
||||
textWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val pos = bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
onItemTextChanged(pos, s?.toString() ?: "")
|
||||
if (pos == RecyclerView.NO_POSITION) return
|
||||
|
||||
val text = s?.toString() ?: ""
|
||||
|
||||
// Prüfe ob ein Newline eingegeben wurde
|
||||
if (text.contains("\n")) {
|
||||
// Newline entfernen und neues Item erstellen
|
||||
val cleanText = text.replace("\n", "")
|
||||
editText.setText(cleanText)
|
||||
editText.setSelection(cleanText.length)
|
||||
onItemTextChanged(pos, cleanText)
|
||||
onAddNewItem(pos + 1)
|
||||
} else {
|
||||
onItemTextChanged(pos, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
editText.addTextChangedListener(textWatcher)
|
||||
|
||||
// Enter-Taste = neues Item
|
||||
editText.setOnEditorActionListener { _, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_NEXT ||
|
||||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
|
||||
val pos = bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
onAddNewItem(pos + 1)
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Button
|
||||
deleteButton.setOnClickListener {
|
||||
val pos = bindingAdapterPosition
|
||||
|
||||
@@ -20,13 +20,42 @@ data class Note(
|
||||
val checklistItems: List<ChecklistItem>? = null
|
||||
) {
|
||||
/**
|
||||
* Serialisiert Note zu JSON (v1.4.0: Nutzt Gson für komplexe Strukturen)
|
||||
* Serialisiert Note zu JSON
|
||||
* v1.4.0: Nutzt Gson für komplexe Strukturen
|
||||
* v1.4.1: Für Checklisten wird ein Fallback-Content generiert, damit ältere
|
||||
* App-Versionen (v1.3.x) die Notiz als Text anzeigen können.
|
||||
*/
|
||||
fun toJson(): String {
|
||||
val gson = com.google.gson.GsonBuilder()
|
||||
.setPrettyPrinting()
|
||||
.create()
|
||||
return gson.toJson(this)
|
||||
|
||||
// v1.4.1: Für Checklisten den Fallback-Content generieren
|
||||
val noteToSerialize = if (noteType == NoteType.CHECKLIST && checklistItems != null) {
|
||||
this.copy(content = generateChecklistFallbackContent())
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
return gson.toJson(noteToSerialize)
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.1: Generiert einen lesbaren Text-Fallback aus Checklist-Items.
|
||||
* Format: GitHub-Style Task-Listen (kompatibel mit Markdown)
|
||||
*
|
||||
* Beispiel:
|
||||
* [ ] Milch kaufen
|
||||
* [x] Brot gekauft
|
||||
* [ ] Eier
|
||||
*
|
||||
* Wird von älteren App-Versionen (v1.3.x) als normaler Text angezeigt.
|
||||
*/
|
||||
private fun generateChecklistFallbackContent(): String {
|
||||
return checklistItems?.sortedBy { it.order }?.joinToString("\n") { item ->
|
||||
val checkbox = if (item.isChecked) "[x]" else "[ ]"
|
||||
"$checkbox ${item.text}"
|
||||
} ?: ""
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,7 +117,7 @@ type: ${noteType.name.lowercase()}
|
||||
|
||||
// Checklist-Items parsen (kann null sein)
|
||||
val checklistItemsType = object : com.google.gson.reflect.TypeToken<List<ChecklistItem>>() {}.type
|
||||
val checklistItems = if (jsonObject.has("checklistItems") &&
|
||||
var checklistItems: List<ChecklistItem>? = if (jsonObject.has("checklistItems") &&
|
||||
!jsonObject.get("checklistItems").isJsonNull
|
||||
) {
|
||||
gson.fromJson<List<ChecklistItem>>(
|
||||
@@ -99,6 +128,19 @@ type: ${noteType.name.lowercase()}
|
||||
null
|
||||
}
|
||||
|
||||
// v1.4.1: Recovery-Mode - Falls Checkliste aber keine Items,
|
||||
// versuche Content als Fallback zu parsen
|
||||
if (noteType == NoteType.CHECKLIST &&
|
||||
(checklistItems == null || checklistItems.isEmpty()) &&
|
||||
rawNote.content.isNotBlank()) {
|
||||
|
||||
val recoveredItems = parseChecklistFromContent(rawNote.content)
|
||||
if (recoveredItems.isNotEmpty()) {
|
||||
Logger.d(TAG, "🔄 Recovered ${recoveredItems.size} checklist items from content fallback")
|
||||
checklistItems = recoveredItems
|
||||
}
|
||||
}
|
||||
|
||||
// Note mit korrekten Werten erstellen
|
||||
Note(
|
||||
id = rawNote.id,
|
||||
@@ -130,6 +172,34 @@ type: ${noteType.name.lowercase()}
|
||||
val syncStatus: SyncStatus? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* v1.4.1: Parst GitHub-Style Checklisten aus Text (Recovery-Mode).
|
||||
*
|
||||
* Unterstützte Formate:
|
||||
* - [ ] Unchecked item
|
||||
* - [x] Checked item
|
||||
* - [X] Checked item (case insensitive)
|
||||
*
|
||||
* Wird verwendet, wenn eine v1.4.0 Checkliste von einer älteren
|
||||
* App-Version (v1.3.x) bearbeitet wurde und die checklistItems verloren gingen.
|
||||
*
|
||||
* @param content Der Text-Content der Notiz
|
||||
* @return Liste von ChecklistItems oder leere Liste
|
||||
*/
|
||||
private fun parseChecklistFromContent(content: String): List<ChecklistItem> {
|
||||
val pattern = Regex("""^\s*\[([ xX])\]\s*(.+)$""", RegexOption.MULTILINE)
|
||||
return pattern.findAll(content).mapIndexed { index, match ->
|
||||
val checked = match.groupValues[1].lowercase() == "x"
|
||||
val text = match.groupValues[2].trim()
|
||||
ChecklistItem(
|
||||
id = UUID.randomUUID().toString(),
|
||||
text = text,
|
||||
isChecked = checked,
|
||||
order = index
|
||||
)
|
||||
}.toList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
|
||||
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
||||
|
||||
@@ -1764,6 +1764,9 @@ class WebDavSyncService(private val context: Context) {
|
||||
* Deletes a note from the server (JSON + Markdown)
|
||||
* Does NOT delete from local storage!
|
||||
*
|
||||
* v1.4.1: Now supports v1.2.0 compatibility mode - also checks ROOT folder
|
||||
* for notes that were created before the /notes/ directory structure.
|
||||
*
|
||||
* @param noteId The ID of the note to delete
|
||||
* @return true if at least one file was deleted, false otherwise
|
||||
*/
|
||||
@@ -1775,12 +1778,21 @@ class WebDavSyncService(private val context: Context) {
|
||||
var deletedJson = false
|
||||
var deletedMd = false
|
||||
|
||||
// Delete JSON
|
||||
// v1.4.1: Try to delete JSON from /notes/ first (standard path)
|
||||
val jsonUrl = getNotesUrl(serverUrl) + "$noteId.json"
|
||||
if (sardine.exists(jsonUrl)) {
|
||||
sardine.delete(jsonUrl)
|
||||
deletedJson = true
|
||||
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json")
|
||||
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json (from /notes/)")
|
||||
} else {
|
||||
// v1.4.1: Fallback - check ROOT folder for v1.2.0 compatibility
|
||||
val rootJsonUrl = serverUrl.trimEnd('/') + "/$noteId.json"
|
||||
Logger.d(TAG, "🔍 JSON not in /notes/, checking ROOT: $rootJsonUrl")
|
||||
if (sardine.exists(rootJsonUrl)) {
|
||||
sardine.delete(rootJsonUrl)
|
||||
deletedJson = true
|
||||
Logger.d(TAG, "🗑️ Deleted from server: $noteId.json (from ROOT - v1.2.0 compat)")
|
||||
}
|
||||
}
|
||||
|
||||
// Delete Markdown (v1.3.0: YAML-scan based approach)
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
android:minHeight="0dp" />
|
||||
|
||||
<!-- Text Input (ohne Box, nur transparent) -->
|
||||
<!-- v1.4.1: Auto-Zeilenumbruch für lange Texte -->
|
||||
<EditText
|
||||
android:id="@+id/etItemText"
|
||||
android:layout_width="0dp"
|
||||
@@ -38,9 +39,7 @@
|
||||
android:layout_weight="1"
|
||||
android:background="@null"
|
||||
android:hint="@string/item_placeholder"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionNext"
|
||||
android:maxLines="3"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
tools:text="Milch kaufen" />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user