diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/navigation/NavGraph.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/navigation/NavGraph.kt index c8ca01a..62e6979 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/navigation/NavGraph.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/navigation/NavGraph.kt @@ -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(null) } var initialPageId by remember { mutableStateOf(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 }) { - notebook.lastPageId - } else { - pages.firstOrNull()?.id ?: return@LaunchedEffect + 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 + // 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("selectedPageId") - if (selectedPageId != null && selectedPageId > 0) { - initialPageId = selectedPageId - savedState.remove("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() diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/notebooks/NotebookListScreen.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/notebooks/NotebookListScreen.kt index e8958c0..a94974b 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/notebooks/NotebookListScreen.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/notebooks/NotebookListScreen.kt @@ -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(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,29 +97,81 @@ fun NotebookListScreen( } }, ) { padding -> - if (notebooks.isEmpty()) { - Column( + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding), + ) { + // Filter and sort bar + Row( modifier = Modifier - .fillMaxSize() - .padding(padding), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Text("No notebooks yet", style = MaterialTheme.typography.bodyLarge) - Text("Tap + to create one", style = MaterialTheme.typography.bodyMedium) + 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 + }, + ) + } + } + } } - } else { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(padding), - ) { - items(notebooks, key = { it.id }) { notebook -> - NotebookItem( - notebook = notebook, - onClick = { onNotebookClick(notebook.id) }, - onLongClick = { notebookToDelete = notebook }, - ) + + 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()) { + items(displayedNotebooks, key = { it.id }) { notebook -> + NotebookItem( + notebook = notebook, + onClick = { onNotebookClick(notebook.id) }, + onLongClick = { notebookToDelete = notebook }, + ) + } } } }