Implement Phase 3: canvas drawing with stylus input
- PadCanvasView: custom View with stylus event handling (historical points for smoothness), Path/Paint stroke rendering, backing bitmap at 1/4 resolution, 60pt grid drawing, Matrix coordinate transform - CanvasState: tool modes (fine/medium pen, eraser, select), zoom/pan state - EditorViewModel: loads/saves strokes to Room on completion - EditorScreen: Compose wrapper with AndroidView + FilterChip toolbar - NavGraph: pages route auto-navigates to first page editor, page size passed through route params Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package net.metacircular.engpad.ui.editor
|
||||
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
|
||||
enum class Tool {
|
||||
PEN_FINE, // 0.38mm = 4.49pt at 300 DPI
|
||||
PEN_MEDIUM, // 0.50mm = 5.91pt at 300 DPI
|
||||
ERASER,
|
||||
SELECT,
|
||||
}
|
||||
|
||||
data class CanvasState(
|
||||
val pageSize: PageSize = PageSize.REGULAR,
|
||||
val tool: Tool = Tool.PEN_FINE,
|
||||
val zoom: Float = 1f,
|
||||
val panX: Float = 0f,
|
||||
val panY: Float = 0f,
|
||||
) {
|
||||
val penWidthPt: Float
|
||||
get() = when (tool) {
|
||||
Tool.PEN_FINE -> 4.49f
|
||||
Tool.PEN_MEDIUM -> 5.91f
|
||||
else -> 0f
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_ZOOM = 0.5f
|
||||
const val MAX_ZOOM = 4f
|
||||
const val GRID_SPACING_PT = 60f // 300 DPI / 5 squares per inch
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package net.metacircular.engpad.ui.editor
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
|
||||
@Composable
|
||||
fun EditorScreen(
|
||||
pageId: Long,
|
||||
pageSize: PageSize,
|
||||
database: EngPadDatabase,
|
||||
) {
|
||||
val repository = remember {
|
||||
PageRepository(database.pageDao(), database.strokeDao())
|
||||
}
|
||||
val viewModel: EditorViewModel = viewModel(
|
||||
factory = EditorViewModel.Factory(pageId, pageSize, repository),
|
||||
)
|
||||
val canvasState by viewModel.canvasState.collectAsState()
|
||||
val strokes by viewModel.strokes.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
val canvasView = remember { PadCanvasView(context) }
|
||||
|
||||
// Update canvas when state changes
|
||||
LaunchedEffect(canvasState) {
|
||||
canvasView.canvasState = canvasState
|
||||
}
|
||||
|
||||
// Update strokes when loaded from DB
|
||||
LaunchedEffect(strokes) {
|
||||
canvasView.setStrokes(strokes)
|
||||
}
|
||||
|
||||
// Wire up stroke completion callback
|
||||
LaunchedEffect(Unit) {
|
||||
canvasView.onStrokeCompleted = { penSize, color, points ->
|
||||
viewModel.onStrokeCompleted(penSize, color, points)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
EditorToolbar(
|
||||
currentTool = canvasState.tool,
|
||||
onToolSelected = { viewModel.setTool(it) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
AndroidView(
|
||||
factory = { canvasView },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package net.metacircular.engpad.ui.editor
|
||||
|
||||
import android.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import net.metacircular.engpad.data.db.toBlob
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.model.Stroke
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
|
||||
class EditorViewModel(
|
||||
private val pageId: Long,
|
||||
private val pageSize: PageSize,
|
||||
private val pageRepository: PageRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _canvasState = MutableStateFlow(CanvasState(pageSize = pageSize))
|
||||
val canvasState: StateFlow<CanvasState> = _canvasState
|
||||
|
||||
private val _strokes = MutableStateFlow<List<Stroke>>(emptyList())
|
||||
val strokes: StateFlow<List<Stroke>> = _strokes
|
||||
|
||||
init {
|
||||
loadStrokes()
|
||||
}
|
||||
|
||||
private fun loadStrokes() {
|
||||
viewModelScope.launch {
|
||||
_strokes.value = pageRepository.getStrokes(pageId)
|
||||
}
|
||||
}
|
||||
|
||||
fun setTool(tool: Tool) {
|
||||
_canvasState.value = _canvasState.value.copy(tool = tool)
|
||||
}
|
||||
|
||||
fun onStrokeCompleted(penSize: Float, color: Int, points: FloatArray) {
|
||||
viewModelScope.launch {
|
||||
val order = pageRepository.getNextStrokeOrder(pageId)
|
||||
val stroke = Stroke(
|
||||
pageId = pageId,
|
||||
penSize = penSize,
|
||||
color = color,
|
||||
pointData = points.toBlob(),
|
||||
strokeOrder = order,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
)
|
||||
val id = pageRepository.addStroke(stroke)
|
||||
// Refresh strokes from DB to get the assigned ID
|
||||
_strokes.value = pageRepository.getStrokes(pageId)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val pageId: Long,
|
||||
private val pageSize: PageSize,
|
||||
private val pageRepository: PageRepository,
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return EditorViewModel(pageId, pageSize, pageRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package net.metacircular.engpad.ui.editor
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withMatrix
|
||||
import androidx.core.graphics.withScale
|
||||
import net.metacircular.engpad.data.db.toFloatArray
|
||||
import net.metacircular.engpad.data.model.Stroke
|
||||
|
||||
/**
|
||||
* Custom View for the drawing canvas. Handles stylus input, stroke rendering,
|
||||
* grid drawing, and zoom/pan via Matrix transforms.
|
||||
*
|
||||
* All stroke coordinates are stored in canonical space (300 DPI).
|
||||
* The viewMatrix transforms canonical -> screen coordinates.
|
||||
*/
|
||||
class PadCanvasView(context: Context) : View(context) {
|
||||
|
||||
// --- State ---
|
||||
var canvasState = CanvasState()
|
||||
set(value) {
|
||||
field = value
|
||||
rebuildViewMatrix()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// --- Strokes ---
|
||||
private val completedStrokes = mutableListOf<StrokeRender>()
|
||||
private var backingBitmap: Bitmap? = null
|
||||
private var backingCanvas: Canvas? = null
|
||||
private var bitmapDirty = true
|
||||
|
||||
// --- In-progress stroke ---
|
||||
private var currentPath: Path? = null
|
||||
private var currentPaint: Paint? = null
|
||||
private val currentPoints = mutableListOf<Float>()
|
||||
|
||||
// --- Transform ---
|
||||
private val viewMatrix = Matrix()
|
||||
private val inverseMatrix = Matrix()
|
||||
|
||||
// --- Callbacks ---
|
||||
var onStrokeCompleted: ((penSize: Float, color: Int, points: FloatArray) -> Unit)? = null
|
||||
|
||||
// --- Grid paint ---
|
||||
private val gridPaint = Paint().apply {
|
||||
color = Color.LTGRAY
|
||||
strokeWidth = 1f
|
||||
style = Paint.Style.STROKE
|
||||
isAntiAlias = false
|
||||
}
|
||||
|
||||
// --- Page background ---
|
||||
private val pagePaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
init {
|
||||
setBackgroundColor(Color.DKGRAY)
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
fun setStrokes(strokes: List<Stroke>) {
|
||||
completedStrokes.clear()
|
||||
for (stroke in strokes) {
|
||||
val points = stroke.pointData.toFloatArray()
|
||||
val path = buildPathFromPoints(points)
|
||||
val paint = buildPaint(stroke.penSize, stroke.color)
|
||||
completedStrokes.add(StrokeRender(path, paint, stroke.id))
|
||||
}
|
||||
bitmapDirty = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun addCompletedStroke(id: Long, penSize: Float, color: Int, points: FloatArray) {
|
||||
val path = buildPathFromPoints(points)
|
||||
val paint = buildPaint(penSize, color)
|
||||
completedStrokes.add(StrokeRender(path, paint, id))
|
||||
bitmapDirty = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun removeStroke(id: Long) {
|
||||
completedStrokes.removeAll { it.id == id }
|
||||
bitmapDirty = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
// --- Drawing ---
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
rebuildViewMatrix()
|
||||
bitmapDirty = true
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.withMatrix(viewMatrix) {
|
||||
// Draw page background
|
||||
drawRect(
|
||||
0f, 0f,
|
||||
canvasState.pageSize.widthPt.toFloat(),
|
||||
canvasState.pageSize.heightPt.toFloat(),
|
||||
pagePaint,
|
||||
)
|
||||
|
||||
// Draw grid
|
||||
drawGrid(this)
|
||||
|
||||
// Draw completed strokes from backing bitmap
|
||||
ensureBacking()
|
||||
backingBitmap?.let { bmp ->
|
||||
// Scale backing bitmap back up (it's rendered at 1/4 resolution)
|
||||
val scaleUp = 1f / BACKING_SCALE
|
||||
withScale(scaleUp, scaleUp) {
|
||||
drawBitmap(bmp, 0f, 0f, null)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw in-progress stroke
|
||||
currentPath?.let { path ->
|
||||
currentPaint?.let { paint ->
|
||||
drawPath(path, paint)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawGrid(canvas: Canvas) {
|
||||
val pageW = canvasState.pageSize.widthPt.toFloat()
|
||||
val pageH = canvasState.pageSize.heightPt.toFloat()
|
||||
val spacing = CanvasState.GRID_SPACING_PT
|
||||
|
||||
var x = 0f
|
||||
while (x <= pageW) {
|
||||
canvas.drawLine(x, 0f, x, pageH, gridPaint)
|
||||
x += spacing
|
||||
}
|
||||
var y = 0f
|
||||
while (y <= pageH) {
|
||||
canvas.drawLine(0f, y, pageW, y, gridPaint)
|
||||
y += spacing
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureBacking() {
|
||||
val pageW = canvasState.pageSize.widthPt
|
||||
val pageH = canvasState.pageSize.heightPt
|
||||
|
||||
val bmpW = (pageW * BACKING_SCALE).toInt()
|
||||
val bmpH = (pageH * BACKING_SCALE).toInt()
|
||||
|
||||
if (backingBitmap == null || backingBitmap!!.width != bmpW || backingBitmap!!.height != bmpH) {
|
||||
backingBitmap?.recycle()
|
||||
backingBitmap = createBitmap(bmpW, bmpH)
|
||||
backingCanvas = Canvas(backingBitmap!!)
|
||||
bitmapDirty = true
|
||||
}
|
||||
|
||||
if (bitmapDirty) {
|
||||
backingBitmap!!.eraseColor(Color.TRANSPARENT)
|
||||
backingCanvas!!.withScale(BACKING_SCALE, BACKING_SCALE) {
|
||||
for (sr in completedStrokes) {
|
||||
drawPath(sr.path, sr.paint)
|
||||
}
|
||||
}
|
||||
bitmapDirty = false
|
||||
}
|
||||
}
|
||||
|
||||
// --- Input handling ---
|
||||
|
||||
@Suppress("ClickableViewAccessibility") // Drawing view — clicks not applicable
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
return when (event.getToolType(0)) {
|
||||
MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.TOOL_TYPE_ERASER -> handleStylusInput(event)
|
||||
MotionEvent.TOOL_TYPE_FINGER -> handleFingerInput(event)
|
||||
else -> super.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleStylusInput(event: MotionEvent): Boolean {
|
||||
if (canvasState.tool == Tool.ERASER || canvasState.tool == Tool.SELECT) {
|
||||
// Eraser and select modes handled in later phases
|
||||
return true
|
||||
}
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val pt = screenToCanonical(event.x, event.y)
|
||||
currentPoints.clear()
|
||||
currentPoints.add(pt[0])
|
||||
currentPoints.add(pt[1])
|
||||
currentPath = Path().apply { moveTo(pt[0], pt[1]) }
|
||||
currentPaint = buildPaint(canvasState.penWidthPt, Color.BLACK)
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val path = currentPath ?: return true
|
||||
// Process historical points for smoothness
|
||||
for (i in 0 until event.historySize) {
|
||||
val pt = screenToCanonical(event.getHistoricalX(i), event.getHistoricalY(i))
|
||||
path.lineTo(pt[0], pt[1])
|
||||
currentPoints.add(pt[0])
|
||||
currentPoints.add(pt[1])
|
||||
}
|
||||
val pt = screenToCanonical(event.x, event.y)
|
||||
path.lineTo(pt[0], pt[1])
|
||||
currentPoints.add(pt[0])
|
||||
currentPoints.add(pt[1])
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (currentPoints.size >= 4) { // At least 2 points (x,y pairs)
|
||||
val points = currentPoints.toFloatArray()
|
||||
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points)
|
||||
}
|
||||
currentPath = null
|
||||
currentPaint = null
|
||||
currentPoints.clear()
|
||||
performClick()
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleFingerInput(event: MotionEvent): Boolean {
|
||||
// Zoom/pan handled in Phase 4
|
||||
return true
|
||||
}
|
||||
|
||||
// --- Coordinate transforms ---
|
||||
|
||||
private fun rebuildViewMatrix() {
|
||||
viewMatrix.reset()
|
||||
val pageW = canvasState.pageSize.widthPt.toFloat()
|
||||
val viewW = width.toFloat().coerceAtLeast(1f)
|
||||
|
||||
val fitScale = viewW / pageW
|
||||
viewMatrix.setScale(fitScale * canvasState.zoom, fitScale * canvasState.zoom)
|
||||
viewMatrix.postTranslate(canvasState.panX, canvasState.panY)
|
||||
|
||||
viewMatrix.invert(inverseMatrix)
|
||||
}
|
||||
|
||||
private fun screenToCanonical(screenX: Float, screenY: Float): FloatArray {
|
||||
val pts = floatArrayOf(screenX, screenY)
|
||||
inverseMatrix.mapPoints(pts)
|
||||
return pts
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private fun buildPathFromPoints(points: FloatArray): Path {
|
||||
val path = Path()
|
||||
if (points.size < 2) return path
|
||||
path.moveTo(points[0], points[1])
|
||||
var i = 2
|
||||
while (i < points.size - 1) {
|
||||
path.lineTo(points[i], points[i + 1])
|
||||
i += 2
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
private fun buildPaint(penSize: Float, color: Int): Paint {
|
||||
return Paint().apply {
|
||||
this.color = color
|
||||
strokeWidth = penSize
|
||||
style = Paint.Style.STROKE
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
strokeJoin = Paint.Join.ROUND
|
||||
isAntiAlias = true
|
||||
}
|
||||
}
|
||||
|
||||
private data class StrokeRender(val path: Path, val paint: Paint, val id: Long)
|
||||
|
||||
companion object {
|
||||
/** Backing bitmap is rendered at 1/4 canonical resolution to save memory. */
|
||||
private const val BACKING_SCALE = 0.25f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.metacircular.engpad.ui.editor
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun EditorToolbar(
|
||||
currentTool: Tool,
|
||||
onToolSelected: (Tool) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(modifier = modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
FilterChip(
|
||||
selected = currentTool == Tool.PEN_FINE,
|
||||
onClick = { onToolSelected(Tool.PEN_FINE) },
|
||||
label = { Text("0.38") },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentTool == Tool.PEN_MEDIUM,
|
||||
onClick = { onToolSelected(Tool.PEN_MEDIUM) },
|
||||
label = { Text("0.5") },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentTool == Tool.ERASER,
|
||||
onClick = { onToolSelected(Tool.ERASER) },
|
||||
label = { Text("Eraser") },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
package net.metacircular.engpad.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
import net.metacircular.engpad.data.model.PageSize
|
||||
import net.metacircular.engpad.data.repository.NotebookRepository
|
||||
import net.metacircular.engpad.data.repository.PageRepository
|
||||
import net.metacircular.engpad.ui.editor.EditorScreen
|
||||
import net.metacircular.engpad.ui.notebooks.NotebookListScreen
|
||||
|
||||
object Routes {
|
||||
const val NOTEBOOKS = "notebooks"
|
||||
const val PAGES = "pages/{notebookId}"
|
||||
const val EDITOR = "editor/{pageId}"
|
||||
const val EDITOR = "editor/{pageId}/{pageSize}"
|
||||
|
||||
fun pages(notebookId: Long) = "pages/$notebookId"
|
||||
fun editor(pageId: Long) = "editor/$pageId"
|
||||
fun editor(pageId: Long, pageSize: PageSize) = "editor/$pageId/${pageSize.name}"
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -37,16 +46,47 @@ fun EngPadNavGraph(
|
||||
arguments = listOf(navArgument("notebookId") { type = NavType.LongType }),
|
||||
) { backStackEntry ->
|
||||
val notebookId = backStackEntry.arguments?.getLong("notebookId") ?: return@composable
|
||||
// Stub — Phase 8 will implement PageListScreen
|
||||
androidx.compose.material3.Text("Pages for notebook $notebookId")
|
||||
// Temporary: navigate directly to first page for now (Phase 8 adds page list)
|
||||
val notebookRepo = remember {
|
||||
NotebookRepository(database.notebookDao(), database.pageDao())
|
||||
}
|
||||
val pageRepo = remember {
|
||||
PageRepository(database.pageDao(), database.strokeDao())
|
||||
}
|
||||
var navigated by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(notebookId) {
|
||||
if (!navigated) {
|
||||
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
|
||||
val pages = pageRepo.getPages(notebookId)
|
||||
// Collect first emission to get the page list
|
||||
pages.collect { pageList ->
|
||||
if (pageList.isNotEmpty() && !navigated) {
|
||||
navigated = true
|
||||
val pageSize = PageSize.fromString(notebook.pageSize)
|
||||
navController.navigate(Routes.editor(pageList[0].id, pageSize)) {
|
||||
// Replace the pages route so back goes to notebook list
|
||||
popUpTo(Routes.NOTEBOOKS)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
composable(
|
||||
route = Routes.EDITOR,
|
||||
arguments = listOf(navArgument("pageId") { type = NavType.LongType }),
|
||||
arguments = listOf(
|
||||
navArgument("pageId") { type = NavType.LongType },
|
||||
navArgument("pageSize") { type = NavType.StringType },
|
||||
),
|
||||
) { backStackEntry ->
|
||||
val pageId = backStackEntry.arguments?.getLong("pageId") ?: return@composable
|
||||
// Stub — Phase 3 will implement EditorScreen
|
||||
androidx.compose.material3.Text("Editor for page $pageId")
|
||||
val pageSizeStr = backStackEntry.arguments?.getString("pageSize") ?: return@composable
|
||||
val pageSize = PageSize.fromString(pageSizeStr)
|
||||
EditorScreen(
|
||||
pageId = pageId,
|
||||
pageSize = pageSize,
|
||||
database = database,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user