Add drag-to-reorder pages and fix page navigation from page list
Drag-to-reorder: - Added sh.calvin.reorderable library for LazyVerticalGrid drag support - Long-press and drag page thumbnails to reorder - Page numbers updated in DB on drop - Visual feedback: slight scale + transparency while dragging Page navigation fix: - Clicking a page in "view all pages" now correctly opens that page (key(pid) forces EditorScreen recreation with the selected page) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,7 @@ dependencies {
|
|||||||
ksp(libs.room.compiler)
|
ksp(libs.room.compiler)
|
||||||
|
|
||||||
implementation(libs.coroutines.android)
|
implementation(libs.coroutines.android)
|
||||||
|
implementation(libs.reorderable)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|||||||
@@ -25,4 +25,7 @@ interface PageDao {
|
|||||||
|
|
||||||
@Query("SELECT * FROM pages WHERE notebook_id = :notebookId ORDER BY page_number ASC")
|
@Query("SELECT * FROM pages WHERE notebook_id = :notebookId ORDER BY page_number ASC")
|
||||||
suspend fun getByNotebookIdList(notebookId: Long): List<Page>
|
suspend fun getByNotebookIdList(notebookId: Long): List<Page>
|
||||||
|
|
||||||
|
@Query("UPDATE pages SET page_number = :pageNumber WHERE id = :id")
|
||||||
|
suspend fun updatePageNumber(id: Long, pageNumber: Int)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ class PageRepository(
|
|||||||
|
|
||||||
suspend fun deletePage(id: Long) = pageDao.deleteById(id)
|
suspend fun deletePage(id: Long) = pageDao.deleteById(id)
|
||||||
|
|
||||||
|
suspend fun reorderPages(notebookId: Long, orderedPageIds: List<Long>) {
|
||||||
|
orderedPageIds.forEachIndexed { index, pageId ->
|
||||||
|
pageDao.updatePageNumber(pageId, index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getStrokes(pageId: Long): List<Stroke> =
|
suspend fun getStrokes(pageId: Long): List<Stroke> =
|
||||||
strokeDao.getByPageId(pageId)
|
strokeDao.getByPageId(pageId)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -98,6 +99,8 @@ fun EngPadNavGraph(
|
|||||||
val ps = pageSize
|
val ps = pageSize
|
||||||
val pid = initialPageId
|
val pid = initialPageId
|
||||||
if (ps != null && pid != null) {
|
if (ps != null && pid != null) {
|
||||||
|
// key(pid) forces EditorScreen + ViewModel recreation when page changes
|
||||||
|
key(pid) {
|
||||||
EditorScreen(
|
EditorScreen(
|
||||||
notebookId = notebookId,
|
notebookId = notebookId,
|
||||||
pageSize = ps,
|
pageSize = ps,
|
||||||
@@ -111,6 +114,7 @@ fun EngPadNavGraph(
|
|||||||
navController.navigate(Routes.pages(notebookId))
|
navController.navigate(Routes.pages(notebookId))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
} // key(pid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
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
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
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.DropdownMenu
|
||||||
@@ -34,9 +35,11 @@ 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
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.nativeCanvas
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -50,6 +53,8 @@ import net.metacircular.engpad.data.model.PageSize
|
|||||||
import net.metacircular.engpad.data.model.Stroke
|
import net.metacircular.engpad.data.model.Stroke
|
||||||
import net.metacircular.engpad.data.repository.PageRepository
|
import net.metacircular.engpad.data.repository.PageRepository
|
||||||
import net.metacircular.engpad.ui.export.PdfExporter
|
import net.metacircular.engpad.ui.export.PdfExporter
|
||||||
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyGridState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -73,6 +78,23 @@ fun PageListScreen(
|
|||||||
|
|
||||||
var pageToDelete by remember { mutableStateOf<Page?>(null) }
|
var pageToDelete by remember { mutableStateOf<Page?>(null) }
|
||||||
|
|
||||||
|
// Maintain a local mutable list for drag reorder
|
||||||
|
val reorderablePages = remember { mutableListOf<Page>() }
|
||||||
|
LaunchedEffect(pages) {
|
||||||
|
reorderablePages.clear()
|
||||||
|
reorderablePages.addAll(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lazyGridState = rememberLazyGridState()
|
||||||
|
val reorderableLazyGridState = rememberReorderableLazyGridState(lazyGridState) { from, to ->
|
||||||
|
val fromIndex = reorderablePages.indexOfFirst { it.id == from.key as Long }
|
||||||
|
val toIndex = reorderablePages.indexOfFirst { it.id == to.key as Long }
|
||||||
|
if (fromIndex >= 0 && toIndex >= 0) {
|
||||||
|
val item = reorderablePages.removeAt(fromIndex)
|
||||||
|
reorderablePages.add(toIndex, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(title = { Text(notebookTitle) })
|
TopAppBar(title = { Text(notebookTitle) })
|
||||||
@@ -100,15 +122,18 @@ fun PageListScreen(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(8.dp),
|
.padding(8.dp),
|
||||||
|
state = lazyGridState,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
items(pages, key = { it.id }) { page ->
|
items(reorderablePages.toList(), key = { it.id }) { page ->
|
||||||
|
ReorderableItem(reorderableLazyGridState, key = page.id) { isDragging ->
|
||||||
PageThumbnail(
|
PageThumbnail(
|
||||||
page = page,
|
page = page,
|
||||||
pageSize = pageSize,
|
pageSize = pageSize,
|
||||||
aspectRatio = aspectRatio,
|
aspectRatio = aspectRatio,
|
||||||
repository = repository,
|
repository = repository,
|
||||||
|
isDragging = isDragging,
|
||||||
onClick = { onPageClick(page.id, pageSize) },
|
onClick = { onPageClick(page.id, pageSize) },
|
||||||
onDelete = { pageToDelete = page },
|
onDelete = { pageToDelete = page },
|
||||||
onExportJpg = {
|
onExportJpg = {
|
||||||
@@ -124,7 +149,17 @@ fun PageListScreen(
|
|||||||
PdfExporter.shareFile(context, file, "image/jpeg")
|
PdfExporter.shareFile(context, file, "image/jpeg")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
modifier = Modifier.longPressDraggableHandle(
|
||||||
|
onDragStopped = {
|
||||||
|
// Persist the new order
|
||||||
|
viewModel.reorderPages(
|
||||||
|
notebookId,
|
||||||
|
reorderablePages.map { it.id },
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,9 +190,11 @@ private fun PageThumbnail(
|
|||||||
pageSize: PageSize,
|
pageSize: PageSize,
|
||||||
aspectRatio: Float,
|
aspectRatio: Float,
|
||||||
repository: PageRepository,
|
repository: PageRepository,
|
||||||
|
isDragging: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onExportJpg: () -> Unit,
|
onExportJpg: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var strokes by remember(page.id) { mutableStateOf<List<Stroke>>(emptyList()) }
|
var strokes by remember(page.id) { mutableStateOf<List<Stroke>>(emptyList()) }
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
@@ -168,6 +205,15 @@ private fun PageThumbnail(
|
|||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
if (isDragging) {
|
||||||
|
scaleX = 1.05f
|
||||||
|
scaleY = 1.05f
|
||||||
|
alpha = 0.8f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(modifier),
|
||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Card(
|
Card(
|
||||||
@@ -175,7 +221,7 @@ private fun PageThumbnail(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = { showMenu = true },
|
onLongClick = { if (!isDragging) showMenu = true },
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Canvas(
|
Canvas(
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ class PageListViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun reorderPages(notebookId: Long, orderedPageIds: List<Long>) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
repository.reorderPages(notebookId, orderedPageIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getStrokes(pageId: Long): List<Stroke> =
|
suspend fun getStrokes(pageId: Long): List<Stroke> =
|
||||||
repository.getStrokes(pageId)
|
repository.getStrokes(pageId)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ coroutines = "1.10.2"
|
|||||||
core-ktx = "1.18.0"
|
core-ktx = "1.18.0"
|
||||||
activity-compose = "1.13.0"
|
activity-compose = "1.13.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
|
reorderable = "3.0.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
|
||||||
@@ -35,6 +36,7 @@ coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutin
|
|||||||
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
||||||
|
|
||||||
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
||||||
|
reorderable = { group = "sh.calvin.reorderable", name = "reorderable", version.ref = "reorderable" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
|
|||||||
Reference in New Issue
Block a user