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:
@@ -1,8 +1,32 @@
|
||||
package net.metacircular.engpad
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.core.content.edit
|
||||
import net.metacircular.engpad.data.db.EngPadDatabase
|
||||
|
||||
class EngPadApp : Application() {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,18 @@ import net.metacircular.engpad.ui.theme.EngPadTheme
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val database = (application as EngPadApp).database
|
||||
val app = application as EngPadApp
|
||||
val database = app.database
|
||||
val lastNotebookId = app.getLastNotebookId()
|
||||
setContent {
|
||||
EngPadTheme {
|
||||
val navController = rememberNavController()
|
||||
EngPadNavGraph(
|
||||
navController = navController,
|
||||
database = database,
|
||||
lastNotebookId = lastNotebookId,
|
||||
onNotebookOpened = { app.setLastNotebookId(it) },
|
||||
onNotebookClosed = { app.clearLastNotebookId() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ fun EditorScreen(
|
||||
val canUndo by viewModel.undoManager.canUndo.collectAsState()
|
||||
val canRedo by viewModel.undoManager.canRedo.collectAsState()
|
||||
val hasSelection by viewModel.hasSelection.collectAsState()
|
||||
val canPaste by viewModel.canPaste.collectAsState()
|
||||
val currentPageIndex by viewModel.currentPageIndex.collectAsState()
|
||||
val pages by viewModel.pages.collectAsState()
|
||||
|
||||
@@ -108,14 +109,20 @@ fun EditorScreen(
|
||||
onUndo = { viewModel.undo() },
|
||||
onRedo = { viewModel.redo() },
|
||||
hasSelection = hasSelection,
|
||||
onCutSelection = {
|
||||
viewModel.cutSelection()
|
||||
canvasView.clearSelection()
|
||||
},
|
||||
onDeleteSelection = {
|
||||
viewModel.deleteSelection()
|
||||
canvasView.clearSelection()
|
||||
},
|
||||
onCopySelection = {
|
||||
viewModel.copySelection()
|
||||
viewModel.copyToClipboard()
|
||||
canvasView.clearSelection()
|
||||
},
|
||||
onPaste = { viewModel.paste() },
|
||||
canPaste = canPaste,
|
||||
onExportPdf = {
|
||||
val currentPage = pages.getOrNull(currentPageIndex) ?: return@EditorToolbar
|
||||
val file = PdfExporter.exportPages(
|
||||
|
||||
@@ -40,6 +40,10 @@ class EditorViewModel(
|
||||
val hasSelection: StateFlow<Boolean> = _hasSelection
|
||||
private var selectedIds = emptySet<Long>()
|
||||
|
||||
private val _canPaste = MutableStateFlow(false)
|
||||
val canPaste: StateFlow<Boolean> = _canPaste
|
||||
private var clipboard = emptyList<Stroke>()
|
||||
|
||||
// Page navigation
|
||||
private val _pages = MutableStateFlow<List<Page>>(emptyList())
|
||||
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() {
|
||||
val toCopy = _strokes.value.filter { it.id in selectedIds }
|
||||
if (toCopy.isEmpty()) return
|
||||
|
||||
@@ -46,10 +46,11 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
// --- Line snap ---
|
||||
private var strokeOriginX = 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 val handler = Handler(Looper.getMainLooper())
|
||||
private val snapRunnable = Runnable { snapToLine() }
|
||||
private val snapRunnable = Runnable { trySnapToLine() }
|
||||
|
||||
// --- Box drawing ---
|
||||
private var boxStartX = 0f
|
||||
@@ -342,7 +343,8 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
@Suppress("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
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)
|
||||
else -> super.onTouchEvent(event)
|
||||
}
|
||||
@@ -372,31 +374,25 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
val pt = screenToCanonical(event.x, event.y)
|
||||
strokeOriginX = pt[0]
|
||||
strokeOriginY = pt[1]
|
||||
maxDistFromOrigin = 0f
|
||||
lastStylusX = pt[0]
|
||||
lastStylusY = pt[1]
|
||||
isSnappedToLine = false
|
||||
currentPoints.clear()
|
||||
currentPoints.add(pt[0])
|
||||
currentPoints.add(pt[1])
|
||||
currentPath = Path().apply { moveTo(pt[0], pt[1]) }
|
||||
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)
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val path = currentPath ?: return true
|
||||
// Track max distance from origin; cancel snap if pen moved far
|
||||
if (!isSnappedToLine) {
|
||||
val pt = screenToCanonical(event.x, event.y)
|
||||
val dx = pt[0] - strokeOriginX
|
||||
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)
|
||||
}
|
||||
}
|
||||
// Track current position for snap check
|
||||
val curPt = screenToCanonical(event.x, event.y)
|
||||
lastStylusX = curPt[0]
|
||||
lastStylusY = curPt[1]
|
||||
|
||||
if (isSnappedToLine) {
|
||||
// In snap mode: draw straight line from origin to current point
|
||||
@@ -445,10 +441,17 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
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
|
||||
// Reset path to just origin — next MOVE will draw the straight line
|
||||
currentPath?.reset()
|
||||
currentPath?.moveTo(strokeOriginX, strokeOriginY)
|
||||
currentPoints.clear()
|
||||
@@ -456,6 +459,7 @@ class PadCanvasView(context: Context) : View(context) {
|
||||
currentPoints.add(strokeOriginY)
|
||||
invalidate()
|
||||
}
|
||||
// If pen has moved far, don't snap — just let freehand continue
|
||||
}
|
||||
|
||||
// --- Box drawing ---
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
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.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,9 +26,39 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
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
|
||||
fun EditorToolbar(
|
||||
currentTool: Tool,
|
||||
@@ -33,8 +68,11 @@ fun EditorToolbar(
|
||||
onUndo: () -> Unit,
|
||||
onRedo: () -> Unit,
|
||||
hasSelection: Boolean,
|
||||
onCutSelection: () -> Unit,
|
||||
onDeleteSelection: () -> Unit,
|
||||
onCopySelection: () -> Unit,
|
||||
onPaste: () -> Unit,
|
||||
canPaste: Boolean,
|
||||
onExportPdf: () -> Unit,
|
||||
onExportJpg: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
@@ -46,7 +84,6 @@ fun EditorToolbar(
|
||||
onLineStyleChanged: (LineStyle) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var showBinderMenu by remember { mutableStateOf(false) }
|
||||
var showGoToPageDialog by remember { mutableStateOf(false) }
|
||||
|
||||
Row(
|
||||
@@ -54,104 +91,143 @@ fun EditorToolbar(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
// Close button
|
||||
TextButton(onClick = onClose) { Text("X") }
|
||||
ToolButton(label = "X", selected = false, onClick = onClose)
|
||||
|
||||
// Binder dropdown
|
||||
TextButton(onClick = { showBinderMenu = true }) {
|
||||
Text("p$currentPageNumber/$totalPages")
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showBinderMenu,
|
||||
onDismissRequest = { showBinderMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("View all pages") },
|
||||
onClick = {
|
||||
showBinderMenu = false
|
||||
onViewAllPages()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Go to page...") },
|
||||
onClick = {
|
||||
showBinderMenu = false
|
||||
showGoToPageDialog = true
|
||||
},
|
||||
)
|
||||
// Page indicator / binder — outlined button style
|
||||
Box {
|
||||
var showBinderMenu by remember { mutableStateOf(false) }
|
||||
OutlinedButton(
|
||||
onClick = { showBinderMenu = true },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
border = BorderStroke(1.dp, Color.Black),
|
||||
) {
|
||||
Text("p$currentPageNumber/$totalPages", fontSize = 13.sp)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showBinderMenu,
|
||||
onDismissRequest = { showBinderMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("View all pages") },
|
||||
onClick = {
|
||||
showBinderMenu = false
|
||||
onViewAllPages()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Go to page\u2026") },
|
||||
onClick = {
|
||||
showBinderMenu = false
|
||||
showGoToPageDialog = true
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
// Tool chips
|
||||
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),
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentTool == Tool.SELECT,
|
||||
onClick = { onToolSelected(Tool.SELECT) },
|
||||
label = { Text("Select") },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
FilterChip(
|
||||
selected = currentTool == Tool.BOX,
|
||||
onClick = { onToolSelected(Tool.BOX) },
|
||||
label = { Text("Box") },
|
||||
modifier = Modifier.padding(end = 4.dp),
|
||||
)
|
||||
LineToolChip(
|
||||
selected = currentTool == Tool.LINE,
|
||||
lineStyle = lineStyle,
|
||||
onSelect = { onToolSelected(Tool.LINE) },
|
||||
onStyleChanged = onLineStyleChanged,
|
||||
)
|
||||
if (hasSelection) {
|
||||
TextButton(onClick = onDeleteSelection) { Text("Del") }
|
||||
TextButton(onClick = onCopySelection) { Text("Copy") }
|
||||
// Tools: 0.38 0.5 line box eraser select
|
||||
ToolButton("0.38", currentTool == Tool.PEN_FINE, { onToolSelected(Tool.PEN_FINE) })
|
||||
ToolButton("0.5", currentTool == Tool.PEN_MEDIUM, { onToolSelected(Tool.PEN_MEDIUM) })
|
||||
|
||||
// Line button with long-press for style
|
||||
Box {
|
||||
var showLineMenu by remember { mutableStateOf(false) }
|
||||
val lineLabel = when (lineStyle) {
|
||||
LineStyle.PLAIN -> "Line"
|
||||
LineStyle.ARROW -> "\u2192"
|
||||
LineStyle.DOUBLE_ARROW -> "\u2194"
|
||||
LineStyle.DASHED -> "- -"
|
||||
}
|
||||
Surface(
|
||||
onClick = { onToolSelected(Tool.LINE) },
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
color = if (currentTool == Tool.LINE) Color.Black else Color.White,
|
||||
contentColor = if (currentTool == Tool.LINE) Color.White else Color.Black,
|
||||
border = BorderStroke(1.dp, Color.Black),
|
||||
modifier = Modifier
|
||||
.padding(end = 4.dp)
|
||||
.combinedClickable(
|
||||
onClick = { onToolSelected(Tool.LINE) },
|
||||
onLongClick = { showLineMenu = true },
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = lineLabel,
|
||||
fontSize = 13.sp,
|
||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showLineMenu,
|
||||
onDismissRequest = { showLineMenu = false },
|
||||
) {
|
||||
LineStyle.entries.forEach { style ->
|
||||
val name = when (style) {
|
||||
LineStyle.PLAIN -> "Plain line"
|
||||
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))
|
||||
var showExportMenu by remember { mutableStateOf(false) }
|
||||
TextButton(onClick = { showExportMenu = true }) { Text("Export") }
|
||||
DropdownMenu(
|
||||
expanded = showExportMenu,
|
||||
onDismissRequest = { showExportMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Export as PDF") },
|
||||
onClick = {
|
||||
showExportMenu = false
|
||||
onExportPdf()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Export as JPG") },
|
||||
onClick = {
|
||||
showExportMenu = false
|
||||
onExportJpg()
|
||||
},
|
||||
)
|
||||
}
|
||||
TextButton(onClick = onUndo, enabled = canUndo) {
|
||||
Text("Undo")
|
||||
|
||||
// Export dropdown — anchored to its button
|
||||
Box {
|
||||
var showExportMenu by remember { mutableStateOf(false) }
|
||||
TextButton(onClick = { showExportMenu = true }) { Text("Export") }
|
||||
DropdownMenu(
|
||||
expanded = showExportMenu,
|
||||
onDismissRequest = { showExportMenu = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("PDF") },
|
||||
onClick = {
|
||||
showExportMenu = false
|
||||
onExportPdf()
|
||||
},
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("JPG") },
|
||||
onClick = {
|
||||
showExportMenu = false
|
||||
onExportJpg()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = onUndo, enabled = canUndo) { Text("Undo") }
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
TextButton(onClick = onRedo, enabled = canRedo) {
|
||||
Text("Redo")
|
||||
}
|
||||
TextButton(onClick = onRedo, enabled = canRedo) { Text("Redo") }
|
||||
}
|
||||
|
||||
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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,25 @@ object Routes {
|
||||
fun EngPadNavGraph(
|
||||
navController: NavHostController,
|
||||
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) {
|
||||
composable(Routes.NOTEBOOKS) {
|
||||
NotebookListScreen(
|
||||
database = database,
|
||||
onNotebookClick = { notebookId ->
|
||||
onNotebookOpened(notebookId)
|
||||
navController.navigate(Routes.editor(notebookId))
|
||||
},
|
||||
)
|
||||
@@ -59,6 +72,7 @@ fun EngPadNavGraph(
|
||||
var initialPageId by remember { mutableStateOf<Long?>(null) }
|
||||
|
||||
LaunchedEffect(notebookId) {
|
||||
onNotebookOpened(notebookId)
|
||||
val notebook = notebookRepo.getById(notebookId) ?: return@LaunchedEffect
|
||||
pageSize = PageSize.fromString(notebook.pageSize)
|
||||
// Use last page or fall back to first page
|
||||
@@ -90,6 +104,7 @@ fun EngPadNavGraph(
|
||||
initialPageId = pid,
|
||||
database = database,
|
||||
onClose = {
|
||||
onNotebookClosed()
|
||||
navController.popBackStack(Routes.NOTEBOOKS, false)
|
||||
},
|
||||
onViewAllPages = {
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- White background -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillColor="#F5F5F5"
|
||||
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>
|
||||
|
||||
@@ -4,8 +4,31 @@
|
||||
android:height="108dp"
|
||||
android:viewportWidth="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 (21–87).
|
||||
Node centers: top (54,35), bottom-left (37,67), bottom-right (71,67).
|
||||
-->
|
||||
|
||||
<!-- Edges (drawn first, behind nodes) -->
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M54,24 L62,72 L54,80 L46,72 Z" />
|
||||
android:strokeColor="#2C2C2C"
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user