Compare commits
37 Commits
ffc4b66601
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91335422aa | ||
|
|
87e08df93a | ||
|
|
835a31a72e | ||
|
|
9eacbc77ea | ||
|
|
1fc28a6438 | ||
|
|
d3e6fbf49b | ||
|
|
a9ce55699e | ||
|
|
3996e8d68c | ||
|
|
1a7ba583d8 | ||
|
|
645e4b9655 | ||
|
|
93b76d4446 | ||
|
|
2842c89bef | ||
|
|
dc08ae98ea | ||
|
|
5e8006323b | ||
|
|
e02de6cbb2 | ||
|
|
3e981de770 | ||
|
|
65e7563e79 | ||
|
|
9bc9501219 | ||
|
|
5ca4253b44 | ||
|
|
eb75f86d45 | ||
|
|
395e2a84bd | ||
|
|
3fb60227df | ||
|
|
9a4eb77c56 | ||
|
|
02c1765214 | ||
|
|
ee93e15af8 | ||
|
|
d31026a606 | ||
|
|
4dca2af645 | ||
|
|
63b5786e43 | ||
|
|
497d804244 | ||
|
|
9ca1464899 | ||
|
|
8f6fda2e61 | ||
|
|
3314aa06b4 | ||
|
|
12c909d6a4 | ||
|
|
c2594fd330 | ||
|
|
00b08e2f92 | ||
|
|
93da9fbcbe | ||
|
|
82dcb1da40 |
@@ -16,6 +16,13 @@ jobs:
|
|||||||
# Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
|
# Der Analyse-Workflow benötigt keine Historie. Ein flacher Checkout
|
||||||
# verhindert, dass der Runner das große Repository-Pack vollständig lädt.
|
# verhindert, dass der Runner das große Repository-Pack vollständig lädt.
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
# Der Web-Workflow braucht keine Android-Artefakte. Insbesondere liegt
|
||||||
|
# dort ein großer Heap-Dump im aktuellen Commit, der auch bei einem
|
||||||
|
# flachen Checkout übertragen würde und den Runner-Checkout abbricht.
|
||||||
|
sparse-checkout: |
|
||||||
|
/*
|
||||||
|
!/android-app/
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
- name: Ensure clean workspace
|
- name: Ensure clean workspace
|
||||||
run: |
|
run: |
|
||||||
@@ -114,11 +121,12 @@ jobs:
|
|||||||
|
|
||||||
- name: OSV-Scanner (SCA)
|
- name: OSV-Scanner (SCA)
|
||||||
run: |
|
run: |
|
||||||
|
cd "$GITHUB_WORKSPACE"
|
||||||
curl -L -o osv-scanner https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
|
curl -L -o osv-scanner https://github.com/google/osv-scanner/releases/latest/download/osv-scanner_linux_amd64
|
||||||
chmod +x osv-scanner
|
chmod +x osv-scanner
|
||||||
./osv-scanner --version
|
./osv-scanner --version
|
||||||
test -f ./package-lock.json
|
test -f "$GITHUB_WORKSPACE/package-lock.json"
|
||||||
./osv-scanner scan -L ./package-lock.json --config ./.osv-scanner.toml
|
./osv-scanner scan -L "$GITHUB_WORKSPACE/package-lock.json" --config "$GITHUB_WORKSPACE/.osv-scanner.toml"
|
||||||
|
|
||||||
deploy-production:
|
deploy-production:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -94,6 +94,10 @@ dist
|
|||||||
/android-app/**/build/
|
/android-app/**/build/
|
||||||
/android-app/local.properties
|
/android-app/local.properties
|
||||||
/android-app/gradle-local.properties
|
/android-app/gradle-local.properties
|
||||||
|
# Android Play Store / release artifacts are generated locally or in CI.
|
||||||
|
/android-app/**/*.aab
|
||||||
|
# JVM Heap-Dumps sind lokale Diagnoseartefakte und dürfen nie ins Repository.
|
||||||
|
*.hprof
|
||||||
|
|
||||||
# Build output (but keep production data!)
|
# Build output (but keep production data!)
|
||||||
.output
|
.output
|
||||||
|
|||||||
@@ -1,4 +1 @@
|
|||||||
[[IgnoredVulns]]
|
# Intentionally empty: retained because the CI scanner is invoked with --config.
|
||||||
id = "GHSA-v3m3-f69x-jf25"
|
|
||||||
ignoreUntil = 2026-12-31
|
|
||||||
reason = "Temporary exception: Quill 2.0.3 is required by the current RichTextEditor implementation, and OSV currently reports no fixed version. Track upstream fix and remove this ignore once a patched release is available."
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,9 +21,20 @@ import de.harheimertc.ui.navigation.NavigationViewModel
|
|||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import de.harheimertc.repositories.AuthRepository
|
||||||
|
import de.harheimertc.repositories.PushTokenRepository
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
@Inject
|
||||||
|
lateinit var authRepository: AuthRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var pushTokenRepository: PushTokenRepository
|
||||||
|
|
||||||
private val notificationRoute = mutableStateOf<String?>(null)
|
private val notificationRoute = mutableStateOf<String?>(null)
|
||||||
|
|
||||||
private val notificationPermissionLauncher = registerForActivityResult(
|
private val notificationPermissionLauncher = registerForActivityResult(
|
||||||
@@ -50,6 +61,23 @@ class MainActivity : ComponentActivity() {
|
|||||||
notificationRoute.value = extractNotificationRoute(intent)
|
notificationRoute.value = extractNotificationRoute(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
syncPushTokenWhenSignedIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side tokens are persistent, but re-registering the current FCM
|
||||||
|
* token is cheap and idempotent. It restores a token if server data was
|
||||||
|
* ever recovered from a backup or manually repaired.
|
||||||
|
*/
|
||||||
|
private fun syncPushTokenWhenSignedIn() {
|
||||||
|
if (authRepository.getToken().isNullOrBlank() && authRepository.getRefreshToken().isNullOrBlank()) return
|
||||||
|
lifecycleScope.launch {
|
||||||
|
pushTokenRepository.registerCurrentDevice()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractNotificationRoute(intent: Intent?): String? =
|
private fun extractNotificationRoute(intent: Intent?): String? =
|
||||||
intent?.getStringExtra(EXTRA_NOTIFICATION_ROUTE)?.takeIf { it.isNotBlank() }
|
intent?.getStringExtra(EXTRA_NOTIFICATION_ROUTE)?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
|
|||||||
@@ -269,6 +269,7 @@ data class NotificationSettingsDto(
|
|||||||
val birthdays: Boolean = false,
|
val birthdays: Boolean = false,
|
||||||
val newContactRequest: Boolean = false,
|
val newContactRequest: Boolean = false,
|
||||||
val newUserRegistration: Boolean = false,
|
val newUserRegistration: Boolean = false,
|
||||||
|
val ownTtrChanges: Boolean = false,
|
||||||
val selectedTeamSlugs: List<String> = emptyList(),
|
val selectedTeamSlugs: List<String> = emptyList(),
|
||||||
val selectedTeamSeason: String? = null,
|
val selectedTeamSeason: String? = null,
|
||||||
val notificationTime: String = "09:00",
|
val notificationTime: String = "09:00",
|
||||||
@@ -282,6 +283,7 @@ data class PushTokenRequest(
|
|||||||
val token: String,
|
val token: String,
|
||||||
val platform: String = "android",
|
val platform: String = "android",
|
||||||
val appVersion: String? = null,
|
val appVersion: String? = null,
|
||||||
|
val installationId: String? = null,
|
||||||
)
|
)
|
||||||
data class BirthdayDto(
|
data class BirthdayDto(
|
||||||
val name: String = "",
|
val name: String = "",
|
||||||
@@ -302,6 +304,7 @@ data class QttrRowDto(
|
|||||||
val playerName: String = "",
|
val playerName: String = "",
|
||||||
val clubName: String = "",
|
val clubName: String = "",
|
||||||
val currentQttr: Int? = null,
|
val currentQttr: Int? = null,
|
||||||
|
val currentTtr: Int? = null,
|
||||||
val previousQttr: Int? = null,
|
val previousQttr: Int? = null,
|
||||||
val birthdate: String? = null,
|
val birthdate: String? = null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class HarheimerMessagingService : FirebaseMessagingService() {
|
|||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
pushTokenRepository.registerToken(token)
|
pushTokenRepository.registerCurrentDevice()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ object HarheimerNotifications {
|
|||||||
"news", "news_expiring" -> Destinations.MemberNews.route
|
"news", "news_expiring" -> Destinations.MemberNews.route
|
||||||
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
|
"event", "events_today", "events_tomorrow" -> Destinations.Termine.route
|
||||||
"team_matches" -> Destinations.Spielplan.route
|
"team_matches" -> Destinations.Spielplan.route
|
||||||
|
"qttr_list_updated", "ttr_change" -> Destinations.Qttr.route
|
||||||
"birthdays" -> Destinations.MemberArea.route
|
"birthdays" -> Destinations.MemberArea.route
|
||||||
"contact_request" -> Destinations.CmsContactRequests.route
|
"contact_request" -> Destinations.CmsContactRequests.route
|
||||||
"user_registration" -> Destinations.CmsBenutzer.route
|
"user_registration" -> Destinations.CmsBenutzer.route
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ data class NotificationPreferences(
|
|||||||
val birthdays: Boolean = false,
|
val birthdays: Boolean = false,
|
||||||
val newContactRequest: Boolean = false,
|
val newContactRequest: Boolean = false,
|
||||||
val newUserRegistration: Boolean = false,
|
val newUserRegistration: Boolean = false,
|
||||||
|
val ownTtrChanges: Boolean = false,
|
||||||
val selectedTeamSlugs: Set<String> = emptySet(),
|
val selectedTeamSlugs: Set<String> = emptySet(),
|
||||||
val selectedTeamSeason: String? = null,
|
val selectedTeamSeason: String? = null,
|
||||||
val notificationTime: String = DEFAULT_NOTIFICATION_TIME,
|
val notificationTime: String = DEFAULT_NOTIFICATION_TIME,
|
||||||
@@ -43,6 +44,7 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
birthdays = preferences.getBoolean(KEY_BIRTHDAYS, false),
|
birthdays = preferences.getBoolean(KEY_BIRTHDAYS, false),
|
||||||
newContactRequest = preferences.getBoolean(KEY_NEW_CONTACT_REQUEST, false),
|
newContactRequest = preferences.getBoolean(KEY_NEW_CONTACT_REQUEST, false),
|
||||||
newUserRegistration = preferences.getBoolean(KEY_NEW_USER_REGISTRATION, false),
|
newUserRegistration = preferences.getBoolean(KEY_NEW_USER_REGISTRATION, false),
|
||||||
|
ownTtrChanges = preferences.getBoolean(KEY_OWN_TTR_CHANGES, false),
|
||||||
selectedTeamSlugs = preferences.getStringSet(KEY_SELECTED_TEAM_SLUGS, emptySet()).orEmpty(),
|
selectedTeamSlugs = preferences.getStringSet(KEY_SELECTED_TEAM_SLUGS, emptySet()).orEmpty(),
|
||||||
selectedTeamSeason = preferences.getString(KEY_SELECTED_TEAM_SEASON, null)?.takeIf { it.isNotBlank() },
|
selectedTeamSeason = preferences.getString(KEY_SELECTED_TEAM_SEASON, null)?.takeIf { it.isNotBlank() },
|
||||||
notificationTime = preferences.getString(KEY_NOTIFICATION_TIME, DEFAULT_NOTIFICATION_TIME) ?: DEFAULT_NOTIFICATION_TIME,
|
notificationTime = preferences.getString(KEY_NOTIFICATION_TIME, DEFAULT_NOTIFICATION_TIME) ?: DEFAULT_NOTIFICATION_TIME,
|
||||||
@@ -69,6 +71,7 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
.putBoolean(KEY_BIRTHDAYS, settings.birthdays)
|
.putBoolean(KEY_BIRTHDAYS, settings.birthdays)
|
||||||
.putBoolean(KEY_NEW_CONTACT_REQUEST, settings.newContactRequest)
|
.putBoolean(KEY_NEW_CONTACT_REQUEST, settings.newContactRequest)
|
||||||
.putBoolean(KEY_NEW_USER_REGISTRATION, settings.newUserRegistration)
|
.putBoolean(KEY_NEW_USER_REGISTRATION, settings.newUserRegistration)
|
||||||
|
.putBoolean(KEY_OWN_TTR_CHANGES, settings.ownTtrChanges)
|
||||||
.putStringSet(KEY_SELECTED_TEAM_SLUGS, settings.selectedTeamSlugs)
|
.putStringSet(KEY_SELECTED_TEAM_SLUGS, settings.selectedTeamSlugs)
|
||||||
.putString(KEY_SELECTED_TEAM_SEASON, settings.selectedTeamSeason)
|
.putString(KEY_SELECTED_TEAM_SEASON, settings.selectedTeamSeason)
|
||||||
.putString(KEY_NOTIFICATION_TIME, settings.notificationTime)
|
.putString(KEY_NOTIFICATION_TIME, settings.notificationTime)
|
||||||
@@ -98,6 +101,7 @@ class NotificationPreferencesRepository @Inject constructor(
|
|||||||
const val KEY_BIRTHDAYS = "birthdays"
|
const val KEY_BIRTHDAYS = "birthdays"
|
||||||
const val KEY_NEW_CONTACT_REQUEST = "new_contact_request"
|
const val KEY_NEW_CONTACT_REQUEST = "new_contact_request"
|
||||||
const val KEY_NEW_USER_REGISTRATION = "new_user_registration"
|
const val KEY_NEW_USER_REGISTRATION = "new_user_registration"
|
||||||
|
const val KEY_OWN_TTR_CHANGES = "own_ttr_changes"
|
||||||
const val KEY_SELECTED_TEAM_SLUGS = "selected_team_slugs"
|
const val KEY_SELECTED_TEAM_SLUGS = "selected_team_slugs"
|
||||||
const val KEY_SELECTED_TEAM_SEASON = "selected_team_season"
|
const val KEY_SELECTED_TEAM_SEASON = "selected_team_season"
|
||||||
const val KEY_NOTIFICATION_TIME = "notification_time"
|
const val KEY_NOTIFICATION_TIME = "notification_time"
|
||||||
@@ -114,6 +118,7 @@ private fun NotificationSettingsDto.toPreferences(): NotificationPreferences = N
|
|||||||
birthdays = birthdays,
|
birthdays = birthdays,
|
||||||
newContactRequest = newContactRequest,
|
newContactRequest = newContactRequest,
|
||||||
newUserRegistration = newUserRegistration,
|
newUserRegistration = newUserRegistration,
|
||||||
|
ownTtrChanges = ownTtrChanges,
|
||||||
selectedTeamSlugs = selectedTeamSlugs.toSet(),
|
selectedTeamSlugs = selectedTeamSlugs.toSet(),
|
||||||
selectedTeamSeason = selectedTeamSeason,
|
selectedTeamSeason = selectedTeamSeason,
|
||||||
notificationTime = notificationTime,
|
notificationTime = notificationTime,
|
||||||
@@ -129,6 +134,7 @@ private fun NotificationPreferences.toDto(): NotificationSettingsDto = Notificat
|
|||||||
birthdays = birthdays,
|
birthdays = birthdays,
|
||||||
newContactRequest = newContactRequest,
|
newContactRequest = newContactRequest,
|
||||||
newUserRegistration = newUserRegistration,
|
newUserRegistration = newUserRegistration,
|
||||||
|
ownTtrChanges = ownTtrChanges,
|
||||||
selectedTeamSlugs = selectedTeamSlugs.toList(),
|
selectedTeamSlugs = selectedTeamSlugs.toList(),
|
||||||
selectedTeamSeason = selectedTeamSeason,
|
selectedTeamSeason = selectedTeamSeason,
|
||||||
notificationTime = notificationTime,
|
notificationTime = notificationTime,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.harheimertc.repositories
|
package de.harheimertc.repositories
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.google.firebase.installations.FirebaseInstallations
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import de.harheimertc.BuildConfig
|
import de.harheimertc.BuildConfig
|
||||||
import de.harheimertc.data.ApiService
|
import de.harheimertc.data.ApiService
|
||||||
@@ -15,19 +16,24 @@ class PushTokenRepository @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
suspend fun registerCurrentDevice(): Result<Unit> = runCatching {
|
suspend fun registerCurrentDevice(): Result<Unit> = runCatching {
|
||||||
val token = FirebaseMessaging.getInstance().token.await()
|
val token = FirebaseMessaging.getInstance().token.await()
|
||||||
registerToken(token).getOrThrow()
|
val installationId = FirebaseInstallations.getInstance().id.await()
|
||||||
|
registerToken(token, installationId).getOrThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun registerToken(token: String): Result<Unit> = runCatching {
|
suspend fun registerToken(token: String, installationId: String? = null): Result<Unit> = runCatching {
|
||||||
if (token.isBlank()) return@runCatching
|
if (token.isBlank()) return@runCatching
|
||||||
retryOnNetworkFailure {
|
retryOnNetworkFailure {
|
||||||
val response = api.registerPushToken(
|
val response = api.registerPushToken(
|
||||||
PushTokenRequest(
|
PushTokenRequest(
|
||||||
token = token,
|
token = token,
|
||||||
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
|
appVersion = "${BuildConfig.VERSION_NAME}+${BuildConfig.VERSION_CODE}",
|
||||||
|
installationId = installationId,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (!response.isSuccessful) error("Push-Token konnte nicht registriert werden.")
|
if (!response.isSuccessful) {
|
||||||
|
val detail = response.errorBody()?.string().orEmpty().replace(Regex("\\s+"), " ").take(180)
|
||||||
|
error("Push-Token konnte nicht registriert werden (HTTP ${response.code()})${if (detail.isBlank()) "" else ": $detail"}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
Log.w("PushTokenRepository", "Push-Token Registrierung fehlgeschlagen", error)
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
|||||||
MenuSection.INTERN -> buildList {
|
MenuSection.INTERN -> buildList {
|
||||||
add(MenuTarget("Übersicht", Destinations.MemberArea.route))
|
add(MenuTarget("Übersicht", Destinations.MemberArea.route))
|
||||||
add(MenuTarget("Mitgliederliste", Destinations.Members.route))
|
add(MenuTarget("Mitgliederliste", Destinations.Members.route))
|
||||||
add(MenuTarget("QTTR", Destinations.Qttr.route))
|
add(MenuTarget("TTR / QTTR", Destinations.Qttr.route))
|
||||||
add(MenuTarget("News", Destinations.MemberNews.route))
|
add(MenuTarget("News", Destinations.MemberNews.route))
|
||||||
add(MenuTarget("Mein Profil", Destinations.Profile.route))
|
add(MenuTarget("Mein Profil", Destinations.Profile.route))
|
||||||
add(MenuTarget("Benachrichtigungen", Destinations.NotificationSettings.route))
|
add(MenuTarget("Benachrichtigungen", Destinations.NotificationSettings.route))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
|||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import de.harheimertc.repositories.LoginRepository
|
import de.harheimertc.repositories.LoginRepository
|
||||||
import de.harheimertc.repositories.PasskeyRepository
|
import de.harheimertc.repositories.PasskeyRepository
|
||||||
|
import de.harheimertc.repositories.PushTokenRepository
|
||||||
import de.harheimertc.ui.components.isValidEmail
|
import de.harheimertc.ui.components.isValidEmail
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -29,6 +30,7 @@ data class LoginUiState(
|
|||||||
class LoginViewModel @Inject constructor(
|
class LoginViewModel @Inject constructor(
|
||||||
private val repository: LoginRepository,
|
private val repository: LoginRepository,
|
||||||
private val passkeyRepository: PasskeyRepository,
|
private val passkeyRepository: PasskeyRepository,
|
||||||
|
private val pushTokenRepository: PushTokenRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(LoginUiState())
|
private val _state = MutableStateFlow(LoginUiState())
|
||||||
val state: StateFlow<LoginUiState> = _state
|
val state: StateFlow<LoginUiState> = _state
|
||||||
@@ -70,6 +72,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
_state.value = current.copy(loading = true, error = null, message = null)
|
_state.value = current.copy(loading = true, error = null, message = null)
|
||||||
repository.login(current.email, current.password)
|
repository.login(current.email, current.password)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
|
viewModelScope.launch { pushTokenRepository.registerCurrentDevice() }
|
||||||
_state.value = current.copy(
|
_state.value = current.copy(
|
||||||
password = "",
|
password = "",
|
||||||
loading = false,
|
loading = false,
|
||||||
@@ -92,6 +95,7 @@ class LoginViewModel @Inject constructor(
|
|||||||
_state.value = current.copy(loading = true, error = null, message = null)
|
_state.value = current.copy(loading = true, error = null, message = null)
|
||||||
passkeyRepository.login(context, current.email)
|
passkeyRepository.login(context, current.email)
|
||||||
.onSuccess { response ->
|
.onSuccess { response ->
|
||||||
|
viewModelScope.launch { pushTokenRepository.registerCurrentDevice() }
|
||||||
_state.value = current.copy(
|
_state.value = current.copy(
|
||||||
password = "",
|
password = "",
|
||||||
loading = false,
|
loading = false,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -248,18 +247,7 @@ fun QttrScreen(
|
|||||||
viewModel: QttrViewModel = hiltViewModel(),
|
viewModel: QttrViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val uriHandler = LocalUriHandler.current
|
MemberAreaPage(navController, showBackNavigation, "TTR- und QTTR-Werte", "Aktuelle Werte der Vereinsmitglieder aus myTischtennis.") {
|
||||||
val externalUrl = "https://www.mytischtennis.de/rankings/andro-rangliste?continent=all&country=Deutschland&all-players=on&as=DE.WE.R4.07&di=DE.WE.R4.07.04&area=DE.WE.R4.07.04.43&clubnr-search=Harheimer+TC&clubnr=43030&fednickname=HeTTV&gender=all¤t-ranking=yes&ttr-range=100%3B3000&birth-range=1926%3B2021"
|
|
||||||
|
|
||||||
MemberAreaPage(navController, showBackNavigation, "QTTR-Werte", "Aus technischen Gründen sind nur die QTTR-Werte verfügbar.") {
|
|
||||||
item {
|
|
||||||
Surface(color = Primary100, shape = RoundedCornerShape(12.dp)) {
|
|
||||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text("Für TTR bitte die myTischtennis-Rangliste verwenden.", color = Primary900)
|
|
||||||
TextButton(onClick = { uriHandler.openUri(externalUrl) }) { Text("myTischtennis öffnen") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
item {
|
||||||
Surface(color = Color.White, shape = RoundedCornerShape(14.dp), shadowElevation = 3.dp) {
|
Surface(color = Color.White, shape = RoundedCornerShape(14.dp), shadowElevation = 3.dp) {
|
||||||
Column(Modifier.fillMaxWidth().padding(18.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
Column(Modifier.fillMaxWidth().padding(18.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
@@ -269,9 +257,9 @@ fun QttrScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
state.loading -> item { LoadingState("QTTR-Werte werden geladen...") }
|
state.loading -> item { LoadingState("TTR- und QTTR-Werte werden geladen...") }
|
||||||
state.error != null -> item { ErrorCard(state.error.orEmpty(), viewModel::load) }
|
state.error != null -> item { ErrorCard(state.error.orEmpty(), viewModel::load) }
|
||||||
state.rows.isEmpty() -> item { Text("Keine QTTR-Werte gefunden.", color = Accent700) }
|
state.rows.isEmpty() -> item { Text("Keine TTR- oder QTTR-Werte gefunden.", color = Accent700) }
|
||||||
else -> items(state.rows.size) { index -> QttrRowCard(state.rows[index], isOwnRow(state.rows[index].playerName, state.currentUserName)) }
|
else -> items(state.rows.size) { index -> QttrRowCard(state.rows[index], isOwnRow(state.rows[index].playerName, state.currentUserName)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -388,7 +376,10 @@ private fun QttrRowCard(row: QttrRowDto, highlighted: Boolean) {
|
|||||||
)
|
)
|
||||||
Text(row.clubName.ifBlank { "Harheimer TC" }, color = qttrNameColor(row.gender, isMinor(row.birthdate)).copy(alpha = 0.88f))
|
Text(row.clubName.ifBlank { "Harheimer TC" }, color = qttrNameColor(row.gender, isMinor(row.birthdate)).copy(alpha = 0.88f))
|
||||||
}
|
}
|
||||||
Text(row.currentQttr?.toString() ?: "-", color = Primary600, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
|
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(3.dp)) {
|
||||||
|
Text("TTR ${row.currentTtr?.toString() ?: "-"}", color = Accent700, style = MaterialTheme.typography.labelLarge)
|
||||||
|
Text("QTTR ${row.currentQttr?.toString() ?: "-"}", color = Primary600, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ private fun MemberAreaCardGrid(navController: NavController) {
|
|||||||
onClick = { navController.navigate(Destinations.MemberNews.route) },
|
onClick = { navController.navigate(Destinations.MemberNews.route) },
|
||||||
)
|
)
|
||||||
MemberAreaCard(
|
MemberAreaCard(
|
||||||
title = "QTTR",
|
title = "TTR / QTTR",
|
||||||
description = "Aktuelle QTTR-Werte der Vereinsmitglieder",
|
description = "Aktuelle TTR- und QTTR-Werte der Vereinsmitglieder",
|
||||||
marker = "Q",
|
marker = "T",
|
||||||
onClick = { navController.navigate(Destinations.Qttr.route) },
|
onClick = { navController.navigate(Destinations.Qttr.route) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import de.harheimertc.notifications.HarheimerNotifications
|
import de.harheimertc.notifications.HarheimerNotifications
|
||||||
|
import de.harheimertc.BuildConfig
|
||||||
import de.harheimertc.repositories.Mannschaft
|
import de.harheimertc.repositories.Mannschaft
|
||||||
import de.harheimertc.repositories.NotificationPreferences
|
import de.harheimertc.repositories.NotificationPreferences
|
||||||
import de.harheimertc.ui.components.LoadingState
|
import de.harheimertc.ui.components.LoadingState
|
||||||
@@ -103,6 +104,23 @@ fun NotificationSettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
NotificationCard("Geräte-Registrierung") {
|
||||||
|
Text("Der Push-Token wird dem aktuell angemeldeten Konto auf diesem Server zugeordnet:", color = Accent700)
|
||||||
|
Text(BuildConfig.API_BASE_URL, color = Accent900, style = MaterialTheme.typography.bodySmall)
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::registerCurrentDevice,
|
||||||
|
enabled = !state.deviceRegistrationInProgress,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text(if (state.deviceRegistrationInProgress) "Gerät wird registriert..." else "Android-Gerät jetzt registrieren")
|
||||||
|
}
|
||||||
|
state.deviceRegistrationMessage?.let { message ->
|
||||||
|
Text(message, color = if (state.deviceRegistrationError) MaterialTheme.colorScheme.error else Color(0xFF166534))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (state.loading) {
|
if (state.loading) {
|
||||||
item { LoadingState("Benachrichtigungseinstellungen werden geladen...") }
|
item { LoadingState("Benachrichtigungseinstellungen werden geladen...") }
|
||||||
} else {
|
} else {
|
||||||
@@ -162,6 +180,9 @@ fun NotificationSettingsScreen(
|
|||||||
ToggleRow("Geburtstage", state.settings.birthdays) {
|
ToggleRow("Geburtstage", state.settings.birthdays) {
|
||||||
viewModel.update(state.settings.copy(birthdays = it))
|
viewModel.update(state.settings.copy(birthdays = it))
|
||||||
}
|
}
|
||||||
|
ToggleRow("Änderung meines TTR-Werts", state.settings.ownTtrChanges) {
|
||||||
|
viewModel.update(state.settings.copy(ownTtrChanges = it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user