Files
simple-notes-sync/IMPROVEMENT_PLAN.md
Inventory69 c55b64dab3 feat: Konfigurierbare Sync-Intervalle + Über-Sektion (v1.1.0) (#1)
* feat: WiFi-Connect Auto-Sync + Debug Logging [skip ci]

- WiFi-Connect Auto-Sync via NetworkCallback + Broadcast (statt WorkManager)
- onResume Auto-Sync mit Toast-Feedback (nur Success)
- File-Logging Feature für Debugging (letzte 500 Einträge)
- Settings: Debug/Logs Section mit Test-Button
- FileProvider für Log-Sharing
- Extensive Debug-Logs für NetworkMonitor + MainActivity
- Material Design 3 Migration (alle 17 Tasks)
- Bug-Fixes: Input underlines, section rename, swipe-to-delete, flat cards

PROBLEM: WiFi-Connect sendet Broadcast aber MainActivity empfängt nicht
→ Benötigt logcat debugging auf anderem Gerät

* 🐛 fix: Remove WiFi-Connect related code and UI elements to streamline sync process

* feat: Konfigurierbare Sync-Intervalle + Über-Sektion (v1.1.0)

## Neue Features

### Konfigurierbare Sync-Intervalle
- Wählbare Intervalle: 15/30/60 Minuten in Settings
- Transparente Akkuverbrauchs-Anzeige (0.2-0.8% pro Tag)
- Sofortige Anwendung ohne App-Neustart
- NetworkMonitor liest Intervall dynamisch aus SharedPreferences

### Über-Sektion
- App-Version & Build-Datum Anzeige
- Klickbare Links zu GitHub Repository & Entwickler-Profil
- Lizenz-Information (MIT License)
- Ersetzt alte Debug/Logs Sektion

## Verbesserungen

- Benutzerfreundliche Doze-Mode Erklärung in Settings
- Keine störenden Sync-Fehler Toasts mehr im Hintergrund
- Modernisierte README mit Badges und kompakter Struktur
- F-Droid Metadaten aktualisiert (changelogs + descriptions)

## Technische Änderungen

- Version Bump: 1.0 → 1.1.0 (versionCode: 1 → 2)
- BUILD_DATE buildConfigField hinzugefügt
- PREF_SYNC_INTERVAL_MINUTES Konstante in Constants.kt
- NetworkMonitor.startPeriodicSync() nutzt konfigurierbare Intervalle
- SettingsActivity: setupSyncIntervalPicker() + setupAboutSection()
- activity_settings.xml: RadioGroup für Intervalle + About Cards
2025-12-22 00:49:24 +01:00

2339 lines
83 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🎯 Simple Notes Sync - Verbesserungsplan
**Erstellt am:** 21. Dezember 2025
**Ziel:** UX-Verbesserungen, Material Design 3, Deutsche Lokalisierung, F-Droid Release
---
## 📋 Übersicht der Probleme & Lösungen
---
## 🆕 NEU: Server-Backup Wiederherstellung
### ❗ Neue Anforderung: Notizen vom Server wiederherstellen
**Problem:**
- User kann keine vollständige Wiederherstellung vom Server machen
- Wenn lokale Daten verloren gehen, keine einfache Recovery
- Nützlich bei Gerätewechsel oder nach App-Neuinstallation
**Lösung:**
#### UI-Komponente (Settings)
```kotlin
// SettingsActivity.kt - Button hinzufügen
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonRestoreFromServer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/restore_from_server"
app:icon="@drawable/ic_cloud_download"
style="@style/Widget.Material3.Button.TonalButton" />
// Click Handler
buttonRestoreFromServer.setOnClickListener {
showRestoreConfirmationDialog()
}
private fun showRestoreConfirmationDialog() {
MaterialAlertDialogBuilder(this)
.setTitle("Vom Server wiederherstellen?")
.setMessage(
"⚠️ WARNUNG:\n\n" +
"• Alle lokalen Notizen werden gelöscht\n" +
"• Alle Notizen vom Server werden heruntergeladen\n" +
"• Diese Aktion kann nicht rückgängig gemacht werden\n\n" +
"Fortfahren?"
)
.setIcon(R.drawable.ic_warning)
.setPositiveButton("Wiederherstellen") { _, _ ->
restoreFromServer()
}
.setNegativeButton("Abbrechen", null)
.show()
}
private fun restoreFromServer() {
lifecycleScope.launch {
try {
// Show progress dialog
val progressDialog = MaterialAlertDialogBuilder(this@SettingsActivity)
.setTitle("Wiederherstelle...")
.setMessage("Lade Notizen vom Server...")
.setCancelable(false)
.create()
progressDialog.show()
val syncService = WebDavSyncService(this@SettingsActivity)
val result = syncService.restoreFromServer()
progressDialog.dismiss()
if (result.isSuccess) {
MaterialAlertDialogBuilder(this@SettingsActivity)
.setTitle("✅ Wiederherstellung erfolgreich")
.setMessage("${result.restoredCount} Notizen vom Server wiederhergestellt")
.setPositiveButton("OK") { _, _ ->
// Trigger MainActivity refresh
val intent = Intent("dev.dettmer.simplenotes.NOTES_CHANGED")
LocalBroadcastManager.getInstance(this@SettingsActivity)
.sendBroadcast(intent)
}
.show()
} else {
showErrorDialog(result.errorMessage ?: "Unbekannter Fehler")
}
} catch (e: Exception) {
showErrorDialog(e.message ?: "Wiederherstellung fehlgeschlagen")
}
}
}
```
#### Backend-Logik (WebDavSyncService)
```kotlin
// WebDavSyncService.kt
data class RestoreResult(
val isSuccess: Boolean,
val restoredCount: Int = 0,
val errorMessage: String? = null
)
suspend fun restoreFromServer(): RestoreResult = withContext(Dispatchers.IO) {
try {
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
val username = prefs.getString(Constants.KEY_USERNAME, null)
val password = prefs.getString(Constants.KEY_PASSWORD, null)
if (serverUrl.isNullOrEmpty() || username.isNullOrEmpty() || password.isNullOrEmpty()) {
return@withContext RestoreResult(
isSuccess = false,
errorMessage = "Server nicht konfiguriert"
)
}
// List all remote files
val sardine = Sardine()
sardine.setCredentials(username, password)
val remoteFiles = sardine.list(serverUrl)
.filter { it.name.endsWith(".json") && !it.isDirectory }
if (remoteFiles.isEmpty()) {
return@withContext RestoreResult(
isSuccess = false,
errorMessage = "Keine Notizen auf dem Server gefunden"
)
}
val restoredNotes = mutableListOf<Note>()
// Download each note
for (file in remoteFiles) {
try {
val content = sardine.get(file.href).toString(Charsets.UTF_8)
val note = Note.fromJson(content)
restoredNotes.add(note)
} catch (e: Exception) {
Log.w(TAG, "Failed to parse ${file.name}: ${e.message}")
// Continue with other files
}
}
if (restoredNotes.isEmpty()) {
return@withContext RestoreResult(
isSuccess = false,
errorMessage = "Keine gültigen Notizen gefunden"
)
}
// Clear local storage and save all notes
withContext(Dispatchers.Main) {
storage.clearAll()
restoredNotes.forEach { note ->
storage.saveNote(note.copy(syncStatus = SyncStatus.SYNCED))
}
}
RestoreResult(
isSuccess = true,
restoredCount = restoredNotes.size
)
} catch (e: Exception) {
Log.e(TAG, "Restore failed", e)
RestoreResult(
isSuccess = false,
errorMessage = e.message ?: "Verbindungsfehler"
)
}
}
```
#### Storage Update
```kotlin
// NotesStorage.kt - Methode hinzufügen
fun clearAll() {
val file = File(context.filesDir, NOTES_FILE)
if (file.exists()) {
file.delete()
}
// Create empty notes list
saveAllNotes(emptyList())
}
```
**Betroffene Dateien:**
- `android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt`
- `android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt`
- `android/app/src/main/java/dev/dettmer/simplenotes/storage/NotesStorage.kt`
- `android/app/src/main/res/layout/activity_settings.xml`
- `android/app/src/main/res/values/strings.xml`
- `android/app/src/main/res/drawable/ic_cloud_download.xml` (neu)
**Zeitaufwand:** 2-3 Stunden
**Strings hinzufügen:**
```xml
<string name="restore_from_server">Vom Server wiederherstellen</string>
<string name="restore_confirmation_title">Vom Server wiederherstellen?</string>
<string name="restore_confirmation_message">⚠️ WARNUNG:\n\n• Alle lokalen Notizen werden gelöscht\n• Alle Notizen vom Server werden heruntergeladen\n• Diese Aktion kann nicht rückgängig gemacht werden\n\nFortfahren?</string>
<string name="restoring">Wiederherstelle...</string>
<string name="restoring_message">Lade Notizen vom Server...</string>
<string name="restore_success_title">✅ Wiederherstellung erfolgreich</string>
<string name="restore_success_message">%d Notizen vom Server wiederhergestellt</string>
<string name="restore_error_not_configured">Server nicht konfiguriert</string>
<string name="restore_error_no_notes">Keine Notizen auf dem Server gefunden</string>
<string name="restore_error_invalid_notes">Keine gültigen Notizen gefunden</string>
```
---
### 1⃣ Server-Status Aktualisierung ⚠️ HOCH
**Problem:**
- Server-Status wird nicht sofort nach erfolgreichem Verbindungstest grün
- User muss App neu öffnen oder Focus ändern
**Lösung:**
```kotlin
// In SettingsActivity.kt nach testConnection()
private fun testConnection() {
lifecycleScope.launch {
try {
showToast("Teste Verbindung...")
val syncService = WebDavSyncService(this@SettingsActivity)
val result = syncService.testConnection()
if (result.isSuccess) {
showToast("Verbindung erfolgreich!")
checkServerStatus() // ✅ HIER HINZUFÜGEN
} else {
showToast("Verbindung fehlgeschlagen: ${result.errorMessage}")
checkServerStatus() // ✅ Auch bei Fehler aktualisieren
}
} catch (e: Exception) {
showToast("Fehler: ${e.message}")
checkServerStatus() // ✅ Auch bei Exception
}
}
}
```
**Betroffene Dateien:**
- `android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt`
**Zeitaufwand:** 15 Minuten
---
### 2⃣ Auto-Save Indikator im Editor ⚠️ HOCH
**Problem:**
- User erkennt nicht, dass automatisch gespeichert wird
- Save-Button fehlt → Verwirrung
- Keine visuelle Rückmeldung über Speicher-Status
**Lösung A: Auto-Save mit Indikator (Empfohlen)**
```kotlin
// NoteEditorActivity.kt
private var autoSaveJob: Job? = null
private lateinit var saveStatusTextView: TextView
private fun setupAutoSave() {
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// Cancel previous save job
autoSaveJob?.cancel()
// Show "Speichere..."
saveStatusTextView.text = "💾 Speichere..."
saveStatusTextView.setTextColor(getColor(android.R.color.darker_gray))
// Debounce: Save after 2 seconds of no typing
autoSaveJob = lifecycleScope.launch {
delay(2000)
saveNoteQuietly()
// Show "Gespeichert ✓"
saveStatusTextView.text = "✓ Gespeichert"
saveStatusTextView.setTextColor(getColor(android.R.color.holo_green_dark))
// Hide after 2 seconds
delay(2000)
saveStatusTextView.text = ""
}
}
// ... beforeTextChanged, onTextChanged
}
editTextTitle.addTextChangedListener(textWatcher)
editTextContent.addTextChangedListener(textWatcher)
}
private fun saveNoteQuietly() {
val title = editTextTitle.text?.toString()?.trim() ?: ""
val content = editTextContent.text?.toString()?.trim() ?: ""
if (title.isEmpty() && content.isEmpty()) return
val note = if (existingNote != null) {
existingNote!!.copy(
title = title,
content = content,
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING
)
} else {
Note(
title = title,
content = content,
deviceId = DeviceIdGenerator.getDeviceId(this),
syncStatus = SyncStatus.LOCAL_ONLY
).also { existingNote = it }
}
storage.saveNote(note)
}
```
**Layout Update:**
```xml
<!-- activity_editor.xml - Oben in Toolbar oder darunter -->
<TextView
android:id="@+id/textViewSaveStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="14sp"
android:layout_gravity="center"
android:padding="8dp" />
```
**Alternative B: Save-Button behalten + Auto-Save**
- Button zeigt "Gespeichert ✓" nach Auto-Save
- Button disabled wenn keine Änderungen
**Betroffene Dateien:**
- `android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt`
- `android/app/src/main/res/layout/activity_editor.xml`
- `android/app/src/main/res/values/strings.xml`
**Zeitaufwand:** 1-2 Stunden
---
### 3⃣ GitHub Releases auf Deutsch ⚠️ MITTEL
**Problem:**
- Release Notes sind auf Englisch
- Asset-Namen teilweise englisch
- Zielgruppe ist deutsch
**Lösung:**
```yaml
# .github/workflows/build-production-apk.yml
# Asset-Namen schon auf Deutsch ✓
# Release Body übersetzen:
body: |
# 📝 Produktions-Release: Simple Notes Sync v${{ env.VERSION_NAME }}
## 📊 Build-Informationen
- **Version:** ${{ env.VERSION_NAME }}+${{ env.BUILD_NUMBER }}
- **Build-Datum:** ${{ env.COMMIT_DATE }}
- **Commit:** ${{ env.SHORT_SHA }}
- **Umgebung:** 🟢 **PRODUKTION**
---
## 📋 Änderungen
${{ env.COMMIT_MSG }}
---
## 📦 Download & Installation
### Welche APK sollte ich herunterladen?
| Dein Gerät | Diese APK herunterladen | Größe | Kompatibilität |
|------------|-------------------------|-------|----------------|
| 🤷 Unsicher? | `simple-notes-sync-v${{ env.VERSION_NAME }}-universal.apk` | ~3 MB | Funktioniert auf allen Geräten |
| Modern (ab 2018) | `simple-notes-sync-v${{ env.VERSION_NAME }}-arm64-v8a.apk` | ~2 MB | Schneller, kleiner |
| Ältere Geräte | `simple-notes-sync-v${{ env.VERSION_NAME }}-armeabi-v7a.apk` | ~2 MB | Ältere ARM-Chips |
### 📲 Installationsschritte
1. Lade die passende APK aus den Assets herunter
2. Aktiviere "Aus unbekannten Quellen installieren" in den Android-Einstellungen
3. Öffne die heruntergeladene APK-Datei
4. Folge den Installationsanweisungen
5. Konfiguriere die WebDAV-Einstellungen in der App
---
## ⚙️ Funktionen
- ✅ Automatische WebDAV-Synchronisation alle 30 Minuten (~0,4% Akku/Tag)
- ✅ Intelligente Gateway-Erkennung (automatische Heimnetzwerk-Erkennung)
- ✅ Material Design 3 Benutzeroberfläche
- ✅ Privatsphäre-fokussiert (kein Tracking, keine Analytics)
- ✅ Offline-First Architektur
---
## 🔄 Aktualisierung von vorheriger Version
Installiere diese APK einfach über die bestehende Installation - alle Daten und Einstellungen bleiben erhalten.
---
## 📱 Obtanium - Automatische Updates
Erhalte automatische Updates mit [Obtanium](https://github.com/ImranR98/Obtanium/releases/latest).
**Einrichtung:**
1. Installiere Obtanium über den obigen Link
2. Füge die App mit dieser URL hinzu: `https://github.com/inventory69/simple-notes-sync`
3. Aktiviere automatische Updates
---
## 🆘 Support
Bei Problemen oder Fragen bitte ein Issue auf GitHub öffnen.
---
## 🔒 Datenschutz & Sicherheit
- Alle Daten werden über deinen eigenen WebDAV-Server synchronisiert
- Keine Analytics oder Tracking von Drittanbietern
- Keine Internet-Berechtigungen außer für WebDAV-Sync
- Alle Sync-Vorgänge verschlüsselt (HTTPS)
- Open Source - prüfe den Code selbst
---
## 🛠️ Technische Details
- **Sprache:** Kotlin
- **UI:** Material Design 3
- **Sync:** WorkManager + WebDAV
- **Target SDK:** Android 16 (API 36)
- **Min SDK:** Android 8.0 (API 26)
```
**Betroffene Dateien:**
- `.github/workflows/build-production-apk.yml`
**Zeitaufwand:** 30 Minuten
---
### 4⃣ Material Design 3 - Vollständige Migration ⚠️ HOCH
**Basierend auf:** `MATERIAL_DESIGN_3_MIGRATION.md` Plan
**Problem:**
- Aktuelles Design ist Material Design 2
- Keine Dynamic Colors (Material You)
- Veraltete Komponenten und Farb-Palette
- Keine Android 12+ Features
**Ziel:**
- ✨ Dynamische Farben aus Wallpaper (Material You)
- 🎨 Modern Design Language
- 🔲 Größere Corner Radius (16dp)
- 📱 Material Symbols Icons
- 📝 Material 3 Typography
- 🌓 Perfekter Dark Mode
- ♿ Bessere Accessibility
---
#### Phase 4.1: Theme & Dynamic Colors (15 Min)
**themes.xml:**
```xml
<resources>
<!-- Base Material 3 Theme -->
<style name="Base.Theme.SimpleNotes" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Dynamic Colors (Android 12+) -->
<item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorOnPrimary">@color/md_theme_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<item name="colorSecondary">@color/md_theme_secondary</item>
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
<item name="colorTertiary">@color/md_theme_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
<item name="colorError">@color/md_theme_error</item>
<item name="colorOnError">@color/md_theme_onError</item>
<item name="colorErrorContainer">@color/md_theme_errorContainer</item>
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
<item name="colorSurface">@color/md_theme_surface</item>
<item name="colorOnSurface">@color/md_theme_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
<item name="colorOutline">@color/md_theme_outline</item>
<item name="colorOutlineVariant">@color/md_theme_outlineVariant</item>
<item name="android:colorBackground">@color/md_theme_background</item>
<item name="colorOnBackground">@color/md_theme_onBackground</item>
<!-- Shapes -->
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.SimpleNotes.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.SimpleNotes.MediumComponent</item>
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.SimpleNotes.LargeComponent</item>
<!-- Status bar -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
</style>
<style name="Theme.SimpleNotes" parent="Base.Theme.SimpleNotes" />
<!-- Shapes -->
<style name="ShapeAppearance.SimpleNotes.SmallComponent" parent="ShapeAppearance.Material3.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">12dp</item>
</style>
<style name="ShapeAppearance.SimpleNotes.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
<style name="ShapeAppearance.SimpleNotes.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>
</resources>
```
**colors.xml (Material 3 Baseline - Grün/Natur Theme):**
```xml
<resources>
<!-- Light Theme Colors -->
<color name="md_theme_primary">#006C4C</color>
<color name="md_theme_onPrimary">#FFFFFF</color>
<color name="md_theme_primaryContainer">#89F8C7</color>
<color name="md_theme_onPrimaryContainer">#002114</color>
<color name="md_theme_secondary">#4D6357</color>
<color name="md_theme_onSecondary">#FFFFFF</color>
<color name="md_theme_secondaryContainer">#CFE9D9</color>
<color name="md_theme_onSecondaryContainer">#0A1F16</color>
<color name="md_theme_tertiary">#3D6373</color>
<color name="md_theme_onTertiary">#FFFFFF</color>
<color name="md_theme_tertiaryContainer">#C1E8FB</color>
<color name="md_theme_onTertiaryContainer">#001F29</color>
<color name="md_theme_error">#BA1A1A</color>
<color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_errorContainer">#FFDAD6</color>
<color name="md_theme_onErrorContainer">#410002</color>
<color name="md_theme_background">#FBFDF9</color>
<color name="md_theme_onBackground">#191C1A</color>
<color name="md_theme_surface">#FBFDF9</color>
<color name="md_theme_onSurface">#191C1A</color>
<color name="md_theme_surfaceVariant">#DCE5DD</color>
<color name="md_theme_onSurfaceVariant">#404943</color>
<color name="md_theme_outline">#707973</color>
<color name="md_theme_outlineVariant">#BFC9C2</color>
</resources>
```
**values-night/colors.xml (neu erstellen):**
```xml
<resources>
<!-- Dark Theme Colors -->
<color name="md_theme_primary">#6DDBAC</color>
<color name="md_theme_onPrimary">#003826</color>
<color name="md_theme_primaryContainer">#005138</color>
<color name="md_theme_onPrimaryContainer">#89F8C7</color>
<color name="md_theme_secondary">#B3CCBD</color>
<color name="md_theme_onSecondary">#1F352A</color>
<color name="md_theme_secondaryContainer">#354B40</color>
<color name="md_theme_onSecondaryContainer">#CFE9D9</color>
<color name="md_theme_tertiary">#A5CCE0</color>
<color name="md_theme_onTertiary">#073543</color>
<color name="md_theme_tertiaryContainer">#254B5B</color>
<color name="md_theme_onTertiaryContainer">#C1E8FB</color>
<color name="md_theme_error">#FFB4AB</color>
<color name="md_theme_onError">#690005</color>
<color name="md_theme_errorContainer">#93000A</color>
<color name="md_theme_onErrorContainer">#FFDAD6</color>
<color name="md_theme_background">#191C1A</color>
<color name="md_theme_onBackground">#E1E3DF</color>
<color name="md_theme_surface">#191C1A</color>
<color name="md_theme_onSurface">#E1E3DF</color>
<color name="md_theme_surfaceVariant">#404943</color>
<color name="md_theme_onSurfaceVariant">#BFC9C2</color>
<color name="md_theme_outline">#8A938C</color>
<color name="md_theme_outlineVariant">#404943</color>
</resources>
```
**MainActivity.kt - Dynamic Colors aktivieren:**
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Enable Dynamic Colors (Android 12+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this)
}
setContentView(R.layout.activity_main)
// ...
}
// Import hinzufügen:
import com.google.android.material.color.DynamicColors
import android.os.Build
```
**Betroffene Dateien:**
- `android/app/src/main/res/values/themes.xml`
- `android/app/src/main/res/values/colors.xml`
- `android/app/src/main/res/values-night/colors.xml` (neu)
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
**Zeitaufwand:** 15 Minuten
---
#### Phase 4.2: MainActivity Layout (10 Min)
**activity_main.xml - Material 3 Update:**
```xml
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:elevation="0dp"
app:title="@string/app_name"
app:titleTextAppearance="@style/TextAppearance.Material3.HeadlineMedium" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingVertical="8dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- Empty State -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardEmptyState"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:visibility="gone"
app:cardElevation="0dp"
app:cardCornerRadius="24dp"
app:cardBackgroundColor="?attr/colorSurfaceVariant">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="48dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📝"
android:textSize="72sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/no_notes_yet"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:gravity="center"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tippe auf + um deine erste Notiz zu erstellen"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Extended FAB -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fabAddNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="@string/add_note"
app:icon="@drawable/ic_add_24" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
```
**Betroffene Dateien:**
- `android/app/src/main/res/layout/activity_main.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
**Zeitaufwand:** 10 Minuten
---
#### Phase 4.3: Note Item Card (10 Min)
**item_note.xml - Material 3 Card:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="2dp"
app:cardCornerRadius="16dp"
android:clickable="true"
android:focusable="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/textViewTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Notiz Titel"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?attr/colorOnSurface"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/imageViewSyncStatus"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/imageViewSyncStatus"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="8dp"
app:tint="?attr/colorOnSurfaceVariant"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textViewContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Notiz Inhalt Vorschau..."
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewTitle" />
<TextView
android:id="@+id/textViewTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="vor 5 Minuten"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewContent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
```
**Betroffene Dateien:**
- `android/app/src/main/res/layout/item_note.xml`
**Zeitaufwand:** 10 Minuten
---
#### Phase 4.4: Editor Layout (10 Min)
**activity_editor.xml - Material 3 TextInputLayouts:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Save Status Chip -->
<com.google.android.material.chip.Chip
android:id="@+id/chipSaveStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:chipIcon="@drawable/ic_check"
style="@style/Widget.Material3.Chip.Assist" />
<!-- Title Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/title"
android:layout_marginBottom="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
app:endIconMode="clear_text"
app:boxCornerRadiusTopStart="16dp"
app:boxCornerRadiusTopEnd="16dp"
app:boxCornerRadiusBottomStart="16dp"
app:boxCornerRadiusBottomEnd="16dp"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:maxLength="100"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Content Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/content"
app:counterEnabled="true"
app:counterMaxLength="10000"
app:boxCornerRadiusTopStart="16dp"
app:boxCornerRadiusTopEnd="16dp"
app:boxCornerRadiusBottomStart="16dp"
app:boxCornerRadiusBottomEnd="16dp"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine|textCapSentences"
android:gravity="top"
android:minLines="10"
android:maxLength="10000"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
```
**Betroffene Dateien:**
- `android/app/src/main/res/layout/activity_editor.xml`
**Zeitaufwand:** 10 Minuten
---
#### Phase 4.5: Material Symbols Icons (15 Min)
**Icons erstellen in `res/drawable/`:**
1. **ic_add_24.xml**
```xml
<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/colorOnPrimary">
<path
android:fillColor="@android:color/white"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>
```
2. **ic_sync_24.xml**
3. **ic_settings_24.xml**
4. **ic_cloud_done_24.xml**
5. **ic_cloud_sync_24.xml**
6. **ic_warning_24.xml**
7. **ic_server_24.xml**
8. **ic_person_24.xml**
9. **ic_lock_24.xml**
10. **ic_cloud_download_24.xml**
11. **ic_check_24.xml**
Tool: https://fonts.google.com/icons
**Betroffene Dateien:**
- `android/app/src/main/res/drawable/` (11 neue Icons)
**Zeitaufwand:** 15 Minuten
---
#### Phase 4.6: Splash Screen (30 Min)
**themes.xml - Splash Screen hinzufügen:**
```xml
<style name="Theme.SimpleNotes.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">?attr/colorPrimary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_app_logo</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.SimpleNotes</item>
</style>
```
**AndroidManifest.xml:**
```xml
<application
android:theme="@style/Theme.SimpleNotes.Starting">
<activity
android:name=".MainActivity"
android:theme="@style/Theme.SimpleNotes.Starting">
```
**MainActivity.kt:**
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// Handle splash screen
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
// Import:
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
```
**build.gradle.kts:**
```kotlin
dependencies {
implementation("androidx.core:core-splashscreen:1.0.1")
}
```
**Betroffene Dateien:**
- `android/app/src/main/res/values/themes.xml`
- `android/app/src/main/AndroidManifest.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
- `android/app/build.gradle.kts`
**Zeitaufwand:** 30 Minuten
---
### Material 3 Gesamtaufwand: ~90 Minuten
---
### 5⃣ F-Droid Release Vorbereitung ⚠️ MITTEL
**Problem:**
- Keine F-Droid Metadata vorhanden
- Keine Build-Variante ohne Google-Dependencies
**Lösung - Verzeichnisstruktur:**
```
simple-notes-sync/
├── metadata/
│ └── de-DE/
│ ├── full_description.txt
│ ├── short_description.txt
│ ├── title.txt
│ └── changelogs/
│ ├── 1.txt
│ ├── 2.txt
│ └── 3.txt
└── fastlane/
└── metadata/
└── android/
└── de-DE/
├── images/
│ ├── icon.png
│ ├── featureGraphic.png
│ └── phoneScreenshots/
│ ├── 1.png
│ ├── 2.png
│ └── 3.png
├── full_description.txt
├── short_description.txt
└── title.txt
```
**metadata/de-DE/full_description.txt:**
```
Simple Notes Sync - Deine privaten Notizen, selbst gehostet
Eine minimalistische, datenschutzfreundliche Notizen-App mit automatischer WebDAV-Synchronisation.
HAUPTMERKMALE:
🔒 Datenschutz
• Alle Daten auf deinem eigenen Server
• Keine Cloud-Dienste von Drittanbietern
• Kein Tracking oder Analytics
• Open Source - transparent und überprüfbar
☁️ Automatische Synchronisation
• WebDAV-Sync alle 30 Minuten
• Intelligente Netzwerkerkennung
• Nur im WLAN (konfigurierbar)
• Minimaler Akkuverbrauch (~0,4%/Tag)
✨ Einfach & Schnell
• Klare, aufgeräumte Benutzeroberfläche
• Blitzschnelle Notiz-Erfassung
• Offline-First Design
• Material Design 3
🔧 Flexibel
• Funktioniert mit jedem WebDAV-Server
• Nextcloud, ownCloud, Apache, etc.
• Docker-Setup verfügbar
• Konflikterkennung und -lösung
TECHNISCHE DETAILS:
• Keine Google Services benötigt
• Keine unnötigen Berechtigungen
• Minimale App-Größe (~2-3 MB)
• Android 8.0+ kompatibel
• Kotlin + Material Design 3
PERFECT FÜR:
• Schnelle Notizen und Ideen
• Einkaufslisten
• Todo-Listen
• Persönliche Gedanken
• Alle, die Wert auf Datenschutz legen
Der Quellcode ist verfügbar auf: https://github.com/inventory69/simple-notes-sync
```
**metadata/de-DE/short_description.txt:**
```
Minimalistische Notizen-App mit selbst-gehosteter WebDAV-Synchronisation
```
**metadata/de-DE/title.txt:**
```
Simple Notes Sync
```
**Build-Flavor ohne Google:**
```kotlin
// build.gradle.kts
android {
flavorDimensions += "version"
productFlavors {
create("fdroid") {
dimension = "version"
// Keine Google/Firebase Dependencies
}
create("playstore") {
dimension = "version"
// Optional: Google Services für Analytics etc.
}
}
}
dependencies {
// Base dependencies (alle Flavors)
implementation(libs.androidx.core.ktx)
implementation(libs.material)
// PlayStore specific (optional)
"playstoreImplementation"("com.google.firebase:firebase-analytics:21.5.0")
}
```
**Betroffene Dateien:**
- `metadata/` (neu)
- `fastlane/` (neu)
- `android/app/build.gradle.kts` (Flavors hinzufügen)
- Screenshots erstellen (phone + tablet)
**Zeitaufwand:** 3-4 Stunden (inkl. Screenshots)
---
### 5⃣ Material Design 3 Theme ⚠️ HOCH
**Problem:**
- Aktuelles Theme ist Material Design 2
- Keine Dynamic Colors (Material You)
- Veraltete Farb-Palette
**Lösung - themes.xml:**
```xml
<!-- res/values/themes.xml -->
<resources>
<!-- Base Material 3 Theme with Dynamic Colors -->
<style name="Base.Theme.SimpleNotes" parent="Theme.Material3.DayNight">
<!-- Dynamic Colors (Android 12+) -->
<item name="colorPrimary">@color/md_theme_primary</item>
<item name="colorOnPrimary">@color/md_theme_onPrimary</item>
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
<item name="colorSecondary">@color/md_theme_secondary</item>
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
<item name="colorTertiary">@color/md_theme_tertiary</item>
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
<item name="colorError">@color/md_theme_error</item>
<item name="colorOnError">@color/md_theme_onError</item>
<item name="colorErrorContainer">@color/md_theme_errorContainer</item>
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
<item name="colorSurface">@color/md_theme_surface</item>
<item name="colorOnSurface">@color/md_theme_onSurface</item>
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
<item name="colorOutline">@color/md_theme_outline</item>
<item name="colorOutlineVariant">@color/md_theme_outlineVariant</item>
<item name="android:colorBackground">@color/md_theme_background</item>
<item name="colorOnBackground">@color/md_theme_onBackground</item>
<!-- Shapes -->
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.SimpleNotes.SmallComponent</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.SimpleNotes.MediumComponent</item>
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.SimpleNotes.LargeComponent</item>
<!-- Typography -->
<item name="textAppearanceHeadlineLarge">@style/TextAppearance.SimpleNotes.HeadlineLarge</item>
<item name="textAppearanceBodyLarge">@style/TextAppearance.SimpleNotes.BodyLarge</item>
<!-- Status bar -->
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.SimpleNotes" parent="Base.Theme.SimpleNotes" />
<!-- Shapes -->
<style name="ShapeAppearance.SimpleNotes.SmallComponent" parent="ShapeAppearance.Material3.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">12dp</item>
</style>
<style name="ShapeAppearance.SimpleNotes.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
<style name="ShapeAppearance.SimpleNotes.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">24dp</item>
</style>
</resources>
```
**colors.xml (Material 3 Baseline):**
```xml
<!-- res/values/colors.xml -->
<resources>
<!-- Light Theme Colors -->
<color name="md_theme_primary">#006C4C</color>
<color name="md_theme_onPrimary">#FFFFFF</color>
<color name="md_theme_primaryContainer">#89F8C7</color>
<color name="md_theme_onPrimaryContainer">#002114</color>
<color name="md_theme_secondary">#4D6357</color>
<color name="md_theme_onSecondary">#FFFFFF</color>
<color name="md_theme_secondaryContainer">#CFE9D9</color>
<color name="md_theme_onSecondaryContainer">#0A1F16</color>
<color name="md_theme_tertiary">#3D6373</color>
<color name="md_theme_onTertiary">#FFFFFF</color>
<color name="md_theme_tertiaryContainer">#C1E8FB</color>
<color name="md_theme_onTertiaryContainer">#001F29</color>
<color name="md_theme_error">#BA1A1A</color>
<color name="md_theme_onError">#FFFFFF</color>
<color name="md_theme_errorContainer">#FFDAD6</color>
<color name="md_theme_onErrorContainer">#410002</color>
<color name="md_theme_background">#FBFDF9</color>
<color name="md_theme_onBackground">#191C1A</color>
<color name="md_theme_surface">#FBFDF9</color>
<color name="md_theme_onSurface">#191C1A</color>
<color name="md_theme_surfaceVariant">#DCE5DD</color>
<color name="md_theme_onSurfaceVariant">#404943</color>
<color name="md_theme_outline">#707973</color>
<color name="md_theme_outlineVariant">#BFC9C2</color>
</resources>
```
**Dynamic Colors aktivieren (MainActivity.kt):**
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// Enable dynamic colors (Android 12+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
DynamicColors.applyToActivityIfAvailable(this)
}
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ...
}
```
**Dependency hinzufügen:**
```kotlin
// build.gradle.kts
dependencies {
implementation("com.google.android.material:material:1.11.0")
}
```
**Betroffene Dateien:**
- `android/app/src/main/res/values/themes.xml`
- `android/app/src/main/res/values/colors.xml`
- `android/app/src/main/res/values-night/colors.xml` (neu)
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
- `android/app/build.gradle.kts`
**Zeitaufwand:** 2-3 Stunden
---
### 6⃣ Settings UI mit Material 3 ⚠️ MITTEL
**Problem:**
- Plain TextInputLayouts ohne Icons
- Keine visuellen Gruppierungen
- Server-Status Wechsel nicht animiert
**Lösung - activity_settings.xml:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/settings"
app:navigationIcon="?attr/homeAsUpIndicator" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Server Settings Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardElevation="2dp"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/server_settings"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:layout_marginBottom="16dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/server_url"
android:layout_marginBottom="12dp"
app:startIconDrawable="@drawable/ic_server"
app:helperText="z.B. https://cloud.example.com/remote.php/dav/files/username/notes"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextServerUrl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/username"
android:layout_marginBottom="12dp"
app:startIconDrawable="@drawable/ic_person"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
app:startIconDrawable="@drawable/ic_lock"
style="@style/Widget.Material3.TextInputLayout.FilledBox"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Server Status Chip -->
<com.google.android.material.chip.Chip
android:id="@+id/chipServerStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="🔍 Prüfe Server..."
app:chipIcon="@drawable/ic_sync"
style="@style/Widget.Material3.Chip.Assist" />
<!-- Connection Test Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonTestConnection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/test_connection"
app:icon="@drawable/ic_check_circle"
style="@style/Widget.Material3.Button.TonalButton" />
<com.google.android.material.button.MaterialButton
android:id="@+id/buttonSyncNow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/sync_now"
app:icon="@drawable/ic_sync"
style="@style/Widget.Material3.Button" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Auto-Sync Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardElevation="2dp"
app:cardCornerRadius="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Automatische Synchronisation"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="12dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/auto_sync"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Synchronisiert alle 30 Minuten"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchAutoSync"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</LinearLayout>
<!-- Info Banner -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorPrimaryContainer"
app:cardElevation="0dp"
app:cardCornerRadius="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textSize="24sp"
android:layout_marginEnd="12dp" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Auto-Sync funktioniert nur im selben WLAN-Netzwerk wie dein Server. Minimaler Akkuverbrauch (~0.4%/Tag)."
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnPrimaryContainer" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
```
**Animierter Server-Status (SettingsActivity.kt):**
```kotlin
private fun updateServerStatus(status: ServerStatus) {
val chip = findViewById<Chip>(R.id.chipServerStatus)
// Animate transition
chip.animate()
.alpha(0f)
.setDuration(150)
.withEndAction {
when (status) {
ServerStatus.CHECKING -> {
chip.text = "🔍 Prüfe Server..."
chip.chipBackgroundColor = ColorStateList.valueOf(
getColor(R.color.md_theme_surfaceVariant)
)
chip.setChipIconResource(R.drawable.ic_sync)
}
ServerStatus.REACHABLE -> {
chip.text = "✅ Server erreichbar"
chip.chipBackgroundColor = ColorStateList.valueOf(
getColor(R.color.md_theme_primaryContainer)
)
chip.setChipIconResource(R.drawable.ic_check_circle)
}
ServerStatus.UNREACHABLE -> {
chip.text = "❌ Nicht erreichbar"
chip.chipBackgroundColor = ColorStateList.valueOf(
getColor(R.color.md_theme_errorContainer)
)
chip.setChipIconResource(R.drawable.ic_error)
}
ServerStatus.NOT_CONFIGURED -> {
chip.text = "⚠️ Nicht konfiguriert"
chip.chipBackgroundColor = ColorStateList.valueOf(
getColor(R.color.md_theme_surfaceVariant)
)
chip.setChipIconResource(R.drawable.ic_warning)
}
}
chip.animate()
.alpha(1f)
.setDuration(150)
.start()
}
.start()
}
enum class ServerStatus {
CHECKING,
REACHABLE,
UNREACHABLE,
NOT_CONFIGURED
}
```
**Icons benötigt (drawable/):**
- `ic_server.xml`
- `ic_person.xml`
- `ic_lock.xml`
- `ic_check_circle.xml`
- `ic_sync.xml`
- `ic_error.xml`
- `ic_warning.xml`
**Betroffene Dateien:**
- `android/app/src/main/res/layout/activity_settings.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt`
- `android/app/src/main/res/drawable/` (Icons)
**Zeitaufwand:** 3-4 Stunden
---
### 7⃣ Main Activity mit Material 3 Cards ⚠️ HOCH
**Problem:**
- Notizen in einfachen ListItems
- Keine Elevation/Shadow
- Swipe-to-Delete fehlt
**Lösung - item_note.xml:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardElevation="2dp"
app:cardCornerRadius="16dp"
android:clickable="true"
android:focusable="true"
app:rippleColor="?attr/colorPrimary">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/textViewTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Notiz Titel"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?attr/colorOnSurface"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/chipSyncStatus"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.chip.Chip
android:id="@+id/chipSyncStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="☁️"
android:textSize="12sp"
app:chipMinHeight="24dp"
style="@style/Widget.Material3.Chip.Assist.Elevated"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/textViewContent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Notiz Inhalt Vorschau..."
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewTitle" />
<TextView
android:id="@+id/textViewTimestamp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="vor 5 Minuten"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/textViewContent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
```
**Swipe-to-Delete (MainActivity.kt):**
```kotlin
private fun setupRecyclerView() {
recyclerView = findViewById(R.id.recyclerView)
adapter = NotesAdapter { note ->
openNoteEditor(note.id)
}
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
// Swipe-to-Delete
val swipeHandler = object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val note = adapter.notes[position]
// Delete with undo
adapter.removeNote(position)
storage.deleteNote(note.id)
Snackbar.make(
findViewById(R.id.coordinator),
"Notiz gelöscht",
Snackbar.LENGTH_LONG
).setAction("RÜCKGÄNGIG") {
adapter.addNote(position, note)
storage.saveNote(note)
}.show()
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val paint = Paint()
paint.color = getColor(R.color.md_theme_errorContainer)
// Draw background
if (dX > 0) {
c.drawRect(
itemView.left.toFloat(),
itemView.top.toFloat(),
dX,
itemView.bottom.toFloat(),
paint
)
} else {
c.drawRect(
itemView.right.toFloat() + dX,
itemView.top.toFloat(),
itemView.right.toFloat(),
itemView.bottom.toFloat(),
paint
)
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
}
ItemTouchHelper(swipeHandler).attachToRecyclerView(recyclerView)
}
```
**Empty State (activity_main.xml):**
```xml
<com.google.android.material.card.MaterialCardView
android:id="@+id/cardEmptyState"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="32dp"
android:visibility="gone"
app:cardElevation="0dp"
app:cardCornerRadius="24dp"
app:cardBackgroundColor="?attr/colorSurfaceVariant">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="48dp"
android:gravity="center">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📝"
android:textSize="72sp"
android:layout_marginBottom="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Noch keine Notizen"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tippe auf + um deine erste Notiz zu erstellen"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
```
**Extended FAB (activity_main.xml):**
```xml
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/fabAddNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:text="Neue Notiz"
app:icon="@drawable/ic_add" />
```
**Betroffene Dateien:**
- `android/app/src/main/res/layout/activity_main.xml`
- `android/app/src/main/res/layout/item_note.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
- `android/app/src/main/java/dev/dettmer/simplenotes/adapters/NotesAdapter.kt`
**Zeitaufwand:** 4-5 Stunden
---
### 8⃣ Editor mit Material 3 ⚠️ MITTEL
**Problem:**
- Einfache EditText-Felder
- Kein Character Counter
- Keine visuelle Trennung
**Lösung - activity_editor.xml:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_close" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Save Status Indicator -->
<com.google.android.material.chip.Chip
android:id="@+id/chipSaveStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="16dp"
android:visibility="gone"
app:chipIcon="@drawable/ic_check"
style="@style/Widget.Material3.Chip.Assist" />
<!-- Title Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Titel"
android:layout_marginBottom="16dp"
app:counterEnabled="true"
app:counterMaxLength="100"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapSentences"
android:maxLength="100"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Content Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Deine Notiz..."
app:counterEnabled="true"
app:counterMaxLength="10000"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine|textCapSentences"
android:gravity="top"
android:minLines="10"
android:maxLength="10000"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Metadata Card -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardElevation="0dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="?attr/colorSurfaceVariant">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp">
<TextView
android:id="@+id/textViewCreatedAt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Erstellt: Heute um 14:30"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
<TextView
android:id="@+id/textViewModifiedAt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Geändert: vor 2 Minuten"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
```
**Betroffene Dateien:**
- `android/app/src/main/res/layout/activity_editor.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/NoteEditorActivity.kt`
**Zeitaufwand:** 2 Stunden
---
### 9⃣ Splash Screen mit Material 3 ⚠️ NIEDRIG
**Problem:**
- Kein moderner Splash Screen
**Lösung:**
```xml
<!-- res/values/themes.xml -->
<style name="Theme.SimpleNotes.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">?attr/colorPrimary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_app_logo</item>
<item name="windowSplashScreenAnimationDuration">500</item>
<item name="postSplashScreenTheme">@style/Theme.SimpleNotes</item>
</style>
```
```xml
<!-- AndroidManifest.xml -->
<application
android:theme="@style/Theme.SimpleNotes.Starting">
<activity
android:name=".MainActivity"
android:theme="@style/Theme.SimpleNotes.Starting">
```
```kotlin
// MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
// Handle the splash screen transition
installSplashScreen()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
```
**Dependency:**
```kotlin
implementation("androidx.core:core-splashscreen:1.0.1")
```
**Betroffene Dateien:**
- `android/app/src/main/res/values/themes.xml`
- `android/app/src/main/AndroidManifest.xml`
- `android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt`
- `android/app/build.gradle.kts`
**Zeitaufwand:** 30 Minuten
---
### 🔟 Deutsche Lokalisierung ⚠️ MITTEL
**Problem:**
- Einige Strings noch auf Englisch
- Release Notes englisch
- Error Messages englisch
**Lösung - strings.xml vervollständigen:**
```xml
<resources>
<!-- App -->
<string name="app_name">Simple Notes</string>
<!-- Main Activity -->
<string name="no_notes_yet">Noch keine Notizen.\nTippe + um eine zu erstellen.</string>
<string name="add_note">Notiz hinzufügen</string>
<string name="sync">Synchronisieren</string>
<string name="settings">Einstellungen</string>
<!-- Editor -->
<string name="edit_note">Notiz bearbeiten</string>
<string name="new_note">Neue Notiz</string>
<string name="title">Titel</string>
<string name="content">Inhalt</string>
<string name="save">Speichern</string>
<string name="delete">Löschen</string>
<string name="saving">Speichere…</string>
<string name="saved">✓ Gespeichert</string>
<string name="auto_save_hint">Änderungen werden automatisch gespeichert</string>
<!-- Settings -->
<string name="server_settings">Server-Einstellungen</string>
<string name="server_url">Server URL</string>
<string name="server_url_hint">z.B. https://cloud.example.com/remote.php/dav/files/username/notes</string>
<string name="username">Benutzername</string>
<string name="password">Passwort</string>
<string name="wifi_settings">WLAN-Einstellungen</string>
<string name="home_ssid">Heim-WLAN SSID</string>
<string name="auto_sync">Auto-Sync aktiviert</string>
<string name="auto_sync_description">Synchronisiert alle 30 Minuten</string>
<string name="auto_sync_info">Auto-Sync funktioniert nur im selben WLAN-Netzwerk wie dein Server. Minimaler Akkuverbrauch (~0.4%/Tag).</string>
<string name="test_connection">Verbindung testen</string>
<string name="sync_now">Jetzt synchronisieren</string>
<string name="sync_status">Sync-Status</string>
<!-- Server Status -->
<string name="server_status_checking">🔍 Prüfe Server…</string>
<string name="server_status_reachable">✅ Server erreichbar</string>
<string name="server_status_unreachable">❌ Nicht erreichbar</string>
<string name="server_status_not_configured">⚠️ Nicht konfiguriert</string>
<!-- Messages -->
<string name="testing_connection">Teste Verbindung…</string>
<string name="connection_successful">Verbindung erfolgreich!</string>
<string name="connection_failed">Verbindung fehlgeschlagen: %s</string>
<string name="synchronizing">Synchronisiere…</string>
<string name="sync_successful">Erfolgreich! %d Notizen synchronisiert</string>
<string name="sync_failed">Sync fehlgeschlagen: %s</string>
<string name="sync_conflicts">Sync abgeschlossen. %d Konflikte erkannt!</string>
<string name="note_saved">Notiz gespeichert</string>
<string name="note_deleted">Notiz gelöscht</string>
<string name="undo">RÜCKGÄNGIG</string>
<!-- Dialogs -->
<string name="delete_note_title">Notiz löschen?</string>
<string name="delete_note_message">Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
<string name="battery_optimization_title">Hintergrund-Synchronisation</string>
<string name="battery_optimization_message">Damit die App im Hintergrund synchronisieren kann, muss die Akku-Optimierung deaktiviert werden.\n\nBitte wähle \'Nicht optimieren\' für Simple Notes.</string>
<string name="open_settings">Einstellungen öffnen</string>
<string name="later">Später</string>
<!-- Errors -->
<string name="error_title_empty">Titel oder Inhalt darf nicht leer sein</string>
<string name="error_network">Netzwerkfehler: %s</string>
<string name="error_server">Server-Fehler: %s</string>
<string name="error_auth">Authentifizierung fehlgeschlagen</string>
<string name="error_unknown">Unbekannter Fehler: %s</string>
<!-- Notifications -->
<string name="notification_channel_name">Notizen Synchronisierung</string>
<string name="notification_channel_description">Benachrichtigungen über Sync-Status</string>
<string name="notification_sync_success">Sync erfolgreich</string>
<string name="notification_sync_success_text">%d Notizen synchronisiert</string>
<string name="notification_sync_failed">Sync fehlgeschlagen</string>
<string name="notification_sync_failed_text">%s</string>
</resources>
```
**Betroffene Dateien:**
- `android/app/src/main/res/values/strings.xml`
- `.github/workflows/build-production-apk.yml`
- Alle `.kt` Dateien mit hardcoded strings
**Zeitaufwand:** 2 Stunden
---
## 📊 Zusammenfassung & Prioritäten
### Phase 1: Kritische UX-Fixes (Sofort) ⚡
**Zeitaufwand: ~3-4 Stunden**
1. ✅ Server-Status Aktualisierung (15 min)
2. ✅ Auto-Save Indikator (1-2 h)
3. ✅ GitHub Releases auf Deutsch (30 min)
4. ✅ Server-Backup Wiederherstellung (2-3 h)
### Phase 2: Material Design 3 Migration (1 Tag) 🎨
**Zeitaufwand: ~90 Minuten**
5. ✅ Theme & Dynamic Colors (15 min)
6. ✅ MainActivity Layout (10 min)
7. ✅ Note Item Card (10 min)
8. ✅ Editor Layout (10 min)
9. ✅ Settings Layout (10 min) - aus Phase 3 vorgezogen
10. ✅ Material Icons (15 min)
11. ✅ Splash Screen (30 min)
### Phase 3: Advanced UI Features (2-3 Tage) 🚀
**Zeitaufwand: ~4-5 Stunden**
12. ✅ Swipe-to-Delete (1 h)
13. ✅ Empty State (30 min)
14. ✅ Animierte Server-Status Änderung (1 h)
15. ✅ Deutsche Lokalisierung vervollständigen (1-2 h)
### Phase 4: F-Droid Release (1 Tag) 📦
**Zeitaufwand: ~4 Stunden**
16. ✅ F-Droid Metadata (3-4 h)
17. ✅ F-Droid Build-Flavor (30 min)
---
## 🎯 Empfohlene Reihenfolge
### Woche 1: Fundament & Kritische Fixes
**Tag 1 (3-4h):** Phase 1 - Kritische UX-Fixes
- Server-Status sofort grün nach Test
- Auto-Save mit visuellem Feedback
- Deutsche Release Notes
- **Server-Backup Funktion** ← NEU & WICHTIG
**Tag 2 (1.5h):** Phase 2 Start - Material Design 3 Foundation
- Theme & Dynamic Colors aktivieren
- MainActivity Layout modernisieren
- Note Item Cards verbessern
**Tag 3 (1.5h):** Phase 2 Fortführung - Material Design 3
- Editor Layout upgraden
- Settings Layout modernisieren
- Material Icons erstellen
- Splash Screen implementieren
### Woche 2: Polish & Release
**Tag 4 (2-3h):** Phase 3 - Advanced Features
- Swipe-to-Delete mit Animation
- Empty State mit Illustration
- Server-Status Animationen
- Deutsche Strings vervollständigen
**Tag 5 (4h):** Phase 4 - F-Droid Vorbereitung
- Metadata erstellen
- Screenshots machen
- Build-Flavor konfigurieren
---
## 🆕 Neue Features Zusammenfassung
### Server-Backup Wiederherstellung
**Warum wichtig:**
- ✅ Gerätewechsel einfach
- ✅ Recovery nach App-Neuinstallation
- ✅ Datensicherheit erhöht
- ✅ User-Vertrauen gestärkt
**Wo in der UI:**
- Settings Activity → neuer Button "Vom Server wiederherstellen"
- Warn-Dialog vor Ausführung
- Progress-Dialog während Download
- Success-Dialog mit Anzahl wiederhergestellter Notizen
**Backend:**
- `WebDavSyncService.restoreFromServer()`
- `NotesStorage.clearAll()`
- Vollständiger Download aller Server-Notizen
- Überschreibt lokale Daten komplett
---
## 📝 Material Design 3 - Schnellreferenz
### Umgesetzt wird:
**Dynamic Colors** - Farben aus Wallpaper (Android 12+)
**Material 3 Components** - Cards, Buttons, TextInputs
**16dp Corner Radius** - Modernere abgerundete Ecken
**Material Symbols** - Neue Icon-Familie
**Typography Scale** - Material 3 Text-Styles
**Dark Mode** - Perfekt abgestimmte Nacht-Farben
**Splash Screen API** - Android 12+ Native Splash
### Design-Token:
- **Primary:** Grün (#006C4C) - Natur, Notizen, Wachstum
- **Secondary:** Grau-Grün - Subtil, harmonisch
- **Surface:** Hell/Dunkel - Abhängig von Theme
- **Shapes:** Small 12dp, Medium 16dp, Large 24dp
---
## 📋 Checkliste vor Start
- [ ] Branch erstellen: `git checkout -b feature/ux-improvements`
- [ ] Backup vom aktuellen Stand
- [ ] Material 3 Dependency prüfen: `com.google.android.material:material:1.11.0`
- [ ] Android Studio aktualisiert
- [ ] Testgerät mit Android 12+ für Dynamic Colors
---
## 🧪 Testing nach Abschluss
### Manuell:
- [ ] Alle Layouts auf Smartphone (Phone)
- [ ] Alle Layouts auf Tablet
- [ ] Dark Mode überall
- [ ] Light Mode überall
- [ ] Dynamic Colors (Android 12+)
- [ ] Server-Backup: Restore funktioniert
- [ ] Server-Backup: Dialog-Texte korrekt
- [ ] Auto-Save: Indikator erscheint
- [ ] Auto-Save: Speichert nach 2s
- [ ] Server-Status: Wird sofort aktualisiert
- [ ] Swipe-to-Delete: Animation smooth
- [ ] Empty State: Zeigt sich bei 0 Notizen
- [ ] Splash Screen: Erscheint beim Start
- [ ] Alle Icons: Richtige Farbe (Tint)
- [ ] Alle Buttons: Funktionieren
- [ ] Deutsch: Keine englischen Strings mehr
### Automatisch:
- [ ] Build erfolgreich (Debug)
- [ ] Build erfolgreich (Release)
- [ ] APK Size akzeptabel (<5 MB)
- [ ] Keine Lint-Errors
- [ ] ProGuard-Regeln funktionieren
---
## 📚 Referenzen & Tools
### Material Design 3:
- [Material Design 3 Guidelines](https://m3.material.io/)
- [Material Theme Builder](https://material-foundation.github.io/material-theme-builder/)
- [Material Symbols Icons](https://fonts.google.com/icons)
### Android:
- [Splash Screen API](https://developer.android.com/develop/ui/views/launch/splash-screen)
- [Dynamic Colors](https://developer.android.com/develop/ui/views/theming/dynamic-colors)
---
## 📝 Nächste Schritte
Soll ich mit **Phase 1** (kritische UX-Fixes + Server-Backup) beginnen?
### Was ich jetzt machen würde:
1. **Server-Backup implementieren** (2-3h)
- Höchste Priorität: User-requested Feature
- Kritisch für Datensicherheit
2. **Server-Status sofort aktualisieren** (15 min)
- Schneller Win
- Verbessert UX sofort
3. **Auto-Save Indikator** (1-2h)
- Eliminiert Verwirrung
- Modernes Pattern
4. **Material 3 Foundation** (90 min)
- Theme & Colors
- Basis für alles weitere
Diese 4 Tasks würden den größten Impact haben und sind in ~4-6 Stunden machbar! 🚀