feat: Add rename functionality for lists with synchronization
This commit is contained in:
+617
-89
@@ -67,7 +67,10 @@ fun ListDetailScreen(
|
|||||||
var showManageColumnsDialog by remember { mutableStateOf(false) }
|
var showManageColumnsDialog by remember { mutableStateOf(false) }
|
||||||
var showAddItemDialog by remember { mutableStateOf(false) }
|
var showAddItemDialog by remember { mutableStateOf(false) }
|
||||||
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
|
var itemToEdit by remember { mutableStateOf<ItemWithValues?>(null) }
|
||||||
var showFilterSortDialog by remember { mutableStateOf(false) }
|
var showSortDialog by remember { mutableStateOf(false) }
|
||||||
|
var showGroupDialog by remember { mutableStateOf(false) }
|
||||||
|
var showFilterDialog by remember { mutableStateOf(false) }
|
||||||
|
var showRenameListDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Filter/Sort state
|
// Filter/Sort state
|
||||||
var sortField by remember { mutableStateOf<Field?>(null) }
|
var sortField by remember { mutableStateOf<Field?>(null) }
|
||||||
@@ -131,7 +134,20 @@ fun ListDetailScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(list?.name ?: "") },
|
title = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.clickable { showRenameListDialog = true },
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(list?.name ?: "")
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = "Rename",
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||||
@@ -183,64 +199,125 @@ fun ListDetailScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
) {
|
) {
|
||||||
// Filter/Sort Toolbar
|
// Filter/Sort/Group Controls - Always visible above table
|
||||||
if (sortField != null || filterField != null || groupByField != null) {
|
Row(
|
||||||
Row(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.fillMaxWidth()
|
||||||
.fillMaxWidth()
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
.padding(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
verticalAlignment = Alignment.CenterVertically
|
||||||
verticalAlignment = Alignment.CenterVertically
|
) {
|
||||||
) {
|
// Sort button
|
||||||
if (sortField != null) {
|
FilterChip(
|
||||||
FilterChip(
|
selected = sortField != null,
|
||||||
selected = true,
|
onClick = { showSortDialog = true },
|
||||||
onClick = {
|
label = {
|
||||||
sortField = null
|
Text(
|
||||||
sortAscending = true
|
if (sortField != null)
|
||||||
},
|
"Sort: ${sortField!!.name} ${if (sortAscending) "↑" else "↓"}"
|
||||||
label = {
|
else
|
||||||
Text("Sort: ${sortField!!.name} ${if (sortAscending) "↑" else "↓"}")
|
"Sort"
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Icon(Icons.Default.Close, contentDescription = "Remove",
|
|
||||||
modifier = Modifier.size(16.dp))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
leadingIcon = {
|
||||||
if (groupByField != null) {
|
Icon(
|
||||||
FilterChip(
|
Icons.Default.Search,
|
||||||
selected = true,
|
contentDescription = null,
|
||||||
onClick = { groupByField = null },
|
modifier = Modifier.size(18.dp)
|
||||||
label = { Text("Group: ${groupByField!!.name}") },
|
|
||||||
trailingIcon = {
|
|
||||||
Icon(Icons.Default.Close, contentDescription = "Remove",
|
|
||||||
modifier = Modifier.size(16.dp))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
trailingIcon = if (sortField != null) {
|
||||||
if (filterField != null) {
|
{
|
||||||
FilterChip(
|
IconButton(
|
||||||
selected = true,
|
onClick = {
|
||||||
onClick = {
|
sortField = null
|
||||||
filterField = null
|
sortAscending = true
|
||||||
filterValue = ""
|
},
|
||||||
},
|
modifier = Modifier.size(18.dp)
|
||||||
label = { Text("Filter: ${filterField!!.name} = \"$filterValue\"") },
|
) {
|
||||||
trailingIcon = {
|
Icon(
|
||||||
Icon(Icons.Default.Close, contentDescription = "Remove",
|
Icons.Default.Close,
|
||||||
modifier = Modifier.size(16.dp))
|
contentDescription = "Clear",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
} else null
|
||||||
|
)
|
||||||
|
|
||||||
IconButton(onClick = { showFilterSortDialog = true }) {
|
// Group button
|
||||||
Icon(Icons.Default.Settings, contentDescription = "Configure")
|
FilterChip(
|
||||||
}
|
selected = groupByField != null,
|
||||||
}
|
onClick = { showGroupDialog = true },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
if (groupByField != null)
|
||||||
|
"Group: ${groupByField!!.name}"
|
||||||
|
else
|
||||||
|
"Group"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Field headers with long-press to delete and resize handles
|
// Field headers with long-press to delete and resize handles
|
||||||
@@ -261,20 +338,6 @@ fun ListDetailScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter/Sort button
|
|
||||||
if (items.isNotEmpty() && sortField == null && filterField == null && groupByField == null) {
|
|
||||||
IconButton(
|
|
||||||
onClick = { showFilterSortDialog = true },
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Settings,
|
|
||||||
contentDescription = "Filter/Sort",
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Items list with synchronized scrolling
|
// Items list with synchronized scrolling
|
||||||
@@ -378,34 +441,63 @@ fun ListDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showFilterSortDialog) {
|
// Sort Dialog
|
||||||
FilterSortDialog(
|
if (showSortDialog) {
|
||||||
|
SortDialog(
|
||||||
fields = fields,
|
fields = fields,
|
||||||
currentSortField = sortField,
|
currentSortField = sortField,
|
||||||
currentSortAscending = sortAscending,
|
currentSortAscending = sortAscending,
|
||||||
currentGroupByField = groupByField,
|
onDismiss = { showSortDialog = false },
|
||||||
currentFilterField = filterField,
|
onApply = { newSortField, newSortAscending ->
|
||||||
currentFilterValue = filterValue,
|
|
||||||
onDismiss = { showFilterSortDialog = false },
|
|
||||||
onApply = { newSortField, newSortAscending, newGroupByField, newFilterField, newFilterValue ->
|
|
||||||
sortField = newSortField
|
sortField = newSortField
|
||||||
sortAscending = newSortAscending
|
sortAscending = newSortAscending
|
||||||
groupByField = newGroupByField
|
showSortDialog = false
|
||||||
filterField = newFilterField
|
|
||||||
filterValue = newFilterValue
|
|
||||||
showFilterSortDialog = false
|
|
||||||
},
|
|
||||||
onClearAll = {
|
|
||||||
sortField = null
|
|
||||||
sortAscending = true
|
|
||||||
groupByField = null
|
|
||||||
filterField = null
|
|
||||||
filterValue = ""
|
|
||||||
showFilterSortDialog = false
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Group Dialog
|
||||||
|
if (showGroupDialog) {
|
||||||
|
GroupDialog(
|
||||||
|
fields = fields,
|
||||||
|
currentGroupByField = groupByField,
|
||||||
|
onDismiss = { showGroupDialog = false },
|
||||||
|
onApply = { newGroupByField ->
|
||||||
|
groupByField = newGroupByField
|
||||||
|
showGroupDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Dialog
|
||||||
|
if (showFilterDialog) {
|
||||||
|
FilterDialog(
|
||||||
|
fields = fields,
|
||||||
|
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 ->
|
itemToEdit?.let { itemWithValues ->
|
||||||
EditItemDialog(
|
EditItemDialog(
|
||||||
fields = fields,
|
fields = fields,
|
||||||
@@ -1990,3 +2082,439 @@ private fun FilterSortDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun SortDialog(
|
||||||
|
fields: List<Field>,
|
||||||
|
currentSortField: Field?,
|
||||||
|
currentSortAscending: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApply: (sortField: Field?, sortAscending: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
var sortField by remember { mutableStateOf(currentSortField) }
|
||||||
|
var sortAscending by remember { mutableStateOf(currentSortAscending) }
|
||||||
|
var sortFieldExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Sort Items",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Sort Field Selection
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = sortFieldExpanded,
|
||||||
|
onExpandedChange = { sortFieldExpanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = sortField?.name ?: "None",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Sort Field") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = sortFieldExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = sortFieldExpanded,
|
||||||
|
onDismissRequest = { sortFieldExpanded = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("None") },
|
||||||
|
onClick = {
|
||||||
|
sortField = null
|
||||||
|
sortFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fields.forEach { field ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(field.name) },
|
||||||
|
onClick = {
|
||||||
|
sortField = field
|
||||||
|
sortFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort Order Selection
|
||||||
|
if (sortField != null) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "Order",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = sortAscending,
|
||||||
|
onClick = { sortAscending = true },
|
||||||
|
label = { Text("Ascending ↑") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
leadingIcon = if (sortAscending) {
|
||||||
|
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
FilterChip(
|
||||||
|
selected = !sortAscending,
|
||||||
|
onClick = { sortAscending = false },
|
||||||
|
label = { Text("Descending ↓") },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
leadingIcon = if (!sortAscending) {
|
||||||
|
{ Icon(Icons.Default.Check, contentDescription = null) }
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { onApply(sortField, sortAscending) }
|
||||||
|
) {
|
||||||
|
Text("Apply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun GroupDialog(
|
||||||
|
fields: List<Field>,
|
||||||
|
currentGroupByField: Field?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApply: (groupByField: Field?) -> Unit
|
||||||
|
) {
|
||||||
|
var groupByField by remember { mutableStateOf(currentGroupByField) }
|
||||||
|
var groupByFieldExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Group Items",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Group items by a field to organize them into categories",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Group By Field Selection
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = groupByFieldExpanded,
|
||||||
|
onExpandedChange = { groupByFieldExpanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = groupByField?.name ?: "None",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Group By Field") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = groupByFieldExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = groupByFieldExpanded,
|
||||||
|
onDismissRequest = { groupByFieldExpanded = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("None") },
|
||||||
|
onClick = {
|
||||||
|
groupByField = null
|
||||||
|
groupByFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fields.forEach { field ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(field.name) },
|
||||||
|
onClick = {
|
||||||
|
groupByField = field
|
||||||
|
groupByFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { onApply(groupByField) }
|
||||||
|
) {
|
||||||
|
Text("Apply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FilterDialog(
|
||||||
|
fields: List<Field>,
|
||||||
|
currentFilterField: Field?,
|
||||||
|
currentFilterValue: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApply: (filterField: Field?, filterValue: String) -> Unit
|
||||||
|
) {
|
||||||
|
var filterField by remember { mutableStateOf(currentFilterField) }
|
||||||
|
var filterValue by remember { mutableStateOf(currentFilterValue) }
|
||||||
|
var filterFieldExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.wrapContentHeight(),
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Filter Items",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Show only items that contain specific text in a field",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
// Filter Field Selection
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = filterFieldExpanded,
|
||||||
|
onExpandedChange = { filterFieldExpanded = it }
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = filterField?.name ?: "None",
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Filter Field") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = filterFieldExpanded) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.menuAnchor()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = filterFieldExpanded,
|
||||||
|
onDismissRequest = { filterFieldExpanded = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("None") },
|
||||||
|
onClick = {
|
||||||
|
filterField = null
|
||||||
|
filterFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
fields.forEach { field ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(field.name) },
|
||||||
|
onClick = {
|
||||||
|
filterField = field
|
||||||
|
filterFieldExpanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Value Input
|
||||||
|
if (filterField != null) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = filterValue,
|
||||||
|
onValueChange = { filterValue = it },
|
||||||
|
label = { Text("Filter Value") },
|
||||||
|
placeholder = { Text("Enter text to filter...") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Action Buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
OutlinedButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(
|
||||||
|
onClick = { onApply(filterField, filterValue) },
|
||||||
|
enabled = filterField == null || filterValue.isNotBlank()
|
||||||
|
) {
|
||||||
|
Text("Apply")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenameListDialog(
|
||||||
|
currentName: String,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onRename: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var newName by remember { mutableStateOf(currentName) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Rename List",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = "Enter a new name for this list",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = newName,
|
||||||
|
onValueChange = { newName = it },
|
||||||
|
label = { Text("List Name") },
|
||||||
|
placeholder = { Text("Enter list name...") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Button(
|
||||||
|
onClick = { onRename(newName) },
|
||||||
|
enabled = newName.isNotBlank() && newName.trim() != currentName
|
||||||
|
) {
|
||||||
|
Text("Rename")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
OutlinedButton(onClick = onDismiss) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+15
@@ -61,6 +61,21 @@ class ListDetailViewModel(
|
|||||||
syncRepository.performSync()
|
syncRepository.performSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun renameList(newName: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val currentList = _list.value
|
||||||
|
if (currentList != null && newName.isNotBlank()) {
|
||||||
|
database.listDao().updateList(
|
||||||
|
currentList.copy(
|
||||||
|
name = newName.trim(),
|
||||||
|
updatedAt = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
performSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addField(name: String, fieldType: String = "STRING", fieldOptions: String = "") {
|
fun addField(name: String, fieldType: String = "STRING", fieldOptions: String = "") {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
|
|||||||
Reference in New Issue
Block a user