From 6a0a0d7ee33bba953efbe78c4b53a968e5c7c742 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sat, 7 Mar 2026 19:48:31 +0100 Subject: [PATCH 01/21] Refactor `Toolbar` and `ToolbarMenu` to use callback functions instead of direct dependency injection of `NavController`, `AppRepository`, and `ExportEngine`. Key changes: - Moved logic for navigation, image picking, and exporting from `Toolbar` components to `EditorView`. - Introduced explicit callback parameters (`onNavigateToLibrary`, `onNavigateToPages`, `onExport`, etc.) to the `Toolbar` composable. - Optimized page number calculation by moving the logic to `EditorView`. - Cleaned up unused imports and simplified the `PositionedToolbar` internal structure. - Improved separation of concerns between UI components and data repositories. --- .../com/ethran/notable/editor/EditorView.kt | 85 +++++++++++++-- .../notable/editor/ui/toolbar/Toolbar.kt | 100 ++++-------------- .../notable/editor/ui/toolbar/ToolbarMenu.kt | 72 +++---------- 3 files changed, 113 insertions(+), 144 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 4b09bb0d..ec9b5a80 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -16,11 +16,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri import androidx.navigation.NavController import com.ethran.notable.data.AppRepository +import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.data.datastore.GlobalAppSettings +import com.ethran.notable.data.db.Notebook +import com.ethran.notable.data.db.getPageIndex +import com.ethran.notable.data.db.getParentFolder +import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History import com.ethran.notable.editor.ui.EditorSurface @@ -37,7 +43,9 @@ import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import com.ethran.notable.ui.convertDpToPixel import com.ethran.notable.ui.theme.InkaTheme +import com.ethran.notable.ui.views.BugReportDestination import com.ethran.notable.ui.views.LibraryDestination +import com.ethran.notable.ui.views.PagesDestination import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -197,7 +205,13 @@ fun EditorView( Spacer(modifier = Modifier.weight(1f)) ScrollIndicator(state = editorState) } - PositionedToolbar(exportEngine,navController, appRepository, editorState, editorControlTower) + PositionedToolbar( + exportEngine, + navController, + appRepository, + editorState, + editorControlTower + ) HorizontalScrollIndicator(state = editorState) } } @@ -213,13 +227,72 @@ fun PositionedToolbar( editorControlTower: EditorControlTower ) { val position = GlobalAppSettings.current.toolbarPosition + val scope = rememberCoroutineScope() + val context = LocalContext.current + + var book by remember(editorState.bookId) { mutableStateOf(null) } + LaunchedEffect(editorState.bookId) { + if (editorState.bookId != null) { + val loadedBook = withContext(Dispatchers.IO) { + appRepository.bookRepository.getById(editorState.bookId) + } + book = loadedBook + } + } + + val pageNumberInfo: String = remember(book?.id, editorState.currentPageId) { + book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" + } + "/" + (book?.pageIds?.size?.toString() ?: "?") + + val toolbar = @Composable { + Toolbar( + state = editorState, + controlTower = editorControlTower, + pageNumberInfo = pageNumberInfo, + onNavigateToLibrary = { + scope.launch { + val page = withContext(Dispatchers.IO) { + appRepository.pageRepository.getById(editorState.currentPageId) + } + val parentFolder = withContext(Dispatchers.IO) { + page?.getParentFolder(appRepository.bookRepository) + } + navController.navigate(LibraryDestination.createRoute(parentFolder)) + } + }, + onNavigateToBugReport = { navController.navigate(BugReportDestination.route) }, + onNavigateToPages = { + if (editorState.bookId != null) { + navController.navigate(PagesDestination.createRoute(editorState.bookId)) + } + }, + onNavigateToHome = { navController.navigate("library") }, + onToggleScribbleToErase = { enabled -> + scope.launch(Dispatchers.IO) { + appRepository.kvProxy.setAppSettings( + GlobalAppSettings.current.copy(scribbleToEraseEnabled = enabled) + ) + } + }, + onImagePicked = { uri -> + scope.launch(Dispatchers.IO) { + try { + val copiedFile = copyImageToDatabase(context, uri) + CanvasEventBus.addImageByUri.value = copiedFile.toUri() + } catch (e: Exception) { + log.e("ImagePicker: copy failed: ${e.message}", e) + } + } + }, + onExport = { target, format -> + exportEngine.export(target, format) + } + ) + } when (position) { AppSettings.Position.Top -> { - Toolbar( - exportEngine, - navController, appRepository, editorState, editorControlTower - ) + toolbar() } AppSettings.Position.Bottom -> { @@ -229,7 +302,7 @@ fun PositionedToolbar( .fillMaxHeight() ) { Spacer(modifier = Modifier.weight(1f)) - Toolbar(exportEngine, navController, appRepository, editorState, editorControlTower) + toolbar() } } } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 418911fd..419aeb3c 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -1,6 +1,7 @@ package com.ethran.notable.editor.ui.toolbar +import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia @@ -19,48 +20,33 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import androidx.navigation.NavController import com.ethran.notable.R -import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.data.db.Notebook -import com.ethran.notable.data.db.getPageIndex -import com.ethran.notable.data.db.getParentFolder import com.ethran.notable.editor.EditorControlTower import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting -import com.ethran.notable.io.ExportEngine +import com.ethran.notable.io.ExportFormat +import com.ethran.notable.io.ExportTarget import com.ethran.notable.ui.dialogs.BackgroundSelector import com.ethran.notable.ui.noRippleClickable -import com.ethran.notable.ui.views.BugReportDestination -import com.ethran.notable.ui.views.LibraryDestination -import com.ethran.notable.ui.views.PagesDestination import compose.icons.FeatherIcons import compose.icons.feathericons.Clipboard import compose.icons.feathericons.EyeOff import compose.icons.feathericons.RefreshCcw import io.shipbook.shipbooksdk.ShipBook -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext private val log = ShipBook.getLogger("Toolbar") @@ -103,19 +89,22 @@ private val SIZES_MARKER_DEFAULT = listOf("M" to 25f, "L" to 40f, "XL" to 60f, " @Composable fun Toolbar( - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, state: EditorState, - controlTower: EditorControlTower + controlTower: EditorControlTower, + pageNumberInfo: String, + onNavigateToLibrary: () -> Unit, + onNavigateToBugReport: () -> Unit, + onNavigateToPages: () -> Unit, + onNavigateToHome: () -> Unit, + onToggleScribbleToErase: (Boolean) -> Unit, + onImagePicked: (Uri) -> Unit, + onExport: suspend (ExportTarget, ExportFormat) -> String ) { val scope = rememberCoroutineScope() - val context = LocalContext.current // Observe zoom level to decide button visibility val zoomLevel by state.pageView.zoomLevel.collectAsState() - val repository = appRepository.bookRepository // Create an activity result launcher for picking visual media (images in this case) val pickMedia = rememberLauncherForActivityResult(contract = PickVisualMedia()) { uri -> @@ -123,25 +112,11 @@ fun Toolbar( log.w("PickVisualMedia: uri is null (user cancelled or provider returned null)") return@rememberLauncherForActivityResult } - scope.launch(Dispatchers.IO) { - try { - // copy image to documents/notabledb/images/filename - val copiedFile = copyImageToDatabase(context, uri) - - // Set isImageLoaded to true - log.i("Image was received and copied, it is now at:${copiedFile.toUri()}") - CanvasEventBus.addImageByUri.value = copiedFile.toUri() - - } catch (e: Exception) { - log.e("ImagePicker: copy failed: ${e.message}", e) - } - } - + onImagePicked(uri) } // on exit of toolbar, update drawing state LaunchedEffect(state.menuStates.isBackgroundSelectorModalOpen, state.menuStates.isMenuOpen) { - // TODO: move it to menuState. log.i("Updating drawing state") state.checkForSelectionsAndMenus() } @@ -345,13 +320,7 @@ fun Toolbar( onMenuOpenChange = { state.menuStates.isStrokeSelectionOpen = it }, value = state.eraser, onChange = { state.eraser = it }, - toggleScribbleToErase = { - scope.launch(Dispatchers.IO) { - appRepository.kvProxy.setAppSettings( - GlobalAppSettings.current.copy(scribbleToEraseEnabled = it) - ) - } - } + toggleScribbleToErase = onToggleScribbleToErase ) Box( Modifier @@ -456,19 +425,6 @@ fun Toolbar( .background(Color.Black) ) if (state.bookId != null) { - var book by remember(state.bookId) { mutableStateOf(null) } - LaunchedEffect(state.bookId) { - val loadedBook = withContext(Dispatchers.IO) { - repository.getById(state.bookId) - } - book = loadedBook - } - - val pageNumber: String = remember(book?.id, state.currentPageId) { - book?.let { (it.getPageIndex(state.currentPageId) + 1).toString() } ?: "?" - } - val totalPageNumber: String = book?.pageIds?.size?.toString() ?: "?" - Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -476,12 +432,10 @@ fun Toolbar( .padding(10.dp, 0.dp) ) { Text( - text = "${pageNumber}/${totalPageNumber}", + text = pageNumberInfo, fontWeight = FontWeight.Light, modifier = Modifier.noRippleClickable { - navController.navigate( - PagesDestination.createRoute(state.bookId) - ) + onNavigateToPages() }, textAlign = TextAlign.Center ) @@ -497,9 +451,7 @@ fun Toolbar( ToolbarButton( iconId = R.drawable.home, // Replace with your library icon resource contentDescription = "library", - onSelect = { - navController.navigate("library") // Navigate to main library - } + onSelect = onNavigateToHome ) Box( Modifier @@ -515,21 +467,9 @@ fun Toolbar( ) if (state.menuStates.isMenuOpen) ToolbarMenu( - exportEngine = exportEngine, - goToBugReport = { navController.navigate(BugReportDestination.route) }, - goToLibrary = { - scope.launch { - val page = withContext(Dispatchers.IO) { - appRepository.pageRepository.getById(state.currentPageId) - } - val parentFolder = withContext(Dispatchers.IO) { - page?.getParentFolder(appRepository.bookRepository) - } - navController.navigate( - LibraryDestination.createRoute(parentFolder) - ) - } - }, + onExport = onExport, + goToBugReport = onNavigateToBugReport, + goToLibrary = onNavigateToLibrary, currentPageId = state.currentPageId, currentBookId = state.bookId, onClose = { state.menuStates.isMenuOpen = false }, diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index b513e039..ce45952f 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -19,16 +19,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.ethran.notable.R -import com.ethran.notable.data.AppRepository import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.editor.canvas.CanvasEventBus -import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget import com.ethran.notable.ui.LocalSnackContext @@ -40,7 +37,7 @@ import kotlinx.coroutines.launch @Composable fun ToolbarMenu( - exportEngine: ExportEngine, + onExport: suspend (ExportTarget, ExportFormat) -> String, goToBugReport: () -> Unit, goToLibrary: () -> Unit, currentPageId: String, @@ -59,7 +56,7 @@ fun ToolbarMenu( properties = PopupProperties(focusable = true), ) { ToolbarMenuContent( - exportEngine = exportEngine, + onExport = onExport, goToBugReport = goToBugReport, goToLibrary = goToLibrary, currentPageId = currentPageId, @@ -72,7 +69,7 @@ fun ToolbarMenu( @Composable private fun ToolbarMenuContent( - exportEngine: ExportEngine, + onExport: suspend (ExportTarget, ExportFormat) -> String, goToBugReport: () -> Unit, goToLibrary: () -> Unit, currentPageId: String, @@ -80,17 +77,8 @@ private fun ToolbarMenuContent( onClose: () -> Unit, onBackgroundSelectorModalOpen: () -> Unit ) { - - val context = LocalContext.current val scope = rememberCoroutineScope() val snackManager = LocalSnackContext.current -// val appRepository = remember { AppRepository(context) } -// val page = appRepository.pageRepository.getById(currentPageId)!! -// val book = -// if (page.notebookId != null) appRepository.bookRepository.getById(page.notebookId) else null - -// val parentFolder = if (book != null) book.parentFolderId -// else page.parentFolderId val exportingPageToPdfMsg = stringResource(R.string.exporting_the_page_to, "PDF") val exportingPageToPngMsg = stringResource(R.string.exporting_the_page_to, "PNG") @@ -103,14 +91,13 @@ private fun ToolbarMenuContent( Column( Modifier - .padding(bottom = (BUTTON_SIZE + 5).dp) // For toolbar is located at the button, + .padding(bottom = (BUTTON_SIZE + 5).dp) .border(1.dp, Color.Black, RectangleShape) .background(Color.White) .width(IntrinsicSize.Max) ) { // Library MenuItem(stringResource(R.string.home_view_name)) { -// goToLibrary() onClose() } @@ -120,10 +107,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "PDF")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToPdfMsg) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = ExportFormat.PDF - ) + onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.PDF) } } onClose() @@ -131,10 +115,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "PNG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToPngMsg) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = ExportFormat.PNG - ) + onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.PNG) } } onClose() @@ -142,10 +123,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "JPEG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToJpegMsg) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = ExportFormat.JPEG - ) + onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.JPEG) } } onClose() @@ -153,10 +131,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "xopp")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToXoppMsg) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = ExportFormat.XOPP - ) + onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.XOPP) } } onClose() @@ -168,10 +143,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_book_to, "PDF")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToPdfMsg) { - exportEngine.export( - target = ExportTarget.Book(bookId = currentBookId), - format = ExportFormat.PDF - ) + onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.PDF) } } onClose() @@ -179,10 +151,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_book_to, "PNG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToPngMsg) { - exportEngine.export( - target = ExportTarget.Book(bookId = currentBookId), - format = ExportFormat.PNG - ) + onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.PNG) } } onClose() @@ -190,10 +159,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_book_to, "xopp")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToXoppMsg) { - exportEngine.export( - target = ExportTarget.Book(bookId = currentBookId), - format = ExportFormat.XOPP - ) + onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.XOPP) } } onClose() @@ -220,7 +186,6 @@ private fun ToolbarMenuContent( } MenuItem(stringResource(R.string.bug_report)) { -// navController.navigate("bugReport") goToBugReport() onClose() } @@ -233,9 +198,9 @@ private fun MenuItem( ) { Box( Modifier - .fillMaxWidth() // occupy the menu's width - .noRippleClickable { onClick() } // click covers entire box - .padding(horizontal = 10.dp, vertical = 8.dp) // inner spacing + .fillMaxWidth() + .noRippleClickable { onClick() } + .padding(horizontal = 10.dp, vertical = 8.dp) ) { Text( text = label, @@ -254,12 +219,3 @@ private fun ColumnScope.DividerCentered() { .background(Color(0xFF777777)) ) } - - -// TODO: Fix it -@Preview(showBackground = true) -@Composable -private fun ToolbarMenuPreview() { - -} - From 48f46a594c863590d4de09075ebae7d279e6c699 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sat, 7 Mar 2026 22:04:53 +0100 Subject: [PATCH 02/21] refactor Toolbar to use a decoupled UI state and content function --- .../notable/editor/ui/toolbar/Toolbar.kt | 432 ++++++++++-------- 1 file changed, 252 insertions(+), 180 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 419aeb3c..f5505c89 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ethran.notable.R import com.ethran.notable.data.datastore.AppSettings @@ -35,6 +36,7 @@ import com.ethran.notable.editor.EditorControlTower import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportFormat @@ -50,37 +52,28 @@ import kotlinx.coroutines.launch private val log = ShipBook.getLogger("Toolbar") -fun presentlyUsedToolIcon(mode: Mode, pen: Pen): Int { - return when (mode) { - Mode.Draw -> { - when (pen) { - Pen.BALLPEN -> R.drawable.ballpen - Pen.REDBALLPEN -> R.drawable.ballpenred - Pen.BLUEBALLPEN -> R.drawable.ballpenblue - Pen.GREENBALLPEN -> R.drawable.ballpengreen - Pen.FOUNTAIN -> R.drawable.fountain - Pen.BRUSH -> R.drawable.brush - Pen.MARKER -> R.drawable.marker - Pen.PENCIL -> R.drawable.pencil - Pen.DASHED -> R.drawable.line_dashed - } - } - - Mode.Erase -> R.drawable.eraser - Mode.Select -> R.drawable.lasso - Mode.Line -> R.drawable.line - } -} - -fun isSelected(state: EditorState, penType: Pen): Boolean { - return if (state.mode == Mode.Draw && state.pen == penType) { - true - } else if (state.mode == Mode.Line && state.pen == penType) { - true - } else { - false - } -} +/** + * UI State for the Toolbar to make it previable and decoupled from logic. + */ +data class ToolbarUiState( + val isToolbarOpen: Boolean = true, + val mode: Mode = Mode.Draw, + val pen: Pen = Pen.BALLPEN, + val penSettings: Map = emptyMap(), + val eraser: Eraser = Eraser.PEN, + val isMenuOpen: Boolean = false, + val isStrokeSelectionOpen: Boolean = false, + val isBackgroundSelectorModalOpen: Boolean = false, + val pageNumberInfo: String = "1/1", + val hasClipboard: Boolean = false, + val showResetView: Boolean = false, + // Background Selector specific data + val backgroundType: String = "native", + val backgroundPath: String = "blank", + val backgroundPageNumber: Int = 0, + val notebookId: String? = null, + val currentPageNumber: Int = 0 +) private val SIZES_STROKES_DEFAULT = listOf("S" to 3f, "M" to 5f, "L" to 10f, "XL" to 20f) @@ -104,6 +97,94 @@ fun Toolbar( // Observe zoom level to decide button visibility val zoomLevel by state.pageView.zoomLevel.collectAsState() + val showResetView = state.pageView.scroll != androidx.compose.ui.geometry.Offset.Zero || zoomLevel != 1.0f + + val uiState = ToolbarUiState( + isToolbarOpen = state.isToolbarOpen, + mode = state.mode, + pen = state.pen, + penSettings = state.penSettings, + eraser = state.eraser, + isMenuOpen = state.menuStates.isMenuOpen, + isStrokeSelectionOpen = state.menuStates.isStrokeSelectionOpen, + isBackgroundSelectorModalOpen = state.menuStates.isBackgroundSelectorModalOpen, + pageNumberInfo = pageNumberInfo, + hasClipboard = state.clipboard != null, + showResetView = showResetView, + backgroundType = state.pageView.pageFromDb?.backgroundType ?: "native", + backgroundPath = state.pageView.pageFromDb?.background ?: "blank", + backgroundPageNumber = state.pageView.getBackgroundPageNumber(), + notebookId = state.pageView.pageFromDb?.notebookId, + currentPageNumber = state.pageView.currentPageNumber + ) + + ToolbarContent( + uiState = uiState, + onToggleToolbar = { state.isToolbarOpen = !state.isToolbarOpen }, + onModeChange = { state.mode = it }, + onPenChange = { pen -> + if (state.mode == Mode.Draw && state.pen == pen) { + state.menuStates.isStrokeSelectionOpen = true + } else { + state.mode = Mode.Draw + state.pen = pen + } + }, + onPenSettingChange = { pen, setting -> + val settings = state.penSettings.toMutableMap() + settings[pen.penName] = setting + state.penSettings = settings + }, + onEraserChange = { state.eraser = it }, + onMenuToggle = { state.menuStates.isMenuOpen = !state.menuStates.isMenuOpen }, + onBackgroundSelectorToggle = { state.menuStates.isBackgroundSelectorModalOpen = it }, + onUndo = { controlTower.undo() }, + onRedo = { controlTower.redo() }, + onPaste = { controlTower.pasteFromClipboard() }, + onResetView = { controlTower.resetZoomAndScroll() }, + onNavigateToLibrary = onNavigateToLibrary, + onNavigateToBugReport = onNavigateToBugReport, + onNavigateToPages = onNavigateToPages, + onNavigateToHome = onNavigateToHome, + onToggleScribbleToErase = onToggleScribbleToErase, + onImagePicked = onImagePicked, + onExport = onExport, + onBackgroundChange = { type, path -> + val updatedPage = if (path == null) + state.pageView.pageFromDb!!.copy(backgroundType = type) + else state.pageView.pageFromDb!!.copy(background = path, backgroundType = type) + state.pageView.updatePageSettings(updatedPage) + scope.launch { CanvasEventBus.refreshUi.emit(Unit) } + }, + onDrawingStateCheck = { state.checkForSelectionsAndMenus() } + ) +} + +@Composable +fun ToolbarContent( + uiState: ToolbarUiState, + onToggleToolbar: () -> Unit, + onModeChange: (Mode) -> Unit, + onPenChange: (Pen) -> Unit, + onPenSettingChange: (Pen, PenSetting) -> Unit, + onEraserChange: (Eraser) -> Unit, + onMenuToggle: () -> Unit, + onBackgroundSelectorToggle: (Boolean) -> Unit, + onUndo: () -> Unit, + onRedo: () -> Unit, + onPaste: () -> Unit, + onResetView: () -> Unit, + onNavigateToLibrary: () -> Unit, + onNavigateToBugReport: () -> Unit, + onNavigateToPages: () -> Unit, + onNavigateToHome: () -> Unit, + onToggleScribbleToErase: (Boolean) -> Unit, + onImagePicked: (Uri) -> Unit, + onExport: suspend (ExportTarget, ExportFormat) -> String, + onBackgroundChange: (String, String?) -> Unit, + onDrawingStateCheck: () -> Unit +) { + val scope = rememberCoroutineScope() // Create an activity result launcher for picking visual media (images in this case) val pickMedia = @@ -116,63 +197,24 @@ fun Toolbar( } // on exit of toolbar, update drawing state - LaunchedEffect(state.menuStates.isBackgroundSelectorModalOpen, state.menuStates.isMenuOpen) { + LaunchedEffect(uiState.isBackgroundSelectorModalOpen, uiState.isMenuOpen) { log.i("Updating drawing state") - state.checkForSelectionsAndMenus() - } - fun handleChangePen(pen: Pen) { - if (state.mode == Mode.Draw && state.pen == pen) { - state.menuStates.isStrokeSelectionOpen = true - } else { - state.mode = Mode.Draw - state.pen = pen - } - } - - fun handleEraser() { - state.mode = Mode.Erase - } - - fun handleSelection() { - state.mode = Mode.Select + onDrawingStateCheck() } - fun handleLine() { - state.mode = Mode.Line - } - - fun onChangeStrokeSetting(penName: String, setting: PenSetting) { - val settings = state.penSettings.toMutableMap() - settings[penName] = setting.copy() - state.penSettings = settings - } - - if (state.menuStates.isBackgroundSelectorModalOpen) { - log.i("Opening page settings modal") + if (uiState.isBackgroundSelectorModalOpen) { BackgroundSelector( - initialPageBackgroundType = state.pageView.pageFromDb?.backgroundType ?: "native", - initialPageBackground = state.pageView.pageFromDb?.background ?: "blank", - initialPageNumberInPdf = state.pageView.getBackgroundPageNumber(), - notebookId = state.pageView.pageFromDb?.notebookId, - pageNumberInBook = state.pageView.currentPageNumber, - onChange = { backgroundType, background -> - val updatedPage = - if (background == null) - state.pageView.pageFromDb!!.copy( - backgroundType = backgroundType - ) - else state.pageView.pageFromDb!!.copy( - background = background, - backgroundType = backgroundType - ) - state.pageView.updatePageSettings(updatedPage) - scope.launch { CanvasEventBus.refreshUi.emit(Unit) } - } - ) { - state.menuStates.isBackgroundSelectorModalOpen = false - } + initialPageBackgroundType = uiState.backgroundType, + initialPageBackground = uiState.backgroundPath, + initialPageNumberInPdf = uiState.backgroundPageNumber, + notebookId = uiState.notebookId, + pageNumberInBook = uiState.currentPageNumber, + onChange = onBackgroundChange, + onClose = { onBackgroundSelectorToggle(false) } + ) } - if (state.isToolbarOpen) { + + if (uiState.isToolbarOpen) { Column( modifier = Modifier .fillMaxWidth() @@ -194,9 +236,7 @@ fun Toolbar( .fillMaxWidth() ) { ToolbarButton( - onSelect = { - state.isToolbarOpen = !state.isToolbarOpen - }, vectorIcon = FeatherIcons.EyeOff, contentDescription = "close toolbar" + onSelect = onToggleToolbar, vectorIcon = FeatherIcons.EyeOff, contentDescription = "close toolbar" ) Box( Modifier @@ -206,88 +246,80 @@ fun Toolbar( ) PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.BALLPEN, icon = R.drawable.ballpen, - isSelected = isSelected(state, Pen.BALLPEN), - onSelect = { handleChangePen(Pen.BALLPEN) }, + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BALLPEN, + onSelect = { onPenChange(Pen.BALLPEN) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.BALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.BALLPEN.penName, it) }) + penSetting = uiState.penSettings[Pen.BALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + onChangeSetting = { onPenSettingChange(Pen.BALLPEN, it) }) if (!GlobalAppSettings.current.monochromeMode) { PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.REDBALLPEN, icon = R.drawable.ballpenred, - isSelected = isSelected(state, Pen.REDBALLPEN), - onSelect = { handleChangePen(Pen.REDBALLPEN) }, + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.REDBALLPEN, + onSelect = { onPenChange(Pen.REDBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.REDBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.REDBALLPEN.penName, it) }, + penSetting = uiState.penSettings[Pen.REDBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.RED), + onChangeSetting = { onPenSettingChange(Pen.REDBALLPEN, it) }, ) PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.BLUEBALLPEN, icon = R.drawable.ballpenblue, - isSelected = isSelected(state, Pen.BLUEBALLPEN), - onSelect = { handleChangePen(Pen.BLUEBALLPEN) }, + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BLUEBALLPEN, + onSelect = { onPenChange(Pen.BLUEBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.BLUEBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.BLUEBALLPEN.penName, it) }, + penSetting = uiState.penSettings[Pen.BLUEBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLUE), + onChangeSetting = { onPenSettingChange(Pen.BLUEBALLPEN, it) }, ) -// Removed to make space for insert tool PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.GREENBALLPEN, icon = R.drawable.ballpengreen, - isSelected = isSelected(state, Pen.GREENBALLPEN), - onSelect = { handleChangePen(Pen.GREENBALLPEN) }, + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.GREENBALLPEN, + onSelect = { onPenChange(Pen.GREENBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.GREENBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.GREENBALLPEN.penName, it) }, + penSetting = uiState.penSettings[Pen.GREENBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.GREEN), + onChangeSetting = { onPenSettingChange(Pen.GREENBALLPEN, it) }, ) } if (GlobalAppSettings.current.neoTools) { PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.PENCIL, icon = R.drawable.pencil, - isSelected = isSelected(state, Pen.PENCIL), - onSelect = { handleChangePen(Pen.PENCIL) }, // Neo-tool! Usage not recommended + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.PENCIL, + onSelect = { onPenChange(Pen.PENCIL) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.PENCIL.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.PENCIL.penName, it) }, + penSetting = uiState.penSettings[Pen.PENCIL.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + onChangeSetting = { onPenSettingChange(Pen.PENCIL, it) }, ) PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.BRUSH, icon = R.drawable.brush, - isSelected = isSelected(state, Pen.BRUSH), - onSelect = { handleChangePen(Pen.BRUSH) }, // Neo-tool! Usage not recommended + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BRUSH, + onSelect = { onPenChange(Pen.BRUSH) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.BRUSH.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.BRUSH.penName, it) }, + penSetting = uiState.penSettings[Pen.BRUSH.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + onChangeSetting = { onPenSettingChange(Pen.BRUSH, it) }, ) } PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.FOUNTAIN, icon = R.drawable.fountain, - isSelected = isSelected(state, Pen.FOUNTAIN), - onSelect = { handleChangePen(Pen.FOUNTAIN) },// Neo-tool! Usage not recommended + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.FOUNTAIN, + onSelect = { onPenChange(Pen.FOUNTAIN) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.FOUNTAIN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.FOUNTAIN.penName, it) }, + penSetting = uiState.penSettings[Pen.FOUNTAIN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + onChangeSetting = { onPenSettingChange(Pen.FOUNTAIN, it) }, ) LineToolbarButton( - unSelect = { state.mode = Mode.Draw }, + unSelect = { onModeChange(Mode.Draw) }, icon = R.drawable.line, - isSelected = state.mode == Mode.Line, - onSelect = { handleLine() }, + isSelected = uiState.mode == Mode.Line, + onSelect = { onModeChange(Mode.Line) }, ) Box( @@ -298,14 +330,13 @@ fun Toolbar( ) PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.MARKER, icon = R.drawable.marker, - isSelected = isSelected(state, Pen.MARKER), - onSelect = { handleChangePen(Pen.MARKER) }, + isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.MARKER, + onSelect = { onPenChange(Pen.MARKER) }, sizes = SIZES_MARKER_DEFAULT, - penSetting = state.penSettings[Pen.MARKER.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.MARKER.penName, it) }) + penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting(40f, android.graphics.Color.LTGRAY), + onChangeSetting = { onPenSettingChange(Pen.MARKER, it) }) Box( Modifier .fillMaxHeight() @@ -313,14 +344,12 @@ fun Toolbar( .background(Color.Black) ) EraserToolbarButton( - isSelected = state.mode == Mode.Erase, - onSelect = { - handleEraser() - }, - onMenuOpenChange = { state.menuStates.isStrokeSelectionOpen = it }, - value = state.eraser, - onChange = { state.eraser = it }, - toggleScribbleToErase = onToggleScribbleToErase + isSelected = uiState.mode == Mode.Erase, + onSelect = { onModeChange(Mode.Erase) }, + value = uiState.eraser, + onChange = onEraserChange, + toggleScribbleToErase = onToggleScribbleToErase, + onMenuOpenChange = {} ) Box( Modifier @@ -329,8 +358,8 @@ fun Toolbar( .background(Color.Black) ) ToolbarButton( - isSelected = state.mode == Mode.Select, - onSelect = { handleSelection() }, + isSelected = uiState.mode == Mode.Select, + onSelect = { onModeChange(Mode.Select) }, iconId = R.drawable.lasso, contentDescription = "lasso" ) @@ -343,7 +372,7 @@ fun Toolbar( ToolbarButton( iconId = R.drawable.image, - contentDescription = "library", + contentDescription = "Image picker", onSelect = { // Call insertImage when the button is tapped log.i("Launching image picker...") @@ -357,13 +386,11 @@ fun Toolbar( .background(Color.Black) ) - if (state.clipboard != null) { + if (uiState.hasClipboard) { ToolbarButton( vectorIcon = FeatherIcons.Clipboard, contentDescription = "paste", - onSelect = { - controlTower.pasteFromClipboard() - } + onSelect = onPaste ) Box( Modifier @@ -373,13 +400,11 @@ fun Toolbar( ) } - // Show "Reset view" only when zoom != 1f or scroll != 0 - val showResetView = state.pageView.scroll.x != 0f || zoomLevel != 1.0f - if (showResetView) { + if (uiState.showResetView) { ToolbarButton( vectorIcon = FeatherIcons.RefreshCcw, contentDescription = "reset zoom and scroll", - onSelect = { controlTower.resetZoomAndScroll() } + onSelect = onResetView ) Box( Modifier @@ -399,21 +424,13 @@ fun Toolbar( ) ToolbarButton( - onSelect = { - scope.launch { - controlTower.undo() - } - }, + onSelect = onUndo, iconId = R.drawable.undo, contentDescription = "undo" ) ToolbarButton( - onSelect = { - scope.launch { - controlTower.redo() - } - }, + onSelect = onRedo, iconId = R.drawable.redo, contentDescription = "redo" ) @@ -424,7 +441,7 @@ fun Toolbar( .width(0.5.dp) .background(Color.Black) ) - if (state.bookId != null) { + if (uiState.notebookId != null) { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -432,11 +449,9 @@ fun Toolbar( .padding(10.dp, 0.dp) ) { Text( - text = pageNumberInfo, + text = uiState.pageNumberInfo, fontWeight = FontWeight.Light, - modifier = Modifier.noRippleClickable { - onNavigateToPages() - }, + modifier = Modifier.noRippleClickable(onNavigateToPages), textAlign = TextAlign.Center ) } @@ -449,7 +464,7 @@ fun Toolbar( } // Add Library Button ToolbarButton( - iconId = R.drawable.home, // Replace with your library icon resource + iconId = R.drawable.home, contentDescription = "library", onSelect = onNavigateToHome ) @@ -461,21 +476,18 @@ fun Toolbar( ) Column { ToolbarButton( - onSelect = { - state.menuStates.isMenuOpen = !state.menuStates.isMenuOpen - }, iconId = R.drawable.menu, contentDescription = "menu" + onSelect = onMenuToggle, iconId = R.drawable.menu, contentDescription = "menu" ) - if (state.menuStates.isMenuOpen) + if (uiState.isMenuOpen) ToolbarMenu( onExport = onExport, goToBugReport = onNavigateToBugReport, goToLibrary = onNavigateToLibrary, - currentPageId = state.currentPageId, - currentBookId = state.bookId, - onClose = { state.menuStates.isMenuOpen = false }, + currentPageId = "", + currentBookId = uiState.notebookId, + onClose = onMenuToggle, onBackgroundSelectorModalOpen = { - log.i("Opening page settings modal") - state.menuStates.isBackgroundSelectorModalOpen = true + onBackgroundSelectorToggle(true) } ) } @@ -491,12 +503,10 @@ fun Toolbar( } else { // Button to show Toolbar ToolbarButton( - onSelect = { state.isToolbarOpen = true }, - iconId = presentlyUsedToolIcon(state.mode, state.pen), - penColor = if (state.mode != Mode.Erase) state.penSettings[state.pen.penName]?.color?.let { - Color( - it - ) + onSelect = onToggleToolbar, + iconId = presentlyUsedToolIcon(uiState.mode, uiState.pen), + penColor = if (uiState.mode != Mode.Erase) uiState.penSettings[uiState.pen.penName]?.color?.let { + Color(it) } else null, contentDescription = "open toolbar", modifier = Modifier @@ -504,4 +514,66 @@ fun Toolbar( .padding(bottom = 1.dp) ) } +} + +fun presentlyUsedToolIcon(mode: Mode, pen: Pen): Int { + return when (mode) { + Mode.Draw -> { + when (pen) { + Pen.BALLPEN -> R.drawable.ballpen + Pen.REDBALLPEN -> R.drawable.ballpenred + Pen.BLUEBALLPEN -> R.drawable.ballpenblue + Pen.GREENBALLPEN -> R.drawable.ballpengreen + Pen.FOUNTAIN -> R.drawable.fountain + Pen.BRUSH -> R.drawable.brush + Pen.MARKER -> R.drawable.marker + Pen.PENCIL -> R.drawable.pencil + Pen.DASHED -> R.drawable.line_dashed + } + } + + Mode.Erase -> R.drawable.eraser + Mode.Select -> R.drawable.lasso + Mode.Line -> R.drawable.line + } +} + +@Composable +@Preview(showBackground = true, widthDp = 1200) +fun ToolbarPreview (){ + val uiState = ToolbarUiState( + isToolbarOpen = true, + mode = Mode.Draw, + pen = Pen.BALLPEN, + penSettings = mapOf( + Pen.BALLPEN.penName to PenSetting(5f, android.graphics.Color.BLACK), + Pen.MARKER.penName to PenSetting(40f, android.graphics.Color.LTGRAY) + ), + pageNumberInfo = "3/12", + notebookId = "dummy_book" + ) + + ToolbarContent( + uiState = uiState, + onToggleToolbar = {}, + onModeChange = {}, + onPenChange = {}, + onPenSettingChange = { _, _ -> }, + onEraserChange = {}, + onMenuToggle = {}, + onBackgroundSelectorToggle = {}, + onUndo = {}, + onRedo = {}, + onPaste = {}, + onResetView = {}, + onNavigateToLibrary = {}, + onNavigateToBugReport = {}, + onNavigateToPages = {}, + onNavigateToHome = {}, + onToggleScribbleToErase = {}, + onImagePicked = {}, + onExport = { _, _ -> "" }, + onBackgroundChange = { _, _ -> }, + onDrawingStateCheck = {} + ) } \ No newline at end of file From 3bfcf27f7902eb5122c8672d6579be7915557abf Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski <77555700+Ethran@users.noreply.github.com> Date: Sat, 7 Mar 2026 22:37:43 +0100 Subject: [PATCH 03/21] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/ethran/notable/editor/EditorView.kt | 12 +++++++++--- .../com/ethran/notable/editor/ui/toolbar/Toolbar.kt | 12 ++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index ec9b5a80..3b692c01 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -240,9 +240,15 @@ fun PositionedToolbar( } } - val pageNumberInfo: String = remember(book?.id, editorState.currentPageId) { - book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" - } + "/" + (book?.pageIds?.size?.toString() ?: "?") + val pageNumberInfo: String = remember( + book?.id, + editorState.currentPageId, + book?.pageIds?.size + ) { + val currentPage = book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" + val totalPages = book?.pageIds?.size?.toString() ?: "?" + "$currentPage/$totalPages" + } val toolbar = @Composable { Toolbar( diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index f5505c89..3b2d493c 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -53,7 +53,7 @@ import kotlinx.coroutines.launch private val log = ShipBook.getLogger("Toolbar") /** - * UI State for the Toolbar to make it previable and decoupled from logic. + * UI State for the Toolbar to make it previewable and decoupled from logic. */ data class ToolbarUiState( val isToolbarOpen: Boolean = true, @@ -483,9 +483,13 @@ fun ToolbarContent( onExport = onExport, goToBugReport = onNavigateToBugReport, goToLibrary = onNavigateToLibrary, - currentPageId = "", + currentPageId = uiState.currentPageId, currentBookId = uiState.notebookId, - onClose = onMenuToggle, + onClose = { + if (uiState.isMenuOpen) { + onMenuToggle() + } + }, onBackgroundSelectorModalOpen = { onBackgroundSelectorToggle(true) } @@ -540,7 +544,7 @@ fun presentlyUsedToolIcon(mode: Mode, pen: Pen): Int { @Composable @Preview(showBackground = true, widthDp = 1200) -fun ToolbarPreview (){ +fun ToolbarPreview() { val uiState = ToolbarUiState( isToolbarOpen = true, mode = Mode.Draw, From a124a13f96352cdaeb20ba0414a8fb6da0268f4f Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sat, 7 Mar 2026 22:38:36 +0100 Subject: [PATCH 04/21] Toolbar.kt refactored pen selection logic into isSelected function --- .../notable/editor/ui/toolbar/Toolbar.kt | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 3b2d493c..6a013c46 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -75,6 +75,22 @@ data class ToolbarUiState( val currentPageNumber: Int = 0 ) +private fun isSelected(state: ToolbarUiState, penType: Pen): Boolean { + return when (state.mode) { + Mode.Draw if state.pen == penType -> { + true + } + + Mode.Line if state.pen == penType -> { + true + } + + else -> { + false + } + } +} + private val SIZES_STROKES_DEFAULT = listOf("S" to 3f, "M" to 5f, "L" to 10f, "XL" to 20f) private val SIZES_MARKER_DEFAULT = listOf("M" to 25f, "L" to 40f, "XL" to 60f, "XXL" to 80f) @@ -184,7 +200,6 @@ fun ToolbarContent( onBackgroundChange: (String, String?) -> Unit, onDrawingStateCheck: () -> Unit ) { - val scope = rememberCoroutineScope() // Create an activity result launcher for picking visual media (images in this case) val pickMedia = @@ -248,7 +263,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.BALLPEN, icon = R.drawable.ballpen, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BALLPEN, + isSelected = isSelected(uiState, Pen.BALLPEN), onSelect = { onPenChange(Pen.BALLPEN) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.BALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), @@ -258,7 +273,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.REDBALLPEN, icon = R.drawable.ballpenred, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.REDBALLPEN, + isSelected = isSelected(uiState, Pen.REDBALLPEN), onSelect = { onPenChange(Pen.REDBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.REDBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.RED), @@ -268,7 +283,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.BLUEBALLPEN, icon = R.drawable.ballpenblue, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BLUEBALLPEN, + isSelected = isSelected(uiState, Pen.BLUEBALLPEN), onSelect = { onPenChange(Pen.BLUEBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.BLUEBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLUE), @@ -277,7 +292,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.GREENBALLPEN, icon = R.drawable.ballpengreen, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.GREENBALLPEN, + isSelected = isSelected(uiState, Pen.GREENBALLPEN), onSelect = { onPenChange(Pen.GREENBALLPEN) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.GREENBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.GREEN), @@ -288,7 +303,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.PENCIL, icon = R.drawable.pencil, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.PENCIL, + isSelected = isSelected(uiState, Pen.PENCIL), onSelect = { onPenChange(Pen.PENCIL) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.PENCIL.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), @@ -298,7 +313,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.BRUSH, icon = R.drawable.brush, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.BRUSH, + isSelected = isSelected(uiState, Pen.BRUSH), onSelect = { onPenChange(Pen.BRUSH) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.BRUSH.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), @@ -308,7 +323,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.FOUNTAIN, icon = R.drawable.fountain, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.FOUNTAIN, + isSelected = isSelected(uiState, Pen.FOUNTAIN), onSelect = { onPenChange(Pen.FOUNTAIN) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.FOUNTAIN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), @@ -332,7 +347,7 @@ fun ToolbarContent( PenToolbarButton( pen = Pen.MARKER, icon = R.drawable.marker, - isSelected = uiState.mode == Mode.Draw && uiState.pen == Pen.MARKER, + isSelected = isSelected(uiState, Pen.MARKER), onSelect = { onPenChange(Pen.MARKER) }, sizes = SIZES_MARKER_DEFAULT, penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting(40f, android.graphics.Color.LTGRAY), From e897157250b2ea4c1eff8d7864b8a0ef64028d56 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 00:05:20 +0100 Subject: [PATCH 05/21] ToolbarMenu simplified export parameters and added preview --- .../notable/editor/ui/toolbar/Toolbar.kt | 16 +++++-- .../notable/editor/ui/toolbar/ToolbarMenu.kt | 46 +++++++++++-------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 6a013c46..afbccdcd 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -72,6 +72,7 @@ data class ToolbarUiState( val backgroundPath: String = "blank", val backgroundPageNumber: Int = 0, val notebookId: String? = null, + val pageId: String? = null, val currentPageNumber: Int = 0 ) @@ -131,6 +132,7 @@ fun Toolbar( backgroundPath = state.pageView.pageFromDb?.background ?: "blank", backgroundPageNumber = state.pageView.getBackgroundPageNumber(), notebookId = state.pageView.pageFromDb?.notebookId, + pageId = state.pageView.pageFromDb?.notebookId, currentPageNumber = state.pageView.currentPageNumber ) @@ -495,11 +497,19 @@ fun ToolbarContent( ) if (uiState.isMenuOpen) ToolbarMenu( - onExport = onExport, + onExportPage = { format -> + uiState.pageId?.let { + onExport( + ExportTarget.Page(it), + format + ) + } ?: "failed to export page, page Id is null" + }, + onExportBook = uiState.notebookId?.let { bookId -> + { format -> onExport(ExportTarget.Book(bookId), format) } + }, goToBugReport = onNavigateToBugReport, goToLibrary = onNavigateToLibrary, - currentPageId = uiState.currentPageId, - currentBookId = uiState.notebookId, onClose = { if (uiState.isMenuOpen) { onMenuToggle() diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index ce45952f..a60d1cac 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup @@ -27,7 +28,6 @@ import com.ethran.notable.R import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.io.ExportFormat -import com.ethran.notable.io.ExportTarget import com.ethran.notable.ui.LocalSnackContext import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.convertDpToPixel @@ -37,11 +37,10 @@ import kotlinx.coroutines.launch @Composable fun ToolbarMenu( - onExport: suspend (ExportTarget, ExportFormat) -> String, + onExportPage: suspend (ExportFormat) -> String, + onExportBook: (suspend (ExportFormat) -> String)? = null, goToBugReport: () -> Unit, goToLibrary: () -> Unit, - currentPageId: String, - currentBookId: String?, onClose: () -> Unit, onBackgroundSelectorModalOpen: () -> Unit ) { @@ -56,11 +55,10 @@ fun ToolbarMenu( properties = PopupProperties(focusable = true), ) { ToolbarMenuContent( - onExport = onExport, + onExportPage = onExportPage, + onExportBook = onExportBook, goToBugReport = goToBugReport, goToLibrary = goToLibrary, - currentPageId = currentPageId, - currentBookId = currentBookId, onClose = onClose, onBackgroundSelectorModalOpen = onBackgroundSelectorModalOpen ) @@ -69,11 +67,10 @@ fun ToolbarMenu( @Composable private fun ToolbarMenuContent( - onExport: suspend (ExportTarget, ExportFormat) -> String, + onExportPage: suspend (ExportFormat) -> String, + onExportBook: (suspend (ExportFormat) -> String)? = null, goToBugReport: () -> Unit, goToLibrary: () -> Unit, - currentPageId: String, - currentBookId: String?, onClose: () -> Unit, onBackgroundSelectorModalOpen: () -> Unit ) { @@ -107,7 +104,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "PDF")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToPdfMsg) { - onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.PDF) + onExportPage(ExportFormat.PDF) } } onClose() @@ -115,7 +112,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "PNG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToPngMsg) { - onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.PNG) + onExportPage(ExportFormat.PNG) } } onClose() @@ -123,7 +120,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "JPEG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToJpegMsg) { - onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.JPEG) + onExportPage(ExportFormat.JPEG) } } onClose() @@ -131,7 +128,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_page_to, "xopp")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingPageToXoppMsg) { - onExport(ExportTarget.Page(pageId = currentPageId), ExportFormat.XOPP) + onExportPage(ExportFormat.XOPP) } } onClose() @@ -139,11 +136,11 @@ private fun ToolbarMenuContent( DividerCentered() // Book exports - if (currentBookId != null) { + if (onExportBook != null) { MenuItem(stringResource(R.string.export_book_to, "PDF")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToPdfMsg) { - onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.PDF) + onExportBook(ExportFormat.PDF) } } onClose() @@ -151,7 +148,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_book_to, "PNG")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToPngMsg) { - onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.PNG) + onExportBook(ExportFormat.PNG) } } onClose() @@ -159,7 +156,7 @@ private fun ToolbarMenuContent( MenuItem(stringResource(R.string.export_book_to, "xopp")) { scope.launch(Dispatchers.IO) { snackManager.runWithSnack(exportingBookToXoppMsg) { - onExport(ExportTarget.Book(bookId = currentBookId), ExportFormat.XOPP) + onExportBook(ExportFormat.XOPP) } } onClose() @@ -219,3 +216,16 @@ private fun ColumnScope.DividerCentered() { .background(Color(0xFF777777)) ) } + +@Composable +@Preview(showBackground = true) +fun ToolbarMenuPreview() { + ToolbarMenuContent( + onExportPage = { "Success" }, + onExportBook = { "Success" }, + goToBugReport = {}, + goToLibrary = {}, + onClose = {}, + onBackgroundSelectorModalOpen = {} + ) +} \ No newline at end of file From 0e1b967b27764a98946e99955e370cec737d3064 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 13:36:17 +0100 Subject: [PATCH 06/21] move Topbar.kt to ui package --- .../java/com/ethran/notable/editor/ui/{toolbar => }/Topbar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename app/src/main/java/com/ethran/notable/editor/ui/{toolbar => }/Topbar.kt (94%) diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Topbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/Topbar.kt similarity index 94% rename from app/src/main/java/com/ethran/notable/editor/ui/toolbar/Topbar.kt rename to app/src/main/java/com/ethran/notable/editor/ui/Topbar.kt index 7f5ffc55..1df9b22b 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Topbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/Topbar.kt @@ -1,4 +1,4 @@ -package com.ethran.notable.editor.ui.toolbar +package com.ethran.notable.editor.ui import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box From 72ef97779443f3ce92a8ef6a32b33d6d5888c66a Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 13:38:11 +0100 Subject: [PATCH 07/21] move PositionedToolbar to its own file --- .../com/ethran/notable/editor/EditorView.kt | 110 +------------- .../editor/ui/toolbar/PositionedToolbar.kt | 135 ++++++++++++++++++ 2 files changed, 136 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 3b692c01..903bd35b 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -1,7 +1,6 @@ package com.ethran.notable.editor import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -16,24 +15,16 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri import androidx.navigation.NavController import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.copyImageToDatabase -import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.EditorSettingCacheManager -import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.data.db.Notebook -import com.ethran.notable.data.db.getPageIndex -import com.ethran.notable.data.db.getParentFolder -import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History import com.ethran.notable.editor.ui.EditorSurface import com.ethran.notable.editor.ui.HorizontalScrollIndicator import com.ethran.notable.editor.ui.ScrollIndicator import com.ethran.notable.editor.ui.SelectedBitmap -import com.ethran.notable.editor.ui.toolbar.Toolbar +import com.ethran.notable.editor.ui.toolbar.PositionedToolbar import com.ethran.notable.gestures.EditorGestureReceiver import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.exportToLinkedFile @@ -43,9 +34,7 @@ import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import com.ethran.notable.ui.convertDpToPixel import com.ethran.notable.ui.theme.InkaTheme -import com.ethran.notable.ui.views.BugReportDestination import com.ethran.notable.ui.views.LibraryDestination -import com.ethran.notable.ui.views.PagesDestination import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -216,100 +205,3 @@ fun EditorView( } } } - - -@Composable -fun PositionedToolbar( - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, - editorState: EditorState, - editorControlTower: EditorControlTower -) { - val position = GlobalAppSettings.current.toolbarPosition - val scope = rememberCoroutineScope() - val context = LocalContext.current - - var book by remember(editorState.bookId) { mutableStateOf(null) } - LaunchedEffect(editorState.bookId) { - if (editorState.bookId != null) { - val loadedBook = withContext(Dispatchers.IO) { - appRepository.bookRepository.getById(editorState.bookId) - } - book = loadedBook - } - } - - val pageNumberInfo: String = remember( - book?.id, - editorState.currentPageId, - book?.pageIds?.size - ) { - val currentPage = book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" - val totalPages = book?.pageIds?.size?.toString() ?: "?" - "$currentPage/$totalPages" - } - - val toolbar = @Composable { - Toolbar( - state = editorState, - controlTower = editorControlTower, - pageNumberInfo = pageNumberInfo, - onNavigateToLibrary = { - scope.launch { - val page = withContext(Dispatchers.IO) { - appRepository.pageRepository.getById(editorState.currentPageId) - } - val parentFolder = withContext(Dispatchers.IO) { - page?.getParentFolder(appRepository.bookRepository) - } - navController.navigate(LibraryDestination.createRoute(parentFolder)) - } - }, - onNavigateToBugReport = { navController.navigate(BugReportDestination.route) }, - onNavigateToPages = { - if (editorState.bookId != null) { - navController.navigate(PagesDestination.createRoute(editorState.bookId)) - } - }, - onNavigateToHome = { navController.navigate("library") }, - onToggleScribbleToErase = { enabled -> - scope.launch(Dispatchers.IO) { - appRepository.kvProxy.setAppSettings( - GlobalAppSettings.current.copy(scribbleToEraseEnabled = enabled) - ) - } - }, - onImagePicked = { uri -> - scope.launch(Dispatchers.IO) { - try { - val copiedFile = copyImageToDatabase(context, uri) - CanvasEventBus.addImageByUri.value = copiedFile.toUri() - } catch (e: Exception) { - log.e("ImagePicker: copy failed: ${e.message}", e) - } - } - }, - onExport = { target, format -> - exportEngine.export(target, format) - } - ) - } - - when (position) { - AppSettings.Position.Top -> { - toolbar() - } - - AppSettings.Position.Bottom -> { - Column( - Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { - Spacer(modifier = Modifier.weight(1f)) - toolbar() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt new file mode 100644 index 00000000..a94786b1 --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt @@ -0,0 +1,135 @@ +package com.ethran.notable.editor.ui.toolbar + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +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.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.core.net.toUri +import androidx.navigation.NavController +import com.ethran.notable.data.AppRepository +import com.ethran.notable.data.copyImageToDatabase +import com.ethran.notable.data.datastore.AppSettings +import com.ethran.notable.data.datastore.GlobalAppSettings +import com.ethran.notable.data.db.Notebook +import com.ethran.notable.data.db.getPageIndex +import com.ethran.notable.data.db.getParentFolder +import com.ethran.notable.editor.EditorControlTower +import com.ethran.notable.editor.canvas.CanvasEventBus +import com.ethran.notable.editor.state.EditorState +import com.ethran.notable.io.ExportEngine +import com.ethran.notable.ui.views.BugReportDestination +import com.ethran.notable.ui.views.LibraryDestination +import com.ethran.notable.ui.views.PagesDestination +import io.shipbook.shipbooksdk.ShipBook +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +private val log = ShipBook.getLogger("Toolbar") + + +@Composable +fun PositionedToolbar( + exportEngine: ExportEngine, + navController: NavController, + appRepository: AppRepository, + editorState: EditorState, + editorControlTower: EditorControlTower +) { + val position = GlobalAppSettings.current.toolbarPosition + val scope = rememberCoroutineScope() + val context = LocalContext.current + + var book by remember(editorState.bookId) { mutableStateOf(null) } + LaunchedEffect(editorState.bookId) { + if (editorState.bookId != null) { + val loadedBook = withContext(Dispatchers.IO) { + appRepository.bookRepository.getById(editorState.bookId) + } + book = loadedBook + } + } + + val pageNumberInfo: String = remember( + book?.id, + editorState.currentPageId, + book?.pageIds?.size + ) { + val currentPage = book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" + val totalPages = book?.pageIds?.size?.toString() ?: "?" + "$currentPage/$totalPages" + } + + val toolbar = @Composable { + Toolbar( + state = editorState, + controlTower = editorControlTower, + pageNumberInfo = pageNumberInfo, + onNavigateToLibrary = { + scope.launch { + val page = withContext(Dispatchers.IO) { + appRepository.pageRepository.getById(editorState.currentPageId) + } + val parentFolder = withContext(Dispatchers.IO) { + page?.getParentFolder(appRepository.bookRepository) + } + navController.navigate(LibraryDestination.createRoute(parentFolder)) + } + }, + onNavigateToBugReport = { navController.navigate(BugReportDestination.route) }, + onNavigateToPages = { + if (editorState.bookId != null) { + navController.navigate(PagesDestination.createRoute(editorState.bookId)) + } + }, + onNavigateToHome = { navController.navigate("library") }, + onToggleScribbleToErase = { enabled -> + scope.launch(Dispatchers.IO) { + appRepository.kvProxy.setAppSettings( + GlobalAppSettings.current.copy(scribbleToEraseEnabled = enabled) + ) + } + }, + onImagePicked = { uri -> + scope.launch(Dispatchers.IO) { + try { + val copiedFile = copyImageToDatabase(context, uri) + CanvasEventBus.addImageByUri.value = copiedFile.toUri() + } catch (e: Exception) { + log.e("ImagePicker: copy failed: ${e.message}", e) + } + } + }, + onExport = { target, format -> + exportEngine.export(target, format) + } + ) + } + + when (position) { + AppSettings.Position.Top -> { + toolbar() + } + + AppSettings.Position.Bottom -> { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { + Spacer(modifier = Modifier.weight(1f)) + toolbar() + } + } + } +} \ No newline at end of file From 0c21f9881c577229b0b468bd45e1d506490b5d54 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 13:45:12 +0100 Subject: [PATCH 08/21] create the EditorViewModel.kt --- .../com/ethran/notable/editor/EditorView.kt | 6 +- .../ethran/notable/editor/EditorViewModel.kt | 72 +++++++++++++++++++ .../notable/editor/ui/toolbar/Toolbar.kt | 25 +------ .../com/ethran/notable/io/ExportEngine.kt | 2 + .../com/ethran/notable/ui/views/HomeView.kt | 2 +- .../com/ethran/notable/ui/views/PagesView.kt | 2 +- 6 files changed, 83 insertions(+), 26 deletions(-) create mode 100644 app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 903bd35b..96b7f24b 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.ethran.notable.data.AppRepository import com.ethran.notable.data.datastore.EditorSettingCacheManager @@ -68,7 +69,8 @@ fun EditorView( appRepository: AppRepository, bookId: String?, pageId: String, - onPageChange: (String) -> Unit + onPageChange: (String) -> Unit, + viewModel: EditorViewModel = hiltViewModel() ) { val context = LocalContext.current val snackManager = LocalSnackContext.current @@ -101,6 +103,8 @@ fun EditorView( } } + val toolbarState by viewModel.toolbarState.collectAsState() + if (pageExists == null) return diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt new file mode 100644 index 00000000..50953682 --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -0,0 +1,72 @@ +package com.ethran.notable.editor; + +import com.ethran.notable.io.ExportEngine; +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.ethran.notable.APP_SETTINGS_KEY +import com.ethran.notable.R +import com.ethran.notable.data.datastore.AppSettings +import com.ethran.notable.data.datastore.GlobalAppSettings +import com.ethran.notable.data.db.KvProxy +import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.utils.Eraser +import com.ethran.notable.editor.utils.Pen +import com.ethran.notable.editor.utils.PenSetting +import com.ethran.notable.io.ExportFormat +import com.ethran.notable.io.ExportTarget +import com.ethran.notable.utils.isLatestVersion +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + + +/** + * UI State for the Toolbar + */ +data class ToolbarUiState( + val isToolbarOpen: Boolean = true, + val mode: Mode = Mode.Draw, + val pen: Pen = Pen.BALLPEN, + val penSettings: Map = emptyMap(), + val eraser: Eraser = Eraser.PEN, + val isMenuOpen: Boolean = false, + val isStrokeSelectionOpen: Boolean = false, + val isBackgroundSelectorModalOpen: Boolean = false, + val pageNumberInfo: String = "1/1", + val hasClipboard: Boolean = false, + val showResetView: Boolean = false, + // Background Selector specific data + val backgroundType: String = "native", + val backgroundPath: String = "blank", + val backgroundPageNumber: Int = 0, + val notebookId: String? = null, + val pageId: String? = null, + val currentPageNumber: Int = 0 +) + +@HiltViewModel +class EditorViewModel @Inject +constructor( + private val exportEngine:ExportEngine, + // other dependencies... +) : ViewModel() { + + fun exportCurrentPage(format: ExportFormat) { + viewModelScope.launch(Dispatchers.IO) { + // UI State: Show loading spinner + _uiState.update { it.copy(isExporting = true) } + + val result = exportEngine.export(ExportTarget.Page(currentPageId), format) + + // Tell UI to show Snackbar based on result + _uiEvents.emit(UiEvent.ShowSnackbar("Export successful!")) + _uiState.update { it.copy(isExporting = false) } + } + } +} diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index afbccdcd..7df4b8fa 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -33,6 +33,7 @@ import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.EditorControlTower +import com.ethran.notable.editor.ToolbarUiState import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.Mode @@ -52,29 +53,7 @@ import kotlinx.coroutines.launch private val log = ShipBook.getLogger("Toolbar") -/** - * UI State for the Toolbar to make it previewable and decoupled from logic. - */ -data class ToolbarUiState( - val isToolbarOpen: Boolean = true, - val mode: Mode = Mode.Draw, - val pen: Pen = Pen.BALLPEN, - val penSettings: Map = emptyMap(), - val eraser: Eraser = Eraser.PEN, - val isMenuOpen: Boolean = false, - val isStrokeSelectionOpen: Boolean = false, - val isBackgroundSelectorModalOpen: Boolean = false, - val pageNumberInfo: String = "1/1", - val hasClipboard: Boolean = false, - val showResetView: Boolean = false, - // Background Selector specific data - val backgroundType: String = "native", - val backgroundPath: String = "blank", - val backgroundPageNumber: Int = 0, - val notebookId: String? = null, - val pageId: String? = null, - val currentPageNumber: Int = 0 -) + private fun isSelected(state: ToolbarUiState, penType: Pen): Boolean { return when (state.mode) { diff --git a/app/src/main/java/com/ethran/notable/io/ExportEngine.kt b/app/src/main/java/com/ethran/notable/io/ExportEngine.kt index 992ef2e0..5dcc23b6 100644 --- a/app/src/main/java/com/ethran/notable/io/ExportEngine.kt +++ b/app/src/main/java/com/ethran/notable/io/ExportEngine.kt @@ -42,6 +42,7 @@ import java.io.OutputStream import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import javax.inject.Inject +import javax.inject.Singleton /* ---------------------------- Public API ---------------------------- */ @@ -59,6 +60,7 @@ data class ExportOptions( val fileName: String? = null ) +@Singleton class ExportEngine @Inject constructor( @param:ApplicationContext private val context: Context, private val appRepository: AppRepository, diff --git a/app/src/main/java/com/ethran/notable/ui/views/HomeView.kt b/app/src/main/java/com/ethran/notable/ui/views/HomeView.kt index 785c01d9..9f63886c 100644 --- a/app/src/main/java/com/ethran/notable/ui/views/HomeView.kt +++ b/app/src/main/java/com/ethran/notable/ui/views/HomeView.kt @@ -52,7 +52,7 @@ import com.ethran.notable.data.AppRepository import com.ethran.notable.data.db.Folder import com.ethran.notable.data.db.Notebook import com.ethran.notable.editor.EditorDestination -import com.ethran.notable.editor.ui.toolbar.Topbar +import com.ethran.notable.editor.ui.Topbar import com.ethran.notable.editor.utils.autoEInkAnimationOnScroll import com.ethran.notable.io.ExportEngine import com.ethran.notable.navigation.NavigationDestination diff --git a/app/src/main/java/com/ethran/notable/ui/views/PagesView.kt b/app/src/main/java/com/ethran/notable/ui/views/PagesView.kt index 80bb3f36..01aadf95 100644 --- a/app/src/main/java/com/ethran/notable/ui/views/PagesView.kt +++ b/app/src/main/java/com/ethran/notable/ui/views/PagesView.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.ethran.notable.editor.ui.toolbar.Topbar +import com.ethran.notable.editor.ui.Topbar import com.ethran.notable.editor.utils.autoEInkAnimationOnScroll import com.ethran.notable.editor.utils.setAnimationMode import com.ethran.notable.navigation.NavigationDestination From c388c05c0929c351c8d67de3063584fea6d9b72a Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 16:56:38 +0100 Subject: [PATCH 09/21] Refactor toolbar logic into `EditorViewModel` using a modern MVI-pattern approach. Key changes: - **Decoupled UI from Logic**: Moved toolbar state management, navigation handling, and side-effect execution (export, image picking, etc.) from the view layer into `EditorViewModel`. - **Introduced Intent-based Actions**: Added `ToolbarAction` to represent user interactions and `EditorUiEvent` for one-time effects like navigation and snackbars. - **Improved State Management**: Replaced legacy state synchronization with a reactive `ToolbarUiState` flow. - **Simplified Components**: Refactored `Toolbar`, `ToolbarMenu`, and `PositionedToolbar` to be stateless, delegating all actions back to the ViewModel. - **Centralized Data Loading**: Moved book/page metadata loading and page numbering logic into the ViewModel. --- .../com/ethran/notable/editor/EditorView.kt | 83 +++- .../ethran/notable/editor/EditorViewModel.kt | 308 +++++++++++- .../editor/ui/toolbar/PositionedToolbar.kt | 127 +---- .../notable/editor/ui/toolbar/Toolbar.kt | 452 +++++------------- .../notable/editor/ui/toolbar/ToolbarMenu.kt | 158 ++---- 5 files changed, 561 insertions(+), 567 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 96b7f24b..f987513a 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -8,17 +8,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +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.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController import com.ethran.notable.data.AppRepository import com.ethran.notable.data.datastore.EditorSettingCacheManager +import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History import com.ethran.notable.editor.ui.EditorSurface @@ -35,7 +38,9 @@ import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import com.ethran.notable.ui.convertDpToPixel import com.ethran.notable.ui.theme.InkaTheme +import com.ethran.notable.ui.views.BugReportDestination import com.ethran.notable.ui.views.LibraryDestination +import com.ethran.notable.ui.views.PagesDestination import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -78,6 +83,7 @@ fun EditorView( var pageExists by remember(pageId) { mutableStateOf(null) } LaunchedEffect(pageId) { + viewModel.loadBookData(bookId, pageId) val exists = withContext(Dispatchers.IO) { appRepository.pageRepository.getById(pageId) != null } @@ -103,9 +109,6 @@ fun EditorView( } } - val toolbarState by viewModel.toolbarState.collectAsState() - - if (pageExists == null) return BoxWithConstraints { @@ -144,6 +147,72 @@ fun EditorView( EditorControlTower(scope, page, history, editorState).apply { registerObservers() } } + // Collect UI Events from ViewModel + LaunchedEffect(Unit) { + viewModel.uiEvents.collect { event -> + when (event) { + is EditorUiEvent.Undo -> editorControlTower.undo() + is EditorUiEvent.Redo -> editorControlTower.redo() + is EditorUiEvent.Paste -> editorControlTower.pasteFromClipboard() + is EditorUiEvent.ResetView -> editorControlTower.resetZoomAndScroll() + is EditorUiEvent.ClearAllStrokes -> { + CanvasEventBus.clearPageSignal.emit(Unit) + } + is EditorUiEvent.NavigateToLibrary -> { + navController.navigate(LibraryDestination.createRoute(event.folderId)) + } + is EditorUiEvent.NavigateToPages -> { + navController.navigate(PagesDestination.createRoute(event.bookId)) + } + EditorUiEvent.NavigateToBugReport -> { + navController.navigate(BugReportDestination.route) + } + is EditorUiEvent.ShowSnackbar -> { + snackManager.displaySnack(SnackConf(text = event.message)) + } + is EditorUiEvent.CopyImageToCanvas -> { + CanvasEventBus.addImageByUri.value = event.uri + } + EditorUiEvent.RefreshCanvas -> { + CanvasEventBus.refreshUi.emit(Unit) + } + EditorUiEvent.CheckDrawingState -> { + editorState.checkForSelectionsAndMenus() + } + is EditorUiEvent.ModeChanged -> { + editorState.mode = event.mode + } + is EditorUiEvent.PenChanged -> { + editorState.pen = event.pen + } + is EditorUiEvent.PenSettingChanged -> { + val newSettings = editorState.penSettings.toMutableMap() + newSettings[event.pen.penName] = event.setting + editorState.penSettings = newSettings + } + is EditorUiEvent.EraserChanged -> { + editorState.eraser = event.eraser + } + is EditorUiEvent.ToolbarVisibilityChanged -> { + editorState.isToolbarOpen = event.visible + } + } + } + } + + // Sync legacy state to ViewModel for Toolbar rendering + val zoomLevel by page.zoomLevel.collectAsState() + LaunchedEffect(zoomLevel, page.scroll, editorState.clipboard, editorState.isToolbarOpen, editorState.mode, editorState.pen, editorState.eraser, editorState.penSettings) { + viewModel.setHasClipboard(editorState.clipboard != null) + viewModel.setShowResetView(page.scroll != Offset.Zero || zoomLevel != 1.0f) + viewModel.updateToolbarSettings(ToolbarUiState( + isToolbarOpen = editorState.isToolbarOpen, + mode = editorState.mode, + pen = editorState.pen, + eraser = editorState.eraser, + penSettings = editorState.penSettings + )) + } DisposableEffect(Unit) { onDispose { @@ -161,7 +230,6 @@ fun EditorView( editorState.pen, editorState.penSettings, editorState.mode, - editorState.isToolbarOpen, editorState.eraser ) { log.i("EditorView: saving") @@ -199,11 +267,8 @@ fun EditorView( ScrollIndicator(state = editorState) } PositionedToolbar( - exportEngine, - navController, - appRepository, - editorState, - editorControlTower + viewModel = viewModel, + onDrawingStateCheck = { editorState.checkForSelectionsAndMenus() } ) HorizontalScrollIndicator(state = editorState) } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 50953682..61af820e 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -1,33 +1,99 @@ -package com.ethran.notable.editor; +package com.ethran.notable.editor -import com.ethran.notable.io.ExportEngine; import android.content.Context -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import android.net.Uri +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.ethran.notable.APP_SETTINGS_KEY -import com.ethran.notable.R -import com.ethran.notable.data.datastore.AppSettings +import com.ethran.notable.data.AppRepository +import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.data.db.KvProxy +import com.ethran.notable.data.db.getPageIndex +import com.ethran.notable.data.db.getParentFolder import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting +import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget -import com.ethran.notable.utils.isLatestVersion import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject +/** + * Toolbar Actions (Intents) representing user interactions. + */ +sealed class ToolbarAction { + object ToggleToolbar : ToolbarAction() + data class ChangeMode(val mode: Mode) : ToolbarAction() + data class ChangePen(val pen: Pen) : ToolbarAction() + data class ChangePenSetting(val pen: Pen, val setting: PenSetting) : ToolbarAction() + data class ChangeEraser(val eraser: Eraser) : ToolbarAction() + object ToggleMenu : ToolbarAction() + data class ToggleBackgroundSelector(val isOpen: Boolean) : ToolbarAction() + data class ToggleScribbleToErase(val enabled: Boolean) : ToolbarAction() + + // Actions that trigger side effects in ControlTower or other components + object Undo : ToolbarAction() + object Redo : ToolbarAction() + object Paste : ToolbarAction() + object ResetView : ToolbarAction() + object ClearAllStrokes : ToolbarAction() + + // Complex Actions + data class ImagePicked(val uri: Uri) : ToolbarAction() + data class ExportPage(val format: ExportFormat) : ToolbarAction() + data class ExportBook(val format: ExportFormat) : ToolbarAction() + data class BackgroundChanged(val type: String, val path: String?) : ToolbarAction() + + // Navigation + object NavigateToLibrary : ToolbarAction() + object NavigateToBugReport : ToolbarAction() + object NavigateToPages : ToolbarAction() + object NavigateToHome : ToolbarAction() +} + +/** + * UI Events for one-time side effects (navigation, snackbars, etc.) + */ +sealed class EditorUiEvent { + data class ShowSnackbar(val message: String) : EditorUiEvent() + data class NavigateToLibrary(val folderId: String?) : EditorUiEvent() + data class NavigateToPages(val bookId: String) : EditorUiEvent() + object NavigateToBugReport : EditorUiEvent() + + // Side effects back to drawing logic/engine + object Undo : EditorUiEvent() + object Redo : EditorUiEvent() + object Paste : EditorUiEvent() + object ResetView : EditorUiEvent() + object ClearAllStrokes : EditorUiEvent() + object RefreshCanvas : EditorUiEvent() + data class CopyImageToCanvas(val uri: Uri) : EditorUiEvent() + + // Signal to check if drawing should be enabled (e.g. menus changed) + object CheckDrawingState : EditorUiEvent() + + // Sync state back to EditorState + data class ModeChanged(val mode: Mode) : EditorUiEvent() + data class PenChanged(val pen: Pen) : EditorUiEvent() + data class PenSettingChanged(val pen: Pen, val setting: PenSetting) : EditorUiEvent() + data class EraserChanged(val eraser: Eraser) : EditorUiEvent() + data class ToolbarVisibilityChanged(val visible: Boolean) : EditorUiEvent() +} /** - * UI State for the Toolbar + * UI State for the Toolbar rendering. */ data class ToolbarUiState( val isToolbarOpen: Boolean = true, @@ -41,32 +107,222 @@ data class ToolbarUiState( val pageNumberInfo: String = "1/1", val hasClipboard: Boolean = false, val showResetView: Boolean = false, - // Background Selector specific data + + // Context needed for visibility rules in UI + val notebookId: String? = null, + val pageId: String? = null, + val isBookActive: Boolean = false, + + // Internal data for BackgroundSelector rendering if it remains stateless val backgroundType: String = "native", val backgroundPath: String = "blank", val backgroundPageNumber: Int = 0, - val notebookId: String? = null, - val pageId: String? = null, val currentPageNumber: Int = 0 ) @HiltViewModel -class EditorViewModel @Inject -constructor( - private val exportEngine:ExportEngine, - // other dependencies... +class EditorViewModel @Inject constructor( + @param:ApplicationContext private val context: Context, + private val appRepository: AppRepository, + private val exportEngine: ExportEngine ) : ViewModel() { - fun exportCurrentPage(format: ExportFormat) { + private val _toolbarState = MutableStateFlow(ToolbarUiState()) + val toolbarState: StateFlow = _toolbarState.asStateFlow() + + private val _uiEvents = MutableSharedFlow() + val uiEvents: SharedFlow = _uiEvents.asSharedFlow() + + // Internal context + private var currentBookId: String? = null + private var currentPageId: String = "" + + fun onToolbarAction(action: ToolbarAction) { + when (action) { + is ToolbarAction.ToggleToolbar -> { + val newVisible = !_toolbarState.value.isToolbarOpen + _toolbarState.update { it.copy(isToolbarOpen = newVisible) } + sendUiEvent(EditorUiEvent.ToolbarVisibilityChanged(newVisible)) + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + is ToolbarAction.ChangeMode -> { + _toolbarState.update { it.copy(mode = action.mode) } + sendUiEvent(EditorUiEvent.ModeChanged(action.mode)) + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + is ToolbarAction.ChangePen -> handlePenChange(action.pen) + is ToolbarAction.ChangePenSetting -> handlePenSettingChange(action.pen, action.setting) + is ToolbarAction.ChangeEraser -> { + _toolbarState.update { it.copy(eraser = action.eraser) } + sendUiEvent(EditorUiEvent.EraserChanged(action.eraser)) + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + is ToolbarAction.ToggleMenu -> { + _toolbarState.update { it.copy(isMenuOpen = !it.isMenuOpen) } + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + is ToolbarAction.ToggleBackgroundSelector -> { + _toolbarState.update { it.copy(isBackgroundSelectorModalOpen = action.isOpen) } + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + + is ToolbarAction.ToggleScribbleToErase -> updateScribbleToErase(action.enabled) + is ToolbarAction.ImagePicked -> handleImagePicked(action.uri) + is ToolbarAction.ExportPage -> handleExport(ExportTarget.Page(currentPageId), action.format) + is ToolbarAction.ExportBook -> { + currentBookId?.let { handleExport(ExportTarget.Book(it), action.format) } + } + is ToolbarAction.BackgroundChanged -> handleBackgroundChange(action.type, action.path) + + ToolbarAction.Undo -> sendUiEvent(EditorUiEvent.Undo) + ToolbarAction.Redo -> sendUiEvent(EditorUiEvent.Redo) + ToolbarAction.Paste -> sendUiEvent(EditorUiEvent.Paste) + ToolbarAction.ResetView -> sendUiEvent(EditorUiEvent.ResetView) + ToolbarAction.ClearAllStrokes -> sendUiEvent(EditorUiEvent.ClearAllStrokes) + + ToolbarAction.NavigateToLibrary -> handleNavigateToLibrary() + ToolbarAction.NavigateToBugReport -> sendUiEvent(EditorUiEvent.NavigateToBugReport) + ToolbarAction.NavigateToPages -> handleNavigateToPages() + ToolbarAction.NavigateToHome -> sendUiEvent(EditorUiEvent.NavigateToLibrary(null)) + } + } + + private fun sendUiEvent(event: EditorUiEvent) { + viewModelScope.launch { _uiEvents.emit(event) } + } + + private fun handlePenChange(pen: Pen) { + _toolbarState.update { state -> + if (state.mode == Mode.Draw && state.pen == pen) { + state.copy(isStrokeSelectionOpen = true) + } else { + sendUiEvent(EditorUiEvent.PenChanged(pen)) + if (state.mode != Mode.Draw) sendUiEvent(EditorUiEvent.ModeChanged(Mode.Draw)) + state.copy(mode = Mode.Draw, pen = pen) + } + } + sendUiEvent(EditorUiEvent.CheckDrawingState) + } + + private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { + _toolbarState.update { state -> + val newSettings = state.penSettings.toMutableMap() + newSettings[pen.penName] = setting + sendUiEvent(EditorUiEvent.PenSettingChanged(pen, setting)) + state.copy(penSettings = newSettings) + } + } + + private fun updateScribbleToErase(enabled: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + appRepository.kvProxy.setAppSettings( + GlobalAppSettings.current.copy(scribbleToEraseEnabled = enabled) + ) + } + } + + private fun handleImagePicked(uri: Uri) { + viewModelScope.launch(Dispatchers.IO) { + try { + val copiedFile = copyImageToDatabase(context, uri) + _uiEvents.emit(EditorUiEvent.CopyImageToCanvas(copiedFile.toUri())) + } catch (e: Exception) { + _uiEvents.emit(EditorUiEvent.ShowSnackbar("Image import failed: ${e.message}")) + } + } + } + + private fun handleExport(target: ExportTarget, format: ExportFormat) { + viewModelScope.launch(Dispatchers.IO) { + try { + val result = exportEngine.export(target, format) + _uiEvents.emit(EditorUiEvent.ShowSnackbar(result)) + } catch (e: Exception) { + _uiEvents.emit(EditorUiEvent.ShowSnackbar("Export failed: ${e.message}")) + } + } + } + + private fun handleBackgroundChange(type: String, path: String?) { + viewModelScope.launch(Dispatchers.IO) { + val page = appRepository.pageRepository.getById(currentPageId) ?: return@launch + val updatedPage = if (path == null) { + page.copy(backgroundType = type) + } else { + page.copy(background = path, backgroundType = type) + } + appRepository.pageRepository.update(updatedPage) + _toolbarState.update { + it.copy( + backgroundType = updatedPage.backgroundType, + backgroundPath = updatedPage.background + ) + } + _uiEvents.emit(EditorUiEvent.RefreshCanvas) + } + } + + private fun handleNavigateToLibrary() { viewModelScope.launch(Dispatchers.IO) { - // UI State: Show loading spinner - _uiState.update { it.copy(isExporting = true) } + val page = appRepository.pageRepository.getById(currentPageId) + val parentFolder = page?.getParentFolder(appRepository.bookRepository) + _uiEvents.emit(EditorUiEvent.NavigateToLibrary(parentFolder)) + } + } + + private fun handleNavigateToPages() { + currentBookId?.let { bookId -> + viewModelScope.launch { + _uiEvents.emit(EditorUiEvent.NavigateToPages(bookId)) + } + } + } + + /** + * Loads context data for the toolbar (page number, background info, etc.) + */ + fun loadBookData(bookId: String?, pageId: String) { + currentBookId = bookId + currentPageId = pageId - val result = exportEngine.export(ExportTarget.Page(currentPageId), format) + viewModelScope.launch(Dispatchers.IO) { + val page = appRepository.pageRepository.getById(pageId) + val book = bookId?.let { appRepository.bookRepository.getById(it) } + + val pageIndex = book?.getPageIndex(pageId) ?: 0 + val totalPages = book?.pageIds?.size ?: 1 + + _toolbarState.update { + it.copy( + notebookId = bookId, + pageId = pageId, + isBookActive = bookId != null, + pageNumberInfo = if (bookId != null) "${pageIndex + 1}/$totalPages" else "1/1", + currentPageNumber = pageIndex, + backgroundType = page?.backgroundType ?: "native", + backgroundPath = page?.background ?: "blank", + ) + } + } + } + + fun setHasClipboard(hasClipboard: Boolean) { + _toolbarState.update { it.copy(hasClipboard = hasClipboard) } + } + + fun setShowResetView(showResetView: Boolean) { + _toolbarState.update { it.copy(showResetView = showResetView) } + } - // Tell UI to show Snackbar based on result - _uiEvents.emit(UiEvent.ShowSnackbar("Export successful!")) - _uiState.update { it.copy(isExporting = false) } + fun updateToolbarSettings(state: ToolbarUiState) { + _toolbarState.update { + it.copy( + isToolbarOpen = state.isToolbarOpen, + mode = state.mode, + pen = state.pen, + eraser = state.eraser, + penSettings = state.penSettings + ) } } } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt index a94786b1..dc51825e 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt @@ -5,128 +5,41 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.core.net.toUri -import androidx.navigation.NavController -import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.data.db.Notebook -import com.ethran.notable.data.db.getPageIndex -import com.ethran.notable.data.db.getParentFolder -import com.ethran.notable.editor.EditorControlTower -import com.ethran.notable.editor.canvas.CanvasEventBus -import com.ethran.notable.editor.state.EditorState -import com.ethran.notable.io.ExportEngine -import com.ethran.notable.ui.views.BugReportDestination -import com.ethran.notable.ui.views.LibraryDestination -import com.ethran.notable.ui.views.PagesDestination -import io.shipbook.shipbooksdk.ShipBook -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - - -private val log = ShipBook.getLogger("Toolbar") - - +import com.ethran.notable.editor.EditorViewModel + +/** + * Container for the Toolbar that handles its positioning (Top or Bottom). + * + * This component is now decoupled from navigation and engine logic, + * delegating actions to the [EditorViewModel]. + */ @Composable fun PositionedToolbar( - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, - editorState: EditorState, - editorControlTower: EditorControlTower + viewModel: EditorViewModel, + onDrawingStateCheck: () -> Unit ) { val position = GlobalAppSettings.current.toolbarPosition - val scope = rememberCoroutineScope() - val context = LocalContext.current - - var book by remember(editorState.bookId) { mutableStateOf(null) } - LaunchedEffect(editorState.bookId) { - if (editorState.bookId != null) { - val loadedBook = withContext(Dispatchers.IO) { - appRepository.bookRepository.getById(editorState.bookId) - } - book = loadedBook - } - } - - val pageNumberInfo: String = remember( - book?.id, - editorState.currentPageId, - book?.pageIds?.size - ) { - val currentPage = book?.let { (it.getPageIndex(editorState.currentPageId) + 1).toString() } ?: "?" - val totalPages = book?.pageIds?.size?.toString() ?: "?" - "$currentPage/$totalPages" - } + val toolbarState by viewModel.toolbarState.collectAsState() val toolbar = @Composable { - Toolbar( - state = editorState, - controlTower = editorControlTower, - pageNumberInfo = pageNumberInfo, - onNavigateToLibrary = { - scope.launch { - val page = withContext(Dispatchers.IO) { - appRepository.pageRepository.getById(editorState.currentPageId) - } - val parentFolder = withContext(Dispatchers.IO) { - page?.getParentFolder(appRepository.bookRepository) - } - navController.navigate(LibraryDestination.createRoute(parentFolder)) - } - }, - onNavigateToBugReport = { navController.navigate(BugReportDestination.route) }, - onNavigateToPages = { - if (editorState.bookId != null) { - navController.navigate(PagesDestination.createRoute(editorState.bookId)) - } - }, - onNavigateToHome = { navController.navigate("library") }, - onToggleScribbleToErase = { enabled -> - scope.launch(Dispatchers.IO) { - appRepository.kvProxy.setAppSettings( - GlobalAppSettings.current.copy(scribbleToEraseEnabled = enabled) - ) - } - }, - onImagePicked = { uri -> - scope.launch(Dispatchers.IO) { - try { - val copiedFile = copyImageToDatabase(context, uri) - CanvasEventBus.addImageByUri.value = copiedFile.toUri() - } catch (e: Exception) { - log.e("ImagePicker: copy failed: ${e.message}", e) - } - } - }, - onExport = { target, format -> - exportEngine.export(target, format) - } + ToolbarContent( + uiState = toolbarState, + onAction = viewModel::onToolbarAction, + onDrawingStateCheck = onDrawingStateCheck ) } when (position) { - AppSettings.Position.Top -> { - toolbar() - } - + AppSettings.Position.Top -> toolbar() AppSettings.Position.Bottom -> { - Column( - Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { + Column(Modifier + .fillMaxWidth() + .fillMaxHeight()) { Spacer(modifier = Modifier.weight(1f)) toolbar() } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 7df4b8fa..44b58d39 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -1,7 +1,5 @@ package com.ethran.notable.editor.ui.toolbar - -import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia @@ -18,9 +16,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -32,169 +27,39 @@ import com.ethran.notable.R import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.editor.EditorControlTower +import com.ethran.notable.editor.ToolbarAction import com.ethran.notable.editor.ToolbarUiState -import com.ethran.notable.editor.canvas.CanvasEventBus -import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.Mode -import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting -import com.ethran.notable.io.ExportFormat -import com.ethran.notable.io.ExportTarget import com.ethran.notable.ui.dialogs.BackgroundSelector import com.ethran.notable.ui.noRippleClickable import compose.icons.FeatherIcons import compose.icons.feathericons.Clipboard import compose.icons.feathericons.EyeOff import compose.icons.feathericons.RefreshCcw -import io.shipbook.shipbooksdk.ShipBook -import kotlinx.coroutines.launch - -private val log = ShipBook.getLogger("Toolbar") - private fun isSelected(state: ToolbarUiState, penType: Pen): Boolean { - return when (state.mode) { - Mode.Draw if state.pen == penType -> { - true - } - - Mode.Line if state.pen == penType -> { - true - } - - else -> { - false - } - } + return (state.mode == Mode.Draw || state.mode == Mode.Line) && state.pen == penType } - private val SIZES_STROKES_DEFAULT = listOf("S" to 3f, "M" to 5f, "L" to 10f, "XL" to 20f) private val SIZES_MARKER_DEFAULT = listOf("M" to 25f, "L" to 40f, "XL" to 60f, "XXL" to 80f) - -@Composable -fun Toolbar( - state: EditorState, - controlTower: EditorControlTower, - pageNumberInfo: String, - onNavigateToLibrary: () -> Unit, - onNavigateToBugReport: () -> Unit, - onNavigateToPages: () -> Unit, - onNavigateToHome: () -> Unit, - onToggleScribbleToErase: (Boolean) -> Unit, - onImagePicked: (Uri) -> Unit, - onExport: suspend (ExportTarget, ExportFormat) -> String -) { - val scope = rememberCoroutineScope() - - // Observe zoom level to decide button visibility - val zoomLevel by state.pageView.zoomLevel.collectAsState() - val showResetView = state.pageView.scroll != androidx.compose.ui.geometry.Offset.Zero || zoomLevel != 1.0f - - val uiState = ToolbarUiState( - isToolbarOpen = state.isToolbarOpen, - mode = state.mode, - pen = state.pen, - penSettings = state.penSettings, - eraser = state.eraser, - isMenuOpen = state.menuStates.isMenuOpen, - isStrokeSelectionOpen = state.menuStates.isStrokeSelectionOpen, - isBackgroundSelectorModalOpen = state.menuStates.isBackgroundSelectorModalOpen, - pageNumberInfo = pageNumberInfo, - hasClipboard = state.clipboard != null, - showResetView = showResetView, - backgroundType = state.pageView.pageFromDb?.backgroundType ?: "native", - backgroundPath = state.pageView.pageFromDb?.background ?: "blank", - backgroundPageNumber = state.pageView.getBackgroundPageNumber(), - notebookId = state.pageView.pageFromDb?.notebookId, - pageId = state.pageView.pageFromDb?.notebookId, - currentPageNumber = state.pageView.currentPageNumber - ) - - ToolbarContent( - uiState = uiState, - onToggleToolbar = { state.isToolbarOpen = !state.isToolbarOpen }, - onModeChange = { state.mode = it }, - onPenChange = { pen -> - if (state.mode == Mode.Draw && state.pen == pen) { - state.menuStates.isStrokeSelectionOpen = true - } else { - state.mode = Mode.Draw - state.pen = pen - } - }, - onPenSettingChange = { pen, setting -> - val settings = state.penSettings.toMutableMap() - settings[pen.penName] = setting - state.penSettings = settings - }, - onEraserChange = { state.eraser = it }, - onMenuToggle = { state.menuStates.isMenuOpen = !state.menuStates.isMenuOpen }, - onBackgroundSelectorToggle = { state.menuStates.isBackgroundSelectorModalOpen = it }, - onUndo = { controlTower.undo() }, - onRedo = { controlTower.redo() }, - onPaste = { controlTower.pasteFromClipboard() }, - onResetView = { controlTower.resetZoomAndScroll() }, - onNavigateToLibrary = onNavigateToLibrary, - onNavigateToBugReport = onNavigateToBugReport, - onNavigateToPages = onNavigateToPages, - onNavigateToHome = onNavigateToHome, - onToggleScribbleToErase = onToggleScribbleToErase, - onImagePicked = onImagePicked, - onExport = onExport, - onBackgroundChange = { type, path -> - val updatedPage = if (path == null) - state.pageView.pageFromDb!!.copy(backgroundType = type) - else state.pageView.pageFromDb!!.copy(background = path, backgroundType = type) - state.pageView.updatePageSettings(updatedPage) - scope.launch { CanvasEventBus.refreshUi.emit(Unit) } - }, - onDrawingStateCheck = { state.checkForSelectionsAndMenus() } - ) -} - @Composable fun ToolbarContent( uiState: ToolbarUiState, - onToggleToolbar: () -> Unit, - onModeChange: (Mode) -> Unit, - onPenChange: (Pen) -> Unit, - onPenSettingChange: (Pen, PenSetting) -> Unit, - onEraserChange: (Eraser) -> Unit, - onMenuToggle: () -> Unit, - onBackgroundSelectorToggle: (Boolean) -> Unit, - onUndo: () -> Unit, - onRedo: () -> Unit, - onPaste: () -> Unit, - onResetView: () -> Unit, - onNavigateToLibrary: () -> Unit, - onNavigateToBugReport: () -> Unit, - onNavigateToPages: () -> Unit, - onNavigateToHome: () -> Unit, - onToggleScribbleToErase: (Boolean) -> Unit, - onImagePicked: (Uri) -> Unit, - onExport: suspend (ExportTarget, ExportFormat) -> String, - onBackgroundChange: (String, String?) -> Unit, - onDrawingStateCheck: () -> Unit + onAction: (ToolbarAction) -> Unit, + onDrawingStateCheck: () -> Unit, ) { + // Activity result launcher for picking images + val pickMedia = rememberLauncherForActivityResult(contract = PickVisualMedia()) { uri -> + uri?.let { onAction(ToolbarAction.ImagePicked(it)) } + } - // Create an activity result launcher for picking visual media (images in this case) - val pickMedia = - rememberLauncherForActivityResult(contract = PickVisualMedia()) { uri -> - if (uri == null) { - log.w("PickVisualMedia: uri is null (user cancelled or provider returned null)") - return@rememberLauncherForActivityResult - } - onImagePicked(uri) - } - - // on exit of toolbar, update drawing state + // On exit or change of toolbar states, check if we should allow raw drawing LaunchedEffect(uiState.isBackgroundSelectorModalOpen, uiState.isMenuOpen) { - log.i("Updating drawing state") onDrawingStateCheck() } @@ -205,8 +70,8 @@ fun ToolbarContent( initialPageNumberInPdf = uiState.backgroundPageNumber, notebookId = uiState.notebookId, pageNumberInBook = uiState.currentPageNumber, - onChange = onBackgroundChange, - onClose = { onBackgroundSelectorToggle(false) } + onChange = { type, path -> onAction(ToolbarAction.BackgroundChanged(type, path)) }, + onClose = { onAction(ToolbarAction.ToggleBackgroundSelector(false)) } ) } @@ -232,272 +97,225 @@ fun ToolbarContent( .fillMaxWidth() ) { ToolbarButton( - onSelect = onToggleToolbar, vectorIcon = FeatherIcons.EyeOff, contentDescription = "close toolbar" - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.ToggleToolbar) }, + vectorIcon = FeatherIcons.EyeOff, + contentDescription = "close toolbar" ) + VerticalDivider() + + // Pens PenToolbarButton( pen = Pen.BALLPEN, icon = R.drawable.ballpen, isSelected = isSelected(uiState, Pen.BALLPEN), - onSelect = { onPenChange(Pen.BALLPEN) }, + onSelect = { onAction(ToolbarAction.ChangePen(Pen.BALLPEN)) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.BALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), - onChangeSetting = { onPenSettingChange(Pen.BALLPEN, it) }) + onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(Pen.BALLPEN, it)) }) if (!GlobalAppSettings.current.monochromeMode) { - PenToolbarButton( - pen = Pen.REDBALLPEN, - icon = R.drawable.ballpenred, - isSelected = isSelected(uiState, Pen.REDBALLPEN), - onSelect = { onPenChange(Pen.REDBALLPEN) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.REDBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.RED), - onChangeSetting = { onPenSettingChange(Pen.REDBALLPEN, it) }, - ) - - PenToolbarButton( - pen = Pen.BLUEBALLPEN, - icon = R.drawable.ballpenblue, - isSelected = isSelected(uiState, Pen.BLUEBALLPEN), - onSelect = { onPenChange(Pen.BLUEBALLPEN) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.BLUEBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLUE), - onChangeSetting = { onPenSettingChange(Pen.BLUEBALLPEN, it) }, - ) - PenToolbarButton( - pen = Pen.GREENBALLPEN, - icon = R.drawable.ballpengreen, - isSelected = isSelected(uiState, Pen.GREENBALLPEN), - onSelect = { onPenChange(Pen.GREENBALLPEN) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.GREENBALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.GREEN), - onChangeSetting = { onPenSettingChange(Pen.GREENBALLPEN, it) }, - ) + listOf( + Triple(Pen.REDBALLPEN, R.drawable.ballpenred, android.graphics.Color.RED), + Triple( + Pen.BLUEBALLPEN, + R.drawable.ballpenblue, + android.graphics.Color.BLUE + ), + Triple( + Pen.GREENBALLPEN, + R.drawable.ballpengreen, + android.graphics.Color.GREEN + ) + ).forEach { (pen, icon, defaultColor) -> + PenToolbarButton( + pen = pen, + icon = icon, + isSelected = isSelected(uiState, pen), + onSelect = { onAction(ToolbarAction.ChangePen(pen)) }, + sizes = SIZES_STROKES_DEFAULT, + penSetting = uiState.penSettings[pen.penName] ?: PenSetting( + 5f, + defaultColor + ), + onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(pen, it)) }, + ) + } } + if (GlobalAppSettings.current.neoTools) { PenToolbarButton( pen = Pen.PENCIL, icon = R.drawable.pencil, isSelected = isSelected(uiState, Pen.PENCIL), - onSelect = { onPenChange(Pen.PENCIL) }, + onSelect = { onAction(ToolbarAction.ChangePen(Pen.PENCIL)) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.PENCIL.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), - onChangeSetting = { onPenSettingChange(Pen.PENCIL, it) }, + onChangeSetting = { + onAction( + ToolbarAction.ChangePenSetting( + Pen.PENCIL, + it + ) + ) + } ) PenToolbarButton( pen = Pen.BRUSH, icon = R.drawable.brush, isSelected = isSelected(uiState, Pen.BRUSH), - onSelect = { onPenChange(Pen.BRUSH) }, + onSelect = { onAction(ToolbarAction.ChangePen(Pen.BRUSH)) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.BRUSH.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), - onChangeSetting = { onPenSettingChange(Pen.BRUSH, it) }, + onChangeSetting = { + onAction( + ToolbarAction.ChangePenSetting( + Pen.BRUSH, + it + ) + ) + } ) } PenToolbarButton( pen = Pen.FOUNTAIN, icon = R.drawable.fountain, isSelected = isSelected(uiState, Pen.FOUNTAIN), - onSelect = { onPenChange(Pen.FOUNTAIN) }, + onSelect = { onAction(ToolbarAction.ChangePen(Pen.FOUNTAIN)) }, sizes = SIZES_STROKES_DEFAULT, penSetting = uiState.penSettings[Pen.FOUNTAIN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), - onChangeSetting = { onPenSettingChange(Pen.FOUNTAIN, it) }, + onChangeSetting = { + onAction( + ToolbarAction.ChangePenSetting( + Pen.FOUNTAIN, + it + ) + ) + }, ) LineToolbarButton( - unSelect = { onModeChange(Mode.Draw) }, + unSelect = { onAction(ToolbarAction.ChangeMode(Mode.Draw)) }, icon = R.drawable.line, isSelected = uiState.mode == Mode.Line, - onSelect = { onModeChange(Mode.Line) }, + onSelect = { onAction(ToolbarAction.ChangeMode(Mode.Line)) }, ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + VerticalDivider() PenToolbarButton( pen = Pen.MARKER, icon = R.drawable.marker, isSelected = isSelected(uiState, Pen.MARKER), - onSelect = { onPenChange(Pen.MARKER) }, + onSelect = { onAction(ToolbarAction.ChangePen(Pen.MARKER)) }, sizes = SIZES_MARKER_DEFAULT, penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting(40f, android.graphics.Color.LTGRAY), - onChangeSetting = { onPenSettingChange(Pen.MARKER, it) }) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(Pen.MARKER, it)) } ) + + VerticalDivider() + EraserToolbarButton( isSelected = uiState.mode == Mode.Erase, - onSelect = { onModeChange(Mode.Erase) }, + onSelect = { onAction(ToolbarAction.ChangeMode(Mode.Erase)) }, value = uiState.eraser, - onChange = onEraserChange, - toggleScribbleToErase = onToggleScribbleToErase, + onChange = { onAction(ToolbarAction.ChangeEraser(it)) }, + toggleScribbleToErase = { onAction(ToolbarAction.ToggleScribbleToErase(it)) }, onMenuOpenChange = {} ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + + VerticalDivider() + ToolbarButton( isSelected = uiState.mode == Mode.Select, - onSelect = { onModeChange(Mode.Select) }, + onSelect = { onAction(ToolbarAction.ChangeMode(Mode.Select)) }, iconId = R.drawable.lasso, contentDescription = "lasso" ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + + VerticalDivider() ToolbarButton( iconId = R.drawable.image, contentDescription = "Image picker", - onSelect = { - // Call insertImage when the button is tapped - log.i("Launching image picker...") - pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) - } - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) } ) + VerticalDivider() + if (uiState.hasClipboard) { ToolbarButton( vectorIcon = FeatherIcons.Clipboard, contentDescription = "paste", - onSelect = onPaste - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.Paste) } ) + VerticalDivider() } if (uiState.showResetView) { ToolbarButton( vectorIcon = FeatherIcons.RefreshCcw, contentDescription = "reset zoom and scroll", - onSelect = onResetView - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.ResetView) } ) + VerticalDivider() } Spacer(Modifier.weight(1f)) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + VerticalDivider() ToolbarButton( - onSelect = onUndo, + onSelect = { onAction(ToolbarAction.Undo) }, iconId = R.drawable.undo, contentDescription = "undo" ) - ToolbarButton( - onSelect = onRedo, + onSelect = { onAction(ToolbarAction.Redo) }, iconId = R.drawable.redo, contentDescription = "redo" ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + VerticalDivider() + if (uiState.notebookId != null) { Box( contentAlignment = Alignment.Center, modifier = Modifier .height(35.dp) - .padding(10.dp, 0.dp) + .padding(horizontal = 10.dp) ) { Text( text = uiState.pageNumberInfo, fontWeight = FontWeight.Light, - modifier = Modifier.noRippleClickable(onNavigateToPages), + modifier = Modifier.noRippleClickable { onAction(ToolbarAction.NavigateToPages) }, textAlign = TextAlign.Center ) } - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + VerticalDivider() } - // Add Library Button + ToolbarButton( iconId = R.drawable.home, contentDescription = "library", - onSelect = onNavigateToHome - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.NavigateToHome) } ) + + VerticalDivider() + Column { ToolbarButton( - onSelect = onMenuToggle, iconId = R.drawable.menu, contentDescription = "menu" + onSelect = { onAction(ToolbarAction.ToggleMenu) }, + iconId = R.drawable.menu, + contentDescription = "menu" ) - if (uiState.isMenuOpen) + if (uiState.isMenuOpen) { ToolbarMenu( - onExportPage = { format -> - uiState.pageId?.let { - onExport( - ExportTarget.Page(it), - format - ) - } ?: "failed to export page, page Id is null" - }, - onExportBook = uiState.notebookId?.let { bookId -> - { format -> onExport(ExportTarget.Book(bookId), format) } - }, - goToBugReport = onNavigateToBugReport, - goToLibrary = onNavigateToLibrary, - onClose = { - if (uiState.isMenuOpen) { - onMenuToggle() - } - }, - onBackgroundSelectorModalOpen = { - onBackgroundSelectorToggle(true) - } + uiState = uiState, + onAction = onAction ) + } } } @@ -511,10 +329,12 @@ fun ToolbarContent( } else { // Button to show Toolbar ToolbarButton( - onSelect = onToggleToolbar, + onSelect = { onAction(ToolbarAction.ToggleToolbar) }, iconId = presentlyUsedToolIcon(uiState.mode, uiState.pen), penColor = if (uiState.mode != Mode.Erase) uiState.penSettings[uiState.pen.penName]?.color?.let { - Color(it) + Color( + it + ) } else null, contentDescription = "open toolbar", modifier = Modifier @@ -524,6 +344,16 @@ fun ToolbarContent( } } +@Composable +private fun VerticalDivider() { + Box( + Modifier + .fillMaxHeight() + .width(0.5.dp) + .background(Color.Black) + ) +} + fun presentlyUsedToolIcon(mode: Mode, pen: Pen): Int { return when (mode) { Mode.Draw -> { @@ -563,25 +393,7 @@ fun ToolbarPreview() { ToolbarContent( uiState = uiState, - onToggleToolbar = {}, - onModeChange = {}, - onPenChange = {}, - onPenSettingChange = { _, _ -> }, - onEraserChange = {}, - onMenuToggle = {}, - onBackgroundSelectorToggle = {}, - onUndo = {}, - onRedo = {}, - onPaste = {}, - onResetView = {}, - onNavigateToLibrary = {}, - onNavigateToBugReport = {}, - onNavigateToPages = {}, - onNavigateToHome = {}, - onToggleScribbleToErase = {}, - onImagePicked = {}, - onExport = { _, _ -> "" }, - onBackgroundChange = { _, _ -> }, + onAction = {}, onDrawingStateCheck = {} ) } \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index a60d1cac..a40435a1 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -26,66 +25,47 @@ import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties import com.ethran.notable.R import com.ethran.notable.data.datastore.BUTTON_SIZE -import com.ethran.notable.editor.canvas.CanvasEventBus +import com.ethran.notable.editor.ToolbarAction +import com.ethran.notable.editor.ToolbarUiState +import com.ethran.notable.editor.state.Mode +import com.ethran.notable.editor.utils.Pen +import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportFormat -import com.ethran.notable.ui.LocalSnackContext -import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.convertDpToPixel import com.ethran.notable.ui.noRippleClickable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +/** + * Menu for the toolbar, providing export options and other page-level actions. + * Centralizes actions via [ToolbarAction]. + */ @Composable fun ToolbarMenu( - onExportPage: suspend (ExportFormat) -> String, - onExportBook: (suspend (ExportFormat) -> String)? = null, - goToBugReport: () -> Unit, - goToLibrary: () -> Unit, - onClose: () -> Unit, - onBackgroundSelectorModalOpen: () -> Unit + uiState: ToolbarUiState, + onAction: (ToolbarAction) -> Unit ) { val context = LocalContext.current Popup( alignment = Alignment.TopEnd, - onDismissRequest = { onClose() }, + onDismissRequest = { onAction(ToolbarAction.ToggleMenu) }, offset = IntOffset( - convertDpToPixel((-10).dp, context).toInt(), convertDpToPixel(50.dp, context).toInt() + convertDpToPixel((-10).dp, context).toInt(), + convertDpToPixel(50.dp, context).toInt() ), properties = PopupProperties(focusable = true), ) { ToolbarMenuContent( - onExportPage = onExportPage, - onExportBook = onExportBook, - goToBugReport = goToBugReport, - goToLibrary = goToLibrary, - onClose = onClose, - onBackgroundSelectorModalOpen = onBackgroundSelectorModalOpen + uiState = uiState, + onAction = onAction ) } } @Composable private fun ToolbarMenuContent( - onExportPage: suspend (ExportFormat) -> String, - onExportBook: (suspend (ExportFormat) -> String)? = null, - goToBugReport: () -> Unit, - goToLibrary: () -> Unit, - onClose: () -> Unit, - onBackgroundSelectorModalOpen: () -> Unit + uiState: ToolbarUiState, + onAction: (ToolbarAction) -> Unit ) { - val scope = rememberCoroutineScope() - val snackManager = LocalSnackContext.current - - val exportingPageToPdfMsg = stringResource(R.string.exporting_the_page_to, "PDF") - val exportingPageToPngMsg = stringResource(R.string.exporting_the_page_to, "PNG") - val exportingPageToJpegMsg = stringResource(R.string.exporting_the_page_to, "JPEG") - val exportingPageToXoppMsg = stringResource(R.string.exporting_the_page_to, "xopp") - val exportingBookToPdfMsg = stringResource(R.string.exporting_the_book_to, "PDF") - val exportingBookToPngMsg = stringResource(R.string.exporting_the_book_to, "PNG") - val exportingBookToXoppMsg = stringResource(R.string.exporting_the_book_to, "xopp") - val clearedAllStrokesMsg = stringResource(R.string.cleared_all_strokes) - Column( Modifier .padding(bottom = (BUTTON_SIZE + 5).dp) @@ -93,105 +73,71 @@ private fun ToolbarMenuContent( .background(Color.White) .width(IntrinsicSize.Max) ) { - // Library + // Home / Library MenuItem(stringResource(R.string.home_view_name)) { - goToLibrary() - onClose() + onAction(ToolbarAction.NavigateToLibrary) + onAction(ToolbarAction.ToggleMenu) } DividerCentered() // Page exports MenuItem(stringResource(R.string.export_page_to, "PDF")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingPageToPdfMsg) { - onExportPage(ExportFormat.PDF) - } - } - onClose() + onAction(ToolbarAction.ExportPage(ExportFormat.PDF)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.export_page_to, "PNG")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingPageToPngMsg) { - onExportPage(ExportFormat.PNG) - } - } - onClose() + onAction(ToolbarAction.ExportPage(ExportFormat.PNG)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.export_page_to, "JPEG")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingPageToJpegMsg) { - onExportPage(ExportFormat.JPEG) - } - } - onClose() + onAction(ToolbarAction.ExportPage(ExportFormat.JPEG)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.export_page_to, "xopp")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingPageToXoppMsg) { - onExportPage(ExportFormat.XOPP) - } - } - onClose() + onAction(ToolbarAction.ExportPage(ExportFormat.XOPP)) + onAction(ToolbarAction.ToggleMenu) } DividerCentered() // Book exports - if (onExportBook != null) { + if (uiState.notebookId != null) { MenuItem(stringResource(R.string.export_book_to, "PDF")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingBookToPdfMsg) { - onExportBook(ExportFormat.PDF) - } - } - onClose() + onAction(ToolbarAction.ExportBook(ExportFormat.PDF)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.export_book_to, "PNG")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingBookToPngMsg) { - onExportBook(ExportFormat.PNG) - } - } - onClose() + onAction(ToolbarAction.ExportBook(ExportFormat.PNG)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.export_book_to, "xopp")) { - scope.launch(Dispatchers.IO) { - snackManager.runWithSnack(exportingBookToXoppMsg) { - onExportBook(ExportFormat.XOPP) - } - } - onClose() + onAction(ToolbarAction.ExportBook(ExportFormat.XOPP)) + onAction(ToolbarAction.ToggleMenu) } DividerCentered() } MenuItem(stringResource(R.string.clean_all_strokes)) { - scope.launch { - CanvasEventBus.clearPageSignal.emit(Unit) - snackManager.displaySnack( - SnackConf( - text = clearedAllStrokesMsg, duration = 3000 - ) - ) - } - onClose() + onAction(ToolbarAction.ClearAllStrokes) + onAction(ToolbarAction.ToggleMenu) } DividerCentered() MenuItem(stringResource(R.string.change_background)) { - onBackgroundSelectorModalOpen() - onClose() + onAction(ToolbarAction.ToggleBackgroundSelector(true)) + onAction(ToolbarAction.ToggleMenu) } MenuItem(stringResource(R.string.bug_report)) { - goToBugReport() - onClose() + onAction(ToolbarAction.NavigateToBugReport) + onAction(ToolbarAction.ToggleMenu) } } } @Composable private fun MenuItem( - label: String, onClick: () -> Unit + label: String, + onClick: () -> Unit ) { Box( Modifier @@ -221,11 +167,13 @@ private fun ColumnScope.DividerCentered() { @Preview(showBackground = true) fun ToolbarMenuPreview() { ToolbarMenuContent( - onExportPage = { "Success" }, - onExportBook = { "Success" }, - goToBugReport = {}, - goToLibrary = {}, - onClose = {}, - onBackgroundSelectorModalOpen = {} + uiState = ToolbarUiState( + isMenuOpen = true, + notebookId = "book1", + mode = Mode.Draw, + pen = Pen.BALLPEN, + penSettings = mapOf(Pen.BALLPEN.penName to PenSetting(5f, android.graphics.Color.BLACK)) + ), + onAction = {} ) -} \ No newline at end of file +} From 89e998de8c4f11342f0d7b8ec0bfdaa842f75954 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 18:19:56 +0100 Subject: [PATCH 10/21] implement reloadFromDb event and rename restartAfterConfChange to reinitSignal --- .../java/com/ethran/notable/MainActivity.kt | 2 +- .../com/ethran/notable/editor/EditorView.kt | 2 +- .../java/com/ethran/notable/editor/PageView.kt | 18 ++++++++---------- .../notable/editor/canvas/CanvasEventBus.kt | 5 ++++- .../editor/canvas/CanvasObserverRegistry.kt | 15 +++++++++++---- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/MainActivity.kt b/app/src/main/java/com/ethran/notable/MainActivity.kt index fa6b6943..6cfc2a91 100644 --- a/app/src/main/java/com/ethran/notable/MainActivity.kt +++ b/app/src/main/java/com/ethran/notable/MainActivity.kt @@ -136,7 +136,7 @@ class MainActivity : ComponentActivity() { super.onRestart() // redraw after device sleep this.lifecycleScope.launch { - CanvasEventBus.restartAfterConfChange.emit(Unit) + CanvasEventBus.reinitSignal.emit(Unit) } } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index f987513a..b0a775de 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -174,7 +174,7 @@ fun EditorView( CanvasEventBus.addImageByUri.value = event.uri } EditorUiEvent.RefreshCanvas -> { - CanvasEventBus.refreshUi.emit(Unit) + CanvasEventBus.reloadFromDb.emit(Unit) } EditorUiEvent.CheckDrawingState -> { editorState.checkForSelectionsAndMenus() diff --git a/app/src/main/java/com/ethran/notable/editor/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index 47fe8d0f..761e2065 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -797,17 +797,15 @@ class PageView( // updates page setting in db, (for instance type of background) -// and redraws page to vew. - fun updatePageSettings(page: Page) { - coroutineScope.launch(Dispatchers.IO) { - appRepository.pageRepository.update(page) - pageFromDb = appRepository.pageRepository.getById(currentPageId) - log.i("Page settings updated, ${pageFromDb?.background} | ${page.background}") - withContext(Dispatchers.Main) { - drawAreaScreenCoordinates(Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) - persistBitmapDebounced() - } + // and redraws page to vew. + suspend fun refreshCurrentPage() { + pageFromDb = appRepository.pageRepository.getById(currentPageId) + log.i("Refresh current page, bacground: ${pageFromDb?.background}") + withContext(Dispatchers.Main) { + drawAreaScreenCoordinates(Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) + persistBitmapDebounced() } + } fun updateDimensions(newWidth: Int, newHeight: Int) { diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt index cedeaf92..154d564e 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt @@ -20,8 +20,11 @@ object CanvasEventBus { val refreshUiImmediately = MutableSharedFlow( replay = 1, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) + val reinitSignal = MutableSharedFlow() + val reloadFromDb = MutableSharedFlow() + + val isDrawing = MutableSharedFlow() - val restartAfterConfChange = MutableSharedFlow() // used for managing drawing state on regain focus val onFocusChange = MutableSharedFlow() diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index b73e08e1..9cc3387b 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -4,9 +4,6 @@ import android.graphics.Rect import androidx.compose.runtime.snapshotFlow import com.ethran.notable.data.AppRepository import com.ethran.notable.data.PageDataManager -import com.ethran.notable.data.db.BookRepository -import com.ethran.notable.data.db.KvProxy -import com.ethran.notable.data.db.PageRepository import com.ethran.notable.editor.PageView import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History @@ -51,6 +48,7 @@ class CanvasObserverRegistry( observeSelectionGesture() observeClearPage() observeRestartAfterConfChange() + observeReloadFromDb() observePenChanges() observeIsDrawingSnapshot() observeToolbar() @@ -157,7 +155,7 @@ class CanvasObserverRegistry( private fun observeRestartAfterConfChange() { coroutineScope.launch { - CanvasEventBus.restartAfterConfChange.collect { + CanvasEventBus.reinitSignal.collect { logCanvasObserver.v("Configuration changed!") drawCanvas.init() drawCanvas.drawCanvasToView(null) @@ -165,6 +163,15 @@ class CanvasObserverRegistry( } } + private fun observeReloadFromDb(){ + coroutineScope.launch { + CanvasEventBus.reloadFromDb.collect { + page.refreshCurrentPage() + refreshManager.refreshUiSuspend() + } + } + } + private fun observePenChanges() { coroutineScope.launch { snapshotFlow { state.pen }.drop(1).collect { From 834e4f4b7cd8f779c178bca66e7da04397da8e38 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Sun, 8 Mar 2026 20:22:30 +0100 Subject: [PATCH 11/21] Refactor menu and drawing state management to EditorViewModel - Migrated menu and drawing state logic from `EditorState` to `EditorViewModel` to consolidate UI control. - Introduced `CanvasEventBus.closeMenusSignal` to trigger menu closures from the canvas layer. - Added `updateDrawingState` to re-evaluate drawing availability based on active menus and selections. - Improved synchronization between `EditorState` and `EditorViewModel` using `LaunchedEffect` observers for zoom, scroll, and selection status. - Added a snackbar notification when clearing all strokes. - Switched to `collectAsStateWithLifecycle` for toolbar state observation. --- .../com/ethran/notable/editor/EditorView.kt | 39 +++++++++-- .../ethran/notable/editor/EditorViewModel.kt | 66 ++++++++++++++++--- .../notable/editor/canvas/CanvasEventBus.kt | 4 ++ .../editor/canvas/CanvasObserverRegistry.kt | 5 +- .../notable/editor/state/EditorState.kt | 27 -------- .../editor/ui/toolbar/PositionedToolbar.kt | 4 +- 6 files changed, 96 insertions(+), 49 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index b0a775de..900f6f92 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.navigation.NavController @@ -157,6 +156,7 @@ fun EditorView( is EditorUiEvent.ResetView -> editorControlTower.resetZoomAndScroll() is EditorUiEvent.ClearAllStrokes -> { CanvasEventBus.clearPageSignal.emit(Unit) + snackManager.displaySnack(SnackConf(text = "Cleared all strokes")) } is EditorUiEvent.NavigateToLibrary -> { navController.navigate(LibraryDestination.createRoute(event.folderId)) @@ -176,9 +176,6 @@ fun EditorView( EditorUiEvent.RefreshCanvas -> { CanvasEventBus.reloadFromDb.emit(Unit) } - EditorUiEvent.CheckDrawingState -> { - editorState.checkForSelectionsAndMenus() - } is EditorUiEvent.ModeChanged -> { editorState.mode = event.mode } @@ -200,11 +197,39 @@ fun EditorView( } } + // Handle Canvas signals in UI + LaunchedEffect(Unit) { + CanvasEventBus.closeMenusSignal.collect { + log.e("Closing all menus") + viewModel.onToolbarAction(ToolbarAction.CloseAllMenus) + } + } + + // Handle focus changes from Canvas + LaunchedEffect(Unit) { + CanvasEventBus.onFocusChange.collect { hasFocus -> + log.e("Canvas has focus: $hasFocus") + viewModel.onFocusChanged(hasFocus) + } + } + // Sync legacy state to ViewModel for Toolbar rendering val zoomLevel by page.zoomLevel.collectAsState() - LaunchedEffect(zoomLevel, page.scroll, editorState.clipboard, editorState.isToolbarOpen, editorState.mode, editorState.pen, editorState.eraser, editorState.penSettings) { + val selectionActive = editorState.selectionState.isNonEmpty() + LaunchedEffect( + zoomLevel, + page.scroll, + editorState.clipboard, + editorState.isToolbarOpen, + editorState.mode, + editorState.pen, + editorState.eraser, + editorState.penSettings, + selectionActive + ) { viewModel.setHasClipboard(editorState.clipboard != null) - viewModel.setShowResetView(page.scroll != Offset.Zero || zoomLevel != 1.0f) + viewModel.setShowResetView(zoomLevel != 1.0f) // page.scroll != Offset.Zero + viewModel.setSelectionActive(selectionActive) viewModel.updateToolbarSettings(ToolbarUiState( isToolbarOpen = editorState.isToolbarOpen, mode = editorState.mode, @@ -268,7 +293,7 @@ fun EditorView( } PositionedToolbar( viewModel = viewModel, - onDrawingStateCheck = { editorState.checkForSelectionsAndMenus() } + onDrawingStateCheck = { viewModel.updateDrawingState() } ) HorizontalScrollIndicator(state = editorState) } diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 61af820e..0ddfe5f5 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -10,6 +10,7 @@ import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.getPageIndex import com.ethran.notable.data.db.getParentFolder +import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen @@ -19,6 +20,7 @@ import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -30,6 +32,10 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject + +private val log = ShipBook.getLogger("EditorViewModel") + + /** * Toolbar Actions (Intents) representing user interactions. */ @@ -61,6 +67,8 @@ sealed class ToolbarAction { object NavigateToBugReport : ToolbarAction() object NavigateToPages : ToolbarAction() object NavigateToHome : ToolbarAction() + + object CloseAllMenus : ToolbarAction() } /** @@ -80,9 +88,6 @@ sealed class EditorUiEvent { object ClearAllStrokes : EditorUiEvent() object RefreshCanvas : EditorUiEvent() data class CopyImageToCanvas(val uri: Uri) : EditorUiEvent() - - // Signal to check if drawing should be enabled (e.g. menus changed) - object CheckDrawingState : EditorUiEvent() // Sync state back to EditorState data class ModeChanged(val mode: Mode) : EditorUiEvent() @@ -107,6 +112,7 @@ data class ToolbarUiState( val pageNumberInfo: String = "1/1", val hasClipboard: Boolean = false, val showResetView: Boolean = false, + val isSelectionActive: Boolean = false, // Context needed for visibility rules in UI val notebookId: String? = null, @@ -143,27 +149,27 @@ class EditorViewModel @Inject constructor( val newVisible = !_toolbarState.value.isToolbarOpen _toolbarState.update { it.copy(isToolbarOpen = newVisible) } sendUiEvent(EditorUiEvent.ToolbarVisibilityChanged(newVisible)) - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() } is ToolbarAction.ChangeMode -> { _toolbarState.update { it.copy(mode = action.mode) } sendUiEvent(EditorUiEvent.ModeChanged(action.mode)) - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() } is ToolbarAction.ChangePen -> handlePenChange(action.pen) is ToolbarAction.ChangePenSetting -> handlePenSettingChange(action.pen, action.setting) is ToolbarAction.ChangeEraser -> { _toolbarState.update { it.copy(eraser = action.eraser) } sendUiEvent(EditorUiEvent.EraserChanged(action.eraser)) - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() } is ToolbarAction.ToggleMenu -> { _toolbarState.update { it.copy(isMenuOpen = !it.isMenuOpen) } - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() } is ToolbarAction.ToggleBackgroundSelector -> { _toolbarState.update { it.copy(isBackgroundSelectorModalOpen = action.isOpen) } - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() } is ToolbarAction.ToggleScribbleToErase -> updateScribbleToErase(action.enabled) @@ -184,6 +190,8 @@ class EditorViewModel @Inject constructor( ToolbarAction.NavigateToBugReport -> sendUiEvent(EditorUiEvent.NavigateToBugReport) ToolbarAction.NavigateToPages -> handleNavigateToPages() ToolbarAction.NavigateToHome -> sendUiEvent(EditorUiEvent.NavigateToLibrary(null)) + + ToolbarAction.CloseAllMenus -> handleCloseAllMenus() } } @@ -201,7 +209,40 @@ class EditorViewModel @Inject constructor( state.copy(mode = Mode.Draw, pen = pen) } } - sendUiEvent(EditorUiEvent.CheckDrawingState) + updateDrawingState() + } + + private fun handleCloseAllMenus() { + log.e("Closing all menus in EditorViewModel") + _toolbarState.update { + it.copy( + isMenuOpen = false, + isStrokeSelectionOpen = false, + isBackgroundSelectorModalOpen = false + ) + } + updateDrawingState() + } + + /** + * Re-evaluates whether drawing should be enabled based on menu and selection states. + * Also handles switching back to drawing mode when menus are closed. + */ + fun updateDrawingState() { + val state = _toolbarState.value + val anyMenuOpen = + state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen + val shouldBeDrawing = !anyMenuOpen && !_toolbarState.value.isSelectionActive + log.e("Drawing state: $shouldBeDrawing") + viewModelScope.launch { + CanvasEventBus.isDrawing.emit(shouldBeDrawing) + } + } + + fun onFocusChanged(isFocused: Boolean) { + if (isFocused) { + updateDrawingState() + } } private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { @@ -314,6 +355,13 @@ class EditorViewModel @Inject constructor( _toolbarState.update { it.copy(showResetView = showResetView) } } + fun setSelectionActive(active: Boolean) { + if (_toolbarState.value.isSelectionActive != active) { + _toolbarState.update { it.copy(isSelectionActive = active) } + updateDrawingState() + } + } + fun updateToolbarSettings(state: ToolbarUiState) { _toolbarState.update { it.copy( diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt index 154d564e..aa58394c 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasEventBus.kt @@ -45,6 +45,10 @@ object CanvasEventBus { // For cleaning whole page, activated from toolbar menu val clearPageSignal = MutableSharedFlow() + // Signal to UI to close any open menus/modals, + // observed in EditorView + val closeMenusSignal = MutableSharedFlow() + // For QuickNav scrolling with previews val saveCurrent = MutableSharedFlow(extraBufferCapacity = 1) diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index 9cc3387b..5219451c 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -103,7 +103,6 @@ class CanvasObserverRegistry( CanvasEventBus.onFocusChange.collect { hasFocus -> logCanvasObserver.v("App has focus: $hasFocus") if (hasFocus) { - state.checkForSelectionsAndMenus() inputHandler.updatePenAndStroke() // The setting might been changed by other app. drawCanvas.drawCanvasToView(null) } else { @@ -202,9 +201,7 @@ class CanvasObserverRegistry( logCanvasObserver.v("isDrawing change to $it") // We need to close all menus if (it) { -// logCallStack("Closing all menus") - state.closeAllMenus() -// EpdController.waitForUpdateFinished() // it does not work. + CanvasEventBus.closeMenusSignal.emit(Unit) waitForEpdRefresh() } inputHandler.updateIsDrawing() diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index 0abb8c1c..d18821c2 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -20,21 +20,6 @@ enum class Mode { Draw, Erase, Select, Line } -@Stable -class MenuStates { - var isStrokeSelectionOpen by mutableStateOf(false) - var isMenuOpen by mutableStateOf(false) - var isBackgroundSelectorModalOpen by mutableStateOf(false) - fun closeAll() { - isStrokeSelectionOpen = false - isMenuOpen = false - isBackgroundSelectorModalOpen = false - } - - val anyMenuOpen: Boolean - get() = isStrokeSelectionOpen || isMenuOpen || isBackgroundSelectorModalOpen -} - class EditorState( val bookId: String? = null, @@ -126,22 +111,11 @@ class EditorState( get() = _clipboard set(value) { this._clipboard = value - // The clipboard content must survive the EditorState, so we store a copy in // a singleton that lives outside of the EditorState Clipboard.content = value } - val menuStates = MenuStates() - fun closeAllMenus() = menuStates.closeAll() - - fun checkForSelectionsAndMenus() { - val shouldBeDrawing = !menuStates.anyMenuOpen && !selectionState.isNonEmpty() - if (isDrawing != shouldBeDrawing) { - log.d("Drawing state should be: $shouldBeDrawing (menus open: ${menuStates.anyMenuOpen}, selection active: ${selectionState.isNonEmpty()})") - isDrawing = shouldBeDrawing - } - } /** * Changes the current page to the one with the specified [id]. @@ -151,7 +125,6 @@ class EditorState( suspend fun changePage(id: String) { log.d("Changing page to $id, from $currentPageId") updateOpenedPage(id) - closeAllMenus() selectionState.reset() } } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt index dc51825e..05070a2c 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt @@ -5,9 +5,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.ethran.notable.data.datastore.AppSettings import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.EditorViewModel @@ -24,7 +24,7 @@ fun PositionedToolbar( onDrawingStateCheck: () -> Unit ) { val position = GlobalAppSettings.current.toolbarPosition - val toolbarState by viewModel.toolbarState.collectAsState() + val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() val toolbar = @Composable { ToolbarContent( From 3d15d7990ab25c9c5119f0341ac6f93b54f22a21 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 10:04:02 +0100 Subject: [PATCH 12/21] code formating --- .../com/ethran/notable/editor/EditorView.kt | 70 +++++++++++-------- .../editor/canvas/CanvasObserverRegistry.kt | 4 +- .../notable/editor/state/EditorState.kt | 4 +- .../editor/ui/toolbar/EraserToolbarButton.kt | 2 - .../editor/ui/toolbar/PositionedToolbar.kt | 8 ++- .../notable/editor/ui/toolbar/Toolbar.kt | 25 +++++-- .../editor/ui/toolbar/ToolbarButton.kt | 1 + .../notable/editor/ui/toolbar/ToolbarMenu.kt | 4 +- 8 files changed, 70 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 900f6f92..48cb2558 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -96,8 +96,7 @@ fun EditorView( log.i("Could not find page, Cleaning book") SnackState.globalSnackFlow.tryEmit( SnackConf( - text = "Could not find page, cleaning book", - duration = 4000 + text = "Could not find page, cleaning book", duration = 4000 ) ) scope.launch(Dispatchers.IO) { @@ -127,17 +126,16 @@ fun EditorView( ) } - val editorState = - remember { - EditorState( - appRepository = appRepository, - bookId = bookId, - pageId = pageId, - pageView = page, - persistedEditorSettings = editorSettingCacheManager.getEditorSettings(), - onPageChange = onPageChange - ) - } + val editorState = remember { + EditorState( + appRepository = appRepository, + bookId = bookId, + pageId = pageId, + pageView = page, + persistedEditorSettings = editorSettingCacheManager.getEditorSettings(), + onPageChange = onPageChange + ) + } val history = remember { History(page) @@ -158,38 +156,49 @@ fun EditorView( CanvasEventBus.clearPageSignal.emit(Unit) snackManager.displaySnack(SnackConf(text = "Cleared all strokes")) } + is EditorUiEvent.NavigateToLibrary -> { navController.navigate(LibraryDestination.createRoute(event.folderId)) } + is EditorUiEvent.NavigateToPages -> { navController.navigate(PagesDestination.createRoute(event.bookId)) } + EditorUiEvent.NavigateToBugReport -> { navController.navigate(BugReportDestination.route) } + is EditorUiEvent.ShowSnackbar -> { snackManager.displaySnack(SnackConf(text = event.message)) } + is EditorUiEvent.CopyImageToCanvas -> { CanvasEventBus.addImageByUri.value = event.uri } + EditorUiEvent.RefreshCanvas -> { CanvasEventBus.reloadFromDb.emit(Unit) } + is EditorUiEvent.ModeChanged -> { editorState.mode = event.mode } + is EditorUiEvent.PenChanged -> { editorState.pen = event.pen } + is EditorUiEvent.PenSettingChanged -> { val newSettings = editorState.penSettings.toMutableMap() newSettings[event.pen.penName] = event.setting editorState.penSettings = newSettings } + is EditorUiEvent.EraserChanged -> { editorState.eraser = event.eraser } + is EditorUiEvent.ToolbarVisibilityChanged -> { editorState.isToolbarOpen = event.visible } @@ -230,21 +239,26 @@ fun EditorView( viewModel.setHasClipboard(editorState.clipboard != null) viewModel.setShowResetView(zoomLevel != 1.0f) // page.scroll != Offset.Zero viewModel.setSelectionActive(selectionActive) - viewModel.updateToolbarSettings(ToolbarUiState( - isToolbarOpen = editorState.isToolbarOpen, - mode = editorState.mode, - pen = editorState.pen, - eraser = editorState.eraser, - penSettings = editorState.penSettings - )) + viewModel.updateToolbarSettings( + ToolbarUiState( + isToolbarOpen = editorState.isToolbarOpen, + mode = editorState.mode, + pen = editorState.pen, + eraser = editorState.eraser, + penSettings = editorState.penSettings + ) + ) } DisposableEffect(Unit) { onDispose { // finish selection operation editorState.selectionState.applySelectionDisplace(page) - if (bookId != null) - exportToLinkedFile(exportEngine, bookId, appRepository.bookRepository) + if (bookId != null) exportToLinkedFile( + exportEngine, + bookId, + appRepository.bookRepository + ) page.disposeOldPage() } } @@ -274,14 +288,10 @@ fun EditorView( InkaTheme { EditorGestureReceiver(controlTower = editorControlTower) EditorSurface( - appRepository = appRepository, - state = editorState, - page = page, - history = history + appRepository = appRepository, state = editorState, page = page, history = history ) SelectedBitmap( - context = context, - controlTower = editorControlTower + context = context, controlTower = editorControlTower ) Row( modifier = Modifier @@ -292,9 +302,7 @@ fun EditorView( ScrollIndicator(state = editorState) } PositionedToolbar( - viewModel = viewModel, - onDrawingStateCheck = { viewModel.updateDrawingState() } - ) + viewModel = viewModel, onDrawingStateCheck = { viewModel.updateDrawingState() }) HorizontalScrollIndicator(state = editorState) } } diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt index 5219451c..b541c793 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasObserverRegistry.kt @@ -162,7 +162,7 @@ class CanvasObserverRegistry( } } - private fun observeReloadFromDb(){ + private fun observeReloadFromDb() { coroutineScope.launch { CanvasEventBus.reloadFromDb.collect { page.refreshCurrentPage() @@ -263,7 +263,7 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.previewPage.debounce(50).collectLatest { pageId -> val pageNumber = - appRepository.getPageNumber(page.pageFromDb?.notebookId!!, pageId) + appRepository.getPageNumber(page.pageFromDb?.notebookId!!, pageId) Log.d("QuickNav", "Previewing page($pageNumber): $pageId") val previewBitmap = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index d18821c2..6c07e12a 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -70,7 +70,6 @@ class EditorState( } - private val log = ShipBook.getLogger("EditorState") var mode by mutableStateOf(persistedEditorSettings?.mode ?: Mode.Draw) // should save @@ -131,8 +130,7 @@ class EditorState( // if state is Move then applySelectionDisplace() will delete original strokes and images enum class PlacementMode { - Move, - Paste + Move, Paste } object Clipboard { diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt index 5f4e23f8..571410dc 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt @@ -32,8 +32,6 @@ import androidx.compose.ui.window.PopupProperties import com.ethran.notable.R import com.ethran.notable.data.datastore.BUTTON_SIZE import com.ethran.notable.data.datastore.GlobalAppSettings -import com.ethran.notable.data.db.KvProxy -import com.ethran.notable.data.db.KvRepository import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.ui.convertDpToPixel diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt index 05070a2c..fabfb84a 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt @@ -37,9 +37,11 @@ fun PositionedToolbar( when (position) { AppSettings.Position.Top -> toolbar() AppSettings.Position.Bottom -> { - Column(Modifier - .fillMaxWidth() - .fillMaxHeight()) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + ) { Spacer(modifier = Modifier.weight(1f)) toolbar() } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 44b58d39..f454b82d 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -111,7 +111,10 @@ fun ToolbarContent( isSelected = isSelected(uiState, Pen.BALLPEN), onSelect = { onAction(ToolbarAction.ChangePen(Pen.BALLPEN)) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.BALLPEN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + penSetting = uiState.penSettings[Pen.BALLPEN.penName] ?: PenSetting( + 5f, + android.graphics.Color.BLACK + ), onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(Pen.BALLPEN, it)) }) if (!GlobalAppSettings.current.monochromeMode) { @@ -150,7 +153,10 @@ fun ToolbarContent( isSelected = isSelected(uiState, Pen.PENCIL), onSelect = { onAction(ToolbarAction.ChangePen(Pen.PENCIL)) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.PENCIL.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + penSetting = uiState.penSettings[Pen.PENCIL.penName] ?: PenSetting( + 5f, + android.graphics.Color.BLACK + ), onChangeSetting = { onAction( ToolbarAction.ChangePenSetting( @@ -167,7 +173,10 @@ fun ToolbarContent( isSelected = isSelected(uiState, Pen.BRUSH), onSelect = { onAction(ToolbarAction.ChangePen(Pen.BRUSH)) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.BRUSH.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + penSetting = uiState.penSettings[Pen.BRUSH.penName] ?: PenSetting( + 5f, + android.graphics.Color.BLACK + ), onChangeSetting = { onAction( ToolbarAction.ChangePenSetting( @@ -184,7 +193,10 @@ fun ToolbarContent( isSelected = isSelected(uiState, Pen.FOUNTAIN), onSelect = { onAction(ToolbarAction.ChangePen(Pen.FOUNTAIN)) }, sizes = SIZES_STROKES_DEFAULT, - penSetting = uiState.penSettings[Pen.FOUNTAIN.penName] ?: PenSetting(5f, android.graphics.Color.BLACK), + penSetting = uiState.penSettings[Pen.FOUNTAIN.penName] ?: PenSetting( + 5f, + android.graphics.Color.BLACK + ), onChangeSetting = { onAction( ToolbarAction.ChangePenSetting( @@ -210,7 +222,10 @@ fun ToolbarContent( isSelected = isSelected(uiState, Pen.MARKER), onSelect = { onAction(ToolbarAction.ChangePen(Pen.MARKER)) }, sizes = SIZES_MARKER_DEFAULT, - penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting(40f, android.graphics.Color.LTGRAY), + penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting( + 40f, + android.graphics.Color.LTGRAY + ), onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(Pen.MARKER, it)) } ) diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarButton.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarButton.kt index 1a391f96..16e811e5 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarButton.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarButton.kt @@ -60,6 +60,7 @@ fun ToolbarButton( tint = if (isSelected) Color.White else Color.Black ) } + text != null -> { Text( text, fontSize = 20.sp, color = if (isSelected) Color.White else Color.Black diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt index a40435a1..0954299d 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/ToolbarMenu.kt @@ -49,7 +49,7 @@ fun ToolbarMenu( alignment = Alignment.TopEnd, onDismissRequest = { onAction(ToolbarAction.ToggleMenu) }, offset = IntOffset( - convertDpToPixel((-10).dp, context).toInt(), + convertDpToPixel((-10).dp, context).toInt(), convertDpToPixel(50.dp, context).toInt() ), properties = PopupProperties(focusable = true), @@ -136,7 +136,7 @@ private fun ToolbarMenuContent( @Composable private fun MenuItem( - label: String, + label: String, onClick: () -> Unit ) { Box( From 096848f362b9b4ccecb413dff48e64e918498ef3 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 10:17:49 +0100 Subject: [PATCH 13/21] EditorViewModel: refactor handleEraserChange and add stroke selection logic --- .../ethran/notable/editor/EditorViewModel.kt | 18 +++++++++++++----- .../editor/ui/toolbar/EraserToolbarButton.kt | 19 +++++-------------- .../notable/editor/ui/toolbar/Toolbar.kt | 3 ++- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 0ddfe5f5..2df31f95 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -46,6 +46,7 @@ sealed class ToolbarAction { data class ChangePenSetting(val pen: Pen, val setting: PenSetting) : ToolbarAction() data class ChangeEraser(val eraser: Eraser) : ToolbarAction() object ToggleMenu : ToolbarAction() + data class UpdateMenuOpenTo(val isOpen: Boolean) : ToolbarAction() data class ToggleBackgroundSelector(val isOpen: Boolean) : ToolbarAction() data class ToggleScribbleToErase(val enabled: Boolean) : ToolbarAction() @@ -158,15 +159,15 @@ class EditorViewModel @Inject constructor( } is ToolbarAction.ChangePen -> handlePenChange(action.pen) is ToolbarAction.ChangePenSetting -> handlePenSettingChange(action.pen, action.setting) - is ToolbarAction.ChangeEraser -> { - _toolbarState.update { it.copy(eraser = action.eraser) } - sendUiEvent(EditorUiEvent.EraserChanged(action.eraser)) - updateDrawingState() - } + is ToolbarAction.ChangeEraser -> handleEraserChange(action.eraser) is ToolbarAction.ToggleMenu -> { _toolbarState.update { it.copy(isMenuOpen = !it.isMenuOpen) } updateDrawingState() } + is ToolbarAction.UpdateMenuOpenTo -> { + _toolbarState.update { it.copy(isStrokeSelectionOpen = action.isOpen) } + updateDrawingState() + } is ToolbarAction.ToggleBackgroundSelector -> { _toolbarState.update { it.copy(isBackgroundSelectorModalOpen = action.isOpen) } updateDrawingState() @@ -212,6 +213,13 @@ class EditorViewModel @Inject constructor( updateDrawingState() } + private fun handleEraserChange(eraser: Eraser) { + _toolbarState.update { it.copy(eraser = eraser) } + sendUiEvent(EditorUiEvent.EraserChanged(eraser)) + updateDrawingState() + } + + private fun handleCloseAllMenus() { log.e("Closing all menus in EditorViewModel") _toolbarState.update { diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt index 571410dc..ab9b614e 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/EraserToolbarButton.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.BasicText 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 @@ -39,27 +38,19 @@ import com.ethran.notable.ui.convertDpToPixel fun EraserToolbarButton( value: Eraser, onChange: (Eraser) -> Unit, - onMenuOpenChange: ((Boolean) -> Unit)?, + isMenuOpen: Boolean, + onMenuOpenChange: (Boolean) -> Unit, isSelected: Boolean, onSelect: () -> Unit, toggleScribbleToErase: (Boolean) -> Unit ) { val context = LocalContext.current - var isMenuOpen by remember { mutableStateOf(false) } - - if (onMenuOpenChange != null) { - LaunchedEffect(isMenuOpen) { - onMenuOpenChange(isMenuOpen) - } - } - Box { - ToolbarButton( isSelected = isSelected, onSelect = { - if (isSelected) isMenuOpen = !isMenuOpen + if (isSelected) onMenuOpenChange(!isMenuOpen) else onSelect() }, iconId = if (value == Eraser.PEN) R.drawable.eraser else R.drawable.eraser_select, @@ -70,7 +61,7 @@ fun EraserToolbarButton( Popup( offset = IntOffset(0, convertDpToPixel(43.dp, context).toInt()), onDismissRequest = { - isMenuOpen = false + onMenuOpenChange(false) }, properties = PopupProperties(focusable = true), alignment = Alignment.TopCenter @@ -131,4 +122,4 @@ fun EraserToolbarButton( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index f454b82d..c726af72 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt @@ -237,7 +237,8 @@ fun ToolbarContent( value = uiState.eraser, onChange = { onAction(ToolbarAction.ChangeEraser(it)) }, toggleScribbleToErase = { onAction(ToolbarAction.ToggleScribbleToErase(it)) }, - onMenuOpenChange = {} + onMenuOpenChange = { onAction(ToolbarAction.UpdateMenuOpenTo(it)) }, + isMenuOpen = uiState.isStrokeSelectionOpen ) VerticalDivider() From 4c1d81add87af6caba8009235b4342eaea49a276 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 11:07:41 +0100 Subject: [PATCH 14/21] change from log.e to log.d --- app/src/main/java/com/ethran/notable/editor/EditorView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorView.kt b/app/src/main/java/com/ethran/notable/editor/EditorView.kt index 48cb2558..a5c28149 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -209,7 +209,7 @@ fun EditorView( // Handle Canvas signals in UI LaunchedEffect(Unit) { CanvasEventBus.closeMenusSignal.collect { - log.e("Closing all menus") + log.d("Closing all menus") viewModel.onToolbarAction(ToolbarAction.CloseAllMenus) } } @@ -217,7 +217,7 @@ fun EditorView( // Handle focus changes from Canvas LaunchedEffect(Unit) { CanvasEventBus.onFocusChange.collect { hasFocus -> - log.e("Canvas has focus: $hasFocus") + log.d("Canvas has focus: $hasFocus") viewModel.onFocusChanged(hasFocus) } } From b24670b8269f978ddc4b56cc2d62195d9a8f70d9 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 11:11:04 +0100 Subject: [PATCH 15/21] change from log.e to log.d --- app/src/main/java/com/ethran/notable/editor/PageView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/ethran/notable/editor/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index 761e2065..03b9c578 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -800,7 +800,7 @@ class PageView( // and redraws page to vew. suspend fun refreshCurrentPage() { pageFromDb = appRepository.pageRepository.getById(currentPageId) - log.i("Refresh current page, bacground: ${pageFromDb?.background}") + log.i("Refresh current page, background: ${pageFromDb?.background}") withContext(Dispatchers.Main) { drawAreaScreenCoordinates(Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) persistBitmapDebounced() From df893fb7dd0797c1ffdcc5b387d5161607a0ca63 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 11:12:45 +0100 Subject: [PATCH 16/21] add comments --- .../main/java/com/ethran/notable/editor/EditorViewModel.kt | 5 +++-- .../main/java/com/ethran/notable/editor/state/EditorState.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 2df31f95..191bf0ba 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -119,12 +119,13 @@ data class ToolbarUiState( val notebookId: String? = null, val pageId: String? = null, val isBookActive: Boolean = false, - + + // TODO: check correctness // Internal data for BackgroundSelector rendering if it remains stateless val backgroundType: String = "native", val backgroundPath: String = "blank", val backgroundPageNumber: Int = 0, - val currentPageNumber: Int = 0 + val currentPageNumber: Int = 1 ) @HiltViewModel diff --git a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt index 6c07e12a..0f271951 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/EditorState.kt @@ -20,7 +20,7 @@ enum class Mode { Draw, Erase, Select, Line } - + // TODO: move to EditorViewModel, or somewhere else, this code shouldnt be here. class EditorState( val bookId: String? = null, val pageId: String, From 9d152ac6cb244b418e9f02541d684f7fa279fcd7 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 11:42:36 +0100 Subject: [PATCH 17/21] EditorViewModel: calculate and update background page number for PDF backgrounds --- .../ethran/notable/editor/EditorViewModel.kt | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 191bf0ba..2e222d30 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -10,6 +10,7 @@ import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.getPageIndex import com.ethran.notable.data.db.getParentFolder +import com.ethran.notable.data.model.BackgroundType import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.Eraser @@ -125,7 +126,7 @@ data class ToolbarUiState( val backgroundType: String = "native", val backgroundPath: String = "blank", val backgroundPageNumber: Int = 0, - val currentPageNumber: Int = 1 + val currentPageNumber: Int = 0 ) @HiltViewModel @@ -302,10 +303,22 @@ class EditorViewModel @Inject constructor( page.copy(background = path, backgroundType = type) } appRepository.pageRepository.update(updatedPage) + + // Calculate background page number + val bgPageNum = when (val bgTypeObj = BackgroundType.fromKey(type)) { + is BackgroundType.Pdf -> bgTypeObj.page + is BackgroundType.AutoPdf -> { + currentBookId?.let { appRepository.getPageNumber(it, currentPageId) } ?: 0 + } + + else -> 0 + } + _toolbarState.update { it.copy( backgroundType = updatedPage.backgroundType, - backgroundPath = updatedPage.background + backgroundPath = updatedPage.background, + backgroundPageNumber = bgPageNum ) } _uiEvents.emit(EditorUiEvent.RefreshCanvas) @@ -341,6 +354,16 @@ class EditorViewModel @Inject constructor( val pageIndex = book?.getPageIndex(pageId) ?: 0 val totalPages = book?.pageIds?.size ?: 1 + + val backgroundTypeObj = BackgroundType.fromKey(page?.backgroundType ?: "native") + val bgPageNumber = when (backgroundTypeObj) { + is BackgroundType.Pdf -> backgroundTypeObj.page + is BackgroundType.AutoPdf -> { + bookId?.let { appRepository.getPageNumber(it, pageId) } ?: 0 + } + + else -> 0 + } _toolbarState.update { it.copy( @@ -351,6 +374,7 @@ class EditorViewModel @Inject constructor( currentPageNumber = pageIndex, backgroundType = page?.backgroundType ?: "native", backgroundPath = page?.background ?: "blank", + backgroundPageNumber = bgPageNumber ) } } From 67aec5367eec2a73036b9da0abb73423431d6dd2 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 12:11:06 +0100 Subject: [PATCH 18/21] PageView.kt updated background caching and logging; changed debug color in pageDrawing.kt --- .../main/java/com/ethran/notable/editor/PageView.kt | 13 ++++++++++--- .../ethran/notable/editor/drawing/pageDrawing.kt | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index 03b9c578..3368a42f 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -95,6 +95,7 @@ class PageView( get() = PageDataManager.getImages(currentPageId) set(value) = PageDataManager.setImages(currentPageId, value) + // warning: The setter is delayed! private var currentBackground: CachedBackground get() = PageDataManager.getBackground(currentPageId) set(value) { @@ -146,10 +147,16 @@ class PageView( If pageNumber is -1, its assumed that the background is image type. */ fun getOrLoadBackground(filePath: String, pageNumber: Int, scale: Float): Bitmap? { - if (!currentBackground.matches(filePath, pageNumber, scale)) + val cached = currentBackground + if (cached.matches(filePath, pageNumber, scale)) { + log.i("Background bitmap (cached): ${cached.bitmap}") + return cached.bitmap + } // 0.1 to avoid constant rerender on zoom. - currentBackground = CachedBackground(filePath, pageNumber, scale + 0.1f) - return currentBackground.bitmap + val newBackground = CachedBackground(filePath, pageNumber, scale + 0.1f) + currentBackground = newBackground + log.i("Background bitmap: ${newBackground.bitmap}") + return newBackground.bitmap } fun getBackgroundPageNumber(): Int { diff --git a/app/src/main/java/com/ethran/notable/editor/drawing/pageDrawing.kt b/app/src/main/java/com/ethran/notable/editor/drawing/pageDrawing.kt index 9726240a..72fa62d9 100644 --- a/app/src/main/java/com/ethran/notable/editor/drawing/pageDrawing.kt +++ b/app/src/main/java/com/ethran/notable/editor/drawing/pageDrawing.kt @@ -137,7 +137,8 @@ fun drawOnCanvasFromPage( // Canvas is scaled, it will scale page area. canvas.withClip(canvasClipBounds) { - drawColor(Color.BLACK) + // for debugging: + drawColor(Color.WHITE) drawBg(page.context, this, backgroundType, background, page.scroll, zoomLevel, page) if (GlobalAppSettings.current.debugMode) { From c1ffe2d586f656539d40eca632859ded74dcf273 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 20:17:57 +0100 Subject: [PATCH 19/21] change log level --- .../main/java/com/ethran/notable/editor/EditorViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 2e222d30..0aa89c6b 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -223,7 +223,7 @@ class EditorViewModel @Inject constructor( private fun handleCloseAllMenus() { - log.e("Closing all menus in EditorViewModel") + log.d("Closing all menus in EditorViewModel") _toolbarState.update { it.copy( isMenuOpen = false, @@ -243,7 +243,7 @@ class EditorViewModel @Inject constructor( val anyMenuOpen = state.isMenuOpen || state.isStrokeSelectionOpen || state.isBackgroundSelectorModalOpen val shouldBeDrawing = !anyMenuOpen && !_toolbarState.value.isSelectionActive - log.e("Drawing state: $shouldBeDrawing") + log.d("Drawing state: $shouldBeDrawing") viewModelScope.launch { CanvasEventBus.isDrawing.emit(shouldBeDrawing) } From d219b89c5bb2b7b2c0ba9fef24eeacad35f0fc41 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 20:20:10 +0100 Subject: [PATCH 20/21] correct typo --- app/src/main/java/com/ethran/notable/editor/PageView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/ethran/notable/editor/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index 3368a42f..dfa36343 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -804,7 +804,7 @@ class PageView( // updates page setting in db, (for instance type of background) - // and redraws page to vew. + // and redraws page to view. suspend fun refreshCurrentPage() { pageFromDb = appRepository.pageRepository.getById(currentPageId) log.i("Refresh current page, background: ${pageFromDb?.background}") From 5321daa74d67246fba7fb589554f3f44be7cf766 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Mon, 9 Mar 2026 20:26:17 +0100 Subject: [PATCH 21/21] move side-effects out of _toolbarState.update in EditorViewModel --- .../com/ethran/notable/editor/EditorViewModel.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt index 0aa89c6b..72ec1db9 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -203,15 +203,24 @@ class EditorViewModel @Inject constructor( } private fun handlePenChange(pen: Pen) { + var penChanged = false + var modeChanged = false + _toolbarState.update { state -> if (state.mode == Mode.Draw && state.pen == pen) { state.copy(isStrokeSelectionOpen = true) } else { - sendUiEvent(EditorUiEvent.PenChanged(pen)) - if (state.mode != Mode.Draw) sendUiEvent(EditorUiEvent.ModeChanged(Mode.Draw)) + penChanged = true + if (state.mode != Mode.Draw) { + modeChanged = true + } state.copy(mode = Mode.Draw, pen = pen) } } + // Fire side-effects outside the update block + if (penChanged) sendUiEvent(EditorUiEvent.PenChanged(pen)) + if (modeChanged) sendUiEvent(EditorUiEvent.ModeChanged(Mode.Draw)) + updateDrawingState() } @@ -259,9 +268,9 @@ class EditorViewModel @Inject constructor( _toolbarState.update { state -> val newSettings = state.penSettings.toMutableMap() newSettings[pen.penName] = setting - sendUiEvent(EditorUiEvent.PenSettingChanged(pen, setting)) state.copy(penSettings = newSettings) } + sendUiEvent(EditorUiEvent.PenSettingChanged(pen, setting)) } private fun updateScribbleToErase(enabled: Boolean) {