Add UI XML files for current and initial app layout
- Created `ui-current.xml` to represent the current state of the app's UI hierarchy. - Created `ui.xml` to represent the initial state of the app's UI hierarchy.
@@ -1,11 +1,13 @@
|
|||||||
package de.harheimertc.data
|
package de.harheimertc.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -14,6 +16,7 @@ class SecureOfflineCache @Inject constructor(
|
|||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val moshi: Moshi,
|
private val moshi: Moshi,
|
||||||
) {
|
) {
|
||||||
|
private val tag = "SecureOfflineCache"
|
||||||
private companion object {
|
private companion object {
|
||||||
const val KEY_BIRTHDAYS = "birthdays"
|
const val KEY_BIRTHDAYS = "birthdays"
|
||||||
const val KEY_QTTR_VALUES = "qttr_values"
|
const val KEY_QTTR_VALUES = "qttr_values"
|
||||||
@@ -29,6 +32,10 @@ class SecureOfflineCache @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
|
buildEncryptedPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEncryptedPreferences() = try {
|
||||||
val masterKey = MasterKey.Builder(context)
|
val masterKey = MasterKey.Builder(context)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
@@ -39,6 +46,28 @@ class SecureOfflineCache @Inject constructor(
|
|||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
} catch (error: GeneralSecurityException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
} catch (error: RuntimeException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recoverEncryptedPreferences(error: Throwable) = try {
|
||||||
|
Log.w(tag, "EncryptedSharedPreferences defekt, Offline-Cache wird neu angelegt", error)
|
||||||
|
context.deleteSharedPreferences("harheimertc_offline_cache")
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"harheimertc_offline_cache",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
|
)
|
||||||
|
} catch (retryError: Throwable) {
|
||||||
|
Log.e(tag, "Offline-Cache konnte nicht wiederhergestellt werden", retryError)
|
||||||
|
throw retryError
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putBirthdays(response: BirthdaysResponse) = put(KEY_BIRTHDAYS, response, BirthdaysResponse::class.java)
|
fun putBirthdays(response: BirthdaysResponse) = put(KEY_BIRTHDAYS, response, BirthdaysResponse::class.java)
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package de.harheimertc.repositories
|
package de.harheimertc.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import de.harheimertc.security.DeviceKeyManager
|
import de.harheimertc.security.DeviceKeyManager
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -13,10 +15,15 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val deviceKeyManager: DeviceKeyManager,
|
private val deviceKeyManager: DeviceKeyManager,
|
||||||
) : AuthRepository {
|
) : AuthRepository {
|
||||||
|
private val tag = "AuthRepository"
|
||||||
private val tokenKey = "auth_token"
|
private val tokenKey = "auth_token"
|
||||||
private val refreshTokenKey = "auth_refresh_token"
|
private val refreshTokenKey = "auth_refresh_token"
|
||||||
private val sessionIdKey = "auth_session_id"
|
private val sessionIdKey = "auth_session_id"
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
|
buildEncryptedPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEncryptedPreferences() = try {
|
||||||
val masterKey = MasterKey.Builder(context)
|
val masterKey = MasterKey.Builder(context)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
@@ -27,6 +34,28 @@ class AuthRepositoryImpl @Inject constructor(
|
|||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
} catch (error: GeneralSecurityException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
} catch (error: RuntimeException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recoverEncryptedPreferences(error: Throwable) = try {
|
||||||
|
Log.w(tag, "EncryptedSharedPreferences defekt, Session wird neu angelegt", error)
|
||||||
|
context.deleteSharedPreferences("harheimertc_auth")
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"harheimertc_auth",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
|
)
|
||||||
|
} catch (retryError: Throwable) {
|
||||||
|
Log.e(tag, "EncryptedSharedPreferences konnte nicht wiederhergestellt werden", retryError)
|
||||||
|
throw retryError
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getToken(): String? = preferences.getString(tokenKey, null)
|
override fun getToken(): String? = preferences.getString(tokenKey, null)
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package de.harheimertc.repositories
|
package de.harheimertc.repositories
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import androidx.security.crypto.MasterKey
|
import androidx.security.crypto.MasterKey
|
||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import com.squareup.moshi.Types
|
import com.squareup.moshi.Types
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import de.harheimertc.data.HomepageSectionDto
|
import de.harheimertc.data.HomepageSectionDto
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -15,10 +17,15 @@ class HomeLayoutPreferences @Inject constructor(
|
|||||||
@param:ApplicationContext private val context: Context,
|
@param:ApplicationContext private val context: Context,
|
||||||
private val moshi: Moshi,
|
private val moshi: Moshi,
|
||||||
) {
|
) {
|
||||||
|
private val tag = "HomeLayoutPreferences"
|
||||||
private val sectionListType = Types.newParameterizedType(List::class.java, HomepageSectionDto::class.java)
|
private val sectionListType = Types.newParameterizedType(List::class.java, HomepageSectionDto::class.java)
|
||||||
private val sectionListAdapter = moshi.adapter<List<HomepageSectionDto>>(sectionListType)
|
private val sectionListAdapter = moshi.adapter<List<HomepageSectionDto>>(sectionListType)
|
||||||
|
|
||||||
private val preferences by lazy {
|
private val preferences by lazy {
|
||||||
|
buildEncryptedPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildEncryptedPreferences() = try {
|
||||||
val masterKey = MasterKey.Builder(context)
|
val masterKey = MasterKey.Builder(context)
|
||||||
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
.build()
|
.build()
|
||||||
@@ -29,6 +36,28 @@ class HomeLayoutPreferences @Inject constructor(
|
|||||||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
)
|
)
|
||||||
|
} catch (error: GeneralSecurityException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
} catch (error: RuntimeException) {
|
||||||
|
recoverEncryptedPreferences(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun recoverEncryptedPreferences(error: Throwable) = try {
|
||||||
|
Log.w(tag, "EncryptedSharedPreferences defekt, Home-Layout wird neu angelegt", error)
|
||||||
|
context.deleteSharedPreferences("harheimertc_home_layout")
|
||||||
|
val masterKey = MasterKey.Builder(context)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
"harheimertc_home_layout",
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
|
||||||
|
)
|
||||||
|
} catch (retryError: Throwable) {
|
||||||
|
Log.e(tag, "Home-Layout-Preferences konnten nicht wiederhergestellt werden", retryError)
|
||||||
|
throw retryError
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSections(): List<HomepageSectionDto>? {
|
fun getSections(): List<HomepageSectionDto>? {
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.Brush
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.harheimertc.BuildConfig
|
import de.harheimertc.BuildConfig
|
||||||
import de.harheimertc.R
|
import de.harheimertc.R
|
||||||
@@ -42,14 +43,17 @@ private enum class MenuSection {
|
|||||||
TRAINING,
|
TRAINING,
|
||||||
NEWSLETTER,
|
NEWSLETTER,
|
||||||
INTERN,
|
INTERN,
|
||||||
|
CMS,
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class MenuTarget(val label: String, val route: String)
|
private data class MenuTarget(val label: String, val route: String)
|
||||||
|
private const val LOGOUT_ROUTE = "__logout__"
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppNavigationHeader(
|
fun AppNavigationHeader(
|
||||||
selectedRoute: String?,
|
selectedRoute: String?,
|
||||||
onNavigate: (String) -> Unit,
|
onNavigate: (String) -> Unit,
|
||||||
|
onLogout: () -> Unit = {},
|
||||||
webTabletNavigation: Boolean = false,
|
webTabletNavigation: Boolean = false,
|
||||||
navigationState: NavigationUiState = NavigationUiState(),
|
navigationState: NavigationUiState = NavigationUiState(),
|
||||||
) {
|
) {
|
||||||
@@ -61,9 +65,9 @@ fun AppNavigationHeader(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
if (webTabletNavigation) {
|
if (webTabletNavigation) {
|
||||||
WebTabletNavigation(selectedRoute, onNavigate, navigationState)
|
WebTabletNavigation(selectedRoute, onNavigate, onLogout, navigationState)
|
||||||
} else {
|
} else {
|
||||||
CompactNavigation(selectedRoute, onNavigate, navigationState)
|
CompactNavigation(selectedRoute, onNavigate, onLogout, navigationState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,24 +76,24 @@ fun AppNavigationHeader(
|
|||||||
private fun CompactNavigation(
|
private fun CompactNavigation(
|
||||||
selectedRoute: String?,
|
selectedRoute: String?,
|
||||||
onNavigate: (String) -> Unit,
|
onNavigate: (String) -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
navigationState: NavigationUiState = NavigationUiState(),
|
navigationState: NavigationUiState = NavigationUiState(),
|
||||||
) {
|
) {
|
||||||
val routeSection = menuSection(selectedRoute)
|
val routeSection = menuSection(selectedRoute)
|
||||||
val sectionOverride = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf<MenuSection?>(null) }
|
val sectionOverride = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf<MenuSection?>(null) }
|
||||||
val section = routeSection ?: sectionOverride.value
|
val section = routeSection ?: sectionOverride.value
|
||||||
val subItems = submenu(section, navigationState)
|
val subItems = submenu(section, navigationState)
|
||||||
var cmsExpanded = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) }
|
|
||||||
val navigateAndClose: (String) -> Unit = { route -> cmsExpanded.value = false; onNavigate(route) }
|
|
||||||
val mainScroll = rememberScrollState()
|
val mainScroll = rememberScrollState()
|
||||||
val subScroll = rememberScrollState()
|
val subScroll = rememberScrollState()
|
||||||
val cmsSubScroll = rememberScrollState()
|
|
||||||
|
|
||||||
BrandRow(onLogin = { onNavigate(Destinations.Login.route) })
|
BrandRow(
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
loggedIn = navigationState.loggedIn,
|
||||||
Row(
|
onLogin = { onNavigate(Destinations.Login.route) },
|
||||||
modifier = Modifier.horizontalScroll(mainScroll),
|
onOpenCms = { sectionOverride.value = MenuSection.CMS },
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
)
|
||||||
) {
|
|
||||||
|
ScrollableMenuRow(scrollState = mainScroll) {
|
||||||
CompactLink("Start", Destinations.Home.route, selectedRoute, onNavigate) { sectionOverride.value = null }
|
CompactLink("Start", Destinations.Home.route, selectedRoute, onNavigate) { sectionOverride.value = null }
|
||||||
CompactSectionLink("Verein", MenuSection.VEREIN, section) { sectionOverride.value = MenuSection.VEREIN }
|
CompactSectionLink("Verein", MenuSection.VEREIN, section) { sectionOverride.value = MenuSection.VEREIN }
|
||||||
CompactSectionLink("Mannschaften", MenuSection.MANNSCHAFTEN, section) { sectionOverride.value = MenuSection.MANNSCHAFTEN }
|
CompactSectionLink("Mannschaften", MenuSection.MANNSCHAFTEN, section) { sectionOverride.value = MenuSection.MANNSCHAFTEN }
|
||||||
@@ -102,79 +106,22 @@ private fun CompactNavigation(
|
|||||||
}
|
}
|
||||||
if (navigationState.loggedIn) {
|
if (navigationState.loggedIn) {
|
||||||
CompactSectionLink("Intern", MenuSection.INTERN, section) { sectionOverride.value = MenuSection.INTERN }
|
CompactSectionLink("Intern", MenuSection.INTERN, section) { sectionOverride.value = MenuSection.INTERN }
|
||||||
|
} else {
|
||||||
|
CompactLink("Login", Destinations.Login.route, selectedRoute, onNavigate) { sectionOverride.value = null }
|
||||||
}
|
}
|
||||||
CompactLink("Kontakt", Destinations.Contact.route, selectedRoute, onNavigate) { sectionOverride.value = null }
|
CompactLink("Kontakt", Destinations.Contact.route, selectedRoute, onNavigate) { sectionOverride.value = null }
|
||||||
if (navigationState.isAdmin || navigationState.canAccessNewsletter || navigationState.canAccessContactRequests) {
|
|
||||||
CompactSectionLink("CMS", MenuSection.INTERN, section) { sectionOverride.value = MenuSection.INTERN }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mainScroll.canScrollBackward) {
|
ScrollableMenuRow(scrollState = subScroll, topPadding = 3.dp) {
|
||||||
Text(
|
subItems.forEach { item ->
|
||||||
"◀",
|
|
||||||
color = Color(0xFFD4D4D8),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.background(Color(0x66000000), RoundedCornerShape(8.dp))
|
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (mainScroll.canScrollForward) {
|
|
||||||
Text(
|
|
||||||
"▶",
|
|
||||||
color = Color(0xFFD4D4D8),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterEnd)
|
|
||||||
.background(Color(0x66000000), RoundedCornerShape(8.dp))
|
|
||||||
.padding(horizontal = 4.dp, vertical = 2.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val cmsIndex = subItems.indexOfFirst { it.label == "CMS" }
|
|
||||||
val cmsChildren = if (cmsIndex >= 0) subItems.subList(cmsIndex + 1, subItems.size) else emptyList<MenuTarget>()
|
|
||||||
if (cmsChildren.any { it.route == selectedRoute }) {
|
|
||||||
cmsExpanded.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(subScroll)
|
|
||||||
.padding(top = 3.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
subItems.forEachIndexed { idx, item ->
|
|
||||||
if (idx == cmsIndex) {
|
|
||||||
SubLink(item.label, item.route == selectedRoute) {
|
SubLink(item.label, item.route == selectedRoute) {
|
||||||
cmsExpanded.value = !cmsExpanded.value
|
if (item.route == LOGOUT_ROUTE) {
|
||||||
}
|
onLogout()
|
||||||
} else if (idx > cmsIndex && cmsIndex >= 0) {
|
|
||||||
// CMS children are rendered below when expanded.
|
|
||||||
} else {
|
} else {
|
||||||
SubLink(item.label, item.route == selectedRoute) { navigateAndClose(item.route) }
|
onNavigate(item.route)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (subItems.isNotEmpty()) {
|
|
||||||
ScrollHintRow(subScroll)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cmsExpanded.value && cmsChildren.isNotEmpty()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(cmsSubScroll)
|
|
||||||
.padding(top = 6.dp, bottom = 3.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
cmsChildren.forEach { child ->
|
|
||||||
SubLink(child.label, child.route == selectedRoute) { onNavigate(child.route) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ScrollHintRow(cmsSubScroll)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,12 +153,13 @@ private fun CompactSectionLink(
|
|||||||
private fun WebTabletNavigation(
|
private fun WebTabletNavigation(
|
||||||
selectedRoute: String?,
|
selectedRoute: String?,
|
||||||
onNavigate: (String) -> Unit,
|
onNavigate: (String) -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
navigationState: NavigationUiState,
|
navigationState: NavigationUiState,
|
||||||
) {
|
) {
|
||||||
val section = menuSection(selectedRoute)
|
val sectionOverride = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf<MenuSection?>(null) }
|
||||||
var cmsExpanded = androidx.compose.runtime.remember { androidx.compose.runtime.mutableStateOf(false) }
|
val section = menuSection(selectedRoute) ?: sectionOverride.value
|
||||||
// Helper that closes the CMS submenu when navigating away
|
val subScroll = rememberScrollState()
|
||||||
val navigateAndClose: (String) -> Unit = { route -> cmsExpanded.value = false; onNavigate(route) }
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Brand()
|
Brand()
|
||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
@@ -220,75 +168,57 @@ private fun WebTabletNavigation(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
MainLink("Start", selectedRoute == Destinations.Home.route, onClick = { navigateAndClose(Destinations.Home.route) })
|
MainLink("Start", selectedRoute == Destinations.Home.route, onClick = { onNavigate(Destinations.Home.route) })
|
||||||
MainLink("Verein", section == MenuSection.VEREIN, onClick = { navigateAndClose(Destinations.VereinAbout.route) })
|
MainLink("Verein", section == MenuSection.VEREIN, onClick = { onNavigate(Destinations.VereinAbout.route) })
|
||||||
MainLink("Mannschaften", section == MenuSection.MANNSCHAFTEN, onClick = { navigateAndClose(Destinations.Mannschaften.route) })
|
MainLink("Mannschaften", section == MenuSection.MANNSCHAFTEN, onClick = { onNavigate(Destinations.Mannschaften.route) })
|
||||||
MainLink("Training", section == MenuSection.TRAINING, onClick = { navigateAndClose(Destinations.Training.route) })
|
MainLink("Training", section == MenuSection.TRAINING, onClick = { onNavigate(Destinations.Training.route) })
|
||||||
MainLink("Termine", selectedRoute == Destinations.Termine.route, onClick = { navigateAndClose(Destinations.Termine.route) })
|
MainLink("Termine", selectedRoute == Destinations.Termine.route, onClick = { onNavigate(Destinations.Termine.route) })
|
||||||
if (navigationState.showGallery) {
|
if (navigationState.showGallery) {
|
||||||
MainLink("Galerie", selectedRoute == Destinations.Gallery.route, onClick = { onNavigate(Destinations.Gallery.route) })
|
MainLink("Galerie", selectedRoute == Destinations.Gallery.route, onClick = { onNavigate(Destinations.Gallery.route) })
|
||||||
}
|
}
|
||||||
MainLink("Newsletter", section == MenuSection.NEWSLETTER, onClick = { onNavigate(Destinations.NewsletterSubscribe.route) })
|
MainLink("Newsletter", section == MenuSection.NEWSLETTER, onClick = { onNavigate(Destinations.NewsletterSubscribe.route) })
|
||||||
if (navigationState.loggedIn) {
|
if (navigationState.loggedIn) {
|
||||||
MainLink("Intern", section == MenuSection.INTERN, onClick = { onNavigate(Destinations.MemberArea.route) })
|
MainLink("Intern", section == MenuSection.INTERN, onClick = { sectionOverride.value = MenuSection.INTERN })
|
||||||
}
|
}
|
||||||
MainLink("Kontakt", selectedRoute == Destinations.Contact.route, primary = true, onClick = { navigateAndClose(Destinations.Contact.route) })
|
MainLink("Kontakt", selectedRoute == Destinations.Contact.route, primary = true, onClick = { onNavigate(Destinations.Contact.route) })
|
||||||
TextButton(onClick = { navigateAndClose(Destinations.Login.route) }) { Text("Login", color = Color.White) }
|
|
||||||
}
|
}
|
||||||
}
|
Spacer(Modifier.width(12.dp))
|
||||||
val subItems = submenu(section, navigationState)
|
if (navigationState.loggedIn) {
|
||||||
// determine CMS parent index and children
|
TextButton(onClick = { sectionOverride.value = MenuSection.CMS }) { Text("CMS", color = Color.White) }
|
||||||
val cmsIndex = subItems.indexOfFirst { it.label == "CMS" }
|
|
||||||
val cmsChildren = if (cmsIndex >= 0) subItems.subList(cmsIndex + 1, subItems.size) else emptyList<MenuTarget>()
|
|
||||||
if (cmsChildren.any { it.route == selectedRoute }) {
|
|
||||||
cmsExpanded.value = true
|
|
||||||
}
|
|
||||||
// First row: render all subitems but do NOT render CMS children here
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState())
|
|
||||||
.padding(top = 3.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
subItems.forEachIndexed { idx, item ->
|
|
||||||
if (idx == cmsIndex) {
|
|
||||||
// CMS parent toggle
|
|
||||||
SubLink(item.label, item.route == selectedRoute) {
|
|
||||||
cmsExpanded.value = !cmsExpanded.value
|
|
||||||
}
|
|
||||||
} else if (idx > cmsIndex && cmsIndex >= 0) {
|
|
||||||
// skip cms children here; they'll be rendered in the second row when expanded
|
|
||||||
} else {
|
} else {
|
||||||
// normal item before CMS: close cms submenu on navigate
|
TextButton(onClick = { onNavigate(Destinations.Login.route) }) { Text("Login", color = Color.White) }
|
||||||
SubLink(item.label, item.route == selectedRoute) { navigateAndClose(item.route) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second row: when CMS expanded, render its children beneath
|
val subItems = submenu(section, navigationState)
|
||||||
if (cmsExpanded.value && cmsChildren.isNotEmpty()) {
|
ScrollableMenuRow(scrollState = subScroll, topPadding = 3.dp) {
|
||||||
Row(
|
subItems.forEach { item ->
|
||||||
modifier = Modifier
|
SubLink(item.label, item.route == selectedRoute) {
|
||||||
.fillMaxWidth()
|
if (item.route == LOGOUT_ROUTE) {
|
||||||
.horizontalScroll(rememberScrollState())
|
onLogout()
|
||||||
.padding(top = 6.dp, bottom = 3.dp),
|
} else {
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
onNavigate(item.route)
|
||||||
) {
|
}
|
||||||
cmsChildren.forEach { child ->
|
|
||||||
SubLink(child.label, child.route == selectedRoute) { onNavigate(child.route) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BrandRow(onLogin: () -> Unit) {
|
private fun BrandRow(
|
||||||
|
loggedIn: Boolean,
|
||||||
|
onLogin: () -> Unit,
|
||||||
|
onOpenCms: () -> Unit,
|
||||||
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Brand()
|
Brand()
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
if (loggedIn) {
|
||||||
|
TextButton(onClick = onOpenCms) { Text("CMS", color = Color.White) }
|
||||||
|
} else {
|
||||||
TextButton(onClick = onLogin) { Text("Login", color = Color.White) }
|
TextButton(onClick = onLogin) { Text("Login", color = Color.White) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -367,25 +297,36 @@ private fun CompactLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ScrollHintRow(scrollState: ScrollState) {
|
private fun ScrollableMenuRow(
|
||||||
if (!scrollState.canScrollBackward && !scrollState.canScrollForward) return
|
scrollState: ScrollState,
|
||||||
|
topPadding: Dp = 0.dp,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(top = 2.dp),
|
.padding(top = topPadding),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (scrollState.canScrollBackward) "◀" else "",
|
if (scrollState.canScrollBackward) "◀" else "",
|
||||||
color = Color(0xFFD4D4D8),
|
color = Color(0xFFD4D4D8),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.width(14.dp),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.horizontalScroll(scrollState),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
if (scrollState.canScrollForward) "▶" else "",
|
if (scrollState.canScrollForward) "▶" else "",
|
||||||
color = Color(0xFFD4D4D8),
|
color = Color(0xFFD4D4D8),
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.width(14.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,12 +372,14 @@ private fun menuSection(route: String?): MenuSection? = when (route) {
|
|||||||
Destinations.NewsletterConfirm.route,
|
Destinations.NewsletterConfirm.route,
|
||||||
Destinations.NewsletterConfirmed.route,
|
Destinations.NewsletterConfirmed.route,
|
||||||
Destinations.NewsletterUnsubscribed.route -> MenuSection.NEWSLETTER
|
Destinations.NewsletterUnsubscribed.route -> MenuSection.NEWSLETTER
|
||||||
|
|
||||||
Destinations.MemberArea.route,
|
Destinations.MemberArea.route,
|
||||||
Destinations.Members.route,
|
Destinations.Members.route,
|
||||||
Destinations.Qttr.route,
|
Destinations.Qttr.route,
|
||||||
Destinations.MemberNews.route,
|
Destinations.MemberNews.route,
|
||||||
Destinations.Profile.route,
|
Destinations.Profile.route,
|
||||||
Destinations.MemberApi.route,
|
Destinations.MemberApi.route -> MenuSection.INTERN
|
||||||
|
|
||||||
Destinations.CmsStartseite.route,
|
Destinations.CmsStartseite.route,
|
||||||
Destinations.CmsInhalte.route,
|
Destinations.CmsInhalte.route,
|
||||||
Destinations.CmsVereinsmeisterschaften.route,
|
Destinations.CmsVereinsmeisterschaften.route,
|
||||||
@@ -447,7 +390,8 @@ private fun menuSection(route: String?): MenuSection? = when (route) {
|
|||||||
Destinations.CmsEinstellungen.route,
|
Destinations.CmsEinstellungen.route,
|
||||||
Destinations.CmsBenutzer.route,
|
Destinations.CmsBenutzer.route,
|
||||||
Destinations.CmsPasswordResetDiagnostics.route,
|
Destinations.CmsPasswordResetDiagnostics.route,
|
||||||
Destinations.Cms.route -> MenuSection.INTERN
|
Destinations.Cms.route -> MenuSection.CMS
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}.let { section ->
|
}.let { section ->
|
||||||
if (section == null && route?.startsWith("mannschaften/") == true) MenuSection.MANNSCHAFTEN else section
|
if (section == null && route?.startsWith("mannschaften/") == true) MenuSection.MANNSCHAFTEN else section
|
||||||
@@ -464,23 +408,27 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
|||||||
MenuTarget("Links", Destinations.Links.route),
|
MenuTarget("Links", Destinations.Links.route),
|
||||||
MenuTarget("Impressum", Destinations.Impressum.route),
|
MenuTarget("Impressum", Destinations.Impressum.route),
|
||||||
)
|
)
|
||||||
|
|
||||||
MenuSection.MANNSCHAFTEN -> listOf(
|
MenuSection.MANNSCHAFTEN -> listOf(
|
||||||
MenuTarget("Übersicht", Destinations.Mannschaften.route),
|
MenuTarget("Übersicht", Destinations.Mannschaften.route),
|
||||||
) + state.teams.map { MenuTarget(it.mannschaft, Destinations.MannschaftDetail.create(it.slug)) } + listOf(
|
) + state.teams.map { MenuTarget(it.mannschaft, Destinations.MannschaftDetail.create(it.slug)) } + listOf(
|
||||||
MenuTarget("Spielpläne", Destinations.Spielplan.route),
|
MenuTarget("Spielpläne", Destinations.Spielplan.route),
|
||||||
MenuTarget("Spielsysteme", Destinations.Spielsysteme.route),
|
MenuTarget("Spielsysteme", Destinations.Spielsysteme.route),
|
||||||
)
|
)
|
||||||
|
|
||||||
MenuSection.TRAINING -> listOf(
|
MenuSection.TRAINING -> listOf(
|
||||||
MenuTarget("Trainingszeiten", Destinations.Training.route),
|
MenuTarget("Trainingszeiten", Destinations.Training.route),
|
||||||
MenuTarget("Trainer", Destinations.Trainer.route),
|
MenuTarget("Trainer", Destinations.Trainer.route),
|
||||||
MenuTarget("Anfänger", Destinations.Anfaenger.route),
|
MenuTarget("Anfänger", Destinations.Anfaenger.route),
|
||||||
MenuTarget("TT-Regeln", Destinations.Regeln.route),
|
MenuTarget("TT-Regeln", Destinations.Regeln.route),
|
||||||
)
|
)
|
||||||
|
|
||||||
MenuSection.NEWSLETTER -> listOf(
|
MenuSection.NEWSLETTER -> listOf(
|
||||||
MenuTarget("Abonnieren", Destinations.NewsletterSubscribe.route),
|
MenuTarget("Abonnieren", Destinations.NewsletterSubscribe.route),
|
||||||
MenuTarget("Abmelden", Destinations.NewsletterUnsubscribe.route),
|
MenuTarget("Abmelden", Destinations.NewsletterUnsubscribe.route),
|
||||||
MenuTarget("Bestätigt", Destinations.NewsletterConfirmed.route),
|
MenuTarget("Bestätigt", Destinations.NewsletterConfirmed.route),
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
||||||
@@ -488,19 +436,23 @@ private fun submenu(section: MenuSection?, state: NavigationUiState): List<MenuT
|
|||||||
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("API-Dokumentation", Destinations.MemberApi.route))
|
add(MenuTarget("API-Dokumentation", Destinations.MemberApi.route))
|
||||||
if (state.isAdmin || state.canAccessContactRequests || state.canAccessNewsletter) add(MenuTarget("CMS", Destinations.Cms.route))
|
|
||||||
// CMS child items (will be rendered when CMS parent is expanded)
|
|
||||||
if (state.isAdmin || state.canAccessContactRequests || state.canAccessNewsletter) add(MenuTarget("Übersicht", Destinations.Cms.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Startseite", Destinations.CmsStartseite.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Sportbetrieb", Destinations.CmsSportbetrieb.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
|
|
||||||
if (state.canAccessNewsletter) add(MenuTarget("Newsletter", Destinations.CmsNewsletter.route))
|
if (state.canAccessNewsletter) add(MenuTarget("Newsletter", Destinations.CmsNewsletter.route))
|
||||||
if (state.canAccessContactRequests) add(MenuTarget("Kontaktanfragen", Destinations.CmsContactRequests.route))
|
if (state.canAccessContactRequests) add(MenuTarget("Kontaktanfragen", Destinations.CmsContactRequests.route))
|
||||||
if (state.isAdmin) add(MenuTarget("Einstellungen", Destinations.CmsEinstellungen.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Benutzer", Destinations.CmsBenutzer.route))
|
|
||||||
if (state.isAdmin) add(MenuTarget("Reset-Diagnose", Destinations.CmsPasswordResetDiagnostics.route))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MenuSection.CMS -> buildList {
|
||||||
|
add(MenuTarget("Übersicht", Destinations.Cms.route))
|
||||||
|
add(MenuTarget("Startseite", Destinations.CmsStartseite.route))
|
||||||
|
add(MenuTarget("Inhalte", Destinations.CmsInhalte.route))
|
||||||
|
add(MenuTarget("Vereinsmeisterschaften", Destinations.CmsVereinsmeisterschaften.route))
|
||||||
|
add(MenuTarget("News", Destinations.MemberNews.route))
|
||||||
|
add(MenuTarget("Sportbetrieb", Destinations.CmsSportbetrieb.route))
|
||||||
|
add(MenuTarget("Mitgliederverwaltung", Destinations.CmsMitgliederverwaltung.route))
|
||||||
|
add(MenuTarget("Kontaktanfragen", Destinations.CmsContactRequests.route))
|
||||||
|
add(MenuTarget("Einstellungen", Destinations.CmsEinstellungen.route))
|
||||||
|
add(MenuTarget("Benutzerverwaltung", Destinations.CmsBenutzer.route))
|
||||||
|
add(MenuTarget("Ausloggen", LOGOUT_ROUTE))
|
||||||
|
}
|
||||||
|
|
||||||
null -> emptyList()
|
null -> emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ fun NavGraph(
|
|||||||
AppNavigationHeader(
|
AppNavigationHeader(
|
||||||
selectedRoute = currentRoute,
|
selectedRoute = currentRoute,
|
||||||
onNavigate = navController::navigateTopLevel,
|
onNavigate = navController::navigateTopLevel,
|
||||||
|
onLogout = {
|
||||||
|
navigationViewModel.logout {
|
||||||
|
navController.navigate(Destinations.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
webTabletNavigation = true,
|
webTabletNavigation = true,
|
||||||
navigationState = navigationState,
|
navigationState = navigationState,
|
||||||
)
|
)
|
||||||
@@ -52,6 +59,8 @@ fun NavGraph(
|
|||||||
de.harheimertc.ui.screens.home.HomeScreen(
|
de.harheimertc.ui.screens.home.HomeScreen(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
showNavigationHeader = !persistentNavigation,
|
showNavigationHeader = !persistentNavigation,
|
||||||
|
navigationViewModel = navigationViewModel,
|
||||||
|
viewModel = hiltViewModel(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Destinations.VereinAbout.route) {
|
composable(Destinations.VereinAbout.route) {
|
||||||
|
|||||||
@@ -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.repositories.AuthRepository
|
||||||
import de.harheimertc.repositories.GalleryRepository
|
import de.harheimertc.repositories.GalleryRepository
|
||||||
import de.harheimertc.repositories.LoginRepository
|
import de.harheimertc.repositories.LoginRepository
|
||||||
import de.harheimertc.repositories.Mannschaft
|
import de.harheimertc.repositories.Mannschaft
|
||||||
@@ -30,6 +31,7 @@ class NavigationViewModel @Inject constructor(
|
|||||||
private val mannschaftenRepository: MannschaftenRepository,
|
private val mannschaftenRepository: MannschaftenRepository,
|
||||||
private val galleryRepository: GalleryRepository,
|
private val galleryRepository: GalleryRepository,
|
||||||
private val loginRepository: LoginRepository,
|
private val loginRepository: LoginRepository,
|
||||||
|
private val authRepository: AuthRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(NavigationUiState())
|
private val _state = MutableStateFlow(NavigationUiState())
|
||||||
val state: StateFlow<NavigationUiState> = _state
|
val state: StateFlow<NavigationUiState> = _state
|
||||||
@@ -44,10 +46,11 @@ class NavigationViewModel @Inject constructor(
|
|||||||
val gallery = async { galleryRepository.hasPublicImages().getOrDefault(false) }
|
val gallery = async { galleryRepository.hasPublicImages().getOrDefault(false) }
|
||||||
val auth = async { loginRepository.status().getOrDefault(de.harheimertc.data.AuthStatusResponse()) }
|
val auth = async { loginRepository.status().getOrDefault(de.harheimertc.data.AuthStatusResponse()) }
|
||||||
val status = auth.await()
|
val status = auth.await()
|
||||||
|
val hasStoredSession = !authRepository.getToken().isNullOrBlank()
|
||||||
_state.value = NavigationUiState(
|
_state.value = NavigationUiState(
|
||||||
teams = teams.await(),
|
teams = teams.await(),
|
||||||
hasGalleryImages = gallery.await(),
|
hasGalleryImages = gallery.await(),
|
||||||
loggedIn = status.isLoggedIn,
|
loggedIn = hasStoredSession || status.isLoggedIn,
|
||||||
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -56,10 +59,22 @@ class NavigationViewModel @Inject constructor(
|
|||||||
fun refreshSession() {
|
fun refreshSession() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val status = loginRepository.status().getOrDefault(de.harheimertc.data.AuthStatusResponse())
|
val status = loginRepository.status().getOrDefault(de.harheimertc.data.AuthStatusResponse())
|
||||||
|
val hasStoredSession = !authRepository.getToken().isNullOrBlank()
|
||||||
_state.value = _state.value.copy(
|
_state.value = _state.value.copy(
|
||||||
loggedIn = status.isLoggedIn,
|
loggedIn = hasStoredSession || status.isLoggedIn,
|
||||||
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
roles = (status.roles + status.user?.roles.orEmpty()).toSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun logout(onComplete: () -> Unit = {}) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loginRepository.logout()
|
||||||
|
_state.value = _state.value.copy(
|
||||||
|
loggedIn = false,
|
||||||
|
roles = emptySet(),
|
||||||
|
)
|
||||||
|
onComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import de.harheimertc.ui.navigation.NavigationViewModel
|
import de.harheimertc.ui.navigation.NavigationViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
@@ -77,9 +76,9 @@ import java.util.Locale
|
|||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
showNavigationHeader: Boolean = true,
|
showNavigationHeader: Boolean = true,
|
||||||
viewModel: HomeViewModel = hiltViewModel(),
|
navigationViewModel: NavigationViewModel,
|
||||||
|
viewModel: HomeViewModel,
|
||||||
) {
|
) {
|
||||||
val navigationViewModel: NavigationViewModel = hiltViewModel()
|
|
||||||
val navigationState by navigationViewModel.state.collectAsState()
|
val navigationState by navigationViewModel.state.collectAsState()
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
var selectedNews by remember { mutableStateOf<NewsDto?>(null) }
|
var selectedNews by remember { mutableStateOf<NewsDto?>(null) }
|
||||||
@@ -107,6 +106,13 @@ fun HomeScreen(
|
|||||||
AppNavigationHeader(
|
AppNavigationHeader(
|
||||||
selectedRoute = Destinations.Home.route,
|
selectedRoute = Destinations.Home.route,
|
||||||
onNavigate = navController::navigate,
|
onNavigate = navController::navigate,
|
||||||
|
onLogout = {
|
||||||
|
navigationViewModel.logout {
|
||||||
|
navController.navigate(Destinations.Home.route) {
|
||||||
|
launchSingleTop = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
navigationState = navigationState,
|
navigationState = navigationState,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
25
plugins/auth-sync.client.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const syncAuthState = async () => {
|
||||||
|
await authStore.checkAuth()
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxtApp.hook('app:mounted', () => {
|
||||||
|
syncAuthState()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.addEventListener('pageshow', (event) => {
|
||||||
|
if (event.persisted) {
|
||||||
|
syncAuthState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
syncAuthState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
BIN
temp/device-35433-after-intern-fix.png
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
temp/device-35433-cms-after-split-2.png
Normal file
|
After Width: | Height: | Size: 911 KiB |
BIN
temp/device-35433-cms-after-split.png
Normal file
|
After Width: | Height: | Size: 911 KiB |
BIN
temp/device-35433-cms-open.png
Normal file
|
After Width: | Height: | Size: 873 KiB |
BIN
temp/device-35433-home.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
temp/device-35433-intern-after-split.png
Normal file
|
After Width: | Height: | Size: 907 KiB |
0
temp/device-37165-arrow-check.png
Normal file
BIN
temp/device-38281-after-fix.png
Normal file
|
After Width: | Height: | Size: 899 KiB |
BIN
temp/device-38281-cms-2.png
Normal file
|
After Width: | Height: | Size: 899 KiB |
BIN
temp/device-38281-cms-open.png
Normal file
|
After Width: | Height: | Size: 944 KiB |
BIN
temp/device-38281-cms.png
Normal file
|
After Width: | Height: | Size: 899 KiB |
BIN
temp/device-38281.png
Normal file
|
After Width: | Height: | Size: 892 KiB |
BIN
temp/device-43477-arrow-check.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
temp/device-43477-intern-inline.png
Normal file
|
After Width: | Height: | Size: 936 KiB |