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
This commit is contained in:
80
.github/workflows/build-production-apk.yml
vendored
80
.github/workflows/build-production-apk.yml
vendored
@@ -102,24 +102,24 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
tag_name: ${{ env.VERSION_TAG }}
|
||||
name: "📝 Simple Notes Sync v${{ env.VERSION_NAME }} (Production)"
|
||||
name: "📝 Simple Notes Sync v${{ env.VERSION_NAME }} (Produktions-Release)"
|
||||
files: apk-output/*.apk
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
# 📝 Production Release: Simple Notes Sync v${{ env.VERSION_NAME }}
|
||||
# 📝 Produktions-Release: Simple Notes Sync v${{ env.VERSION_NAME }}
|
||||
|
||||
## Build Information
|
||||
## Build-Informationen
|
||||
|
||||
- **Version:** ${{ env.VERSION_NAME }}+${{ env.BUILD_NUMBER }}
|
||||
- **Build Date:** ${{ env.COMMIT_DATE }}
|
||||
- **Build-Datum:** ${{ env.COMMIT_DATE }}
|
||||
- **Commit:** ${{ env.SHORT_SHA }}
|
||||
- **Environment:** 🟢 **PRODUCTION**
|
||||
- **Umgebung:** 🟢 **PRODUKTION**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Changes
|
||||
## 📋 Änderungen
|
||||
|
||||
${{ env.COMMIT_MSG }}
|
||||
|
||||
@@ -127,69 +127,69 @@ jobs:
|
||||
|
||||
## 📦 Download & Installation
|
||||
|
||||
### Which APK should I download?
|
||||
### Welche APK soll ich herunterladen?
|
||||
|
||||
| Your Device | Download This APK | Size | Compatibility |
|
||||
|-------------|------------------|------|---------------|
|
||||
| 🤷 Not sure? | `simple-notes-sync-v${{ env.VERSION_NAME }}-universal.apk` | ~5 MB | Works on all devices |
|
||||
| Modern (2018+) | `simple-notes-sync-v${{ env.VERSION_NAME }}-arm64-v8a.apk` | ~3 MB | Faster, smaller |
|
||||
| Older devices | `simple-notes-sync-v${{ env.VERSION_NAME }}-armeabi-v7a.apk` | ~3 MB | Older ARM chips |
|
||||
| Dein Gerät | Lade diese APK herunter | Größe | Kompatibilität |
|
||||
|------------|------------------------|-------|----------------|
|
||||
| 🤷 Nicht sicher? | `simple-notes-sync-v${{ env.VERSION_NAME }}-universal.apk` | ~5 MB | Funktioniert auf allen Geräten |
|
||||
| Modern (2018+) | `simple-notes-sync-v${{ env.VERSION_NAME }}-arm64-v8a.apk` | ~3 MB | Schneller, kleiner |
|
||||
| Ältere Geräte | `simple-notes-sync-v${{ env.VERSION_NAME }}-armeabi-v7a.apk` | ~3 MB | Ältere ARM-Chips |
|
||||
|
||||
### Installation Steps
|
||||
1. Download the appropriate APK from the assets below
|
||||
2. Enable "Install from unknown sources" in Android settings
|
||||
3. Open the downloaded APK file
|
||||
4. Follow the installation prompts
|
||||
5. Configure WebDAV settings in the app
|
||||
### Installationsschritte
|
||||
1. Lade die passende APK aus den Assets unten herunter
|
||||
2. Aktiviere "Installation aus unbekannten Quellen" in den Android-Einstellungen
|
||||
3. Öffne die heruntergeladene APK-Datei
|
||||
4. Folge den Installationsanweisungen
|
||||
5. Konfiguriere die WebDAV-Einstellungen in der App
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Features
|
||||
## ⚙️ Funktionen
|
||||
|
||||
- ✅ Automatic WebDAV sync every 30 minutes (~0.4% battery/day)
|
||||
- ✅ Smart gateway detection (home network auto-detection)
|
||||
- ✅ Material Design 3 UI
|
||||
- ✅ Privacy-focused (no tracking, no analytics)
|
||||
- ✅ Offline-first architecture
|
||||
- ✅ Automatische WebDAV-Synchronisation alle 30 Minuten (~0,4% Akku/Tag)
|
||||
- ✅ Intelligente Gateway-Erkennung (automatische Heimnetzwerk-Erkennung)
|
||||
- ✅ Material Design 3 Oberfläche
|
||||
- ✅ Datenschutzorientiert (kein Tracking, keine Analysen)
|
||||
- ✅ Offline-First Architektur
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Updating from Previous Version
|
||||
## 🔄 Update von vorheriger Version
|
||||
|
||||
Simply install this APK over the existing installation - all data and settings will be preserved.
|
||||
Installiere diese APK einfach über die bestehende Installation - alle Daten und Einstellungen bleiben erhalten.
|
||||
|
||||
---
|
||||
|
||||
## 📱 Obtanium - Auto-Update App
|
||||
|
||||
Get automatic updates with [Obtanium](https://github.com/ImranR98/Obtanium/releases/latest).
|
||||
Erhalte automatische Updates mit [Obtanium](https://github.com/ImranR98/Obtanium/releases/latest).
|
||||
|
||||
**Setup:**
|
||||
1. Install Obtanium from the link above
|
||||
2. Add app with this URL: `https://github.com/dettmersLiq/simple-notes-sync`
|
||||
3. Enable auto-updates
|
||||
**Einrichtung:**
|
||||
1. Installiere Obtanium über den Link oben
|
||||
2. Füge die App mit dieser URL hinzu: `https://github.com/dettmersLiq/simple-notes-sync`
|
||||
3. Aktiviere Auto-Updates
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support
|
||||
|
||||
For issues or questions, please open an issue on GitHub.
|
||||
Bei Problemen oder Fragen öffne bitte ein Issue auf GitHub.
|
||||
|
||||
---
|
||||
|
||||
## 🔒 Privacy & Security
|
||||
## 🔒 Datenschutz & Sicherheit
|
||||
|
||||
- All data synced via your own WebDAV server
|
||||
- No third-party analytics or tracking
|
||||
- No internet permissions except for WebDAV sync
|
||||
- All sync operations encrypted (HTTPS)
|
||||
- Open source - audit the code yourself
|
||||
- Alle Daten werden über deinen eigenen WebDAV-Server synchronisiert
|
||||
- Keine Drittanbieter-Analysen oder Tracking
|
||||
- Keine Internet-Berechtigungen außer für WebDAV-Sync
|
||||
- Alle Synchronisationsvorgänge verschlüsselt (HTTPS)
|
||||
- Open Source - prüfe den Code selbst
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Built With
|
||||
## 🛠️ Erstellt mit
|
||||
|
||||
- **Language:** Kotlin
|
||||
- **Sprache:** Kotlin
|
||||
- **UI:** Material Design 3
|
||||
- **Sync:** WorkManager + WebDAV
|
||||
- **Target SDK:** Android 16 (API 36)
|
||||
|
||||
2338
IMPROVEMENT_PLAN.md
Normal file
2338
IMPROVEMENT_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
174
README.md
174
README.md
@@ -1,143 +1,128 @@
|
||||
# Simple Notes Sync 📝
|
||||
|
||||
> Minimalistische Android-App für Offline-Notizen mit automatischer WLAN-Synchronisierung
|
||||
> **Minimalistische Android Notiz-App mit automatischer WLAN-Synchronisierung**
|
||||
|
||||
Eine schlanke Notiz-App ohne Schnickschnack - perfekt für schnelle Gedanken, die automatisch zu Hause synchronisiert werden.
|
||||
[](https://www.android.com/)
|
||||
[](https://kotlinlang.org/)
|
||||
[](https://m3.material.io/)
|
||||
[](LICENSE)
|
||||
|
||||
---
|
||||
Schlanke Offline-Notizen ohne Schnickschnack - deine Daten bleiben bei dir. Automatische Synchronisierung zu deinem eigenen WebDAV-Server, kein Google, kein Microsoft, keine Cloud.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📝 **Offline-first** - Notizen werden lokal gespeichert und sind immer verfügbar
|
||||
- 🔄 **Auto-Sync** - Automatische Synchronisierung wenn du im Heimnetzwerk bist
|
||||
- 🏠 **WebDAV Server** - Deine Daten bleiben bei dir (Docker-Container)
|
||||
- 🔋 **Akkuschonend** - Nur ~0.4% Akkuverbrauch pro Tag
|
||||
- 🚫 **Keine Cloud** - Keine Google, keine Microsoft, keine Drittanbieter
|
||||
- 🔐 **Privacy** - Keine Tracking, keine Analytics, keine Standort-Berechtigungen
|
||||
- 📝 **Offline-First** - Notizen lokal gespeichert, immer verfügbar
|
||||
- 🔄 **Auto-Sync** - Konfigurierbare Intervalle (15/30/60 Min.) mit ~0.2-0.8% Akku/Tag
|
||||
- 🏠 **Self-Hosted** - Deine Daten auf deinem Server (WebDAV)
|
||||
- 🎨 **Material Design 3** - Modern & Dynamic Theming
|
||||
- 🔋 **Akkuschonend** - Optimiert für Hintergrund-Synchronisierung
|
||||
- 🔐 **Privacy-First** - Kein Tracking, keine Analytics, keine Cloud
|
||||
- 🚫 **Keine Berechtigungen** - Nur Internet für WebDAV Sync
|
||||
|
||||
---
|
||||
## 📥 Quick Download
|
||||
|
||||
## 📥 Installation
|
||||
**Android APK:** [📱 Neueste Version herunterladen](https://github.com/inventory69/simple-notes-sync/releases/latest)
|
||||
|
||||
### Android App
|
||||
|
||||
**Option 1: APK herunterladen**
|
||||
|
||||
1. Neueste [Release](../../releases/latest) öffnen
|
||||
2. `app-debug.apk` herunterladen
|
||||
3. APK auf dem Handy installieren
|
||||
|
||||
**Option 2: Selbst bauen**
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew assembleDebug
|
||||
# APK: android/app/build/outputs/apk/debug/app-debug.apk
|
||||
```
|
||||
|
||||
### WebDAV Server
|
||||
|
||||
Der Server läuft als Docker-Container und speichert deine Notizen.
|
||||
|
||||
```bash
|
||||
cd server
|
||||
cp .env.example .env
|
||||
nano .env # Passwort anpassen!
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
**Server testen:**
|
||||
```bash
|
||||
curl -u noteuser:dein_passwort http://192.168.0.XXX:8080/
|
||||
```
|
||||
💡 **Tipp:** Nutze [Obtainium](https://github.com/ImranR98/Obtainium) für automatische Updates!
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Schnellstart
|
||||
|
||||
1. **Server starten** (siehe oben)
|
||||
2. **App installieren** und öffnen
|
||||
3. **Einstellungen öffnen** (⚙️ Symbol oben rechts)
|
||||
4. **Server konfigurieren:**
|
||||
- Server-URL: `http://192.168.0.XXX:8080/notes`
|
||||
### 1️⃣ WebDAV Server starten
|
||||
|
||||
```fish
|
||||
cd server
|
||||
cp .env.example .env
|
||||
# Passwort in .env anpassen
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 2️⃣ App installieren & konfigurieren
|
||||
|
||||
1. APK herunterladen und installieren
|
||||
2. App öffnen → **Einstellungen** (⚙️)
|
||||
3. Server konfigurieren:
|
||||
- URL: `http://192.168.0.XXX:8080/notes`
|
||||
- Benutzername: `noteuser`
|
||||
- Passwort: (aus `.env` Datei)
|
||||
- Auto-Sync: **AN**
|
||||
5. **Fertig!** Notizen werden jetzt automatisch synchronisiert
|
||||
- Passwort: (aus `.env`)
|
||||
4. **Auto-Sync aktivieren**
|
||||
5. **Sync-Intervall wählen** (15/30/60 Min.)
|
||||
|
||||
**Fertig!** Notizen werden automatisch synchronisiert 🎉
|
||||
|
||||
---
|
||||
|
||||
## 💡 Wie funktioniert Auto-Sync?
|
||||
## ⚙️ Sync-Intervalle
|
||||
|
||||
Die App prüft **alle 30 Minuten**, ob:
|
||||
- ✅ WLAN verbunden ist
|
||||
- ✅ Server im gleichen Netzwerk erreichbar ist
|
||||
- ✅ Neue Notizen vorhanden sind
|
||||
| Intervall | Akku/Tag | Anwendungsfall |
|
||||
|-----------|----------|----------------|
|
||||
| **15 Min** | ~0.8% (~23 mAh) | ⚡ Maximale Aktualität |
|
||||
| **30 Min** | ~0.4% (~12 mAh) | ✓ Empfohlen - Ausgewogen |
|
||||
| **60 Min** | ~0.2% (~6 mAh) | 🔋 Maximale Akkulaufzeit |
|
||||
|
||||
Wenn alle Bedingungen erfüllt → **Automatische Synchronisierung**
|
||||
|
||||
**Wichtig:** Funktioniert nur im selben Netzwerk wie der Server (kein Internet-Zugriff nötig!)
|
||||
💡 **Hinweis:** Android Doze Mode kann Sync im Standby auf ~60 Min. verzögern (betrifft alle Apps).
|
||||
|
||||
---
|
||||
|
||||
## 🔋 Akkuverbrauch
|
||||
## <EFBFBD> Neue Features in v1.1.0
|
||||
|
||||
| Komponente | Verbrauch/Tag |
|
||||
|------------|---------------|
|
||||
| WorkManager (alle 30 Min) | ~0.3% |
|
||||
| Netzwerk-Checks | ~0.1% |
|
||||
| **Total** | **~0.4%** |
|
||||
### Konfigurierbare Sync-Intervalle
|
||||
- ⏱️ Wählbare Intervalle: 15/30/60 Minuten
|
||||
- 📊 Transparente Akkuverbrauchs-Anzeige
|
||||
- <20> Sofortige Anwendung ohne App-Neustart
|
||||
|
||||
Bei einem 3000 mAh Akku entspricht das ~12 mAh pro Tag.
|
||||
### Über-Sektion
|
||||
- <20> App-Version & Build-Datum
|
||||
- 🌐 Links zu GitHub Repo & Entwickler
|
||||
- ⚖️ Lizenz-Information
|
||||
|
||||
### Verbesserungen
|
||||
- 🎯 Benutzerfreundliche Doze-Mode Erklärung
|
||||
- 🔕 Keine störenden Sync-Fehler Toasts im Hintergrund
|
||||
- 📝 Erweiterte Debug-Logs für Troubleshooting
|
||||
|
||||
---
|
||||
|
||||
## 📱 Screenshots
|
||||
## 🛠️ Selbst bauen
|
||||
|
||||
_TODO: Screenshots hinzufügen_
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Technische Details
|
||||
|
||||
Mehr Infos zur Architektur und Implementierung findest du in der [technischen Dokumentation](DOCS.md).
|
||||
|
||||
**Stack:**
|
||||
- **Android:** Kotlin, Material Design 3, WorkManager
|
||||
- **Server:** Docker, WebDAV (bytemark/webdav)
|
||||
- **Sync:** Sardine Android (WebDAV Client)
|
||||
```fish
|
||||
cd android
|
||||
./gradlew assembleStandardRelease
|
||||
# APK: android/app/build/outputs/apk/standard/release/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Auto-Sync funktioniert nicht
|
||||
|
||||
1. **Akku-Optimierung deaktivieren**
|
||||
- Einstellungen → Apps → Simple Notes → Akku → Nicht optimieren
|
||||
2. **WLAN-Verbindung prüfen**
|
||||
- Funktioniert nur im selben Netzwerk wie Server
|
||||
3. **Server-Status checken**
|
||||
- Settings → "Verbindung testen"
|
||||
|
||||
### Server nicht erreichbar
|
||||
|
||||
```bash
|
||||
# Server Status prüfen
|
||||
docker-compose ps
|
||||
```fish
|
||||
# Status prüfen
|
||||
docker compose ps
|
||||
|
||||
# Logs ansehen
|
||||
docker-compose logs -f
|
||||
docker compose logs -f
|
||||
|
||||
# IP-Adresse finden
|
||||
ip addr show | grep "inet " | grep -v 127.0.0.1
|
||||
```
|
||||
|
||||
### Auto-Sync funktioniert nicht
|
||||
|
||||
1. **Akku-Optimierung deaktivieren**
|
||||
- Einstellungen → Apps → Simple Notes → Akku → Nicht optimieren
|
||||
2. **WLAN Verbindung prüfen**
|
||||
- App funktioniert nur im selben Netzwerk wie der Server
|
||||
3. **Server-Status in App prüfen**
|
||||
- Settings → Server-Status sollte "Erreichbar" zeigen
|
||||
|
||||
Mehr Details in der [Dokumentation](DOCS.md).
|
||||
Mehr Details: [📖 Dokumentation](DOCS.md)
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Beitragen
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions sind willkommen! Bitte öffne ein Issue oder Pull Request.
|
||||
|
||||
@@ -149,5 +134,4 @@ MIT License - siehe [LICENSE](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
**Projekt Start:** 19. Dezember 2025
|
||||
**Status:** ✅ Funktional & Produktiv
|
||||
**Version:** 1.1.0 · **Status:** ✅ Produktiv · **Gebaut mit:** Kotlin + Material Design 3
|
||||
|
||||
@@ -5,6 +5,9 @@ plugins {
|
||||
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
android {
|
||||
namespace = "dev.dettmer.simplenotes"
|
||||
@@ -14,10 +17,13 @@ android {
|
||||
applicationId = "dev.dettmer.simplenotes"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = 2 // 🔥 F-Droid Release v1.1.0
|
||||
versionName = "1.1.0" // 🔥 Configurable Sync Interval + About Section
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// 🔥 NEU: Build Date für About Screen
|
||||
buildConfigField("String", "BUILD_DATE", "\"${getBuildDate()}\"")
|
||||
}
|
||||
|
||||
// Enable multiple APKs per ABI for smaller downloads
|
||||
@@ -29,6 +35,21 @@ android {
|
||||
isUniversalApk = true // Also generate universal APK
|
||||
}
|
||||
}
|
||||
|
||||
// Product Flavors for F-Droid and standard builds
|
||||
flavorDimensions += "distribution"
|
||||
productFlavors {
|
||||
create("fdroid") {
|
||||
dimension = "distribution"
|
||||
// F-Droid builds have no proprietary dependencies
|
||||
// All dependencies in this project are already FOSS-compatible
|
||||
}
|
||||
|
||||
create("standard") {
|
||||
dimension = "distribution"
|
||||
// Standard builds can include Play Services in the future if needed
|
||||
}
|
||||
}
|
||||
|
||||
// Signing configuration for release builds
|
||||
signingConfigs {
|
||||
@@ -86,6 +107,9 @@ dependencies {
|
||||
implementation(libs.androidx.activity)
|
||||
implementation(libs.androidx.constraintlayout)
|
||||
|
||||
// Splash Screen API (Android 12+)
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
|
||||
// Unsere Dependencies (DIREKT mit Versionen - viel einfacher!)
|
||||
implementation("com.github.thegrizzlylabs:sardine-android:0.8") {
|
||||
exclude(group = "xpp3", module = "xpp3")
|
||||
@@ -104,4 +128,10 @@ dependencies {
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
}
|
||||
|
||||
// 🔥 NEU: Helper function für Build Date
|
||||
fun getBuildDate(): String {
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
return dateFormat.format(Date())
|
||||
}
|
||||
@@ -31,7 +31,8 @@
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.SimpleNotes.Splash">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -60,6 +61,17 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- FileProvider für Log-Sharing -->
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -13,39 +13,52 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
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.color.DynamicColors
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import dev.dettmer.simplenotes.adapters.NotesAdapter
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
import dev.dettmer.simplenotes.sync.SyncWorker
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var recyclerViewNotes: RecyclerView
|
||||
private lateinit var textViewEmpty: TextView
|
||||
private lateinit var emptyStateCard: MaterialCardView
|
||||
private lateinit var fabAddNote: FloatingActionButton
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private lateinit var adapter: NotesAdapter
|
||||
private val storage by lazy { NotesStorage(this) }
|
||||
|
||||
private val prefs by lazy {
|
||||
getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
private const val REQUEST_NOTIFICATION_PERMISSION = 1001
|
||||
private const val REQUEST_SETTINGS = 1002
|
||||
private const val MIN_AUTO_SYNC_INTERVAL_MS = 60_000L // 1 Minute
|
||||
private const val PREF_LAST_AUTO_SYNC_TIME = "last_auto_sync_timestamp"
|
||||
}
|
||||
|
||||
/**
|
||||
* BroadcastReceiver für Background-Sync Completion
|
||||
* BroadcastReceiver für Background-Sync Completion (Periodic Sync)
|
||||
*/
|
||||
private val syncCompletedReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
@@ -63,9 +76,21 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// Install Splash Screen (Android 12+)
|
||||
installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Apply Dynamic Colors for Android 12+ (Material You)
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// File Logging aktivieren wenn eingestellt
|
||||
if (prefs.getBoolean("file_logging_enabled", false)) {
|
||||
Logger.enableFileLogging(this)
|
||||
}
|
||||
|
||||
// Permission für Notifications (Android 13+)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
requestNotificationPermission()
|
||||
@@ -82,14 +107,87 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
Logger.d(TAG, "📱 MainActivity.onResume() - Registering receivers")
|
||||
|
||||
// Register BroadcastReceiver für Background-Sync
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(
|
||||
syncCompletedReceiver,
|
||||
IntentFilter(SyncWorker.ACTION_SYNC_COMPLETED)
|
||||
)
|
||||
Logger.d(TAG, "📡 BroadcastReceiver registered")
|
||||
|
||||
Logger.d(TAG, "📡 BroadcastReceiver registered (sync-completed)")
|
||||
|
||||
// Reload notes
|
||||
loadNotes()
|
||||
|
||||
// Trigger Auto-Sync beim App-Wechsel in Vordergrund (Toast)
|
||||
triggerAutoSync("onResume")
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatischer Sync (onResume)
|
||||
* - Nutzt WiFi-gebundenen Socket (VPN Fix!)
|
||||
* - Nur Success-Toast (kein "Auto-Sync..." Toast)
|
||||
*
|
||||
* NOTE: WiFi-Connect Sync nutzt WorkManager (auch wenn App geschlossen!)
|
||||
*/
|
||||
private fun triggerAutoSync(source: String = "unknown") {
|
||||
// Throttling: Max 1 Sync pro Minute
|
||||
if (!canTriggerAutoSync()) {
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "🔄 Auto-sync triggered ($source)")
|
||||
|
||||
// Update last sync timestamp
|
||||
prefs.edit().putLong(PREF_LAST_AUTO_SYNC_TIME, System.currentTimeMillis()).apply()
|
||||
|
||||
// GLEICHER Sync-Code wie manueller Sync (funktioniert!)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val syncService = WebDavSyncService(this@MainActivity)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
syncService.syncNotes()
|
||||
}
|
||||
|
||||
// Feedback abhängig von Source
|
||||
if (result.isSuccess && result.syncedCount > 0) {
|
||||
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
|
||||
|
||||
// onResume: Nur Success-Toast
|
||||
showToast("✅ Gesynct: ${result.syncedCount} Notizen")
|
||||
loadNotes()
|
||||
|
||||
} else if (result.isSuccess) {
|
||||
Logger.d(TAG, "ℹ️ Auto-sync ($source): No changes")
|
||||
|
||||
} else {
|
||||
Logger.e(TAG, "❌ Auto-sync failed ($source): ${result.errorMessage}")
|
||||
// Kein Toast - App ist im Hintergrund
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 Auto-sync exception ($source): ${e.message}")
|
||||
// Kein Toast - App ist im Hintergrund
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob Auto-Sync getriggert werden darf (Throttling)
|
||||
*/
|
||||
private fun canTriggerAutoSync(): Boolean {
|
||||
val lastSyncTime = prefs.getLong(PREF_LAST_AUTO_SYNC_TIME, 0)
|
||||
val now = System.currentTimeMillis()
|
||||
val timeSinceLastSync = now - lastSyncTime
|
||||
|
||||
if (timeSinceLastSync < MIN_AUTO_SYNC_INTERVAL_MS) {
|
||||
val remainingSeconds = (MIN_AUTO_SYNC_INTERVAL_MS - timeSinceLastSync) / 1000
|
||||
Logger.d(TAG, "⏳ Auto-sync throttled - wait ${remainingSeconds}s")
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
@@ -102,7 +200,7 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun findViews() {
|
||||
recyclerViewNotes = findViewById(R.id.recyclerViewNotes)
|
||||
textViewEmpty = findViewById(R.id.textViewEmpty)
|
||||
emptyStateCard = findViewById(R.id.emptyStateCard)
|
||||
fabAddNote = findViewById(R.id.fabAddNote)
|
||||
toolbar = findViewById(R.id.toolbar)
|
||||
}
|
||||
@@ -117,6 +215,57 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
recyclerViewNotes.adapter = adapter
|
||||
recyclerViewNotes.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
// Setup Swipe-to-Delete
|
||||
setupSwipeToDelete()
|
||||
}
|
||||
|
||||
private fun setupSwipeToDelete() {
|
||||
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
0, // No drag
|
||||
ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // Swipe left or 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.currentList[position]
|
||||
val notesCopy = adapter.currentList.toMutableList()
|
||||
|
||||
// Remove from list immediately for visual feedback
|
||||
notesCopy.removeAt(position)
|
||||
adapter.submitList(notesCopy)
|
||||
|
||||
// Show Snackbar with UNDO
|
||||
Snackbar.make(
|
||||
recyclerViewNotes,
|
||||
"Notiz gelöscht",
|
||||
Snackbar.LENGTH_LONG
|
||||
).setAction("RÜCKGÄNGIG") {
|
||||
// UNDO: Restore note in list
|
||||
loadNotes()
|
||||
}.addCallback(object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
if (event != DISMISS_EVENT_ACTION) {
|
||||
// Snackbar dismissed without UNDO → Actually delete the note
|
||||
storage.deleteNote(note.id)
|
||||
loadNotes()
|
||||
}
|
||||
}
|
||||
}).show()
|
||||
}
|
||||
|
||||
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
|
||||
// Require 80% swipe to trigger
|
||||
return 0.8f
|
||||
}
|
||||
})
|
||||
|
||||
itemTouchHelper.attachToRecyclerView(recyclerViewNotes)
|
||||
}
|
||||
|
||||
private fun setupFab() {
|
||||
@@ -129,8 +278,8 @@ class MainActivity : AppCompatActivity() {
|
||||
val notes = storage.loadAllNotes()
|
||||
adapter.submitList(notes)
|
||||
|
||||
// Empty state
|
||||
textViewEmpty.visibility = if (notes.isEmpty()) {
|
||||
// Material 3 Empty State Card
|
||||
emptyStateCard.visibility = if (notes.isEmpty()) {
|
||||
android.view.View.VISIBLE
|
||||
} else {
|
||||
android.view.View.GONE
|
||||
@@ -146,7 +295,9 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun openSettings() {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
val intent = Intent(this, SettingsActivity::class.java)
|
||||
@Suppress("DEPRECATION")
|
||||
startActivityForResult(intent, REQUEST_SETTINGS)
|
||||
}
|
||||
|
||||
private fun triggerManualSync() {
|
||||
@@ -205,6 +356,16 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode == REQUEST_SETTINGS && resultCode == RESULT_OK) {
|
||||
// Restore was successful, reload notes
|
||||
loadNotes()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.MenuItem
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
@@ -27,6 +28,10 @@ class NoteEditorActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Apply Dynamic Colors for Android 12+ (Material You)
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
setContentView(R.layout.activity_editor)
|
||||
|
||||
storage = NotesStorage(this)
|
||||
@@ -89,7 +94,7 @@ class NoteEditorActivity : AppCompatActivity() {
|
||||
val content = editTextContent.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (title.isEmpty() && content.isEmpty()) {
|
||||
showToast("Titel oder Inhalt darf nicht leer sein")
|
||||
showToast("Notiz ist leer")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,25 +10,43 @@ import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.RadioGroup
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import dev.dettmer.simplenotes.sync.WebDavSyncService
|
||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.utils.showToast
|
||||
import java.io.File
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SettingsActivity"
|
||||
private const val GITHUB_REPO_URL = "https://github.com/inventory69/simple-notes-sync"
|
||||
private const val GITHUB_PROFILE_URL = "https://github.com/inventory69"
|
||||
private const val LICENSE_URL = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
|
||||
}
|
||||
|
||||
private lateinit var editTextServerUrl: EditText
|
||||
@@ -37,7 +55,20 @@ class SettingsActivity : AppCompatActivity() {
|
||||
private lateinit var switchAutoSync: SwitchCompat
|
||||
private lateinit var buttonTestConnection: Button
|
||||
private lateinit var buttonSyncNow: Button
|
||||
private lateinit var buttonRestoreFromServer: Button
|
||||
private lateinit var textViewServerStatus: TextView
|
||||
private lateinit var chipAutoSaveStatus: Chip
|
||||
|
||||
// Sync Interval UI
|
||||
private lateinit var radioGroupSyncInterval: RadioGroup
|
||||
|
||||
// About Section UI
|
||||
private lateinit var textViewAppVersion: TextView
|
||||
private lateinit var cardGitHubRepo: MaterialCardView
|
||||
private lateinit var cardDeveloperProfile: MaterialCardView
|
||||
private lateinit var cardLicense: MaterialCardView
|
||||
|
||||
private var autoSaveIndicatorJob: Job? = null
|
||||
|
||||
private val prefs by lazy {
|
||||
getSharedPreferences(Constants.PREFS_NAME, MODE_PRIVATE)
|
||||
@@ -45,6 +76,10 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Apply Dynamic Colors for Android 12+ (Material You)
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
|
||||
setContentView(R.layout.activity_settings)
|
||||
|
||||
// Setup toolbar
|
||||
@@ -58,6 +93,8 @@ class SettingsActivity : AppCompatActivity() {
|
||||
findViews()
|
||||
loadSettings()
|
||||
setupListeners()
|
||||
setupSyncIntervalPicker()
|
||||
setupAboutSection()
|
||||
}
|
||||
|
||||
private fun findViews() {
|
||||
@@ -67,7 +104,18 @@ class SettingsActivity : AppCompatActivity() {
|
||||
switchAutoSync = findViewById(R.id.switchAutoSync)
|
||||
buttonTestConnection = findViewById(R.id.buttonTestConnection)
|
||||
buttonSyncNow = findViewById(R.id.buttonSyncNow)
|
||||
buttonRestoreFromServer = findViewById(R.id.buttonRestoreFromServer)
|
||||
textViewServerStatus = findViewById(R.id.textViewServerStatus)
|
||||
chipAutoSaveStatus = findViewById(R.id.chipAutoSaveStatus)
|
||||
|
||||
// Sync Interval UI
|
||||
radioGroupSyncInterval = findViewById(R.id.radioGroupSyncInterval)
|
||||
|
||||
// About Section UI
|
||||
textViewAppVersion = findViewById(R.id.textViewAppVersion)
|
||||
cardGitHubRepo = findViewById(R.id.cardGitHubRepo)
|
||||
cardDeveloperProfile = findViewById(R.id.cardDeveloperProfile)
|
||||
cardLicense = findViewById(R.id.cardLicense)
|
||||
}
|
||||
|
||||
private fun loadSettings() {
|
||||
@@ -91,16 +139,122 @@ class SettingsActivity : AppCompatActivity() {
|
||||
syncNow()
|
||||
}
|
||||
|
||||
buttonRestoreFromServer.setOnClickListener {
|
||||
saveSettings()
|
||||
showRestoreConfirmation()
|
||||
}
|
||||
|
||||
switchAutoSync.setOnCheckedChangeListener { _, isChecked ->
|
||||
onAutoSyncToggled(isChecked)
|
||||
showAutoSaveIndicator()
|
||||
}
|
||||
|
||||
// Server Status Check bei Settings-Änderung
|
||||
editTextServerUrl.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) {
|
||||
checkServerStatus()
|
||||
showAutoSaveIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
editTextUsername.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) showAutoSaveIndicator()
|
||||
}
|
||||
|
||||
editTextPassword.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) showAutoSaveIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sync interval picker with radio buttons
|
||||
*/
|
||||
private fun setupSyncIntervalPicker() {
|
||||
// Load current interval from preferences
|
||||
val currentInterval = prefs.getLong(Constants.PREF_SYNC_INTERVAL_MINUTES, Constants.DEFAULT_SYNC_INTERVAL_MINUTES)
|
||||
|
||||
// Set checked radio button based on current interval
|
||||
val checkedId = when (currentInterval) {
|
||||
15L -> R.id.radioInterval15
|
||||
30L -> R.id.radioInterval30
|
||||
60L -> R.id.radioInterval60
|
||||
else -> R.id.radioInterval30 // Default
|
||||
}
|
||||
radioGroupSyncInterval.check(checkedId)
|
||||
|
||||
// Listen for interval changes
|
||||
radioGroupSyncInterval.setOnCheckedChangeListener { _, checkedId ->
|
||||
val newInterval = when (checkedId) {
|
||||
R.id.radioInterval15 -> 15L
|
||||
R.id.radioInterval60 -> 60L
|
||||
else -> 30L // R.id.radioInterval30 or fallback
|
||||
}
|
||||
|
||||
// Save new interval to preferences
|
||||
prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, newInterval).apply()
|
||||
|
||||
// Restart periodic sync with new interval (only if auto-sync is enabled)
|
||||
if (prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)) {
|
||||
val networkMonitor = NetworkMonitor(this)
|
||||
networkMonitor.startMonitoring()
|
||||
|
||||
val intervalText = when (newInterval) {
|
||||
15L -> "15 Minuten"
|
||||
30L -> "30 Minuten"
|
||||
60L -> "60 Minuten"
|
||||
else -> "$newInterval Minuten"
|
||||
}
|
||||
showToast("⏱️ Sync-Intervall auf $intervalText geändert")
|
||||
Logger.i(TAG, "Sync interval changed to $newInterval minutes, restarted periodic sync")
|
||||
} else {
|
||||
showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup about section with version info and clickable cards
|
||||
*/
|
||||
private fun setupAboutSection() {
|
||||
// Display app version with build date
|
||||
try {
|
||||
val versionName = BuildConfig.VERSION_NAME
|
||||
val versionCode = BuildConfig.VERSION_CODE
|
||||
val buildDate = BuildConfig.BUILD_DATE
|
||||
|
||||
textViewAppVersion.text = "Version $versionName ($versionCode)\nErstellt am: $buildDate"
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to load version info", e)
|
||||
textViewAppVersion.text = "Version nicht verfügbar"
|
||||
}
|
||||
|
||||
// GitHub Repository Card
|
||||
cardGitHubRepo.setOnClickListener {
|
||||
openUrl(GITHUB_REPO_URL)
|
||||
}
|
||||
|
||||
// Developer Profile Card
|
||||
cardDeveloperProfile.setOnClickListener {
|
||||
openUrl(GITHUB_PROFILE_URL)
|
||||
}
|
||||
|
||||
// License Card
|
||||
cardLicense.setOnClickListener {
|
||||
openUrl(LICENSE_URL)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens URL in browser
|
||||
*/
|
||||
private fun openUrl(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to open URL: $url", e)
|
||||
showToast("❌ Fehler beim Öffnen des Links")
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
@@ -122,11 +276,14 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
if (result.isSuccess) {
|
||||
showToast("Verbindung erfolgreich!")
|
||||
checkServerStatus() // ✅ Server-Status sofort aktualisieren
|
||||
} else {
|
||||
showToast("Verbindung fehlgeschlagen: ${result.errorMessage}")
|
||||
checkServerStatus() // ✅ Auch bei Fehler aktualisieren
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showToast("Fehler: ${e.message}")
|
||||
checkServerStatus() // ✅ Auch bei Exception aktualisieren
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,11 +301,14 @@ class SettingsActivity : AppCompatActivity() {
|
||||
} else {
|
||||
showToast("Erfolgreich! ${result.syncedCount} Notizen synchronisiert")
|
||||
}
|
||||
checkServerStatus() // ✅ Server-Status nach Sync aktualisieren
|
||||
} else {
|
||||
showToast("Sync fehlgeschlagen: ${result.errorMessage}")
|
||||
checkServerStatus() // ✅ Auch bei Fehler aktualisieren
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showToast("Fehler: ${e.message}")
|
||||
checkServerStatus() // ✅ Auch bei Exception aktualisieren
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,6 +420,75 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAutoSaveIndicator() {
|
||||
// Cancel previous job if still running
|
||||
autoSaveIndicatorJob?.cancel()
|
||||
|
||||
// Show saving indicator
|
||||
chipAutoSaveStatus.apply {
|
||||
visibility = android.view.View.VISIBLE
|
||||
text = "💾 Speichere..."
|
||||
setChipBackgroundColorResource(android.R.color.darker_gray)
|
||||
}
|
||||
|
||||
// Save settings
|
||||
saveSettings()
|
||||
|
||||
// Show saved confirmation after short delay
|
||||
autoSaveIndicatorJob = lifecycleScope.launch {
|
||||
delay(300) // Short delay to show "Speichere..."
|
||||
chipAutoSaveStatus.apply {
|
||||
text = "✓ Gespeichert"
|
||||
setChipBackgroundColorResource(android.R.color.holo_green_light)
|
||||
}
|
||||
delay(2000) // Show for 2 seconds
|
||||
chipAutoSaveStatus.visibility = android.view.View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRestoreConfirmation() {
|
||||
android.app.AlertDialog.Builder(this)
|
||||
.setTitle(R.string.restore_confirmation_title)
|
||||
.setMessage(R.string.restore_confirmation_message)
|
||||
.setPositiveButton(R.string.restore_button) { _, _ ->
|
||||
performRestore()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performRestore() {
|
||||
val progressDialog = android.app.ProgressDialog(this).apply {
|
||||
setMessage(getString(R.string.restore_progress))
|
||||
setCancelable(false)
|
||||
show()
|
||||
}
|
||||
|
||||
CoroutineScope(Dispatchers.Main).launch {
|
||||
try {
|
||||
val webdavService = WebDavSyncService(this@SettingsActivity)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
webdavService.restoreFromServer()
|
||||
}
|
||||
|
||||
progressDialog.dismiss()
|
||||
|
||||
if (result.isSuccess) {
|
||||
showToast(getString(R.string.restore_success, result.restoredCount))
|
||||
// Refresh MainActivity's note list
|
||||
setResult(RESULT_OK)
|
||||
} else {
|
||||
showToast(getString(R.string.restore_error, result.errorMessage))
|
||||
}
|
||||
checkServerStatus()
|
||||
} catch (e: Exception) {
|
||||
progressDialog.dismiss()
|
||||
showToast(getString(R.string.restore_error, e.message))
|
||||
checkServerStatus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package dev.dettmer.simplenotes
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.sync.NetworkMonitor
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
|
||||
class SimpleNotesApplication : Application() {
|
||||
|
||||
@@ -16,6 +18,13 @@ class SimpleNotesApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
// File-Logging ZUERST aktivieren (damit alle Logs geschrieben werden!)
|
||||
val prefs = getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
if (prefs.getBoolean("file_logging_enabled", false)) {
|
||||
Logger.enableFileLogging(this)
|
||||
Logger.d(TAG, "📝 File logging enabled at Application startup")
|
||||
}
|
||||
|
||||
Logger.d(TAG, "🚀 Application onCreate()")
|
||||
|
||||
// Initialize notification channel
|
||||
|
||||
@@ -37,5 +37,16 @@ class NotesStorage(private val context: Context) {
|
||||
return file.delete()
|
||||
}
|
||||
|
||||
fun deleteAllNotes(): Boolean {
|
||||
return try {
|
||||
notesDir.listFiles()
|
||||
?.filter { it.extension == "json" }
|
||||
?.forEach { it.delete() }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getNotesDir(): File = notesDir
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
package dev.dettmer.simplenotes.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.net.wifi.WifiManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import androidx.work.*
|
||||
import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* NetworkMonitor: Verwaltet WorkManager-basiertes Auto-Sync
|
||||
* WICHTIG: Kein NetworkCallback mehr - WorkManager macht das für uns!
|
||||
* NetworkMonitor: Verwaltet Auto-Sync
|
||||
* - Periodic WorkManager für Auto-Sync alle 30min
|
||||
* - NetworkCallback für WiFi-Connect Detection → WorkManager OneTime Sync
|
||||
*/
|
||||
class NetworkMonitor(private val context: Context) {
|
||||
|
||||
@@ -22,30 +26,145 @@ class NetworkMonitor(private val context: Context) {
|
||||
context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
private val connectivityManager by lazy {
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
}
|
||||
|
||||
// 🔥 Track last connected network ID to detect network changes (SSID wechsel, WiFi an/aus)
|
||||
// null = kein Netzwerk, sonst Network.toString() als eindeutiger Identifier
|
||||
private var lastConnectedNetworkId: String? = null
|
||||
|
||||
/**
|
||||
* Startet WorkManager mit Network Constraints
|
||||
* WorkManager kümmert sich automatisch um WiFi-Erkennung!
|
||||
* NetworkCallback: Erkennt WiFi-Verbindung und triggert WorkManager
|
||||
* WorkManager funktioniert auch wenn App geschlossen ist!
|
||||
*/
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
|
||||
Logger.d(TAG, "🌐 NetworkCallback.onAvailable() triggered")
|
||||
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
Logger.d(TAG, " Network capabilities: $capabilities")
|
||||
|
||||
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
Logger.d(TAG, " Is WiFi: $isWifi")
|
||||
|
||||
if (isWifi) {
|
||||
val currentNetworkId = network.toString()
|
||||
Logger.d(TAG, "📶 WiFi network connected: $currentNetworkId")
|
||||
|
||||
// 🔥 Trigger bei:
|
||||
// 1. WiFi aus -> WiFi an (lastConnectedNetworkId == null)
|
||||
// 2. SSID-Wechsel (lastConnectedNetworkId != currentNetworkId)
|
||||
// NICHT triggern bei: App-Restart mit gleichem WiFi
|
||||
|
||||
if (lastConnectedNetworkId != currentNetworkId) {
|
||||
if (lastConnectedNetworkId == null) {
|
||||
Logger.d(TAG, " 🎯 WiFi state changed: OFF -> ON (network: $currentNetworkId)")
|
||||
} else {
|
||||
Logger.d(TAG, " 🎯 WiFi network changed: $lastConnectedNetworkId -> $currentNetworkId")
|
||||
}
|
||||
|
||||
lastConnectedNetworkId = currentNetworkId
|
||||
|
||||
// Auto-Sync check
|
||||
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||
Logger.d(TAG, " Auto-Sync enabled: $autoSyncEnabled")
|
||||
|
||||
if (autoSyncEnabled) {
|
||||
Logger.d(TAG, " ✅ Triggering WorkManager...")
|
||||
triggerWifiConnectSync()
|
||||
} else {
|
||||
Logger.d(TAG, " ❌ Auto-sync disabled - not triggering")
|
||||
}
|
||||
} else {
|
||||
Logger.d(TAG, " ⚠️ Same WiFi network as before - ignoring (no network change)")
|
||||
}
|
||||
} else {
|
||||
Logger.d(TAG, " ⚠️ Not WiFi - ignoring")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
super.onLost(network)
|
||||
|
||||
val lostNetworkId = network.toString()
|
||||
Logger.d(TAG, "🔴 NetworkCallback.onLost() - Network disconnected: $lostNetworkId")
|
||||
|
||||
if (lastConnectedNetworkId == lostNetworkId) {
|
||||
Logger.d(TAG, " Last WiFi network lost - resetting state")
|
||||
lastConnectedNetworkId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggert WiFi-Connect Sync via WorkManager
|
||||
* WorkManager wacht App auf (funktioniert auch wenn App geschlossen!)
|
||||
*/
|
||||
private fun triggerWifiConnectSync() {
|
||||
Logger.d(TAG, "📡 Scheduling WiFi-Connect sync via WorkManager")
|
||||
|
||||
// 🔥 WICHTIG: NetworkType.UNMETERED constraint!
|
||||
// Ohne Constraint könnte WorkManager den Job auf Cellular ausführen
|
||||
// (z.B. wenn WiFi disconnected bevor Job startet)
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only!
|
||||
.build()
|
||||
|
||||
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.setConstraints(constraints) // 🔥 Constraints hinzugefügt
|
||||
.addTag(Constants.SYNC_WORK_TAG)
|
||||
.addTag("wifi-connect")
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(syncRequest)
|
||||
Logger.d(TAG, "✅ WiFi-Connect sync scheduled (WIFI ONLY, WorkManager will wake app if needed)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet WorkManager mit Network Constraints + NetworkCallback
|
||||
*/
|
||||
fun startMonitoring() {
|
||||
val autoSyncEnabled = prefs.getBoolean(Constants.KEY_AUTO_SYNC, false)
|
||||
|
||||
if (!autoSyncEnabled) {
|
||||
Logger.d(TAG, "Auto-sync disabled - stopping periodic work")
|
||||
Logger.d(TAG, "Auto-sync disabled - stopping all monitoring")
|
||||
stopMonitoring()
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "🚀 Starting WorkManager-based auto-sync")
|
||||
Logger.d(TAG, "🚀 Starting NetworkMonitor (WorkManager + WiFi Callback)")
|
||||
|
||||
// 1. WorkManager für periodic sync
|
||||
startPeriodicSync()
|
||||
|
||||
// 2. NetworkCallback für WiFi-Connect Detection
|
||||
startWifiMonitoring()
|
||||
}
|
||||
|
||||
/**
|
||||
* Startet WorkManager periodic sync
|
||||
* 🔥 Interval aus SharedPrefs konfigurierbar (15/30/60 min)
|
||||
*/
|
||||
private fun startPeriodicSync() {
|
||||
// 🔥 Interval aus SharedPrefs lesen
|
||||
val intervalMinutes = prefs.getLong(
|
||||
Constants.PREF_SYNC_INTERVAL_MINUTES,
|
||||
Constants.DEFAULT_SYNC_INTERVAL_MINUTES
|
||||
)
|
||||
|
||||
Logger.d(TAG, "📅 Configuring periodic sync: ${intervalMinutes}min interval")
|
||||
|
||||
// Constraints: Nur wenn WiFi connected
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi only
|
||||
.build()
|
||||
|
||||
// Periodic Work Request - prüft alle 30 Minuten (Battery optimized)
|
||||
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(
|
||||
30, TimeUnit.MINUTES, // Optimiert: 30 Min statt 15 Min
|
||||
10, TimeUnit.MINUTES // Flex interval
|
||||
intervalMinutes, TimeUnit.MINUTES, // 🔥 Dynamisch!
|
||||
5, TimeUnit.MINUTES // Flex interval
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.addTag(Constants.SYNC_WORK_TAG)
|
||||
@@ -53,107 +172,103 @@ class NetworkMonitor(private val context: Context) {
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
AUTO_SYNC_WORK_NAME,
|
||||
ExistingPeriodicWorkPolicy.UPDATE, // UPDATE statt KEEP für immediate trigger
|
||||
ExistingPeriodicWorkPolicy.UPDATE, // 🔥 Update bei Interval-Änderung
|
||||
syncRequest
|
||||
)
|
||||
|
||||
Logger.d(TAG, "✅ Periodic auto-sync scheduled (every 30min when on WiFi)")
|
||||
|
||||
// Trigger sofortigen Sync wenn WiFi bereits connected
|
||||
triggerImmediateSync()
|
||||
Logger.d(TAG, "✅ Periodic sync scheduled (every ${intervalMinutes}min)")
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt WorkManager Auto-Sync
|
||||
* Startet NetworkCallback für WiFi-Connect Detection
|
||||
*/
|
||||
private fun startWifiMonitoring() {
|
||||
try {
|
||||
Logger.d(TAG, "🚀 Starting WiFi monitoring...")
|
||||
|
||||
val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
Logger.d(TAG, " NetworkRequest built: WIFI + INTERNET capability")
|
||||
|
||||
connectivityManager.registerNetworkCallback(request, networkCallback)
|
||||
Logger.d(TAG, "✅✅✅ WiFi NetworkCallback registered successfully")
|
||||
Logger.d(TAG, " Callback will trigger on WiFi connect/disconnect")
|
||||
|
||||
// 🔥 FIX: Initialisiere wasWifiConnected State beim Start
|
||||
// onAvailable() wird nur bei NEUEN Verbindungen getriggert!
|
||||
initializeWifiState()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌❌❌ Failed to register NetworkCallback", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialisiert lastConnectedNetworkId beim App-Start
|
||||
* Wichtig damit wir echte Netzwerk-Wechsel von App-Restarts unterscheiden können
|
||||
*/
|
||||
private fun initializeWifiState() {
|
||||
try {
|
||||
Logger.d(TAG, "🔍 Initializing WiFi state...")
|
||||
|
||||
val activeNetwork = connectivityManager.activeNetwork
|
||||
if (activeNetwork == null) {
|
||||
Logger.d(TAG, " ❌ No active network - lastConnectedNetworkId = null")
|
||||
lastConnectedNetworkId = null
|
||||
return
|
||||
}
|
||||
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
|
||||
val isWifi = capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
|
||||
if (isWifi) {
|
||||
lastConnectedNetworkId = activeNetwork.toString()
|
||||
Logger.d(TAG, " ✅ Initial WiFi network: $lastConnectedNetworkId")
|
||||
Logger.d(TAG, " 📡 WiFi already connected at startup - onAvailable() will only trigger on network change")
|
||||
} else {
|
||||
lastConnectedNetworkId = null
|
||||
Logger.d(TAG, " ⚠️ Not on WiFi at startup")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Error initializing WiFi state", e)
|
||||
lastConnectedNetworkId = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob WiFi aktuell verbunden ist
|
||||
* @return true wenn WiFi verbunden, false sonst (Cellular, offline, etc.)
|
||||
*/
|
||||
fun isWiFiConnected(): Boolean {
|
||||
return try {
|
||||
val activeNetwork = connectivityManager.activeNetwork ?: return false
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false
|
||||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Error checking WiFi status", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stoppt WorkManager Auto-Sync + NetworkCallback
|
||||
*/
|
||||
fun stopMonitoring() {
|
||||
Logger.d(TAG, "🛑 Stopping auto-sync")
|
||||
|
||||
// Stop WorkManager
|
||||
WorkManager.getInstance(context).cancelUniqueWork(AUTO_SYNC_WORK_NAME)
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger sofortigen Sync (z.B. nach Settings-Änderung)
|
||||
*/
|
||||
private fun triggerImmediateSync() {
|
||||
if (!isConnectedToHomeWifi()) {
|
||||
Logger.d(TAG, "Not on home WiFi - skipping immediate sync")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.d(TAG, "<EFBFBD> Triggering immediate sync...")
|
||||
|
||||
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.addTag(Constants.SYNC_WORK_TAG)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueue(syncRequest)
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob connected zu Home WiFi via Gateway IP Check
|
||||
*/
|
||||
private fun isConnectedToHomeWifi(): Boolean {
|
||||
val gatewayIP = getGatewayIP() ?: return false
|
||||
|
||||
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
|
||||
if (serverUrl.isNullOrEmpty()) return false
|
||||
|
||||
val serverIP = extractIPFromUrl(serverUrl)
|
||||
if (serverIP == null) return false
|
||||
|
||||
val sameNetwork = isSameNetwork(gatewayIP, serverIP)
|
||||
Logger.d(TAG, "Gateway: $gatewayIP, Server: $serverIP → Same network: $sameNetwork")
|
||||
|
||||
return sameNetwork
|
||||
}
|
||||
|
||||
private fun getGatewayIP(): String? {
|
||||
return try {
|
||||
val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
|
||||
as WifiManager
|
||||
val dhcpInfo = wifiManager.dhcpInfo
|
||||
val gateway = dhcpInfo.gateway
|
||||
|
||||
val ip = String.format(
|
||||
"%d.%d.%d.%d",
|
||||
gateway and 0xFF,
|
||||
(gateway shr 8) and 0xFF,
|
||||
(gateway shr 16) and 0xFF,
|
||||
(gateway shr 24) and 0xFF
|
||||
)
|
||||
ip
|
||||
// Unregister NetworkCallback
|
||||
try {
|
||||
connectivityManager.unregisterNetworkCallback(networkCallback)
|
||||
Logger.d(TAG, "✅ WiFi monitoring stopped")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to get gateway IP: ${e.message}")
|
||||
null
|
||||
// Already unregistered
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractIPFromUrl(url: String): String? {
|
||||
return try {
|
||||
val urlObj = java.net.URL(url)
|
||||
val host = urlObj.host
|
||||
|
||||
if (host.matches(Regex("\\d+\\.\\d+\\.\\d+\\.\\d+"))) {
|
||||
host
|
||||
} else {
|
||||
val addr = java.net.InetAddress.getByName(host)
|
||||
addr.hostAddress
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "Failed to extract IP: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSameNetwork(ip1: String, ip2: String): Boolean {
|
||||
val parts1 = ip1.split(".")
|
||||
val parts2 = ip2.split(".")
|
||||
|
||||
if (parts1.size != 4 || parts2.size != 4) return false
|
||||
|
||||
return parts1[0] == parts2[0] &&
|
||||
parts1[1] == parts2[1] &&
|
||||
parts1[2] == parts2[2]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.content.Intent
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import dev.dettmer.simplenotes.BuildConfig
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import dev.dettmer.simplenotes.utils.NotificationHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -21,25 +22,72 @@ class SyncWorker(
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
Logger.d(TAG, "🔄 SyncWorker started")
|
||||
Logger.d(TAG, "Context: ${applicationContext.javaClass.simpleName}")
|
||||
Logger.d(TAG, "Thread: ${Thread.currentThread().name}")
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
Logger.d(TAG, "🔄 SyncWorker.doWork() ENTRY")
|
||||
Logger.d(TAG, "Context: ${applicationContext.javaClass.simpleName}")
|
||||
Logger.d(TAG, "Thread: ${Thread.currentThread().name}")
|
||||
Logger.d(TAG, "RunAttempt: $runAttemptCount")
|
||||
}
|
||||
|
||||
return@withContext try {
|
||||
// Start sync (kein "in progress" notification mehr)
|
||||
val syncService = WebDavSyncService(applicationContext)
|
||||
Logger.d(TAG, "🚀 Starting sync...")
|
||||
Logger.d(TAG, "📊 Attempt: ${runAttemptCount}")
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "📍 Step 1: Before WebDavSyncService creation")
|
||||
}
|
||||
|
||||
val result = syncService.syncNotes()
|
||||
// Try-catch um Service-Creation
|
||||
val syncService = try {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Creating WebDavSyncService with applicationContext...")
|
||||
}
|
||||
WebDavSyncService(applicationContext).also {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " ✅ WebDavSyncService created successfully")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in WebDavSyncService constructor!", e)
|
||||
Logger.e(TAG, "Exception: ${e.javaClass.name}: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "📍 Step 2: Before syncNotes() call")
|
||||
Logger.d(TAG, " SyncService: $syncService")
|
||||
}
|
||||
|
||||
// Try-catch um syncNotes
|
||||
val result = try {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Calling syncService.syncNotes()...")
|
||||
}
|
||||
syncService.syncNotes().also {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " ✅ syncNotes() returned")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in syncNotes()!", e)
|
||||
Logger.e(TAG, "Exception: ${e.javaClass.name}: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "📍 Step 3: Processing result")
|
||||
Logger.d(TAG, "📦 Sync result: success=${result.isSuccess}, count=${result.syncedCount}, error=${result.errorMessage}")
|
||||
}
|
||||
|
||||
if (result.isSuccess) {
|
||||
Logger.d(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "📍 Step 4: Success path")
|
||||
}
|
||||
Logger.i(TAG, "✅ Sync successful: ${result.syncedCount} notes")
|
||||
|
||||
// Nur Notification zeigen wenn tatsächlich etwas gesynct wurde
|
||||
if (result.syncedCount > 0) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Showing success notification...")
|
||||
}
|
||||
NotificationHelper.showSyncSuccess(
|
||||
applicationContext,
|
||||
result.syncedCount
|
||||
@@ -49,10 +97,20 @@ class SyncWorker(
|
||||
}
|
||||
|
||||
// **UI REFRESH**: Broadcast für MainActivity
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Broadcasting sync completed...")
|
||||
}
|
||||
broadcastSyncCompleted(true, result.syncedCount)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "✅ SyncWorker.doWork() SUCCESS")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
Result.success()
|
||||
} else {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "📍 Step 4: Failure path")
|
||||
}
|
||||
Logger.e(TAG, "❌ Sync failed: ${result.errorMessage}")
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
@@ -62,19 +120,39 @@ class SyncWorker(
|
||||
// Broadcast auch bei Fehler (damit UI refresht)
|
||||
broadcastSyncCompleted(false, 0)
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "❌ SyncWorker.doWork() FAILURE")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
Result.failure()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
Logger.e(TAG, "💥💥💥 FATAL EXCEPTION in doWork() 💥💥💥")
|
||||
Logger.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||
Logger.e(TAG, "Exception message: ${e.message}")
|
||||
Logger.e(TAG, "Stack trace:", e)
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
e.message ?: "Unknown error"
|
||||
)
|
||||
|
||||
broadcastSyncCompleted(false, 0)
|
||||
try {
|
||||
NotificationHelper.showSyncError(
|
||||
applicationContext,
|
||||
e.message ?: "Unknown error"
|
||||
)
|
||||
} catch (notifError: Exception) {
|
||||
Logger.e(TAG, "Failed to show error notification", notifError)
|
||||
}
|
||||
|
||||
try {
|
||||
broadcastSyncCompleted(false, 0)
|
||||
} catch (broadcastError: Exception) {
|
||||
Logger.e(TAG, "Failed to broadcast", broadcastError)
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
Result.failure()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package dev.dettmer.simplenotes.sync
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import com.thegrizzlylabs.sardineandroid.Sardine
|
||||
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
|
||||
import dev.dettmer.simplenotes.BuildConfig
|
||||
import dev.dettmer.simplenotes.models.Note
|
||||
import dev.dettmer.simplenotes.models.SyncStatus
|
||||
import dev.dettmer.simplenotes.storage.NotesStorage
|
||||
@@ -10,6 +13,14 @@ import dev.dettmer.simplenotes.utils.Constants
|
||||
import dev.dettmer.simplenotes.utils.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.NetworkInterface
|
||||
import java.net.Proxy
|
||||
import java.net.Socket
|
||||
import javax.net.SocketFactory
|
||||
|
||||
class WebDavSyncService(private val context: Context) {
|
||||
|
||||
@@ -17,17 +28,158 @@ class WebDavSyncService(private val context: Context) {
|
||||
private const val TAG = "WebDavSyncService"
|
||||
}
|
||||
|
||||
private val storage = NotesStorage(context)
|
||||
private val storage: NotesStorage
|
||||
private val prefs = context.getSharedPreferences(Constants.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
init {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
Logger.d(TAG, "🏗️ WebDavSyncService INIT")
|
||||
Logger.d(TAG, "Context: ${context.javaClass.simpleName}")
|
||||
Logger.d(TAG, "Thread: ${Thread.currentThread().name}")
|
||||
}
|
||||
|
||||
try {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " Creating NotesStorage...")
|
||||
}
|
||||
storage = NotesStorage(context)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " ✅ NotesStorage created successfully")
|
||||
Logger.d(TAG, " Notes dir: ${storage.getNotesDir()}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in NotesStorage creation!", e)
|
||||
Logger.e(TAG, "Exception: ${e.javaClass.name}: ${e.message}")
|
||||
throw e
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Logger.d(TAG, " SharedPreferences: $prefs")
|
||||
Logger.d(TAG, "✅ WebDavSyncService INIT complete")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Findet WiFi Interface IP-Adresse (um VPN zu umgehen)
|
||||
*/
|
||||
private fun getWiFiInetAddress(): InetAddress? {
|
||||
try {
|
||||
Logger.d(TAG, "🔍 getWiFiInetAddress() called")
|
||||
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val network = connectivityManager.activeNetwork
|
||||
Logger.d(TAG, " Active network: $network")
|
||||
|
||||
if (network == null) {
|
||||
Logger.d(TAG, "❌ No active network")
|
||||
return null
|
||||
}
|
||||
|
||||
val capabilities = connectivityManager.getNetworkCapabilities(network)
|
||||
Logger.d(TAG, " Network capabilities: $capabilities")
|
||||
|
||||
if (capabilities == null) {
|
||||
Logger.d(TAG, "❌ No network capabilities")
|
||||
return null
|
||||
}
|
||||
|
||||
// Nur wenn WiFi aktiv
|
||||
if (!capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||
Logger.d(TAG, "⚠️ Not on WiFi, using default routing")
|
||||
return null
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Network is WiFi, searching for interface...")
|
||||
|
||||
// Finde WiFi Interface
|
||||
val interfaces = NetworkInterface.getNetworkInterfaces()
|
||||
while (interfaces.hasMoreElements()) {
|
||||
val iface = interfaces.nextElement()
|
||||
|
||||
Logger.d(TAG, " Checking interface: ${iface.name}, isUp=${iface.isUp}")
|
||||
|
||||
// WiFi Interfaces: wlan0, wlan1, etc.
|
||||
if (!iface.name.startsWith("wlan")) continue
|
||||
if (!iface.isUp) continue
|
||||
|
||||
val addresses = iface.inetAddresses
|
||||
while (addresses.hasMoreElements()) {
|
||||
val addr = addresses.nextElement()
|
||||
|
||||
Logger.d(TAG, " Address: ${addr.hostAddress}, IPv4=${addr is Inet4Address}, loopback=${addr.isLoopbackAddress}, linkLocal=${addr.isLinkLocalAddress}")
|
||||
|
||||
// Nur IPv4, nicht loopback, nicht link-local
|
||||
if (addr is Inet4Address && !addr.isLoopbackAddress && !addr.isLinkLocalAddress) {
|
||||
Logger.d(TAG, "✅ Found WiFi IP: ${addr.hostAddress} on ${iface.name}")
|
||||
return addr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.w(TAG, "⚠️ No WiFi interface found, using default routing")
|
||||
return null
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Failed to get WiFi interface", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom SocketFactory die an WiFi-IP bindet (VPN Fix)
|
||||
*/
|
||||
private inner class WiFiSocketFactory(private val wifiAddress: InetAddress) : SocketFactory() {
|
||||
override fun createSocket(): Socket {
|
||||
val socket = Socket()
|
||||
socket.bind(InetSocketAddress(wifiAddress, 0))
|
||||
Logger.d(TAG, "🔌 Socket bound to WiFi IP: ${wifiAddress.hostAddress}")
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun createSocket(host: String, port: Int): Socket {
|
||||
val socket = createSocket()
|
||||
socket.connect(InetSocketAddress(host, port))
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
|
||||
return createSocket(host, port)
|
||||
}
|
||||
|
||||
override fun createSocket(host: InetAddress, port: Int): Socket {
|
||||
val socket = createSocket()
|
||||
socket.connect(InetSocketAddress(host, port))
|
||||
return socket
|
||||
}
|
||||
|
||||
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
|
||||
return createSocket(address, port)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSardine(): Sardine? {
|
||||
val username = prefs.getString(Constants.KEY_USERNAME, null) ?: return null
|
||||
val password = prefs.getString(Constants.KEY_PASSWORD, null) ?: return null
|
||||
|
||||
// Einfach standard OkHttpSardine - funktioniert im manuellen Sync!
|
||||
android.util.Log.d(TAG, "🔧 Creating OkHttpSardine")
|
||||
Logger.d(TAG, "🔧 Creating OkHttpSardine with WiFi binding")
|
||||
Logger.d(TAG, " Context: ${context.javaClass.simpleName}")
|
||||
|
||||
return OkHttpSardine().apply {
|
||||
// Versuche WiFi-IP zu finden
|
||||
val wifiAddress = getWiFiInetAddress()
|
||||
|
||||
val okHttpClient = if (wifiAddress != null) {
|
||||
Logger.d(TAG, "✅ Using WiFi-bound socket factory")
|
||||
OkHttpClient.Builder()
|
||||
.socketFactory(WiFiSocketFactory(wifiAddress))
|
||||
.build()
|
||||
} else {
|
||||
Logger.d(TAG, "⚠️ Using default OkHttpClient (no WiFi binding)")
|
||||
OkHttpClient.Builder().build()
|
||||
}
|
||||
|
||||
return OkHttpSardine(okHttpClient).apply {
|
||||
setCredentials(username, password)
|
||||
}
|
||||
}
|
||||
@@ -83,58 +235,102 @@ class WebDavSyncService(private val context: Context) {
|
||||
}
|
||||
|
||||
suspend fun syncNotes(): SyncResult = withContext(Dispatchers.IO) {
|
||||
android.util.Log.d(TAG, "🔄 syncNotes() called")
|
||||
android.util.Log.d(TAG, "Context: ${context.javaClass.simpleName}")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
Logger.d(TAG, "🔄 syncNotes() ENTRY")
|
||||
Logger.d(TAG, "Context: ${context.javaClass.simpleName}")
|
||||
Logger.d(TAG, "Thread: ${Thread.currentThread().name}")
|
||||
|
||||
return@withContext try {
|
||||
val sardine = getSardine()
|
||||
Logger.d(TAG, "📍 Step 1: Getting Sardine client")
|
||||
|
||||
val sardine = try {
|
||||
getSardine()
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in getSardine()!", e)
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
|
||||
if (sardine == null) {
|
||||
android.util.Log.e(TAG, "❌ Sardine is null - credentials missing")
|
||||
Logger.e(TAG, "❌ Sardine is null - credentials missing")
|
||||
return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-Zugangsdaten nicht konfiguriert"
|
||||
)
|
||||
}
|
||||
Logger.d(TAG, " ✅ Sardine client created")
|
||||
|
||||
Logger.d(TAG, "📍 Step 2: Getting server URL")
|
||||
val serverUrl = getServerUrl()
|
||||
if (serverUrl == null) {
|
||||
android.util.Log.e(TAG, "❌ Server URL is null")
|
||||
Logger.e(TAG, "❌ Server URL is null")
|
||||
return@withContext SyncResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-URL nicht konfiguriert"
|
||||
)
|
||||
}
|
||||
|
||||
android.util.Log.d(TAG, "📡 Server URL: $serverUrl")
|
||||
android.util.Log.d(TAG, "🔐 Credentials configured: ${prefs.getString(Constants.KEY_USERNAME, null) != null}")
|
||||
Logger.d(TAG, "📡 Server URL: $serverUrl")
|
||||
Logger.d(TAG, "🔐 Credentials configured: ${prefs.getString(Constants.KEY_USERNAME, null) != null}")
|
||||
|
||||
var syncedCount = 0
|
||||
var conflictCount = 0
|
||||
|
||||
Logger.d(TAG, "📍 Step 3: Checking server directory")
|
||||
// Ensure server directory exists
|
||||
android.util.Log.d(TAG, "🔍 Checking if server directory exists...")
|
||||
if (!sardine.exists(serverUrl)) {
|
||||
android.util.Log.d(TAG, "📁 Creating server directory...")
|
||||
sardine.createDirectory(serverUrl)
|
||||
try {
|
||||
Logger.d(TAG, "🔍 Checking if server directory exists...")
|
||||
if (!sardine.exists(serverUrl)) {
|
||||
Logger.d(TAG, "📁 Creating server directory...")
|
||||
sardine.createDirectory(serverUrl)
|
||||
}
|
||||
Logger.d(TAG, " ✅ Server directory ready")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH checking/creating server directory!", e)
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📍 Step 4: Uploading local notes")
|
||||
// Upload local notes
|
||||
android.util.Log.d(TAG, "⬆️ Uploading local notes...")
|
||||
val uploadedCount = uploadLocalNotes(sardine, serverUrl)
|
||||
syncedCount += uploadedCount
|
||||
android.util.Log.d(TAG, "✅ Uploaded: $uploadedCount notes")
|
||||
try {
|
||||
Logger.d(TAG, "⬆️ Uploading local notes...")
|
||||
val uploadedCount = uploadLocalNotes(sardine, serverUrl)
|
||||
syncedCount += uploadedCount
|
||||
Logger.d(TAG, "✅ Uploaded: $uploadedCount notes")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in uploadLocalNotes()!", e)
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📍 Step 5: Downloading remote notes")
|
||||
// Download remote notes
|
||||
android.util.Log.d(TAG, "⬇️ Downloading remote notes...")
|
||||
val downloadResult = downloadRemoteNotes(sardine, serverUrl)
|
||||
syncedCount += downloadResult.downloadedCount
|
||||
conflictCount += downloadResult.conflictCount
|
||||
android.util.Log.d(TAG, "✅ Downloaded: ${downloadResult.downloadedCount} notes, Conflicts: ${downloadResult.conflictCount}")
|
||||
try {
|
||||
Logger.d(TAG, "⬇️ Downloading remote notes...")
|
||||
val downloadResult = downloadRemoteNotes(sardine, serverUrl)
|
||||
syncedCount += downloadResult.downloadedCount
|
||||
conflictCount += downloadResult.conflictCount
|
||||
Logger.d(TAG, "✅ Downloaded: ${downloadResult.downloadedCount} notes, Conflicts: ${downloadResult.conflictCount}")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH in downloadRemoteNotes()!", e)
|
||||
e.printStackTrace()
|
||||
throw e
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📍 Step 6: Saving sync timestamp")
|
||||
// Update last sync timestamp
|
||||
saveLastSyncTimestamp()
|
||||
try {
|
||||
saveLastSyncTimestamp()
|
||||
Logger.d(TAG, " ✅ Timestamp saved")
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "💥 CRASH saving timestamp!", e)
|
||||
e.printStackTrace()
|
||||
// Non-fatal, continue
|
||||
}
|
||||
|
||||
android.util.Log.d(TAG, "🎉 Sync completed successfully - Total synced: $syncedCount")
|
||||
Logger.d(TAG, "🎉 Sync completed successfully - Total synced: $syncedCount")
|
||||
Logger.d(TAG, "═══════════════════════════════════════")
|
||||
|
||||
SyncResult(
|
||||
isSuccess = true,
|
||||
@@ -143,8 +339,13 @@ class WebDavSyncService(private val context: Context) {
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e(TAG, "💥 Sync exception: ${e.message}", e)
|
||||
android.util.Log.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||
Logger.e(TAG, "═══════════════════════════════════════")
|
||||
Logger.e(TAG, "💥💥💥 FATAL EXCEPTION in syncNotes() 💥💥💥")
|
||||
Logger.e(TAG, "Exception type: ${e.javaClass.name}")
|
||||
Logger.e(TAG, "Exception message: ${e.message}")
|
||||
Logger.e(TAG, "Stack trace:")
|
||||
e.printStackTrace()
|
||||
Logger.e(TAG, "═══════════════════════════════════════")
|
||||
|
||||
SyncResult(
|
||||
isSuccess = false,
|
||||
@@ -253,4 +454,95 @@ class WebDavSyncService(private val context: Context) {
|
||||
fun getLastSyncTimestamp(): Long {
|
||||
return prefs.getLong(Constants.KEY_LAST_SYNC, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore all notes from server - overwrites local storage
|
||||
* @return RestoreResult with count of restored notes
|
||||
*/
|
||||
suspend fun restoreFromServer(): RestoreResult = withContext(Dispatchers.IO) {
|
||||
return@withContext try {
|
||||
val sardine = getSardine() ?: return@withContext RestoreResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-Zugangsdaten nicht konfiguriert",
|
||||
restoredCount = 0
|
||||
)
|
||||
|
||||
val serverUrl = getServerUrl() ?: return@withContext RestoreResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Server-URL nicht konfiguriert",
|
||||
restoredCount = 0
|
||||
)
|
||||
|
||||
Logger.d(TAG, "🔄 Starting restore from server...")
|
||||
|
||||
// List all files on server
|
||||
val resources = sardine.list(serverUrl)
|
||||
val jsonFiles = resources.filter {
|
||||
!it.isDirectory && it.name.endsWith(".json")
|
||||
}
|
||||
|
||||
Logger.d(TAG, "📂 Found ${jsonFiles.size} files on server")
|
||||
|
||||
val restoredNotes = mutableListOf<Note>()
|
||||
|
||||
// Download and parse each file
|
||||
for (resource in jsonFiles) {
|
||||
try {
|
||||
val fileUrl = serverUrl.trimEnd('/') + "/" + resource.name
|
||||
val content = sardine.get(fileUrl).bufferedReader().use { it.readText() }
|
||||
|
||||
val note = Note.fromJson(content)
|
||||
if (note != null) {
|
||||
restoredNotes.add(note)
|
||||
Logger.d(TAG, "✅ Downloaded: ${note.title}")
|
||||
} else {
|
||||
Logger.e(TAG, "❌ Failed to parse ${resource.name}: Note.fromJson returned null")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Failed to download ${resource.name}", e)
|
||||
// Continue with other files
|
||||
}
|
||||
}
|
||||
|
||||
if (restoredNotes.isEmpty()) {
|
||||
return@withContext RestoreResult(
|
||||
isSuccess = false,
|
||||
errorMessage = "Keine Notizen auf Server gefunden",
|
||||
restoredCount = 0
|
||||
)
|
||||
}
|
||||
|
||||
// Clear local storage
|
||||
Logger.d(TAG, "🗑️ Clearing local storage...")
|
||||
storage.deleteAllNotes()
|
||||
|
||||
// Save all restored notes
|
||||
Logger.d(TAG, "💾 Saving ${restoredNotes.size} notes...")
|
||||
restoredNotes.forEach { note ->
|
||||
storage.saveNote(note.copy(syncStatus = SyncStatus.SYNCED))
|
||||
}
|
||||
|
||||
Logger.d(TAG, "✅ Restore completed: ${restoredNotes.size} notes")
|
||||
|
||||
RestoreResult(
|
||||
isSuccess = true,
|
||||
errorMessage = null,
|
||||
restoredCount = restoredNotes.size
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Logger.e(TAG, "❌ Restore failed", e)
|
||||
RestoreResult(
|
||||
isSuccess = false,
|
||||
errorMessage = e.message ?: "Unbekannter Fehler",
|
||||
restoredCount = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class RestoreResult(
|
||||
val isSuccess: Boolean,
|
||||
val errorMessage: String?,
|
||||
val restoredCount: Int
|
||||
)
|
||||
|
||||
@@ -10,6 +10,10 @@ object Constants {
|
||||
const val KEY_AUTO_SYNC = "auto_sync_enabled"
|
||||
const val KEY_LAST_SYNC = "last_sync_timestamp"
|
||||
|
||||
// 🔥 NEU: Sync Interval Configuration
|
||||
const val PREF_SYNC_INTERVAL_MINUTES = "sync_interval_minutes"
|
||||
const val DEFAULT_SYNC_INTERVAL_MINUTES = 30L
|
||||
|
||||
// WorkManager
|
||||
const val SYNC_WORK_TAG = "notes_sync"
|
||||
const val SYNC_DELAY_SECONDS = 5L
|
||||
|
||||
@@ -1,30 +1,122 @@
|
||||
package dev.dettmer.simplenotes.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dev.dettmer.simplenotes.BuildConfig
|
||||
import java.io.File
|
||||
import java.io.FileWriter
|
||||
import java.io.PrintWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Logger: Debug logs nur bei DEBUG builds
|
||||
* Logger: Debug logs nur bei DEBUG builds + File Logging
|
||||
* Release builds zeigen nur Errors/Warnings
|
||||
*/
|
||||
object Logger {
|
||||
|
||||
private var fileLoggingEnabled = false
|
||||
private var logFile: File? = null
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||
private val maxLogEntries = 500 // Nur letzte 500 Einträge
|
||||
|
||||
/**
|
||||
* Aktiviert File-Logging für Debugging
|
||||
*/
|
||||
fun enableFileLogging(context: Context) {
|
||||
try {
|
||||
logFile = File(context.filesDir, "simplenotes_debug.log")
|
||||
fileLoggingEnabled = true
|
||||
|
||||
// Clear old log
|
||||
logFile?.writeText("")
|
||||
|
||||
i("Logger", "📝 File logging enabled: ${logFile?.absolutePath}")
|
||||
} catch (e: Exception) {
|
||||
Log.e("Logger", "Failed to enable file logging", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deaktiviert File-Logging
|
||||
*/
|
||||
fun disableFileLogging() {
|
||||
fileLoggingEnabled = false
|
||||
i("Logger", "📝 File logging disabled")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Log-Datei zurück
|
||||
*/
|
||||
fun getLogFile(): File? = logFile
|
||||
|
||||
/**
|
||||
* Schreibt Log-Eintrag in Datei
|
||||
*/
|
||||
private fun writeToFile(level: String, tag: String, message: String, throwable: Throwable? = null) {
|
||||
if (!fileLoggingEnabled || logFile == null) return
|
||||
|
||||
try {
|
||||
val timestamp = dateFormat.format(Date())
|
||||
val logEntry = buildString {
|
||||
append("$timestamp [$level] $tag: $message\n")
|
||||
throwable?.let {
|
||||
append(" Exception: ${it.message}\n")
|
||||
append(" ${it.stackTraceToString()}\n")
|
||||
}
|
||||
}
|
||||
|
||||
// Append to file
|
||||
FileWriter(logFile, true).use { writer ->
|
||||
writer.write(logEntry)
|
||||
}
|
||||
|
||||
// Trim file if too large
|
||||
trimLogFile()
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("Logger", "Failed to write to log file", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Begrenzt Log-Datei auf maxLogEntries
|
||||
*/
|
||||
private fun trimLogFile() {
|
||||
try {
|
||||
val lines = logFile?.readLines() ?: return
|
||||
if (lines.size > maxLogEntries) {
|
||||
val trimmed = lines.takeLast(maxLogEntries)
|
||||
logFile?.writeText(trimmed.joinToString("\n") + "\n")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("Logger", "Failed to trim log file", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun d(tag: String, message: String) {
|
||||
// Logcat nur in DEBUG builds
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(tag, message)
|
||||
}
|
||||
// File-Logging IMMER (wenn enabled)
|
||||
writeToFile("DEBUG", tag, message)
|
||||
}
|
||||
|
||||
fun v(tag: String, message: String) {
|
||||
// Logcat nur in DEBUG builds
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.v(tag, message)
|
||||
}
|
||||
// File-Logging IMMER (wenn enabled)
|
||||
writeToFile("VERBOSE", tag, message)
|
||||
}
|
||||
|
||||
fun i(tag: String, message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.i(tag, message)
|
||||
}
|
||||
// INFO logs IMMER zeigen (auch in Release) - wichtige Events
|
||||
Log.i(tag, message)
|
||||
// File-Logging IMMER (wenn enabled)
|
||||
writeToFile("INFO", tag, message)
|
||||
}
|
||||
|
||||
// Errors und Warnings IMMER zeigen (auch in Release)
|
||||
@@ -34,9 +126,11 @@ object Logger {
|
||||
} else {
|
||||
Log.e(tag, message)
|
||||
}
|
||||
writeToFile("ERROR", tag, message, throwable)
|
||||
}
|
||||
|
||||
fun w(tag: String, message: String) {
|
||||
Log.w(tag, message)
|
||||
writeToFile("WARN", tag, message)
|
||||
}
|
||||
}
|
||||
|
||||
10
android/app/src/main/res/drawable/ic_splash_icon.xml
Normal file
10
android/app/src/main/res/drawable/ic_splash_icon.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- Simple Note Icon for Splash Screen -->
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M32,24h44c4.4,0 8,3.6 8,8v44c0,4.4 -3.6,8 -8,8H32c-4.4,0 -8,-3.6 -8,-8V32c0,-4.4 3.6,-8 8,-8zM40,40v6h28v-6H40zM40,52v6h28v-6H40zM40,64v6h20v-6H40z"/>
|
||||
</vector>
|
||||
@@ -5,31 +5,46 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<!-- Material 3 Toolbar -->
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:elevation="0dp"
|
||||
app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel"
|
||||
app:title="@string/edit_note" />
|
||||
app:title="@string/edit_note"
|
||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
<!-- Material 3 Outlined TextInputLayout with 16dp corners -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="16dp"
|
||||
android:hint="@string/title"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="100">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:maxLines="2" />
|
||||
android:maxLines="2"
|
||||
android:maxLength="100"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Material 3 Outlined TextInputLayout for Content -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
@@ -38,7 +53,14 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="@string/content"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:boxCornerRadiusTopStart="16dp"
|
||||
app:boxCornerRadiusTopEnd="16dp"
|
||||
app:boxCornerRadiusBottomStart="16dp"
|
||||
app:boxCornerRadiusBottomEnd="16dp"
|
||||
app:endIconMode="clear_text"
|
||||
app:counterEnabled="true"
|
||||
app:counterMaxLength="10000">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextContent"
|
||||
@@ -46,7 +68,9 @@
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="top|start"
|
||||
android:inputType="textMultiLine"
|
||||
android:scrollbars="vertical" />
|
||||
android:scrollbars="vertical"
|
||||
android:maxLength="10000"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
@@ -4,39 +4,83 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<!-- Material 3: AppBarLayout ohne Elevation für bessere Material You Integration -->
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
app:elevation="0dp"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/app_name" />
|
||||
android:elevation="0dp"
|
||||
app:title="@string/app_name"
|
||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<!-- RecyclerView mit größerem Padding für Material 3 -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerViewNotes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:padding="8dp"
|
||||
android:padding="16dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewEmpty"
|
||||
android:layout_width="wrap_content"
|
||||
<!-- Material 3 Empty State Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/emptyStateCard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/no_notes_yet"
|
||||
android:textSize="18sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:visibility="gone"
|
||||
app:cardElevation="0dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
style="@style/Widget.Material3.CardView.Filled">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:gravity="center">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_state_emoji"
|
||||
android:textSize="64sp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_state_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/empty_state_message"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:gravity="center" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Material 3: Extended FAB (wird später mit Text erweitert in Task 11) -->
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAddNote"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -44,6 +88,7 @@
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="16dp"
|
||||
android:contentDescription="@string/add_note"
|
||||
app:srcCompat="@android:drawable/ic_input_add" />
|
||||
app:srcCompat="@android:drawable/ic_input_add"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Large" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
@@ -5,19 +5,24 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<!-- Material 3 Toolbar -->
|
||||
<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/settings"
|
||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1">
|
||||
android:layout_weight="1"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -25,145 +30,554 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<!-- Server Settings -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
<!-- Auto-Save Status Indicator -->
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chipAutoSaveStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_settings"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="16dp" />
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone"
|
||||
android:textSize="12sp"
|
||||
app:chipIconEnabled="false" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/server_url"
|
||||
android:layout_marginBottom="8dp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<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="8dp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox">
|
||||
|
||||
<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"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
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>
|
||||
|
||||
<!-- WiFi Settings -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/wifi_settings"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- Server Status -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Server-Status:"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewServerStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Prüfe..."
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Info Box -->
|
||||
<TextView
|
||||
<!-- Material 3 Card: Server Configuration -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:padding="12dp"
|
||||
android:background="@drawable/info_background"
|
||||
android:text="ℹ️ 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)"
|
||||
android:textSize="14sp"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textColor="@android:color/black" />
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/auto_sync"
|
||||
android:textSize="16sp"
|
||||
android:layout_gravity="center_vertical" />
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switchAutoSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
<!-- Section Header -->
|
||||
<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" />
|
||||
|
||||
</LinearLayout>
|
||||
<!-- Server URL with Icon -->
|
||||
<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"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:startIconDrawable="@android:drawable/ic_menu_compass"
|
||||
app:endIconMode="clear_text"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusBottomEnd="12dp">
|
||||
|
||||
<!-- Actions -->
|
||||
<Button
|
||||
android:id="@+id/buttonTestConnection"
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextServerUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Username with Icon -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/username"
|
||||
android:layout_marginBottom="12dp"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:startIconDrawable="@android:drawable/ic_menu_myplaces"
|
||||
app:endIconMode="clear_text"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusBottomEnd="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextUsername"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Password with Icon -->
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/password"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
app:startIconDrawable="@android:drawable/ic_lock_lock"
|
||||
app:endIconMode="password_toggle"
|
||||
app:boxCornerRadiusTopStart="12dp"
|
||||
app:boxCornerRadiusTopEnd="12dp"
|
||||
app:boxCornerRadiusBottomStart="12dp"
|
||||
app:boxCornerRadiusBottomEnd="12dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/editTextPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textPassword"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Server Status Chip -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/server_status_label"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewServerStatus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/server_status_checking"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:textColor="?attr/colorOutline" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonTestConnection"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/test_connection"
|
||||
android:layout_marginEnd="8dp"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonSyncNow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_now"
|
||||
style="@style/Widget.Material3.Button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Material 3 Card: Auto-Sync Settings -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/test_connection"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/buttonSyncNow"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Section Header -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_settings"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- Info Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/auto_sync_info"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:lineSpacingMultiplier="1.3" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Auto-Sync Switch with MaterialSwitch -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/auto_sync"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switchAutoSync"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Material 3 Card: Backup & Restore -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/sync_now"
|
||||
android:layout_marginTop="8dp" />
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Section Header -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/backup_restore_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- Warning Info Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="?attr/colorErrorContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="@string/backup_restore_warning"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:lineSpacingMultiplier="1.3" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Restore Button -->
|
||||
<Button
|
||||
android:id="@+id/buttonRestoreFromServer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/restore_from_server"
|
||||
style="@style/Widget.Material3.Button.TonalButton" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Material 3 Card: Sync Interval Configuration -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Section Header -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sync-Intervall"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<!-- Info Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp"
|
||||
android:text="Legt fest, wie oft die App im Hintergrund synchronisiert. Kürzere Intervalle bedeuten aktuellere Daten, verbrauchen aber etwas mehr Akku.\n\n⏱️ Hinweis: Wenn dein Smartphone im Standby ist, kann Android die Synchronisation verzögern (bis zu 60 Min.), um Akku zu sparen. Das ist normal und betrifft alle Hintergrund-Apps."
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:lineSpacingMultiplier="1.3" />
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Sync Interval Radio Group -->
|
||||
<RadioGroup
|
||||
android:id="@+id/radioGroupSyncInterval"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- 15 Minutes Option -->
|
||||
<RadioButton
|
||||
android:id="@+id/radioInterval15"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⚡ Alle 15 Minuten"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:paddingVertical="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh)"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOutline"
|
||||
android:paddingStart="48dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<!-- 30 Minutes Option (Recommended) -->
|
||||
<RadioButton
|
||||
android:id="@+id/radioInterval30"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="✓ Alle 30 Minuten (Empfohlen)"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:paddingVertical="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh)"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOutline"
|
||||
android:paddingStart="48dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<!-- 60 Minutes Option -->
|
||||
<RadioButton
|
||||
android:id="@+id/radioInterval60"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔋 Alle 60 Minuten"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:paddingVertical="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt)"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOutline"
|
||||
android:paddingStart="48dp" />
|
||||
|
||||
</RadioGroup>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Material 3 Card: About Section -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
app:cardCornerRadius="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Section Header -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Über diese App"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<!-- App Info Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<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="📱 App-Version"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewAppVersion"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Version wird geladen..."
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- GitHub Repository Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardGitHubRepo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<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="🌐 GitHub Repository"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Quellcode, Issues & Dokumentation"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Developer Profile Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardDeveloperProfile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<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="👤 Entwickler"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GitHub Profil: @inventory69"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- License Card -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/cardLicense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<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="⚖️ Lizenz"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:layout_marginBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="MIT License - Open Source"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Material 3 Bottom Sheet Dialog für Lösch-Bestätigung -->
|
||||
<LinearLayout
|
||||
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:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<!-- Icon -->
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:src="@android:drawable/ic_menu_delete"
|
||||
android:tint="?attr/colorError"
|
||||
android:contentDescription="@string/delete" />
|
||||
|
||||
<!-- Title -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/delete_note_title"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<!-- Message -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/delete_note_message"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnCancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/cancel"
|
||||
android:layout_marginEnd="8dp"
|
||||
style="@style/Widget.Material3.Button.TextButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDelete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/delete"
|
||||
app:backgroundTint="?attr/colorError"
|
||||
style="@style/Widget.Material3.Button" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,61 +1,70 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Material 3: Filled Card Style (Flat, No Shadow) -->
|
||||
<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="4dp"
|
||||
app:cardElevation="2dp"
|
||||
app:cardCornerRadius="8dp">
|
||||
android:layout_marginHorizontal="8dp"
|
||||
android:layout_marginVertical="6dp"
|
||||
style="@style/Widget.Material3.CardView.Filled"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
android:padding="20dp">
|
||||
|
||||
<!-- Material 3 Typography: TitleMedium -->
|
||||
<TextView
|
||||
android:id="@+id/textViewTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Note Title"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/note_title_placeholder"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:maxLines="2"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<!-- Material 3 Typography: BodyMedium -->
|
||||
<TextView
|
||||
android:id="@+id/textViewContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="Note content preview..."
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/note_content_placeholder"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end" />
|
||||
|
||||
<!-- Metadata Row mit Timestamp und Sync-Status -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<!-- Material 3 Typography: LabelSmall -->
|
||||
<TextView
|
||||
android:id="@+id/textViewTimestamp"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="Vor 2 Std"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorTertiary" />
|
||||
android:text="@string/note_timestamp_placeholder"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textColor="?attr/colorOutline" />
|
||||
|
||||
<!-- Sync-Status Icon mit Theme-Farbe -->
|
||||
<ImageView
|
||||
android:id="@+id/imageViewSyncStatus"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="@android:drawable/ic_popup_sync"
|
||||
android:tint="?attr/colorPrimary"
|
||||
android:contentDescription="@string/sync_status" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
33
android/app/src/main/res/values-night/colors.xml
Normal file
33
android/app/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Material 3 Dark Theme Colors -->
|
||||
<color name="md_theme_dark_primary">#D0BCFF</color>
|
||||
<color name="md_theme_dark_onPrimary">#381E72</color>
|
||||
<color name="md_theme_dark_primaryContainer">#4F378B</color>
|
||||
<color name="md_theme_dark_onPrimaryContainer">#EADDFF</color>
|
||||
|
||||
<color name="md_theme_dark_secondary">#CCC2DC</color>
|
||||
<color name="md_theme_dark_onSecondary">#332D41</color>
|
||||
<color name="md_theme_dark_secondaryContainer">#4A4458</color>
|
||||
<color name="md_theme_dark_onSecondaryContainer">#E8DEF8</color>
|
||||
|
||||
<color name="md_theme_dark_tertiary">#EFB8C8</color>
|
||||
<color name="md_theme_dark_onTertiary">#492532</color>
|
||||
<color name="md_theme_dark_tertiaryContainer">#633B48</color>
|
||||
<color name="md_theme_dark_onTertiaryContainer">#FFD8E4</color>
|
||||
|
||||
<color name="md_theme_dark_error">#F2B8B5</color>
|
||||
<color name="md_theme_dark_onError">#601410</color>
|
||||
<color name="md_theme_dark_errorContainer">#8C1D18</color>
|
||||
<color name="md_theme_dark_onErrorContainer">#F9DEDC</color>
|
||||
|
||||
<color name="md_theme_dark_background">#1C1B1F</color>
|
||||
<color name="md_theme_dark_onBackground">#E6E1E5</color>
|
||||
<color name="md_theme_dark_surface">#1C1B1F</color>
|
||||
<color name="md_theme_dark_onSurface">#E6E1E5</color>
|
||||
<color name="md_theme_dark_surfaceVariant">#49454F</color>
|
||||
<color name="md_theme_dark_onSurfaceVariant">#CAC4D0</color>
|
||||
|
||||
<color name="md_theme_dark_outline">#938F99</color>
|
||||
<color name="md_theme_dark_outlineVariant">#49454F</color>
|
||||
</resources>
|
||||
@@ -1,7 +1,35 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.SimpleNotes" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your dark theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
|
||||
<!-- Material 3 Dark Color System -->
|
||||
<item name="colorPrimary">@color/md_theme_dark_primary</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_dark_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/md_theme_dark_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/md_theme_dark_onPrimaryContainer</item>
|
||||
|
||||
<item name="colorSecondary">@color/md_theme_dark_secondary</item>
|
||||
<item name="colorOnSecondary">@color/md_theme_dark_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/md_theme_dark_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/md_theme_dark_onSecondaryContainer</item>
|
||||
|
||||
<item name="colorTertiary">@color/md_theme_dark_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/md_theme_dark_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/md_theme_dark_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/md_theme_dark_onTertiaryContainer</item>
|
||||
|
||||
<item name="colorError">@color/md_theme_dark_error</item>
|
||||
<item name="colorOnError">@color/md_theme_dark_onError</item>
|
||||
<item name="colorErrorContainer">@color/md_theme_dark_errorContainer</item>
|
||||
<item name="colorOnErrorContainer">@color/md_theme_dark_onErrorContainer</item>
|
||||
|
||||
<item name="android:colorBackground">@color/md_theme_dark_background</item>
|
||||
<item name="colorOnBackground">@color/md_theme_dark_onBackground</item>
|
||||
<item name="colorSurface">@color/md_theme_dark_surface</item>
|
||||
<item name="colorOnSurface">@color/md_theme_dark_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/md_theme_dark_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/md_theme_dark_onSurfaceVariant</item>
|
||||
|
||||
<item name="colorOutline">@color/md_theme_dark_outline</item>
|
||||
<item name="colorOutlineVariant">@color/md_theme_dark_outlineVariant</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -1,5 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Base colors -->
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<!-- Material 3 Light Theme Colors -->
|
||||
<color name="md_theme_light_primary">#6750A4</color>
|
||||
<color name="md_theme_light_onPrimary">#FFFFFF</color>
|
||||
<color name="md_theme_light_primaryContainer">#EADDFF</color>
|
||||
<color name="md_theme_light_onPrimaryContainer">#21005D</color>
|
||||
|
||||
<color name="md_theme_light_secondary">#625B71</color>
|
||||
<color name="md_theme_light_onSecondary">#FFFFFF</color>
|
||||
<color name="md_theme_light_secondaryContainer">#E8DEF8</color>
|
||||
<color name="md_theme_light_onSecondaryContainer">#1D192B</color>
|
||||
|
||||
<color name="md_theme_light_tertiary">#7D5260</color>
|
||||
<color name="md_theme_light_onTertiary">#FFFFFF</color>
|
||||
<color name="md_theme_light_tertiaryContainer">#FFD8E4</color>
|
||||
<color name="md_theme_light_onTertiaryContainer">#31111D</color>
|
||||
|
||||
<color name="md_theme_light_error">#B3261E</color>
|
||||
<color name="md_theme_light_onError">#FFFFFF</color>
|
||||
<color name="md_theme_light_errorContainer">#F9DEDC</color>
|
||||
<color name="md_theme_light_onErrorContainer">#410E0B</color>
|
||||
|
||||
<color name="md_theme_light_background">#FFFBFE</color>
|
||||
<color name="md_theme_light_onBackground">#1C1B1F</color>
|
||||
<color name="md_theme_light_surface">#FFFBFE</color>
|
||||
<color name="md_theme_light_onSurface">#1C1B1F</color>
|
||||
<color name="md_theme_light_surfaceVariant">#E7E0EC</color>
|
||||
<color name="md_theme_light_onSurfaceVariant">#49454F</color>
|
||||
|
||||
<color name="md_theme_light_outline">#79747E</color>
|
||||
<color name="md_theme_light_outlineVariant">#CAC4D0</color>
|
||||
|
||||
<!-- Material 3 Dark Theme Colors (base declarations for lint) -->
|
||||
<color name="md_theme_dark_primary">#D0BCFF</color>
|
||||
<color name="md_theme_dark_onPrimary">#381E72</color>
|
||||
<color name="md_theme_dark_primaryContainer">#4F378B</color>
|
||||
<color name="md_theme_dark_onPrimaryContainer">#EADDFF</color>
|
||||
|
||||
<color name="md_theme_dark_secondary">#CCC2DC</color>
|
||||
<color name="md_theme_dark_onSecondary">#332D41</color>
|
||||
<color name="md_theme_dark_secondaryContainer">#4A4458</color>
|
||||
<color name="md_theme_dark_onSecondaryContainer">#E8DEF8</color>
|
||||
|
||||
<color name="md_theme_dark_tertiary">#EFB8C8</color>
|
||||
<color name="md_theme_dark_onTertiary">#492532</color>
|
||||
<color name="md_theme_dark_tertiaryContainer">#633B48</color>
|
||||
<color name="md_theme_dark_onTertiaryContainer">#FFD8E4</color>
|
||||
|
||||
<color name="md_theme_dark_error">#F2B8B5</color>
|
||||
<color name="md_theme_dark_onError">#601410</color>
|
||||
<color name="md_theme_dark_errorContainer">#8C1D18</color>
|
||||
<color name="md_theme_dark_onErrorContainer">#F9DEDC</color>
|
||||
|
||||
<color name="md_theme_dark_background">#1C1B1F</color>
|
||||
<color name="md_theme_dark_onBackground">#E6E1E5</color>
|
||||
<color name="md_theme_dark_surface">#1C1B1F</color>
|
||||
<color name="md_theme_dark_onSurface">#E6E1E5</color>
|
||||
<color name="md_theme_dark_surfaceVariant">#49454F</color>
|
||||
<color name="md_theme_dark_onSurfaceVariant">#CAC4D0</color>
|
||||
|
||||
<color name="md_theme_dark_outline">#938F99</color>
|
||||
<color name="md_theme_dark_outlineVariant">#49454F</color>
|
||||
</resources>
|
||||
@@ -1,23 +1,60 @@
|
||||
<resources>
|
||||
<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>
|
||||
|
||||
<!-- Empty State -->
|
||||
<string name="empty_state_emoji">📝</string>
|
||||
<string name="empty_state_title">Noch keine Notizen</string>
|
||||
<string name="empty_state_message">Tippe auf ➕ um deine erste Notiz zu erstellen</string>
|
||||
|
||||
<!-- Note 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>
|
||||
|
||||
<!-- Note List Item (Preview placeholders) -->
|
||||
<string name="note_title_placeholder">Note Title</string>
|
||||
<string name="note_content_placeholder">Note content preview…</string>
|
||||
<string name="note_timestamp_placeholder">Vor 2 Std</string>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<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>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="server_settings">Server-Einstellungen</string>
|
||||
<string name="server_url">Server URL</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="server_status_label">Server-Status:</string>
|
||||
<string name="server_status_checking">Prüfe…</string>
|
||||
<string name="test_connection">Verbindung testen</string>
|
||||
<string name="sync_now">Jetzt synchronisieren</string>
|
||||
|
||||
<!-- Auto-Sync Settings -->
|
||||
<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="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>
|
||||
|
||||
<!-- Backup & Restore -->
|
||||
<string name="backup_restore_title">Backup & Wiederherstellung</string>
|
||||
<string name="backup_restore_warning">⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="restore_from_server">Vom Server wiederherstellen</string>
|
||||
<string name="restore_confirmation_title">⚠️ Vom Server wiederherstellen?</string>
|
||||
<string name="restore_confirmation_message">WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden!</string>
|
||||
<string name="restore_button">Wiederherstellen</string>
|
||||
<string name="restore_progress">Stelle Notizen wieder her…</string>
|
||||
<string name="restore_success">✓ %d Notizen wiederhergestellt</string>
|
||||
<string name="restore_error">Fehler: %s</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Base.Theme.SimpleNotes" parent="Theme.Material3.DayNight.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
<!-- Material 3 Color System -->
|
||||
<item name="colorPrimary">@color/md_theme_light_primary</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_light_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/md_theme_light_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/md_theme_light_onPrimaryContainer</item>
|
||||
|
||||
<item name="colorSecondary">@color/md_theme_light_secondary</item>
|
||||
<item name="colorOnSecondary">@color/md_theme_light_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/md_theme_light_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/md_theme_light_onSecondaryContainer</item>
|
||||
|
||||
<item name="colorTertiary">@color/md_theme_light_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/md_theme_light_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/md_theme_light_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/md_theme_light_onTertiaryContainer</item>
|
||||
|
||||
<item name="colorError">@color/md_theme_light_error</item>
|
||||
<item name="colorOnError">@color/md_theme_light_onError</item>
|
||||
<item name="colorErrorContainer">@color/md_theme_light_errorContainer</item>
|
||||
<item name="colorOnErrorContainer">@color/md_theme_light_onErrorContainer</item>
|
||||
|
||||
<item name="android:colorBackground">@color/md_theme_light_background</item>
|
||||
<item name="colorOnBackground">@color/md_theme_light_onBackground</item>
|
||||
<item name="colorSurface">@color/md_theme_light_surface</item>
|
||||
<item name="colorOnSurface">@color/md_theme_light_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/md_theme_light_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/md_theme_light_onSurfaceVariant</item>
|
||||
|
||||
<item name="colorOutline">@color/md_theme_light_outline</item>
|
||||
<item name="colorOutlineVariant">@color/md_theme_light_outlineVariant</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.SimpleNotes" parent="Base.Theme.SimpleNotes" />
|
||||
</resources>
|
||||
|
||||
<!-- Splash Screen Theme (Android 12+) -->
|
||||
<style name="Theme.SimpleNotes.Splash" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">?attr/colorPrimary</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash_icon</item>
|
||||
<item name="windowSplashScreenAnimationDuration">500</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.SimpleNotes</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
5
android/app/src/main/res/xml/file_paths.xml
Normal file
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- App-internal files directory -->
|
||||
<files-path name="logs" path="." />
|
||||
</paths>
|
||||
38
android/fastlane/README.md
Normal file
38
android/fastlane/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# F-Droid Metadata
|
||||
|
||||
Diese Verzeichnisstruktur enthält alle Metadaten für die F-Droid-Veröffentlichung.
|
||||
|
||||
## Struktur
|
||||
|
||||
```
|
||||
fastlane/metadata/android/de-DE/
|
||||
├── title.txt # App-Name (max 50 Zeichen)
|
||||
├── short_description.txt # Kurzbeschreibung (max 80 Zeichen)
|
||||
├── full_description.txt # Vollständige Beschreibung (max 4000 Zeichen)
|
||||
├── changelogs/
|
||||
│ └── 1.txt # Changelog für Version 1
|
||||
└── images/
|
||||
└── phoneScreenshots/ # Screenshots (PNG/JPG, 320-3840px breit)
|
||||
├── 1.png # Hauptansicht (Notizliste)
|
||||
├── 2.png # Notiz-Editor
|
||||
├── 3.png # Settings
|
||||
└── 4.png # Empty State
|
||||
```
|
||||
|
||||
## Screenshots erstellen
|
||||
|
||||
Verwende einen Android Emulator oder physisches Gerät mit:
|
||||
- Material You Theme aktiviert
|
||||
- Deutsche Sprache
|
||||
- Screenshots in hoher Auflösung (1080x2400 empfohlen)
|
||||
|
||||
### Screenshot-Reihenfolge:
|
||||
1. **Notizliste** - Mit mehreren Beispiel-Notizen, Sync-Status sichtbar
|
||||
2. **Editor** - Zeige eine bearbeitete Notiz mit Titel und Inhalt
|
||||
3. **Settings** - Server-Konfiguration mit erfolgreichem Server-Status
|
||||
4. **Empty State** - Schöne leere Ansicht mit Material 3 Card
|
||||
|
||||
## F-Droid Build-Konfiguration
|
||||
|
||||
Die App verwendet den `fdroid` Build-Flavor ohne proprietäre Dependencies.
|
||||
Siehe `build.gradle.kts` für Details.
|
||||
8
android/fastlane/metadata/android/de-DE/changelogs/1.txt
Normal file
8
android/fastlane/metadata/android/de-DE/changelogs/1.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
• Material Design 3 mit Dynamic Colors
|
||||
• Swipe-to-Delete mit Bestätigungsdialog
|
||||
• Server Backup & Restore Funktion
|
||||
• Verbesserte Empty State Ansicht
|
||||
• Deutsche Lokalisierung
|
||||
• Splash Screen Support (Android 12+)
|
||||
• Performance-Verbesserungen
|
||||
• Bug-Fixes
|
||||
5
android/fastlane/metadata/android/de-DE/changelogs/2.txt
Normal file
5
android/fastlane/metadata/android/de-DE/changelogs/2.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
• Konfigurierbares Sync-Interval (15/30/60 Minuten)
|
||||
• Transparente Batterie-Verbrauchsanzeige (gemessen: 0.4%/Tag bei 30min)
|
||||
• Doze Mode Optimierungen für zuverlässigere Background-Syncs
|
||||
• About-Section mit App-Informationen und GitHub-Links
|
||||
• Diverse Bugfixes und Performance-Verbesserungen
|
||||
37
android/fastlane/metadata/android/de-DE/full_description.txt
Normal file
37
android/fastlane/metadata/android/de-DE/full_description.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
Simple Notes Sync ist eine minimalistische Notizen-App mit WebDAV-Synchronisation.
|
||||
|
||||
HAUPTFUNKTIONEN:
|
||||
|
||||
• Einfache Notizen erstellen und bearbeiten
|
||||
• WebDAV-Synchronisation mit eigenem Server
|
||||
• Automatische Synchronisation im Heim-WLAN
|
||||
• Konfigurierbares Sync-Interval (15/30/60 Minuten)
|
||||
• Transparente Batterie-Verbrauchsanzeige
|
||||
• Material Design 3 mit Dynamic Colors (Android 12+)
|
||||
• Swipe-to-Delete mit Bestätigungsdialog
|
||||
• Server-Backup & Wiederherstellung
|
||||
• Komplett offline nutzbar
|
||||
• Keine Werbung, keine Tracker
|
||||
|
||||
DATENSCHUTZ:
|
||||
|
||||
Deine Daten bleiben bei dir! Die App kommuniziert nur mit deinem eigenen WebDAV-Server. Keine Cloud-Dienste, keine Tracking-Bibliotheken, keine Analysetools.
|
||||
|
||||
SYNCHRONISATION:
|
||||
|
||||
• Unterstützt alle WebDAV-Server (Nextcloud, ownCloud, etc.)
|
||||
• Konfigurierbares Interval: 15, 30 oder 60 Minuten
|
||||
• Gemessener Akkuverbrauch: nur ~0.4% pro Tag (bei 30min)
|
||||
• Doze Mode optimiert für zuverlässige Background-Syncs
|
||||
• Manuelle Synchronisation jederzeit möglich
|
||||
• Konfliktfreie Zusammenführung durch Timestamps
|
||||
|
||||
MATERIAL DESIGN 3:
|
||||
|
||||
• Moderne Benutzeroberfläche
|
||||
• Dynamic Colors (Material You) auf Android 12+
|
||||
• Dark Mode Support
|
||||
• Intuitive Gesten (Swipe-to-Delete)
|
||||
|
||||
Open Source unter MIT-Lizenz
|
||||
Quellcode: https://github.com/inventory69/simple-notes-sync
|
||||
@@ -0,0 +1 @@
|
||||
Einfache Notizen-App mit WebDAV-Synchronisation
|
||||
1
android/fastlane/metadata/android/de-DE/title.txt
Normal file
1
android/fastlane/metadata/android/de-DE/title.txt
Normal file
@@ -0,0 +1 @@
|
||||
Simple Notes Sync
|
||||
Reference in New Issue
Block a user