feat: update navigation logic in MainApp and enhance ListDetailScreen interactions; externalize strings in SettingsScreen and strings.xml

This commit is contained in:
2025-11-13 17:29:08 +01:00
parent c0ded946d8
commit 6b5ae7f578
4 changed files with 233 additions and 167 deletions
@@ -74,11 +74,16 @@ fun MainApp() {
NavigationBarItem(
selected = currentRoute == Routes.TABLES || currentRoute?.startsWith("list/") == true,
onClick = {
if (currentRoute?.startsWith("list/") == true) {
// If we're in a list detail, pop back to the tables root instead of creating a new entry
navController.popBackStack(Routes.TABLES, inclusive = false)
} else if (currentRoute != Routes.TABLES) {
navController.navigate(Routes.TABLES) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
},
icon = { androidx.compose.material3.Icon(Icons.Default.Home, contentDescription = null) },
label = { Text("Tables") },
@@ -87,11 +92,16 @@ fun MainApp() {
// Highlight settings tab for both Settings screen and Logs screen
selected = currentRoute == Routes.SETTINGS || currentRoute == Routes.LOGS,
onClick = {
if (currentRoute == Routes.LOGS) {
// Return to Settings without rebuilding entire graph
navController.popBackStack(Routes.SETTINGS, inclusive = false)
} else if (currentRoute != Routes.SETTINGS) {
navController.navigate(Routes.SETTINGS) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
}
},
icon = { androidx.compose.material3.Icon(Icons.Default.Settings, contentDescription = null) },
label = { Text("Settings") },
@@ -202,7 +202,13 @@ fun ListDetailScreen(
val localCtx = LocalContext.current
val localPrefs = remember { PreferencesManager.getInstance(localCtx) }
ConnectionStatusAction(prefs = localPrefs)
IconButton(onClick = { showAddItemDialog = true }) {
IconButton(onClick = {
if (stableFields.isEmpty()) {
showManageColumnsDialog = true
} else {
showAddItemDialog = true
}
}) {
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_item))
}
},
@@ -253,7 +259,7 @@ fun ListDetailScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "No fields yet. Add fields to get started!",
text = "No columns yet. Add columns to get started!",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -463,7 +469,7 @@ fun ListDetailScreen(
}
prefs.setColumnWidths(listId, fieldWidths.mapValues { it.value.value })
},
label = { Text("Auto-fit") },
label = { Text(stringResource(R.string.auto_fit)) },
leadingIcon = {
Icon(
Icons.Default.FitScreen,
@@ -476,44 +482,10 @@ fun ListDetailScreen(
// Header will be rendered as a stickyHeader inside the LazyColumn below
// Items list with synchronized scrolling
if (stableItems.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.no_items),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} else if (processedItems.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No items match the current filter",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = {
filterField = null
filterValue = ""
}) {
Text("Clear Filter")
}
}
}
} else {
// Unified layout: always render headers, then content state/messages
val listState = rememberLazyListState()
// If a new item was just added, scroll to bottom after composition
LaunchedEffect(processedItems.size, pendingScrollToBottom) {
if (pendingScrollToBottom) {
// Scroll to the last real item index
if (pendingScrollToBottom && processedItems.isNotEmpty()) {
val lastIndex = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0)
try {
listState.animateScrollToItem(lastIndex)
@@ -529,7 +501,7 @@ fun ListDetailScreen(
.fillMaxSize()
.horizontalScroll(horizontalScrollState),
) {
// Fixed header (does not participate in vertical scroll)
// Fixed header (always visible even with zero items)
Row(
modifier =
Modifier
@@ -556,7 +528,6 @@ fun ListDetailScreen(
onHeaderClick = { showManageColumnsDialog = true },
alignment = columnAlignments[field.id] ?: "start",
onAlignmentChange = { newAlign ->
// Update local state and persist via ViewModel + cache to prefs for backward-compat
columnAlignments[field.id] = newAlign
viewModel.updateFieldAlignment(field.id, newAlign)
prefs.setColumnAlignments(listId, columnAlignments.toMap())
@@ -564,28 +535,55 @@ fun ListDetailScreen(
)
}
}
}
// Loading overlay only while nothing is available to render yet
if (isLoading && stableFields.isEmpty() && stableItems.isEmpty()) {
when {
isLoading && stableItems.isEmpty() -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) { androidx.compose.material3.CircularProgressIndicator() }
}
stableItems.isEmpty() -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
androidx.compose.material3.CircularProgressIndicator()
Text(
text = stringResource(R.string.no_items),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
)
}
}
processedItems.isEmpty() -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "No items match the current filter",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = {
filterField = null
filterValue = ""
}) { Text(stringResource(R.string.clear_filter)) }
}
}
}
// Items list below the fixed header
else -> {
LazyColumn(
modifier =
Modifier
.fillMaxSize(),
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(0.dp),
) {
groupedItems.entries.forEach { (_, groupItems) ->
// Show items in the group
items(
items = groupItems,
key = { it.item.id },
@@ -606,6 +604,7 @@ fun ListDetailScreen(
}
}
}
}
if (showManageColumnsDialog) {
ManageColumnsDialog(
@@ -153,7 +153,7 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = "Server Connection",
text = stringResource(R.string.server_connection),
style = MaterialTheme.typography.titleMedium,
)
@@ -169,7 +169,7 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "Connected to",
text = stringResource(R.string.connected_to),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -186,7 +186,7 @@ fun SettingsScreen(
// Authentication section removed
Text(
text = "Appearance",
text = stringResource(R.string.appearance),
style = MaterialTheme.typography.titleMedium,
)
@@ -198,19 +198,19 @@ fun SettingsScreen(
FilterChip(
selected = themeMode == PreferencesManager.THEME_MODE_SYSTEM,
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_SYSTEM) },
label = { Text("System") },
label = { Text(stringResource(R.string.theme_system)) },
leadingIcon = { Icon(Icons.Filled.SettingsBrightness, contentDescription = null) },
)
FilterChip(
selected = themeMode == PreferencesManager.THEME_MODE_LIGHT,
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_LIGHT) },
label = { Text("Light") },
label = { Text(stringResource(R.string.theme_light)) },
leadingIcon = { Icon(Icons.Filled.Brightness7, contentDescription = null) },
)
FilterChip(
selected = themeMode == PreferencesManager.THEME_MODE_DARK,
onClick = { preferencesManager.setThemeMode(PreferencesManager.THEME_MODE_DARK) },
label = { Text("Dark") },
label = { Text(stringResource(R.string.theme_dark)) },
leadingIcon = { Icon(Icons.Filled.Brightness4, contentDescription = null) },
)
}
@@ -222,9 +222,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("Use Material 3 colors")
Text(stringResource(R.string.use_material3_colors))
Text(
text = "Dynamic colors on supported devices",
text = stringResource(R.string.dynamic_colors_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -239,9 +239,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("AMOLED dark")
Text(stringResource(R.string.amoled_dark))
Text(
text = "Pure black background in dark mode",
text = stringResource(R.string.amoled_dark_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -251,7 +251,7 @@ fun SettingsScreen(
// Sync settings
Text(
text = "Sync",
text = stringResource(R.string.sync),
style = MaterialTheme.typography.titleMedium,
)
@@ -267,7 +267,7 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = "HTTP polling interval (ms)",
text = stringResource(R.string.http_polling_interval_ms),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -296,7 +296,7 @@ fun SettingsScreen(
syncIntervalError = null
}
}) {
Text("Apply")
Text(stringResource(R.string.apply))
}
}
if (syncIntervalError != null) {
@@ -307,7 +307,7 @@ fun SettingsScreen(
)
} else {
Text(
text = "Current: $syncIntervalMs ms (min 250, max 600000)",
text = stringResource(R.string.current_interval, syncIntervalMs),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -317,7 +317,7 @@ fun SettingsScreen(
// Notifications section (moved inside the main scroll column so it doesn't overlay other content)
Text(
text = "Notifications",
text = stringResource(R.string.notifications),
style = MaterialTheme.typography.titleMedium,
)
Card(
@@ -334,9 +334,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("List added")
Text(stringResource(R.string.notify_list_added_title))
Text(
text = "Notify when a new list/table is created",
text = stringResource(R.string.notify_list_added_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -360,9 +360,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("List edited")
Text(stringResource(R.string.notify_list_edited_title))
Text(
text = "Notify when a list/table name changes",
text = stringResource(R.string.notify_list_edited_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -386,9 +386,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("List content updated")
Text(stringResource(R.string.notify_list_content_title))
Text(
text = "Notify when fields or items change (add/edit/delete)",
text = stringResource(R.string.notify_list_content_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -412,9 +412,9 @@ fun SettingsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("List removed")
Text(stringResource(R.string.notify_list_removed_title))
Text(
text = "Notify when a list/table is deleted",
text = stringResource(R.string.notify_list_removed_desc),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -433,7 +433,7 @@ fun SettingsScreen(
)
}
Text(
text = "Notifications fire only when the app is in background or not visible.",
text = stringResource(R.string.notifications_footer),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -461,14 +461,14 @@ fun SettingsScreen(
Button(onClick = onNavigateToLogs, modifier = Modifier.fillMaxWidth()) {
Icon(Icons.AutoMirrored.Filled.List, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Open Logs")
Text(stringResource(R.string.open_logs))
}
}
}
// Leave Server section
Text(
text = "Leave Server",
text = stringResource(R.string.leave_server_section),
style = MaterialTheme.typography.titleMedium,
)
Card(
@@ -481,27 +481,27 @@ fun SettingsScreen(
) {
// Warning/info text appears above the action button
Text(
text = "Leaving removes local data",
text = stringResource(R.string.leaving_removes_local_data),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = "Disconnects and clears local database",
text = stringResource(R.string.disconnects_and_clears_db),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "Clears stored server URL and password",
text = stringResource(R.string.clears_server_settings),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "Deletes all local data (tables, fields, items)",
text = stringResource(R.string.deletes_all_local),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = "Returns to server setup screen",
text = stringResource(R.string.returns_to_setup),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -524,9 +524,9 @@ fun SettingsScreen(
strokeWidth = 2.dp,
)
Spacer(modifier = Modifier.width(8.dp))
Text("Leaving...")
Text(stringResource(R.string.leaving_progress))
} else {
Text("Leave Server")
Text(stringResource(R.string.leave_server))
}
}
}
@@ -536,12 +536,9 @@ fun SettingsScreen(
if (showLeaveDialog) {
AlertDialog(
onDismissRequest = { showLeaveDialog = false },
title = { Text("Leave Server?") },
title = { Text(stringResource(R.string.leave_server_dialog_title)) },
text = {
Text(
"All your data will be synced to the server before disconnecting. " +
"After leaving, all local data will be deleted and you'll need to set up a new connection. " +
"This action cannot be undone.",
Text(stringResource(R.string.leave_server_dialog_body),
)
},
confirmButton = {
@@ -589,12 +586,12 @@ fun SettingsScreen(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text("Leave Server")
Text(stringResource(R.string.leave_server))
}
},
dismissButton = {
TextButton(onClick = { showLeaveDialog = false }) {
Text("Cancel")
Text(stringResource(R.string.cancel))
}
},
)
@@ -3,8 +3,8 @@
<string name="lists">Tables</string>
<string name="create_list">Create Table</string>
<string name="list_name">Table Name</string>
<string name="add_field">Add Field</string>
<string name="field_name">Field Name</string>
<string name="add_field">Add Column</string>
<string name="field_name">Column Name</string>
<string name="add_item">Add Item</string>
<string name="add">Add</string>
<string name="delete">Delete</string>
@@ -20,7 +20,67 @@
<string name="syncing">Syncing…</string>
<string name="sync_error">Sync error</string>
<string name="delete_list">Delete Table</string>
<string name="delete_field">Delete Field</string>
<string name="delete_field">Delete Column</string>
<string name="delete_item">Delete Item</string>
<string name="confirm_delete">Are you sure you want to delete this?</string>
<!-- Newly externalized strings -->
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<string name="appearance">Appearance</string>
<string name="use_material3_colors">Use Material 3 colors</string>
<string name="amoled_dark">AMOLED dark</string>
<string name="dynamic_colors_desc">Dynamic colors on supported devices</string>
<string name="amoled_dark_desc">Pure black background in dark mode</string>
<string name="server_connection">Server Connection</string>
<string name="connected_to">Connected to</string>
<string name="http_polling_interval_ms">HTTP polling interval (ms)</string>
<string name="apply">Apply</string>
<string name="current_interval">Current: %1$d ms (min 250, max 600000)</string>
<string name="notifications">Notifications</string>
<string name="notify_list_added_title">List added</string>
<string name="notify_list_added_desc">Notify when a new list/table is created</string>
<string name="notify_list_edited_title">List edited</string>
<string name="notify_list_edited_desc">Notify when a list/table name changes</string>
<string name="notify_list_content_title">List content updated</string>
<string name="notify_list_content_desc">Notify when columns or items change (add/edit/delete)</string>
<string name="notify_list_removed_title">List removed</string>
<string name="notify_list_removed_desc">Notify when a list/table is deleted</string>
<string name="notifications_footer">Notifications fire only when the app is in background or not visible.</string>
<string name="open_logs">Open Logs</string>
<string name="leave_server_section">Leave Server</string>
<string name="leaving_removes_local_data">Leaving removes local data</string>
<string name="disconnects_and_clears_db">Disconnects and clears local database</string>
<string name="clears_server_settings">Clears stored server URL and password</string>
<string name="deletes_all_local">Deletes all local data (tables, columns, items)</string>
<string name="returns_to_setup">Returns to server setup screen</string>
<string name="leave_server">Leave Server</string>
<string name="leaving_progress">Leaving...</string>
<string name="leave_server_dialog_title">Leave Server?</string>
<string name="leave_server_dialog_body">All your data will sync to the server before disconnecting. After leaving, all local data will be deleted and you must set up a new connection. This action cannot be undone.</string>
<string name="clear_filter">Clear Filter</string>
<string name="no_items_match_filter">No items match the current filter</string>
<string name="auto_fit">Auto-fit</string>
<string name="resize">Resize</string>
<string name="field_type">Field Type</string>
<!-- Field type labels -->
<string name="ft_text">Text</string>
<string name="ft_multiline_text">Multi-line Text</string>
<string name="ft_number">Number</string>
<string name="ft_currency">Currency</string>
<string name="ft_percentage">Percentage</string>
<string name="ft_dropdown">Dropdown</string>
<string name="ft_autocomplete">Autocomplete</string>
<string name="ft_checkbox">Checkbox</string>
<string name="ft_switch">Switch</string>
<string name="ft_url">URL</string>
<string name="ft_email">Email</string>
<string name="ft_phone">Phone</string>
<string name="ft_date">Date</string>
<string name="ft_time">Time</string>
<string name="ft_datetime">Date &amp; Time</string>
<string name="ft_duration">Duration</string>
<string name="ft_image">Image</string>
<string name="ft_file">File</string>
<!-- Additional existing types could be added similarly -->
</resources>