diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt index 0ff5254..7a58e10 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/preferences/PreferencesManager.kt @@ -271,7 +271,7 @@ class PreferencesManager( private const val KEY_NOTIFY_LIST_ADDED = "notify_list_added" private const val KEY_NOTIFY_LIST_EDITED = "notify_list_edited" private const val KEY_NOTIFY_LIST_REMOVED = "notify_list_removed" - private const val KEY_NOTIFY_LIST_CONTENT_UPDATED = "notify_list_content_updated" + private const val KEY_NOTIFY_LIST_CONTENT_UPDATED = "notify_list_content_updated" private const val KEY_LAST_LIST_NOTIFY_CHECK_TS = "last_list_notify_check_ts" private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId 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 d04ba7d..f135c7d 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 @@ -166,11 +166,12 @@ fun ListDetailScreen( val savedAlign = prefs.getColumnAlignments(listId) stableFields.forEach { field -> val raw = (field.alignment.ifBlank { savedAlign[field.id] ?: "start" }).lowercase() - columnAlignments[field.id] = when (raw) { - "center" -> "center" - "end", "right" -> "end" - else -> "start" - } + columnAlignments[field.id] = + when (raw) { + "center" -> "center" + "end", "right" -> "end" + else -> "start" + } } } @@ -269,333 +270,335 @@ fun ListDetailScreen( Modifier .fillMaxSize(), ) { - // Filter/Sort/Group Controls - Always visible above table - Row( - modifier = - Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .background(MaterialTheme.colorScheme.surfaceVariant) - .padding(horizontal = 8.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - // Sort button - FilterChip( - selected = sortField != null, - onClick = { showSortDialog = true }, - label = { - Text( - if (sortField != null) { - "Sort: ${sortField!!.name} ${if (sortAscending) "↑" else "↓"}" - } else { - "Sort" - }, - ) - }, - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - trailingIcon = - if (sortField != null) { - { - IconButton( - onClick = { - sortField = null - sortAscending = true - }, - modifier = Modifier.size(18.dp), - ) { - Icon( - Icons.Default.Close, - contentDescription = "Clear", - modifier = Modifier.size(16.dp), - ) - } - } - } else { - null - }, - ) - - // Group button - FilterChip( - selected = groupByField != null, - onClick = { showGroupDialog = true }, - label = { - Text( - if (groupByField != null) { - "Group: ${groupByField!!.name}" - } else { - "Group" - }, - ) - }, - leadingIcon = { - Icon( - Icons.AutoMirrored.Filled.List, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - trailingIcon = - if (groupByField != null) { - { - IconButton( - onClick = { groupByField = null }, - modifier = Modifier.size(18.dp), - ) { - Icon( - Icons.Default.Close, - contentDescription = "Clear", - modifier = Modifier.size(16.dp), - ) - } - } - } else { - null - }, - ) - - // Filter button - FilterChip( - selected = filterField != null, - onClick = { showFilterDialog = true }, - label = { - Text( - if (filterField != null) { - "Filter: ${filterField!!.name}" - } else { - "Filter" - }, - ) - }, - leadingIcon = { - Icon( - Icons.Default.Add, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - trailingIcon = - if (filterField != null) { - { - IconButton( - onClick = { - filterField = null - filterValue = "" - }, - modifier = Modifier.size(18.dp), - ) { - Icon( - Icons.Default.Close, - contentDescription = "Clear", - modifier = Modifier.size(16.dp), - ) - } - } - } else { - null - }, - ) - - // Auto-resize button (chip style) - // Auto-fit cache keyed by fieldId with a compact signature based on timestamps and counts - val autoFitCache = remember { mutableStateMapOf>() } - FilterChip( - selected = false, - onClick = { - stableFields.forEach { field -> - // Build a compact signature: field name, field.updatedAt, item count, max updatedAt among values - var maxValUpdated = 0L - stableItems.forEach { item -> - val v = item.values.find { it.fieldId == field.id } - if (v != null && v.updatedAt > maxValUpdated) maxValUpdated = v.updatedAt - } - val signature = - AutoFitSignature( - name = field.name, - fieldUpdatedAt = field.updatedAt, - itemCount = stableItems.size, - maxValueUpdatedAt = maxValUpdated, - ) - - val cached = autoFitCache[field.id] - if (cached != null && cached.first == signature) { - // Use cached width - fieldWidths[field.id] = cached.second.dp - return@forEach - } - - // Measure header and max content width - val headerPx = - textMeasurer - .measure(AnnotatedString(field.name), style = headerTextStyle) - .size.width - .toFloat() - var maxContentPx = 0f - stableItems.forEach { itemWithValues -> - val v = itemWithValues.values.find { it.fieldId == field.id }?.value - val display = getDisplayTextForMeasure(field, v) - if (display.isNotEmpty()) { - val w = - textMeasurer - .measure(AnnotatedString(display), style = bodyTextStyle) - .size.width - .toFloat() - if (w > maxContentPx) maxContentPx = w - } - } - val widthDpValue = - with(density) { - val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp - val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp - val base = maxOf(headerDp, contentDp, 100.dp) - (base + 6.dp).value - } - fieldWidths[field.id] = widthDpValue.dp - autoFitCache[field.id] = signature to widthDpValue - } - prefs.setColumnWidths(listId, fieldWidths.mapValues { it.value.value }) - }, - label = { Text("Auto-fit") }, - leadingIcon = { - Icon( - Icons.Default.FitScreen, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - }, - ) - } - - // 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, + // Filter/Sort/Group Controls - Always visible above table + Row( + modifier = + Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .background(MaterialTheme.colorScheme.surfaceVariant) + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = stringResource(R.string.no_items), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, + // Sort button + FilterChip( + selected = sortField != null, + onClick = { showSortDialog = true }, + label = { + Text( + if (sortField != null) { + "Sort: ${sortField!!.name} ${if (sortAscending) "↑" else "↓"}" + } else { + "Sort" + }, + ) + }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, + trailingIcon = + if (sortField != null) { + { + IconButton( + onClick = { + sortField = null + sortAscending = true + }, + modifier = Modifier.size(18.dp), + ) { + Icon( + Icons.Default.Close, + contentDescription = "Clear", + modifier = Modifier.size(16.dp), + ) + } + } + } else { + null + }, + ) + + // Group button + FilterChip( + selected = groupByField != null, + onClick = { showGroupDialog = true }, + label = { + Text( + if (groupByField != null) { + "Group: ${groupByField!!.name}" + } else { + "Group" + }, + ) + }, + leadingIcon = { + Icon( + Icons.AutoMirrored.Filled.List, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, + trailingIcon = + if (groupByField != null) { + { + IconButton( + onClick = { groupByField = null }, + modifier = Modifier.size(18.dp), + ) { + Icon( + Icons.Default.Close, + contentDescription = "Clear", + modifier = Modifier.size(16.dp), + ) + } + } + } else { + null + }, + ) + + // Filter button + FilterChip( + selected = filterField != null, + onClick = { showFilterDialog = true }, + label = { + Text( + if (filterField != null) { + "Filter: ${filterField!!.name}" + } else { + "Filter" + }, + ) + }, + leadingIcon = { + Icon( + Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, + trailingIcon = + if (filterField != null) { + { + IconButton( + onClick = { + filterField = null + filterValue = "" + }, + modifier = Modifier.size(18.dp), + ) { + Icon( + Icons.Default.Close, + contentDescription = "Clear", + modifier = Modifier.size(16.dp), + ) + } + } + } else { + null + }, + ) + + // Auto-resize button (chip style) + // Auto-fit cache keyed by fieldId with a compact signature based on timestamps and counts + val autoFitCache = remember { mutableStateMapOf>() } + FilterChip( + selected = false, + onClick = { + stableFields.forEach { field -> + // Build a compact signature: field name, field.updatedAt, item count, max updatedAt among values + var maxValUpdated = 0L + stableItems.forEach { item -> + val v = item.values.find { it.fieldId == field.id } + if (v != null && v.updatedAt > maxValUpdated) maxValUpdated = v.updatedAt + } + val signature = + AutoFitSignature( + name = field.name, + fieldUpdatedAt = field.updatedAt, + itemCount = stableItems.size, + maxValueUpdatedAt = maxValUpdated, + ) + + val cached = autoFitCache[field.id] + if (cached != null && cached.first == signature) { + // Use cached width + fieldWidths[field.id] = cached.second.dp + return@forEach + } + + // Measure header and max content width + val headerPx = + textMeasurer + .measure(AnnotatedString(field.name), style = headerTextStyle) + .size.width + .toFloat() + var maxContentPx = 0f + stableItems.forEach { itemWithValues -> + val v = itemWithValues.values.find { it.fieldId == field.id }?.value + val display = getDisplayTextForMeasure(field, v) + if (display.isNotEmpty()) { + val w = + textMeasurer + .measure(AnnotatedString(display), style = bodyTextStyle) + .size.width + .toFloat() + if (w > maxContentPx) maxContentPx = w + } + } + val widthDpValue = + with(density) { + val headerDp = headerPx.toDp() + 12.dp + 12.dp + 24.dp + 2.dp + val contentDp = maxContentPx.toDp() + 8.dp + 8.dp + 2.dp + val base = maxOf(headerDp, contentDp, 100.dp) + (base + 6.dp).value + } + fieldWidths[field.id] = widthDpValue.dp + autoFitCache[field.id] = signature to widthDpValue + } + prefs.setColumnWidths(listId, fieldWidths.mapValues { it.value.value }) + }, + label = { Text("Auto-fit") }, + leadingIcon = { + Icon( + Icons.Default.FitScreen, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, ) } - } else if (processedItems.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center, - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + + // 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 = "No items match the current filter", + text = stringResource(R.string.no_items), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, ) - Spacer(modifier = Modifier.height(8.dp)) - TextButton(onClick = { - filterField = null - filterValue = "" - }) { - Text("Clear Filter") + } + } 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 { - 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) + } 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 } - pendingScrollToBottom = false } - } - Column( - modifier = - Modifier - .fillMaxSize() - .horizontalScroll(horizontalScrollState), - ) { - // Fixed header (does not participate in vertical scroll) - Row( + Column( modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surface), - verticalAlignment = Alignment.CenterVertically, + .fillMaxSize() + .horizontalScroll(horizontalScrollState), ) { - 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()) - }, - ) + // 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()) + }, + ) + } + } + + // 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() + } } } - // 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() - } - } - } - - // 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 }, - ) + // 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 }, + ) + } } } } @@ -603,129 +606,129 @@ fun ListDetailScreen( } } } - } - if (showManageColumnsDialog) { - ManageColumnsDialog( - fields = stableFields, - onDismiss = { - showManageColumnsDialog = false - // Refresh alignments from current fields (DB-backed) - stableFields.forEach { field -> - val a = field.alignment.lowercase().ifBlank { columnAlignments[field.id] ?: "start" } - columnAlignments[field.id] = when (a) { - "center" -> "center" - "end", "right" -> "end" - else -> "start" + if (showManageColumnsDialog) { + ManageColumnsDialog( + fields = stableFields, + onDismiss = { + showManageColumnsDialog = false + // Refresh alignments from current fields (DB-backed) + stableFields.forEach { field -> + val a = field.alignment.lowercase().ifBlank { columnAlignments[field.id] ?: "start" } + columnAlignments[field.id] = + when (a) { + "center" -> "center" + "end", "right" -> "end" + else -> "start" + } } - } - }, - onAddField = { name, fieldType, fieldOptions -> - viewModel.addField(name, fieldType, fieldOptions) - }, - onUpdateField = { fieldId, name, fieldType, fieldOptions -> - viewModel.updateField(fieldId, name, fieldType, fieldOptions) - }, - onUpdateAlignment = { fieldId, alignment -> - viewModel.updateFieldAlignment(fieldId, alignment) - }, - onDeleteField = { fieldId -> - viewModel.deleteField(fieldId) - }, - onReorderFields = { reorderedFields -> - viewModel.reorderFields(reorderedFields) - }, - ) - } - - if (showAddItemDialog) { - AddItemDialog( - fields = stableFields, - onDismiss = { showAddItemDialog = false }, - onAdd = { fieldValues -> - viewModel.addItemWithValues(fieldValues) - showAddItemDialog = false - pendingScrollToBottom = true - }, - ) - } - - // Sort Dialog - if (showSortDialog) { - SortDialog( - fields = stableFields, - currentSortField = sortField, - currentSortAscending = sortAscending, - onDismiss = { showSortDialog = false }, - onApply = { newSortField, newSortAscending -> - sortField = newSortField - sortAscending = newSortAscending - showSortDialog = false - }, - ) - } - - // Group Dialog - if (showGroupDialog) { - GroupDialog( - fields = stableFields, - currentGroupByField = groupByField, - onDismiss = { showGroupDialog = false }, - onApply = { newGroupByField -> - groupByField = newGroupByField - showGroupDialog = false - }, - ) - } - - // Filter Dialog - if (showFilterDialog) { - FilterDialog( - fields = stableFields, - currentFilterField = filterField, - currentFilterValue = filterValue, - onDismiss = { showFilterDialog = false }, - onApply = { newFilterField, newFilterValue -> - filterField = newFilterField - filterValue = newFilterValue - showFilterDialog = false - }, - ) - } - - // Rename List Dialog - if (showRenameListDialog) { - list?.let { currentList -> - RenameListDialog( - currentName = currentList.name, - onDismiss = { showRenameListDialog = false }, - onRename = { newName -> - viewModel.renameList(newName) - showRenameListDialog = false + }, + onAddField = { name, fieldType, fieldOptions -> + viewModel.addField(name, fieldType, fieldOptions) + }, + onUpdateField = { fieldId, name, fieldType, fieldOptions -> + viewModel.updateField(fieldId, name, fieldType, fieldOptions) + }, + onUpdateAlignment = { fieldId, alignment -> + viewModel.updateFieldAlignment(fieldId, alignment) + }, + onDeleteField = { fieldId -> + viewModel.deleteField(fieldId) + }, + onReorderFields = { reorderedFields -> + viewModel.reorderFields(reorderedFields) }, ) } - } - itemToEdit?.let { itemWithValues -> - EditItemDialog( - fields = stableFields, - itemWithValues = itemWithValues, - onDismiss = { itemToEdit = null }, - onUpdate = { fieldValues -> - fieldValues.forEach { (valueId, newValue) -> - viewModel.updateItemValue(valueId, newValue) - } - itemToEdit = null - }, - onDelete = { - viewModel.deleteItem(itemWithValues.item.id) - itemToEdit = null - }, - ) + if (showAddItemDialog) { + AddItemDialog( + fields = stableFields, + onDismiss = { showAddItemDialog = false }, + onAdd = { fieldValues -> + viewModel.addItemWithValues(fieldValues) + showAddItemDialog = false + pendingScrollToBottom = true + }, + ) + } + + // Sort Dialog + if (showSortDialog) { + SortDialog( + fields = stableFields, + currentSortField = sortField, + currentSortAscending = sortAscending, + onDismiss = { showSortDialog = false }, + onApply = { newSortField, newSortAscending -> + sortField = newSortField + sortAscending = newSortAscending + showSortDialog = false + }, + ) + } + + // Group Dialog + if (showGroupDialog) { + GroupDialog( + fields = stableFields, + currentGroupByField = groupByField, + onDismiss = { showGroupDialog = false }, + onApply = { newGroupByField -> + groupByField = newGroupByField + showGroupDialog = false + }, + ) + } + + // Filter Dialog + if (showFilterDialog) { + FilterDialog( + fields = stableFields, + currentFilterField = filterField, + currentFilterValue = filterValue, + onDismiss = { showFilterDialog = false }, + onApply = { newFilterField, newFilterValue -> + filterField = newFilterField + filterValue = newFilterValue + showFilterDialog = false + }, + ) + } + + // Rename List Dialog + if (showRenameListDialog) { + list?.let { currentList -> + RenameListDialog( + currentName = currentList.name, + onDismiss = { showRenameListDialog = false }, + onRename = { newName -> + viewModel.renameList(newName) + showRenameListDialog = false + }, + ) + } + } + + itemToEdit?.let { itemWithValues -> + EditItemDialog( + fields = stableFields, + itemWithValues = itemWithValues, + onDismiss = { itemToEdit = null }, + onUpdate = { fieldValues -> + fieldValues.forEach { (valueId, newValue) -> + viewModel.updateItemValue(valueId, newValue) + } + itemToEdit = null + }, + onDelete = { + viewModel.deleteItem(itemWithValues.item.id) + itemToEdit = null + }, + ) + } + // END ListDetailScreen composable } - // END ListDetailScreen composable -} // Extra closing brace to properly terminate ListDetailScreen; previous edits removed one } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt index 0f4f7f1..f866744 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailViewModel.kt @@ -1,10 +1,10 @@ package com.collabtable.app.ui.screens import android.content.Context -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.lifecycle.Lifecycle import androidx.lifecycle.ProcessLifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import androidx.room.withTransaction import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.data.model.CollabList @@ -51,6 +51,7 @@ class ListDetailViewModel( var hasList = false var hasFields = false var hasItems = false + fun maybeLoaded() { if (hasFields && hasItems) { _isLoading.value = false @@ -128,10 +129,13 @@ class ListDetailViewModel( } private fun maybeNotifyListContentUpdated() { - val prefs = com.collabtable.app.data.preferences.PreferencesManager.getInstance(context) + val prefs = + com.collabtable.app.data.preferences.PreferencesManager + .getInstance(context) if (prefs.notifyListContentUpdated.value && !isInForeground()) { val name = _list.value?.name ?: "Table" - com.collabtable.app.notifications.NotificationHelper.showListContentUpdated(context, listId, name) + com.collabtable.app.notifications.NotificationHelper + .showListContentUpdated(context, listId, name) } } @@ -254,16 +258,20 @@ class ListDetailViewModel( } } - fun updateFieldAlignment(fieldId: String, alignment: String) { + fun updateFieldAlignment( + fieldId: String, + alignment: String, + ) { viewModelScope.launch { val field = database.fieldDao().getFieldById(fieldId) if (field != null) { val ts = System.currentTimeMillis() - val normalized = when (alignment.lowercase()) { - "center" -> "center" - "end", "right" -> "end" - else -> "start" - } + val normalized = + when (alignment.lowercase()) { + "center" -> "center" + "end", "right" -> "end" + else -> "start" + } database.withTransaction { database.fieldDao().updateField( field.copy( diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt index d36d8a3..7bea04a 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListsScreen.kt @@ -4,6 +4,9 @@ package com.collabtable.app.ui.screens import android.Manifest import android.content.pm.PackageManager +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -50,10 +53,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat -import android.os.Build import com.collabtable.app.R import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.data.model.CollabList @@ -117,7 +117,10 @@ fun ListsScreen( prefsLocal.setNotifyListAddedEnabled(granted) prefsLocal.setNotifyListEditedEnabled(granted) prefsLocal.setNotifyListRemovedEnabled(granted) - try { prefsLocal.setNotifyListContentUpdatedEnabled(granted) } catch (_: Throwable) {} + try { + prefsLocal.setNotifyListContentUpdatedEnabled(granted) + } catch (_: Throwable) { + } } ConnectionStatusAction( diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt index 1274382..a9d7fc3 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ServerSetupScreen.kt @@ -1,8 +1,8 @@ package com.collabtable.app.ui.screens -import android.os.Build import android.Manifest import android.content.pm.PackageManager +import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Arrangement @@ -46,8 +46,8 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.collabtable.app.data.preferences.PreferencesManager import androidx.core.content.ContextCompat +import com.collabtable.app.data.preferences.PreferencesManager @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -64,6 +64,7 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) { val validationError by viewModel.validationError.collectAsState() var completed by remember { mutableStateOf(false) } + fun finishOnce() { if (!completed) { completed = true @@ -99,7 +100,10 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) { preferencesManager.setNotifyListAddedEnabled(true) preferencesManager.setNotifyListEditedEnabled(true) preferencesManager.setNotifyListRemovedEnabled(true) - try { preferencesManager.setNotifyListContentUpdatedEnabled(true) } catch (_: Throwable) {} + try { + preferencesManager.setNotifyListContentUpdatedEnabled(true) + } catch (_: Throwable) { + } finishOnce() } else { // Request permission; callback will complete @@ -109,7 +113,10 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) { preferencesManager.setNotifyListAddedEnabled(true) preferencesManager.setNotifyListEditedEnabled(true) preferencesManager.setNotifyListRemovedEnabled(true) - try { preferencesManager.setNotifyListContentUpdatedEnabled(true) } catch (_: Throwable) {} + try { + preferencesManager.setNotifyListContentUpdatedEnabled(true) + } catch (_: Throwable) { + } finishOnce() } } diff --git a/CollabTableServer/src/db.ts b/CollabTableServer/src/db.ts index 4ce2ef9..9aafd9b 100644 --- a/CollabTableServer/src/db.ts +++ b/CollabTableServer/src/db.ts @@ -10,9 +10,6 @@ type Param = any; type ExecResult = { changes: number }; -type TxClient = { - query: (text: string, params?: any[]) => Promise>; -}; interface DBAdapter { queryAll(sql: string, params?: Param[]): Promise; @@ -66,7 +63,6 @@ class SqliteAdapter implements DBAdapter { fieldType TEXT NOT NULL, fieldOptions TEXT, alignment TEXT NOT NULL DEFAULT 'start', - alignment TEXT NOT NULL DEFAULT 'start', listId TEXT NOT NULL, "order" INTEGER NOT NULL, createdAt INTEGER NOT NULL, @@ -222,8 +218,7 @@ class PostgresAdapter implements DBAdapter { private async runQuery(sql: string, params: Param[] = [], client?: PoolClient): Promise> { // Replace backticks and convert ? to $1..$n const text = convertQMarksToPg(replaceQuotedIdentifiers(sql)); - const runner = client || this.pool; - // @ts-ignore - Pool and PoolClient share query signature + const runner: Pool | PoolClient = client || this.pool; return runner.query(text, params); }