diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt index f3061ea..27025db 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/navigation/MainApp.kt @@ -74,10 +74,15 @@ fun MainApp() { NavigationBarItem( selected = currentRoute == Routes.TABLES || currentRoute?.startsWith("list/") == true, onClick = { - navController.navigate(Routes.TABLES) { - popUpTo(navController.graph.findStartDestination().id) { saveState = true } - launchSingleTop = true - restoreState = true + 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) }, @@ -87,10 +92,15 @@ fun MainApp() { // Highlight settings tab for both Settings screen and Logs screen selected = currentRoute == Routes.SETTINGS || currentRoute == Routes.LOGS, onClick = { - navController.navigate(Routes.SETTINGS) { - popUpTo(navController.graph.findStartDestination().id) { saveState = true } - launchSingleTop = true - restoreState = true + 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) }, diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index f135c7d..2e7efed 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -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,128 +482,121 @@ 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") - } + // Unified layout: always render headers, then content state/messages + val listState = rememberLazyListState() + LaunchedEffect(processedItems.size, pendingScrollToBottom) { + if (pendingScrollToBottom && processedItems.isNotEmpty()) { + val lastIndex = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0) + try { + listState.animateScrollToItem(lastIndex) + } catch (_: Exception) { + listState.scrollToItem(lastIndex) } + pendingScrollToBottom = false } - } else { - 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 - val lastIndex = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0) - try { - listState.animateScrollToItem(lastIndex) - } catch (_: Exception) { - listState.scrollToItem(lastIndex) - } - pendingScrollToBottom = false - } - } - Column( + } + Column( + modifier = + Modifier + .fillMaxSize() + .horizontalScroll(horizontalScrollState), + ) { + // Fixed header (always visible even with zero items) + Row( modifier = Modifier - .fillMaxSize() - .horizontalScroll(horizontalScrollState), + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surface), + verticalAlignment = Alignment.CenterVertically, ) { - // Fixed header (does not participate in vertical scroll) - Row( - modifier = - Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface), - verticalAlignment = Alignment.CenterVertically, - ) { - stableFields.forEach { field -> - key(field.id) { - FieldHeader( - field = field, - width = fieldWidths[field.id] ?: 150.dp, - onWidthChange = { delta -> - val currentWidth = fieldWidths[field.id] ?: 150.dp - val newWidth = (currentWidth.value + delta).coerceAtLeast(100f) - fieldWidths[field.id] = newWidth.dp - prefs.setColumnWidths( - listId, - fieldWidths.mapValues { it.value.value }, - ) - }, - scrollState = horizontalScrollState, - isLast = (field.id == stableFields.lastOrNull()?.id), - 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()) - }, - ) - } + stableFields.forEach { field -> + key(field.id) { + FieldHeader( + field = field, + width = fieldWidths[field.id] ?: 150.dp, + onWidthChange = { delta -> + val currentWidth = fieldWidths[field.id] ?: 150.dp + val newWidth = (currentWidth.value + delta).coerceAtLeast(100f) + fieldWidths[field.id] = newWidth.dp + prefs.setColumnWidths( + listId, + fieldWidths.mapValues { it.value.value }, + ) + }, + scrollState = horizontalScrollState, + isLast = (field.id == stableFields.lastOrNull()?.id), + onHeaderClick = { showManageColumnsDialog = true }, + alignment = columnAlignments[field.id] ?: "start", + onAlignmentChange = { newAlign -> + columnAlignments[field.id] = newAlign + viewModel.updateFieldAlignment(field.id, newAlign) + prefs.setColumnAlignments(listId, columnAlignments.toMap()) + }, + ) } + } + } - // Loading overlay only while nothing is available to render yet - if (isLoading && stableFields.isEmpty() && stableItems.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - androidx.compose.material3.CircularProgressIndicator() + when { + isLoading && stableItems.isEmpty() -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { androidx.compose.material3.CircularProgressIndicator() } + } + stableItems.isEmpty() -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + 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 - LazyColumn( - 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 }, - contentType = { "row" }, - ) { itemWithValues -> - ItemRow( - fields = stableFields, - fieldWidths = fieldWidths, - fieldAlignments = columnAlignments, - itemWithValues = itemWithValues, - onClick = { itemToEdit = itemWithValues }, - ) + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(0.dp), + ) { + groupedItems.entries.forEach { (_, groupItems) -> + items( + items = groupItems, + key = { it.item.id }, + contentType = { "row" }, + ) { itemWithValues -> + ItemRow( + fields = stableFields, + fieldWidths = fieldWidths, + fieldAlignments = columnAlignments, + itemWithValues = itemWithValues, + onClick = { itemToEdit = itemWithValues }, + ) + } } } } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt index 8c82f5d..3f2ad89 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/SettingsScreen.kt @@ -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, ) @@ -453,7 +453,7 @@ fun SettingsScreen( modifier = Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Text( + Text( text = "View application logs", 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)) } }, ) diff --git a/CollabTableAndroid/app/src/main/res/values/strings.xml b/CollabTableAndroid/app/src/main/res/values/strings.xml index e8c524a..7eafa35 100644 --- a/CollabTableAndroid/app/src/main/res/values/strings.xml +++ b/CollabTableAndroid/app/src/main/res/values/strings.xml @@ -3,8 +3,8 @@ Tables Create Table Table Name - Add Field - Field Name + Add Column + Column Name Add Item Add Delete @@ -20,7 +20,67 @@ Syncing… Sync error Delete Table - Delete Field + Delete Column Delete Item Are you sure you want to delete this? + + System + Light + Dark + Appearance + Use Material 3 colors + AMOLED dark + Dynamic colors on supported devices + Pure black background in dark mode + Server Connection + Connected to + HTTP polling interval (ms) + Apply + Current: %1$d ms (min 250, max 600000) + Notifications + List added + Notify when a new list/table is created + List edited + Notify when a list/table name changes + List content updated + Notify when columns or items change (add/edit/delete) + List removed + Notify when a list/table is deleted + Notifications fire only when the app is in background or not visible. + Open Logs + Leave Server + Leaving removes local data + Disconnects and clears local database + Clears stored server URL and password + Deletes all local data (tables, columns, items) + Returns to server setup screen + Leave Server + Leaving... + Leave Server? + 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. + Clear Filter + No items match the current filter + Auto-fit + Resize + Field Type + + Text + Multi-line Text + Number + Currency + Percentage + Dropdown + Autocomplete + Checkbox + Switch + URL + Email + Phone + Date + Time + Date & Time + Duration + Image + File +