UI polish: toolbar redesign, clipboard, snap fix, startup state

Toolbar:
- Replace FilterChip with custom ToolButton (no ripple, instant response)
- Reorder tools: 0.38 0.5 Line Box Eraser Select
- DropdownMenus anchored to buttons via Box wrapper (drop below)
- Page indicator styled as OutlinedButton (visible affordance)
- Cut/Del/Copy/Paste operations (internal clipboard)

Line snap fix:
- Check pen position when timer fires instead of canceling on movement
- Handles EMR stylus micro-tremor correctly

EMR eraser button:
- TOOL_TYPE_ERASER events route to eraser handler regardless of active tool
- Physical eraser end of pen works as temporary eraser

App state:
- Startup navigates to last-visited notebook/page via SharedPreferences
- Closing notebook (X button) clears last notebook so app starts at list
- Updated app icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 16:15:58 -07:00
parent e81dd60f30
commit 408ba57051
9 changed files with 316 additions and 168 deletions

View File

@@ -1,8 +1,32 @@
package net.metacircular.engpad package net.metacircular.engpad
import android.app.Application import android.app.Application
import android.content.Context
import androidx.core.content.edit
import net.metacircular.engpad.data.db.EngPadDatabase 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 {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getLong(KEY_LAST_NOTEBOOK, 0)
}
fun setLastNotebookId(id: Long) {
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
putLong(KEY_LAST_NOTEBOOK, id)
}
}
fun clearLastNotebookId() {
getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit {
remove(KEY_LAST_NOTEBOOK)
}
}
companion object {
private const val PREFS_NAME = "engpad_prefs"
private const val KEY_LAST_NOTEBOOK = "last_notebook_id"
}
} }

View File

@@ -10,13 +10,18 @@ import net.metacircular.engpad.ui.theme.EngPadTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val database = (application as EngPadApp).database val app = application as EngPadApp
val database = app.database
val lastNotebookId = app.getLastNotebookId()
setContent { setContent {
EngPadTheme { EngPadTheme {
val navController = rememberNavController() val navController = rememberNavController()
EngPadNavGraph( EngPadNavGraph(
navController = navController, navController = navController,
database = database, database = database,
lastNotebookId = lastNotebookId,
onNotebookOpened = { app.setLastNotebookId(it) },
onNotebookClosed = { app.clearLastNotebookId() },
) )
} }
} }

View File

