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 { object Routes {
const val NOTEBOOKS = "notebooks" const val NOTEBOOKS = "notebooks"
const val EDITOR = "editor/{notebookId}" const val EDITOR = "editor/{notebookId}?pageId={pageId}"
const val PAGES = "pages/{notebookId}" 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" fun pages(notebookId: Long) = "pages/$notebookId"
} }
@@ -65,9 +67,13 @@ fun EngPadNavGraph(
} }
composable( composable(
route = Routes.EDITOR, 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 -> ) { backStackEntry ->
val notebookId = backStackEntry.arguments?.getLong("notebookId") ?: return@composable val notebookId = backStackEntry.arguments?.getLong("notebookId") ?: return@composable
val requestedPageId = backStackEntry.arguments?.getLong("pageId") ?: 0L
val notebookRepo = remember { val notebookRepo = remember {
NotebookRepository(database.notebookDao(), database.pageDao()) NotebookRepository(database.notebookDao(), database.pageDao())
} }
@@ -78,27 +84,23 @@ fun EngPadNavGraph(
var pageSize by remember { mutableStateOf<PageSize?>(null) } var pageSize by remember { mutableStateOf<PageSize?>(null) }
var initialPageId by remember { mutableStateOf<Long?>(null) } var initialPageId by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(notebookId) { LaunchedEffect(notebookId, requestedPageId) {
onNotebookOpened(notebookId) onNotebookOpened(notebookId)
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
pageSize = PageSize.fromString(notebook.pageSize) pageSize = PageSize.fromString(notebook.pageSize)
// Use last page or fall back to first page
val pages = pageRepo.getPagesList(notebookId) 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 notebook.lastPageId
} else { // Fall back to first page
pages.firstOrNull()?.id ?: return@LaunchedEffect 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 ps = pageSize
val pid = initialPageId val pid = initialPageId
if (ps != null && pid != null) { if (ps != null && pid != null) {
@@ -150,10 +152,10 @@ fun EngPadNavGraph(
database = database, database = database,
onPageClick = { pageId, _ -> onPageClick = { pageId, _ ->
onViewAllPagesChanged(false) onViewAllPagesChanged(false)
navController.previousBackStackEntry // Navigate to editor with explicit page ID
?.savedStateHandle // Pop both the page list and the old editor, then push new editor
?.set("selectedPageId", pageId) navController.popBackStack(Routes.NOTEBOOKS, false)
navController.popBackStack() navController.navigate(Routes.editor(notebookId, pageId))
}, },
onClose = { onClose = {
onNotebookClosed() onNotebookClosed()

View File

@@ -3,16 +3,21 @@ package net.metacircular.engpad.ui.notebooks
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -41,6 +46,14 @@ import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NotebookListScreen( fun NotebookListScreen(
@@ -57,6 +70,23 @@ fun NotebookListScreen(
var showCreateDialog by remember { mutableStateOf(false) } var showCreateDialog by remember { mutableStateOf(false) }
var notebookToDelete by remember { mutableStateOf<Notebook?>(null) } 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( Scaffold(
topBar = { topBar = {
TopAppBar(title = { Text("Engineering Pad :: Library") }) TopAppBar(title = { Text("Engineering Pad :: Library") })
@@ -67,24 +97,75 @@ fun NotebookListScreen(
} }
}, },
) { padding -> ) { padding ->
if (notebooks.isEmpty()) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(padding), .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, verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
if (notebooks.isEmpty()) {
Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge) Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge)
Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium) Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium)
} else {
Text("No matches", style = MaterialTheme.typography.bodyLarge)
}
} }
} else { } else {
LazyColumn( LazyColumn(modifier = Modifier.fillMaxSize()) {
modifier = Modifier items(displayedNotebooks, key = { it.id }) { notebook ->
.fillMaxSize()
.padding(padding),
) {
items(notebooks, key = { it.id }) { notebook ->
NotebookItem( NotebookItem(
notebook = notebook, notebook = notebook,
onClick = { onNotebookClick(notebook.id) }, onClick = { onNotebookClick(notebook.id) },
@@ -94,6 +175,7 @@ fun NotebookListScreen(
} }
} }
} }
}
if (showCreateDialog) { if (showCreateDialog) {
CreateNotebookDialog( CreateNotebookDialog(