feat: automatischer Sync nach Login/Reconnect, manuelle Push/Pull-Buttons entfernen
- WebSocket Connected-Event löst jetzt automatisch pullSync() aus (nach Login = Full Sync, nach Reconnect = inkrementell) - Push/Pull-Buttons ersetzt durch Hinweis 'Synchronisierung erfolgt automatisch' + Fallback-Button 'Jetzt synchronisieren' - ServerUrl: Default-Wert (VPS-IP) als Konstante in SettingsKey, Reset-Button neben dem URL-Feld - SettingsUiState: serverUrl Default = DEFAULT_SERVER_URL - Tests angepasst (SettingsRepositoryImplTest)
This commit is contained in:
parent
0fee89ec32
commit
5434c00f20
8 changed files with 348 additions and 31 deletions
60
.github/prompts/publish.prompt.md
vendored
Normal file
60
.github/prompts/publish.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
description: "publish – Neue App-Version bauen und auf dem VPS veröffentlichen. Bumpt die Version, baut die APK, lädt sie auf den Server hoch und verifiziert QR-Code-Seite + Update-API."
|
||||||
|
name: "publish"
|
||||||
|
agent: agent
|
||||||
|
tools: [read, search, execute/runInTerminal, execute/sendToTerminal, execute/getTerminalOutput, edit]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lies **zuerst** die Publish-Skill-Datei `.github/skills/publish/SKILL.md` vollständig mit `read_file`.
|
||||||
|
|
||||||
|
Führe danach den vollständigen Publish-Workflow durch:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 1 – Version ermitteln
|
||||||
|
|
||||||
|
1. Lies `app/build.gradle.kts` und ermittle den aktuellen `versionCode` und `versionName`.
|
||||||
|
2. Frage den User, auf welche Version gebumpt werden soll (Vorschlag: versionCode +1, versionName minor bump).
|
||||||
|
3. Ändere `versionCode` und `versionName` in `app/build.gradle.kts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 2 – Build & Test (Quality Gate)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat assembleDebug test
|
||||||
|
```
|
||||||
|
|
||||||
|
- **BUILD SUCCESSFUL** → weiter mit Schritt 3.
|
||||||
|
- **Fehler** → analysieren und beheben (max. 3 Zyklen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 3 – Commit & Push
|
||||||
|
|
||||||
|
1. Erstelle einen Commit: `release: v{versionName} (build {versionCode})`
|
||||||
|
2. Push auf origin (gemäß Git-Konventionen: ankündigen, User kann abbrechen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 4 – APK auf VPS veröffentlichen
|
||||||
|
|
||||||
|
Führe das Publish-Skript aus:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& ".github/skills/publish/publish-apk.ps1" -VersionCode <code> -VersionName "<name>"
|
||||||
|
```
|
||||||
|
|
||||||
|
Verwende `mode=sync` mit `timeout=120000`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 5 – Ergebnis berichten
|
||||||
|
|
||||||
|
Berichte kurz:
|
||||||
|
- Neue Version (versionCode + versionName)
|
||||||
|
- Build/Test-Status (✅ / ❌)
|
||||||
|
- Push-Status (✅ / ❌)
|
||||||
|
- VPS-Deployment (✅ / ❌)
|
||||||
|
- Homepage-URL: `http://195.246.231.210:8080/`
|
||||||
|
- Version-API: `http://195.246.231.210:8080/api/version`
|
||||||
140
.github/skills/publish/SKILL.md
vendored
Normal file
140
.github/skills/publish/SKILL.md
vendored
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
---
|
||||||
|
name: publish
|
||||||
|
description: "APK publizieren: Version bumpen, APK bauen, auf VPS hochladen, Server-Version aktualisieren, QR-Code/Download-Seite verifizieren. Verwende diesen Skill immer wenn eine neue App-Version veröffentlicht werden soll – auch wenn der User nur 'publish', 'release', 'veröffentlichen', 'neue Version', 'APK hochladen', 'APK deployen', 'Update bereitstellen' oder 'QR-Code aktualisieren' sagt. Trigger-Phrasen: 'publish', 'release', 'veröffentlichen', 'APK hochladen', 'neue Version', 'Update ausrollen', 'QR-Code'."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill: Publish (APK auf VPS veröffentlichen)
|
||||||
|
|
||||||
|
Dieser Skill deckt den vollständigen Workflow ab, um eine neue App-Version auf dem VPS bereitzustellen, sodass:
|
||||||
|
|
||||||
|
1. Die **Homepage** (QR-Code + Download-Link) die aktuelle APK anbietet
|
||||||
|
2. Die **Update-Prüfung in der App** die neue Version erkennt und zum Download anbietet
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architektur-Überblick
|
||||||
|
|
||||||
|
```
|
||||||
|
App (lokal) VPS (195.246.231.210)
|
||||||
|
───────────── ─────────────────────
|
||||||
|
app/build.gradle.kts /opt/krisenvorrat/
|
||||||
|
versionCode / versionName ├── docker-compose.yml (KRISENVORRAT_APP_VERSION_CODE/NAME)
|
||||||
|
├── data/app-latest.apk (statisch ausgeliefert)
|
||||||
|
./gradlew assembleDebug └── Server-Container (Ktor)
|
||||||
|
→ app-debug.apk ├── GET / → Homepage mit QR-Code
|
||||||
|
├── GET /api/version → JSON {versionCode, versionName, apkUrl}
|
||||||
|
scp → /opt/krisenvorrat/data/ └── GET /static/* → Dateien aus data/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wie die Update-Prüfung funktioniert
|
||||||
|
|
||||||
|
1. App ruft `GET {serverUrl}/api/version` auf → erhält `VersionInfo(versionCode, versionName, apkUrl)`
|
||||||
|
2. `CheckForUpdateUseCase` vergleicht `versionInfo.versionCode > BuildConfig.VERSION_CODE`
|
||||||
|
3. Falls neuer → zeigt Download-Button, lädt APK herunter, installiert via `ApkInstaller`
|
||||||
|
|
||||||
|
### Wie die Homepage funktioniert
|
||||||
|
|
||||||
|
- `GET /` liefert HTML mit QR-Code (via qrcodejs) + Download-Link auf `/static/app-latest.apk`
|
||||||
|
- Version wird aus `KRISENVORRAT_APP_VERSION_CODE` / `KRISENVORRAT_APP_VERSION_NAME` Env-Vars gelesen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Publish-Workflow (Schritte)
|
||||||
|
|
||||||
|
### Schritt 1 – Version bumpen
|
||||||
|
|
||||||
|
In `app/build.gradle.kts`:
|
||||||
|
- `versionCode` um 1 erhöhen
|
||||||
|
- `versionName` passend anpassen (z.B. "1.2" → "1.3")
|
||||||
|
|
||||||
|
### Schritt 2 – Build & Test
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\gradlew.bat assembleDebug test
|
||||||
|
```
|
||||||
|
|
||||||
|
Die APK liegt danach unter: `app/build/outputs/apk/debug/app-debug.apk`
|
||||||
|
|
||||||
|
### Schritt 3 – APK auf VPS hochladen
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
scp app/build/outputs/apk/debug/app-debug.apk root@195.246.231.210:/opt/krisenvorrat/data/app-latest.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
**Voraussetzung:** SSH-Agent muss laufen und Key geladen sein (siehe vps-deploy Skill).
|
||||||
|
|
||||||
|
### Schritt 4 – Server-Version aktualisieren
|
||||||
|
|
||||||
|
Die Version wird über Environment-Variablen in der `docker-compose.yml` auf dem VPS gesetzt:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Neue Werte per sed in docker-compose.yml eintragen
|
||||||
|
ssh root@195.246.231.210 "cd /opt/krisenvorrat && sed -i 's/KRISENVORRAT_APP_VERSION_CODE=.*/KRISENVORRAT_APP_VERSION_CODE=<neuer_code>/' docker-compose.yml && sed -i 's/KRISENVORRAT_APP_VERSION_NAME=.*/KRISENVORRAT_APP_VERSION_NAME=<neuer_name>/' docker-compose.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
Falls die Env-Vars noch nicht in der docker-compose.yml stehen, müssen sie einmalig hinzugefügt werden:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh root@195.246.231.210 "cd /opt/krisenvorrat && sed -i '/KRISENVORRAT_JWT_SECRET/a\ - KRISENVORRAT_APP_VERSION_CODE=<code>\n - KRISENVORRAT_APP_VERSION_NAME=<name>' docker-compose.yml"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 5 – Server neustarten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
ssh root@195.246.231.210 "cd /opt/krisenvorrat && docker compose up -d"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 6 – Verifizieren
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Version-API prüfen
|
||||||
|
Invoke-WebRequest -Uri "http://195.246.231.210:8080/api/version" -UseBasicParsing | Select-Object -ExpandProperty Content
|
||||||
|
|
||||||
|
# Homepage prüfen (QR-Code-Seite)
|
||||||
|
Invoke-WebRequest -Uri "http://195.246.231.210:8080/" -UseBasicParsing | Select-Object StatusCode, StatusDescription
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartete Ausgabe von `/api/version`:
|
||||||
|
```json
|
||||||
|
{"versionCode":<neuer_code>,"versionName":"<neuer_name>","apkUrl":"http://195.246.231.210:8080/static/app-latest.apk"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automatisiertes Skript
|
||||||
|
|
||||||
|
Das Skript `publish-apk.ps1` in diesem Skill-Ordner automatisiert Schritte 3–6:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& ".github/skills/publish/publish-apk.ps1" -VersionCode 4 -VersionName "1.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Pflicht | Beschreibung |
|
||||||
|
| -------------- | ------- | --------------------------------------------- |
|
||||||
|
| `-VersionCode` | ja | Neuer versionCode (Integer) |
|
||||||
|
| `-VersionName` | ja | Neuer versionName (String, z.B. "1.3") |
|
||||||
|
| `-ApkPath` | nein | Pfad zur APK (Default: debug-APK) |
|
||||||
|
| `-SkipVerify` | nein | Verifizierung überspringen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wichtige Dateien
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|-------------|
|
||||||
|
| `app/build.gradle.kts` (L18-19) | `versionCode` / `versionName` |
|
||||||
|
| `server/src/main/resources/application.conf` (L18-21) | Server-Version-Defaults + Env-Var-Override |
|
||||||
|
| `server/src/main/kotlin/.../routes/VersionRoutes.kt` | Homepage + `/api/version` Endpoint |
|
||||||
|
| `server/src/main/kotlin/.../plugins/Routing.kt` | `staticFiles("/static", File("data"))` |
|
||||||
|
| `app/src/main/java/.../usecase/CheckForUpdateUseCase.kt` | Update-Prüfung in der App |
|
||||||
|
| `app/src/main/java/.../ui/update/UpdateViewModel.kt` | Update-UI + Download-Logik |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bekannte Lücken / TODOs
|
||||||
|
|
||||||
|
- **Kein Release-Build:** Aktuell wird `assembleDebug` verwendet. Für Produktion → Signing-Config + `assembleRelease` einrichten.
|
||||||
|
- **Kein HTTPS:** Server läuft auf HTTP. QR-Code zeigt `http://`-URL. Für Play Store / Sicherheit → Caddy als Reverse Proxy.
|
||||||
|
- **Manuelle Version-Env-Vars:** Beim ersten Publish müssen die Env-Vars in die VPS docker-compose eingefügt werden. Das Skript erledigt das automatisch.
|
||||||
98
.github/skills/publish/publish-apk.ps1
vendored
Normal file
98
.github/skills/publish/publish-apk.ps1
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Publiziert eine APK auf den Krisenvorrat VPS.
|
||||||
|
.DESCRIPTION
|
||||||
|
Lädt die APK auf den VPS hoch, aktualisiert die Version in der
|
||||||
|
docker-compose.yml und startet den Server-Container neu.
|
||||||
|
.PARAMETER VersionCode
|
||||||
|
Neuer versionCode (Integer, muss > aktueller sein).
|
||||||
|
.PARAMETER VersionName
|
||||||
|
Neuer versionName (z.B. "1.3").
|
||||||
|
.PARAMETER ApkPath
|
||||||
|
Pfad zur APK-Datei. Default: app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
.PARAMETER SkipVerify
|
||||||
|
Verifizierung nach dem Deploy überspringen.
|
||||||
|
#>
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory)] [int] $VersionCode,
|
||||||
|
[Parameter(Mandatory)] [string] $VersionName,
|
||||||
|
[string] $ApkPath = "app/build/outputs/apk/debug/app-debug.apk",
|
||||||
|
[switch] $SkipVerify
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$VPS = "root@195.246.231.210"
|
||||||
|
$RemoteDir = "/opt/krisenvorrat"
|
||||||
|
|
||||||
|
# --- Preflight ---
|
||||||
|
Write-Host "=== Publish APK v$VersionName (build $VersionCode) ===" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
if (-not (Test-Path $ApkPath)) {
|
||||||
|
Write-Error "APK nicht gefunden: $ApkPath – bitte zuerst './gradlew assembleDebug' ausfuehren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# SSH-Agent prüfen
|
||||||
|
$sshKeys = ssh-add -l 2>&1
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Error "SSH-Agent hat keinen Key geladen. Bitte 'ssh-add' ausfuehren."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Host "[OK] SSH-Agent aktiv" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- Schritt 1: APK hochladen ---
|
||||||
|
Write-Host "`n[1/4] APK hochladen..." -ForegroundColor Yellow
|
||||||
|
ssh $VPS "mkdir -p $RemoteDir/data"
|
||||||
|
scp $ApkPath "${VPS}:${RemoteDir}/data/app-latest.apk"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "APK-Upload fehlgeschlagen."; exit 1 }
|
||||||
|
Write-Host "[OK] APK hochgeladen" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- Schritt 2: Version in docker-compose.yml aktualisieren ---
|
||||||
|
Write-Host "`n[2/4] Version in docker-compose.yml aktualisieren..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Prüfen ob Env-Vars bereits vorhanden sind
|
||||||
|
$hasVersionCode = ssh $VPS "grep -c 'KRISENVORRAT_APP_VERSION_CODE' $RemoteDir/docker-compose.yml 2>/dev/null || echo 0"
|
||||||
|
$hasVersionCode = [int]$hasVersionCode.Trim()
|
||||||
|
|
||||||
|
if ($hasVersionCode -gt 0) {
|
||||||
|
# Update bestehende Einträge
|
||||||
|
ssh $VPS "cd $RemoteDir && sed -i 's/KRISENVORRAT_APP_VERSION_CODE=.*/KRISENVORRAT_APP_VERSION_CODE=$VersionCode/' docker-compose.yml && sed -i 's/KRISENVORRAT_APP_VERSION_NAME=.*/KRISENVORRAT_APP_VERSION_NAME=$VersionName/' docker-compose.yml"
|
||||||
|
} else {
|
||||||
|
# Erstmalig hinzufügen (nach JWT_SECRET-Zeile)
|
||||||
|
ssh $VPS "cd $RemoteDir && sed -i '/KRISENVORRAT_JWT_SECRET/a\ - KRISENVORRAT_APP_VERSION_CODE=$VersionCode\n - KRISENVORRAT_APP_VERSION_NAME=$VersionName' docker-compose.yml"
|
||||||
|
}
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "Version-Update fehlgeschlagen."; exit 1 }
|
||||||
|
Write-Host "[OK] Version gesetzt: $VersionName (build $VersionCode)" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- Schritt 3: Server neustarten ---
|
||||||
|
Write-Host "`n[3/4] Server-Container neustarten..." -ForegroundColor Yellow
|
||||||
|
ssh $VPS "cd $RemoteDir && docker compose up -d"
|
||||||
|
if ($LASTEXITCODE -ne 0) { Write-Error "Container-Neustart fehlgeschlagen."; exit 1 }
|
||||||
|
Write-Host "[OK] Container neu gestartet" -ForegroundColor Green
|
||||||
|
|
||||||
|
# --- Schritt 4: Verifizieren ---
|
||||||
|
if ($SkipVerify) {
|
||||||
|
Write-Host "`n[4/4] Verifizierung uebersprungen." -ForegroundColor DarkGray
|
||||||
|
} else {
|
||||||
|
Write-Host "`n[4/4] Verifizieren (warte 5s auf Server-Start)..." -ForegroundColor Yellow
|
||||||
|
Start-Sleep -Seconds 5
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = Invoke-WebRequest -Uri "http://195.246.231.210:8080/api/version" -UseBasicParsing -TimeoutSec 10
|
||||||
|
$versionJson = $response.Content | ConvertFrom-Json
|
||||||
|
|
||||||
|
if ($versionJson.versionCode -eq $VersionCode -and $versionJson.versionName -eq $VersionName) {
|
||||||
|
Write-Host "[OK] Server meldet Version $($versionJson.versionName) (build $($versionJson.versionCode))" -ForegroundColor Green
|
||||||
|
Write-Host " APK-URL: $($versionJson.apkUrl)" -ForegroundColor DarkGray
|
||||||
|
} else {
|
||||||
|
Write-Warning "Server meldet unerwartete Version: $($versionJson | ConvertTo-Json -Compress)"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warning "Verifizierung fehlgeschlagen: $_"
|
||||||
|
Write-Host "Manuell pruefen: http://195.246.231.210:8080/api/version" -ForegroundColor DarkGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n=== Publish abgeschlossen ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "Homepage: http://195.246.231.210:8080/" -ForegroundColor DarkGray
|
||||||
|
Write-Host "API: http://195.246.231.210:8080/api/version" -ForegroundColor DarkGray
|
||||||
|
|
@ -11,7 +11,7 @@ internal sealed class SettingsKey<T>(val key: String, val defaultValue: T) {
|
||||||
data object HouseholdSize : StringKey("household_size")
|
data object HouseholdSize : StringKey("household_size")
|
||||||
data object DailyKcalPerPerson : StringKey("daily_kcal_per_person")
|
data object DailyKcalPerPerson : StringKey("daily_kcal_per_person")
|
||||||
data object AgeGroups : StringKey("age_groups")
|
data object AgeGroups : StringKey("age_groups")
|
||||||
data object ServerUrl : StringKey("server_url")
|
data object ServerUrl : StringKey("server_url", DEFAULT_SERVER_URL)
|
||||||
data object AuthAccessToken : StringKey("auth_access_token")
|
data object AuthAccessToken : StringKey("auth_access_token")
|
||||||
data object AuthRefreshToken : StringKey("auth_refresh_token")
|
data object AuthRefreshToken : StringKey("auth_refresh_token")
|
||||||
data object AuthUsername : StringKey("auth_username")
|
data object AuthUsername : StringKey("auth_username")
|
||||||
|
|
@ -23,6 +23,8 @@ internal sealed class SettingsKey<T>(val key: String, val defaultValue: T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val DEFAULT_SERVER_URL = "http://195.246.231.210:8080"
|
||||||
|
|
||||||
val SENSITIVE_KEYS: Set<StringKey> = setOf(
|
val SENSITIVE_KEYS: Set<StringKey> = setOf(
|
||||||
StringKey.AuthAccessToken,
|
StringKey.AuthAccessToken,
|
||||||
StringKey.AuthRefreshToken,
|
StringKey.AuthRefreshToken,
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,15 @@ import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
|
@ -260,15 +264,27 @@ internal fun SettingsScreen(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
Row(
|
||||||
value = uiState.serverUrl,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
onValueChange = viewModel::onServerUrlChanged,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
label = { Text("Server-URL") },
|
) {
|
||||||
placeholder = { Text("https://example.com") },
|
OutlinedTextField(
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
value = uiState.serverUrl,
|
||||||
singleLine = true,
|
onValueChange = viewModel::onServerUrlChanged,
|
||||||
modifier = Modifier.fillMaxWidth()
|
label = { Text("Server-URL") },
|
||||||
)
|
placeholder = { Text("https://example.com") },
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
IconButton(onClick = viewModel::resetServerUrl) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Refresh,
|
||||||
|
contentDescription = "Standard-Server wiederherstellen"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
|
@ -342,25 +358,19 @@ internal fun SettingsScreen(
|
||||||
uiState.serverUrl.isNotBlank() &&
|
uiState.serverUrl.isNotBlank() &&
|
||||||
uiState.syncStatus !is SyncStatus.Running
|
uiState.syncStatus !is SyncStatus.Running
|
||||||
|
|
||||||
Row(
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
text = "Synchronisierung erfolgt automatisch.",
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
style = MaterialTheme.typography.bodySmall,
|
||||||
) {
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
OutlinedButton(
|
)
|
||||||
onClick = viewModel::pushSync,
|
|
||||||
enabled = isSyncEnabled,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
) {
|
|
||||||
Text("Push (Hochladen)")
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedButton(
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
onClick = viewModel::pullSync,
|
|
||||||
enabled = isSyncEnabled,
|
OutlinedButton(
|
||||||
modifier = Modifier.weight(1f)
|
onClick = viewModel::pullSync,
|
||||||
) {
|
enabled = isSyncEnabled,
|
||||||
Text("Pull (Herunterladen)")
|
) {
|
||||||
}
|
Text("Jetzt synchronisieren")
|
||||||
}
|
}
|
||||||
|
|
||||||
when (val syncStatus = uiState.syncStatus) {
|
when (val syncStatus = uiState.syncStatus) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ internal data class SettingsUiState(
|
||||||
val isImporting: Boolean = false,
|
val isImporting: Boolean = false,
|
||||||
val importResult: ImportResult? = null,
|
val importResult: ImportResult? = null,
|
||||||
val pendingImportUri: Uri? = null,
|
val pendingImportUri: Uri? = null,
|
||||||
val serverUrl: String = "",
|
val serverUrl: String = de.krisenvorrat.app.domain.model.SettingsKey.DEFAULT_SERVER_URL,
|
||||||
val isLoggedIn: Boolean = false,
|
val isLoggedIn: Boolean = false,
|
||||||
val loggedInUsername: String = "",
|
val loggedInUsername: String = "",
|
||||||
val loginUsername: String = "",
|
val loginUsername: String = "",
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import de.krisenvorrat.app.data.sync.WebSocketClient
|
||||||
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
import de.krisenvorrat.app.data.sync.WebSocketEvent
|
||||||
import de.krisenvorrat.app.domain.model.AgeGroup
|
import de.krisenvorrat.app.domain.model.AgeGroup
|
||||||
import de.krisenvorrat.app.domain.model.AgeGroupEntry
|
import de.krisenvorrat.app.domain.model.AgeGroupEntry
|
||||||
|
import de.krisenvorrat.app.domain.model.SettingsKey
|
||||||
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
import de.krisenvorrat.app.domain.model.SettingsKey.StringKey
|
||||||
import de.krisenvorrat.app.domain.model.defaultAgeGroups
|
import de.krisenvorrat.app.domain.model.defaultAgeGroups
|
||||||
import de.krisenvorrat.app.domain.model.parseAgeGroupsFromJson
|
import de.krisenvorrat.app.domain.model.parseAgeGroupsFromJson
|
||||||
|
|
@ -58,6 +59,7 @@ internal class SettingsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
is WebSocketEvent.Connected -> {
|
is WebSocketEvent.Connected -> {
|
||||||
_uiState.update { it.copy(syncStatus = SyncStatus.Idle) }
|
_uiState.update { it.copy(syncStatus = SyncStatus.Idle) }
|
||||||
|
pullSync(fullSync = false)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +72,7 @@ internal class SettingsViewModel @Inject constructor(
|
||||||
try {
|
try {
|
||||||
val ageGroups = loadAgeGroupsWithMigration()
|
val ageGroups = loadAgeGroupsWithMigration()
|
||||||
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
val serverUrl = settingsRepository.getString(StringKey.ServerUrl)
|
||||||
|
.ifBlank { SettingsKey.DEFAULT_SERVER_URL }
|
||||||
val authUsername = settingsRepository.getString(StringKey.AuthUsername)
|
val authUsername = settingsRepository.getString(StringKey.AuthUsername)
|
||||||
val accessToken = settingsRepository.getString(StringKey.AuthAccessToken)
|
val accessToken = settingsRepository.getString(StringKey.AuthAccessToken)
|
||||||
val isLoggedIn = authUsername.isNotBlank() && accessToken.isNotBlank()
|
val isLoggedIn = authUsername.isNotBlank() && accessToken.isNotBlank()
|
||||||
|
|
@ -150,6 +153,10 @@ internal class SettingsViewModel @Inject constructor(
|
||||||
_uiState.update { it.copy(serverUrl = value, isSaved = false) }
|
_uiState.update { it.copy(serverUrl = value, isSaved = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetServerUrl() {
|
||||||
|
_uiState.update { it.copy(serverUrl = SettingsKey.DEFAULT_SERVER_URL, isSaved = false) }
|
||||||
|
}
|
||||||
|
|
||||||
fun onLoginUsernameChanged(value: String) {
|
fun onLoginUsernameChanged(value: String) {
|
||||||
_uiState.update { it.copy(loginUsername = value, loginError = null) }
|
_uiState.update { it.copy(loginUsername = value, loginError = null) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class SettingsRepositoryImplTest {
|
||||||
val result = repository.getString(SettingsKey.StringKey.ServerUrl)
|
val result = repository.getString(SettingsKey.StringKey.ServerUrl)
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals("", result)
|
assertEquals(SettingsKey.DEFAULT_SERVER_URL, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -215,6 +215,6 @@ class SettingsRepositoryImplTest {
|
||||||
val result = repository.observeString(SettingsKey.StringKey.ServerUrl).first()
|
val result = repository.observeString(SettingsKey.StringKey.ServerUrl).first()
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
assertEquals("", result)
|
assertEquals(SettingsKey.DEFAULT_SERVER_URL, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue