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:
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.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.ScrollableTabRow
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.ui.common.HermesScaffold
|
||||
|
||||
enum class SettingsTab {
|
||||
CONNECTION,
|
||||
APPEARANCE,
|
||||
BEHAVIOR,
|
||||
ABOUT,
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
@@ -77,6 +86,7 @@ fun SettingsScreen(
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
var selectedTab by remember { mutableStateOf(SettingsTab.CONNECTION) }
|
||||
|
||||
BackHandler(onBack = onBack)
|
||||
|
||||
@@ -85,6 +95,34 @@ fun SettingsScreen(
|
||||
showBack = true,
|
||||
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(
|
||||
modifier =
|
||||
Modifier
|
||||
@@ -93,6 +131,8 @@ fun SettingsScreen(
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
when (selectedTab) {
|
||||
SettingsTab.CONNECTION -> {
|
||||
// Connection section
|
||||
SectionCard(title = stringResource(R.string.settings_sec_connection)) {
|
||||
// Connection Profiles management in Settings
|
||||
@@ -108,8 +148,14 @@ fun SettingsScreen(
|
||||
onClick = { profilesExpanded = true },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
|
||||
Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default))
|
||||
val activeProfile =
|
||||
state.profiles.firstOrNull { it.id == state.selectedProfileId }
|
||||
Text(
|
||||
text =
|
||||
activeProfile?.name ?: stringResource(
|
||||
R.string.settings_profile_default,
|
||||
),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
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
|
||||
SectionCard(title = stringResource(R.string.settings_sec_behavior)) {
|
||||
Row(
|
||||
@@ -295,7 +419,8 @@ fun SettingsScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
SettingsTab.APPEARANCE -> {
|
||||
// Appearance section
|
||||
SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
|
||||
Text(
|
||||
@@ -390,11 +515,26 @@ fun SettingsScreen(
|
||||
text = {
|
||||
Text(
|
||||
when (preset) {
|
||||
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default)
|
||||
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)
|
||||
ThemePreset.DEFAULT ->
|
||||
stringResource(
|
||||
R.string.theme_preset_default,
|
||||
)
|
||||
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) {
|
||||
Column(modifier = Modifier.padding(top = 12.dp)) {
|
||||
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,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@@ -577,7 +721,11 @@ fun SettingsScreen(
|
||||
IconButton(onClick = { viewModel.removeNavItem(name) }) {
|
||||
Icon(
|
||||
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) {
|
||||
val available = viewModel.availableNavItems.filter { it.first !in state.selectedNavItems }
|
||||
val available =
|
||||
viewModel.availableNavItems.filter { it.first !in state.selectedNavItems }
|
||||
if (available.isNotEmpty()) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.ABOUT -> {
|
||||
// ── About section ─────────────────────────────────────────
|
||||
SectionCard(title = stringResource(R.string.settings_sec_about)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
@@ -723,7 +796,10 @@ fun SettingsScreen(
|
||||
},
|
||||
)
|
||||
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))
|
||||
@@ -735,8 +811,9 @@ fun SettingsScreen(
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,10 @@
|
||||
<string name="content_desc_edit">Edit</string>
|
||||
|
||||
<!-- 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_item_profile">Connection Profile</string>
|
||||
<string name="settings_profile_default">Standalone / Default</string>
|
||||
|
||||
Reference in new issue
Block a user