diff --git a/CHANGELOG.md b/CHANGELOG.md
index d84fefd..26d69e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
---
+## [1.4.0] - 2026-01-10
+
+### 🎉 New Feature: Checklists
+
+- **✅ Checklist Notes**
+ - New note type: Checklists with tap-to-toggle items
+ - Add items via dedicated input field with "+" button
+ - Drag & drop reordering (long-press to activate)
+ - Swipe-to-delete items
+ - Visual distinction: Checked items get strikethrough styling
+ - Type selector when creating new notes (Text or Checklist)
+
+- **📝 Markdown Integration**
+ - Checklists export as GitHub-style task lists (`- [ ]` / `- [x]`)
+ - Compatible with Obsidian, Notion, and other Markdown editors
+ - Full round-trip: Edit in Obsidian → Sync back to app
+ - YAML frontmatter includes `type: checklist` for identification
+
+### Fixed
+
+- **� Markdown Parsing Robustness**
+ - Fixed content extraction after title (was returning empty for some formats)
+ - Now handles single newline after title (was requiring double newline)
+ - Protection: Skips import if parsed content is empty but local has content
+
+- **📂 Duplicate Filename Handling**
+ - Notes with identical titles now get unique Markdown filenames
+ - Format: `title_shortid.md` (e.g., `test_71540ca9.md`)
+ - Prevents data loss from filename collisions
+
+- **🔔 Notification UX**
+ - No sync notifications when app is in foreground
+ - User sees changes directly in UI - no redundant notification
+ - Background syncs still show notifications as expected
+
+### Privacy Improvements
+
+- **🔒 WiFi Permissions Removed**
+ - Removed `ACCESS_WIFI_STATE` permission
+ - Removed `CHANGE_WIFI_STATE` permission
+ - WiFi binding now works via IP detection instead of SSID matching
+ - Cleaned up all SSID-related code from codebase and documentation
+
+### Technical Improvements
+
+- **📦 New Data Model**
+ - `NoteType` enum: `TEXT`, `CHECKLIST`
+ - `ChecklistItem` data class with id, text, isChecked, order
+ - `Note.kt` extended with `noteType` and `checklistItems` fields
+
+- **🔄 Sync Protocol v1.4.0**
+ - JSON format updated to include checklist fields
+ - Full backward compatibility with v1.3.x notes
+ - Robust JSON parsing with manual field extraction
+
+---
+
## [1.3.2] - 2026-01-10
### Changed
diff --git a/QUICKSTART.en.md b/QUICKSTART.en.md
index b42ff3a..43128bd 100644
--- a/QUICKSTART.en.md
+++ b/QUICKSTART.en.md
@@ -74,7 +74,6 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
| **WebDAV Server URL** | `http://YOUR-SERVER-IP:8080/` |
| **Username** | `noteuser` |
| **Password** | (your password from `.env`) |
- | **Gateway SSID** | Name of your WiFi network |
> **💡 Note:** Enter only the base URL (without `/notes`). The app automatically creates `/notes/` for JSON files and `/notes-md/` for Markdown export.
@@ -158,9 +157,8 @@ For reliable auto-sync:
# Should show "Up"
```
-2. **Same WiFi?**
+2. **Same network?**
- Smartphone and server must be on same network
- - Check SSID in app settings
3. **IP address correct?**
```bash
@@ -193,9 +191,9 @@ For reliable auto-sync:
2. **Battery optimization disabled?**
- See [Disable Battery Optimization](#-disable-battery-optimization)
-3. **On correct WiFi?**
- - Sync only works when SSID = Gateway SSID
- - Check current SSID in Android settings
+3. **Connected to WiFi?**
+ - Auto-sync triggers on any WiFi connection
+ - Check if you're connected to a WiFi network
4. **Test manually:**
- ⚙️ Settings → "Sync now"
diff --git a/QUICKSTART.md b/QUICKSTART.md
index 3626167..ead0005 100644
--- a/QUICKSTART.md
+++ b/QUICKSTART.md
@@ -74,7 +74,6 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
| **WebDAV Server URL** | `http://DEINE-SERVER-IP:8080/` |
| **Benutzername** | `noteuser` |
| **Passwort** | (dein Passwort aus `.env`) |
- | **Gateway SSID** | Name deines WLAN-Netzwerks |
> **💡 Hinweis:** Gib nur die Base-URL ein (ohne `/notes`). Die App erstellt automatisch `/notes/` für JSON-Dateien und `/notes-md/` für Markdown-Export.
@@ -158,9 +157,8 @@ Für zuverlässigen Auto-Sync:
# Sollte "Up" zeigen
```
-2. **Gleiche WLAN?**
+2. **Gleiches Netzwerk?**
- Smartphone und Server müssen im selben Netzwerk sein
- - Prüfe SSID in App-Einstellungen
3. **IP-Adresse korrekt?**
```bash
@@ -193,9 +191,9 @@ Für zuverlässigen Auto-Sync:
2. **Akku-Optimierung deaktiviert?**
- Siehe [Akku-Optimierung](#-akku-optimierung-deaktivieren)
-3. **Im richtigen WLAN?**
- - Sync funktioniert nur wenn SSID = Gateway SSID
- - Prüfe aktuelle SSID in Android-Einstellungen
+3. **Mit WiFi verbunden?**
+ - Auto-Sync triggert bei jeder WiFi-Verbindung
+ - Prüfe, ob du mit einem WLAN verbunden bist
4. **Manuell testen:**
- ⚙️ Einstellungen → "Jetzt synchronisieren"
diff --git a/README.en.md b/README.en.md
index 013d86e..66fdd7d 100644
--- a/README.en.md
+++ b/README.en.md
@@ -26,11 +26,12 @@
## ✨ Highlights
+- ✅ **NEW: Checklists** - Tap-to-check, drag & drop, swipe-to-delete
- 📝 **Offline-first** - Works without internet
-- 🔄 **Auto-sync** - Home WiFi only (15/30/60 min)
+- 🔄 **Auto-sync** - On WiFi connection (15/30/60 min)
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
- 💾 **Local backup** - Export/Import as JSON file
-- 🖥️ **Desktop integration** - Markdown export for VS Code, Typora, etc.
+- 🖥️ **Desktop integration** - Markdown export for Obsidian, VS Code, Typora
- 🔋 **Battery-friendly** - ~0.2-0.8% per day
- 🎨 **Material Design 3** - Dark mode & dynamic colors
@@ -101,4 +102,4 @@ MIT License - see [LICENSE](LICENSE)
---
-**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3
+**v1.4.0** · Built with ❤️ using Kotlin + Material Design 3
diff --git a/README.md b/README.md
index 30c5168..b0ddc13 100644
--- a/README.md
+++ b/README.md
@@ -26,11 +26,12 @@
## ✨ Highlights
+- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop, Swipe-to-Delete
- 📝 **Offline-First** - Funktioniert ohne Internet
-- 🔄 **Auto-Sync** - Nur im Heim-WLAN (15/30/60 Min)
+- 🔄 **Auto-Sync** - Bei WiFi-Verbindung (15/30/60 Min)
- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
- 💾 **Lokales Backup** - Export/Import als JSON-Datei
-- 🖥️ **Desktop-Integration** - Markdown-Export für VS Code, Typora, etc.
+- 🖥️ **Desktop-Integration** - Markdown-Export für Obsidian, VS Code, Typora
- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
@@ -104,4 +105,4 @@ MIT License - siehe [LICENSE](LICENSE)
---
-**v1.3.2** · Built with ❤️ using Kotlin + Material Design 3
+**v1.4.0** · Built with ❤️ using Kotlin + Material Design 3
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index f33fbfc..95a044f 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -20,8 +20,8 @@ android {
applicationId = "dev.dettmer.simplenotes"
minSdk = 24
targetSdk = 36
- versionCode = 10 // 🚀 v1.3.2: Lint-Cleanup "Clean Slate"
- versionName = "1.3.2" // 🚀 v1.3.2: Code-Qualität-Release (alle einfachen Lint-Issues behoben)
+ versionCode = 11 // 🚀 v1.4.0: Checklists Feature
+ versionName = "1.4.0" // 🚀 v1.4.0: Checklists, Multi-Device Sync Fixes, UX Improvements
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fe254e1..be931a7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,8 +5,6 @@
-
-
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
index 16d249a..f7e2272 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt
@@ -44,6 +44,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import android.view.View
import android.widget.LinearLayout
+import android.view.Gravity
+import android.widget.PopupMenu
+import dev.dettmer.simplenotes.models.NoteType
class MainActivity : AppCompatActivity() {
@@ -551,12 +554,55 @@ class MainActivity : AppCompatActivity() {
}).show()
}
+ /**
+ * v1.4.0: Setup FAB mit Dropdown für Notiz-Typ Auswahl
+ */
private fun setupFab() {
- fabAddNote.setOnClickListener {
- openNoteEditor(null)
+ fabAddNote.setOnClickListener { view ->
+ showNoteTypePopup(view)
}
}
+ /**
+ * v1.4.0: Zeigt Popup-Menü zur Auswahl des Notiz-Typs
+ */
+ private fun showNoteTypePopup(anchor: View) {
+ val popupMenu = PopupMenu(this, anchor, Gravity.END)
+ popupMenu.inflate(R.menu.menu_fab_note_types)
+
+ // Icons im Popup anzeigen (via Reflection, da standardmäßig ausgeblendet)
+ try {
+ val fields = popupMenu.javaClass.declaredFields
+ for (field in fields) {
+ if ("mPopup" == field.name) {
+ field.isAccessible = true
+ val menuPopupHelper = field.get(popupMenu)
+ val classPopupHelper = Class.forName(menuPopupHelper.javaClass.name)
+ val setForceIcons = classPopupHelper.getMethod("setForceShowIcon", Boolean::class.java)
+ setForceIcons.invoke(menuPopupHelper, true)
+ break
+ }
+ }
+ } catch (e: Exception) {
+ Logger.w(TAG, "Could not force show icons in popup menu: ${e.message}")
+ }
+
+ popupMenu.setOnMenuItemClickListener { menuItem ->
+ val noteType = when (menuItem.itemId) {
+ R.id.action_create_text_note -> NoteType.TEXT
+ R.id.action_create_checklist -> NoteType.CHECKLIST
+ else -> return@setOnMenuItemClickListener false
+ }
+
+ val intent = Intent(this, NoteEditorActivity::class.java)
+ intent.putExtra(NoteEditorActivity.EXTRA_NOTE_TYPE, noteType.name)
+ startActivity(intent)
+ true
+ }
+
+ popupMenu.show()
+ }
+
private fun loadNotes() {
val notes = storage.loadAllNotes()
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt
index 5dd9704..3e82256 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt
@@ -3,27 +3,58 @@ package dev.dettmer.simplenotes
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
+import android.view.View
+import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.MaterialToolbar
+import com.google.android.material.button.MaterialButton
import com.google.android.material.color.DynamicColors
import com.google.android.material.textfield.TextInputEditText
+import com.google.android.material.textfield.TextInputLayout
+import dev.dettmer.simplenotes.adapters.ChecklistEditorAdapter
+import dev.dettmer.simplenotes.models.ChecklistItem
import dev.dettmer.simplenotes.models.Note
+import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
+import dev.dettmer.simplenotes.utils.Logger
import dev.dettmer.simplenotes.utils.showToast
+/**
+ * Editor Activity für Notizen und Checklisten
+ *
+ * v1.4.0: Unterstützt jetzt sowohl TEXT als auch CHECKLIST Notizen
+ */
class NoteEditorActivity : AppCompatActivity() {
+ // Views
+ private lateinit var toolbar: MaterialToolbar
+ private lateinit var tilTitle: TextInputLayout
private lateinit var editTextTitle: TextInputEditText
+ private lateinit var tilContent: TextInputLayout
private lateinit var editTextContent: TextInputEditText
+ private lateinit var checklistContainer: LinearLayout
+ private lateinit var rvChecklistItems: RecyclerView
+ private lateinit var btnAddItem: MaterialButton
+
private lateinit var storage: NotesStorage
+ // State
private var existingNote: Note? = null
+ private var currentNoteType: NoteType = NoteType.TEXT
+ private val checklistItems = mutableListOf()
+ private var checklistAdapter: ChecklistEditorAdapter? = null
+ private var itemTouchHelper: ItemTouchHelper? = null
companion object {
+ private const val TAG = "NoteEditorActivity"
const val EXTRA_NOTE_ID = "extra_note_id"
+ const val EXTRA_NOTE_TYPE = "extra_note_type"
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -36,39 +67,172 @@ class NoteEditorActivity : AppCompatActivity() {
storage = NotesStorage(this)
- // Setup toolbar
- val toolbar = findViewById(R.id.toolbar)
- setSupportActionBar(toolbar)
- supportActionBar?.apply {
- setDisplayHomeAsUpEnabled(true)
- // 🔥 v1.1.2: Use default back arrow (Material Design) instead of X icon
- // Icon is set in XML: app:navigationIcon="?attr/homeAsUpIndicator"
- }
-
- // Find views
+ findViews()
+ setupToolbar()
+ loadNoteOrDetermineType()
+ setupUIForNoteType()
+ }
+
+ private fun findViews() {
+ toolbar = findViewById(R.id.toolbar)
+ tilTitle = findViewById(R.id.tilTitle)
editTextTitle = findViewById(R.id.editTextTitle)
+ tilContent = findViewById(R.id.tilContent)
editTextContent = findViewById(R.id.editTextContent)
-
- // Load existing note if editing
+ checklistContainer = findViewById(R.id.checklistContainer)
+ rvChecklistItems = findViewById(R.id.rvChecklistItems)
+ btnAddItem = findViewById(R.id.btnAddItem)
+ }
+
+ private fun setupToolbar() {
+ setSupportActionBar(toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(true)
+ }
+
+ private fun loadNoteOrDetermineType() {
val noteId = intent.getStringExtra(EXTRA_NOTE_ID)
+
if (noteId != null) {
+ // Existierende Notiz laden
existingNote = storage.loadNote(noteId)
- existingNote?.let {
- editTextTitle.setText(it.title)
- editTextContent.setText(it.content)
- supportActionBar?.title = "Notiz bearbeiten"
+ existingNote?.let { note ->
+ editTextTitle.setText(note.title)
+ currentNoteType = note.noteType
+
+ when (note.noteType) {
+ NoteType.TEXT -> {
+ editTextContent.setText(note.content)
+ supportActionBar?.title = getString(R.string.edit_note)
+ }
+ NoteType.CHECKLIST -> {
+ note.checklistItems?.let { items ->
+ checklistItems.clear()
+ checklistItems.addAll(items.sortedBy { it.order })
+ }
+ supportActionBar?.title = getString(R.string.edit_checklist)
+ }
+ }
}
} else {
- supportActionBar?.title = "Neue Notiz"
+ // Neue Notiz - Typ aus Intent
+ val typeString = intent.getStringExtra(EXTRA_NOTE_TYPE) ?: NoteType.TEXT.name
+ currentNoteType = try {
+ NoteType.valueOf(typeString)
+ } catch (e: IllegalArgumentException) {
+ Logger.w(TAG, "Invalid note type '$typeString', defaulting to TEXT: ${e.message}")
+ NoteType.TEXT
+ }
+
+ when (currentNoteType) {
+ NoteType.TEXT -> {
+ supportActionBar?.title = getString(R.string.new_note)
+ }
+ NoteType.CHECKLIST -> {
+ supportActionBar?.title = getString(R.string.new_checklist)
+ // Erstes leeres Item hinzufügen
+ checklistItems.add(ChecklistItem.createEmpty(0))
+ }
+ }
+ }
+ }
+
+ private fun setupUIForNoteType() {
+ when (currentNoteType) {
+ NoteType.TEXT -> {
+ tilContent.visibility = View.VISIBLE
+ checklistContainer.visibility = View.GONE
+ }
+ NoteType.CHECKLIST -> {
+ tilContent.visibility = View.GONE
+ checklistContainer.visibility = View.VISIBLE
+ setupChecklistRecyclerView()
+ }
+ }
+ }
+
+ private fun setupChecklistRecyclerView() {
+ checklistAdapter = ChecklistEditorAdapter(
+ items = checklistItems,
+ onItemCheckedChanged = { position, isChecked ->
+ if (position in checklistItems.indices) {
+ checklistItems[position].isChecked = isChecked
+ }
+ },
+ onItemTextChanged = { position, newText ->
+ if (position in checklistItems.indices) {
+ checklistItems[position] = checklistItems[position].copy(text = newText)
+ }
+ },
+ onItemDeleted = { position ->
+ deleteChecklistItem(position)
+ },
+ onAddNewItem = { position ->
+ addChecklistItemAt(position)
+ },
+ onStartDrag = { viewHolder ->
+ itemTouchHelper?.startDrag(viewHolder)
+ }
+ )
+
+ rvChecklistItems.apply {
+ layoutManager = LinearLayoutManager(this@NoteEditorActivity)
+ adapter = checklistAdapter
+ }
+
+ // Drag & Drop Setup
+ val callback = object : ItemTouchHelper.SimpleCallback(
+ ItemTouchHelper.UP or ItemTouchHelper.DOWN,
+ 0 // Kein Swipe
+ ) {
+ override fun onMove(
+ recyclerView: RecyclerView,
+ viewHolder: RecyclerView.ViewHolder,
+ target: RecyclerView.ViewHolder
+ ): Boolean {
+ val from = viewHolder.bindingAdapterPosition
+ val to = target.bindingAdapterPosition
+ checklistAdapter?.moveItem(from, to)
+ return true
+ }
+
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ // Nicht verwendet
+ }
+
+ override fun isLongPressDragEnabled(): Boolean = false // Nur via Handle
+ }
+
+ itemTouchHelper = ItemTouchHelper(callback)
+ itemTouchHelper?.attachToRecyclerView(rvChecklistItems)
+
+ // Add Item Button
+ btnAddItem.setOnClickListener {
+ addChecklistItemAt(checklistItems.size)
+ }
+ }
+
+ private fun addChecklistItemAt(position: Int) {
+ val newItem = ChecklistItem.createEmpty(position)
+ checklistAdapter?.insertItem(position, newItem)
+
+ // Zum neuen Item scrollen und fokussieren
+ rvChecklistItems.scrollToPosition(position)
+ checklistAdapter?.focusItem(rvChecklistItems, position)
+ }
+
+ private fun deleteChecklistItem(position: Int) {
+ checklistAdapter?.removeItem(position)
+
+ // Wenn letztes Item gelöscht, automatisch neues hinzufügen
+ if (checklistItems.isEmpty()) {
+ addChecklistItemAt(0)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_editor, menu)
-
- // Show delete only for existing notes
+ // Delete nur für existierende Notizen
menu.findItem(R.id.action_delete)?.isVisible = existingNote != null
-
return true
}
@@ -92,51 +256,96 @@ class NoteEditorActivity : AppCompatActivity() {
private fun saveNote() {
val title = editTextTitle.text?.toString()?.trim() ?: ""
- val content = editTextContent.text?.toString()?.trim() ?: ""
- if (title.isEmpty() && content.isEmpty()) {
- showToast("Notiz ist leer")
- return
+ when (currentNoteType) {
+ NoteType.TEXT -> {
+ val content = editTextContent.text?.toString()?.trim() ?: ""
+
+ if (title.isEmpty() && content.isEmpty()) {
+ showToast(getString(R.string.note_is_empty))
+ return
+ }
+
+ val note = if (existingNote != null) {
+ existingNote!!.copy(
+ title = title,
+ content = content,
+ noteType = NoteType.TEXT,
+ checklistItems = null,
+ updatedAt = System.currentTimeMillis(),
+ syncStatus = SyncStatus.PENDING
+ )
+ } else {
+ Note(
+ title = title,
+ content = content,
+ noteType = NoteType.TEXT,
+ checklistItems = null,
+ deviceId = DeviceIdGenerator.getDeviceId(this),
+ syncStatus = SyncStatus.LOCAL_ONLY
+ )
+ }
+
+ storage.saveNote(note)
+ }
+
+ NoteType.CHECKLIST -> {
+ // Leere Items filtern
+ val validItems = checklistItems.filter { it.text.isNotBlank() }
+
+ if (title.isEmpty() && validItems.isEmpty()) {
+ showToast(getString(R.string.note_is_empty))
+ return
+ }
+
+ // Order neu setzen
+ val orderedItems = validItems.mapIndexed { index, item ->
+ item.copy(order = index)
+ }
+
+ val note = if (existingNote != null) {
+ existingNote!!.copy(
+ title = title,
+ content = "", // Leer für Checklisten
+ noteType = NoteType.CHECKLIST,
+ checklistItems = orderedItems,
+ updatedAt = System.currentTimeMillis(),
+ syncStatus = SyncStatus.PENDING
+ )
+ } else {
+ Note(
+ title = title,
+ content = "",
+ noteType = NoteType.CHECKLIST,
+ checklistItems = orderedItems,
+ deviceId = DeviceIdGenerator.getDeviceId(this),
+ syncStatus = SyncStatus.LOCAL_ONLY
+ )
+ }
+
+ storage.saveNote(note)
+ }
}
- val note = if (existingNote != null) {
- // Update existing note
- existingNote!!.copy(
- title = title,
- content = content,
- updatedAt = System.currentTimeMillis(),
- syncStatus = SyncStatus.PENDING
- )
- } else {
- // Create new note
- Note(
- title = title,
- content = content,
- deviceId = DeviceIdGenerator.getDeviceId(this),
- syncStatus = SyncStatus.LOCAL_ONLY
- )
- }
-
- storage.saveNote(note)
- showToast("Notiz gespeichert")
+ showToast(getString(R.string.note_saved))
finish()
}
private fun confirmDelete() {
AlertDialog.Builder(this)
- .setTitle("Notiz löschen?")
- .setMessage("Diese Aktion kann nicht rückgängig gemacht werden.")
- .setPositiveButton("Löschen") { _, _ ->
+ .setTitle(getString(R.string.delete_note_title))
+ .setMessage(getString(R.string.delete_note_message))
+ .setPositiveButton(getString(R.string.delete)) { _, _ ->
deleteNote()
}
- .setNegativeButton("Abbrechen", null)
+ .setNegativeButton(getString(R.string.cancel), null)
.show()
}
private fun deleteNote() {
existingNote?.let {
storage.deleteNote(it.id)
- showToast("Notiz gelöscht")
+ showToast(getString(R.string.note_deleted))
finish()
}
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/adapters/ChecklistEditorAdapter.kt b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/ChecklistEditorAdapter.kt
new file mode 100644
index 0000000..4fdd014
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/ChecklistEditorAdapter.kt
@@ -0,0 +1,181 @@
+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
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.checkbox.MaterialCheckBox
+import dev.dettmer.simplenotes.R
+import dev.dettmer.simplenotes.models.ChecklistItem
+
+/**
+ * Adapter für die Bearbeitung von Checklist-Items im Editor
+ *
+ * v1.4.0: Checklisten-Feature
+ */
+class ChecklistEditorAdapter(
+ private val items: MutableList,
+ private val onItemCheckedChanged: (Int, Boolean) -> Unit,
+ private val onItemTextChanged: (Int, String) -> Unit,
+ private val onItemDeleted: (Int) -> Unit,
+ private val onAddNewItem: (Int) -> Unit,
+ private val onStartDrag: (RecyclerView.ViewHolder) -> Unit
+) : RecyclerView.Adapter() {
+
+ inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ val dragHandle: ImageView = view.findViewById(R.id.ivDragHandle)
+ val checkbox: MaterialCheckBox = view.findViewById(R.id.cbItem)
+ val editText: EditText = view.findViewById(R.id.etItemText)
+ val deleteButton: ImageButton = view.findViewById(R.id.btnDeleteItem)
+
+ private var textWatcher: TextWatcher? = null
+
+ @Suppress("NestedBlockDepth", "UNUSED_PARAMETER")
+ fun bind(item: ChecklistItem, position: Int) {
+ // Vorherigen TextWatcher entfernen um Loops zu vermeiden
+ textWatcher?.let { editText.removeTextChangedListener(it) }
+
+ // Checkbox
+ checkbox.isChecked = item.isChecked
+ checkbox.setOnCheckedChangeListener { _, isChecked ->
+ onItemCheckedChanged(bindingAdapterPosition, isChecked)
+ updateStrikethrough(isChecked)
+ }
+
+ // Text
+ editText.setText(item.text)
+ updateStrikethrough(item.isChecked)
+
+ // TextWatcher für Änderungen
+ 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() ?: "")
+ }
+ }
+ }
+ 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
+ if (pos != RecyclerView.NO_POSITION) {
+ onItemDeleted(pos)
+ }
+ }
+
+ // Drag Handle Touch Listener
+ dragHandle.setOnTouchListener { _, event ->
+ if (event.actionMasked == MotionEvent.ACTION_DOWN) {
+ onStartDrag(this)
+ }
+ false
+ }
+ }
+
+ private fun updateStrikethrough(isChecked: Boolean) {
+ if (isChecked) {
+ editText.paintFlags = editText.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
+ editText.alpha = CHECKED_ITEM_ALPHA
+ } else {
+ editText.paintFlags = editText.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
+ editText.alpha = UNCHECKED_ITEM_ALPHA
+ }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val view = LayoutInflater.from(parent.context)
+ .inflate(R.layout.item_checklist_editor, parent, false)
+ return ViewHolder(view)
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(items[position], position)
+ }
+
+ override fun getItemCount(): Int = items.size
+
+ /**
+ * Bewegt ein Item von einer Position zu einer anderen (für Drag & Drop)
+ */
+ fun moveItem(fromPosition: Int, toPosition: Int) {
+ val item = items.removeAt(fromPosition)
+ items.add(toPosition, item)
+ notifyItemMoved(fromPosition, toPosition)
+
+ // Order-Werte aktualisieren
+ items.forEachIndexed { index, checklistItem ->
+ checklistItem.order = index
+ }
+ }
+
+ /**
+ * Entfernt ein Item an der angegebenen Position
+ */
+ fun removeItem(position: Int) {
+ if (position in items.indices) {
+ items.removeAt(position)
+ notifyItemRemoved(position)
+ // Order-Werte aktualisieren
+ items.forEachIndexed { index, checklistItem ->
+ checklistItem.order = index
+ }
+ }
+ }
+
+ /**
+ * Fügt ein neues Item an der angegebenen Position ein
+ */
+ fun insertItem(position: Int, item: ChecklistItem) {
+ items.add(position, item)
+ notifyItemInserted(position)
+ // Order-Werte aktualisieren
+ items.forEachIndexed { index, checklistItem ->
+ checklistItem.order = index
+ }
+ }
+
+ /**
+ * Fokussiert das EditText des Items an der angegebenen Position
+ */
+ fun focusItem(recyclerView: RecyclerView, position: Int) {
+ recyclerView.post {
+ val viewHolder = recyclerView.findViewHolderForAdapterPosition(position) as? ViewHolder
+ viewHolder?.editText?.requestFocus()
+ }
+ }
+
+ companion object {
+ /** Alpha-Wert für abgehakte Items (durchgestrichen) */
+ private const val CHECKED_ITEM_ALPHA = 0.6f
+ /** Alpha-Wert für nicht abgehakte Items */
+ private const val UNCHECKED_ITEM_ALPHA = 1.0f
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt
index d0c0e2d..6161d61 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt
@@ -11,11 +11,17 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note
+import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
import dev.dettmer.simplenotes.utils.Constants
import dev.dettmer.simplenotes.utils.toReadableTime
import dev.dettmer.simplenotes.utils.truncate
+/**
+ * Adapter für die Notizen-Liste
+ *
+ * v1.4.0: Unterstützt jetzt TEXT und CHECKLIST Notizen
+ */
class NotesAdapter(
private val onNoteClick: (Note) -> Unit
) : ListAdapter(NoteDiffCallback()) {
@@ -31,16 +37,46 @@ class NotesAdapter(
}
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ private val ivNoteTypeIcon: ImageView = itemView.findViewById(R.id.ivNoteTypeIcon)
private val textViewTitle: TextView = itemView.findViewById(R.id.textViewTitle)
private val textViewContent: TextView = itemView.findViewById(R.id.textViewContent)
+ private val textViewChecklistPreview: TextView = itemView.findViewById(R.id.textViewChecklistPreview)
private val textViewTimestamp: TextView = itemView.findViewById(R.id.textViewTimestamp)
private val imageViewSyncStatus: ImageView = itemView.findViewById(R.id.imageViewSyncStatus)
fun bind(note: Note) {
- textViewTitle.text = note.title.ifEmpty { "Ohne Titel" }
- textViewContent.text = note.content.truncate(100)
+ // Titel
+ textViewTitle.text = note.title.ifEmpty {
+ itemView.context.getString(R.string.untitled)
+ }
textViewTimestamp.text = note.updatedAt.toReadableTime()
+ // v1.4.0: Typ-spezifische Anzeige
+ when (note.noteType) {
+ NoteType.TEXT -> {
+ ivNoteTypeIcon.setImageResource(R.drawable.ic_note_24)
+ textViewContent.text = note.content.truncate(100)
+ textViewContent.visibility = View.VISIBLE
+ textViewChecklistPreview.visibility = View.GONE
+ }
+ NoteType.CHECKLIST -> {
+ ivNoteTypeIcon.setImageResource(R.drawable.ic_checklist_24)
+ textViewContent.visibility = View.GONE
+ textViewChecklistPreview.visibility = View.VISIBLE
+
+ // Fortschritt berechnen
+ val items = note.checklistItems ?: emptyList()
+ val checkedCount = items.count { it.isChecked }
+ val totalCount = items.size
+
+ textViewChecklistPreview.text = if (totalCount > 0) {
+ itemView.context.getString(R.string.checklist_progress, checkedCount, totalCount)
+ } else {
+ itemView.context.getString(R.string.empty_checklist)
+ }
+ }
+ }
+
// Sync Icon nur zeigen wenn Sync konfiguriert ist
val prefs = itemView.context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistItem.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistItem.kt
new file mode 100644
index 0000000..b07fc08
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/ChecklistItem.kt
@@ -0,0 +1,34 @@
+package dev.dettmer.simplenotes.models
+
+import java.util.UUID
+
+/**
+ * Repräsentiert ein einzelnes Item in einer Checkliste
+ *
+ * v1.4.0: Checklisten-Feature
+ *
+ * @property id Eindeutige ID für Sync-Konflikterkennung
+ * @property text Der Text des Items
+ * @property isChecked Ob das Item abgehakt ist
+ * @property order Sortierreihenfolge (0-basiert)
+ */
+data class ChecklistItem(
+ val id: String = UUID.randomUUID().toString(),
+ val text: String = "",
+ var isChecked: Boolean = false,
+ var order: Int = 0
+) {
+ companion object {
+ /**
+ * Erstellt ein neues leeres ChecklistItem
+ */
+ fun createEmpty(order: Int): ChecklistItem {
+ return ChecklistItem(
+ id = UUID.randomUUID().toString(),
+ text = "",
+ isChecked = false,
+ order = order
+ )
+ }
+ }
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
index a43b5a6..ab2b16b 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/Note.kt
@@ -14,56 +14,125 @@ data class Note(
val createdAt: Long = System.currentTimeMillis(),
val updatedAt: Long = System.currentTimeMillis(),
val deviceId: String,
- val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY
+ val syncStatus: SyncStatus = SyncStatus.LOCAL_ONLY,
+ // v1.4.0: Checklisten-Felder
+ val noteType: NoteType = NoteType.TEXT,
+ val checklistItems: List? = null
) {
+ /**
+ * Serialisiert Note zu JSON (v1.4.0: Nutzt Gson für komplexe Strukturen)
+ */
fun toJson(): String {
- return """
- {
- "id": "$id",
- "title": "${title.escapeJson()}",
- "content": "${content.escapeJson()}",
- "createdAt": $createdAt,
- "updatedAt": $updatedAt,
- "deviceId": "$deviceId",
- "syncStatus": "${syncStatus.name}"
- }
- """.trimIndent()
+ val gson = com.google.gson.GsonBuilder()
+ .setPrettyPrinting()
+ .create()
+ return gson.toJson(this)
}
/**
* Konvertiert Note zu Markdown mit YAML Frontmatter (Task #1.2.0-08)
* Format kompatibel mit Obsidian, Joplin, Typora
+ * v1.4.0: Unterstützt jetzt auch Checklisten-Format
*/
fun toMarkdown(): String {
- return """
+ val header = """
---
id: $id
created: ${formatISO8601(createdAt)}
updated: ${formatISO8601(updatedAt)}
device: $deviceId
+type: ${noteType.name.lowercase()}
---
# $title
-$content
- """.trimIndent()
+""".trimIndent()
+
+ return when (noteType) {
+ NoteType.TEXT -> header + content
+ NoteType.CHECKLIST -> {
+ val checklistMarkdown = checklistItems?.sortedBy { it.order }?.joinToString("\n") { item ->
+ val checkbox = if (item.isChecked) "[x]" else "[ ]"
+ "- $checkbox ${item.text}"
+ } ?: ""
+ header + checklistMarkdown
+ }
+ }
}
companion object {
private const val TAG = "Note"
+ /**
+ * Parst JSON zu Note-Objekt mit Backward Compatibility für alte Notizen ohne noteType
+ */
fun fromJson(json: String): Note? {
return try {
val gson = com.google.gson.Gson()
- gson.fromJson(json, Note::class.java)
+ val jsonObject = com.google.gson.JsonParser.parseString(json).asJsonObject
+
+ // Backward Compatibility: Alte Notizen ohne noteType bekommen TEXT
+ val noteType = if (jsonObject.has("noteType") && !jsonObject.get("noteType").isJsonNull) {
+ try {
+ NoteType.valueOf(jsonObject.get("noteType").asString)
+ } catch (e: Exception) {
+ Logger.w(TAG, "Unknown noteType, defaulting to TEXT: ${e.message}")
+ NoteType.TEXT
+ }
+ } else {
+ NoteType.TEXT
+ }
+
+ // Parsen der Basis-Note
+ val rawNote = gson.fromJson(json, NoteRaw::class.java)
+
+ // Checklist-Items parsen (kann null sein)
+ val checklistItemsType = object : com.google.gson.reflect.TypeToken>() {}.type
+ val checklistItems = if (jsonObject.has("checklistItems") &&
+ !jsonObject.get("checklistItems").isJsonNull
+ ) {
+ gson.fromJson>(
+ jsonObject.get("checklistItems"),
+ checklistItemsType
+ )
+ } else {
+ null
+ }
+
+ // Note mit korrekten Werten erstellen
+ Note(
+ id = rawNote.id,
+ title = rawNote.title,
+ content = rawNote.content,
+ createdAt = rawNote.createdAt,
+ updatedAt = rawNote.updatedAt,
+ deviceId = rawNote.deviceId,
+ syncStatus = rawNote.syncStatus ?: SyncStatus.LOCAL_ONLY,
+ noteType = noteType,
+ checklistItems = checklistItems
+ )
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
null
}
}
+ /**
+ * Hilfsklasse für Gson-Parsing mit nullable Feldern
+ */
+ private data class NoteRaw(
+ val id: String = UUID.randomUUID().toString(),
+ val title: String = "",
+ val content: String = "",
+ val createdAt: Long = System.currentTimeMillis(),
+ val updatedAt: Long = System.currentTimeMillis(),
+ val deviceId: String = "",
+ val syncStatus: SyncStatus? = null
+ )
+
/**
* Parst Markdown zurück zu Note-Objekt (Task #1.2.0-09)
+ * v1.4.0: Unterstützt jetzt auch Checklisten-Format
*
* @param md Markdown-String mit YAML Frontmatter
* @return Note-Objekt oder null bei Parse-Fehler
@@ -91,10 +160,47 @@ $content
.firstOrNull { it.startsWith("# ") }
?.removePrefix("# ")?.trim() ?: "Untitled"
- // Extract content (everything after heading)
- val content = contentBlock
- .substringAfter("# $title\n\n", "")
- .trim()
+ // v1.4.0: Prüfe ob type: checklist im Frontmatter
+ val noteTypeStr = metadata["type"]?.lowercase() ?: "text"
+ val noteType = when (noteTypeStr) {
+ "checklist" -> NoteType.CHECKLIST
+ else -> NoteType.TEXT
+ }
+
+ // v1.4.0: Parse Content basierend auf Typ
+ // FIX: Robusteres Parsing - suche nach dem Titel-Header und extrahiere den Rest
+ val titleLineIndex = contentBlock.lines().indexOfFirst { it.startsWith("# ") }
+ val contentAfterTitle = if (titleLineIndex >= 0) {
+ // Alles nach der Titel-Zeile, überspringe führende Leerzeilen
+ contentBlock.lines()
+ .drop(titleLineIndex + 1)
+ .dropWhile { it.isBlank() }
+ .joinToString("\n")
+ .trim()
+ } else {
+ // Fallback: Gesamter Content (kein Titel gefunden)
+ contentBlock.trim()
+ }
+
+ val content: String
+ val checklistItems: List?
+
+ if (noteType == NoteType.CHECKLIST) {
+ // Parse Checklist Items
+ val checklistRegex = Regex("^- \\[([ xX])\\] (.*)$", RegexOption.MULTILINE)
+ checklistItems = checklistRegex.findAll(contentAfterTitle).mapIndexed { index, matchResult ->
+ ChecklistItem(
+ id = UUID.randomUUID().toString(),
+ text = matchResult.groupValues[2].trim(),
+ isChecked = matchResult.groupValues[1].lowercase() == "x",
+ order = index
+ )
+ }.toList().ifEmpty { null }
+ content = "" // Checklisten haben keinen "content"
+ } else {
+ content = contentAfterTitle
+ checklistItems = null
+ }
Note(
id = metadata["id"] ?: UUID.randomUUID().toString(),
@@ -103,7 +209,9 @@ $content
createdAt = parseISO8601(metadata["created"] ?: ""),
updatedAt = parseISO8601(metadata["updated"] ?: ""),
deviceId = metadata["device"] ?: "desktop",
- syncStatus = SyncStatus.SYNCED // Annahme: Vom Server importiert
+ syncStatus = SyncStatus.SYNCED, // Annahme: Vom Server importiert
+ noteType = noteType,
+ checklistItems = checklistItems
)
} catch (e: Exception) {
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/models/NoteType.kt b/android/app/src/main/java/dev/dettmer/simplenotes/models/NoteType.kt
new file mode 100644
index 0000000..d010958
--- /dev/null
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/models/NoteType.kt
@@ -0,0 +1,11 @@
+package dev.dettmer.simplenotes.models
+
+/**
+ * Definiert die verschiedenen Notiz-Typen
+ *
+ * v1.4.0: Checklisten-Feature
+ */
+enum class NoteType {
+ TEXT, // Normale Text-Notiz (Standard)
+ CHECKLIST // Checkliste mit abhakbaren Items
+}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
index 4641312..1516b46 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/SyncWorker.kt
@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.sync
+import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
@@ -22,6 +23,21 @@ class SyncWorker(
const val ACTION_SYNC_COMPLETED = "dev.dettmer.simplenotes.SYNC_COMPLETED"
}
+ /**
+ * Prüft ob die App im Vordergrund ist.
+ * Wenn ja, brauchen wir keine Benachrichtigung - die UI zeigt die Änderungen direkt.
+ */
+ private fun isAppInForeground(): Boolean {
+ val activityManager = applicationContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ val appProcesses = activityManager.runningAppProcesses ?: return false
+ val packageName = applicationContext.packageName
+
+ return appProcesses.any { process ->
+ process.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND &&
+ process.processName == packageName
+ }
+ }
+
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
if (BuildConfig.DEBUG) {
Logger.d(TAG, "═══════════════════════════════════════")
@@ -131,14 +147,20 @@ class SyncWorker(
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
+ // UND die App nicht im Vordergrund ist (sonst sieht User die Änderungen direkt)
if (result.syncedCount > 0) {
- if (BuildConfig.DEBUG) {
- Logger.d(TAG, " Showing success notification...")
+ val appInForeground = isAppInForeground()
+ if (appInForeground) {
+ Logger.d(TAG, "ℹ️ App in foreground - skipping notification (UI shows changes)")
+ } else {
+ if (BuildConfig.DEBUG) {
+ Logger.d(TAG, " Showing success notification...")
+ }
+ NotificationHelper.showSyncSuccess(
+ applicationContext,
+ result.syncedCount
+ )
}
- NotificationHelper.showSyncSuccess(
- applicationContext,
- result.syncedCount
- )
} else {
Logger.d(TAG, "ℹ️ No changes to sync - no notification")
}
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt
index 8560ff5..f70cbea 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt
@@ -41,6 +41,7 @@ class WebDavSyncService(private val context: Context) {
private const val SOCKET_TIMEOUT_MS = 2000
private const val MAX_FILENAME_LENGTH = 200
private const val ETAG_PREVIEW_LENGTH = 8
+ private const val CONTENT_PREVIEW_LENGTH = 50
// 🔒 v1.3.1: Mutex um parallele Syncs zu verhindern
private val syncMutex = Mutex()
@@ -858,8 +859,31 @@ class WebDavSyncService(private val context: Context) {
}
// Sanitize Filename (Task #1.2.0-12)
- val filename = sanitizeFilename(note.title) + ".md"
- val noteUrl = "$mdUrl/$filename"
+ val baseFilename = sanitizeFilename(note.title)
+ var filename = "$baseFilename.md"
+ var noteUrl = "$mdUrl/$filename"
+
+ // Prüfe ob Datei bereits existiert und von anderer Note stammt
+ try {
+ if (sardine.exists(noteUrl)) {
+ // Lese existierende Datei und prüfe ID im YAML-Header
+ val existingContent = sardine.get(noteUrl).bufferedReader().use { it.readText() }
+ val existingIdMatch = Regex("^---\\n.*?\\nid:\\s*([a-f0-9-]+)", RegexOption.DOT_MATCHES_ALL)
+ .find(existingContent)
+ val existingId = existingIdMatch?.groupValues?.get(1)
+
+ if (existingId != null && existingId != note.id) {
+ // Andere Note hat gleichen Titel - verwende ID-Suffix
+ val shortId = note.id.take(8)
+ filename = "${baseFilename}_$shortId.md"
+ noteUrl = "$mdUrl/$filename"
+ Logger.d(TAG, "📝 Duplicate title, using: $filename")
+ }
+ }
+ } catch (e: Exception) {
+ Logger.w(TAG, "⚠️ Could not check existing file: ${e.message}")
+ // Continue with default filename
+ }
// Konvertiere zu Markdown
val mdContent = note.toMarkdown().toByteArray()
@@ -884,6 +908,29 @@ class WebDavSyncService(private val context: Context) {
.trim('_', ' ') // Trim Underscores/Spaces
}
+ /**
+ * Generiert eindeutigen Markdown-Dateinamen für eine Notiz.
+ * Bei Duplikaten wird die Note-ID als Suffix angehängt.
+ *
+ * @param note Die Notiz
+ * @param usedFilenames Set der bereits verwendeten Dateinamen (ohne .md)
+ * @return Eindeutiger Dateiname (ohne .md Extension)
+ */
+ private fun getUniqueMarkdownFilename(note: Note, usedFilenames: MutableSet): String {
+ val baseFilename = sanitizeFilename(note.title)
+
+ return if (usedFilenames.contains(baseFilename)) {
+ // Duplikat - hänge gekürzte ID an
+ val shortId = note.id.take(8)
+ val uniqueFilename = "${baseFilename}_$shortId"
+ usedFilenames.add(uniqueFilename)
+ uniqueFilename
+ } else {
+ usedFilenames.add(baseFilename)
+ baseFilename
+ }
+ }
+
/**
* Exportiert ALLE lokalen Notizen als Markdown (Initial-Export)
*
@@ -927,6 +974,9 @@ class WebDavSyncService(private val context: Context) {
val totalCount = allNotes.size
var exportedCount = 0
+ // Track used filenames to handle duplicates
+ val usedFilenames = mutableSetOf()
+
Logger.d(TAG, "📝 Found $totalCount notes to export")
allNotes.forEachIndexed { index, note ->
@@ -934,8 +984,8 @@ class WebDavSyncService(private val context: Context) {
// Progress-Callback
onProgress(index + 1, totalCount)
- // Sanitize Filename
- val filename = sanitizeFilename(note.title) + ".md"
+ // Eindeutiger Filename (mit Duplikat-Handling)
+ val filename = getUniqueMarkdownFilename(note, usedFilenames) + ".md"
val noteUrl = "$mdUrl/$filename"
// Konvertiere zu Markdown
@@ -945,7 +995,7 @@ class WebDavSyncService(private val context: Context) {
sardine.put(noteUrl, mdContent, "text/markdown")
exportedCount++
- Logger.d(TAG, " ✅ Exported [${index + 1}/$totalCount]: ${note.title}")
+ Logger.d(TAG, " ✅ Exported [${index + 1}/$totalCount]: ${note.title} -> $filename")
} catch (e: Exception) {
Logger.e(TAG, "❌ Failed to export ${note.title}: ${e.message}")
@@ -1539,13 +1589,27 @@ class WebDavSyncService(private val context: Context) {
Logger.w(TAG, " ⚠️ Failed to parse ${resource.name} - fromMarkdown returned null")
continue
}
+
+ // v1.4.0 FIX: Validierung - leere TEXT-Notizen nicht importieren wenn lokal Content existiert
+ val localNote = storage.loadNote(mdNote.id)
+ if (mdNote.noteType == dev.dettmer.simplenotes.models.NoteType.TEXT &&
+ mdNote.content.isBlank() &&
+ localNote != null && localNote.content.isNotBlank()) {
+ Logger.w(
+ TAG,
+ " ⚠️ Skipping ${resource.name}: " +
+ "MD content empty but local has content - likely parse error!"
+ )
+ continue
+ }
+
Logger.d(
TAG,
" Parsed: id=${mdNote.id}, title=${mdNote.title}, " +
- "updatedAt=${Date(mdNote.updatedAt)}"
+ "updatedAt=${Date(mdNote.updatedAt)}, " +
+ "content=${mdNote.content.take(CONTENT_PREVIEW_LENGTH)}..."
)
- val localNote = storage.loadNote(mdNote.id)
Logger.d(
TAG,
" Local note: " + if (localNote == null) {
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
index 56e9eec..ea90e82 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WifiSyncReceiver.kt
@@ -5,12 +5,17 @@ import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
-import android.net.wifi.WifiManager
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import dev.dettmer.simplenotes.utils.Constants
import java.util.concurrent.TimeUnit
+/**
+ * WiFi-Sync BroadcastReceiver
+ *
+ * Triggert Sync wenn WiFi verbunden wird (jedes WiFi, keine SSID-Prüfung mehr)
+ * Die eigentliche Server-Erreichbarkeitsprüfung erfolgt im SyncWorker.
+ */
class WifiSyncReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@@ -22,34 +27,24 @@ class WifiSyncReceiver : BroadcastReceiver() {
return
}
- // Check if connected to home WiFi
- if (isConnectedToHomeWifi(context)) {
+ // Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
+ if (isConnectedToWifi(context)) {
scheduleSyncWork(context)
}
}
- @Suppress("ReturnCount") // Early returns for WiFi validation checks
- private fun isConnectedToHomeWifi(context: Context): Boolean {
- val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
- val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
-
+ /**
+ * Prüft ob ein WiFi-Netzwerk verbunden ist (beliebiges WiFi)
+ * Die Server-Erreichbarkeitsprüfung erfolgt erst im SyncWorker.
+ */
+ private fun isConnectedToWifi(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
- if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
- return false
- }
-
- // Get current SSID
- val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
- as WifiManager
- val wifiInfo = wifiManager.connectionInfo
- val currentSSID = wifiInfo.ssid.replace("\"", "")
-
- return currentSSID == homeSSID
+ return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
private fun scheduleSyncWork(context: Context) {
diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
index b4f9fe5..e10daac 100644
--- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
+++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Constants.kt
@@ -6,7 +6,6 @@ object Constants {
const val KEY_SERVER_URL = "server_url"
const val KEY_USERNAME = "username"
const val KEY_PASSWORD = "password"
- const val KEY_HOME_SSID = "home_ssid"
const val KEY_AUTO_SYNC = "auto_sync_enabled"
const val KEY_LAST_SYNC = "last_sync_timestamp"
diff --git a/android/app/src/main/res/drawable/ic_checklist_24.xml b/android/app/src/main/res/drawable/ic_checklist_24.xml
new file mode 100644
index 0000000..9723cba
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_checklist_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_delete_24.xml b/android/app/src/main/res/drawable/ic_delete_24.xml
new file mode 100644
index 0000000..e2db651
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_delete_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_drag_handle_24.xml b/android/app/src/main/res/drawable/ic_drag_handle_24.xml
new file mode 100644
index 0000000..9fe249a
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_drag_handle_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/drawable/ic_note_24.xml b/android/app/src/main/res/drawable/ic_note_24.xml
new file mode 100644
index 0000000..9c78cbf
--- /dev/null
+++ b/android/app/src/main/res/drawable/ic_note_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/android/app/src/main/res/layout/activity_editor.xml b/android/app/src/main/res/layout/activity_editor.xml
index 41943fc..6aa5638 100644
--- a/android/app/src/main/res/layout/activity_editor.xml
+++ b/android/app/src/main/res/layout/activity_editor.xml
@@ -2,6 +2,7 @@
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_checklist_editor.xml b/android/app/src/main/res/layout/item_checklist_editor.xml
new file mode 100644
index 0000000..3afec45
--- /dev/null
+++ b/android/app/src/main/res/layout/item_checklist_editor.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/layout/item_note.xml b/android/app/src/main/res/layout/item_note.xml
index 97d882c..c60c229 100644
--- a/android/app/src/main/res/layout/item_note.xml
+++ b/android/app/src/main/res/layout/item_note.xml
@@ -1,8 +1,10 @@
+
-
-
+
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 36c1114..b390b2e 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -24,6 +24,7 @@
Note Title
Note content preview…
Vor 2 Std
+ Ohne Titel
Notiz löschen?
@@ -42,10 +43,9 @@
Sync-Einstellungen
- Heim-WLAN SSID
Auto-Sync aktiviert
Sync-Status
- ℹ️ Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert nur im selben Netzwerk\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)
+ ℹ️ Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)
Backup & Wiederherstellung
@@ -66,4 +66,27 @@
ℹ️ Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.
+
+
+
+
+
+
+ Notiz
+ Liste
+
+
+ Neue Liste
+ Liste bearbeiten
+ Element hinzufügen
+ Neues Element…
+ Element verschieben
+ Element löschen
+ Notiz ist leer
+ Notiz gespeichert
+ Notiz gelöscht
+
+
+ %1$d/%2$d erledigt
+ Keine Einträge
diff --git a/docs/DOCS.en.md b/docs/DOCS.en.md
index b353f90..5adeeba 100644
--- a/docs/DOCS.en.md
+++ b/docs/DOCS.en.md
@@ -78,7 +78,7 @@ val syncRequest = PeriodicWorkRequestBuilder(
### Network Detection
-Instead of SSID-based detection (Android 13+ privacy issues), we use **Gateway IP Comparison**:
+We use **Gateway IP Comparison** to check if the server is reachable:
```kotlin
fun isInHomeNetwork(): Boolean {
@@ -127,7 +127,7 @@ The app uses **4 different sync triggers** with different use cases:
| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes |
| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ Yes |
| **3. Background Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Every 15/30/60 minutes (configurable) | ✅ Yes |
-| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi enabled/SSID changed | ✅ Yes |
+| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi connected | ✅ Yes |
### Server Reachability Check (Pre-Check)
@@ -168,7 +168,7 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
| Manual Sync | Toast: "Server not reachable" | Toast: "✅ Synced: X notes" | None |
| Auto-Sync (onResume) | Silent abort (no toast) | Toast: "✅ Synced: X notes" | Max. 1x/min |
| Background Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | 15/30/60 min |
-| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | SSID-based |
+| WiFi-Connect Sync | Silent abort (no toast) | Silent (LocalBroadcast only) | WiFi-based |
---
@@ -349,7 +349,7 @@ The app requires **minimal permissions**:
```
**No Location Permissions!**
-Earlier versions required `ACCESS_FINE_LOCATION` for SSID detection. Now we use Gateway IP Comparison.
+We use Gateway IP Comparison instead of SSID detection. No location permission required.
---
diff --git a/docs/DOCS.md b/docs/DOCS.md
index 1f1fe17..b34be0d 100644
--- a/docs/DOCS.md
+++ b/docs/DOCS.md
@@ -78,7 +78,7 @@ val syncRequest = PeriodicWorkRequestBuilder(
### Network Detection
-Statt SSID-basierter Erkennung (Android 13+ Privacy-Probleme) verwenden wir **Gateway IP Comparison**:
+Wir verwenden **Gateway IP Comparison** um zu prüfen, ob der Server erreichbar ist:
```kotlin
fun isInHomeNetwork(): Boolean {
@@ -127,7 +127,7 @@ Die App verwendet **4 verschiedene Sync-Trigger** mit unterschiedlichen Anwendun
| **1. Manueller Sync** | `MainActivity.kt` | `triggerManualSync()` | User klickt auf Sync-Button im Menü | ✅ Ja |
| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App wird geöffnet/fortgesetzt | ✅ Ja |
| **3. Hintergrund-Sync (Periodic)** | `SyncWorker.kt` | `doWork()` | Alle 15/30/60 Minuten (konfigurierbar) | ✅ Ja |
-| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi an/SSID-Wechsel | ✅ Ja |
+| **4. WiFi-Connect Sync** | `NetworkMonitor.kt` → `SyncWorker.kt` | `triggerWifiConnectSync()` | WiFi verbunden | ✅ Ja |
### Server-Erreichbarkeits-Check (Pre-Check)
@@ -168,7 +168,7 @@ suspend fun isServerReachable(): Boolean = withContext(Dispatchers.IO) {
| Manueller Sync | Toast: "Server nicht erreichbar" | Toast: "✅ Gesynct: X Notizen" | Keins |
| Auto-Sync (onResume) | Silent abort (kein Toast) | Toast: "✅ Gesynct: X Notizen" | Max. 1x/Min |
| Hintergrund-Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | 15/30/60 Min |
-| WiFi-Connect Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | SSID-basiert |
+| WiFi-Connect Sync | Silent abort (kein Toast) | Silent (LocalBroadcast only) | WiFi-basiert |
---
@@ -349,7 +349,7 @@ Die App benötigt **minimale Permissions**:
```
**Keine Location Permissions!**
-Frühere Versionen benötigten `ACCESS_FINE_LOCATION` für SSID-Erkennung. Jetzt verwenden wir Gateway IP Comparison.
+Wir verwenden Gateway IP Comparison statt SSID-Erkennung. Keine Standortberechtigung nötig.
---
diff --git a/docs/FEATURES.en.md b/docs/FEATURES.en.md
index b55857b..0fbdb8f 100644
--- a/docs/FEATURES.en.md
+++ b/docs/FEATURES.en.md
@@ -8,8 +8,16 @@
## 📝 Note Management
+### Note Types
+- ✅ **Text notes** - Classic free-form notes
+- ✅ **Checklists** _(NEW in v1.4.0)_ - Task lists with tap-to-check
+ - ➕ Add items via input field
+ - ☑️ Tap to check/uncheck
+ - 📌 Long-press for drag & drop sorting
+ - 🗑️ Swipe-to-delete individual items
+ - ~~Strikethrough~~ for completed entries
+
### Basic Features
-- ✅ **Simple text notes** - Focus on content, no distractions
- ✅ **Auto-save** - No manual saving needed
- ✅ **Title + content** - Clear structure for each note
- ✅ **Timestamps** - Creation and modification date automatically
@@ -52,9 +60,11 @@
### Markdown Export
- ✅ **Automatic export** - Each note → `.md` file
+- ✅ **Checklists as task lists** _(NEW)_ - `- [ ]` / `- [x]` format (GitHub-compatible)
- ✅ **Dual-format** - JSON (master) + Markdown (mirror)
- ✅ **Filename sanitization** - Safe filenames from titles
-- ✅ **Frontmatter metadata** - YAML with ID, timestamps, tags
+- ✅ **Duplicate handling** _(NEW)_ - ID suffix for same titles
+- ✅ **Frontmatter metadata** - YAML with ID, timestamps, type
- ✅ **WebDAV sync** - Parallel to JSON sync
- ✅ **Optional** - Toggle in settings
- ✅ **Initial export** - All existing notes when activated
@@ -81,16 +91,16 @@
### Auto-Sync
- ✅ **Interval selection** - 15, 30 or 60 minutes
-- ✅ **WiFi binding** - Only in configured home WiFi
+- ✅ **WiFi trigger** - Sync on WiFi connection _(no SSID restriction)_
- ✅ **Battery-friendly** - ~0.2-0.8% per day
-- ✅ **Smart server check** - No errors on foreign networks
+- ✅ **Smart server check** - Sync only when server is reachable
- ✅ **WorkManager** - Reliable background execution
- ✅ **Battery optimization compatible** - Works even with Doze mode
### Sync Triggers (6 total)
1. ✅ **Periodic sync** - Automatically after interval
2. ✅ **App-start sync** - When opening the app
-3. ✅ **WiFi-connect sync** - When home WiFi connects
+3. ✅ **WiFi-connect sync** - On any WiFi connection
4. ✅ **Manual sync** - Button in settings
5. ✅ **Pull-to-refresh** - Swipe gesture in notes list
6. ✅ **Settings-save sync** - After server configuration
@@ -109,7 +119,6 @@
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
- ✅ **Username/password** - Basic authentication
- ✅ **Connection test** - Test in settings
-- ✅ **Gateway SSID** - WiFi name for auto-sync
- ✅ **Server URL normalization** - Automatic `/notes/` and `/notes-md/` _(NEW in v1.2.1)_
- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/`
@@ -142,7 +151,7 @@
### Battery Efficiency
- ✅ **Optimized sync intervals** - 15/30/60 min
- ✅ **WiFi-only** - No mobile data sync
-- ✅ **Smart server check** - Only in home WiFi
+- ✅ **Smart server check** - Sync only when server is reachable
- ✅ **WorkManager** - System-optimized execution
- ✅ **Doze mode compatible** - Sync runs even in standby
- ✅ **Measured consumption:**
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index 62b5f27..c00d7ba 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -8,8 +8,16 @@
## 📝 Notiz-Verwaltung
+### Notiz-Typen
+- ✅ **Textnotizen** - Klassische Freitext-Notizen
+- ✅ **Checklisten** _(NEU in v1.4.0)_ - Aufgabenlisten mit Tap-to-Check
+ - ➕ Items hinzufügen über Eingabefeld
+ - ☑️ Tap zum Abhaken/Wieder-Öffnen
+ - 📌 Long-Press für Drag & Drop Sortierung
+ - 🗑️ Swipe-to-Delete für einzelne Items
+ - ~~Durchstreichen~~ bei erledigten Einträgen
+
### Basis-Funktionen
-- ✅ **Einfache Textnotizen** - Fokus auf Inhalt, keine Ablenkung
- ✅ **Automatisches Speichern** - Kein manuelles Speichern nötig
- ✅ **Titel + Inhalt** - Klare Struktur für jede Notiz
- ✅ **Zeitstempel** - Erstellungs- und Änderungsdatum automatisch
@@ -52,9 +60,11 @@
### Markdown-Export
- ✅ **Automatischer Export** - Jede Notiz → `.md` Datei
+- ✅ **Checklisten als Task-Listen** _(NEU)_ - `- [ ]` / `- [x]` Format (GitHub-kompatibel)
- ✅ **Dual-Format** - JSON (Master) + Markdown (Mirror)
- ✅ **Dateinamen-Sanitization** - Sichere Dateinamen aus Titeln
-- ✅ **Frontmatter-Metadata** - YAML mit ID, Timestamps, Tags
+- ✅ **Duplikat-Handling** _(NEU)_ - ID-Suffix bei gleichen Titeln
+- ✅ **Frontmatter-Metadata** - YAML mit ID, Timestamps, Type
- ✅ **WebDAV-Sync** - Parallel zum JSON-Sync
- ✅ **Optional** - In Einstellungen ein/ausschaltbar
- ✅ **Initial Export** - Alle bestehenden Notizen beim Aktivieren
@@ -81,16 +91,16 @@
### Auto-Sync
- ✅ **Intervall-Auswahl** - 15, 30 oder 60 Minuten
-- ✅ **WLAN-Bindung** - Nur im konfigurierten Heim-WLAN
+- ✅ **WiFi-Trigger** - Sync bei WiFi-Verbindung _(keine SSID-Einschränkung)_
- ✅ **Akkuschonend** - ~0.2-0.8% pro Tag
-- ✅ **Smart Server-Check** - Keine Fehler in fremden Netzwerken
+- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar
- ✅ **WorkManager** - Zuverlässige Background-Ausführung
- ✅ **Battery-Optimierung kompatibel** - Funktioniert auch mit Doze Mode
### Sync-Trigger (6 Stück)
1. ✅ **Periodic Sync** - Automatisch nach Intervall
2. ✅ **App-Start Sync** - Beim Öffnen der App
-3. ✅ **WiFi-Connect Sync** - Wenn Heim-WLAN verbindet
+3. ✅ **WiFi-Connect Sync** - Bei jeder WiFi-Verbindung
4. ✅ **Manual Sync** - Button in Einstellungen
5. ✅ **Pull-to-Refresh** - Wisch-Geste in Notizliste
6. ✅ **Settings-Save Sync** - Nach Server-Konfiguration
@@ -109,7 +119,6 @@
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
- ✅ **Username/Password** - Basic Authentication
- ✅ **Connection Test** - In Einstellungen testen
-- ✅ **Gateway SSID** - WLAN-Name für Auto-Sync
- ✅ **Server-URL Normalisierung** - Automatisches `/notes/` und `/notes-md/` _(NEU in v1.2.1)_
- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/`
@@ -141,8 +150,8 @@
### Akku-Effizienz
- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min
-- ✅ **WLAN-Only** - Kein Mobile Data Sync
-- ✅ **Smart Server-Check** - Nur im Heim-WLAN
+- ✅ **WiFi-Only** - Kein Mobile Data Sync
+- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar
- ✅ **WorkManager** - System-optimierte Ausführung
- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby
- ✅ **Gemessener Verbrauch:**
diff --git a/fastlane/metadata/android/de-DE/changelogs/11.txt b/fastlane/metadata/android/de-DE/changelogs/11.txt
new file mode 100644
index 0000000..86ac44c
--- /dev/null
+++ b/fastlane/metadata/android/de-DE/changelogs/11.txt
@@ -0,0 +1,12 @@
+NEU: Checklisten!
+- Erstelle Checklisten-Notizen mit Tap-to-Check
+- Markdown-Export als GitHub-Style Aufgabenlisten
+
+Fixes:
+- Robusteres Markdown-Parsing
+- Doppelte Dateinamen bekommen ID-Suffix
+- Keine Benachrichtigungen wenn App offen ist
+
+Privacy:
+- 2 WiFi-Permissions entfernt (ACCESS/CHANGE_WIFI_STATE)
+- WiFi-Binding funktioniert bereits ohne SSID-Zugriff!
diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt
index 0239a9a..8065c9e 100644
--- a/fastlane/metadata/android/de-DE/full_description.txt
+++ b/fastlane/metadata/android/de-DE/full_description.txt
@@ -2,16 +2,15 @@ Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisatio
HAUPTFUNKTIONEN:
-• Einfache Notizen erstellen und bearbeiten
+• Text-Notizen und Checklisten erstellen
+• Checklisten mit Tap-to-Check, Drag & Drop, Swipe-to-Delete
• WebDAV-Synchronisation mit eigenem Server
• Multi-Device Sync (Handy, Tablet, Desktop)
• Markdown-Export für Obsidian/Desktop-Editoren
+• Checklisten als GitHub-Style Task-Listen exportieren
• Automatische Synchronisation im Heim-WLAN
• Konfigurierbares Sync-Interval (15/30/60 Minuten)
-• Transparente Batterie-Verbrauchsanzeige
• Material Design 3 mit Dynamic Colors (Android 12+)
-• Swipe-to-Delete mit Server-Sync
-• Server-Backup & Wiederherstellung (Merge/Replace/Overwrite)
• Komplett offline nutzbar
• Keine Werbung, keine Tracker
diff --git a/fastlane/metadata/android/de-DE/short_description.txt b/fastlane/metadata/android/de-DE/short_description.txt
index 1dce9c4..b694185 100644
--- a/fastlane/metadata/android/de-DE/short_description.txt
+++ b/fastlane/metadata/android/de-DE/short_description.txt
@@ -1 +1 @@
-Einfache Notizen-App mit WebDAV-Synchronisation
\ No newline at end of file
+Notizen & Checklisten mit WebDAV-Sync zu deinem eigenen Server
\ No newline at end of file
diff --git a/fastlane/metadata/android/en-US/changelogs/11.txt b/fastlane/metadata/android/en-US/changelogs/11.txt
new file mode 100644
index 0000000..602060f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/11.txt
@@ -0,0 +1,12 @@
+NEW: Checklists!
+- Create checklist notes with tap-to-check items
+- Markdown export as GitHub-style task lists
+
+Fixes:
+- More robust Markdown parsing
+- Duplicate filenames get ID suffix
+- No notifications when app is open
+
+Privacy:
+- Removed 2 WiFi permissions (ACCESS/CHANGE_WIFI_STATE)
+- WiFi binding already works without SSID access!
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index a5197a5..92257a2 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -2,16 +2,15 @@ Simple Notes Sync is a minimalist note-taking app with WebDAV synchronization.
KEY FEATURES:
-• Create and edit simple notes
+• Create text notes and checklists
+• Checklists with tap-to-check, drag & drop, swipe-to-delete
• WebDAV synchronization with your own server
• Multi-device sync (phone, tablet, desktop)
• Markdown export for Obsidian/desktop editors
+• Checklists export as GitHub-style task lists
• Automatic synchronization on home WiFi
• Configurable sync interval (15/30/60 minutes)
-• Transparent battery usage display
• Material Design 3 with Dynamic Colors (Android 12+)
-• Swipe-to-delete with server sync
-• Server backup & restore (Merge/Replace/Overwrite)
• Fully usable offline
• No ads, no trackers
diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt
index 4c3c773..f149f8e 100644
--- a/fastlane/metadata/android/en-US/short_description.txt
+++ b/fastlane/metadata/android/en-US/short_description.txt
@@ -1 +1 @@
-Simple note-taking app with WebDAV synchronization
+Notes & checklists with WebDAV sync to your own server
diff --git a/metadata/dev.dettmer.simplenotes.yml b/metadata/dev.dettmer.simplenotes.yml
index afc526c..56332b5 100644
--- a/metadata/dev.dettmer.simplenotes.yml
+++ b/metadata/dev.dettmer.simplenotes.yml
@@ -133,7 +133,21 @@ Builds:
scandelete:
- android/gradle/wrapper
+ - versionName: 1.4.0
+ versionCode: 11
+ commit: v1.4.0
+ 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.3.2
-CurrentVersionCode: 10
+CurrentVersion: 1.4.0
+CurrentVersionCode: 11