3 Commits

Author SHA1 Message Date
inventory69
7128c25bd5 Merge feature/v1.4.1-bugfixes: Bugfixes + Checklist improvements 2026-01-11 21:59:24 +01:00
inventory69
356ccde627 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
2026-01-11 21:59:09 +01:00
inventory69
9b37078cce Change repository URL in CONTRIBUTING.md
Updated repository URL in Quick Start section.
2026-01-11 16:23:51 +01:00
12 changed files with 213 additions and 33 deletions

1
.gitignore vendored
View File

@@ -42,3 +42,4 @@ Thumbs.db
*.tmp
*.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
### 🎉 New Feature: Checklists

View File

@@ -14,7 +14,7 @@ Danke, dass du zu Simple Notes Sync beitragen möchtest!
1. **Fork & Clone**
```bash
git clone https://github.com/DEIN-USERNAME/simple-notes-sync.git
git clone https://github.com/inventory69/simple-notes-sync.git
cd simple-notes-sync
```
@@ -139,7 +139,7 @@ Thanks for wanting to contribute to Simple Notes Sync!
1. **Fork & Clone**
```bash
git clone https://github.com/YOUR-USERNAME/simple-notes-sync.git
git clone https://github.com/inventory69/simple-notes-sync.git
cd simple-notes-sync
```

View File

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

View File

@@ -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>,

View File

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

View File

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

View File

@@ -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)

View File

@@ -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" />

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:
- 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
UpdateCheckMode: Tags
CurrentVersion: 1.4.0
CurrentVersionCode: 11
CurrentVersion: 1.4.1
CurrentVersionCode: 12