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 4b09bb0d..a5c28149 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 @@ -9,6 +8,7 @@ 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 @@ -16,18 +16,18 @@ 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.AppSettings import com.ethran.notable.data.datastore.EditorSettingCacheManager -import com.ethran.notable.data.datastore.GlobalAppSettings +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 @@ -37,7 +37,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 @@ -71,7 +73,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 @@ -79,6 +82,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 } @@ -92,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) { @@ -104,7 +107,6 @@ fun EditorView( } } - if (pageExists == null) return BoxWithConstraints { @@ -124,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) @@ -143,13 +144,121 @@ 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) + 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 + } + } + } + } + + // Handle Canvas signals in UI + LaunchedEffect(Unit) { + CanvasEventBus.closeMenusSignal.collect { + log.d("Closing all menus") + viewModel.onToolbarAction(ToolbarAction.CloseAllMenus) + } + } + + // Handle focus changes from Canvas + LaunchedEffect(Unit) { + CanvasEventBus.onFocusChange.collect { hasFocus -> + log.d("Canvas has focus: $hasFocus") + viewModel.onFocusChanged(hasFocus) + } + } + + // Sync legacy state to ViewModel for Toolbar rendering + val zoomLevel by page.zoomLevel.collectAsState() + 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(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 + ) + ) + } 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() } } @@ -160,7 +269,6 @@ fun EditorView( editorState.pen, editorState.penSettings, editorState.mode, - editorState.isToolbarOpen, editorState.eraser ) { log.i("EditorView: saving") @@ -180,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 @@ -197,40 +301,9 @@ fun EditorView( Spacer(modifier = Modifier.weight(1f)) ScrollIndicator(state = editorState) } - PositionedToolbar(exportEngine,navController, appRepository, editorState, editorControlTower) + PositionedToolbar( + viewModel = viewModel, onDrawingStateCheck = { viewModel.updateDrawingState() }) HorizontalScrollIndicator(state = editorState) } } } - - -@Composable -fun PositionedToolbar( - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, - editorState: EditorState, - editorControlTower: EditorControlTower -) { - val position = GlobalAppSettings.current.toolbarPosition - - when (position) { - AppSettings.Position.Top -> { - Toolbar( - exportEngine, - navController, appRepository, editorState, editorControlTower - ) - } - - AppSettings.Position.Bottom -> { - Column( - Modifier - .fillMaxWidth() - .fillMaxHeight() - ) { - Spacer(modifier = Modifier.weight(1f)) - Toolbar(exportEngine, navController, appRepository, editorState, editorControlTower) - } - } - } -} \ No newline at end of file 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..72ec1db9 --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -0,0 +1,418 @@ +package com.ethran.notable.editor + +import android.content.Context +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +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.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 +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 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 +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 javax.inject.Inject + + +private val log = ShipBook.getLogger("EditorViewModel") + + +/** + * 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 UpdateMenuOpenTo(val isOpen: Boolean) : 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() + + object CloseAllMenus : 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() + + // 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 rendering. + */ +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, + val isSelectionActive: Boolean = false, + + // Context needed for visibility rules in UI + 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 +) + +@HiltViewModel +class EditorViewModel @Inject constructor( + @param:ApplicationContext private val context: Context, + private val appRepository: AppRepository, + private val exportEngine: ExportEngine +) : ViewModel() { + + 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)) + updateDrawingState() + } + is ToolbarAction.ChangeMode -> { + _toolbarState.update { it.copy(mode = action.mode) } + sendUiEvent(EditorUiEvent.ModeChanged(action.mode)) + updateDrawingState() + } + is ToolbarAction.ChangePen -> handlePenChange(action.pen) + is ToolbarAction.ChangePenSetting -> handlePenSettingChange(action.pen, action.setting) + 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() + } + + 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)) + + ToolbarAction.CloseAllMenus -> handleCloseAllMenus() + } + } + + private fun sendUiEvent(event: EditorUiEvent) { + viewModelScope.launch { _uiEvents.emit(event) } + } + + 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 { + 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() + } + + private fun handleEraserChange(eraser: Eraser) { + _toolbarState.update { it.copy(eraser = eraser) } + sendUiEvent(EditorUiEvent.EraserChanged(eraser)) + updateDrawingState() + } + + + private fun handleCloseAllMenus() { + log.d("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.d("Drawing state: $shouldBeDrawing") + viewModelScope.launch { + CanvasEventBus.isDrawing.emit(shouldBeDrawing) + } + } + + fun onFocusChanged(isFocused: Boolean) { + if (isFocused) { + updateDrawingState() + } + } + + private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { + _toolbarState.update { state -> + val newSettings = state.penSettings.toMutableMap() + newSettings[pen.penName] = setting + state.copy(penSettings = newSettings) + } + sendUiEvent(EditorUiEvent.PenSettingChanged(pen, setting)) + } + + 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) + + // 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, + backgroundPageNumber = bgPageNum + ) + } + _uiEvents.emit(EditorUiEvent.RefreshCanvas) + } + } + + private fun handleNavigateToLibrary() { + viewModelScope.launch(Dispatchers.IO) { + 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 + + 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 + + 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( + 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", + backgroundPageNumber = bgPageNumber + ) + } + } + } + + fun setHasClipboard(hasClipboard: Boolean) { + _toolbarState.update { it.copy(hasClipboard = hasClipboard) } + } + + fun setShowResetView(showResetView: Boolean) { + _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( + 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/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index 47fe8d0f..dfa36343 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 { @@ -797,17 +804,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 view. + suspend fun refreshCurrentPage() { + pageFromDb = appRepository.pageRepository.getById(currentPageId) + log.i("Refresh current page, background: ${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..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 @@ -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() @@ -42,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 b73e08e1..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 @@ -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() @@ -105,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 { @@ -157,7 +154,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 +162,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 { @@ -195,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() @@ -259,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/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) { 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..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,22 +20,7 @@ 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 -} - - + // TODO: move to EditorViewModel, or somewhere else, this code shouldnt be here. class EditorState( val bookId: String? = null, val pageId: String, @@ -85,7 +70,6 @@ class EditorState( } - private val log = ShipBook.getLogger("EditorState") var mode by mutableStateOf(persistedEditorSettings?.mode ?: Mode.Draw) // should save @@ -126,22 +110,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,15 +124,13 @@ class EditorState( suspend fun changePage(id: String) { log.d("Changing page to $id, from $currentPageId") updateOpenedPage(id) - closeAllMenus() selectionState.reset() } } // 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/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 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..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 @@ -32,8 +31,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 @@ -41,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, @@ -72,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 @@ -133,4 +122,4 @@ fun EraserToolbarButton( } } } -} \ 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..fabfb84a --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PositionedToolbar.kt @@ -0,0 +1,50 @@ +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.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 + +/** + * 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( + viewModel: EditorViewModel, + onDrawingStateCheck: () -> Unit +) { + val position = GlobalAppSettings.current.toolbarPosition + val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() + + val toolbar = @Composable { + ToolbarContent( + uiState = toolbarState, + onAction = viewModel::onToolbarAction, + onDrawingStateCheck = onDrawingStateCheck + ) + } + + 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/Toolbar.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/Toolbar.kt index 418911fd..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 @@ -1,6 +1,5 @@ package com.ethran.notable.editor.ui.toolbar - import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia @@ -17,187 +16,66 @@ 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.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.tooling.preview.Preview 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.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.ExportEngine 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") -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 - } +private fun isSelected(state: ToolbarUiState, penType: Pen): Boolean { + return (state.mode == Mode.Draw || state.mode == Mode.Line) && state.pen == penType } -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 - } -} - - 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( - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, - state: EditorState, - controlTower: EditorControlTower +fun ToolbarContent( + uiState: ToolbarUiState, + onAction: (ToolbarAction) -> Unit, + onDrawingStateCheck: () -> Unit, ) { - 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 -> - if (uri == null) { - 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) - } - } - - } - - // 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() - } - 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 - } - - fun handleLine() { - state.mode = Mode.Line + // Activity result launcher for picking images + val pickMedia = rememberLauncherForActivityResult(contract = PickVisualMedia()) { uri -> + uri?.let { onAction(ToolbarAction.ImagePicked(it)) } } - fun onChangeStrokeSetting(penName: String, setting: PenSetting) { - val settings = state.penSettings.toMutableMap() - settings[penName] = setting.copy() - state.penSettings = settings + // On exit or change of toolbar states, check if we should allow raw drawing + LaunchedEffect(uiState.isBackgroundSelectorModalOpen, uiState.isMenuOpen) { + onDrawingStateCheck() } - 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 = { type, path -> onAction(ToolbarAction.BackgroundChanged(type, path)) }, + onClose = { onAction(ToolbarAction.ToggleBackgroundSelector(false)) } + ) } - if (state.isToolbarOpen) { + + if (uiState.isToolbarOpen) { Column( modifier = Modifier .fillMaxWidth() @@ -219,325 +97,241 @@ fun Toolbar( .fillMaxWidth() ) { ToolbarButton( - onSelect = { - state.isToolbarOpen = !state.isToolbarOpen - }, 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( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.BALLPEN, icon = R.drawable.ballpen, - isSelected = isSelected(state, Pen.BALLPEN), - onSelect = { handleChangePen(Pen.BALLPEN) }, + isSelected = isSelected(uiState, Pen.BALLPEN), + onSelect = { onAction(ToolbarAction.ChangePen(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 = { onAction(ToolbarAction.ChangePenSetting(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) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.REDBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.REDBALLPEN.penName, it) }, - ) - - PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, - pen = Pen.BLUEBALLPEN, - icon = R.drawable.ballpenblue, - isSelected = isSelected(state, Pen.BLUEBALLPEN), - onSelect = { handleChangePen(Pen.BLUEBALLPEN) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.BLUEBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.BLUEBALLPEN.penName, 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) }, - sizes = SIZES_STROKES_DEFAULT, - penSetting = state.penSettings[Pen.GREENBALLPEN.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.GREENBALLPEN.penName, 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( - 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 = isSelected(uiState, Pen.PENCIL), + onSelect = { onAction(ToolbarAction.ChangePen(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 = { + onAction( + ToolbarAction.ChangePenSetting( + 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 = isSelected(uiState, Pen.BRUSH), + onSelect = { onAction(ToolbarAction.ChangePen(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 = { + onAction( + ToolbarAction.ChangePenSetting( + 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 = isSelected(uiState, Pen.FOUNTAIN), + onSelect = { onAction(ToolbarAction.ChangePen(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 = { + onAction( + ToolbarAction.ChangePenSetting( + Pen.FOUNTAIN, + it + ) + ) + }, ) LineToolbarButton( - unSelect = { state.mode = Mode.Draw }, + unSelect = { onAction(ToolbarAction.ChangeMode(Mode.Draw)) }, icon = R.drawable.line, - isSelected = state.mode == Mode.Line, - onSelect = { handleLine() }, + isSelected = uiState.mode == Mode.Line, + onSelect = { onAction(ToolbarAction.ChangeMode(Mode.Line)) }, ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) - ) + VerticalDivider() PenToolbarButton( - onStrokeMenuOpenChange = { state.isDrawing = !it }, pen = Pen.MARKER, icon = R.drawable.marker, - isSelected = isSelected(state, Pen.MARKER), - onSelect = { handleChangePen(Pen.MARKER) }, + isSelected = isSelected(uiState, Pen.MARKER), + onSelect = { onAction(ToolbarAction.ChangePen(Pen.MARKER)) }, sizes = SIZES_MARKER_DEFAULT, - penSetting = state.penSettings[Pen.MARKER.penName] ?: return, - onChangeSetting = { onChangeStrokeSetting(Pen.MARKER.penName, it) }) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + penSetting = uiState.penSettings[Pen.MARKER.penName] ?: PenSetting( + 40f, + android.graphics.Color.LTGRAY + ), + onChangeSetting = { onAction(ToolbarAction.ChangePenSetting(Pen.MARKER, it)) } ) + + VerticalDivider() + EraserToolbarButton( - isSelected = state.mode == Mode.Erase, - onSelect = { - handleEraser() - }, - 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) - ) - } - } - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + isSelected = uiState.mode == Mode.Erase, + onSelect = { onAction(ToolbarAction.ChangeMode(Mode.Erase)) }, + value = uiState.eraser, + onChange = { onAction(ToolbarAction.ChangeEraser(it)) }, + toggleScribbleToErase = { onAction(ToolbarAction.ToggleScribbleToErase(it)) }, + onMenuOpenChange = { onAction(ToolbarAction.UpdateMenuOpenTo(it)) }, + isMenuOpen = uiState.isStrokeSelectionOpen ) + + VerticalDivider() + ToolbarButton( - isSelected = state.mode == Mode.Select, - onSelect = { handleSelection() }, + isSelected = uiState.mode == 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 = "library", - 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) + contentDescription = "Image picker", + onSelect = { pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly)) } ) - if (state.clipboard != null) { + VerticalDivider() + + if (uiState.hasClipboard) { ToolbarButton( vectorIcon = FeatherIcons.Clipboard, contentDescription = "paste", - onSelect = { - controlTower.pasteFromClipboard() - } - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.Paste) } ) + VerticalDivider() } - // 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() } - ) - 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 = { - scope.launch { - controlTower.undo() - } - }, + onSelect = { onAction(ToolbarAction.Undo) }, iconId = R.drawable.undo, contentDescription = "undo" ) - ToolbarButton( - onSelect = { - scope.launch { - controlTower.redo() - } - }, + onSelect = { onAction(ToolbarAction.Redo) }, iconId = R.drawable.redo, contentDescription = "redo" ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .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() ?: "?" + 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 = "${pageNumber}/${totalPageNumber}", + text = uiState.pageNumberInfo, fontWeight = FontWeight.Light, - modifier = Modifier.noRippleClickable { - navController.navigate( - PagesDestination.createRoute(state.bookId) - ) - }, + 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, // Replace with your library icon resource + iconId = R.drawable.home, contentDescription = "library", - onSelect = { - navController.navigate("library") // Navigate to main library - } - ) - Box( - Modifier - .fillMaxHeight() - .width(0.5.dp) - .background(Color.Black) + onSelect = { onAction(ToolbarAction.NavigateToHome) } ) + + VerticalDivider() + Column { ToolbarButton( - onSelect = { - state.menuStates.isMenuOpen = !state.menuStates.isMenuOpen - }, iconId = R.drawable.menu, contentDescription = "menu" + onSelect = { onAction(ToolbarAction.ToggleMenu) }, + iconId = R.drawable.menu, + contentDescription = "menu" ) - if (state.menuStates.isMenuOpen) + if (uiState.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) - ) - } - }, - currentPageId = state.currentPageId, - currentBookId = state.bookId, - onClose = { state.menuStates.isMenuOpen = false }, - onBackgroundSelectorModalOpen = { - log.i("Opening page settings modal") - state.menuStates.isBackgroundSelectorModalOpen = true - } + uiState = uiState, + onAction = onAction ) + } } } @@ -551,9 +345,9 @@ 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 { + 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 ) @@ -564,4 +358,58 @@ fun Toolbar( .padding(bottom = 1.dp) ) } +} + +@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 -> { + 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, + onAction = {}, + onDrawingStateCheck = {} + ) } \ No newline at end of file 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 b513e039..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 @@ -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 @@ -25,217 +24,126 @@ 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.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.io.ExportTarget -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( - exportEngine: ExportEngine, - goToBugReport: () -> Unit, - goToLibrary: () -> Unit, - currentPageId: String, - currentBookId: String?, - 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( - exportEngine = exportEngine, - goToBugReport = goToBugReport, - goToLibrary = goToLibrary, - currentPageId = currentPageId, - currentBookId = currentBookId, - onClose = onClose, - onBackgroundSelectorModalOpen = onBackgroundSelectorModalOpen + uiState = uiState, + onAction = onAction ) } } @Composable private fun ToolbarMenuContent( - exportEngine: ExportEngine, - goToBugReport: () -> Unit, - goToLibrary: () -> Unit, - currentPageId: String, - currentBookId: String?, - onClose: () -> Unit, - onBackgroundSelectorModalOpen: () -> Unit + uiState: ToolbarUiState, + onAction: (ToolbarAction) -> 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") - 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) // 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 + // 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) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = 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) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = 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) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = 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) { - exportEngine.export( - target = ExportTarget.Page(pageId = currentPageId), - format = ExportFormat.XOPP - ) - } - } - onClose() + onAction(ToolbarAction.ExportPage(ExportFormat.XOPP)) + onAction(ToolbarAction.ToggleMenu) } DividerCentered() // Book exports - if (currentBookId != null) { + if (uiState.notebookId != null) { 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 - ) - } - } - onClose() + onAction(ToolbarAction.ExportBook(ExportFormat.PDF)) + onAction(ToolbarAction.ToggleMenu) } 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 - ) - } - } - onClose() + onAction(ToolbarAction.ExportBook(ExportFormat.PNG)) + onAction(ToolbarAction.ToggleMenu) } 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 - ) - } - } - 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)) { -// navController.navigate("bugReport") - goToBugReport() - onClose() + onAction(ToolbarAction.NavigateToBugReport) + onAction(ToolbarAction.ToggleMenu) } } } @Composable private fun MenuItem( - label: String, onClick: () -> Unit + label: String, + onClick: () -> Unit ) { 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, @@ -255,11 +163,17 @@ private fun ColumnScope.DividerCentered() { ) } - -// TODO: Fix it -@Preview(showBackground = true) @Composable -private fun ToolbarMenuPreview() { - +@Preview(showBackground = true) +fun ToolbarMenuPreview() { + ToolbarMenuContent( + 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 = {} + ) } - 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