Fix page selection from page list, add library filter and sort

Page selection fix:
- Pass selected page ID as a route parameter (editor/{notebookId}?pageId=X)
  instead of savedStateHandle, which was unreliable
- Pop to notebook list then navigate to editor with explicit page ID
- Eliminates stale page selection bug

Library filter/sort:
- Text filter field filters notebooks by name (case-insensitive)
- Sort dropdown: Name, Last Edited, Last Opened
- Clicking the current sort field toggles asc/desc direction
- Defaults to Last Edited descending
- Shows "No matches" when filter has no results vs "No notebooks yet"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 19:08:32 -07:00
parent ffd9b05ab1
commit 1e13361e7e
2 changed files with 125 additions and 41 deletions

View File

@@ -22,10 +22,12 @@ import net.metacircular.engpad.ui.pages.PageListScreen
object Routes {
const val NOTEBOOKS = "notebooks"
const val EDITOR = "editor/{notebookId}"
const val EDITOR = "editor/{notebookId}?pageId={pageId}"
const val PAGES = "pages/{notebookId}"
fun editor(notebookId: Long) = "editor/$notebookId"
fun editor(notebookId: Long, pageId: Long = 0) =
if (pageId > 0) "editor/$notebookId?pageId=$pageId"
else "editor/$notebookId"
fun pages(notebookId: Long) = "pages/$notebookId"
}
@@ -65,9 +67,13 @@ fun EngPadNavGraph(
}
composable(
route = Routes.EDITOR,
arguments = listOf(navArgument("notebookId") { type = NavType.LongType }),
arguments = listOf(
navArgument("notebookId") { type = NavType.LongType },
navArgument("pageId") { type = NavType.LongType; defaultValue = 0L },
),
) { backStackEntry ->
val notebookId = backStackEntry.arguments?.getLong("notebookId") ?: return@composable
val requestedPageId = backStackEntry.arguments?.getLong("pageId") ?: 0L
val notebookRepo = remember {
NotebookRepository(database.notebookDao(), database.pageDao())
}
@@ -78,27 +84,23 @@ fun EngPadNavGraph(
var pageSize by remember { mutableStateOf<PageSize?>(null) }
var initialPageId by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(notebookId) {
LaunchedEffect(notebookId, requestedPageId) {
onNotebookOpened(notebookId)
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
pageSize = PageSize.fromString(notebook.pageSize)
// Use last page or fall back to first page
val pages = pageRepo.getPagesList(notebookId)
initialPageId = if (notebook.lastPageId > 0 && pages.any { it.id == notebook.lastPageId }) {
initialPageId = when {
// Explicit page requested (from page list click)
requestedPageId > 0 && pages.any { it.id == requestedPageId } ->
requestedPageId
// Resume last visited page
notebook.lastPageId > 0 && pages.any { it.id == notebook.lastPageId } ->
notebook.lastPageId
} else {
pages.firstOrNull()?.id ?: return@LaunchedEffect
// Fall back to first page
else -> pages.firstOrNull()?.id ?: return@LaunchedEffect
}
}
// Check for page selection from the page list screen
val savedState = backStackEntry.savedStateHandle
val selectedPageId = savedState.get<Long>("selectedPageId")
if (selectedPageId != null && selectedPageId > 0) {
initialPageId = selectedPageId
savedState.remove<Long>("selectedPageId")
}
val ps = pageSize
val pid = initialPageId
if (ps != null && pid != null) {
@@ -150,10 +152,10 @@ fun EngPadNavGraph(
database = database,
onPageClick = { pageId, _ ->
onViewAllPagesChanged(false)
navController.previousBackStackEntry
?.savedStateHandle
?.set("selectedPageId", pageId)
navController.popBackStack()
// Navigate to editor with explicit page ID
// Pop both the page list and the old editor, then push new editor
navController.popBackStack(Routes.NOTEBOOKS, false)
navController.navigate(Routes.editor(notebookId, pageId))
},
onClose = {
onNotebookClosed()

View File

@@ -3,16 +3,21 @@ package net.metacircular.engpad.ui.notebooks
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme
@@ -41,6 +46,14 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
private enum class SortField(val label: String) {
NAME("Name"),
LAST_EDITED("Last Edited"),
LAST_OPENED("Last Opened"),
}
private enum class SortDir { ASC, DESC }
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NotebookListScreen(
@@ -57,6 +70,23 @@ fun NotebookListScreen(
var showCreateDialog by remember { mutableStateOf(false) }
var notebookToDelete by remember { mutableStateOf<Notebook?>(null) }
var filterText by remember { mutableStateOf("") }
var sortField by remember { mutableStateOf(SortField.LAST_EDITED) }
var sortDir by remember { mutableStateOf(SortDir.DESC) }
// Apply filter and sort
val displayedNotebooks = remember(notebooks, filterText, sortField, sortDir) {
var list = if (filterText.isBlank()) notebooks
else notebooks.filter { it.title.contains(filterText, ignoreCase = true) }
list = when (sortField) {
SortField.NAME -> list.sortedBy { it.title.lowercase() }
SortField.LAST_EDITED -> list.sortedBy { it.updatedAt }
SortField.LAST_OPENED -> list.sortedBy { it.updatedAt }
}
if (sortDir == SortDir.DESC) list.reversed() else list
}
Scaffold(
topBar = {
TopAppBar(title = { Text("Engineering Pad :: Library") })
@@ -67,24 +97,75 @@ fun NotebookListScreen(
}
},
) { padding ->
if (notebooks.isEmpty()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
// Filter and sort bar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
OutlinedTextField(
value = filterText,
onValueChange = { filterText = it },
label = { Text("Filter") },
singleLine = true,
modifier = Modifier.weight(1f),
)
Spacer(modifier = Modifier.width(8.dp))
// Sort dropdown
Box {
var showSortMenu by remember { mutableStateOf(false) }
val dirArrow = if (sortDir == SortDir.ASC) "\u2191" else "\u2193"
TextButton(onClick = { showSortMenu = true }) {
Text("${sortField.label} $dirArrow")
}
DropdownMenu(
expanded = showSortMenu,
onDismissRequest = { showSortMenu = false },
) {
SortField.entries.forEach { field ->
DropdownMenuItem(
text = {
val check = if (field == sortField) "\u2713 " else " "
Text("$check${field.label}")
},
onClick = {
if (field == sortField) {
// Toggle direction
sortDir = if (sortDir == SortDir.ASC) SortDir.DESC else SortDir.ASC
} else {
sortField = field
sortDir = if (field == SortField.NAME) SortDir.ASC else SortDir.DESC
}
showSortMenu = false
},
)
}
}
}
}
if (displayedNotebooks.isEmpty()) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (notebooks.isEmpty()) {
Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge)
Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium)
} else {
Text("No matches", style = MaterialTheme.typography.bodyLarge)
}
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(padding),
) {
items(notebooks, key = { it.id }) { notebook ->
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(displayedNotebooks, key = { it.id }) { notebook ->
NotebookItem(
notebook = notebook,
onClick = { onNotebookClick(notebook.id) },
@@ -94,6 +175,7 @@ fun NotebookListScreen(
}
}
}
}
if (showCreateDialog) {
CreateNotebookDialog(