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_ADDED = "notify_list_added"
private const val KEY_NOTIFY_LIST_EDITED = "notify_list_edited" 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_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 KEY_LAST_LIST_NOTIFY_CHECK_TS = "last_list_notify_check_ts"
private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId private const val COLUMN_WIDTHS_PREFIX = "column_widths_" // + listId
private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId private const val COLUMN_ALIGN_PREFIX = "column_align_" // + listId
@@ -166,11 +166,12 @@ fun ListDetailScreen(
val savedAlign = prefs.getColumnAlignments(listId) val savedAlign = prefs.getColumnAlignments(listId)
stableFields.forEach { field -> stableFields.forEach { field ->
val raw = (field.alignment.ifBlank { savedAlign[field.id] ?: "start" }).lowercase() val raw = (field.alignment.ifBlank { savedAlign[field.id] ?: "start" }).lowercase()
columnAlignments[field.id] = when (raw) { columnAlignments[field.id] =
"center" -> "center" when (raw) {
"end", "right" -> "end" "center" -> "center"
else -> "start" "end", "right" -> "end"
} else -> "start"
}
} }
} }
@@ -269,333 +270,335 @@ fun ListDetailScreen(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
) { ) {
// Filter/Sort/Group Controls - Always visible above table // Filter/Sort/Group Controls - Always visible above table
Row( Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState())
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically, 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,
) { ) {
Text( // Sort button
text = stringResource(R.string.no_items), FilterChip(
style = MaterialTheme.typography.bodyLarge, selected = sortField != null,
color = MaterialTheme.colorScheme.onSurfaceVariant, 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( // Header will be rendered as a stickyHeader inside the LazyColumn below
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center, // Items list with synchronized scrolling
) { if (stableItems.isEmpty()) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text( Text(
text = "No items match the current filter", text = stringResource(R.string.no_items),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Spacer(modifier = Modifier.height(8.dp)) }
TextButton(onClick = { } else if (processedItems.isEmpty()) {
filterField = null Box(
filterValue = "" modifier = Modifier.fillMaxSize(),
}) { contentAlignment = Alignment.Center,
Text("Clear Filter") ) {
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 {
} else { val listState = rememberLazyListState()
val listState = rememberLazyListState() // If a new item was just added, scroll to bottom after composition
// If a new item was just added, scroll to bottom after composition LaunchedEffect(processedItems.size, pendingScrollToBottom) {
LaunchedEffect(processedItems.size, pendingScrollToBottom) { if (pendingScrollToBottom) {
if (pendingScrollToBottom) { // Scroll to the last real item index
// Scroll to the last real item index val lastIndex = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0)
val lastIndex = (listState.layoutInfo.totalItemsCount - 1).coerceAtLeast(0) try {
try { listState.animateScrollToItem(lastIndex)
listState.animateScrollToItem(lastIndex) } catch (_: Exception) {
} catch (_: Exception) { listState.scrollToItem(lastIndex)
listState.scrollToItem(lastIndex) }
pendingScrollToBottom = false
} }
pendingScrollToBottom = false
} }
} Column(
Column(
modifier =
Modifier
.fillMaxSize()
.horizontalScroll(horizontalScrollState),
) {
// Fixed header (does not participate in vertical scroll)
Row(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxSize()
.background(MaterialTheme.colorScheme.surface), .horizontalScroll(horizontalScrollState),
verticalAlignment = Alignment.CenterVertically,
) { ) {
stableFields.forEach { field -> // Fixed header (does not participate in vertical scroll)
key(field.id) { Row(
FieldHeader( modifier =
field = field, Modifier
width = fieldWidths[field.id] ?: 150.dp, .fillMaxWidth()
onWidthChange = { delta -> .background(MaterialTheme.colorScheme.surface),
val currentWidth = fieldWidths[field.id] ?: 150.dp verticalAlignment = Alignment.CenterVertically,
val newWidth = (currentWidth.value + delta).coerceAtLeast(100f) ) {
fieldWidths[field.id] = newWidth.dp stableFields.forEach { field ->
prefs.setColumnWidths( key(field.id) {
listId, FieldHeader(
fieldWidths.mapValues { it.value.value }, field = field,
) width = fieldWidths[field.id] ?: 150.dp,
}, onWidthChange = { delta ->
scrollState = horizontalScrollState, val currentWidth = fieldWidths[field.id] ?: 150.dp
isLast = (field.id == stableFields.lastOrNull()?.id), val newWidth = (currentWidth.value + delta).coerceAtLeast(100f)
onHeaderClick = { showManageColumnsDialog = true }, fieldWidths[field.id] = newWidth.dp
alignment = columnAlignments[field.id] ?: "start", prefs.setColumnWidths(
onAlignmentChange = { newAlign -> listId,
// Update local state and persist via ViewModel + cache to prefs for backward-compat fieldWidths.mapValues { it.value.value },
columnAlignments[field.id] = newAlign )
viewModel.updateFieldAlignment(field.id, newAlign) },
prefs.setColumnAlignments(listId, columnAlignments.toMap()) 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 // Items list below the fixed header
if (isLoading && stableFields.isEmpty() && stableItems.isEmpty()) { LazyColumn(
Box( modifier =
modifier = Modifier.fillMaxSize(), Modifier
contentAlignment = Alignment.Center, .fillMaxSize(),
) { state = listState,
androidx.compose.material3.CircularProgressIndicator() contentPadding = PaddingValues(0.dp),
} ) {
} groupedItems.entries.forEach { (_, groupItems) ->
} // Show items in the group
items(
// Items list below the fixed header items = groupItems,
LazyColumn( key = { it.item.id },
modifier = Modifier contentType = { "row" },
.fillMaxSize(), ) { itemWithValues ->
state = listState, ItemRow(
contentPadding = PaddingValues(0.dp), fields = stableFields,
) { fieldWidths = fieldWidths,
groupedItems.entries.forEach { (_, groupItems) -> fieldAlignments = columnAlignments,
// Show items in the group itemWithValues = itemWithValues,
items( onClick = { itemToEdit = itemWithValues },
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) { if (showManageColumnsDialog) {
ManageColumnsDialog( ManageColumnsDialog(
fields = stableFields, fields = stableFields,
onDismiss = { onDismiss = {
showManageColumnsDialog = false showManageColumnsDialog = false
// Refresh alignments from current fields (DB-backed) // Refresh alignments from current fields (DB-backed)
stableFields.forEach { field -> stableFields.forEach { field ->
val a = field.alignment.lowercase().ifBlank { columnAlignments[field.id] ?: "start" } val a = field.alignment.lowercase().ifBlank { columnAlignments[field.id] ?: "start" }
columnAlignments[field.id] = when (a) { columnAlignments[field.id] =
"center" -> "center" when (a) {
"end", "right" -> "end" "center" -> "center"
else -> "start" "end", "right" -> "end"
else -> "start"
}
} }
} },
}, onAddField = { name, fieldType, fieldOptions ->
onAddField = { name, fieldType, fieldOptions -> viewModel.addField(name, fieldType, fieldOptions)
viewModel.addField(name, fieldType, fieldOptions) },
}, onUpdateField = { fieldId, name, fieldType, fieldOptions ->
onUpdateField = { fieldId, name, fieldType, fieldOptions -> viewModel.updateField(fieldId, name, fieldType, fieldOptions)
viewModel.updateField(fieldId, name, fieldType, fieldOptions) },
}, onUpdateAlignment = { fieldId, alignment ->
onUpdateAlignment = { fieldId, alignment -> viewModel.updateFieldAlignment(fieldId, alignment)
viewModel.updateFieldAlignment(fieldId, alignment) },
}, onDeleteField = { fieldId ->
onDeleteField = { fieldId -> viewModel.deleteField(fieldId)
viewModel.deleteField(fieldId) },
}, onReorderFields = { reorderedFields ->
onReorderFields = { reorderedFields -> viewModel.reorderFields(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
}, },
) )
} }
}
itemToEdit?.let { itemWithValues -> if (showAddItemDialog) {
EditItemDialog( AddItemDialog(
fields = stableFields, fields = stableFields,
itemWithValues = itemWithValues, onDismiss = { showAddItemDialog = false },
onDismiss = { itemToEdit = null }, onAdd = { fieldValues ->
onUpdate = { fieldValues -> viewModel.addItemWithValues(fieldValues)
fieldValues.forEach { (valueId, newValue) -> showAddItemDialog = false
viewModel.updateItemValue(valueId, newValue) pendingScrollToBottom = true
} },
itemToEdit = null )
}, }
onDelete = {
viewModel.deleteItem(itemWithValues.item.id) // Sort Dialog
itemToEdit = null 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 // Extra closing brace to properly terminate ListDetailScreen; previous edits removed one
} }
@@ -1,10 +1,10 @@
package com.collabtable.app.ui.screens package com.collabtable.app.ui.screens
import android.content.Context import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.room.withTransaction import androidx.room.withTransaction
import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.data.database.CollabTableDatabase
import com.collabtable.app.data.model.CollabList import com.collabtable.app.data.model.CollabList
@@ -51,6 +51,7 @@ class ListDetailViewModel(
var hasList = false var hasList = false
var hasFields = false var hasFields = false
var hasItems = false var hasItems = false
fun maybeLoaded() { fun maybeLoaded() {
if (hasFields && hasItems) { if (hasFields && hasItems) {
_isLoading.value = false _isLoading.value = false
@@ -128,10 +129,13 @@ class ListDetailViewModel(
} }
private fun maybeNotifyListContentUpdated() { 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()) { if (prefs.notifyListContentUpdated.value && !isInForeground()) {
val name = _list.value?.name ?: "Table" 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 { viewModelScope.launch {
val field = database.fieldDao().getFieldById(fieldId) val field = database.fieldDao().getFieldById(fieldId)
if (field != null) { if (field != null) {
val ts = System.currentTimeMillis() val ts = System.currentTimeMillis()
val normalized = when (alignment.lowercase()) { val normalized =
"center" -> "center" when (alignment.lowercase()) {
"end", "right" -> "end" "center" -> "center"
else -> "start" "end", "right" -> "end"
} else -> "start"
}
database.withTransaction { database.withTransaction {
database.fieldDao().updateField( database.fieldDao().updateField(
field.copy( field.copy(
@@ -4,6 +4,9 @@ package com.collabtable.app.ui.screens
import android.Manifest import android.Manifest
import android.content.pm.PackageManager 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import android.os.Build
import com.collabtable.app.R import com.collabtable.app.R
import com.collabtable.app.data.database.CollabTableDatabase import com.collabtable.app.data.database.CollabTableDatabase
import com.collabtable.app.data.model.CollabList import com.collabtable.app.data.model.CollabList
@@ -117,7 +117,10 @@ fun ListsScreen(
prefsLocal.setNotifyListAddedEnabled(granted) prefsLocal.setNotifyListAddedEnabled(granted)
prefsLocal.setNotifyListEditedEnabled(granted) prefsLocal.setNotifyListEditedEnabled(granted)
prefsLocal.setNotifyListRemovedEnabled(granted) prefsLocal.setNotifyListRemovedEnabled(granted)
try { prefsLocal.setNotifyListContentUpdatedEnabled(granted) } catch (_: Throwable) {} try {
prefsLocal.setNotifyListContentUpdatedEnabled(granted)
} catch (_: Throwable) {
}
} }
ConnectionStatusAction( ConnectionStatusAction(
@@ -1,8 +1,8 @@
package com.collabtable.app.ui.screens package com.collabtable.app.ui.screens
import android.os.Build
import android.Manifest import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement 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.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.collabtable.app.data.preferences.PreferencesManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.collabtable.app.data.preferences.PreferencesManager
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -64,6 +64,7 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) {
val validationError by viewModel.validationError.collectAsState() val validationError by viewModel.validationError.collectAsState()
var completed by remember { mutableStateOf(false) } var completed by remember { mutableStateOf(false) }
fun finishOnce() { fun finishOnce() {
if (!completed) { if (!completed) {
completed = true completed = true
@@ -99,7 +100,10 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) {
preferencesManager.setNotifyListAddedEnabled(true) preferencesManager.setNotifyListAddedEnabled(true)
preferencesManager.setNotifyListEditedEnabled(true) preferencesManager.setNotifyListEditedEnabled(true)
preferencesManager.setNotifyListRemovedEnabled(true) preferencesManager.setNotifyListRemovedEnabled(true)
try { preferencesManager.setNotifyListContentUpdatedEnabled(true) } catch (_: Throwable) {} try {
preferencesManager.setNotifyListContentUpdatedEnabled(true)
} catch (_: Throwable) {
}
finishOnce() finishOnce()
} else { } else {
// Request permission; callback will complete // Request permission; callback will complete
@@ -109,7 +113,10 @@ fun ServerSetupScreen(onSetupComplete: () -> Unit) {
preferencesManager.setNotifyListAddedEnabled(true) preferencesManager.setNotifyListAddedEnabled(true)
preferencesManager.setNotifyListEditedEnabled(true) preferencesManager.setNotifyListEditedEnabled(true)
preferencesManager.setNotifyListRemovedEnabled(true) preferencesManager.setNotifyListRemovedEnabled(true)
try { preferencesManager.setNotifyListContentUpdatedEnabled(true) } catch (_: Throwable) {} try {
preferencesManager.setNotifyListContentUpdatedEnabled(true)
} catch (_: Throwable) {
}
finishOnce() finishOnce()
} }
} }
+1 -6
View File
@@ -10,9 +10,6 @@ type Param = any;
type ExecResult = { changes: number }; type ExecResult = { changes: number };
type TxClient = {
query: (text: string, params?: any[]) => Promise<QueryResult<any>>;
};
interface DBAdapter { interface DBAdapter {
queryAll(sql: string, params?: Param[]): Promise<any[]>; queryAll(sql: string, params?: Param[]): Promise<any[]>;
@@ -66,7 +63,6 @@ class SqliteAdapter implements DBAdapter {
fieldType TEXT NOT NULL, fieldType TEXT NOT NULL,
fieldOptions TEXT, fieldOptions TEXT,
alignment TEXT NOT NULL DEFAULT 'start', alignment TEXT NOT NULL DEFAULT 'start',
alignment TEXT NOT NULL DEFAULT 'start',
listId TEXT NOT NULL, listId TEXT NOT NULL,
"order" INTEGER NOT NULL, "order" INTEGER NOT NULL,
createdAt 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>> { private async runQuery(sql: string, params: Param[] = [], client?: PoolClient): Promise<QueryResult<any>> {
// Replace backticks and convert ? to $1..$n // Replace backticks and convert ? to $1..$n
const text = convertQMarksToPg(replaceQuotedIdentifiers(sql)); const text = convertQMarksToPg(replaceQuotedIdentifiers(sql));
const runner = client || this.pool; const runner: Pool | PoolClient = client || this.pool;
// @ts-ignore - Pool and PoolClient share query signature
return runner.query(text, params); return runner.query(text, params);
} }