refactor(settings): drill-down navigation replacing tabbed layout (#605)
* refactor(settings): drill-down navigation replacing tabbed layout Replaces the 4-tab settings layout with a native Android-style drill-down: a single category list (SettingsScreen) where each row pushes a dedicated sub-page onto the Navigation3 back stack (SettingsConnection / Appearance / Chat / NavBar / Behavior / About). Each sub-page is its own NavKey destination registered in Navigation.kt and reuses the existing section composables. - SettingsScreen becomes the category list with icon + label + summary (Appearance row shows live theme + language, e.g. "Dark · 한국어"). - New SettingsSubPages.kt hosts the 6 full-screen sub-pages. - AboutSection promoted out of SettingsScreen into its own sub-page. - SettingsViewModel remains the single source of truth; no persistence changes. - Adds settings_summary_* strings for list subtitles. Consumes the in-app language switcher from the prior commit (AppearancePage passes appLanguage through to AppearanceSection). * [bug] fix settings sub-page back button opening drawer on first tap The ModalNavigationDrawer wraps the whole app and stays gesture-enabled on the Settings root (a DRAWER_GESTURE_SCREEN). Drilling into a sub-page (SettingsConnection/Appearance/Chat/NavBar/Behavior/About) disables gestures, but the existing LaunchedEffect only snapped the drawer closed when it was already isOpen. The drawer's scrim surface lingered during the transition and intercepted the first back-button tap, sliding the sidebar open instead of navigating back. Now snapTo(Closed) unconditionally whenever gestures are disabled — a safe no-op when already closed — so the sub-page back button works on the first tap. * [bug] fix settings sub-page back button opening drawer (gesture-surface race) The first "back" tap on a settings sub-page opened the sidebar instead of navigating back. Root cause: Settings root is a gesture-enabled drawer screen, and the sub-pages are the first destinations ever drilled down from it. When navigating in, ModalNavigationDrawer's gesture interceptor surface lingered for a frame (gesture -> non-gesture transition), so the top-left tap was consumed by the drawer instead of the Back IconButton. The previous snapTo fix only reset drawer *state*, not the gesture layer, so it didn't help. Fix: NavigationController now holds a closeDrawer hook (set by MainNavigation) and invokes it synchronously inside navigateTo() when pushing a non-primary sub-page. The drawer is dismissed in the same tap handler that triggers the navigation, before any recomposition, so the sub-page is never tappable while the gesture surface is still mounted. The LaunchedEffect(gesturesEnabled) snapTo remains as a backstop for non-navigateTo entries. * fix(#603): delay drawer gesture activation to prevent residual swipe * fix(#603): fix settings back navigation and drawer auto-open --------- Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
b3765bd106
commit
3e357594be
7 files changed
+501
-201
No files matched your search
@@ -33,8 +33,10 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -42,6 +44,7 @@ import androidx.compose.ui.platform.testTag
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.navigation3.runtime.NavBackStack
|
import androidx.navigation3.runtime.NavBackStack
|
||||||
import androidx.navigation3.runtime.NavKey
|
import androidx.navigation3.runtime.NavKey
|
||||||
import androidx.navigation3.runtime.entryProvider
|
import androidx.navigation3.runtime.entryProvider
|
||||||
@@ -51,6 +54,13 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
|
|||||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||||
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
||||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsAboutPage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsAppearancePage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsBehaviorPage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsChatPage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsConnectionPage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsNavBarPage
|
||||||
|
import com.m57.hermescontrol.ui.settings.SettingsViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
import com.m57.hermescontrol.ui.authlogin.AuthLoginScreen as AuthLoginScreenContent
|
||||||
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
import com.m57.hermescontrol.ui.landing.LandingScreen as LandingScreenContent
|
||||||
@@ -104,6 +114,46 @@ private fun appEntryProvider(
|
|||||||
screen.content(sessionId, openDrawer)
|
screen.content(sessionId, openDrawer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Settings drill-down sub-pages ───────────────────────────────────
|
||||||
|
entry<SettingsConnection> {
|
||||||
|
SettingsConnectionPage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
onLogout = { /* handled by caller via goBack fallback */ },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<SettingsAppearance> {
|
||||||
|
SettingsAppearancePage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<SettingsChat> {
|
||||||
|
SettingsChatPage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<SettingsNavBar> {
|
||||||
|
SettingsNavBarPage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<SettingsBehavior> {
|
||||||
|
SettingsBehaviorPage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<SettingsAbout> {
|
||||||
|
SettingsAboutPage(
|
||||||
|
onBack = { NavigationController.goBack() },
|
||||||
|
onLogout = { /* handled by caller via goBack fallback */ },
|
||||||
|
viewModel = viewModel { SettingsViewModel() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -119,6 +169,10 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
// Synchronous drawer dismiss hook used by NavigationController.navigateTo
|
||||||
|
// when drilling into a non-gesture sub-page (see NavigationController.closeDrawer).
|
||||||
|
NavigationController.closeDrawer = { scope.launch { drawerState.close() } }
|
||||||
|
|
||||||
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
|
val bottomNavItemsState by AuthManager.bottomNavItemsFlow.collectAsState()
|
||||||
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
|
val bottomNavDisplayMode by AuthManager.bottomNavDisplayModeFlow.collectAsState()
|
||||||
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
|
val bottomNavItems = resolveBottomNavItems(bottomNavItemsState)
|
||||||
@@ -133,20 +187,35 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
currentScreen != AuthLoginScreen &&
|
currentScreen != AuthLoginScreen &&
|
||||||
currentScreen != PairingCodeEntryScreen
|
currentScreen != PairingCodeEntryScreen
|
||||||
val gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS
|
val gesturesEnabled = currentScreen in DRAWER_GESTURE_SCREENS
|
||||||
|
var drawerGesturesEnabled by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(gesturesEnabled) {
|
||||||
|
if (gesturesEnabled) {
|
||||||
|
// Delay enabling gestures slightly to prevent residual swipe-to-open from back button taps
|
||||||
|
kotlinx.coroutines.delay(200)
|
||||||
|
drawerGesturesEnabled = true
|
||||||
|
} else {
|
||||||
|
drawerGesturesEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
|
val openDrawer: () -> Unit = { scope.launch { drawerState.open() } }
|
||||||
|
|
||||||
// B7 (Jun 30 2026, kanban t_424): close drawer if gestures are disabled to dismiss scrim
|
// B7 (Jun 30 2026, kanban t_424): close drawer if gestures are disabled to dismiss scrim.
|
||||||
LaunchedEffect(gesturesEnabled) {
|
// Also force-close on transition into a non-gesture screen so the drawer's scrim
|
||||||
if (!gesturesEnabled && drawerState.isOpen) {
|
// surface cannot intercept the first back-button tap (e.g. settings sub-pages drilled
|
||||||
|
// down from the gesture-enabled Settings root). snapTo(Closed) is a safe no-op when
|
||||||
|
// already closed.
|
||||||
|
LaunchedEffect(drawerGesturesEnabled) {
|
||||||
|
if (!drawerGesturesEnabled) {
|
||||||
drawerState.snapTo(DrawerValue.Closed)
|
drawerState.snapTo(DrawerValue.Closed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
gesturesEnabled = gesturesEnabled,
|
gesturesEnabled = drawerGesturesEnabled,
|
||||||
drawerContent = {
|
drawerContent = {
|
||||||
if (gesturesEnabled) {
|
|
||||||
ModalDrawerSheet(
|
ModalDrawerSheet(
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
) {
|
) {
|
||||||
@@ -231,7 +300,6 @@ fun MainNavigation(sessionId: String? = null) {
|
|||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ object NavigationController {
|
|||||||
var backStack: NavBackStack<NavKey>? = null
|
var backStack: NavBackStack<NavKey>? = null
|
||||||
var pendingSessionId: String? = null
|
var pendingSessionId: String? = null
|
||||||
|
|
||||||
|
// Set by MainNavigation so navigation can synchronously dismiss the drawer
|
||||||
|
// (e.g. when drilling into a non-gesture sub-page) before a recomposition,
|
||||||
|
// preventing the drawer's gesture surface from intercepting the first tap.
|
||||||
|
var closeDrawer: (() -> Unit)? = null
|
||||||
|
|
||||||
// Bottom-nav primary screens — dynamic, updated by Navigation.kt via
|
// Bottom-nav primary screens — dynamic, updated by Navigation.kt via
|
||||||
// updatePrimaryScreens() when the user customises the bottom nav bar.
|
// updatePrimaryScreens() when the user customises the bottom nav bar.
|
||||||
// Default matches the default 5 bottom-nav items.
|
// Default matches the default 5 bottom-nav items.
|
||||||
@@ -46,6 +51,10 @@ object NavigationController {
|
|||||||
|
|
||||||
if (isPrimaryScreen(key)) {
|
if (isPrimaryScreen(key)) {
|
||||||
stack.clear()
|
stack.clear()
|
||||||
|
} else {
|
||||||
|
// Drilling into a sub-page (non-primary): dismiss the drawer
|
||||||
|
// synchronously so its gesture surface can't eat the next tap.
|
||||||
|
closeDrawer?.invoke()
|
||||||
}
|
}
|
||||||
stack.add(key)
|
stack.add(key)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,3 +54,17 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable data object ProvidersScreen : NavKey
|
@Serializable data object ProvidersScreen : NavKey
|
||||||
|
|
||||||
@Serializable data object AnalyticsScreen : NavKey
|
@Serializable data object AnalyticsScreen : NavKey
|
||||||
|
|
||||||
|
// ── Settings drill-down sub-pages ──────────────────────────────────────
|
||||||
|
|
||||||
|
@Serializable data object SettingsConnection : NavKey
|
||||||
|
|
||||||
|
@Serializable data object SettingsAppearance : NavKey
|
||||||
|
|
||||||
|
@Serializable data object SettingsChat : NavKey
|
||||||
|
|
||||||
|
@Serializable data object SettingsNavBar : NavKey
|
||||||
|
|
||||||
|
@Serializable data object SettingsBehavior : NavKey
|
||||||
|
|
||||||
|
@Serializable data object SettingsAbout : NavKey
|
||||||
@@ -208,9 +208,7 @@ object ScreenRegistry {
|
|||||||
DrawerSection.INSPECT,
|
DrawerSection.INSPECT,
|
||||||
) { sessionId, openDrawer ->
|
) { sessionId, openDrawer ->
|
||||||
SettingsScreenContent(
|
SettingsScreenContent(
|
||||||
onBack = {
|
onOpenDrawer = openDrawer,
|
||||||
NavigationController.goBack()
|
|
||||||
},
|
|
||||||
onNavigateToLogin = {
|
onNavigateToLogin = {
|
||||||
NavigationController.navigateTo(AuthLoginScreen)
|
NavigationController.navigateTo(AuthLoginScreen)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,62 +1,53 @@
|
|||||||
package com.m57.hermescontrol.ui.settings
|
package com.m57.hermescontrol.ui.settings
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ListAlt
|
||||||
|
import androidx.compose.material.icons.filled.ChatBubbleOutline
|
||||||
|
import androidx.compose.material.icons.filled.Dashboard
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Palette
|
||||||
|
import androidx.compose.material.icons.filled.Tune
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.BuildConfig
|
import com.m57.hermescontrol.NavigationController
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.SettingsAbout
|
||||||
|
import com.m57.hermescontrol.SettingsAppearance
|
||||||
|
import com.m57.hermescontrol.SettingsBehavior
|
||||||
|
import com.m57.hermescontrol.SettingsChat
|
||||||
|
import com.m57.hermescontrol.SettingsConnection
|
||||||
|
import com.m57.hermescontrol.SettingsNavBar
|
||||||
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.NavIcon
|
import com.m57.hermescontrol.ui.common.NavIcon
|
||||||
import com.m57.hermescontrol.ui.settings.components.AppearanceSection
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.BehaviorSection
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.ChatSection
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.ConnectionSection
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.NavBarSection
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.TestConnectionButton
|
|
||||||
import com.m57.hermescontrol.ui.settings.components.TestResultCard
|
|
||||||
|
|
||||||
enum class SettingsTab {
|
|
||||||
CONNECTION,
|
|
||||||
APPEARANCE,
|
|
||||||
BEHAVIOR,
|
|
||||||
ABOUT,
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
onBack: () -> Unit,
|
onOpenDrawer: (() -> Unit)? = null,
|
||||||
onNavigateToLogin: () -> Unit = {},
|
onNavigateToLogin: () -> Unit = {},
|
||||||
onLogout: () -> Unit = {},
|
onLogout: () -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -64,178 +55,153 @@ fun SettingsScreen(
|
|||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(state.navigateToLogin) {
|
|
||||||
if (state.navigateToLogin) {
|
|
||||||
onNavigateToLogin()
|
|
||||||
viewModel.clearNavigateToLogin()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var passwordVisible by remember { mutableStateOf(false) }
|
|
||||||
var selectedTab by remember { mutableStateOf(SettingsTab.CONNECTION) }
|
|
||||||
|
|
||||||
BackHandler(onBack = onBack)
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_settings)) },
|
title = { Text(stringResource(R.string.screen_settings)) },
|
||||||
navigationIcon = NavIcon.Back(onBack),
|
navigationIcon = onOpenDrawer?.let { NavIcon.Menu(it) },
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
modifier
|
||||||
.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
PrimaryScrollableTabRow(
|
|
||||||
selectedTabIndex = selectedTab.ordinal,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
edgePadding = 0.dp,
|
|
||||||
) {
|
|
||||||
SettingsTab.entries.forEach { tab ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedTab == tab,
|
|
||||||
onClick = { selectedTab = tab },
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
when (tab) {
|
|
||||||
SettingsTab.CONNECTION -> stringResource(R.string.settings_tab_connection)
|
|
||||||
SettingsTab.APPEARANCE -> stringResource(R.string.settings_tab_appearance)
|
|
||||||
SettingsTab.BEHAVIOR -> stringResource(R.string.settings_tab_behavior)
|
|
||||||
SettingsTab.ABOUT -> stringResource(R.string.settings_tab_about)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
) {
|
||||||
when (selectedTab) {
|
SettingsCategoryCard(
|
||||||
SettingsTab.CONNECTION -> {
|
items =
|
||||||
ConnectionSection(
|
listOf(
|
||||||
state = state,
|
SettingsRow(
|
||||||
viewModel = viewModel,
|
icon = Icons.AutoMirrored.Filled.ListAlt,
|
||||||
passwordVisible = passwordVisible,
|
label = stringResource(R.string.settings_sec_connection),
|
||||||
onPasswordVisibilityToggle = { passwordVisible = !passwordVisible },
|
summary =
|
||||||
|
if (state.profiles.isNotEmpty()) {
|
||||||
|
stringResource(
|
||||||
|
R.string.settings_summary_profiles,
|
||||||
|
state.profiles.size,
|
||||||
)
|
)
|
||||||
|
|
||||||
TestResultCard(testResult = state.testResult)
|
|
||||||
TestConnectionButton(
|
|
||||||
isTesting = state.isTesting,
|
|
||||||
onTest = viewModel::testConnection,
|
|
||||||
)
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
viewModel.logout()
|
|
||||||
onLogout()
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors =
|
|
||||||
ButtonDefaults.buttonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.error,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.settings_logout))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsTab.BEHAVIOR -> {
|
|
||||||
BehaviorSection(
|
|
||||||
autoReconnect = state.autoReconnect,
|
|
||||||
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsTab.APPEARANCE -> {
|
|
||||||
AppearanceSection(
|
|
||||||
themePreference = state.themePreference,
|
|
||||||
onThemeChange = viewModel::onThemeChange,
|
|
||||||
useDynamicColors = state.useDynamicColors,
|
|
||||||
onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange,
|
|
||||||
themePreset = state.themePreset,
|
|
||||||
onThemePresetChange = viewModel::onThemePresetChange,
|
|
||||||
appLanguage = state.appLanguage,
|
|
||||||
onAppLanguageChange = viewModel::onAppLanguageChange,
|
|
||||||
)
|
|
||||||
|
|
||||||
ChatSection(
|
|
||||||
typingEffectEnabled = state.typingEffectEnabled,
|
|
||||||
onTypingEffectEnabledChange = viewModel::onTypingEffectEnabledChange,
|
|
||||||
typingEffectDelayMs = state.typingEffectDelayMs,
|
|
||||||
onTypingEffectDelayMsChange = viewModel::onTypingEffectDelayMsChange,
|
|
||||||
)
|
|
||||||
|
|
||||||
NavBarSection(
|
|
||||||
bottomNavDisplayMode = state.bottomNavDisplayMode,
|
|
||||||
onBottomNavDisplayModeChange = viewModel::onBottomNavDisplayModeChange,
|
|
||||||
selectedNavItems = state.selectedNavItems,
|
|
||||||
availableNavItems = viewModel.availableNavItems,
|
|
||||||
onReorderNavItems = viewModel::reorderNavItems,
|
|
||||||
onRemoveNavItem = viewModel::removeNavItem,
|
|
||||||
onAddNavItem = viewModel::addNavItem,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsTab.ABOUT -> {
|
|
||||||
// ── About section ─────────────────────────────────────────
|
|
||||||
SectionCard(title = stringResource(R.string.settings_sec_about)) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.settings_about_app_name),
|
|
||||||
style =
|
|
||||||
MaterialTheme.typography.titleMedium.copy(
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
InfoRow(
|
|
||||||
label = stringResource(R.string.settings_about_version),
|
|
||||||
value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
|
|
||||||
)
|
|
||||||
InfoRow(
|
|
||||||
label = stringResource(R.string.settings_about_build),
|
|
||||||
value =
|
|
||||||
if (BuildConfig.DEBUG) {
|
|
||||||
stringResource(R.string.settings_about_debug)
|
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.settings_about_release)
|
null
|
||||||
},
|
},
|
||||||
)
|
onClick = { NavigationController.navigateTo(SettingsConnection) },
|
||||||
if (BuildConfig.GIT_SHA.isNotBlank()) {
|
),
|
||||||
InfoRow(
|
SettingsRow(
|
||||||
label = stringResource(R.string.settings_about_commit),
|
icon = Icons.Filled.Palette,
|
||||||
value = BuildConfig.GIT_SHA,
|
label = stringResource(R.string.settings_sec_appearance),
|
||||||
)
|
summary = appearanceSummary(state),
|
||||||
}
|
onClick = { NavigationController.navigateTo(SettingsAppearance) },
|
||||||
|
),
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
SettingsRow(
|
||||||
Text(
|
icon = Icons.Filled.ChatBubbleOutline,
|
||||||
text = "https://github.com/Hy4ri/hermes-mobile",
|
label = stringResource(R.string.settings_sec_chat),
|
||||||
style =
|
summary =
|
||||||
MaterialTheme.typography.bodySmall.copy(
|
if (state.typingEffectEnabled) {
|
||||||
color = MaterialTheme.colorScheme.primary,
|
stringResource(R.string.settings_summary_typing_on)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.settings_summary_typing_off)
|
||||||
|
},
|
||||||
|
onClick = { NavigationController.navigateTo(SettingsChat) },
|
||||||
|
),
|
||||||
|
SettingsRow(
|
||||||
|
icon = Icons.Filled.Dashboard,
|
||||||
|
label = stringResource(R.string.settings_sec_nav_bar),
|
||||||
|
onClick = { NavigationController.navigateTo(SettingsNavBar) },
|
||||||
|
),
|
||||||
|
SettingsRow(
|
||||||
|
icon = Icons.Filled.Tune,
|
||||||
|
label = stringResource(R.string.settings_sec_behavior),
|
||||||
|
summary =
|
||||||
|
if (state.autoReconnect) {
|
||||||
|
stringResource(R.string.settings_summary_on)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.settings_summary_off)
|
||||||
|
},
|
||||||
|
onClick = { NavigationController.navigateTo(SettingsBehavior) },
|
||||||
|
),
|
||||||
|
SettingsRow(
|
||||||
|
icon = Icons.Filled.Info,
|
||||||
|
label = stringResource(R.string.settings_sec_about),
|
||||||
|
onClick = { NavigationController.navigateTo(SettingsAbout) },
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
@Composable
|
||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
|
private fun appearanceSummary(state: SettingsUiState): String {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
val theme =
|
||||||
|
when (state.themePreference) {
|
||||||
|
ThemePreference.SYSTEM -> stringResource(R.string.settings_summary_theme_system)
|
||||||
|
ThemePreference.LIGHT -> stringResource(R.string.settings_summary_theme_light)
|
||||||
|
ThemePreference.DARK -> stringResource(R.string.settings_summary_theme_dark)
|
||||||
}
|
}
|
||||||
|
val lang =
|
||||||
|
when (state.appLanguage) {
|
||||||
|
"system" -> stringResource(R.string.language_system)
|
||||||
|
"ko" -> stringResource(R.string.language_korean)
|
||||||
|
else -> stringResource(R.string.language_english)
|
||||||
}
|
}
|
||||||
|
return "$theme · $lang"
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class SettingsRow(
|
||||||
|
val icon: ImageVector,
|
||||||
|
val label: String,
|
||||||
|
val summary: String? = null,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsCategoryCard(items: List<SettingsRow>) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
elevation = CardDefaults.cardElevation(1.dp),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
items.forEachIndexed { index, row ->
|
||||||
|
ListItem(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = row.onClick),
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = row.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = row.label,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent =
|
||||||
|
row.summary?.let { summary ->
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if (index < items.lastIndex) {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(start = 56.dp, end = 16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,11 +213,7 @@ internal fun InfoRow(
|
|||||||
label: String,
|
label: String,
|
||||||
value: String,
|
value: String,
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp)) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = label,
|
text = label,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
package com.m57.hermescontrol.ui.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
|
import com.m57.hermescontrol.ui.common.NavIcon
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.AboutSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.AppearanceSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.BehaviorSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.ChatSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.ConnectionSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.NavBarSection
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.TestConnectionButton
|
||||||
|
import com.m57.hermescontrol.ui.settings.components.TestResultCard
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drill-down sub-pages for Settings. Each is its own NavKey destination
|
||||||
|
* (see Navigation.kt) so the native back stack handles navigation — no
|
||||||
|
* manual routing. The SettingsViewModel stays the single source of truth.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsConnectionPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_connection)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
ConnectionSection(
|
||||||
|
state = state,
|
||||||
|
viewModel = viewModel,
|
||||||
|
passwordVisible = passwordVisible,
|
||||||
|
onPasswordVisibilityToggle = { passwordVisible = !passwordVisible },
|
||||||
|
)
|
||||||
|
|
||||||
|
TestResultCard(testResult = state.testResult)
|
||||||
|
TestConnectionButton(
|
||||||
|
isTesting = state.isTesting,
|
||||||
|
onTest = viewModel::testConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
viewModel.logout()
|
||||||
|
onLogout()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors =
|
||||||
|
ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.settings_logout))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsAppearancePage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_appearance)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
AppearanceSection(
|
||||||
|
themePreference = state.themePreference,
|
||||||
|
onThemeChange = viewModel::onThemeChange,
|
||||||
|
useDynamicColors = state.useDynamicColors,
|
||||||
|
onUseDynamicColorsChange = viewModel::onUseDynamicColorsChange,
|
||||||
|
themePreset = state.themePreset,
|
||||||
|
onThemePresetChange = viewModel::onThemePresetChange,
|
||||||
|
appLanguage = state.appLanguage,
|
||||||
|
onAppLanguageChange = viewModel::onAppLanguageChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsChatPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_chat)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
ChatSection(
|
||||||
|
typingEffectEnabled = state.typingEffectEnabled,
|
||||||
|
onTypingEffectEnabledChange = viewModel::onTypingEffectEnabledChange,
|
||||||
|
typingEffectDelayMs = state.typingEffectDelayMs,
|
||||||
|
onTypingEffectDelayMsChange = viewModel::onTypingEffectDelayMsChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsNavBarPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_nav_bar)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
NavBarSection(
|
||||||
|
bottomNavDisplayMode = state.bottomNavDisplayMode,
|
||||||
|
onBottomNavDisplayModeChange = viewModel::onBottomNavDisplayModeChange,
|
||||||
|
selectedNavItems = state.selectedNavItems,
|
||||||
|
availableNavItems = viewModel.availableNavItems,
|
||||||
|
onReorderNavItems = viewModel::reorderNavItems,
|
||||||
|
onRemoveNavItem = viewModel::removeNavItem,
|
||||||
|
onAddNavItem = viewModel::addNavItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsBehaviorPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_behavior)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
BehaviorSection(
|
||||||
|
autoReconnect = state.autoReconnect,
|
||||||
|
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsAboutPage(
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onLogout: () -> Unit,
|
||||||
|
viewModel: SettingsViewModel = viewModel { SettingsViewModel() },
|
||||||
|
) {
|
||||||
|
HermesScaffold(
|
||||||
|
title = { Text(stringResource(R.string.settings_sec_about)) },
|
||||||
|
navigationIcon = NavIcon.Back(onBack),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
AboutSection(onLogout = onLogout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -179,6 +179,16 @@
|
|||||||
<string name="settings_about_debug">Debug</string>
|
<string name="settings_about_debug">Debug</string>
|
||||||
<string name="settings_about_release">Release</string>
|
<string name="settings_about_release">Release</string>
|
||||||
<string name="settings_about_commit">Commit</string>
|
<string name="settings_about_commit">Commit</string>
|
||||||
|
|
||||||
|
<!-- Settings list summaries (drill-down) -->
|
||||||
|
<string name="settings_summary_profiles">%1$d saved</string>
|
||||||
|
<string name="settings_summary_typing_on">Typing effect on</string>
|
||||||
|
<string name="settings_summary_typing_off">Typing effect off</string>
|
||||||
|
<string name="settings_summary_on">On</string>
|
||||||
|
<string name="settings_summary_off">Off</string>
|
||||||
|
<string name="settings_summary_theme_system">System</string>
|
||||||
|
<string name="settings_summary_theme_light">Light</string>
|
||||||
|
<string name="settings_summary_theme_dark">Dark</string>
|
||||||
<string name="action_rename">Rename</string>
|
<string name="action_rename">Rename</string>
|
||||||
<string name="action_save">Save</string>
|
<string name="action_save">Save</string>
|
||||||
<string name="settings_action_add_profile">Add Profile</string>
|
<string name="settings_action_add_profile">Add Profile</string>
|
||||||
|
|||||||
Reference in new issue
Block a user