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.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))
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in new issue
Block a user