fix: correct indentation and formatting in PreferencesManager and ListDetailScreen for improved readability

This commit is contained in:
2025-11-12 00:22:16 +01:00
parent f3181116d4
commit a433dec4cf
6 changed files with 472 additions and 456 deletions
@@ -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
@@ -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<String, Pair<AutoFitSignature, Float>>() }
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<String, Pair<AutoFitSignature, Float>>() }
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
}
@@ -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(
@@ -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(
@@ -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()
}
}
+1 -6
View File
@@ -10,9 +10,6 @@ type Param = any;
type ExecResult = { changes: number };
type TxClient = {
query: (text: string, params?: any[]) => Promise<QueryResult<any>>;
};
interface DBAdapter {
queryAll(sql: string, params?: Param[]): Promise<any[]>;
@@ -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<QueryResult<any>> {
// 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);
}