dev #41
@@ -0,0 +1,49 @@
|
|||||||
|
package de.harheimertc.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ConnectivityMonitor @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
) {
|
||||||
|
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
private val _online = MutableStateFlow(hasInternetAccess())
|
||||||
|
val online: StateFlow<Boolean> = _online.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch { poll() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun poll() {
|
||||||
|
while (currentCoroutineContext().isActive) {
|
||||||
|
val current = hasInternetAccess()
|
||||||
|
if (_online.value != current) {
|
||||||
|
_online.value = current
|
||||||
|
}
|
||||||
|
delay(10_000L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasInternetAccess(): Boolean {
|
||||||
|
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
|
||||||
|
?: return false
|
||||||
|
val network = manager.activeNetwork ?: return false
|
||||||
|
val capabilities = manager.getNetworkCapabilities(network) ?: return false
|
||||||
|
return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,27 +24,27 @@ class GalleryRepository @Inject constructor(
|
|||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
) {
|
) {
|
||||||
suspend fun hasPublicImages(): Result<Boolean> = runCatching {
|
suspend fun hasPublicImages(): Result<Boolean> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.galerieList(page = 1, perPage = 1)
|
val response = api.galerieList(page = 1, perPage = 1)
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
response.body()?.images.orEmpty().isNotEmpty()
|
response.body()?.images.orEmpty().isNotEmpty()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchImages(page: Int = 1, perPage: Int = 60): Result<GalleryPage> {
|
suspend fun fetchImages(page: Int = 1, perPage: Int = 60): Result<GalleryPage> {
|
||||||
return try {
|
return runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val resp = api.galerieList(page = page, perPage = perPage)
|
val resp = api.galerieList(page = page, perPage = perPage)
|
||||||
if (resp.isSuccessful) {
|
if (resp.isSuccessful) {
|
||||||
val body = resp.body()
|
val body = resp.body()
|
||||||
Result.success(
|
|
||||||
GalleryPage(
|
GalleryPage(
|
||||||
images = body?.images.orEmpty().map { it.toGalleryImage() },
|
images = body?.images.orEmpty().map { it.toGalleryImage() },
|
||||||
pagination = body?.pagination ?: GalleryPaginationDto(),
|
pagination = body?.pagination ?: GalleryPaginationDto(),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Result.failure(Exception("HTTP ${resp.code()}"))
|
error("HTTP ${resp.code()}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Result.failure(e)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
val diagnostics = mutableListOf<String>()
|
val diagnostics = mutableListOf<String>()
|
||||||
|
|
||||||
val termine = runCatching {
|
val termine = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.termine()
|
val response = api.termine()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.errorBody()?.string().orEmpty()
|
val errorBody = response.errorBody()?.string().orEmpty()
|
||||||
@@ -44,6 +45,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
error("Termine konnten nicht geladen werden (HTTP ${response.code()}).")
|
error("Termine konnten nicht geladen werden (HTTP ${response.code()}).")
|
||||||
}
|
}
|
||||||
response.body()?.termine.orEmpty()
|
response.body()?.termine.orEmpty()
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
captureLoadIssue("fetchHomeData.termine", error)
|
captureLoadIssue("fetchHomeData.termine", error)
|
||||||
if (diagnostics.none { it.contains("GET /api/termine") }) {
|
if (diagnostics.none { it.contains("GET /api/termine") }) {
|
||||||
@@ -58,6 +60,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}.getOrDefault(emptyList())
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
val spielplanResponse = runCatching {
|
val spielplanResponse = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.spielplan()
|
val response = api.spielplan()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.errorBody()?.string().orEmpty()
|
val errorBody = response.errorBody()?.string().orEmpty()
|
||||||
@@ -71,6 +74,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
error("Spielplan konnte nicht geladen werden (HTTP ${response.code()}).")
|
error("Spielplan konnte nicht geladen werden (HTTP ${response.code()}).")
|
||||||
}
|
}
|
||||||
response.body()
|
response.body()
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
captureLoadIssue("fetchHomeData.spielplan", error)
|
captureLoadIssue("fetchHomeData.spielplan", error)
|
||||||
if (diagnostics.none { it.contains("GET /api/spielplan") }) {
|
if (diagnostics.none { it.contains("GET /api/spielplan") }) {
|
||||||
@@ -86,6 +90,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
val spiele = spielplanResponse?.data.orEmpty()
|
val spiele = spielplanResponse?.data.orEmpty()
|
||||||
|
|
||||||
val news = runCatching {
|
val news = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.publicNews()
|
val response = api.publicNews()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.errorBody()?.string().orEmpty()
|
val errorBody = response.errorBody()?.string().orEmpty()
|
||||||
@@ -99,6 +104,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
error("News konnten nicht geladen werden (HTTP ${response.code()}).")
|
error("News konnten nicht geladen werden (HTTP ${response.code()}).")
|
||||||
}
|
}
|
||||||
response.body()?.news.orEmpty()
|
response.body()?.news.orEmpty()
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
captureLoadIssue("fetchHomeData.news", error)
|
captureLoadIssue("fetchHomeData.news", error)
|
||||||
if (diagnostics.none { it.contains("GET /api/news-public") }) {
|
if (diagnostics.none { it.contains("GET /api/news-public") }) {
|
||||||
@@ -113,6 +119,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}.getOrDefault(emptyList())
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
val homepageSections = runCatching {
|
val homepageSections = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.config()
|
val response = api.config()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.errorBody()?.string().orEmpty()
|
val errorBody = response.errorBody()?.string().orEmpty()
|
||||||
@@ -126,6 +133,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
error("Konfiguration konnte nicht geladen werden (HTTP ${response.code()}).")
|
error("Konfiguration konnte nicht geladen werden (HTTP ${response.code()}).")
|
||||||
}
|
}
|
||||||
response.body()?.homepage?.sections.orEmpty()
|
response.body()?.homepage?.sections.orEmpty()
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
captureLoadIssue("fetchHomeData.config", error)
|
captureLoadIssue("fetchHomeData.config", error)
|
||||||
if (diagnostics.none { it.contains("GET /api/config") }) {
|
if (diagnostics.none { it.contains("GET /api/config") }) {
|
||||||
@@ -140,6 +148,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}.getOrDefault(emptyList())
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
val heroImageUrl = runCatching {
|
val heroImageUrl = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.heroImages()
|
val response = api.heroImages()
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val errorBody = response.errorBody()?.string().orEmpty()
|
val errorBody = response.errorBody()?.string().orEmpty()
|
||||||
@@ -154,6 +163,7 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}
|
}
|
||||||
val variants = response.body()?.variants.orEmpty()
|
val variants = response.body()?.variants.orEmpty()
|
||||||
pickRandomHeroImage(variants)
|
pickRandomHeroImage(variants)
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
captureLoadIssue("fetchHomeData.heroImages", error)
|
captureLoadIssue("fetchHomeData.heroImages", error)
|
||||||
if (diagnostics.none { it.contains("GET /api/hero-images") }) {
|
if (diagnostics.none { it.contains("GET /api/hero-images") }) {
|
||||||
@@ -187,9 +197,11 @@ class HomeRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchSpielplanForSeason(season: String): Result<SpielplanResponse> = runCatching {
|
suspend fun fetchSpielplanForSeason(season: String): Result<SpielplanResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.spielplan(season)
|
val response = api.spielplan(season)
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
|
}
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Sentry.withScope { scope ->
|
Sentry.withScope { scope ->
|
||||||
scope.setTag("repository", "HomeRepository")
|
scope.setTag("repository", "HomeRepository")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class LoginRepository @Inject constructor(
|
|||||||
suspend fun login(email: String, password: String): Result<LoginResponse> = runCatching {
|
suspend fun login(email: String, password: String): Result<LoginResponse> = runCatching {
|
||||||
val endpoint = "api/auth/login"
|
val endpoint = "api/auth/login"
|
||||||
val requestPreview = "{email=\"${maskEmail(email)}\", client=\"android\", deviceName=\"Harheimer TC Android-App\"}"
|
val requestPreview = "{email=\"${maskEmail(email)}\", client=\"android\", deviceName=\"Harheimer TC Android-App\"}"
|
||||||
val response = api.login(LoginRequest(email.trim(), password))
|
val response = retryOnNetworkFailure { api.login(LoginRequest(email.trim(), password)) }
|
||||||
if (!response.isSuccessful) {
|
if (!response.isSuccessful) {
|
||||||
val body = response.errorBody()?.string().orEmpty()
|
val body = response.errorBody()?.string().orEmpty()
|
||||||
val serverMessage = extractServerMessage(body)
|
val serverMessage = extractServerMessage(body)
|
||||||
@@ -71,11 +71,11 @@ class LoginRepository @Inject constructor(
|
|||||||
return@runCatching AuthStatusResponse()
|
return@runCatching AuthStatusResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = api.authStatus()
|
var response = retryOnNetworkFailure { api.authStatus() }
|
||||||
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
|
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
|
||||||
var status = response.body() ?: AuthStatusResponse()
|
var status = response.body() ?: AuthStatusResponse()
|
||||||
if (!status.isLoggedIn && authRepository.getRefreshToken() != null && sessionRefresher.refreshAccessToken()) {
|
if (!status.isLoggedIn && authRepository.getRefreshToken() != null && sessionRefresher.refreshAccessToken()) {
|
||||||
response = api.authStatus()
|
response = retryOnNetworkFailure { api.authStatus() }
|
||||||
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
|
if (!response.isSuccessful) error("Status konnte nicht geprüft werden.")
|
||||||
status = response.body() ?: AuthStatusResponse()
|
status = response.body() ?: AuthStatusResponse()
|
||||||
}
|
}
|
||||||
@@ -93,16 +93,20 @@ class LoginRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resetPassword(email: String): Result<AuthMessageResponse> = runCatching {
|
suspend fun resetPassword(email: String): Result<AuthMessageResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.resetPassword(ResetPasswordRequest(email.trim()))
|
val response = api.resetPassword(ResetPasswordRequest(email.trim()))
|
||||||
if (!response.isSuccessful) error("Anfrage konnte nicht gesendet werden.")
|
if (!response.isSuccessful) error("Anfrage konnte nicht gesendet werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun register(request: RegistrationRequest): Result<AuthMessageResponse> = runCatching {
|
suspend fun register(request: RegistrationRequest): Result<AuthMessageResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.register(request)
|
val response = api.register(request)
|
||||||
if (!response.isSuccessful) error("Registrierung fehlgeschlagen.")
|
if (!response.isSuccessful) error("Registrierung fehlgeschlagen.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun extractServerMessage(raw: String): String? {
|
private fun extractServerMessage(raw: String): String? {
|
||||||
if (raw.isBlank()) return null
|
if (raw.isBlank()) return null
|
||||||
|
|||||||
@@ -26,16 +26,20 @@ data class Mannschaft(
|
|||||||
@Singleton
|
@Singleton
|
||||||
class MannschaftenRepository @Inject constructor(private val api: ApiService) {
|
class MannschaftenRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun fetchMannschaften(season: String? = null): Result<List<Mannschaft>> = runCatching {
|
suspend fun fetchMannschaften(season: String? = null): Result<List<Mannschaft>> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.mannschaften(season)
|
val response = api.mannschaften(season)
|
||||||
if (!response.isSuccessful) error("Mannschaften konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Mannschaften konnten nicht geladen werden.")
|
||||||
parseCsv(response.body()?.string().orEmpty())
|
parseCsv(response.body()?.string().orEmpty())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchSeasons(): Result<MannschaftenSeasonsResponse> = runCatching {
|
suspend fun fetchSeasons(): Result<MannschaftenSeasonsResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.mannschaftenSeasons()
|
val response = api.mannschaftenSeasons()
|
||||||
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Saisons konnten nicht geladen werden.")
|
||||||
response.body() ?: error("Saisons konnten nicht geladen werden.")
|
response.body() ?: error("Saisons konnten nicht geladen werden.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseCsv(csv: String): List<Mannschaft> = csv.lineSequence()
|
private fun parseCsv(csv: String): List<Mannschaft> = csv.lineSequence()
|
||||||
.filter(String::isNotBlank)
|
.filter(String::isNotBlank)
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package de.harheimertc.repositories
|
||||||
|
|
||||||
|
import java.net.ConnectException
|
||||||
|
import java.net.NoRouteToHostException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.net.UnknownHostException
|
||||||
|
import javax.net.ssl.SSLException
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
internal suspend fun <T> retryOnNetworkFailure(
|
||||||
|
retryDelayMillis: Long = 10_000L,
|
||||||
|
block: suspend () -> T,
|
||||||
|
): T {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
return block()
|
||||||
|
} catch (error: Throwable) {
|
||||||
|
if (error is CancellationException) throw error
|
||||||
|
if (!error.isRetryableNetworkError()) throw error
|
||||||
|
delay(retryDelayMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Throwable.isRetryableNetworkError(): Boolean = when (this) {
|
||||||
|
is UnknownHostException,
|
||||||
|
is ConnectException,
|
||||||
|
is NoRouteToHostException,
|
||||||
|
is SocketTimeoutException,
|
||||||
|
is SSLException -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class NewsletterRepository @Inject constructor(private val api: ApiService) {
|
class NewsletterRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun groups(): Result<NewsletterGroupsResponse> = runCatching {
|
suspend fun groups(): Result<NewsletterGroupsResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.publicNewsletterGroups()
|
val response = api.publicNewsletterGroups()
|
||||||
if (!response.isSuccessful) error("Newsletter konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Newsletter konnten nicht geladen werden.")
|
||||||
response.body() ?: error("Leere Antwort vom Server.")
|
response.body() ?: error("Leere Antwort vom Server.")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun subscribe(groupId: String, email: String, name: String?): Result<AuthMessageResponse> = runCatching {
|
suspend fun subscribe(groupId: String, email: String, name: String?): Result<AuthMessageResponse> = runCatching {
|
||||||
val response = api.subscribeNewsletter(NewsletterSubscriptionRequest(groupId, email.trim(), name?.trim().orEmpty()))
|
val response = api.subscribeNewsletter(NewsletterSubscriptionRequest(groupId, email.trim(), name?.trim().orEmpty()))
|
||||||
@@ -26,8 +28,10 @@ class NewsletterRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun confirm(token: String): Result<AuthMessageResponse> = runCatching {
|
suspend fun confirm(token: String): Result<AuthMessageResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.confirmNewsletter(token)
|
val response = api.confirmNewsletter(token)
|
||||||
if (!response.isSuccessful) error("Newsletter-Bestätigung fehlgeschlagen.")
|
if (!response.isSuccessful) error("Newsletter-Bestätigung fehlgeschlagen.")
|
||||||
response.body() ?: AuthMessageResponse(success = true, message = "Newsletter-Anmeldung bestätigt.")
|
response.body() ?: AuthMessageResponse(success = true, message = "Newsletter-Anmeldung bestätigt.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PasskeyRepository @Inject constructor(
|
|||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
) {
|
) {
|
||||||
suspend fun login(context: Context, email: String?): Result<LoginResponse> = runCatching {
|
suspend fun login(context: Context, email: String?): Result<LoginResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val optionsResponse = api.passkeyAuthenticationOptions(
|
val optionsResponse = api.passkeyAuthenticationOptions(
|
||||||
PasskeyAuthenticationOptionsRequest(email = email?.trim()?.takeIf(String::isNotBlank)),
|
PasskeyAuthenticationOptionsRequest(email = email?.trim()?.takeIf(String::isNotBlank)),
|
||||||
)
|
)
|
||||||
@@ -59,15 +60,19 @@ class PasskeyRepository @Inject constructor(
|
|||||||
?: error("Der Server hat kein Zugriffstoken geliefert.")
|
?: error("Der Server hat kein Zugriffstoken geliefert.")
|
||||||
authRepository.setSession(token, body.refreshToken, body.sessionId)
|
authRepository.setSession(token, body.refreshToken, body.sessionId)
|
||||||
body
|
body
|
||||||
|
}
|
||||||
}.recoverCredentialCancellation("Passkey-Anmeldung abgebrochen.")
|
}.recoverCredentialCancellation("Passkey-Anmeldung abgebrochen.")
|
||||||
|
|
||||||
suspend fun list(): Result<PasskeysResponse> = runCatching {
|
suspend fun list(): Result<PasskeysResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.passkeys()
|
val response = api.passkeys()
|
||||||
if (!response.isSuccessful) error("Passkeys konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Passkeys konnten nicht geladen werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun add(context: Context, name: String = "Android-App"): Result<AuthMessageResponse> = runCatching {
|
suspend fun add(context: Context, name: String = "Android-App"): Result<AuthMessageResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val optionsResponse = api.passkeyRegistrationOptions(PasskeyRegistrationOptionsRequest())
|
val optionsResponse = api.passkeyRegistrationOptions(PasskeyRegistrationOptionsRequest())
|
||||||
if (!optionsResponse.isSuccessful) error("Passkey-Erstellung konnte nicht gestartet werden.")
|
if (!optionsResponse.isSuccessful) error("Passkey-Erstellung konnte nicht gestartet werden.")
|
||||||
val optionsJson = optionsResponse.body()?.string()?.extractJsonObject("options")
|
val optionsJson = optionsResponse.body()?.string()?.extractJsonObject("options")
|
||||||
@@ -90,6 +95,7 @@ class PasskeyRepository @Inject constructor(
|
|||||||
)
|
)
|
||||||
if (!response.isSuccessful) error("Passkey konnte nicht hinzugefügt werden.")
|
if (!response.isSuccessful) error("Passkey konnte nicht hinzugefügt werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
|
}
|
||||||
}.recoverCredentialCancellation("Passkey-Erstellung abgebrochen.")
|
}.recoverCredentialCancellation("Passkey-Erstellung abgebrochen.")
|
||||||
|
|
||||||
suspend fun remove(credentialId: String): Result<AuthMessageResponse> = runCatching {
|
suspend fun remove(credentialId: String): Result<AuthMessageResponse> = runCatching {
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class ProfileRepository @Inject constructor(private val api: ApiService) {
|
class ProfileRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun load(): Result<ProfileResponse> = runCatching {
|
suspend fun load(): Result<ProfileResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.profile()
|
val response = api.profile()
|
||||||
if (!response.isSuccessful) error("Profil konnte nicht geladen werden.")
|
if (!response.isSuccessful) error("Profil konnte nicht geladen werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun save(request: ProfileUpdateRequest): Result<ProfileResponse> = runCatching {
|
suspend fun save(request: ProfileUpdateRequest): Result<ProfileResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.updateProfile(request)
|
val response = api.updateProfile(request)
|
||||||
if (!response.isSuccessful) error("Profil konnte nicht gespeichert werden.")
|
if (!response.isSuccessful) error("Profil konnte nicht gespeichert werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,12 +31,15 @@ data class MeisterschaftResult(
|
|||||||
@Singleton
|
@Singleton
|
||||||
class PublicPagesRepository @Inject constructor(private val api: ApiService) {
|
class PublicPagesRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
|
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.config()
|
val response = api.config()
|
||||||
if (!response.isSuccessful) error("Inhalte konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Inhalte konnten nicht geladen werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchSpielsysteme(): Result<List<Spielsystem>> = runCatching {
|
suspend fun fetchSpielsysteme(): Result<List<Spielsystem>> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.spielsysteme()
|
val response = api.spielsysteme()
|
||||||
if (!response.isSuccessful) error("Spielsysteme konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Spielsysteme konnten nicht geladen werden.")
|
||||||
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
||||||
@@ -52,8 +55,10 @@ class PublicPagesRepository @Inject constructor(private val api: ApiService) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchVereinsmeisterschaften(): Result<List<MeisterschaftResult>> = runCatching {
|
suspend fun fetchVereinsmeisterschaften(): Result<List<MeisterschaftResult>> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.vereinsmeisterschaften()
|
val response = api.vereinsmeisterschaften()
|
||||||
if (!response.isSuccessful) error("Vereinsmeisterschaften konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Vereinsmeisterschaften konnten nicht geladen werden.")
|
||||||
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
parseCsv(response.body()?.string().orEmpty()).drop(1).mapNotNull { values ->
|
||||||
@@ -71,6 +76,7 @@ class PublicPagesRepository @Inject constructor(private val api: ApiService) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun parseCsv(csv: String): List<List<String>> =
|
private fun parseCsv(csv: String): List<List<String>> =
|
||||||
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
csv.lineSequence().filter(String::isNotBlank).map(::parseCsvLine).toList()
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class SpielplanRepository @Inject constructor(private val api: ApiService) {
|
class SpielplanRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun fetchSpielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
|
suspend fun fetchSpielplan(season: String? = null): Result<SpielplanResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.spielplan(season)
|
val response = api.spielplan(season)
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
val body = response.body() ?: error("Leere Antwort")
|
val body = response.body() ?: error("Leere Antwort")
|
||||||
if (!body.success) error(body.message ?: "Spielplan konnte nicht geladen werden.")
|
if (!body.success) error(body.message ?: "Spielplan konnte nicht geladen werden.")
|
||||||
body
|
body
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun fetchTeamTable(team: String, season: String? = null): Result<TeamTableResponse> = runCatching {
|
suspend fun fetchTeamTable(team: String, season: String? = null): Result<TeamTableResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.spielplanTable(team, season)
|
val response = api.spielplanTable(team, season)
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
val body = response.body() ?: error("Leere Antwort")
|
val body = response.body() ?: error("Leere Antwort")
|
||||||
@@ -24,3 +27,4 @@ class SpielplanRepository @Inject constructor(private val api: ApiService) {
|
|||||||
body
|
body
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class TermineRepository @Inject constructor(private val api: ApiService) {
|
class TermineRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun fetchTermine(): Result<List<TerminDto>> = runCatching {
|
suspend fun fetchTermine(): Result<List<TerminDto>> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.termine()
|
val response = api.termine()
|
||||||
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
if (!response.isSuccessful) error("HTTP ${response.code()}")
|
||||||
response.body()?.termine.orEmpty()
|
response.body()?.termine.orEmpty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class TrainingRepository @Inject constructor(private val api: ApiService) {
|
class TrainingRepository @Inject constructor(private val api: ApiService) {
|
||||||
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
|
suspend fun fetchConfig(): Result<ConfigResponse> = runCatching {
|
||||||
|
retryOnNetworkFailure {
|
||||||
val response = api.config()
|
val response = api.config()
|
||||||
if (!response.isSuccessful) error("Trainingsinformationen konnten nicht geladen werden.")
|
if (!response.isSuccessful) error("Trainingsinformationen konnten nicht geladen werden.")
|
||||||
response.body() ?: error("Leere Antwort")
|
response.body() ?: error("Leere Antwort")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,16 @@ package de.harheimertc.ui.navigation
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.NavHostController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -35,6 +41,16 @@ fun NavGraph(
|
|||||||
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
|
||||||
val persistentNavigation = maxWidth >= 600.dp
|
val persistentNavigation = maxWidth >= 600.dp
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
navigationState.connectionNote?.let { message ->
|
||||||
|
Surface(color = Color(0xFFFFF4E5), modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
color = Color(0xFF7C2D12),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (persistentNavigation) {
|
if (persistentNavigation) {
|
||||||
AppNavigationHeader(
|
AppNavigationHeader(
|
||||||
selectedRoute = currentRoute,
|
selectedRoute = currentRoute,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.harheimertc.ui.navigation
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import de.harheimertc.data.ConnectivityMonitor
|
||||||
import de.harheimertc.repositories.AuthRepository
|
import de.harheimertc.repositories.AuthRepository
|
||||||
import de.harheimertc.repositories.GalleryRepository
|
import de.harheimertc.repositories.GalleryRepository
|
||||||
import de.harheimertc.repositories.LoginRepository
|
import de.harheimertc.repositories.LoginRepository
|
||||||
@@ -19,6 +20,7 @@ data class NavigationUiState(
|
|||||||
val hasGalleryImages: Boolean = false,
|
val hasGalleryImages: Boolean = false,
|
||||||
val loggedIn: Boolean = false,
|
val loggedIn: Boolean = false,
|
||||||
val roles: Set<String> = emptySet(),
|
val roles: Set<String> = emptySet(),
|
||||||
|
val connectionNote: String? = null,
|
||||||
) {
|
) {
|
||||||
val isAdmin: Boolean get() = "admin" in roles
|
val isAdmin: Boolean get() = "admin" in roles
|
||||||
val canAccessNewsletter: Boolean get() = roles.any { it in setOf("admin", "vorstand", "newsletter") }
|
val canAccessNewsletter: Boolean get() = roles.any { it in setOf("admin", "vorstand", "newsletter") }
|
||||||
@@ -32,12 +34,22 @@ class NavigationViewModel @Inject constructor(
|
|||||||
private val galleryRepository: GalleryRepository,
|
private val galleryRepository: GalleryRepository,
|
||||||
private val loginRepository: LoginRepository,
|
private val loginRepository: LoginRepository,
|
||||||
private val authRepository: AuthRepository,
|
private val authRepository: AuthRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(NavigationUiState())
|
private val _state = MutableStateFlow(NavigationUiState())
|
||||||
val state: StateFlow<NavigationUiState> = _state
|
val state: StateFlow<NavigationUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadNavigationData()
|
loadNavigationData()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
connectionNote = if (online) null else "Keine Verbindung. Die App versucht alle 10 Sekunden erneut zu laden.",
|
||||||
|
)
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadNavigationData() {
|
fun loadNavigationData() {
|
||||||
@@ -52,6 +64,7 @@ class NavigationViewModel @Inject constructor(
|
|||||||
hasGalleryImages = gallery.await(),
|
hasGalleryImages = gallery.await(),
|
||||||
loggedIn = hasStoredSession || status.isLoggedIn,
|
loggedIn = hasStoredSession || status.isLoggedIn,
|
||||||
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
||||||
|
connectionNote = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +76,7 @@ class NavigationViewModel @Inject constructor(
|
|||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
loggedIn = hasStoredSession || status.isLoggedIn,
|
loggedIn = hasStoredSession || status.isLoggedIn,
|
||||||
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
||||||
|
connectionNote = _state.value.connectionNote,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +87,7 @@ class NavigationViewModel @Inject constructor(
|
|||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
loggedIn = false,
|
loggedIn = false,
|
||||||
roles = emptySet(),
|
roles = emptySet(),
|
||||||
|
connectionNote = _state.value.connectionNote,
|
||||||
)
|
)
|
||||||
onComplete()
|
onComplete()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.cms
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import de.harheimertc.data.ConnectivityMonitor
|
||||||
import de.harheimertc.data.CmsUserDto
|
import de.harheimertc.data.CmsUserDto
|
||||||
import de.harheimertc.data.ConfigResponse
|
import de.harheimertc.data.ConfigResponse
|
||||||
import de.harheimertc.data.ContactRequestDto
|
import de.harheimertc.data.ContactRequestDto
|
||||||
@@ -43,12 +44,22 @@ data class CmsUiState(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class CmsViewModel @Inject constructor(
|
class CmsViewModel @Inject constructor(
|
||||||
private val repository: CmsRepository,
|
private val repository: CmsRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(CmsUiState())
|
private val _state = MutableStateFlow(CmsUiState())
|
||||||
val state: StateFlow<CmsUiState> = _state
|
val state: StateFlow<CmsUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
load()
|
load()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
if (online && wasOnline == false) {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.memberarea
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import de.harheimertc.data.ConnectivityMonitor
|
||||||
import de.harheimertc.data.AuthStatusResponse
|
import de.harheimertc.data.AuthStatusResponse
|
||||||
import de.harheimertc.data.MemberDto
|
import de.harheimertc.data.MemberDto
|
||||||
import de.harheimertc.data.NewsDto
|
import de.harheimertc.data.NewsDto
|
||||||
@@ -24,12 +25,22 @@ data class MembersUiState(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MembersViewModel @Inject constructor(
|
class MembersViewModel @Inject constructor(
|
||||||
private val repository: MemberAreaRepository,
|
private val repository: MemberAreaRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(MembersUiState())
|
private val _state = MutableStateFlow(MembersUiState())
|
||||||
val state: StateFlow<MembersUiState> = _state
|
val state: StateFlow<MembersUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
load()
|
load()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
if (online && wasOnline == false) {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateQuery(query: String) {
|
fun updateQuery(query: String) {
|
||||||
@@ -87,12 +98,22 @@ data class MemberNewsUiState(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MemberNewsViewModel @Inject constructor(
|
class MemberNewsViewModel @Inject constructor(
|
||||||
private val repository: MemberAreaRepository,
|
private val repository: MemberAreaRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(MemberNewsUiState())
|
private val _state = MutableStateFlow(MemberNewsUiState())
|
||||||
val state: StateFlow<MemberNewsUiState> = _state
|
val state: StateFlow<MemberNewsUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
load()
|
load()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
if (online && wasOnline == false) {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
@@ -118,12 +139,22 @@ data class QttrUiState(
|
|||||||
class QttrViewModel @Inject constructor(
|
class QttrViewModel @Inject constructor(
|
||||||
private val repository: MemberAreaRepository,
|
private val repository: MemberAreaRepository,
|
||||||
private val loginRepository: LoginRepository,
|
private val loginRepository: LoginRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(QttrUiState())
|
private val _state = MutableStateFlow(QttrUiState())
|
||||||
val state: StateFlow<QttrUiState> = _state
|
val state: StateFlow<QttrUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
load()
|
load()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
if (online && wasOnline == false) {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.harheimertc.ui.screens.memberarea
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import de.harheimertc.data.ConnectivityMonitor
|
||||||
import de.harheimertc.data.BirthdayDto
|
import de.harheimertc.data.BirthdayDto
|
||||||
import de.harheimertc.repositories.MemberAreaRepository
|
import de.harheimertc.repositories.MemberAreaRepository
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
@@ -19,12 +20,22 @@ data class MemberAreaUiState(
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MemberAreaViewModel @Inject constructor(
|
class MemberAreaViewModel @Inject constructor(
|
||||||
private val repository: MemberAreaRepository,
|
private val repository: MemberAreaRepository,
|
||||||
|
private val connectivityMonitor: ConnectivityMonitor,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(MemberAreaUiState())
|
private val _state = MutableStateFlow(MemberAreaUiState())
|
||||||
val state: StateFlow<MemberAreaUiState> = _state
|
val state: StateFlow<MemberAreaUiState> = _state
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadBirthdays()
|
loadBirthdays()
|
||||||
|
viewModelScope.launch {
|
||||||
|
var wasOnline: Boolean? = null
|
||||||
|
connectivityMonitor.online.collect { online ->
|
||||||
|
if (online && wasOnline == false) {
|
||||||
|
loadBirthdays()
|
||||||
|
}
|
||||||
|
wasOnline = online
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadBirthdays() {
|
fun loadBirthdays() {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ LOCAL_API_BASE_URL=https://harheimertc.tsschulz.de/
|
|||||||
PRODUCTION_API_BASE_URL=https://harheimertc.de/
|
PRODUCTION_API_BASE_URL=https://harheimertc.de/
|
||||||
|
|
||||||
# Android app versioning for Play Store uploads
|
# Android app versioning for Play Store uploads
|
||||||
ANDROID_VERSION_CODE=20
|
ANDROID_VERSION_CODE=21
|
||||||
ANDROID_VERSION_NAME=0.9.15
|
ANDROID_VERSION_NAME=0.9.16
|
||||||
|
|
||||||
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
|
# Temporary hotfix: disable R8 minification for release to avoid Retrofit generic signature stripping.
|
||||||
RELEASE_MINIFY_ENABLED=false
|
RELEASE_MINIFY_ENABLED=false
|
||||||
|
|||||||
Reference in New Issue
Block a user