Add page long-press menu (delete/export JPG) in view all pages

- Long-press a page thumbnail to show context menu with Delete and
  Export as JPG options
- Delete shows confirmation dialog
- Export renders page at 300 DPI and shares via intent
- FAB always creates new pages (no empty-page restriction in page list)
- Note: drag-to-reorder deferred — requires custom grid drag handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 17:11:23 -07:00
parent 6a628d2435
commit 984f19af06
2 changed files with 116 additions and 33 deletions

View File

@@ -1,11 +1,12 @@
package net.metacircular.engpad.ui.pages
import android.graphics.Color as AndroidColor
import android.graphics.Paint as AndroidPaint
import android.graphics.Path as AndroidPath
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
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.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
@@ -14,12 +15,16 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
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
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -27,20 +32,24 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.core.graphics.withScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.graphics.withScale
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.launch
import net.metacircular.engpad.data.db.EngPadDatabase
import net.metacircular.engpad.data.db.toFloatArray
import net.metacircular.engpad.data.model.Page
import net.metacircular.engpad.data.model.PageSize
import net.metacircular.engpad.data.model.Stroke
import net.metacircular.engpad.data.repository.PageRepository
import net.metacircular.engpad.ui.export.PdfExporter
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -59,6 +68,10 @@ fun PageListScreen(
)
val pages by viewModel.pages.collectAsState()
val aspectRatio = pageSize.widthPt.toFloat() / pageSize.heightPt.toFloat()
val context = LocalContext.current
val scope = rememberCoroutineScope()
var pageToDelete by remember { mutableStateOf<Page?>(null) }
Scaffold(
topBar = {
@@ -97,13 +110,45 @@ fun PageListScreen(
aspectRatio = aspectRatio,
repository = repository,
onClick = { onPageClick(page.id, pageSize) },
onDelete = { pageToDelete = page },
onExportJpg = {
scope.launch {
val strokes = viewModel.getStrokes(page.id)
val file = PdfExporter.exportPageAsJpg(
context = context,
notebookTitle = notebookTitle,
pageSize = pageSize,
pageNumber = page.pageNumber,
strokes = strokes,
)
PdfExporter.shareFile(context, file, "image/jpeg")
}
},
)
}
}
}
}
pageToDelete?.let { page ->
AlertDialog(
onDismissRequest = { pageToDelete = null },
title = { Text("Delete Page") },
text = { Text("Delete page ${page.pageNumber}? This cannot be undone.") },
confirmButton = {
TextButton(onClick = {
viewModel.deletePage(page.id)
pageToDelete = null
}) { Text("Delete") }
},
dismissButton = {
TextButton(onClick = { pageToDelete = null }) { Text("Cancel") }
},
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun PageThumbnail(
page: Page,
@@ -111,8 +156,11 @@ private fun PageThumbnail(
aspectRatio: Float,
repository: PageRepository,
onClick: () -> Unit,
onDelete: () -> Unit,
onExportJpg: () -> Unit,
) {
var strokes by remember(page.id) { mutableStateOf<List<Stroke>>(emptyList()) }
var showMenu by remember { mutableStateOf(false) }
LaunchedEffect(page.id) {
strokes = repository.getStrokes(page.id)
@@ -120,10 +168,15 @@ private fun PageThumbnail(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.clickable(onClick = onClick),
) {
Box {
Card(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = onClick,
onLongClick = { showMenu = true },
),
) {
Canvas(
modifier = Modifier
@@ -161,6 +214,26 @@ private fun PageThumbnail(
}
}
}
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}",
style = MaterialTheme.typography.bodySmall,

View File

@@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.metacircular.engpad.data.model.Page
import net.metacircular.engpad.data.model.Stroke
import net.metacircular.engpad.data.repository.PageRepository
class PageListViewModel(
@@ -24,6 +25,15 @@ class PageListViewModel(
}
}
fun deletePage(pageId: Long) {
viewModelScope.launch {
repository.deletePage(pageId)
}
}
suspend fun getStrokes(pageId: Long): List<Stroke> =
repository.getStrokes(pageId)
class Factory(
private val notebookId: Long,
private val repository: PageRepository,