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) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 17:28:02 -07:00
parent 2692f0eb0d
commit b18e77177e
5 changed files with 92 additions and 76 deletions

View File

@@ -8,25 +8,24 @@ import net.metacircular.engpad.data.db.EngPadDatabase
class EngPadApp : Application() { class EngPadApp : Application() {
val database: EngPadDatabase by lazy { EngPadDatabase.getInstance(this) } val database: EngPadDatabase by lazy { EngPadDatabase.getInstance(this) }
fun getLastNotebookId(): Long { private val prefs by lazy { getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getLong(KEY_LAST_NOTEBOOK, 0)
}
fun setLastNotebookId(id: Long) { fun getLastNotebookId(): Long = prefs.getLong(KEY_LAST_NOTEBOOK, 0)
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putLong(KEY_LAST_NOTEBOOK, id)
}
}
fun clearLastNotebookId() { fun setLastNotebookId(id: Long) = prefs.edit { putLong(KEY_LAST_NOTEBOOK, id) }
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
fun clearLastNotebookId() = prefs.edit {
remove(KEY_LAST_NOTEBOOK) remove(KEY_LAST_NOTEBOOK)
remove(KEY_VIEW_ALL_PAGES)
} }
}
fun isViewAllPages(): Boolean = prefs.getBoolean(KEY_VIEW_ALL_PAGES, false)
fun setViewAllPages(value: Boolean) = prefs.edit { putBoolean(KEY_VIEW_ALL_PAGES, value) }
companion object { companion object {
private const val PREFS_NAME = "engpad_prefs" private const val PREFS_NAME = "engpad_prefs"
private const val KEY_LAST_NOTEBOOK = "last_notebook_id" private const val KEY_LAST_NOTEBOOK = "last_notebook_id"
private const val KEY_VIEW_ALL_PAGES = "view_all_pages"
} }
} }

View File

@@ -13,6 +13,7 @@ class MainActivity : ComponentActivity() {
val app = application as EngPadApp val app = application as EngPadApp
val database = app.database val database = app.database
val lastNotebookId = app.getLastNotebookId() val lastNotebookId = app.getLastNotebookId()
val wasViewAllPages = app.isViewAllPages()
setContent { setContent {
EngPadTheme { EngPadTheme {
val navController = rememberNavController() val navController = rememberNavController()
@@ -20,8 +21,10 @@ class MainActivity : ComponentActivity() {
navController = navController, navController = navController,
database = database, database = database,
lastNotebookId = lastNotebookId, lastNotebookId = lastNotebookId,
lastViewAllPages = wasViewAllPages,
onNotebookOpened = { app.setLastNotebookId(it) }, onNotebookOpened = { app.setLastNotebookId(it) },
onNotebookClosed = { app.clearLastNotebookId() }, onNotebookClosed = { app.clearLastNotebookId() },
onViewAllPagesChanged = { app.setViewAllPages(it) },
) )
} }
} }

View File

