From b18e77177ebb28ffd0b3b06a8743ca1a093ce4f5 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 17:28:02 -0700 Subject: [PATCH] Fix page list: drag reorder, navigation, delete renumbering, state Drag reorder: - Separated long-press drag from tap/menu: long-press-and-drag reorders, tap opens page, overflow menu (three dots) for delete/export - Fixed combinedClickable conflict that prevented drag from working Page navigation: - View-all-pages state persisted in SharedPreferences, restored on startup - Cleared when navigating back to editor from page list Delete renumbering: - After deleting a page, remaining pages are renumbered sequentially Co-Authored-By: Claude Opus 4.6 (1M context) --- .../net/metacircular/engpad/EngPadApp.kt | 25 ++-- .../net/metacircular/engpad/MainActivity.kt | 3 + .../engpad/ui/navigation/NavGraph.kt | 12 +- .../engpad/ui/pages/PageListScreen.kt | 125 +++++++++--------- .../engpad/ui/pages/PageListViewModel.kt | 3 + 5 files changed, 92 insertions(+), 76 deletions(-) diff --git a/app/src/main/kotlin/net/metacircular/engpad/EngPadApp.kt b/app/src/main/kotlin/net/metacircular/engpad/EngPadApp.kt index a1f47db..3eb3dbf 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/EngPadApp.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/EngPadApp.kt @@ -8,25 +8,24 @@ import net.metacircular.engpad.data.db.EngPadDatabase class EngPadApp : Application() { val database: EngPadDatabase by lazy { EngPadDatabase.getInstance(this) } - fun getLastNotebookId(): Long { - val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - return prefs.getLong(KEY_LAST_NOTEBOOK, 0) + private val prefs by lazy { getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } + + fun getLastNotebookId(): Long = prefs.getLong(KEY_LAST_NOTEBOOK, 0) + + fun setLastNotebookId(id: Long) = prefs.edit { putLong(KEY_LAST_NOTEBOOK, id) } + + fun clearLastNotebookId() = prefs.edit { + remove(KEY_LAST_NOTEBOOK) + remove(KEY_VIEW_ALL_PAGES) } - fun setLastNotebookId(id: Long) { - getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { - putLong(KEY_LAST_NOTEBOOK, id) - } - } + fun isViewAllPages(): Boolean = prefs.getBoolean(KEY_VIEW_ALL_PAGES, false) - fun clearLastNotebookId() { - getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { - remove(KEY_LAST_NOTEBOOK) - } - } + fun setViewAllPages(value: Boolean) = prefs.edit { putBoolean(KEY_VIEW_ALL_PAGES, value) } companion object { private const val PREFS_NAME = "engpad_prefs" private const val KEY_LAST_NOTEBOOK = "last_notebook_id" + private const val KEY_VIEW_ALL_PAGES = "view_all_pages" } } diff --git a/app/src/main/kotlin/net/metacircular/engpad/MainActivity.kt b/app/src/main/kotlin/net/metacircular/engpad/MainActivity.kt index d45ba11..409a6a9 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/MainActivity.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/MainActivity.kt @@ -13,6 +13,7 @@ class MainActivity : ComponentActivity() { val app = application as EngPadApp val database = app.database val lastNotebookId = app.getLastNotebookId() + val wasViewAllPages = app.isViewAllPages() setContent { EngPadTheme { val navController = rememberNavController() @@ -20,8 +21,10 @@ class MainActivity : ComponentActivity() { navController = navController, database = database, lastNotebookId = lastNotebookId, + lastViewAllPages = wasViewAllPages, onNotebookOpened = { app.setLastNotebookId(it) }, onNotebookClosed = { app.clearLastNotebookId() }, + onViewAllPagesChanged = { app.setViewAllPages(it) }, ) } } 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 0a80c4b..f3e69c2 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 @@ -35,15 +35,22 @@ fun EngPadNavGraph( navController: NavHostController, database: EngPadDatabase, lastNotebookId: Long = 0, + lastViewAllPages: Boolean = false, onNotebookOpened: (Long) -> Unit = {}, onNotebookClosed: () -> Unit = {}, + onViewAllPagesChanged: (Boolean) -> Unit = {}, ) { // Auto-navigate to last notebook on startup var autoNavigated by remember { mutableStateOf(false) } LaunchedEffect(lastNotebookId) { if (!autoNavigated && lastNotebookId > 0) { autoNavigated = true - navController.navigate(Routes.editor(lastNotebookId)) + if (lastViewAllPages) { + navController.navigate(Routes.editor(lastNotebookId)) + navController.navigate(Routes.pages(lastNotebookId)) + } else { + navController.navigate(Routes.editor(lastNotebookId)) + } } } @@ -111,6 +118,7 @@ fun EngPadNavGraph( navController.popBackStack(Routes.NOTEBOOKS, false) }, onViewAllPages = { + onViewAllPagesChanged(true) navController.navigate(Routes.pages(notebookId)) }, ) @@ -145,7 +153,7 @@ fun EngPadNavGraph( pageSize = pageSize, database = database, onPageClick = { pageId, _ -> - // Pass selected page ID back to the editor + onViewAllPagesChanged(false) navController.previousBackStackEntry ?.savedStateHandle ?.set("selectedPageId", pageId) diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListScreen.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListScreen.kt index 63c72de..45371fa 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListScreen.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListScreen.kt @@ -3,8 +3,7 @@ package net.metacircular.engpad.ui.pages import android.graphics.Paint as AndroidPaint import android.graphics.Path as AndroidPath import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,6 +11,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items @@ -24,6 +24,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -31,8 +32,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -78,7 +79,6 @@ fun PageListScreen( var pageToDelete by remember { mutableStateOf(null) } - // Maintain an observable mutable list for drag reorder val reorderablePages = remember { mutableStateListOf() } LaunchedEffect(pages) { reorderablePages.clear() @@ -151,7 +151,6 @@ fun PageListScreen( }, modifier = Modifier.longPressDraggableHandle( onDragStopped = { - // Persist the new order viewModel.reorderPages( notebookId, reorderablePages.map { it.id }, @@ -183,7 +182,6 @@ fun PageListScreen( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun PageThumbnail( page: Page, @@ -205,80 +203,85 @@ private fun PageThumbnail( Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier + modifier = modifier .graphicsLayer { if (isDragging) { scaleX = 1.05f scaleY = 1.05f alpha = 0.8f } - } - .then(modifier), + }, ) { Box { Card( modifier = Modifier .fillMaxWidth() - .combinedClickable( - onClick = onClick, - onLongClick = { if (!isDragging) showMenu = true }, - ), + .clickable(onClick = onClick), ) { - Canvas( - modifier = Modifier - .fillMaxWidth() - .aspectRatio(aspectRatio), - ) { - val scaleX = size.width / pageSize.widthPt.toFloat() - val scaleY = size.height / pageSize.heightPt.toFloat() - val scale = minOf(scaleX, scaleY) + Box { + Canvas( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(aspectRatio), + ) { + val scaleX = size.width / pageSize.widthPt.toFloat() + val scaleY = size.height / pageSize.heightPt.toFloat() + val scale = minOf(scaleX, scaleY) - drawIntoCanvas { canvas -> - val nativeCanvas = canvas.nativeCanvas - nativeCanvas.withScale(scale, scale) { - for (stroke in strokes) { - val points = stroke.pointData.toFloatArray() - if (points.size < 2) continue - val path = AndroidPath() - path.moveTo(points[0], points[1]) - var i = 2 - while (i < points.size - 1) { - path.lineTo(points[i], points[i + 1]) - i += 2 + drawIntoCanvas { canvas -> + val nativeCanvas = canvas.nativeCanvas + nativeCanvas.withScale(scale, scale) { + for (stroke in strokes) { + val points = stroke.pointData.toFloatArray() + if (points.size < 2) continue + val path = AndroidPath() + path.moveTo(points[0], points[1]) + var i = 2 + while (i < points.size - 1) { + path.lineTo(points[i], points[i + 1]) + i += 2 + } + val paint = AndroidPaint().apply { + color = stroke.color + strokeWidth = stroke.penSize + style = AndroidPaint.Style.STROKE + strokeCap = AndroidPaint.Cap.ROUND + strokeJoin = AndroidPaint.Join.ROUND + isAntiAlias = false + } + drawPath(path, paint) } - val paint = AndroidPaint().apply { - color = stroke.color - strokeWidth = stroke.penSize - style = AndroidPaint.Style.STROKE - strokeCap = AndroidPaint.Cap.ROUND - strokeJoin = AndroidPaint.Join.ROUND - isAntiAlias = false - } - drawPath(path, paint) } } } + + // Overflow menu button (top-right corner) + Box(modifier = Modifier.align(Alignment.TopEnd)) { + Surface( + onClick = { showMenu = true }, + color = androidx.compose.ui.graphics.Color.Transparent, + modifier = Modifier.size(32.dp), + ) { + Box(contentAlignment = Alignment.Center) { + Text("\u22EE", style = MaterialTheme.typography.bodyLarge) + } + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + DropdownMenuItem( + text = { Text("Export as JPG") }, + onClick = { showMenu = false; onExportJpg() }, + ) + DropdownMenuItem( + text = { Text("Delete page") }, + onClick = { showMenu = false; onDelete() }, + ) + } + } } } - DropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - ) { - DropdownMenuItem( - text = { Text("Export as JPG") }, - onClick = { - showMenu = false - onExportJpg() - }, - ) - DropdownMenuItem( - text = { Text("Delete page") }, - onClick = { - showMenu = false - onDelete() - }, - ) - } } Text( text = "Page ${page.pageNumber}", diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListViewModel.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListViewModel.kt index 70c4cc0..e53cac9 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListViewModel.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/pages/PageListViewModel.kt @@ -28,6 +28,9 @@ class PageListViewModel( fun deletePage(pageId: Long) { viewModelScope.launch { repository.deletePage(pageId) + // Renumber remaining pages sequentially + val remaining = repository.getPagesList(notebookId) + repository.reorderPages(notebookId, remaining.map { it.id }) } }