feat: configurable bottom navigation bar (Issue #23)
## What Adds a new Navigation Bar section to Settings where users can customize which items appear in the bottom navigation bar. ## Features - Pick from any of the 19 drawer screens (max 5 items) - Remove items with a close/delete button - Items persist via SharedPreferences (CSV format) - Invalid names are silently dropped - Existing screens and auth-aware filtering respected Closes #23
This commit is contained in:
6 files changed
+179
-18
No files matched your search
@@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -17,6 +18,8 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.Button
|
||||
@@ -24,6 +27,8 @@ import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@@ -227,6 +232,60 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation Bar section
|
||||
SectionCard(title = "Navigation Bar") {
|
||||
Text(
|
||||
text = "Bottom bar items (${state.selectedNavItems.size}/5)",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
state.selectedNavItems.forEach { name ->
|
||||
val label = viewModel.availableNavItems.firstOrNull { it.first == name }?.second ?: name
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||
IconButton(onClick = { viewModel.removeNavItem(name) }) {
|
||||
Icon(Icons.Filled.Close, contentDescription = "Remove $label")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.selectedNavItems.size < 5) {
|
||||
val available = viewModel.availableNavItems.filter { it.first !in state.selectedNavItems }
|
||||
if (available.isNotEmpty()) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Box {
|
||||
OutlinedButton(onClick = { expanded = true }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
Spacer(Modifier.size(4.dp))
|
||||
Text("Add Item")
|
||||
}
|
||||
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
available.forEach { (name, label) ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(label) },
|
||||
onClick = {
|
||||
viewModel.addNavItem(name)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Maximum of 5 items",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Test result
|
||||
AnimatedVisibility(
|
||||
visible = state.testResult != null,
|
||||
|
||||
@@ -22,6 +22,7 @@ data class SettingsUiState(
|
||||
val isTesting: Boolean = false,
|
||||
val testResult: String? = null,
|
||||
val isSaved: Boolean = false,
|
||||
val selectedNavItems: List<String> = emptyList(),
|
||||
)
|
||||
|
||||
class SettingsViewModel : ViewModel() {
|
||||
@@ -43,6 +44,7 @@ class SettingsViewModel : ViewModel() {
|
||||
// choice from persistent storage on init. Previously this slot
|
||||
// was missing — always defaulted to ThemePreference.SYSTEM.
|
||||
themePreference = AuthManager.getThemePreference(),
|
||||
selectedNavItems = AuthManager.getBottomNavItems(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -67,6 +69,52 @@ class SettingsViewModel : ViewModel() {
|
||||
_uiState.update { it.copy(themePreference = theme, isSaved = false) }
|
||||
}
|
||||
|
||||
/** All screens available for bottom-nav selection (name → display label). */
|
||||
val availableNavItems: List<Pair<String, String>> =
|
||||
listOf(
|
||||
"ChatScreen" to "Chat",
|
||||
"SkillsScreen" to "Skills",
|
||||
"CronJobsScreen" to "Cron Jobs",
|
||||
"SystemScreen" to "System",
|
||||
"SettingsScreen" to "Settings",
|
||||
"ProfilesScreen" to "Profiles",
|
||||
"WebhooksScreen" to "Webhooks",
|
||||
"GatewayScreen" to "Gateway",
|
||||
"ToolsetsScreen" to "Toolsets",
|
||||
"PluginsScreen" to "Plugins",
|
||||
"ConfigScreen" to "Config",
|
||||
"McpServersScreen" to "MCP Servers",
|
||||
"ModelScreen" to "Models",
|
||||
"PairingScreen" to "Pairing",
|
||||
"KeysScreen" to "Keys",
|
||||
"ChannelsScreen" to "Channels",
|
||||
"LogsScreen" to "Logs",
|
||||
"KanbanScreen" to "Kanban",
|
||||
"AchievementsScreen" to "Achievements",
|
||||
)
|
||||
|
||||
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
|
||||
fun addNavItem(name: String) {
|
||||
val current = _uiState.value.selectedNavItems
|
||||
if (name in current || current.size >= 5) return
|
||||
val updated = current + name
|
||||
_uiState.update { it.copy(selectedNavItems = updated) }
|
||||
AuthManager.setBottomNavItems(updated)
|
||||
}
|
||||
|
||||
/** Remove a nav item from the bottom bar. Auto-saves. */
|
||||
fun removeNavItem(name: String) {
|
||||
val updated = _uiState.value.selectedNavItems - name
|
||||
_uiState.update { it.copy(selectedNavItems = updated) }
|
||||
AuthManager.setBottomNavItems(updated)
|
||||
}
|
||||
|
||||
/** Reorder the selected nav items. Auto-saves. */
|
||||
fun reorderNavItems(items: List<String>) {
|
||||
_uiState.update { it.copy(selectedNavItems = items) }
|
||||
AuthManager.setBottomNavItems(items)
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val state = _uiState.value
|
||||
val port = state.port.toIntOrNull() ?: 9119
|
||||
|
||||
Reference in new issue
Block a user