@@ -46,6 +46,7 @@ fun EditorScreen(
val canUndo by viewModel.undoManager.canUndo.collectAsState() val canUndo by viewModel.undoManager.canUndo.collectAsState()
val canRedo by viewModel.undoManager.canRedo.collectAsState() val canRedo by viewModel.undoManager.canRedo.collectAsState()
val hasSelection by viewModel.hasSelection.collectAsState() val hasSelection by viewModel.hasSelection.collectAsState()
val canPaste by viewModel.canPaste.collectAsState()
val currentPageIndex by viewModel.currentPageIndex.collectAsState() val currentPageIndex by viewModel.currentPageIndex.collectAsState()
val pages by viewModel.pages.collectAsState() val pages by viewModel.pages.collectAsState()
@@ -108,14 +109,20 @@ fun EditorScreen(
onUndo = { viewModel.undo() }, onUndo = { viewModel.undo() },
onRedo = { viewModel.redo() }, onRedo = { viewModel.redo() },
hasSelection = hasSelection, hasSelection = hasSelection,
onCutSelection = {
viewModel.cutSelection()
canvasView.clearSelection()
},
onDeleteSelection = { onDeleteSelection = {
viewModel.deleteSelection() viewModel.deleteSelection()
canvasView.clearSelection() canvasView.clearSelection()
}, },
onCopySelection = { onCopySelection = {
viewModel.copySelection() viewModel.copyToClipboard()
canvasView.clearSelection() canvasView.clearSelection()
}, },
onPaste = { viewModel.paste() },
canPaste = canPaste,
onExportPdf = { onExportPdf = {
val currentPage = pages.getOrNull(currentPageIndex) ?: return@EditorToolbar val currentPage = pages.getOrNull(currentPageIndex) ?: return@EditorToolbar
val file = PdfExporter.exportPages( val file = PdfExporter.exportPages(

View File

@@ -40,6 +40,10 @@ class EditorViewModel(
val hasSelection: StateFlow<Boolean> = _hasSelection val hasSelection: StateFlow<Boolean> = _hasSelection
private var selectedIds = emptySet<Long>() private var selectedIds = emptySet<Long>()
private val _canPaste = MutableStateFlow(false)
val canPaste: StateFlow<Boolean> = _canPaste
private var clipboard = emptyList<Stroke>()
// Page navigation // Page navigation
private val _pages = MutableStateFlow<List<Page>>(emptyList()) private val _pages = MutableStateFlow<List<Page>>(emptyList())
val pages: StateFlow<List<Page>> = _pages val pages: StateFlow<List<Page>> = _pages
@@ -204,6 +208,40 @@ class EditorViewModel(
} }
} }
fun cutSelection() {
clipboard = _strokes.value.filter { it.id in selectedIds }
_canPaste.value = clipboard.isNotEmpty()
deleteSelection()
}
fun copyToClipboard() {
clipboard = _strokes.value.filter { it.id in selectedIds }
_canPaste.value = clipboard.isNotEmpty()
clearSelection()
}
fun paste() {
if (clipboard.isEmpty()) return
viewModelScope.launch {
val pageId = _currentPageId.value
undoManager.perform(
CopyStrokesAction(
strokes = clipboard,
pageId = pageId,
repository = pageRepository,
onExecute = { copies ->
_strokes.value = _strokes.value + copies
copies.forEach { onStrokeAdded?.invoke(it) }
},
onUndo = { ids ->
_strokes.value = _strokes.value.filter { it.id !in ids }
ids.forEach { onStrokeRemoved?.invoke(it) }
},
)
)
}
}
fun copySelection() { fun copySelection() {
val toCopy = _strokes.value.filter { it.id in selectedIds } val toCopy = _strokes.value.filter { it.id in selectedIds }
if (toCopy.isEmpty()) return if (toCopy.isEmpty()) return

View File

@@ -46,10 +46,11 @@ class PadCanvasView(context: Context) : View(context) {
// --- Line snap --- // --- Line snap ---
private var strokeOriginX = 0f private var strokeOriginX = 0f
private var strokeOriginY = 0f private var strokeOriginY = 0f
private var maxDistFromOrigin = 0f private var lastStylusX = 0f // Current pen position for snap check
private var lastStylusY = 0f
private var isSnappedToLine = false private var isSnappedToLine = false
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val snapRunnable = Runnable { snapToLine() } private val snapRunnable = Runnable { trySnapToLine() }
// --- Box drawing --- // --- Box drawing ---
private var boxStartX = 0f private var boxStartX = 0f
@@ -342,7 +343,8 @@ class PadCanvasView(context: Context) : View(context) {
@Suppress("ClickableViewAccessibility") @Suppress("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
return when (event.getToolType(0)) { return when (event.getToolType(0)) {
MotionEvent.TOOL_TYPE_STYLUS, MotionEvent.TOOL_TYPE_ERASER -> handleStylusInput(event) MotionEvent.TOOL_TYPE_STYLUS -> handleStylusInput(event)
MotionEvent.TOOL_TYPE_ERASER -> handleEraserInput(event) // EMR pen eraser button
MotionEvent.TOOL_TYPE_FINGER -> handleFingerInput(event) MotionEvent.TOOL_TYPE_FINGER -> handleFingerInput(event)
else -> super.onTouchEvent(event) else -> super.onTouchEvent(event)
} }
@@ -372,31 +374,25 @@ class PadCanvasView(context: Context) : View(context) {
val pt = screenToCanonical(event.x, event.y) val pt = screenToCanonical(event.x, event.y)
strokeOriginX = pt[0] strokeOriginX = pt[0]
strokeOriginY = pt[1] strokeOriginY = pt[1]
maxDistFromOrigin = 0f lastStylusX = pt[0]
lastStylusY = pt[1]
isSnappedToLine = false isSnappedToLine = false
currentPoints.clear() currentPoints.clear()
currentPoints.add(pt[0]) currentPoints.add(pt[0])
currentPoints.add(pt[1]) currentPoints.add(pt[1])
currentPath = Path().apply { moveTo(pt[0], pt[1]) } currentPath = Path().apply { moveTo(pt[0], pt[1]) }
currentPaint = buildPaint(canvasState.penWidthPt, Color.BLACK) currentPaint = buildPaint(canvasState.penWidthPt, Color.BLACK)
// Start snap timer // Start snap timer — will check distance when it fires
handler.postDelayed(snapRunnable, LINE_SNAP_DELAY_MS) handler.postDelayed(snapRunnable, LINE_SNAP_DELAY_MS)
invalidate() invalidate()
return true return true
} }
MotionEvent.ACTION_MOVE -> { MotionEvent.ACTION_MOVE -> {
val path = currentPath ?: return true val path = currentPath ?: return true
// Track max distance from origin; cancel snap if pen moved far // Track current position for snap check
if (!isSnappedToLine) { val curPt = screenToCanonical(event.x, event.y)
val pt = screenToCanonical(event.x, event.y) lastStylusX = curPt[0]
val dx = pt[0] - strokeOriginX lastStylusY = curPt[1]
val dy = pt[1] - strokeOriginY
val dist = Math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
if (dist > maxDistFromOrigin) maxDistFromOrigin = dist
if (maxDistFromOrigin > LINE_SNAP_MOVE_THRESHOLD) {
handler.removeCallbacks(snapRunnable)
}
}
if (isSnappedToLine) { if (isSnappedToLine) {
// In snap mode: draw straight line from origin to current point // In snap mode: draw straight line from origin to current point
@@ -445,10 +441,17 @@ class PadCanvasView(context: Context) : View(context) {
return true return true
} }
private fun snapToLine() { /**
if (currentPath != null) { * Called by the snap timer. Checks if pen is still near origin —
* if so, activate line snap. If pen has moved far, do nothing.
*/
private fun trySnapToLine() {
if (currentPath == null) return
val dx = lastStylusX - strokeOriginX
val dy = lastStylusY - strokeOriginY
val dist = Math.sqrt((dx * dx + dy * dy).toDouble()).toFloat()
if (dist <= LINE_SNAP_MOVE_THRESHOLD) {
isSnappedToLine = true isSnappedToLine = true
// Reset path to just origin — next MOVE will draw the straight line
currentPath?.reset() currentPath?.reset()
currentPath?.moveTo(strokeOriginX, strokeOriginY) currentPath?.moveTo(strokeOriginX, strokeOriginY)
currentPoints.clear() currentPoints.clear()
@@ -456,6 +459,7 @@ class PadCanvasView(context: Context) : View(context) {
currentPoints.add(strokeOriginY) currentPoints.add(strokeOriginY)
invalidate() invalidate()
} }
// If pen has moved far, don't snap — just let freehand continue
} }
// --- Box drawing --- // --- Box drawing ---

View File

@@ -1,17 +1,22 @@
package net.metacircular.engpad.ui.editor package net.metacircular.engpad.ui.editor
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilterChip import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
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.runtime.Composable import androidx.compose.runtime.Composable
@@ -21,9 +26,39 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
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.Color
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
/**
* Tool button — simple Surface with border. No ripple animation for fast
* e-ink response. Selected state shown with filled background.
*/
@Composable
private fun ToolButton(
label: String,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Surface(
onClick = onClick,
shape = RoundedCornerShape(8.dp),
color = if (selected) Color.Black else Color.White,
contentColor = if (selected) Color.White else Color.Black,
border = BorderStroke(1.dp, Color.Black),
modifier = modifier.padding(end = 4.dp),
) {
Text(
text = label,
fontSize = 13.sp,
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun EditorToolbar( fun EditorToolbar(
currentTool: Tool, currentTool: Tool,
@@ -33,8 +68,11 @@ fun EditorToolbar(
onUndo: () -> Unit, onUndo: () -> Unit,
onRedo: () -> Unit, onRedo: () -> Unit,
hasSelection: Boolean, hasSelection: Boolean,
onCutSelection: () -> Unit,
onDeleteSelection: () -> Unit, onDeleteSelection: () -> Unit,
onCopySelection: () -> Unit, onCopySelection: () -> Unit,
onPaste: () -> Unit,
canPaste: Boolean,
onExportPdf: () -> Unit, onExportPdf: () -> Unit,
onExportJpg: () -> Unit, onExportJpg: () -> Unit,
onClose: () -> Unit, onClose: () -> Unit,
@@ -46,7 +84,6 @@ fun EditorToolbar(
onLineStyleChanged: (LineStyle) -> Unit, onLineStyleChanged: (LineStyle) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var showBinderMenu by remember { mutableStateOf(false) }
var showGoToPageDialog by remember { mutableStateOf(false) } var showGoToPageDialog by remember { mutableStateOf(false) }
Row( Row(
@@ -54,104 +91,143 @@ fun EditorToolbar(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
// Close button // Close button
TextButton(onClick = onClose) { Text("X") } ToolButton(label = "X", selected = false, onClick = onClose)
// Binder dropdown // Page indicator / binder — outlined button style
TextButton(onClick = { showBinderMenu = true }) { Box {
Text("p$currentPageNumber/$totalPages") var showBinderMenu by remember { mutableStateOf(false) }
} OutlinedButton(
DropdownMenu( onClick = { showBinderMenu = true },
expanded = showBinderMenu, shape = RoundedCornerShape(8.dp),
onDismissRequest = { showBinderMenu = false }, border = BorderStroke(1.dp, Color.Black),
) { ) {
DropdownMenuItem( Text("p$currentPageNumber/$totalPages", fontSize = 13.sp)
text = { Text("View all pages") }, }
onClick = { DropdownMenu(
showBinderMenu = false expanded = showBinderMenu,
onViewAllPages() onDismissRequest = { showBinderMenu = false },
}, ) {
) DropdownMenuItem(
DropdownMenuItem( text = { Text("View all pages") },
text = { Text("Go to page...") }, onClick = {
onClick = { showBinderMenu = false
showBinderMenu = false onViewAllPages()
showGoToPageDialog = true },
}, )
) DropdownMenuItem(
text = { Text("Go to page\u2026") },
onClick = {
showBinderMenu = false
showGoToPageDialog = true
},
)
}
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
// Tool chips // Tools: 0.38 0.5 line box eraser select
FilterChip( ToolButton("0.38", currentTool == Tool.PEN_FINE, { onToolSelected(Tool.PEN_FINE) })
selected = currentTool == Tool.PEN_FINE, ToolButton("0.5", currentTool == Tool.PEN_MEDIUM, { onToolSelected(Tool.PEN_MEDIUM) })
onClick = { onToolSelected(Tool.PEN_FINE) },
label = { Text("0.38") }, // Line button with long-press for style
modifier = Modifier.padding(end = 4.dp), Box {
) var showLineMenu by remember { mutableStateOf(false) }
FilterChip( val lineLabel = when (lineStyle) {
selected = currentTool == Tool.PEN_MEDIUM, LineStyle.PLAIN -> "Line"
onClick = { onToolSelected(Tool.PEN_MEDIUM) }, LineStyle.ARROW -> "\u2192"
label = { Text("0.5") }, LineStyle.DOUBLE_ARROW -> "\u2194"
modifier = Modifier.padding(end = 4.dp), LineStyle.DASHED -> "- -"
) }
FilterChip( Surface(
selected = currentTool == Tool.ERASER, onClick = { onToolSelected(Tool.LINE) },
onClick = { onToolSelected(Tool.ERASER) }, shape = RoundedCornerShape(8.dp),
label = { Text("Eraser") }, color = if (currentTool == Tool.LINE) Color.Black else Color.White,
modifier = Modifier.padding(end = 4.dp), contentColor = if (currentTool == Tool.LINE) Color.White else Color.Black,
) border = BorderStroke(1.dp, Color.Black),
FilterChip( modifier = Modifier
selected = currentTool == Tool.SELECT, .padding(end = 4.dp)
onClick = { onToolSelected(Tool.SELECT) }, .combinedClickable(
label = { Text("Select") }, onClick = { onToolSelected(Tool.LINE) },
modifier = Modifier.padding(end = 4.dp), onLongClick = { showLineMenu = true },
) ),
FilterChip( ) {
selected = currentTool == Tool.BOX, Text(
onClick = { onToolSelected(Tool.BOX) }, text = lineLabel,
label = { Text("Box") }, fontSize = 13.sp,
modifier = Modifier.padding(end = 4.dp), modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
) )
LineToolChip( }
selected = currentTool == Tool.LINE, DropdownMenu(
lineStyle = lineStyle, expanded = showLineMenu,
onSelect = { onToolSelected(Tool.LINE) }, onDismissRequest = { showLineMenu = false },
onStyleChanged = onLineStyleChanged, ) {
) LineStyle.entries.forEach { style ->
if (hasSelection) { val name = when (style) {
TextButton(onClick = onDeleteSelection) { Text("Del") } LineStyle.PLAIN -> "Plain line"
TextButton(onClick = onCopySelection) { Text("Copy") } LineStyle.ARROW -> "Arrow \u2192"
LineStyle.DOUBLE_ARROW -> "Double \u2194"
LineStyle.DASHED -> "Dashed"
}
DropdownMenuItem(
text = { Text(name) },
onClick = {
showLineMenu = false
onLineStyleChanged(style)
onToolSelected(Tool.LINE)
},
)
}
}
} }
ToolButton("Box", currentTool == Tool.BOX, { onToolSelected(Tool.BOX) })
ToolButton("Eraser", currentTool == Tool.ERASER, { onToolSelected(Tool.ERASER) })
ToolButton("Select", currentTool == Tool.SELECT, { onToolSelected(Tool.SELECT) })
// Selection operations: cut / del / copy / paste
if (hasSelection || canPaste) {
Spacer(modifier = Modifier.width(4.dp))
if (hasSelection) {
TextButton(onClick = onCutSelection) { Text("Cut") }
TextButton(onClick = onDeleteSelection) { Text("Del") }
TextButton(onClick = onCopySelection) { Text("Copy") }
}
if (canPaste) {
TextButton(onClick = onPaste) { Text("Paste") }
}
}
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
var showExportMenu by remember { mutableStateOf(false) }
TextButton(onClick = { showExportMenu = true }) { Text("Export") } // Export dropdown — anchored to its button
DropdownMenu( Box {
expanded = showExportMenu, var showExportMenu by remember { mutableStateOf(false) }
onDismissRequest = { showExportMenu = false }, TextButton(onClick = { showExportMenu = true }) { Text("Export") }
) { DropdownMenu(
DropdownMenuItem( expanded = showExportMenu,
text = { Text("Export as PDF") }, onDismissRequest = { showExportMenu = false },
onClick = { ) {
showExportMenu = false DropdownMenuItem(
onExportPdf() text = { Text("PDF") },
}, onClick = {
) showExportMenu = false
DropdownMenuItem( onExportPdf()
text = { Text("Export as JPG") }, },
onClick = { )
showExportMenu = false DropdownMenuItem(
onExportJpg() text = { Text("JPG") },
}, onClick = {
) showExportMenu = false
} onExportJpg()
TextButton(onClick = onUndo, enabled = canUndo) { },
Text("Undo") )
}
} }
TextButton(onClick = onUndo, enabled = canUndo) { Text("Undo") }
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
TextButton(onClick = onRedo, enabled = canRedo) { TextButton(onClick = onRedo, enabled = canRedo) { Text("Redo") }
Text("Redo")
}
} }
if (showGoToPageDialog) { if (showGoToPageDialog) {
@@ -207,53 +283,3 @@ private fun GoToPageDialog(
}, },
) )
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun LineToolChip(
selected: Boolean,
lineStyle: LineStyle,
onSelect: () -> Unit,
onStyleChanged: (LineStyle) -> Unit,
) {
var showStyleMenu by remember { mutableStateOf(false) }
val label = when (lineStyle) {
LineStyle.PLAIN -> "Line"
LineStyle.ARROW -> "Arrow"
LineStyle.DOUBLE_ARROW -> "\u2194" // ↔
LineStyle.DASHED -> "Dash"
}
FilterChip(
selected = selected,
onClick = { onSelect() },
label = { Text(label) },
modifier = Modifier
.padding(end = 4.dp)
.combinedClickable(
onClick = { onSelect() },
onLongClick = { showStyleMenu = true },
),
)
DropdownMenu(
expanded = showStyleMenu,
onDismissRequest = { showStyleMenu = false },
) {
LineStyle.entries.forEach { style ->
val styleName = when (style) {
LineStyle.PLAIN -> "Plain line"
LineStyle.ARROW -> "Single arrow \u2192"
LineStyle.DOUBLE_ARROW -> "Double arrow \u2194"
LineStyle.DASHED -> "Dashed line"
}
DropdownMenuItem(
text = { Text(styleName) },
onClick = {
showStyleMenu = false
onStyleChanged(style)
onSelect()
},
)
}
}
}

View File

@@ -33,12 +33,25 @@ object Routes {
fun EngPadNavGraph( fun EngPadNavGraph(
navController: NavHostController, navController: NavHostController,
database: EngPadDatabase, database: EngPadDatabase,
lastNotebookId: Long = 0,
onNotebookOpened: (Long) -> Unit = {},
onNotebookClosed: () -> Unit = {},
) { ) {
// Auto-navigate to last notebook on startup
var autoNavigated by remember { mutableStateOf(false) }
LaunchedEffect(lastNotebookId) {
if (!autoNavigated && lastNotebookId > 0) {
autoNavigated = true
navController.navigate(Routes.editor(lastNotebookId))
}
}
NavHost(navController = navController, startDestination = Routes.NOTEBOOKS) { NavHost(navController = navController, startDestination = Routes.NOTEBOOKS) {
composable(Routes.NOTEBOOKS) { composable(Routes.NOTEBOOKS) {
NotebookListScreen( NotebookListScreen(
database = database, database = database,
onNotebookClick = { notebookId -> onNotebookClick = { notebookId ->
onNotebookOpened(notebookId)
navController.navigate(Routes.editor(notebookId)) navController.navigate(Routes.editor(notebookId))
}, },
) )
@@ -59,6 +72,7 @@ fun EngPadNavGraph(
var initialPageId by remember { mutableStateOf<Long?>(null) } var initialPageId by remember { mutableStateOf<Long?>(null) }
LaunchedEffect(notebookId) { LaunchedEffect(notebookId) {
onNotebookOpened(notebookId)
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
pageSize = PageSize.fromString(notebook.pageSize) pageSize = PageSize.fromString(notebook.pageSize)
// Use last page or fall back to first page // Use last page or fall back to first page
@@ -90,6 +104,7 @@ fun EngPadNavGraph(
initialPageId = pid, initialPageId = pid,
database = database, database = database,
onClose = { onClose = {
onNotebookClosed()
navController.popBackStack(Routes.NOTEBOOKS, false) navController.popBackStack(Routes.NOTEBOOKS, false)
}, },
onViewAllPages = { onViewAllPages = {

View File

@@ -4,7 +4,13 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<!-- White background -->
<path <path
android:fillColor="#FFFFFF" android:fillColor="#F5F5F5"
android:pathData="M0,0h108v108H0z" /> android:pathData="M0,0h108v108H0z" />
<!-- Subtle grid lines evoking graph paper / blueprint -->
<path
android:strokeColor="#E0E0E0"
android:strokeWidth="0.5"
android:pathData="M18,0 V108 M30,0 V108 M42,0 V108 M54,0 V108 M66,0 V108 M78,0 V108 M90,0 V108 M0,18 H108 M0,30 H108 M0,42 H108 M0,54 H108 M0,66 H108 M0,78 H108 M0,90 H108" />
</vector> </vector>

View File

@@ -4,8 +4,31 @@
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<!-- Simple pen icon --> <!--
Architecture node graph: three connected nodes in a triangle.
All content within the 66dp adaptive-icon safe zone (2187).
Node centers: top (54,35), bottom-left (37,67), bottom-right (71,67).
-->
<!-- Edges (drawn first, behind nodes) -->
<path <path
android:fillColor="#000000" android:strokeColor="#2C2C2C"
android:pathData="M54,24 L62,72 L54,80 L46,72 Z" /> android:strokeWidth="2"
android:strokeLineCap="round"
android:pathData="M54,35 L37,67 M54,35 L71,67 M37,67 L71,67" />
<!-- Top node -->
<path
android:fillColor="#2C2C2C"
android:pathData="M48,29 L60,29 Q63,29 63,32 L63,38 Q63,41 60,41 L48,41 Q45,41 45,38 L45,32 Q45,29 48,29 Z" />
<!-- Bottom-left node -->
<path
android:fillColor="#2C2C2C"
android:pathData="M31,61 L43,61 Q46,61 46,64 L46,70 Q46,73 43,73 L31,73 Q28,73 28,70 L28,64 Q28,61 31,61 Z" />
<!-- Bottom-right node -->
<path
android:fillColor="#2C2C2C"
android:pathData="M65,61 L77,61 Q80,61 80,64 L80,70 Q80,73 77,73 L65,73 Q62,73 62,70 L62,64 Q62,61 65,61 Z" />
</vector> </vector>