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:
@@ -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 {
|
||||||
notebook.lastPageId
|
// Explicit page requested (from page list click)
|
||||||
} else {
|
requestedPageId > 0 && pages.any { it.id == requestedPageId } ->
|
||||||
pages.firstOrNull()?.id ?: return@LaunchedEffect
|
requestedPageId
|
||||||
|
// Resume last visited page
|
||||||
|
notebook.lastPageId > 0 && pages.any { it.id == notebook.lastPageId } ->
|
||||||
|
notebook.lastPageId
|
||||||
|
// 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 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()
|
||||||
|
|||||||
@@ -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,29 +97,81 @@ fun NotebookListScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
if (notebooks.isEmpty()) {
|
Column(
|
||||||
Column(
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
) {
|
||||||
|
// Filter and sort bar
|
||||||
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxWidth()
|
||||||
.padding(padding),
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge)
|
OutlinedTextField(
|
||||||
Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium)
|
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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LazyColumn(
|
if (displayedNotebooks.isEmpty()) {
|
||||||
modifier = Modifier
|
Column(
|
||||||
.fillMaxSize()
|
modifier = Modifier.fillMaxSize(),
|
||||||
.padding(padding),
|
verticalArrangement = Arrangement.Center,
|
||||||
) {
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
items(notebooks, key = { it.id }) { notebook ->
|
) {
|
||||||
NotebookItem(
|
if (notebooks.isEmpty()) {
|
||||||
notebook = notebook,
|
Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge)
|
||||||
onClick = { onNotebookClick(notebook.id) },
|
Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium)
|
||||||
onLongClick = { notebookToDelete = notebook },
|
} else {
|
||||||
)
|
Text("No matches", style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||||
|
items(displayedNotebooks, key = { it.id }) { notebook ->
|
||||||
|
NotebookItem(
|
||||||
|
notebook = notebook,
|
||||||
|
onClick = { onNotebookClick(notebook.id) },
|
||||||
|
onLongClick = { notebookToDelete = notebook },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user