@@ -35,15 +35,22 @@ fun EngPadNavGraph(
navController: NavHostController, navController: NavHostController,
database: EngPadDatabase, database: EngPadDatabase,
lastNotebookId: Long = 0, lastNotebookId: Long = 0,
lastViewAllPages: Boolean = false,
onNotebookOpened: (Long) -> Unit = {}, onNotebookOpened: (Long) -> Unit = {},
onNotebookClosed: () -> Unit = {}, onNotebookClosed: () -> Unit = {},
onViewAllPagesChanged: (Boolean) -> Unit = {},
) { ) {
// Auto-navigate to last notebook on startup // Auto-navigate to last notebook on startup
var autoNavigated by remember { mutableStateOf(false) } var autoNavigated by remember { mutableStateOf(false) }
LaunchedEffect(lastNotebookId) { LaunchedEffect(lastNotebookId) {
if (!autoNavigated && lastNotebookId > 0) { if (!autoNavigated && lastNotebookId > 0) {
autoNavigated = true autoNavigated = true
if (lastViewAllPages) {
navController.navigate(Routes.editor(lastNotebookId)) 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) navController.popBackStack(Routes.NOTEBOOKS, false)
}, },
onViewAllPages = { onViewAllPages = {
onViewAllPagesChanged(true)
navController.navigate(Routes.pages(notebookId)) navController.navigate(Routes.pages(notebookId))
}, },
) )
@@ -145,7 +153,7 @@ fun EngPadNavGraph(
pageSize = pageSize, pageSize = pageSize,
database = database, database = database,
onPageClick = { pageId, _ -> onPageClick = { pageId, _ ->
// Pass selected page ID back to the editor onViewAllPagesChanged(false)
navController.previousBackStackEntry navController.previousBackStackEntry
?.savedStateHandle ?.savedStateHandle
?.set("selectedPageId", pageId) ?.set("selectedPageId", pageId)

View File

@@ -3,8 +3,7 @@ package net.metacircular.engpad.ui.pages
import android.graphics.Paint as AndroidPaint import android.graphics.Paint as AndroidPaint
import android.graphics.Path as AndroidPath import android.graphics.Path as AndroidPath
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable
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.Box
import androidx.compose.foundation.layout.Column 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.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.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items 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.FloatingActionButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@@ -31,8 +32,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -78,7 +79,6 @@ fun PageListScreen(
var pageToDelete by remember { mutableStateOf<Page?>(null) } var pageToDelete by remember { mutableStateOf<Page?>(null) }
// Maintain an observable mutable list for drag reorder
val reorderablePages = remember { mutableStateListOf<Page>() } val reorderablePages = remember { mutableStateListOf<Page>() }
LaunchedEffect(pages) { LaunchedEffect(pages) {
reorderablePages.clear() reorderablePages.clear()
@@ -151,7 +151,6 @@ fun PageListScreen(
}, },
modifier = Modifier.longPressDraggableHandle( modifier = Modifier.longPressDraggableHandle(
onDragStopped = { onDragStopped = {
// Persist the new order
viewModel.reorderPages( viewModel.reorderPages(
notebookId, notebookId,
reorderablePages.map { it.id }, reorderablePages.map { it.id },
@@ -183,7 +182,6 @@ fun PageListScreen(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun PageThumbnail( private fun PageThumbnail(
page: Page, page: Page,
@@ -205,25 +203,22 @@ private fun PageThumbnail(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = modifier
.graphicsLayer { .graphicsLayer {
if (isDragging) { if (isDragging) {
scaleX = 1.05f scaleX = 1.05f
scaleY = 1.05f scaleY = 1.05f
alpha = 0.8f alpha = 0.8f
} }
} },
.then(modifier),
) { ) {
Box { Box {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .clickable(onClick = onClick),
onClick = onClick,
onLongClick = { if (!isDragging) showMenu = true },
),
) { ) {
Box {
Canvas( Canvas(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -259,6 +254,17 @@ private fun PageThumbnail(
} }
} }
} }
// 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( DropdownMenu(
expanded = showMenu, expanded = showMenu,
@@ -266,20 +272,17 @@ private fun PageThumbnail(
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Export as JPG") }, text = { Text("Export as JPG") },
onClick = { onClick = { showMenu = false; onExportJpg() },
showMenu = false
onExportJpg()
},
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Delete page") }, text = { Text("Delete page") },
onClick = { onClick = { showMenu = false; onDelete() },
showMenu = false
onDelete()
},
) )
} }
} }
}
}
}
Text( Text(
text = "Page ${page.pageNumber}", text = "Page ${page.pageNumber}",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,

View File

@@ -28,6 +28,9 @@ class PageListViewModel(
fun deletePage(pageId: Long) { fun deletePage(pageId: Long) {
viewModelScope.launch { viewModelScope.launch {
repository.deletePage(pageId) repository.deletePage(pageId)
// Renumber remaining pages sequentially
val remaining = repository.getPagesList(notebookId)
repository.reorderPages(notebookId, remaining.map { it.id })
} }
} }