feat(v1.4.0): Checklists feature + WiFi permission cleanup
Features: - Interactive checklists with tap-to-check, drag & drop sorting - GitHub-flavored Markdown export (- [ ] / - [x]) - FAB menu for note type selection Fixes: - Improved Markdown parsing (robust line-based content extraction) - Better duplicate filename handling (ID suffix) - Foreground notification suppression Privacy: - Removed ACCESS_WIFI_STATE and CHANGE_WIFI_STATE permissions (SSID binding was never used, app only checks connectivity state) Code Quality: - Fixed 7 Detekt warnings (SwallowedException, MaxLineLength, MagicNumber)
This commit is contained in:
57
CHANGELOG.md
57
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
|
||||||
|
|
||||||
|
- **<2A> 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
|
## [1.3.2] - 2026-01-10
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
|
|||||||
| **WebDAV Server URL** | `http://YOUR-SERVER-IP:8080/` |
|
| **WebDAV Server URL** | `http://YOUR-SERVER-IP:8080/` |
|
||||||
| **Username** | `noteuser` |
|
| **Username** | `noteuser` |
|
||||||
| **Password** | (your password from `.env`) |
|
| **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.
|
> **💡 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"
|
# Should show "Up"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Same WiFi?**
|
2. **Same network?**
|
||||||
- Smartphone and server must be on same network
|
- Smartphone and server must be on same network
|
||||||
- Check SSID in app settings
|
|
||||||
|
|
||||||
3. **IP address correct?**
|
3. **IP address correct?**
|
||||||
```bash
|
```bash
|
||||||
@@ -193,9 +191,9 @@ For reliable auto-sync:
|
|||||||
2. **Battery optimization disabled?**
|
2. **Battery optimization disabled?**
|
||||||
- See [Disable Battery Optimization](#-disable-battery-optimization)
|
- See [Disable Battery Optimization](#-disable-battery-optimization)
|
||||||
|
|
||||||
3. **On correct WiFi?**
|
3. **Connected to WiFi?**
|
||||||
- Sync only works when SSID = Gateway SSID
|
- Auto-sync triggers on any WiFi connection
|
||||||
- Check current SSID in Android settings
|
- Check if you're connected to a WiFi network
|
||||||
|
|
||||||
4. **Test manually:**
|
4. **Test manually:**
|
||||||
- ⚙️ Settings → "Sync now"
|
- ⚙️ Settings → "Sync now"
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ ip addr show | grep "inet " | grep -v 127.0.0.1
|
|||||||
| **WebDAV Server URL** | `http://DEINE-SERVER-IP:8080/` |
|
| **WebDAV Server URL** | `http://DEINE-SERVER-IP:8080/` |
|
||||||
| **Benutzername** | `noteuser` |
|
| **Benutzername** | `noteuser` |
|
||||||
| **Passwort** | (dein Passwort aus `.env`) |
|
| **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.
|
> **💡 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
|
# Sollte "Up" zeigen
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Gleiche WLAN?**
|
2. **Gleiches Netzwerk?**
|
||||||
- Smartphone und Server müssen im selben Netzwerk sein
|
- Smartphone und Server müssen im selben Netzwerk sein
|
||||||
- Prüfe SSID in App-Einstellungen
|
|
||||||
|
|
||||||
3. **IP-Adresse korrekt?**
|
3. **IP-Adresse korrekt?**
|
||||||
```bash
|
```bash
|
||||||
@@ -193,9 +191,9 @@ Für zuverlässigen Auto-Sync:
|
|||||||
2. **Akku-Optimierung deaktiviert?**
|
2. **Akku-Optimierung deaktiviert?**
|
||||||
- Siehe [Akku-Optimierung](#-akku-optimierung-deaktivieren)
|
- Siehe [Akku-Optimierung](#-akku-optimierung-deaktivieren)
|
||||||
|
|
||||||
3. **Im richtigen WLAN?**
|
3. **Mit WiFi verbunden?**
|
||||||
- Sync funktioniert nur wenn SSID = Gateway SSID
|
- Auto-Sync triggert bei jeder WiFi-Verbindung
|
||||||
- Prüfe aktuelle SSID in Android-Einstellungen
|
- Prüfe, ob du mit einem WLAN verbunden bist
|
||||||
|
|
||||||
4. **Manuell testen:**
|
4. **Manuell testen:**
|
||||||
- ⚙️ Einstellungen → "Jetzt synchronisieren"
|
- ⚙️ Einstellungen → "Jetzt synchronisieren"
|
||||||
|
|||||||
@@ -26,11 +26,12 @@
|
|||||||
|
|
||||||
## ✨ Highlights
|
## ✨ Highlights
|
||||||
|
|
||||||
|
- ✅ **NEW: Checklists** - Tap-to-check, drag & drop, swipe-to-delete
|
||||||
- 📝 **Offline-first** - Works without internet
|
- 📝 **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)
|
- 🔒 **Self-hosted** - Your data stays with you (WebDAV)
|
||||||
- 💾 **Local backup** - Export/Import as JSON file
|
- 💾 **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
|
- 🔋 **Battery-friendly** - ~0.2-0.8% per day
|
||||||
- 🎨 **Material Design 3** - Dark mode & dynamic colors
|
- 🎨 **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
|
||||||
|
|||||||
@@ -26,11 +26,12 @@
|
|||||||
|
|
||||||
## ✨ Highlights
|
## ✨ Highlights
|
||||||
|
|
||||||
|
- ✅ **NEU: Checklisten** - Tap-to-Check, Drag & Drop, Swipe-to-Delete
|
||||||
- 📝 **Offline-First** - Funktioniert ohne Internet
|
- 📝 **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)
|
- 🔒 **Self-Hosted** - Deine Daten bleiben bei dir (WebDAV)
|
||||||
- 💾 **Lokales Backup** - Export/Import als JSON-Datei
|
- 💾 **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
|
- 🔋 **Akkuschonend** - ~0.2-0.8% pro Tag
|
||||||
- 🎨 **Material Design 3** - Dark Mode & Dynamic Colors
|
- 🎨 **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
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ android {
|
|||||||
applicationId = "dev.dettmer.simplenotes"
|
applicationId = "dev.dettmer.simplenotes"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 10 // 🚀 v1.3.2: Lint-Cleanup "Clean Slate"
|
versionCode = 11 // 🚀 v1.4.0: Checklists Feature
|
||||||
versionName = "1.3.2" // 🚀 v1.3.2: Code-Qualität-Release (alle einfachen Lint-Issues behoben)
|
versionName = "1.4.0" // 🚀 v1.4.0: Checklists, Multi-Device Sync Fixes, UX Improvements
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
<!-- Network & Sync Permissions -->
|
<!-- Network & Sync Permissions -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- Notifications -->
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
|||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
@@ -551,12 +554,55 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}).show()
|
}).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.4.0: Setup FAB mit Dropdown für Notiz-Typ Auswahl
|
||||||
|
*/
|
||||||
private fun setupFab() {
|
private fun setupFab() {
|
||||||
fabAddNote.setOnClickListener {
|
fabAddNote.setOnClickListener { view ->
|
||||||
openNoteEditor(null)
|
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() {
|
private fun loadNotes() {
|
||||||
val notes = storage.loadAllNotes()
|
val notes = storage.loadAllNotes()
|
||||||
|
|
||||||
|
|||||||
@@ -3,27 +3,58 @@ package dev.dettmer.simplenotes
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
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.appbar.MaterialToolbar
|
||||||
|
import com.google.android.material.button.MaterialButton
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
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.Note
|
||||||
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.models.SyncStatus
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||||
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
import dev.dettmer.simplenotes.utils.DeviceIdGenerator
|
||||||
|
import dev.dettmer.simplenotes.utils.Logger
|
||||||
import dev.dettmer.simplenotes.utils.showToast
|
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() {
|
class NoteEditorActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
// Views
|
||||||
|
private lateinit var toolbar: MaterialToolbar
|
||||||
|
private lateinit var tilTitle: TextInputLayout
|
||||||
private lateinit var editTextTitle: TextInputEditText
|
private lateinit var editTextTitle: TextInputEditText
|
||||||
|
private lateinit var tilContent: TextInputLayout
|
||||||
private lateinit var editTextContent: TextInputEditText
|
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
|
private lateinit var storage: NotesStorage
|
||||||
|
|
||||||
|
// State
|
||||||
private var existingNote: Note? = null
|
private var existingNote: Note? = null
|
||||||
|
private var currentNoteType: NoteType = NoteType.TEXT
|
||||||
|
private val checklistItems = mutableListOf<ChecklistItem>()
|
||||||
|
private var checklistAdapter: ChecklistEditorAdapter? = null
|
||||||
|
private var itemTouchHelper: ItemTouchHelper? = null
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "NoteEditorActivity"
|
||||||
const val EXTRA_NOTE_ID = "extra_note_id"
|
const val EXTRA_NOTE_ID = "extra_note_id"
|
||||||
|
const val EXTRA_NOTE_TYPE = "extra_note_type"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -36,39 +67,172 @@ class NoteEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
storage = NotesStorage(this)
|
storage = NotesStorage(this)
|
||||||
|
|
||||||
// Setup toolbar
|
findViews()
|
||||||
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
|
setupToolbar()
|
||||||
setSupportActionBar(toolbar)
|
loadNoteOrDetermineType()
|
||||||
supportActionBar?.apply {
|
setupUIForNoteType()
|
||||||
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
|
private fun findViews() {
|
||||||
|
toolbar = findViewById(R.id.toolbar)
|
||||||
|
tilTitle = findViewById(R.id.tilTitle)
|
||||||
editTextTitle = findViewById(R.id.editTextTitle)
|
editTextTitle = findViewById(R.id.editTextTitle)
|
||||||
|
tilContent = findViewById(R.id.tilContent)
|
||||||
editTextContent = findViewById(R.id.editTextContent)
|
editTextContent = findViewById(R.id.editTextContent)
|
||||||
|
checklistContainer = findViewById(R.id.checklistContainer)
|
||||||
|
rvChecklistItems = findViewById(R.id.rvChecklistItems)
|
||||||
|
btnAddItem = findViewById(R.id.btnAddItem)
|
||||||
|
}
|
||||||
|
|
||||||
// Load existing note if editing
|
private fun setupToolbar() {
|
||||||
|
setSupportActionBar(toolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadNoteOrDetermineType() {
|
||||||
val noteId = intent.getStringExtra(EXTRA_NOTE_ID)
|
val noteId = intent.getStringExtra(EXTRA_NOTE_ID)
|
||||||
|
|
||||||
if (noteId != null) {
|
if (noteId != null) {
|
||||||
|
// Existierende Notiz laden
|
||||||
existingNote = storage.loadNote(noteId)
|
existingNote = storage.loadNote(noteId)
|
||||||
existingNote?.let {
|
existingNote?.let { note ->
|
||||||
editTextTitle.setText(it.title)
|
editTextTitle.setText(note.title)
|
||||||
editTextContent.setText(it.content)
|
currentNoteType = note.noteType
|
||||||
supportActionBar?.title = "Notiz bearbeiten"
|
|
||||||
|
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 {
|
} 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 {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_editor, menu)
|
menuInflater.inflate(R.menu.menu_editor, menu)
|
||||||
|
// Delete nur für existierende Notizen
|
||||||
// Show delete only for existing notes
|
|
||||||
menu.findItem(R.id.action_delete)?.isVisible = existingNote != null
|
menu.findItem(R.id.action_delete)?.isVisible = existingNote != null
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,51 +256,96 @@ class NoteEditorActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun saveNote() {
|
private fun saveNote() {
|
||||||
val title = editTextTitle.text?.toString()?.trim() ?: ""
|
val title = editTextTitle.text?.toString()?.trim() ?: ""
|
||||||
val content = editTextContent.text?.toString()?.trim() ?: ""
|
|
||||||
|
|
||||||
if (title.isEmpty() && content.isEmpty()) {
|
when (currentNoteType) {
|
||||||
showToast("Notiz ist leer")
|
NoteType.TEXT -> {
|
||||||
return
|
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) {
|
showToast(getString(R.string.note_saved))
|
||||||
// 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")
|
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun confirmDelete() {
|
private fun confirmDelete() {
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("Notiz löschen?")
|
.setTitle(getString(R.string.delete_note_title))
|
||||||
.setMessage("Diese Aktion kann nicht rückgängig gemacht werden.")
|
.setMessage(getString(R.string.delete_note_message))
|
||||||
.setPositiveButton("Löschen") { _, _ ->
|
.setPositiveButton(getString(R.string.delete)) { _, _ ->
|
||||||
deleteNote()
|
deleteNote()
|
||||||
}
|
}
|
||||||
.setNegativeButton("Abbrechen", null)
|
.setNegativeButton(getString(R.string.cancel), null)
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteNote() {
|
private fun deleteNote() {
|
||||||
existingNote?.let {
|
existingNote?.let {
|
||||||
storage.deleteNote(it.id)
|
storage.deleteNote(it.id)
|
||||||
showToast("Notiz gelöscht")
|
showToast(getString(R.string.note_deleted))
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ChecklistItem>,
|
||||||
|
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<ChecklistEditorAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,11 +11,17 @@ import androidx.recyclerview.widget.ListAdapter
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dev.dettmer.simplenotes.R
|
import dev.dettmer.simplenotes.R
|
||||||
import dev.dettmer.simplenotes.models.Note
|
import dev.dettmer.simplenotes.models.Note
|
||||||
|
import dev.dettmer.simplenotes.models.NoteType
|
||||||
import dev.dettmer.simplenotes.models.SyncStatus
|
import dev.dettmer.simplenotes.models.SyncStatus
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import dev.dettmer.simplenotes.utils.toReadableTime
|
import dev.dettmer.simplenotes.utils.toReadableTime
|
||||||
import dev.dettmer.simplenotes.utils.truncate
|
import dev.dettmer.simplenotes.utils.truncate
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter für die Notizen-Liste
|
||||||
|
*
|
||||||
|
* v1.4.0: Unterstützt jetzt TEXT und CHECKLIST Notizen
|
||||||
|
*/
|
||||||
class NotesAdapter(
|
class NotesAdapter(
|
||||||
private val onNoteClick: (Note) -> Unit
|
private val onNoteClick: (Note) -> Unit
|
||||||
) : ListAdapter<Note, NotesAdapter.NoteViewHolder>(NoteDiffCallback()) {
|
) : ListAdapter<Note, NotesAdapter.NoteViewHolder>(NoteDiffCallback()) {
|
||||||
@@ -31,16 +37,46 @@ class NotesAdapter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner class NoteViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
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 textViewTitle: TextView = itemView.findViewById(R.id.textViewTitle)
|
||||||
private val textViewContent: TextView = itemView.findViewById(R.id.textViewContent)
|
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 textViewTimestamp: TextView = itemView.findViewById(R.id.textViewTimestamp)
|
||||||
private val imageViewSyncStatus: ImageView = itemView.findViewById(R.id.imageViewSyncStatus)
|
private val imageViewSyncStatus: ImageView = itemView.findViewById(R.id.imageViewSyncStatus)
|
||||||
|
|
||||||
fun bind(note: Note) {
|
fun bind(note: Note) {
|
||||||
textViewTitle.text = note.title.ifEmpty { "Ohne Titel" }
|
// Titel
|
||||||
textViewContent.text = note.content.truncate(100)
|
textViewTitle.text = note.title.ifEmpty {
|
||||||
|
itemView.context.getString(R.string.untitled)
|
||||||
|
}
|
||||||
textViewTimestamp.text = note.updatedAt.toReadableTime()
|
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
|
// Sync Icon nur zeigen wenn Sync konfiguriert ist
|
||||||
val prefs = itemView.context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = itemView.context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,56 +14,125 @@ data class Note(
|
|||||||
val createdAt: Long = System.currentTimeMillis(),
|
val createdAt: Long = System.currentTimeMillis(),
|
||||||
val updatedAt: Long = System.currentTimeMillis(),
|
val updatedAt: Long = System.currentTimeMillis(),
|
||||||
val deviceId: String,
|
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<ChecklistItem>? = null
|
||||||
) {
|
) {
|
||||||
|
/**
|
||||||
|
* Serialisiert Note zu JSON (v1.4.0: Nutzt Gson für komplexe Strukturen)
|
||||||
|
*/
|
||||||
fun toJson(): String {
|
fun toJson(): String {
|
||||||
return """
|
val gson = com.google.gson.GsonBuilder()
|
||||||
{
|
.setPrettyPrinting()
|
||||||
"id": "$id",
|
.create()
|
||||||
"title": "${title.escapeJson()}",
|
return gson.toJson(this)
|
||||||
"content": "${content.escapeJson()}",
|
|
||||||
"createdAt": $createdAt,
|
|
||||||
"updatedAt": $updatedAt,
|
|
||||||
"deviceId": "$deviceId",
|
|
||||||
"syncStatus": "${syncStatus.name}"
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Konvertiert Note zu Markdown mit YAML Frontmatter (Task #1.2.0-08)
|
* Konvertiert Note zu Markdown mit YAML Frontmatter (Task #1.2.0-08)
|
||||||
* Format kompatibel mit Obsidian, Joplin, Typora
|
* Format kompatibel mit Obsidian, Joplin, Typora
|
||||||
|
* v1.4.0: Unterstützt jetzt auch Checklisten-Format
|
||||||
*/
|
*/
|
||||||
fun toMarkdown(): String {
|
fun toMarkdown(): String {
|
||||||
return """
|
val header = """
|
||||||
---
|
---
|
||||||
id: $id
|
id: $id
|
||||||
created: ${formatISO8601(createdAt)}
|
created: ${formatISO8601(createdAt)}
|
||||||
updated: ${formatISO8601(updatedAt)}
|
updated: ${formatISO8601(updatedAt)}
|
||||||
device: $deviceId
|
device: $deviceId
|
||||||
|
type: ${noteType.name.lowercase()}
|
||||||
---
|
---
|
||||||
|
|
||||||
# $title
|
# $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 {
|
companion object {
|
||||||
private const val TAG = "Note"
|
private const val TAG = "Note"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst JSON zu Note-Objekt mit Backward Compatibility für alte Notizen ohne noteType
|
||||||
|
*/
|
||||||
fun fromJson(json: String): Note? {
|
fun fromJson(json: String): Note? {
|
||||||
return try {
|
return try {
|
||||||
val gson = com.google.gson.Gson()
|
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<List<ChecklistItem>>() {}.type
|
||||||
|
val checklistItems = if (jsonObject.has("checklistItems") &&
|
||||||
|
!jsonObject.get("checklistItems").isJsonNull
|
||||||
|
) {
|
||||||
|
gson.fromJson<List<ChecklistItem>>(
|
||||||
|
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) {
|
} catch (e: Exception) {
|
||||||
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
|
Logger.w(TAG, "Failed to parse JSON: ${e.message}")
|
||||||
null
|
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)
|
* 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
|
* @param md Markdown-String mit YAML Frontmatter
|
||||||
* @return Note-Objekt oder null bei Parse-Fehler
|
* @return Note-Objekt oder null bei Parse-Fehler
|
||||||
@@ -91,10 +160,47 @@ $content
|
|||||||
.firstOrNull { it.startsWith("# ") }
|
.firstOrNull { it.startsWith("# ") }
|
||||||
?.removePrefix("# ")?.trim() ?: "Untitled"
|
?.removePrefix("# ")?.trim() ?: "Untitled"
|
||||||
|
|
||||||
// Extract content (everything after heading)
|
// v1.4.0: Prüfe ob type: checklist im Frontmatter
|
||||||
val content = contentBlock
|
val noteTypeStr = metadata["type"]?.lowercase() ?: "text"
|
||||||
.substringAfter("# $title\n\n", "")
|
val noteType = when (noteTypeStr) {
|
||||||
.trim()
|
"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<ChecklistItem>?
|
||||||
|
|
||||||
|
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(
|
Note(
|
||||||
id = metadata["id"] ?: UUID.randomUUID().toString(),
|
id = metadata["id"] ?: UUID.randomUUID().toString(),
|
||||||
@@ -103,7 +209,9 @@ $content
|
|||||||
createdAt = parseISO8601(metadata["created"] ?: ""),
|
createdAt = parseISO8601(metadata["created"] ?: ""),
|
||||||
updatedAt = parseISO8601(metadata["updated"] ?: ""),
|
updatedAt = parseISO8601(metadata["updated"] ?: ""),
|
||||||
deviceId = metadata["device"] ?: "desktop",
|
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) {
|
} catch (e: Exception) {
|
||||||
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
|
Logger.w(TAG, "Failed to parse Markdown: ${e.message}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.dettmer.simplenotes.sync
|
package dev.dettmer.simplenotes.sync
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
@@ -22,6 +23,21 @@ class SyncWorker(
|
|||||||
const val ACTION_SYNC_COMPLETED = "dev.dettmer.simplenotes.SYNC_COMPLETED"
|
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) {
|
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Logger.d(TAG, "═══════════════════════════════════════")
|
Logger.d(TAG, "═══════════════════════════════════════")
|
||||||
@@ -131,14 +147,20 @@ class SyncWorker(
|
|||||||
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||||
|
|
||||||
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
|
// 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 (result.syncedCount > 0) {
|
||||||
if (BuildConfig.DEBUG) {
|
val appInForeground = isAppInForeground()
|
||||||
Logger.d(TAG, " Showing success notification...")
|
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 {
|
} else {
|
||||||
Logger.d(TAG, "ℹ️ No changes to sync - no notification")
|
Logger.d(TAG, "ℹ️ No changes to sync - no notification")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
private const val SOCKET_TIMEOUT_MS = 2000
|
private const val SOCKET_TIMEOUT_MS = 2000
|
||||||
private const val MAX_FILENAME_LENGTH = 200
|
private const val MAX_FILENAME_LENGTH = 200
|
||||||
private const val ETAG_PREVIEW_LENGTH = 8
|
private const val ETAG_PREVIEW_LENGTH = 8
|
||||||
|
private const val CONTENT_PREVIEW_LENGTH = 50
|
||||||
|
|
||||||
// 🔒 v1.3.1: Mutex um parallele Syncs zu verhindern
|
// 🔒 v1.3.1: Mutex um parallele Syncs zu verhindern
|
||||||
private val syncMutex = Mutex()
|
private val syncMutex = Mutex()
|
||||||
@@ -858,8 +859,31 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize Filename (Task #1.2.0-12)
|
// Sanitize Filename (Task #1.2.0-12)
|
||||||
val filename = sanitizeFilename(note.title) + ".md"
|
val baseFilename = sanitizeFilename(note.title)
|
||||||
val noteUrl = "$mdUrl/$filename"
|
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
|
// Konvertiere zu Markdown
|
||||||
val mdContent = note.toMarkdown().toByteArray()
|
val mdContent = note.toMarkdown().toByteArray()
|
||||||
@@ -884,6 +908,29 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
.trim('_', ' ') // Trim Underscores/Spaces
|
.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>): 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)
|
* Exportiert ALLE lokalen Notizen als Markdown (Initial-Export)
|
||||||
*
|
*
|
||||||
@@ -927,6 +974,9 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
val totalCount = allNotes.size
|
val totalCount = allNotes.size
|
||||||
var exportedCount = 0
|
var exportedCount = 0
|
||||||
|
|
||||||
|
// Track used filenames to handle duplicates
|
||||||
|
val usedFilenames = mutableSetOf<String>()
|
||||||
|
|
||||||
Logger.d(TAG, "📝 Found $totalCount notes to export")
|
Logger.d(TAG, "📝 Found $totalCount notes to export")
|
||||||
|
|
||||||
allNotes.forEachIndexed { index, note ->
|
allNotes.forEachIndexed { index, note ->
|
||||||
@@ -934,8 +984,8 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
// Progress-Callback
|
// Progress-Callback
|
||||||
onProgress(index + 1, totalCount)
|
onProgress(index + 1, totalCount)
|
||||||
|
|
||||||
// Sanitize Filename
|
// Eindeutiger Filename (mit Duplikat-Handling)
|
||||||
val filename = sanitizeFilename(note.title) + ".md"
|
val filename = getUniqueMarkdownFilename(note, usedFilenames) + ".md"
|
||||||
val noteUrl = "$mdUrl/$filename"
|
val noteUrl = "$mdUrl/$filename"
|
||||||
|
|
||||||
// Konvertiere zu Markdown
|
// Konvertiere zu Markdown
|
||||||
@@ -945,7 +995,7 @@ class WebDavSyncService(private val context: Context) {
|
|||||||
sardine.put(noteUrl, mdContent, "text/markdown")
|
sardine.put(noteUrl, mdContent, "text/markdown")
|
||||||
|
|
||||||
exportedCount++
|
exportedCount++
|
||||||
Logger.d(TAG, " ✅ Exported [${index + 1}/$totalCount]: ${note.title}")
|
Logger.d(TAG, " ✅ Exported [${index + 1}/$totalCount]: ${note.title} -> $filename")
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Logger.e(TAG, "❌ Failed to export ${note.title}: ${e.message}")
|
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")
|
Logger.w(TAG, " ⚠️ Failed to parse ${resource.name} - fromMarkdown returned null")
|
||||||
continue
|
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(
|
Logger.d(
|
||||||
TAG,
|
TAG,
|
||||||
" Parsed: id=${mdNote.id}, title=${mdNote.title}, " +
|
" 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(
|
Logger.d(
|
||||||
TAG,
|
TAG,
|
||||||
" Local note: " + if (localNote == null) {
|
" Local note: " + if (localNote == null) {
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.wifi.WifiManager
|
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import dev.dettmer.simplenotes.utils.Constants
|
import dev.dettmer.simplenotes.utils.Constants
|
||||||
import java.util.concurrent.TimeUnit
|
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() {
|
class WifiSyncReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
@@ -22,34 +27,24 @@ class WifiSyncReceiver : BroadcastReceiver() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if connected to home WiFi
|
// Check if connected to any WiFi (SSID-Prüfung entfernt in v1.4.0)
|
||||||
if (isConnectedToHomeWifi(context)) {
|
if (isConnectedToWifi(context)) {
|
||||||
scheduleSyncWork(context)
|
scheduleSyncWork(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReturnCount") // Early returns for WiFi validation checks
|
/**
|
||||||
private fun isConnectedToHomeWifi(context: Context): Boolean {
|
* Prüft ob ein WiFi-Netzwerk verbunden ist (beliebiges WiFi)
|
||||||
val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
* Die Server-Erreichbarkeitsprüfung erfolgt erst im SyncWorker.
|
||||||
val homeSSID = prefs.getString(Constants.KEY_HOME_SSID, null) ?: return false
|
*/
|
||||||
|
private fun isConnectedToWifi(context: Context): Boolean {
|
||||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
|
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE)
|
||||||
as ConnectivityManager
|
as ConnectivityManager
|
||||||
|
|
||||||
val network = connectivityManager.activeNetwork ?: return false
|
val network = connectivityManager.activeNetwork ?: return false
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
|
||||||
|
|
||||||
if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scheduleSyncWork(context: Context) {
|
private fun scheduleSyncWork(context: Context) {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ object Constants {
|
|||||||
const val KEY_SERVER_URL = "server_url"
|
const val KEY_SERVER_URL = "server_url"
|
||||||
const val KEY_USERNAME = "username"
|
const val KEY_USERNAME = "username"
|
||||||
const val KEY_PASSWORD = "password"
|
const val KEY_PASSWORD = "password"
|
||||||
const val KEY_HOME_SSID = "home_ssid"
|
|
||||||
const val KEY_AUTO_SYNC = "auto_sync_enabled"
|
const val KEY_AUTO_SYNC = "auto_sync_enabled"
|
||||||
const val KEY_LAST_SYNC = "last_sync_timestamp"
|
const val KEY_LAST_SYNC = "last_sync_timestamp"
|
||||||
|
|
||||||
|
|||||||
10
android/app/src/main/res/drawable/ic_checklist_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_checklist_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnSurface">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22,7h-9v2h9V7zM22,15h-9v2h9V15zM5.54,11L2,7.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,11zM5.54,19L2,15.46l1.41,-1.41l2.12,2.12l4.24,-4.24l1.41,1.41L5.54,19z"/>
|
||||||
|
</vector>
|
||||||
10
android/app/src/main/res/drawable/ic_delete_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_delete_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnSurfaceVariant">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||||
|
</vector>
|
||||||
10
android/app/src/main/res/drawable/ic_drag_handle_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_drag_handle_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnSurfaceVariant">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,9H4v2h16V9zM4,15h16v-2H4v2z"/>
|
||||||
|
</vector>
|
||||||
10
android/app/src/main/res/drawable/ic_note_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_note_24.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorOnSurface">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M14,2H6C4.9,2 4.01,2.9 4.01,4L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8L14,2zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
|
||||||
|
</vector>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
@@ -18,8 +19,9 @@
|
|||||||
app:title="@string/edit_note"
|
app:title="@string/edit_note"
|
||||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||||
|
|
||||||
<!-- Material 3 Outlined TextInputLayout with 16dp corners -->
|
<!-- Title Input (für beide Typen) -->
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="16dp"
|
android:layout_margin="16dp"
|
||||||
@@ -44,8 +46,9 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<!-- Material 3 Outlined TextInputLayout for Content -->
|
<!-- Content Input (nur für TEXT sichtbar) -->
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/tilContent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
@@ -74,4 +77,39 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<!-- v1.4.0: Checklist Container (nur für CHECKLIST sichtbar) -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/checklistContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<!-- Checklist Items RecyclerView -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/rvChecklistItems"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:listitem="@layout/item_checklist_editor" />
|
||||||
|
|
||||||
|
<!-- Add Item Button -->
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/btnAddItem"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton.Icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:text="@string/add_item"
|
||||||
|
app:icon="@android:drawable/ic_input_add" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
57
android/app/src/main/res/layout/item_checklist_editor.xml
Normal file
57
android/app/src/main/res/layout/item_checklist_editor.xml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- v1.4.0: Checklist Item Layout für Editor -->
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:minHeight="48dp">
|
||||||
|
|
||||||
|
<!-- Drag Handle -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivDragHandle"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:src="@drawable/ic_drag_handle_24"
|
||||||
|
android:contentDescription="@string/reorder_item"
|
||||||
|
android:importantForAccessibility="yes" />
|
||||||
|
|
||||||
|
<!-- Checkbox -->
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/cbItem"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:minHeight="0dp" />
|
||||||
|
|
||||||
|
<!-- Text Input (ohne Box, nur transparent) -->
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/etItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:background="@null"
|
||||||
|
android:hint="@string/item_placeholder"
|
||||||
|
android:inputType="text"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:maxLines="3"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||||
|
tools:text="Milch kaufen" />
|
||||||
|
|
||||||
|
<!-- Delete Button -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btnDeleteItem"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/ic_delete_24"
|
||||||
|
android:contentDescription="@string/delete_item"
|
||||||
|
app:tint="?attr/colorOnSurfaceVariant" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Material 3: Filled Card Style (Flat, No Shadow) -->
|
<!-- Material 3: Filled Card Style (Flat, No Shadow) -->
|
||||||
|
<!-- v1.4.0: Unterstützt jetzt TEXT und CHECKLIST Notizen -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
@@ -17,17 +19,37 @@
|
|||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="20dp">
|
android:padding="20dp">
|
||||||
|
|
||||||
<!-- Material 3 Typography: TitleMedium -->
|
<!-- v1.4.0: Header Row mit Icon und Titel -->
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/textViewTitle"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="@string/note_title_placeholder"
|
android:orientation="horizontal"
|
||||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
android:gravity="center_vertical">
|
||||||
android:maxLines="2"
|
|
||||||
android:ellipsize="end" />
|
|
||||||
|
|
||||||
<!-- Material 3 Typography: BodyMedium -->
|
<!-- v1.4.0: Note Type Icon -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ivNoteTypeIcon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:src="@drawable/ic_note_24"
|
||||||
|
app:tint="?attr/colorPrimary"
|
||||||
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
<!-- Material 3 Typography: TitleMedium -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="@string/note_title_placeholder"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Content Preview (für TEXT Notizen) -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textViewContent"
|
android:id="@+id/textViewContent"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -39,6 +61,18 @@
|
|||||||
android:maxLines="3"
|
android:maxLines="3"
|
||||||
android:ellipsize="end" />
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<!-- v1.4.0: Checklist Preview (für CHECKLIST Notizen) -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textViewChecklistPreview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||||
|
android:textColor="?attr/colorOnSurfaceVariant"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="2/5 erledigt" />
|
||||||
|
|
||||||
<!-- Metadata Row mit Timestamp und Sync-Status -->
|
<!-- Metadata Row mit Timestamp und Sync-Status -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
11
android/app/src/main/res/menu/menu_fab_note_types.xml
Normal file
11
android/app/src/main/res/menu/menu_fab_note_types.xml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_create_text_note"
|
||||||
|
android:icon="@drawable/ic_note_24"
|
||||||
|
android:title="@string/create_text_note" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_create_checklist"
|
||||||
|
android:icon="@drawable/ic_checklist_24"
|
||||||
|
android:title="@string/create_checklist" />
|
||||||
|
</menu>
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
<string name="note_title_placeholder">Note Title</string>
|
<string name="note_title_placeholder">Note Title</string>
|
||||||
<string name="note_content_placeholder">Note content preview…</string>
|
<string name="note_content_placeholder">Note content preview…</string>
|
||||||
<string name="note_timestamp_placeholder">Vor 2 Std</string>
|
<string name="note_timestamp_placeholder">Vor 2 Std</string>
|
||||||
|
<string name="untitled">Ohne Titel</string>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog -->
|
||||||
<string name="delete_note_title">Notiz löschen?</string>
|
<string name="delete_note_title">Notiz löschen?</string>
|
||||||
@@ -42,10 +43,9 @@
|
|||||||
|
|
||||||
<!-- Auto-Sync Settings -->
|
<!-- Auto-Sync Settings -->
|
||||||
<string name="sync_settings">Sync-Einstellungen</string>
|
<string name="sync_settings">Sync-Einstellungen</string>
|
||||||
<string name="home_ssid">Heim-WLAN SSID</string>
|
|
||||||
<string name="auto_sync">Auto-Sync aktiviert</string>
|
<string name="auto_sync">Auto-Sync aktiviert</string>
|
||||||
<string name="sync_status">Sync-Status</string>
|
<string name="sync_status">Sync-Status</string>
|
||||||
<string name="auto_sync_info">ℹ️ 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)</string>
|
<string name="auto_sync_info">ℹ️ 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)</string>
|
||||||
|
|
||||||
<!-- Backup & Restore -->
|
<!-- Backup & Restore -->
|
||||||
<string name="backup_restore_title">Backup & Wiederherstellung</string>
|
<string name="backup_restore_title">Backup & Wiederherstellung</string>
|
||||||
@@ -66,4 +66,27 @@
|
|||||||
|
|
||||||
<!-- Debug/Logging Section (v1.3.2) -->
|
<!-- Debug/Logging Section (v1.3.2) -->
|
||||||
<string name="file_logging_privacy_notice">ℹ️ 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.</string>
|
<string name="file_logging_privacy_notice">ℹ️ 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.</string>
|
||||||
|
|
||||||
|
<!-- ========================== -->
|
||||||
|
<!-- CHECKLIST FEATURE (v1.4.0) -->
|
||||||
|
<!-- ========================== -->
|
||||||
|
|
||||||
|
<!-- FAB Menu -->
|
||||||
|
<string name="create_text_note">Notiz</string>
|
||||||
|
<string name="create_checklist">Liste</string>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<string name="new_checklist">Neue Liste</string>
|
||||||
|
<string name="edit_checklist">Liste bearbeiten</string>
|
||||||
|
<string name="add_item">Element hinzufügen</string>
|
||||||
|
<string name="item_placeholder">Neues Element…</string>
|
||||||
|
<string name="reorder_item">Element verschieben</string>
|
||||||
|
<string name="delete_item">Element löschen</string>
|
||||||
|
<string name="note_is_empty">Notiz ist leer</string>
|
||||||
|
<string name="note_saved">Notiz gespeichert</string>
|
||||||
|
<string name="note_deleted">Notiz gelöscht</string>
|
||||||
|
|
||||||
|
<!-- List Preview -->
|
||||||
|
<string name="checklist_progress">%1$d/%2$d erledigt</string>
|
||||||
|
<string name="empty_checklist">Keine Einträge</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
|||||||
|
|
||||||
### Network Detection
|
### 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
|
```kotlin
|
||||||
fun isInHomeNetwork(): Boolean {
|
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 |
|
| **1. Manual Sync** | `MainActivity.kt` | `triggerManualSync()` | User clicks sync button in menu | ✅ Yes |
|
||||||
| **2. Auto-Sync (onResume)** | `MainActivity.kt` | `triggerAutoSync()` | App opened/resumed | ✅ 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 |
|
| **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)
|
### 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 |
|
| 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 |
|
| 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 |
|
| 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!**
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
|||||||
|
|
||||||
### Network Detection
|
### 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
|
```kotlin
|
||||||
fun isInHomeNetwork(): Boolean {
|
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 |
|
| **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 |
|
| **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 |
|
| **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)
|
### 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 |
|
| 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 |
|
| 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 |
|
| 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!**
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,16 @@
|
|||||||
|
|
||||||
## 📝 Note Management
|
## 📝 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
|
### Basic Features
|
||||||
- ✅ **Simple text notes** - Focus on content, no distractions
|
|
||||||
- ✅ **Auto-save** - No manual saving needed
|
- ✅ **Auto-save** - No manual saving needed
|
||||||
- ✅ **Title + content** - Clear structure for each note
|
- ✅ **Title + content** - Clear structure for each note
|
||||||
- ✅ **Timestamps** - Creation and modification date automatically
|
- ✅ **Timestamps** - Creation and modification date automatically
|
||||||
@@ -52,9 +60,11 @@
|
|||||||
|
|
||||||
### Markdown Export
|
### Markdown Export
|
||||||
- ✅ **Automatic export** - Each note → `.md` file
|
- ✅ **Automatic export** - Each note → `.md` file
|
||||||
|
- ✅ **Checklists as task lists** _(NEW)_ - `- [ ]` / `- [x]` format (GitHub-compatible)
|
||||||
- ✅ **Dual-format** - JSON (master) + Markdown (mirror)
|
- ✅ **Dual-format** - JSON (master) + Markdown (mirror)
|
||||||
- ✅ **Filename sanitization** - Safe filenames from titles
|
- ✅ **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
|
- ✅ **WebDAV sync** - Parallel to JSON sync
|
||||||
- ✅ **Optional** - Toggle in settings
|
- ✅ **Optional** - Toggle in settings
|
||||||
- ✅ **Initial export** - All existing notes when activated
|
- ✅ **Initial export** - All existing notes when activated
|
||||||
@@ -81,16 +91,16 @@
|
|||||||
|
|
||||||
### Auto-Sync
|
### Auto-Sync
|
||||||
- ✅ **Interval selection** - 15, 30 or 60 minutes
|
- ✅ **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
|
- ✅ **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
|
- ✅ **WorkManager** - Reliable background execution
|
||||||
- ✅ **Battery optimization compatible** - Works even with Doze mode
|
- ✅ **Battery optimization compatible** - Works even with Doze mode
|
||||||
|
|
||||||
### Sync Triggers (6 total)
|
### Sync Triggers (6 total)
|
||||||
1. ✅ **Periodic sync** - Automatically after interval
|
1. ✅ **Periodic sync** - Automatically after interval
|
||||||
2. ✅ **App-start sync** - When opening the app
|
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
|
4. ✅ **Manual sync** - Button in settings
|
||||||
5. ✅ **Pull-to-refresh** - Swipe gesture in notes list
|
5. ✅ **Pull-to-refresh** - Swipe gesture in notes list
|
||||||
6. ✅ **Settings-save sync** - After server configuration
|
6. ✅ **Settings-save sync** - After server configuration
|
||||||
@@ -109,7 +119,6 @@
|
|||||||
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
|
- ✅ **HTTP/HTTPS** - HTTP only local, HTTPS for external
|
||||||
- ✅ **Username/password** - Basic authentication
|
- ✅ **Username/password** - Basic authentication
|
||||||
- ✅ **Connection test** - Test in settings
|
- ✅ **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)_
|
- ✅ **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/`
|
- ✅ **Flexible URL input** - Both variants work: `http://server/` and `http://server/notes/`
|
||||||
|
|
||||||
@@ -142,7 +151,7 @@
|
|||||||
### Battery Efficiency
|
### Battery Efficiency
|
||||||
- ✅ **Optimized sync intervals** - 15/30/60 min
|
- ✅ **Optimized sync intervals** - 15/30/60 min
|
||||||
- ✅ **WiFi-only** - No mobile data sync
|
- ✅ **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
|
- ✅ **WorkManager** - System-optimized execution
|
||||||
- ✅ **Doze mode compatible** - Sync runs even in standby
|
- ✅ **Doze mode compatible** - Sync runs even in standby
|
||||||
- ✅ **Measured consumption:**
|
- ✅ **Measured consumption:**
|
||||||
|
|||||||
@@ -8,8 +8,16 @@
|
|||||||
|
|
||||||
## 📝 Notiz-Verwaltung
|
## 📝 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
|
### Basis-Funktionen
|
||||||
- ✅ **Einfache Textnotizen** - Fokus auf Inhalt, keine Ablenkung
|
|
||||||
- ✅ **Automatisches Speichern** - Kein manuelles Speichern nötig
|
- ✅ **Automatisches Speichern** - Kein manuelles Speichern nötig
|
||||||
- ✅ **Titel + Inhalt** - Klare Struktur für jede Notiz
|
- ✅ **Titel + Inhalt** - Klare Struktur für jede Notiz
|
||||||
- ✅ **Zeitstempel** - Erstellungs- und Änderungsdatum automatisch
|
- ✅ **Zeitstempel** - Erstellungs- und Änderungsdatum automatisch
|
||||||
@@ -52,9 +60,11 @@
|
|||||||
|
|
||||||
### Markdown-Export
|
### Markdown-Export
|
||||||
- ✅ **Automatischer Export** - Jede Notiz → `.md` Datei
|
- ✅ **Automatischer Export** - Jede Notiz → `.md` Datei
|
||||||
|
- ✅ **Checklisten als Task-Listen** _(NEU)_ - `- [ ]` / `- [x]` Format (GitHub-kompatibel)
|
||||||
- ✅ **Dual-Format** - JSON (Master) + Markdown (Mirror)
|
- ✅ **Dual-Format** - JSON (Master) + Markdown (Mirror)
|
||||||
- ✅ **Dateinamen-Sanitization** - Sichere Dateinamen aus Titeln
|
- ✅ **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
|
- ✅ **WebDAV-Sync** - Parallel zum JSON-Sync
|
||||||
- ✅ **Optional** - In Einstellungen ein/ausschaltbar
|
- ✅ **Optional** - In Einstellungen ein/ausschaltbar
|
||||||
- ✅ **Initial Export** - Alle bestehenden Notizen beim Aktivieren
|
- ✅ **Initial Export** - Alle bestehenden Notizen beim Aktivieren
|
||||||
@@ -81,16 +91,16 @@
|
|||||||
|
|
||||||
### Auto-Sync
|
### Auto-Sync
|
||||||
- ✅ **Intervall-Auswahl** - 15, 30 oder 60 Minuten
|
- ✅ **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
|
- ✅ **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
|
- ✅ **WorkManager** - Zuverlässige Background-Ausführung
|
||||||
- ✅ **Battery-Optimierung kompatibel** - Funktioniert auch mit Doze Mode
|
- ✅ **Battery-Optimierung kompatibel** - Funktioniert auch mit Doze Mode
|
||||||
|
|
||||||
### Sync-Trigger (6 Stück)
|
### Sync-Trigger (6 Stück)
|
||||||
1. ✅ **Periodic Sync** - Automatisch nach Intervall
|
1. ✅ **Periodic Sync** - Automatisch nach Intervall
|
||||||
2. ✅ **App-Start Sync** - Beim Öffnen der App
|
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
|
4. ✅ **Manual Sync** - Button in Einstellungen
|
||||||
5. ✅ **Pull-to-Refresh** - Wisch-Geste in Notizliste
|
5. ✅ **Pull-to-Refresh** - Wisch-Geste in Notizliste
|
||||||
6. ✅ **Settings-Save Sync** - Nach Server-Konfiguration
|
6. ✅ **Settings-Save Sync** - Nach Server-Konfiguration
|
||||||
@@ -109,7 +119,6 @@
|
|||||||
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
|
- ✅ **HTTP/HTTPS** - HTTP nur lokal, HTTPS für extern
|
||||||
- ✅ **Username/Password** - Basic Authentication
|
- ✅ **Username/Password** - Basic Authentication
|
||||||
- ✅ **Connection Test** - In Einstellungen testen
|
- ✅ **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)_
|
- ✅ **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/`
|
- ✅ **Flexible URL-Eingabe** - Beide Varianten funktionieren: `http://server/` und `http://server/notes/`
|
||||||
|
|
||||||
@@ -141,8 +150,8 @@
|
|||||||
|
|
||||||
### Akku-Effizienz
|
### Akku-Effizienz
|
||||||
- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min
|
- ✅ **Optimierte Sync-Intervalle** - 15/30/60 Min
|
||||||
- ✅ **WLAN-Only** - Kein Mobile Data Sync
|
- ✅ **WiFi-Only** - Kein Mobile Data Sync
|
||||||
- ✅ **Smart Server-Check** - Nur im Heim-WLAN
|
- ✅ **Smart Server-Check** - Sync nur wenn Server erreichbar
|
||||||
- ✅ **WorkManager** - System-optimierte Ausführung
|
- ✅ **WorkManager** - System-optimierte Ausführung
|
||||||
- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby
|
- ✅ **Doze Mode kompatibel** - Sync läuft auch im Standby
|
||||||
- ✅ **Gemessener Verbrauch:**
|
- ✅ **Gemessener Verbrauch:**
|
||||||
|
|||||||
234
docs/SETTINGS_REDESIGN_PLAN.md
Normal file
234
docs/SETTINGS_REDESIGN_PLAN.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# Settings-Redesign Plan (v1.5.0)
|
||||||
|
|
||||||
|
## 📋 Übersicht
|
||||||
|
|
||||||
|
Die aktuelle Settings-Activity hat 857 Zeilen XML und ist unübersichtlich geworden.
|
||||||
|
Ziel: Moderne, gruppierte Settings nach Material Design 3 Richtlinien.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Aktuelle Struktur (v1.4.0)
|
||||||
|
|
||||||
|
### Vorhandene Cards (6 Stück)
|
||||||
|
1. **Server-Konfiguration** (~200 Zeilen)
|
||||||
|
- Protokoll-Auswahl (HTTP/HTTPS)
|
||||||
|
- Server-URL
|
||||||
|
- Username/Password
|
||||||
|
- Verbindung testen
|
||||||
|
|
||||||
|
2. **Sync-Einstellungen** (~160 Zeilen)
|
||||||
|
- Auto-Sync Toggle
|
||||||
|
- Sync-Intervall (15/30/60 Min)
|
||||||
|
- Jetzt synchronisieren Button
|
||||||
|
|
||||||
|
3. **Markdown-Integration** (~100 Zeilen)
|
||||||
|
- Markdown Auto-Sync Toggle
|
||||||
|
- Manueller Markdown-Sync Button
|
||||||
|
|
||||||
|
4. **Backup & Wiederherstellung** (~100 Zeilen)
|
||||||
|
- Backup erstellen
|
||||||
|
- Aus Datei wiederherstellen
|
||||||
|
- Vom Server wiederherstellen
|
||||||
|
|
||||||
|
5. **Debug/Entwickler** (~100 Zeilen)
|
||||||
|
- File Logging Toggle
|
||||||
|
- Logs exportieren
|
||||||
|
- Logs löschen
|
||||||
|
|
||||||
|
6. **Über die App** (~100 Zeilen)
|
||||||
|
- Version
|
||||||
|
- GitHub-Links
|
||||||
|
- Lizenz
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Neue Struktur (v1.5.0)
|
||||||
|
|
||||||
|
### Ansatz: PreferenceFragmentCompat + Material 3
|
||||||
|
|
||||||
|
Anstatt XML-Cards mit manuellen Views verwenden wir das moderne Preference-System:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ ⚙️ Einstellungen │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 🔄 SYNCHRONISATION │
|
||||||
|
│ ├─ Server-Verbindung → │
|
||||||
|
│ ├─ Auto-Sync 🔘 │
|
||||||
|
│ └─ Sync-Intervall 30 Min │
|
||||||
|
│ │
|
||||||
|
│ 📝 NOTIZEN │
|
||||||
|
│ └─ Markdown-Export 🔘 │
|
||||||
|
│ │
|
||||||
|
│ 💾 DATEN │
|
||||||
|
│ ├─ Backup erstellen → │
|
||||||
|
│ ├─ Wiederherstellen → │
|
||||||
|
│ └─ Vom Server laden → │
|
||||||
|
│ │
|
||||||
|
│ 🔧 ERWEITERT │
|
||||||
|
│ ├─ Datei-Logging 🔘 │
|
||||||
|
│ └─ Logs verwalten → │
|
||||||
|
│ │
|
||||||
|
│ ℹ️ ÜBER │
|
||||||
|
│ ├─ Version 1.5.0 │
|
||||||
|
│ ├─ GitHub Repository → │
|
||||||
|
│ ├─ Entwickler → │
|
||||||
|
│ └─ Lizenz MIT │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Technische Implementierung
|
||||||
|
|
||||||
|
### Option A: PreferenceFragmentCompat (Empfohlen)
|
||||||
|
**Vorteile:**
|
||||||
|
- Native Android Preference-System
|
||||||
|
- Automatische State-Verwaltung
|
||||||
|
- Eingebaute Material 3 Styles
|
||||||
|
- Hierarchische Navigation (Sub-Screens)
|
||||||
|
- Weniger Code (~300 Zeilen statt 1148)
|
||||||
|
|
||||||
|
**Dateien:**
|
||||||
|
```
|
||||||
|
res/xml/
|
||||||
|
├── preferences_root.xml # Hauptmenü mit Kategorien
|
||||||
|
├── preferences_sync.xml # Server-Konfiguration
|
||||||
|
├── preferences_data.xml # Backup-Optionen
|
||||||
|
└── preferences_debug.xml # Entwickler-Optionen
|
||||||
|
|
||||||
|
SettingsActivity.kt # Wird zu SettingsFragment.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option B: Jetpack Compose (Zukunftssicher)
|
||||||
|
**Vorteile:**
|
||||||
|
- Modernste UI-Technologie
|
||||||
|
- Deklarativer Code
|
||||||
|
- Hot Reload während Entwicklung
|
||||||
|
- Besser für komplexe Custom-UI
|
||||||
|
|
||||||
|
**Nachteile:**
|
||||||
|
- Größere Migration
|
||||||
|
- Mischung mit View-System komplizierter
|
||||||
|
- Lernkurve
|
||||||
|
|
||||||
|
### Empfehlung: **Option A** für v1.5.0
|
||||||
|
PreferenceFragmentCompat ist der richtige Mittelweg:
|
||||||
|
- Schnell zu implementieren (~1-2 Tage)
|
||||||
|
- Native Material 3 Unterstützung
|
||||||
|
- Etabliertes Pattern
|
||||||
|
- Compose-Migration kann später erfolgen (v2.0.0)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Design-Prinzipien (Material 3)
|
||||||
|
|
||||||
|
### 1. Preference Categories
|
||||||
|
```xml
|
||||||
|
<PreferenceCategory
|
||||||
|
app:title="Synchronisation"
|
||||||
|
app:iconSpaceReserved="false">
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Switch Preferences
|
||||||
|
```xml
|
||||||
|
<SwitchPreferenceCompat
|
||||||
|
app:key="auto_sync"
|
||||||
|
app:title="Auto-Sync"
|
||||||
|
app:summary="Automatisch bei WLAN-Verbindung"
|
||||||
|
app:icon="@drawable/ic_sync" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Navigations-Preferences (→ Sub-Screen)
|
||||||
|
```xml
|
||||||
|
<Preference
|
||||||
|
app:key="server_config"
|
||||||
|
app:title="Server-Verbindung"
|
||||||
|
app:summary="192.168.0.188:8080"
|
||||||
|
app:fragment="dev.dettmer.simplenotes.settings.ServerSettingsFragment" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. List Preferences (Dropdown)
|
||||||
|
```xml
|
||||||
|
<ListPreference
|
||||||
|
app:key="sync_interval"
|
||||||
|
app:title="Sync-Intervall"
|
||||||
|
app:entries="@array/sync_interval_entries"
|
||||||
|
app:entryValues="@array/sync_interval_values"
|
||||||
|
app:defaultValue="30" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Neue Dateistruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
app/src/main/java/dev/dettmer/simplenotes/
|
||||||
|
├── settings/
|
||||||
|
│ ├── SettingsFragment.kt # Haupt-Preference-Fragment
|
||||||
|
│ ├── ServerSettingsFragment.kt # Server-Konfiguration
|
||||||
|
│ ├── BackupSettingsFragment.kt # Backup/Restore Dialoge
|
||||||
|
│ └── DebugSettingsFragment.kt # Logging & Logs
|
||||||
|
|
||||||
|
app/src/main/res/xml/
|
||||||
|
├── preferences_root.xml
|
||||||
|
├── preferences_server.xml
|
||||||
|
├── preferences_backup.xml
|
||||||
|
└── preferences_debug.xml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Implementierungs-Checklist
|
||||||
|
|
||||||
|
### Phase 1: Grundstruktur
|
||||||
|
- [ ] `preferences_root.xml` erstellen
|
||||||
|
- [ ] `SettingsFragment.kt` mit PreferenceFragmentCompat
|
||||||
|
- [ ] SettingsActivity als Container anpassen
|
||||||
|
- [ ] Kategorien: Sync, Notizen, Daten, Erweitert, Über
|
||||||
|
|
||||||
|
### Phase 2: Server-Konfiguration
|
||||||
|
- [ ] `preferences_server.xml` für Server-Details
|
||||||
|
- [ ] `ServerSettingsFragment.kt` mit Custom-Dialogen
|
||||||
|
- [ ] Verbindungstest-Button als Preference-Action
|
||||||
|
- [ ] Protocol-Auswahl als ListPreference
|
||||||
|
|
||||||
|
### Phase 3: Sync & Markdown
|
||||||
|
- [ ] Auto-Sync als SwitchPreference
|
||||||
|
- [ ] Sync-Intervall als ListPreference
|
||||||
|
- [ ] Markdown-Export als SwitchPreference
|
||||||
|
- [ ] "Jetzt synchronisieren" als Action-Preference
|
||||||
|
|
||||||
|
### Phase 4: Backup & Debug
|
||||||
|
- [ ] Backup-Aktionen als Preferences
|
||||||
|
- [ ] Logging-Toggle
|
||||||
|
- [ ] Log-Export/Clear Aktionen
|
||||||
|
|
||||||
|
### Phase 5: Über & Polish
|
||||||
|
- [ ] Version, Links, Lizenz
|
||||||
|
- [ ] Icons für alle Kategorien
|
||||||
|
- [ ] Animationen & Transitions
|
||||||
|
- [ ] Dark Mode Testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ Zeitschätzung
|
||||||
|
|
||||||
|
| Phase | Aufwand |
|
||||||
|
|-------|---------|
|
||||||
|
| Phase 1: Grundstruktur | 2-3h |
|
||||||
|
| Phase 2: Server | 3-4h |
|
||||||
|
| Phase 3: Sync | 2h |
|
||||||
|
| Phase 4: Backup/Debug | 2h |
|
||||||
|
| Phase 5: Polish | 2h |
|
||||||
|
| **Gesamt** | **~12h** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 Referenzen
|
||||||
|
|
||||||
|
- [Material 3 Settings](https://m3.material.io/components/lists/overview)
|
||||||
|
- [AndroidX Preference](https://developer.android.com/develop/ui/views/components/settings)
|
||||||
|
- [Preference Styling](https://developer.android.com/develop/ui/views/components/settings/organize-your-settings)
|
||||||
12
fastlane/metadata/android/de-DE/changelogs/11.txt
Normal file
12
fastlane/metadata/android/de-DE/changelogs/11.txt
Normal file
@@ -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!
|
||||||
@@ -2,16 +2,15 @@ Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisatio
|
|||||||
|
|
||||||
HAUPTFUNKTIONEN:
|
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
|
• WebDAV-Synchronisation mit eigenem Server
|
||||||
• Multi-Device Sync (Handy, Tablet, Desktop)
|
• Multi-Device Sync (Handy, Tablet, Desktop)
|
||||||
• Markdown-Export für Obsidian/Desktop-Editoren
|
• Markdown-Export für Obsidian/Desktop-Editoren
|
||||||
|
• Checklisten als GitHub-Style Task-Listen exportieren
|
||||||
• Automatische Synchronisation im Heim-WLAN
|
• Automatische Synchronisation im Heim-WLAN
|
||||||
• Konfigurierbares Sync-Interval (15/30/60 Minuten)
|
• Konfigurierbares Sync-Interval (15/30/60 Minuten)
|
||||||
• Transparente Batterie-Verbrauchsanzeige
|
|
||||||
• Material Design 3 mit Dynamic Colors (Android 12+)
|
• Material Design 3 mit Dynamic Colors (Android 12+)
|
||||||
• Swipe-to-Delete mit Server-Sync
|
|
||||||
• Server-Backup & Wiederherstellung (Merge/Replace/Overwrite)
|
|
||||||
• Komplett offline nutzbar
|
• Komplett offline nutzbar
|
||||||
• Keine Werbung, keine Tracker
|
• Keine Werbung, keine Tracker
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Einfache Notizen-App mit WebDAV-Synchronisation
|
Notizen & Checklisten mit WebDAV-Sync zu deinem eigenen Server
|
||||||
12
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
12
fastlane/metadata/android/en-US/changelogs/11.txt
Normal file
@@ -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!
|
||||||
@@ -2,16 +2,15 @@ Simple Notes Sync is a minimalist note-taking app with WebDAV synchronization.
|
|||||||
|
|
||||||
KEY FEATURES:
|
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
|
• WebDAV synchronization with your own server
|
||||||
• Multi-device sync (phone, tablet, desktop)
|
• Multi-device sync (phone, tablet, desktop)
|
||||||
• Markdown export for Obsidian/desktop editors
|
• Markdown export for Obsidian/desktop editors
|
||||||
|
• Checklists export as GitHub-style task lists
|
||||||
• Automatic synchronization on home WiFi
|
• Automatic synchronization on home WiFi
|
||||||
• Configurable sync interval (15/30/60 minutes)
|
• Configurable sync interval (15/30/60 minutes)
|
||||||
• Transparent battery usage display
|
|
||||||
• Material Design 3 with Dynamic Colors (Android 12+)
|
• Material Design 3 with Dynamic Colors (Android 12+)
|
||||||
• Swipe-to-delete with server sync
|
|
||||||
• Server backup & restore (Merge/Replace/Overwrite)
|
|
||||||
• Fully usable offline
|
• Fully usable offline
|
||||||
• No ads, no trackers
|
• No ads, no trackers
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Simple note-taking app with WebDAV synchronization
|
Notes & checklists with WebDAV sync to your own server
|
||||||
|
|||||||
@@ -133,7 +133,21 @@ Builds:
|
|||||||
scandelete:
|
scandelete:
|
||||||
- android/gradle/wrapper
|
- 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
|
AutoUpdateMode: Version
|
||||||
UpdateCheckMode: Tags
|
UpdateCheckMode: Tags
|
||||||
CurrentVersion: 1.3.2
|
CurrentVersion: 1.4.0
|
||||||
CurrentVersionCode: 10
|
CurrentVersionCode: 11
|
||||||
|
|||||||
Reference in New Issue
Block a user