feat: Reorganize settings into categorized tab pages (#345)

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Adheres to ktlint conventions

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Fixed UI bug by removing padding (`edgePadding = 0.dp`) in the TabRow to avoid excessive white space
- Adheres to ktlint conventions

* fix(#345): remove double padding on settings tabs

HermesScaffold already applies Scaffold's inner padding (top bar height +
status bar insets) via its own Box wrapper. The tabbed settings layout
was applying the same paddingValues a second time on the outer Column,
causing the tab row to sit ~64dp too low. Fix removes the redundant
.padding(paddingValues) call.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-06-24 11:26:10 +03:00
1 parent 3605ca7537
commit 11ca9b55c8
2 files changed
+172 -91

No files matched your search

@@ -39,12 +39,14 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
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.getValue import androidx.compose.runtime.getValue
@@ -68,6 +70,13 @@ import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset import com.m57.hermescontrol.theme.ThemePreset
import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.HermesScaffold
enum class SettingsTab {
CONNECTION,
APPEARANCE,
BEHAVIOR,
ABOUT,
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -77,6 +86,7 @@ fun SettingsScreen(
) { ) {
val state by viewModel.uiState.collectAsStateWithLifecycle() val state by viewModel.uiState.collectAsStateWithLifecycle()
var passwordVisible by remember { mutableStateOf(false) } var passwordVisible by remember { mutableStateOf(false) }
var selectedTab by remember { mutableStateOf(SettingsTab.CONNECTION) }
BackHandler(onBack = onBack) BackHandler(onBack = onBack)
@@ -85,6 +95,34 @@ fun SettingsScreen(
showBack = true, showBack = true,
onBack = onBack, onBack = onBack,
) { ) {
Column(
modifier =
Modifier
.fillMaxSize(),
) {
ScrollableTabRow(
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( Column(
modifier = modifier =
Modifier Modifier
@@ -93,6 +131,8 @@ fun SettingsScreen(
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
when (selectedTab) {
SettingsTab.CONNECTION -> {
// Connection section // Connection section
SectionCard(title = stringResource(R.string.settings_sec_connection)) { SectionCard(title = stringResource(R.string.settings_sec_connection)) {
// Connection Profiles management in Settings // Connection Profiles management in Settings
@@ -108,8 +148,14 @@ fun SettingsScreen(
onClick = { profilesExpanded = true }, onClick = { profilesExpanded = true },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId } val activeProfile =
Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default)) state.profiles.firstOrNull { it.id == state.selectedProfileId }
Text(
text =
activeProfile?.name ?: stringResource(
R.string.settings_profile_default,
),
)
} }
DropdownMenu( DropdownMenu(
expanded = profilesExpanded, expanded = profilesExpanded,
@@ -264,6 +310,84 @@ fun SettingsScreen(
} }
} }
// Test result
AnimatedVisibility(
visible = state.testResult != null,
enter = fadeIn(),
exit = fadeOut(),
) {
state.testResult?.let { result ->
Card(
colors =
CardDefaults.cardColors(
containerColor =
if (result.startsWith("✅")) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.errorContainer
},
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = result,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
// Save indicator
AnimatedVisibility(
visible = state.isSaved,
enter = fadeIn(),
exit = fadeOut(),
) {
Text(
text = stringResource(R.string.settings_save_success),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
),
)
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlinedButton(
onClick = viewModel::testConnection,
enabled = !state.isTesting,
modifier = Modifier.weight(1f),
) {
if (state.isTesting) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
)
} else {
Text(stringResource(R.string.settings_action_test_connection))
}
}
Button(
onClick = viewModel::save,
modifier = Modifier.weight(1f),
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringResource(R.string.action_save))
}
}
Spacer(modifier = Modifier.height(16.dp))
}
SettingsTab.BEHAVIOR -> {
// Behavior section // Behavior section
SectionCard(title = stringResource(R.string.settings_sec_behavior)) { SectionCard(title = stringResource(R.string.settings_sec_behavior)) {
Row( Row(
@@ -295,7 +419,8 @@ fun SettingsScreen(
) )
} }
} }
}
SettingsTab.APPEARANCE -> {
// Appearance section // Appearance section
SectionCard(title = stringResource(R.string.settings_sec_appearance)) { SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
Text( Text(
@@ -390,11 +515,26 @@ fun SettingsScreen(
text = { text = {
Text( Text(
when (preset) { when (preset) {
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default) ThemePreset.DEFAULT ->
ThemePreset.MONOCHROME -> stringResource(R.string.theme_preset_monochrome) stringResource(
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox) R.string.theme_preset_default,
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin) )
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled) ThemePreset.MONOCHROME ->
stringResource(
R.string.theme_preset_monochrome,
)
ThemePreset.GRUVBOX ->
stringResource(
R.string.theme_preset_gruvbox,
)
ThemePreset.CATPPUCCIN ->
stringResource(
R.string.theme_preset_catppuccin,
)
ThemePreset.AMOLED ->
stringResource(
R.string.theme_preset_amoled,
)
}, },
) )
}, },
@@ -443,7 +583,11 @@ fun SettingsScreen(
AnimatedVisibility(visible = state.typingEffectEnabled) { AnimatedVisibility(visible = state.typingEffectEnabled) {
Column(modifier = Modifier.padding(top = 12.dp)) { Column(modifier = Modifier.padding(top = 12.dp)) {
Text( Text(
text = stringResource(R.string.settings_item_typing_delay, state.typingEffectDelayMs), text =
stringResource(
R.string.settings_item_typing_delay,
state.typingEffectDelayMs,
),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -577,7 +721,11 @@ fun SettingsScreen(
IconButton(onClick = { viewModel.removeNavItem(name) }) { IconButton(onClick = { viewModel.removeNavItem(name) }) {
Icon( Icon(
Icons.Filled.Close, Icons.Filled.Close,
contentDescription = stringResource(R.string.content_desc_remove_item, label), contentDescription =
stringResource(
R.string.content_desc_remove_item,
label,
),
) )
} }
} }
@@ -585,7 +733,8 @@ fun SettingsScreen(
} }
if (state.selectedNavItems.size < 5) { if (state.selectedNavItems.size < 5) {
val available = viewModel.availableNavItems.filter { it.first !in state.selectedNavItems } val available =
viewModel.availableNavItems.filter { it.first !in state.selectedNavItems }
if (available.isNotEmpty()) { if (available.isNotEmpty()) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box { Box {
@@ -615,84 +764,8 @@ fun SettingsScreen(
) )
} }
} }
// Test result
AnimatedVisibility(
visible = state.testResult != null,
enter = fadeIn(),
exit = fadeOut(),
) {
state.testResult?.let { result ->
Card(
colors =
CardDefaults.cardColors(
containerColor =
if (result.startsWith("✅")) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.errorContainer
},
),
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = result,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodyMedium,
)
} }
} SettingsTab.ABOUT -> {
}
// Save indicator
AnimatedVisibility(
visible = state.isSaved,
enter = fadeIn(),
exit = fadeOut(),
) {
Text(
text = stringResource(R.string.settings_save_success),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
),
)
}
// Buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
OutlinedButton(
onClick = viewModel::testConnection,
enabled = !state.isTesting,
modifier = Modifier.weight(1f),
) {
if (state.isTesting) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
)
} else {
Text(stringResource(R.string.settings_action_test_connection))
}
}
Button(
onClick = viewModel::save,
modifier = Modifier.weight(1f),
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text(stringResource(R.string.action_save))
}
}
Spacer(modifier = Modifier.height(16.dp))
// ── About section ───────────────────────────────────────── // ── About section ─────────────────────────────────────────
SectionCard(title = stringResource(R.string.settings_sec_about)) { SectionCard(title = stringResource(R.string.settings_sec_about)) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -723,7 +796,10 @@ fun SettingsScreen(
}, },
) )
if (BuildConfig.GIT_SHA.isNotBlank()) { if (BuildConfig.GIT_SHA.isNotBlank()) {
InfoRow(label = stringResource(R.string.settings_about_commit), value = BuildConfig.GIT_SHA) InfoRow(
label = stringResource(R.string.settings_about_commit),
value = BuildConfig.GIT_SHA,
)
} }
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@@ -735,8 +811,9 @@ fun SettingsScreen(
), ),
) )
} }
}
Spacer(modifier = Modifier.height(16.dp)) }
}
} }
} }
} }
+4
View File
@@ -105,6 +105,10 @@
<string name="content_desc_edit">Edit</string> <string name="content_desc_edit">Edit</string>
<!-- Settings Screen --> <!-- Settings Screen -->
<string name="settings_tab_connection">Connection</string>
<string name="settings_tab_appearance">Appearance</string>
<string name="settings_tab_behavior">Behavior</string>
<string name="settings_tab_about">About</string>
<string name="settings_sec_connection">Connection</string> <string name="settings_sec_connection">Connection</string>
<string name="settings_item_profile">Connection Profile</string> <string name="settings_item_profile">Connection Profile</string>
<string name="settings_profile_default">Standalone / Default</string> <string name="settings_profile_default">Standalone / Default</string>