feat(CalendarView): enhance training slot merging and event normalization
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
- Added a new method to normalize training time keys, improving consistency in event handling. - Updated the logic for merging recurring training slots to include only relevant events, enhancing calendar management. - Improved subtitle handling for merged training slots to reflect recurring status and additional details, providing clearer event information.
This commit is contained in:
@@ -284,21 +284,33 @@ export default {
|
||||
|
||||
this.loading = false;
|
||||
},
|
||||
normalizeTrainingTimeKey(time) {
|
||||
return String(time || '')
|
||||
.trim()
|
||||
.replace(/[\u2013\u2012\u2212]/g, '-')
|
||||
.replace(/\s*-\s*/g, '-')
|
||||
.replace(/\s+/g, '');
|
||||
},
|
||||
/**
|
||||
* Mehrere regelmäßige Trainingszeiten mit identischem Wochentag und gleichem Uhrzeit-Fenster
|
||||
* (z. B. parallel genutzte Gruppen / Dubletten) zu einem Kalendereintrag zusammenführen.
|
||||
* Alle Trainings-Einträge (Tagebuch + regelmäßige Zeiten) mit gleichem Kalendertag und gleichem
|
||||
* Zeitfenster-Text zu einem Eintrag zusammenführen (z. B. Dublette „Training“ + Gruppen).
|
||||
*/
|
||||
mergeRecurringTrainingSlots(events) {
|
||||
const genericTitle = (t) => !t || /^training$/i.test(String(t).trim());
|
||||
const slotMap = new Map();
|
||||
const passthrough = [];
|
||||
for (const e of events) {
|
||||
if (e.type !== 'training' || !e.isRecurringTraining || !e.time || !e.date) {
|
||||
if (e.type !== 'training' || !e.date) {
|
||||
passthrough.push(e);
|
||||
continue;
|
||||
}
|
||||
const timeKey = this.normalizeTrainingTimeKey(e.time);
|
||||
if (!timeKey) {
|
||||
passthrough.push(e);
|
||||
continue;
|
||||
}
|
||||
const dk = this.toDateKey(e.date);
|
||||
const slotKey = `${dk}|${e.time}`;
|
||||
const slotKey = `${dk}|${timeKey}`;
|
||||
if (!slotMap.has(slotKey)) {
|
||||
slotMap.set(slotKey, []);
|
||||
}
|
||||
@@ -316,12 +328,25 @@ export default {
|
||||
const specific = rawTitles.filter((t) => !genericTitle(t));
|
||||
const titleJoined = specific.length ? specific.join(' · ') : (rawTitles.join(' · ') || base.title);
|
||||
const safeIdKey = slotKey.replace(/\|/g, '-');
|
||||
const hasRecurring = sorted.some((x) => x.isRecurringTraining);
|
||||
const otherSubtitles = [...new Set(
|
||||
sorted.map((x) => String(x.subtitle || '').trim()).filter((s) => s && s !== 'Regelmäßige Trainingszeit')
|
||||
)];
|
||||
let subtitleJoined = '';
|
||||
if (hasRecurring) {
|
||||
subtitleJoined = otherSubtitles.length
|
||||
? `Regelmäßige Trainingszeit · ${otherSubtitles.join(' · ')}`
|
||||
: 'Regelmäßige Trainingszeit';
|
||||
} else {
|
||||
subtitleJoined = otherSubtitles.join(' · ') || String(base.subtitle || '').trim() || '';
|
||||
}
|
||||
mergedSlots.push({
|
||||
...base,
|
||||
id: `training-merged-${safeIdKey}`,
|
||||
title: titleJoined,
|
||||
subtitle: 'Regelmäßige Trainingszeit',
|
||||
startsAt: Math.min(...sorted.map((x) => x.startsAt))
|
||||
subtitle: subtitleJoined,
|
||||
startsAt: Math.min(...sorted.map((x) => x.startsAt)),
|
||||
isRecurringTraining: hasRecurring
|
||||
});
|
||||
}
|
||||
return [...passthrough, ...mergedSlots];
|
||||
|
||||
@@ -180,10 +180,10 @@ Web: `DiaryView.vue` (sehr groß). API-Cluster (Auszug – in Web nach `apiClien
|
||||
|
||||
## Phase 10 – Persönliche Konten & Integrationen
|
||||
|
||||
- [ ] **Persönliche Einstellungen** vollständig (`PersonalSettings.vue` vs. aktuelles „Mehr“)
|
||||
- [ ] **MyTischtennis-Konto** (`MyTischtennisAccount.vue`)
|
||||
- [ ] **ClickTT-Konto** (`ClickTtAccount.vue`)
|
||||
- [ ] **ClickTT-Ansicht** (`ClickTtView.vue`)
|
||||
- [x] **Persönliche Einstellungen** (`PersonalSettings.vue`) – Mehr → Hub: Sprache, MyTT, ClickTT; `PersonalHubScreens.kt` + `AppRoot.kt` `SettingsScreen`
|
||||
- [x] **MyTischtennis-Konto** (`MyTischtennisAccount.vue`) – `MyTischtennisApi`, Dialog/Verify/Unlink in `PersonalHubScreens.kt`
|
||||
- [x] **ClickTT-Konto** (`ClickTtAccount.vue`) – `ClickTtAccountApi`, analog in `PersonalHubScreens.kt`
|
||||
- [x] **ClickTT-Ansicht** (`ClickTtView.vue`) – Proxy-URL + `WebView` in `ClickTtBrowserScreen`
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3972,6 +3972,15 @@ private fun RowSwitch(label: String, checked: Boolean, onChecked: (Boolean) -> U
|
||||
private fun SettingsScreen(dependencies: AppDependencies) {
|
||||
var clubAdminSection by remember { mutableStateOf<ClubAdminSettingsSection?>(null) }
|
||||
var stammdatenSection by remember { mutableStateOf<ClubStammdatenSection?>(null) }
|
||||
var personalHub by remember { mutableStateOf<PersonalHubDestination?>(null) }
|
||||
if (personalHub != null) {
|
||||
PersonalHubFlowScreen(
|
||||
destination = personalHub!!,
|
||||
dependencies = dependencies,
|
||||
onBack = { personalHub = null },
|
||||
)
|
||||
return
|
||||
}
|
||||
if (clubAdminSection != null) {
|
||||
ClubAdminFlowScreen(
|
||||
destination = clubAdminSection!!,
|
||||
@@ -4059,15 +4068,23 @@ private fun SettingsScreen(dependencies: AppDependencies) {
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTitle(tr("mobile.language", "Sprache"))
|
||||
MobileStrings.supportedLanguages.forEach { language ->
|
||||
TextButton(
|
||||
onClick = { dependencies.applicationScope.launch { dependencies.languageManager.selectLanguage(language.code) } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(if (language.code == LocalLanguageCode.current) "✓ ${language.label}" else language.label)
|
||||
}
|
||||
}
|
||||
SectionTitle(tr("settings.personalSettings", "Persönliche Einstellungen"))
|
||||
TextButton(
|
||||
onClick = { personalHub = PersonalHubDestination.PersonalLanguage },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text(tr("mobile.language", "Sprache")) }
|
||||
TextButton(
|
||||
onClick = { personalHub = PersonalHubDestination.MyTischtennisAccount },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text(tr("myTischtennisAccount.title", "myTischtennis-Account")) }
|
||||
TextButton(
|
||||
onClick = { personalHub = PersonalHubDestination.ClickTtAccount },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text(tr("navigation.clickTtAccount", "HTTV / click-TT-Account")) }
|
||||
TextButton(
|
||||
onClick = { personalHub = PersonalHubDestination.ClickTtBrowser },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) { Text(tr("navigation.clickTtBrowser", "HTTV / click-TT")) }
|
||||
SectionTitle(tr("mobile.legal", "Rechtliches"))
|
||||
Text(
|
||||
tr("mobile.legalBrowserHint", "Öffnet die Web-Oberfläche des konfigurierten Backends im Browser."),
|
||||
|
||||
@@ -0,0 +1,915 @@
|
||||
package de.tt_tagebuch.app.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import de.tt_tagebuch.app.AppDependencies
|
||||
import de.tt_tagebuch.shared.api.http.ApiException
|
||||
import de.tt_tagebuch.shared.api.models.ClickTtAccountDto
|
||||
import de.tt_tagebuch.shared.api.models.ClickTtAccountStatusDto
|
||||
import de.tt_tagebuch.shared.api.models.ClickTtAccountUpsertBody
|
||||
import de.tt_tagebuch.shared.api.models.MyTischtennisAccountDto
|
||||
import de.tt_tagebuch.shared.api.models.MyTischtennisAccountUpsertBody
|
||||
import de.tt_tagebuch.shared.api.models.MyTischtennisStatusDto
|
||||
import de.tt_tagebuch.shared.i18n.MobileStrings
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.OffsetDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.UUID
|
||||
|
||||
private val PersonalHubPad = 20.dp
|
||||
private val PersonalHubTouchMin = 48.dp
|
||||
|
||||
internal enum class PersonalHubDestination {
|
||||
PersonalLanguage,
|
||||
MyTischtennisAccount,
|
||||
ClickTtAccount,
|
||||
ClickTtBrowser,
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PersonalHubFlowScreen(
|
||||
destination: PersonalHubDestination,
|
||||
dependencies: AppDependencies,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
BackHandler(onBack = onBack)
|
||||
when (destination) {
|
||||
PersonalHubDestination.PersonalLanguage -> PersonalLanguageScreen(dependencies, onBack)
|
||||
PersonalHubDestination.MyTischtennisAccount -> MyTischtennisAccountScreen(dependencies, onBack)
|
||||
PersonalHubDestination.ClickTtAccount -> ClickTtAccountScreen(dependencies, onBack)
|
||||
PersonalHubDestination.ClickTtBrowser -> ClickTtBrowserScreen(dependencies, onBack)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PersonalHubTopBar(title: String, onBack: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
Text(title, style = MaterialTheme.typography.h6, fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
private fun formatDateTimeUi(iso: String?): String {
|
||||
if (iso.isNullOrBlank()) return "–"
|
||||
return try {
|
||||
val odt = OffsetDateTime.parse(iso)
|
||||
odt.format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
|
||||
} catch (_: Exception) {
|
||||
iso.take(19).replace('T', ' ')
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PersonalLanguageScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(PersonalHubPad),
|
||||
) {
|
||||
PersonalHubTopBar(tr("settings.personalSettings", "Persönliche Einstellungen"), onBack)
|
||||
Card(modifier = Modifier.fillMaxWidth(), elevation = 1.dp) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(tr("settings.language", "Sprache"), fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.subtitle1)
|
||||
Text(
|
||||
tr("settings.languageDescription", "Wähle deine bevorzugte Sprache für die Anwendung"),
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Text(
|
||||
tr("settings.selectLanguage", "Sprache auswählen"),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
)
|
||||
MobileStrings.supportedLanguages.forEach { language ->
|
||||
TextButton(
|
||||
onClick = { dependencies.applicationScope.launch { dependencies.languageManager.selectLanguage(language.code) } },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin),
|
||||
) {
|
||||
Text(if (language.code == LocalLanguageCode.current) "✓ ${language.label}" else language.label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MyTischtennisAccountScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
val scope = rememberCoroutineScope()
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var account by remember { mutableStateOf<MyTischtennisAccountDto?>(null) }
|
||||
var status by remember { mutableStateOf<MyTischtennisStatusDto?>(null) }
|
||||
var err by remember { mutableStateOf<String?>(null) }
|
||||
var refresh by remember { mutableIntStateOf(0) }
|
||||
var showEditor by remember { mutableStateOf(false) }
|
||||
var loginMode by remember { mutableStateOf(false) }
|
||||
var verifyBusy by remember { mutableStateOf(false) }
|
||||
var confirmUnlink by remember { mutableStateOf(false) }
|
||||
var infoMsg by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(refresh) {
|
||||
loading = true
|
||||
err = null
|
||||
runCatching {
|
||||
val a = dependencies.myTischtennisApi.getAccount()
|
||||
val s = dependencies.myTischtennisApi.getStatus()
|
||||
account = a.account
|
||||
status = s
|
||||
}.onFailure { err = it.message }
|
||||
loading = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(PersonalHubPad),
|
||||
) {
|
||||
PersonalHubTopBar(tr("myTischtennisAccount.title", "myTischtennis-Account"), onBack)
|
||||
infoMsg?.let {
|
||||
Text(it, color = MaterialTheme.colors.primary, modifier = Modifier.padding(bottom = 8.dp))
|
||||
}
|
||||
err?.let { Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(bottom = 8.dp)) }
|
||||
when {
|
||||
loading -> CircularProgressIndicator(modifier = Modifier.padding(top = 24.dp))
|
||||
account != null -> {
|
||||
val a = account!!
|
||||
Card(modifier = Modifier.fillMaxWidth(), elevation = 1.dp) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(tr("myTischtennisAccount.linkedAccount", "Verknüpfter Account"), fontWeight = FontWeight.SemiBold)
|
||||
detailRow(tr("myTischtennisAccount.email", "E-Mail"), a.email ?: "–")
|
||||
detailRow(
|
||||
tr("myTischtennisAccount.passwordSaved", "Passwort gespeichert"),
|
||||
if (status?.hasPassword == true) tr("myTischtennisAccount.yes", "Ja") else tr("myTischtennisAccount.no", "Nein"),
|
||||
)
|
||||
if (!a.clubId.isNullOrBlank()) {
|
||||
val clubLine = buildString {
|
||||
append(a.clubName ?: "")
|
||||
append(" (")
|
||||
append(a.clubId)
|
||||
if (!a.fedNickname.isNullOrBlank()) append(" - ").append(a.fedNickname)
|
||||
append(")")
|
||||
}
|
||||
detailRow(tr("myTischtennisAccount.club", "Verein"), clubLine)
|
||||
}
|
||||
a.lastLoginSuccess?.let { detailRow(tr("myTischtennisAccount.lastSuccessfulLogin", "Letzter erfolgreicher Login"), formatDateTimeUi(it)) }
|
||||
a.lastLoginAttempt?.let { detailRow(tr("myTischtennisAccount.lastLoginAttempt", "Letzter Login-Versuch"), formatDateTimeUi(it)) }
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = { loginMode = false; showEditor = true },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(tr("myTischtennisAccount.editAccount", "Account bearbeiten")) }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
verifyBusy = true
|
||||
err = null
|
||||
runCatching {
|
||||
dependencies.myTischtennisApi.verifyLogin(null)
|
||||
refresh++
|
||||
infoMsg = tr("myTischtennisAccount.loginSuccessful", "Login erfolgreich")
|
||||
}.onFailure { e ->
|
||||
if (e is ApiException && e.statusCode == 400) {
|
||||
loginMode = true
|
||||
showEditor = true
|
||||
} else {
|
||||
err = e.message
|
||||
}
|
||||
}
|
||||
verifyBusy = false
|
||||
}
|
||||
},
|
||||
enabled = !verifyBusy,
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin).padding(top = 8.dp),
|
||||
) {
|
||||
Text(if (verifyBusy) "…" else tr("myTischtennisAccount.loginAgain", "Erneut einloggen"))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { confirmUnlink = true },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin).padding(top = 8.dp),
|
||||
) { Text(tr("myTischtennisAccount.unlinkAccount", "Account trennen")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Text(tr("myTischtennisAccount.noAccountLinked", "Kein myTischtennis-Account verknüpft."))
|
||||
Button(
|
||||
onClick = { loginMode = false; showEditor = true },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 12.dp).heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(tr("myTischtennisAccount.linkAccount", "Account verknüpfen")) }
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Card(modifier = Modifier.fillMaxWidth(), elevation = 1.dp) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(tr("myTischtennisAccount.aboutMyTischtennis", "Über myTischtennis"), fontWeight = FontWeight.SemiBold)
|
||||
Text(tr("myTischtennisAccount.aboutDescription", ""), modifier = Modifier.padding(top = 6.dp))
|
||||
Text("• " + tr("myTischtennisAccount.aboutFeature1", ""), modifier = Modifier.padding(top = 4.dp))
|
||||
Text("• " + tr("myTischtennisAccount.aboutFeature2", ""), modifier = Modifier.padding(top = 2.dp))
|
||||
Text("• " + tr("myTischtennisAccount.aboutFeature3", ""), modifier = Modifier.padding(top = 2.dp))
|
||||
Text(
|
||||
tr("messages.info", "Hinweis") + ": " + tr("myTischtennisAccount.aboutHint", ""),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
style = MaterialTheme.typography.caption,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showEditor) {
|
||||
MyTischtennisEditorDialog(
|
||||
account = account,
|
||||
loginMode = loginMode,
|
||||
tr = ::tr,
|
||||
onDismiss = { showEditor = false; loginMode = false },
|
||||
onSaved = {
|
||||
showEditor = false
|
||||
loginMode = false
|
||||
refresh++
|
||||
infoMsg = tr("myTischtennisAccount.accountSaved", "Gespeichert")
|
||||
},
|
||||
onLoggedIn = {
|
||||
showEditor = false
|
||||
loginMode = false
|
||||
refresh++
|
||||
infoMsg = tr("myTischtennisAccount.loginSuccessful", "Login erfolgreich")
|
||||
},
|
||||
scope = scope,
|
||||
api = dependencies.myTischtennisApi,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmUnlink) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmUnlink = false },
|
||||
title = { Text(tr("myTischtennisAccount.unlinkAccountTitle", "Account trennen")) },
|
||||
text = { Text(tr("myTischtennisAccount.unlinkAccountConfirm", "Wirklich trennen?")) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
dependencies.myTischtennisApi.deleteAccount()
|
||||
account = null
|
||||
status = null
|
||||
confirmUnlink = false
|
||||
refresh++
|
||||
infoMsg = tr("myTischtennisAccount.accountUnlinked", "Getrennt")
|
||||
}.onFailure { err = it.message; confirmUnlink = false }
|
||||
}
|
||||
},
|
||||
) { Text(tr("myTischtennisAccount.unlinkAccount", "Trennen")) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmUnlink = false }) { Text(tr("myTischtennisDialog.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun detailRow(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(label, fontWeight = FontWeight.Medium, modifier = Modifier.weight(0.45f))
|
||||
Text(value, modifier = Modifier.weight(0.55f))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MyTischtennisEditorDialog(
|
||||
account: MyTischtennisAccountDto?,
|
||||
loginMode: Boolean,
|
||||
tr: (String, String) -> String,
|
||||
onDismiss: () -> Unit,
|
||||
onSaved: () -> Unit,
|
||||
onLoggedIn: () -> Unit,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
api: de.tt_tagebuch.shared.api.MyTischtennisApi,
|
||||
) {
|
||||
var email by remember(account?.id, loginMode) { mutableStateOf(account?.email.orEmpty()) }
|
||||
var password by remember(account?.id, loginMode) { mutableStateOf("") }
|
||||
var savePw by remember(account?.id, loginMode) { mutableStateOf(account?.savePassword == true) }
|
||||
var autoRat by remember(account?.id, loginMode) { mutableStateOf(account?.autoUpdateRatings == true) }
|
||||
var userPw by remember(account?.id, loginMode) { mutableStateOf("") }
|
||||
var err by remember { mutableStateOf<String?>(null) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
if (loginMode) tr("myTischtennisDialog.login", "Einloggen")
|
||||
else if (account != null) tr("myTischtennisDialog.editAccount", "Bearbeiten")
|
||||
else tr("myTischtennisDialog.linkAccount", "Verknüpfen"),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
err?.let { Text(it, color = MaterialTheme.colors.error) }
|
||||
if (loginMode) {
|
||||
OutlinedTextField(value = email, onValueChange = {}, readOnly = true, label = { Text(tr("myTischtennisDialog.email", "E-Mail")) }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(tr("myTischtennisDialog.password", "Passwort")) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
OutlinedTextField(value = email, onValueChange = { email = it }, label = { Text(tr("myTischtennisDialog.email", "E-Mail")) }, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(tr("myTischtennisDialog.password", "Passwort")) },
|
||||
placeholder = { Text(tr("myTischtennisDialog.passwordPlaceholderKeep", "")) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Switch(checked = savePw, onCheckedChange = { savePw = it })
|
||||
Text(tr("myTischtennisDialog.savePassword", "Passwort speichern"), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Switch(checked = autoRat, onCheckedChange = { autoRat = it }, enabled = savePw)
|
||||
Text(tr("myTischtennisDialog.autoUpdateRatings", "Auto-Ratings"), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
if (password.isNotBlank()) {
|
||||
OutlinedTextField(
|
||||
value = userPw,
|
||||
onValueChange = { userPw = it },
|
||||
label = { Text(tr("myTischtennisDialog.appPassword", "App-Passwort")) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !busy && if (loginMode) {
|
||||
password.isNotBlank()
|
||||
} else {
|
||||
email.isNotBlank() && (password.isBlank() || userPw.isNotBlank())
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
err = null
|
||||
runCatching {
|
||||
if (loginMode) {
|
||||
api.verifyLogin(password)
|
||||
onLoggedIn()
|
||||
} else {
|
||||
if (password.isNotBlank() && userPw.isBlank()) return@launch
|
||||
api.upsertAccount(
|
||||
MyTischtennisAccountUpsertBody(
|
||||
email = email.trim(),
|
||||
password = password.ifBlank { null },
|
||||
savePassword = savePw,
|
||||
autoUpdateRatings = if (savePw) autoRat else false,
|
||||
userPassword = userPw.ifBlank { null },
|
||||
),
|
||||
)
|
||||
onSaved()
|
||||
}
|
||||
}.onFailure { e -> err = e.message }
|
||||
busy = false
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
if (loginMode) tr("myTischtennisDialog.login", "Login")
|
||||
else tr("myTischtennisDialog.save", "Speichern"),
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss, enabled = !busy) { Text(tr("myTischtennisDialog.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickTtAccountScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
fun ct(key: String, fb: String) = MobileStrings.get(languageCode, key, fb) // same; use navigation keys
|
||||
val scope = rememberCoroutineScope()
|
||||
var loading by remember { mutableStateOf(true) }
|
||||
var account by remember { mutableStateOf<ClickTtAccountDto?>(null) }
|
||||
var status by remember { mutableStateOf<ClickTtAccountStatusDto?>(null) }
|
||||
var err by remember { mutableStateOf<String?>(null) }
|
||||
var refresh by remember { mutableIntStateOf(0) }
|
||||
var showEditor by remember { mutableStateOf(false) }
|
||||
var loginMode by remember { mutableStateOf(false) }
|
||||
var verifyBusy by remember { mutableStateOf(false) }
|
||||
var confirmDelete by remember { mutableStateOf(false) }
|
||||
var infoMsg by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
LaunchedEffect(refresh) {
|
||||
loading = true
|
||||
err = null
|
||||
runCatching {
|
||||
val a = dependencies.clickTtAccountApi.getAccount()
|
||||
val s = dependencies.clickTtAccountApi.getStatus()
|
||||
account = a.account
|
||||
status = s
|
||||
}.onFailure { err = it.message }
|
||||
loading = false
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(PersonalHubPad),
|
||||
) {
|
||||
PersonalHubTopBar(tr("navigation.clickTtAccount", "HTTV / click-TT-Account"), onBack)
|
||||
infoMsg?.let { Text(it, color = MaterialTheme.colors.primary, modifier = Modifier.padding(bottom = 8.dp)) }
|
||||
err?.let { Text(it, color = MaterialTheme.colors.error, modifier = Modifier.padding(bottom = 8.dp)) }
|
||||
when {
|
||||
loading -> CircularProgressIndicator(modifier = Modifier.padding(top = 24.dp))
|
||||
account != null -> {
|
||||
val a = account!!
|
||||
Card(modifier = Modifier.fillMaxWidth(), elevation = 1.dp) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(ct("mobile.clickTt.linked", "Verknüpfter Account"), fontWeight = FontWeight.SemiBold)
|
||||
detailRow(ct("mobile.clickTt.username", "Benutzername"), a.username ?: "–")
|
||||
detailRow(
|
||||
ct("mobile.clickTt.passwordSaved", "Passwort gespeichert"),
|
||||
if (status?.hasPassword == true) tr("myTischtennisAccount.yes", "Ja") else tr("myTischtennisAccount.no", "Nein"),
|
||||
)
|
||||
a.lastLoginSuccess?.let { detailRow(ct("mobile.clickTt.lastOk", "Letzter erfolgreicher Login"), formatDateTimeUi(it)) }
|
||||
a.lastLoginAttempt?.let { detailRow(ct("mobile.clickTt.lastTry", "Letzter Login-Versuch"), formatDateTimeUi(it)) }
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = { loginMode = false; showEditor = true },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(ct("mobile.clickTt.edit", "Account bearbeiten")) }
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
verifyBusy = true
|
||||
err = null
|
||||
runCatching {
|
||||
dependencies.clickTtAccountApi.verifyLogin(null)
|
||||
refresh++
|
||||
infoMsg = ct("mobile.clickTt.verifyOk", "Login erfolgreich")
|
||||
}.onFailure { e ->
|
||||
if (e is ApiException && e.statusCode == 400) {
|
||||
loginMode = true
|
||||
showEditor = true
|
||||
} else {
|
||||
err = e.message
|
||||
}
|
||||
}
|
||||
verifyBusy = false
|
||||
}
|
||||
},
|
||||
enabled = !verifyBusy,
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin).padding(top = 8.dp),
|
||||
) {
|
||||
Text(if (verifyBusy) "…" else ct("mobile.clickTt.verifyAgain", "Login erneut testen"))
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { confirmDelete = true },
|
||||
modifier = Modifier.fillMaxWidth().heightIn(min = PersonalHubTouchMin).padding(top = 8.dp),
|
||||
) { Text(ct("mobile.clickTt.delete", "Account löschen")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Text(ct("mobile.clickTt.none", "Es ist noch kein HTTV-/click-TT-Account hinterlegt."))
|
||||
Button(
|
||||
onClick = { loginMode = false; showEditor = true },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 12.dp).heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(ct("mobile.clickTt.link", "Account verknüpfen")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showEditor) {
|
||||
ClickTtEditorDialog(
|
||||
account = account,
|
||||
loginMode = loginMode,
|
||||
tr = ::ct,
|
||||
onDismiss = { showEditor = false; loginMode = false },
|
||||
onSaved = {
|
||||
showEditor = false
|
||||
loginMode = false
|
||||
refresh++
|
||||
infoMsg = ct("mobile.clickTt.saved", "Gespeichert")
|
||||
},
|
||||
onLoggedIn = {
|
||||
showEditor = false
|
||||
loginMode = false
|
||||
refresh++
|
||||
infoMsg = ct("mobile.clickTt.verifyOk", "Login erfolgreich")
|
||||
},
|
||||
scope = scope,
|
||||
api = dependencies.clickTtAccountApi,
|
||||
)
|
||||
}
|
||||
|
||||
if (confirmDelete) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirmDelete = false },
|
||||
title = { Text(ct("mobile.clickTt.deleteTitle", "Account löschen")) },
|
||||
text = { Text(ct("mobile.clickTt.deleteConfirm", "Wirklich löschen?")) },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
runCatching {
|
||||
dependencies.clickTtAccountApi.deleteAccount()
|
||||
account = null
|
||||
status = null
|
||||
confirmDelete = false
|
||||
refresh++
|
||||
infoMsg = ct("mobile.clickTt.deleted", "Gelöscht")
|
||||
}.onFailure { err = it.message; confirmDelete = false }
|
||||
}
|
||||
},
|
||||
) { Text(ct("mobile.clickTt.delete", "Löschen")) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { confirmDelete = false }) { Text(tr("myTischtennisDialog.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClickTtEditorDialog(
|
||||
account: ClickTtAccountDto?,
|
||||
loginMode: Boolean,
|
||||
tr: (String, String) -> String,
|
||||
onDismiss: () -> Unit,
|
||||
onSaved: () -> Unit,
|
||||
onLoggedIn: () -> Unit,
|
||||
scope: kotlinx.coroutines.CoroutineScope,
|
||||
api: de.tt_tagebuch.shared.api.ClickTtAccountApi,
|
||||
) {
|
||||
var username by remember(account?.id, loginMode) { mutableStateOf(account?.username.orEmpty()) }
|
||||
var password by remember(account?.id, loginMode) { mutableStateOf("") }
|
||||
var savePw by remember(account?.id, loginMode) { mutableStateOf(account?.savePassword == true) }
|
||||
var userPw by remember(account?.id, loginMode) { mutableStateOf("") }
|
||||
var err by remember { mutableStateOf<String?>(null) }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = {
|
||||
Text(
|
||||
if (loginMode) tr("mobile.clickTt.loginTitle", "Login")
|
||||
else if (account != null) tr("mobile.clickTt.editTitle", "Account bearbeiten")
|
||||
else tr("mobile.clickTt.linkTitle", "Account verknüpfen"),
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
err?.let { Text(it, color = MaterialTheme.colors.error) }
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { if (!loginMode) username = it },
|
||||
readOnly = loginMode,
|
||||
label = { Text(tr("mobile.clickTt.username", "Benutzername")) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(tr("mobile.clickTt.password", "Passwort")) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (!loginMode) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Switch(checked = savePw, onCheckedChange = { savePw = it })
|
||||
Text(tr("mobile.clickTt.savePassword", "Passwort speichern"), modifier = Modifier.padding(start = 8.dp))
|
||||
}
|
||||
if (password.isNotBlank()) {
|
||||
OutlinedTextField(
|
||||
value = userPw,
|
||||
onValueChange = { userPw = it },
|
||||
label = { Text(tr("mobile.clickTt.appPassword", "App-Passwort")) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = !busy && if (loginMode) {
|
||||
password.isNotBlank()
|
||||
} else {
|
||||
username.isNotBlank() && (password.isBlank() || userPw.isNotBlank())
|
||||
},
|
||||
onClick = {
|
||||
scope.launch {
|
||||
busy = true
|
||||
err = null
|
||||
runCatching {
|
||||
if (loginMode) {
|
||||
api.verifyLogin(password)
|
||||
onLoggedIn()
|
||||
} else {
|
||||
if (password.isNotBlank() && userPw.isBlank()) return@launch
|
||||
api.upsertAccount(
|
||||
ClickTtAccountUpsertBody(
|
||||
username = username.trim(),
|
||||
password = password.ifBlank { null },
|
||||
savePassword = savePw,
|
||||
userPassword = userPw.ifBlank { null },
|
||||
),
|
||||
)
|
||||
onSaved()
|
||||
}
|
||||
}.onFailure { e -> err = e.message }
|
||||
busy = false
|
||||
}
|
||||
},
|
||||
) {
|
||||
Text(if (loginMode) tr("mobile.clickTt.testLogin", "Login testen") else tr("myTischtennisDialog.save", "Speichern"))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss, enabled = !busy) { Text(tr("myTischtennisDialog.cancel", "Abbrechen")) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private enum class ClickTtPageKind { LeaguePage, ClubInfo, RegionMeetings, DirectUrl, PresetHttvHome, PresetTtdeLogin }
|
||||
|
||||
private val clickTtPresetUrls = mapOf(
|
||||
ClickTtPageKind.PresetHttvHome to "https://httv.de",
|
||||
ClickTtPageKind.PresetTtdeLogin to "https://ttde-id.liga.nu/oauth2/authz/ttde?scope=nuLiga&response_type=code&redirect_uri=https%3A%2F%2Fhttv.click-tt.de%2Fcgi-bin%2FWebObjects%2FnuLigaTTDE.woa%2Fwa%2FoAuthLogin&state=nonce%3DVF6WbXUOvTjpsGq9zoZ6oxTH7625JEGH&client_id=XtVpGjXKAhz3BZuu",
|
||||
)
|
||||
|
||||
private fun encodeQuery(s: String): String = URLEncoder.encode(s, StandardCharsets.UTF_8.name())
|
||||
|
||||
private fun buildClickTtProxyUrl(
|
||||
backendBase: String,
|
||||
sid: String,
|
||||
kind: ClickTtPageKind,
|
||||
association: String,
|
||||
championship: String,
|
||||
clubId: String,
|
||||
directUrl: String,
|
||||
): String {
|
||||
val base = backendBase.trimEnd('/')
|
||||
val sb = StringBuilder(base).append("/api/clicktt/proxy?sid=").append(encodeQuery(sid))
|
||||
when (kind) {
|
||||
ClickTtPageKind.DirectUrl -> {
|
||||
sb.append("&url=").append(encodeQuery(directUrl.trim()))
|
||||
}
|
||||
ClickTtPageKind.PresetHttvHome, ClickTtPageKind.PresetTtdeLogin -> {
|
||||
val u = clickTtPresetUrls[kind]!!
|
||||
sb.append("&url=").append(encodeQuery(u))
|
||||
}
|
||||
ClickTtPageKind.LeaguePage -> {
|
||||
sb.append("&type=leaguePage")
|
||||
sb.append("&association=").append(encodeQuery(association))
|
||||
sb.append("&championship=").append(encodeQuery(championship))
|
||||
}
|
||||
ClickTtPageKind.RegionMeetings -> {
|
||||
sb.append("&type=regionMeetings")
|
||||
sb.append("&association=").append(encodeQuery(association))
|
||||
sb.append("&championship=").append(encodeQuery(championship))
|
||||
}
|
||||
ClickTtPageKind.ClubInfo -> {
|
||||
sb.append("&type=clubInfo")
|
||||
sb.append("&association=").append(encodeQuery(association))
|
||||
sb.append("&clubId=").append(encodeQuery(clubId.trim()))
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Composable
|
||||
private fun ClickTtBrowserScreen(dependencies: AppDependencies, onBack: () -> Unit) {
|
||||
val languageCode = LocalLanguageCode.current
|
||||
fun tr(key: String, fb: String) = MobileStrings.get(languageCode, key, fb)
|
||||
var kind by remember { mutableStateOf(ClickTtPageKind.LeaguePage) }
|
||||
var assocMenu by remember { mutableStateOf(false) }
|
||||
var pageMenu by remember { mutableStateOf(false) }
|
||||
var association by remember { mutableStateOf("HeTTV") }
|
||||
var championship by remember { mutableStateOf("HTTV 25/26") }
|
||||
var clubId by remember { mutableStateOf("") }
|
||||
var directUrl by remember { mutableStateOf("") }
|
||||
var sid by remember { mutableStateOf(UUID.randomUUID().toString()) }
|
||||
var loadedUrl by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val assocs = listOf("HeTTV", "RTTV", "WTTV", "TTVNw")
|
||||
fun canLoad(): Boolean = when (kind) {
|
||||
ClickTtPageKind.DirectUrl -> directUrl.isNotBlank()
|
||||
ClickTtPageKind.ClubInfo -> clubId.isNotBlank()
|
||||
else -> true
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(PersonalHubPad),
|
||||
) {
|
||||
PersonalHubTopBar(tr("navigation.clickTtBrowser", "HTTV / click-TT"), onBack)
|
||||
Text(
|
||||
tr("mobile.clickTtBrowser.hint", "Lade click-TT-Seiten über den Server-Proxy. JavaScript ist aktiviert."),
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.padding(bottom = 8.dp),
|
||||
)
|
||||
Text(tr("mobile.clickTtBrowser.pageType", "Seitentyp"), style = MaterialTheme.typography.caption)
|
||||
Box {
|
||||
TextButton(onClick = { pageMenu = true }, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
when (kind) {
|
||||
ClickTtPageKind.LeaguePage -> "Ligenübersicht"
|
||||
ClickTtPageKind.ClubInfo -> "Vereinsinfo"
|
||||
ClickTtPageKind.RegionMeetings -> "Regionsspielplan"
|
||||
ClickTtPageKind.DirectUrl -> "Direkte URL"
|
||||
ClickTtPageKind.PresetHttvHome -> "HTTV Einstieg"
|
||||
ClickTtPageKind.PresetTtdeLogin -> "TTDE Login"
|
||||
},
|
||||
)
|
||||
}
|
||||
DropdownMenu(expanded = pageMenu, onDismissRequest = { pageMenu = false }) {
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.LeaguePage; pageMenu = false }) { Text("Ligenübersicht") }
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.ClubInfo; pageMenu = false }) { Text("Vereinsinfo") }
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.RegionMeetings; pageMenu = false }) { Text("Regionsspielplan") }
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.DirectUrl; pageMenu = false }) { Text("Direkte URL") }
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.PresetHttvHome; pageMenu = false }) { Text("HTTV Einstieg") }
|
||||
DropdownMenuItem(onClick = { kind = ClickTtPageKind.PresetTtdeLogin; pageMenu = false }) { Text("TTDE Login") }
|
||||
}
|
||||
}
|
||||
if (kind == ClickTtPageKind.LeaguePage || kind == ClickTtPageKind.ClubInfo || kind == ClickTtPageKind.RegionMeetings) {
|
||||
Text(tr("mobile.clickTtBrowser.association", "Verband"), style = MaterialTheme.typography.caption, modifier = Modifier.padding(top = 8.dp))
|
||||
Box {
|
||||
TextButton(onClick = { assocMenu = true }) { Text(association) }
|
||||
DropdownMenu(expanded = assocMenu, onDismissRequest = { assocMenu = false }) {
|
||||
assocs.forEach { a ->
|
||||
DropdownMenuItem(onClick = { association = a; assocMenu = false }) { Text(a) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kind == ClickTtPageKind.LeaguePage || kind == ClickTtPageKind.RegionMeetings) {
|
||||
OutlinedTextField(
|
||||
value = championship,
|
||||
onValueChange = { championship = it },
|
||||
label = { Text(tr("mobile.clickTtBrowser.championship", "Championship / Saison")) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
if (kind == ClickTtPageKind.ClubInfo) {
|
||||
OutlinedTextField(
|
||||
value = clubId,
|
||||
onValueChange = { clubId = it },
|
||||
label = { Text(tr("mobile.clickTtBrowser.clubId", "Vereins-ID")) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
if (kind == ClickTtPageKind.DirectUrl) {
|
||||
OutlinedTextField(
|
||||
value = directUrl,
|
||||
onValueChange = { directUrl = it },
|
||||
label = { Text("URL") },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
singleLine = false,
|
||||
minLines = 2,
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!canLoad()) return@Button
|
||||
sid = UUID.randomUUID().toString()
|
||||
loadedUrl = buildClickTtProxyUrl(
|
||||
dependencies.apiConfig.baseUrl,
|
||||
sid,
|
||||
kind,
|
||||
association,
|
||||
championship,
|
||||
clubId,
|
||||
directUrl,
|
||||
)
|
||||
},
|
||||
enabled = canLoad(),
|
||||
modifier = Modifier.weight(1f).heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(tr("mobile.clickTtBrowser.load", "Laden")) }
|
||||
OutlinedButton(
|
||||
onClick = { loadedUrl = null },
|
||||
modifier = Modifier.heightIn(min = PersonalHubTouchMin),
|
||||
) { Text(tr("mobile.clickTtBrowser.clear", "Leeren")) }
|
||||
}
|
||||
Spacer(Modifier.height(8.dp))
|
||||
val u = loadedUrl
|
||||
if (u != null) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
WebView(ctx).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
)
|
||||
settings.javaScriptEnabled = true
|
||||
settings.domStorageEnabled = true
|
||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
webViewClient = WebViewClient()
|
||||
webChromeClient = WebChromeClient()
|
||||
loadUrl(u)
|
||||
}
|
||||
},
|
||||
update = { w ->
|
||||
if (w.url != u) w.loadUrl(u)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
tr("mobile.clickTtBrowser.placeholder", "Seite laden …"),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
.padding(top = 16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user