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:
inventory69
2026-01-11 21:53:49 +01:00
parent 9b37078cce
commit 356ccde627
11 changed files with 211 additions and 31 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ Thumbs.db
*.tmp *.tmp
*.swp *.swp
*~ *~
test-apks/

View File

@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
--- ---
## [1.4.1] - 2026-01-11
### Fixed
- **🗑️ Löschen älterer Notizen (v1.2.0 Kompatibilität)**
- Notizen aus App-Version v1.2.0 oder früher werden jetzt korrekt vom Server gelöscht
- Behebt Problem bei Multi-Device-Nutzung mit älteren Notizen
- **🔄 Checklisten-Sync Abwärtskompatibilität**
- Checklisten werden jetzt auch als Text-Fallback im `content`-Feld gespeichert
- Ältere App-Versionen (v1.3.x) zeigen Checklisten als lesbaren Text
- Format: GitHub-Style Task-Listen (`[ ] Item` / `[x] Item`)
- Recovery-Mode: Falls Checklisten-Items verloren gehen, werden sie aus dem Content wiederhergestellt
### Improved
- **📝 Checklisten Auto-Zeilenumbruch**
- Lange Checklisten-Texte werden jetzt automatisch umgebrochen
- Keine Begrenzung auf 3 Zeilen mehr
- Enter-Taste erstellt weiterhin ein neues Item
### Looking Ahead
> 🚀 **v1.5.0** wird das nächste größere Release. Wir sammeln Ideen und Feedback!
> Feature-Requests gerne als [GitHub Issue](https://github.com/inventory69/simple-notes-sync/issues) einreichen.
---
## [1.4.0] - 2026-01-10 ## [1.4.0] - 2026-01-10
### 🎉 New Feature: Checklists ### 🎉 New Feature: Checklists

View File

@@ -20,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes" applicationId = "dev.dettmer.simplenotes"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 36
versionCode = 11 // 🚀 v1.4.0: Checklists Feature versionCode = 12 // 🔧 v1.4.1: Bugfixes (Root-Delete, Checklist Compat)
versionName = "1.4.0" // 🚀 v1.4.0: Checklists, Multi-Device Sync Fixes, UX Improvements versionName = "1.4.1" // 🔧 v1.4.1: Root-Folder Delete Fix, Checklisten-Sync Abwärtskompatibilität
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -131,6 +131,9 @@ class MainActivity : AppCompatActivity() {
setupRecyclerView() setupRecyclerView()
setupFab() setupFab()
// v1.4.1: Migrate checklists for backwards compatibility
migrateChecklistsForBackwardsCompat()
loadNotes() loadNotes()
// 🔄 v1.3.1: Observe sync state for UI updates // 🔄 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( override fun onRequestPermissionsResult(
requestCode: Int, requestCode: Int,
permissions: Array<out String>, permissions: Array<out String>,

View File

@@ -3,12 +3,10 @@ package dev.dettmer.simplenotes.adapters
import android.graphics.Paint import android.graphics.Paint
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText import android.widget.EditText
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
@@ -55,33 +53,31 @@ class ChecklistEditorAdapter(
editText.setText(item.text) editText.setText(item.text)
updateStrikethrough(item.isChecked) updateStrikethrough(item.isChecked)
// TextWatcher für Änderungen // v1.4.1: TextWatcher für Änderungen + Enter-Erkennung für neues Item
textWatcher = object : TextWatcher { textWatcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
val pos = bindingAdapterPosition val pos = bindingAdapterPosition
if (pos != RecyclerView.NO_POSITION) { if (pos == RecyclerView.NO_POSITION) return
onItemTextChanged(pos, s?.toString() ?: "")
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) 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 // Delete Button
deleteButton.setOnClickListener { deleteButton.setOnClickListener {
val pos = bindingAdapterPosition val pos = bindingAdapterPosition

View File

@@ -20,13 +20,42 @@ data class Note(
val checklistItems: List<ChecklistItem>? = null 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 { fun toJson(): String {
val gson = com.google.gson.GsonBuilder() val gson = com.google.gson.GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
.create() .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) // Checklist-Items parsen (kann null sein)
val checklistItemsType = object : com.google.gson.reflect.TypeToken<List<ChecklistItem>>() {}.type 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 !jsonObject.get("checklistItems").isJsonNull
) { ) {
gson.fromJson<List<ChecklistItem>>( gson.fromJson<List<ChecklistItem>>(
@@ -99,6 +128,19 @@ type: ${noteType.name.lowercase()}
null 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 mit korrekten Werten erstellen
Note( Note(
id = rawNote.id, id = rawNote.id,
@@ -130,6 +172,34 @@ type: ${noteType.name.lowercase()}
val syncStatus: SyncStatus? = null 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) * Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
* v1.4.0: Unterstützt jetzt auch Checklisten-Format * v1.4.0: Unterstützt jetzt auch Checklisten-Format

View File

@@ -1764,6 +1764,9 @@ class WebDavSyncService(private val context: Context) {
* Deletes a note from the server (JSON + Markdown) * Deletes a note from the server (JSON + Markdown)
* Does NOT delete from local storage! * 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 * @param noteId The ID of the note to delete
* @return true if at least one file was deleted, false otherwise * @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 deletedJson = false
var deletedMd = false var deletedMd = false
// Delete JSON // v1.4.1: Try to delete JSON from /notes/ first (standard path)
val jsonUrl = getNotesUrl(serverUrl) + "$noteId.json" val jsonUrl = getNotesUrl(serverUrl) + "$noteId.json"
if (sardine.exists(jsonUrl)) { if (sardine.exists(jsonUrl)) {
sardine.delete(jsonUrl) sardine.delete(jsonUrl)
deletedJson = true 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) // Delete Markdown (v1.3.0: YAML-scan based approach)

View File

@@ -31,6 +31,7 @@
android:minHeight="0dp" /> android:minHeight="0dp" />
<!-- Text Input (ohne Box, nur transparent) --> <!-- Text Input (ohne Box, nur transparent) -->
<!-- v1.4.1: Auto-Zeilenumbruch für lange Texte -->
<EditText <EditText
android:id="@+id/etItemText" android:id="@+id/etItemText"
android:layout_width="0dp" android:layout_width="0dp"
@@ -38,9 +39,7 @@
android:layout_weight="1" android:layout_weight="1"
android:background="@null" android:background="@null"
android:hint="@string/item_placeholder" android:hint="@string/item_placeholder"
android:inputType="text" android:inputType="textMultiLine|textCapSentences"
android:imeOptions="actionNext"
android:maxLines="3"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
tools:text="Milch kaufen" /> tools:text="Milch kaufen" />

View File

@@ -0,0 +1,4 @@
• Bugfix: Löschen von Notizen aus älteren App-Versionen (v1.2.0)
• Bugfix: Checklisten-Sync mit älteren App-Versionen (v1.3.x)
• Checklisten werden jetzt auch als Text-Fallback gespeichert
• Neu: Checklisten-Texte werden automatisch umgebrochen

View File

@@ -0,0 +1,5 @@
• Bugfix: Deleting notes from older app versions (v1.2.0)
• Bugfix: Checklist sync with older app versions (v1.3.x)
• Checklists are now also saved as text fallback
• New: Checklist texts now wrap automatically
p automatically

View File

@@ -147,7 +147,21 @@ Builds:
scandelete: scandelete:
- android/gradle/wrapper - android/gradle/wrapper
- versionName: 1.4.1
versionCode: 12
commit: v1.4.1
subdir: android/app
sudo:
- apt-get update
- apt-get install -y openjdk-17-jdk-headless
- update-java-alternatives -a
gradle:
- fdroid
prebuild: sed -i -e '/signingConfig/d' build.gradle.kts
scandelete:
- android/gradle/wrapper
AutoUpdateMode: Version AutoUpdateMode: Version
UpdateCheckMode: Tags UpdateCheckMode: Tags
CurrentVersion: 1.4.0 CurrentVersion: 1.4.1
CurrentVersionCode: 11 CurrentVersionCode: 12