From 984f19af063528d13284a712b85e20d06069bbda Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 24 Mar 2026 17:11:23 -0700 Subject: [PATCH] Add page long-press menu (delete/export JPG) in view all pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../engpad/ui/pages/PageListScreen.kt | 139 +++++++++++++----- .../engpad/ui/pages/PageListViewModel.kt | 10 ++ 2 files changed, 116 insertions(+), 33 deletions(-) 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 071c023..94afa4b 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 @@ -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(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>(emptyList()) } + var showMenu by remember { mutableStateOf(false) } LaunchedEffect(page.id) { strokes = repository.getStrokes(page.id) @@ -120,46 +168,71 @@ private fun PageThumbnail( Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.clickable(onClick = onClick), ) { - Card( - modifier = Modifier.fillMaxWidth(), - ) { - Canvas( + Box { + Card( modifier = Modifier .fillMaxWidth() - .aspectRatio(aspectRatio), + .combinedClickable( + onClick = onClick, + onLongClick = { showMenu = true }, + ), ) { - val scaleX = size.width / pageSize.widthPt.toFloat() - val scaleY = size.height / pageSize.heightPt.toFloat() - val scale = minOf(scaleX, scaleY) + 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) } } } } + 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 8b27843..40067b5 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 @@ -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 = + repository.getStrokes(pageId) + class Factory( private val notebookId: Long, private val repository: PageRepository,