fix: correct indentation and formatting in PreferencesManager and ListDetailScreen for improved readability
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
+434
-431
@@ -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
|
||||
}
|
||||
|
||||
+18
-10
@@ -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(
|
||||
|
||||
+11
-4
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user