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
+