diff --git a/app/src/main/java/com/ethran/notable/MainActivity.kt b/app/src/main/java/com/ethran/notable/MainActivity.kt index f22f975f..cbbf1a47 100644 --- a/app/src/main/java/com/ethran/notable/MainActivity.kt +++ b/app/src/main/java/com/ethran/notable/MainActivity.kt @@ -167,6 +167,13 @@ class MainActivity : ComponentActivity() { CanvasEventBus.onFocusChange.emit(hasFocus) } } + override fun onResume() { + super.onResume() + enableFullScreen() + lifecycleScope.launch { + CanvasEventBus.onFocusChange.emit(true) + } + } // when the screen orientation is changed, set new screen width restart is not necessary, 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 813220e0..ffa362f9 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -185,12 +185,14 @@ fun EditorView( } // Handle focus changes from Canvas - LaunchedEffect(Unit) { - CanvasEventBus.onFocusChange.collect { hasFocus -> - log.d("Canvas has focus: $hasFocus") - viewModel.onFocusChanged(hasFocus) - } - } +// LaunchedEffect(Unit) { +// CanvasEventBus.onFocusChange.collect { hasFocus -> +// log.d("Canvas has focus: $hasFocus") +// if (hasFocus) +// viewModel.updateDrawingState() +// +// } +// } val toolbarState by viewModel.toolbarState.collectAsStateWithLifecycle() 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 322fa429..1d3256c2 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -20,6 +20,7 @@ import com.ethran.notable.editor.state.ClipboardStore import com.ethran.notable.editor.state.History import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.state.SelectionState +import com.ethran.notable.editor.utils.DeviceCompat import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting @@ -103,7 +104,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 ToggleEraserManu(val isOpen: Boolean) : ToolbarAction() data class ToggleBackgroundSelector(val isOpen: Boolean) : ToolbarAction() data class ToggleScribbleToErase(val enabled: Boolean) : ToolbarAction() @@ -250,17 +251,17 @@ class EditorViewModel @Inject constructor( is ToolbarAction.ChangeEraser -> handleEraserChange(action.eraser) is ToolbarAction.ToggleMenu -> { _toolbarState.update { it.copy(isMenuOpen = !it.isMenuOpen) } - updateDrawingState() +// updateDrawingState() // on focus change is doing this } - is ToolbarAction.UpdateMenuOpenTo -> { + is ToolbarAction.ToggleEraserManu -> { _toolbarState.update { it.copy(isStrokeSelectionOpen = action.isOpen) } - updateDrawingState() +// updateDrawingState() // on focus change is doing this } is ToolbarAction.ToggleBackgroundSelector -> { _toolbarState.update { it.copy(isBackgroundSelectorModalOpen = action.isOpen) } - updateDrawingState() +// updateDrawingState() // on focus change is doing this } is ToolbarAction.ToggleScribbleToErase -> updateScribbleToErase(action.enabled) @@ -441,21 +442,17 @@ class EditorViewModel @Inject constructor( * Re-evaluates whether drawing should be enabled based on menu and selection states. */ fun updateDrawingState() { - log.v("updateDrawingState") + // It get called three times on canvas creation. val shouldBeDrawing = _toolbarState.value.isDrawingAllowed _toolbarState.update { it.copy(isDrawing = shouldBeDrawing) } - log.d("Drawing state: $shouldBeDrawing") + log.d("updateDrawingState: Drawing state: $shouldBeDrawing") viewModelScope.launch { + if (shouldBeDrawing) + DeviceCompat.delayBeforeResumingDrawing() CanvasEventBus.isDrawing.emit(shouldBeDrawing) } } - fun onFocusChanged(isFocused: Boolean) { - if (isFocused) { - updateDrawingState() - } - } - // -------------------------------------------------------- // Book / Page Data // -------------------------------------------------------- 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 59ea86bf..16dd361e 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -270,6 +270,7 @@ class PageView( } private fun loadPage() { +// loadingJob?.cancel() logCache.i("Init from persist layer, pageId: $currentPageId") windowedCanvas.scale(zoomLevel.value, zoomLevel.value) loadingJob = coroutineScope.launch(Dispatchers.IO) { @@ -284,7 +285,7 @@ class PageView( } // TODO: If we put it in loadPage(…) sometimes it will try to refresh // without seeing strokes, I have no idea why. - coroutineScope.launch(Dispatchers.Main.immediate) { + coroutineScope.launch(Dispatchers.Main) { CanvasEventBus.forceUpdate.emit(null) } // sleep(5000) @@ -444,7 +445,7 @@ class PageView( // let's control that the last preview fits the present orientation. Otherwise we'll ask for a redraw. if (bitmapFromDisc.height == windowedCanvas.height && bitmapFromDisc.width == windowedCanvas.width) { windowedCanvas.drawBitmap(bitmapFromDisc, 0f, 0f, Paint()) - log.d("loaded initial bitmap") + log.d("loaded initial bitmap, drawing to canvas: ${windowedCanvas.hashCode()}, bitmap: ${windowedBitmap.hashCode()}") return true } else log.i("Image preview does not fit canvas area - redrawing") @@ -457,6 +458,7 @@ class PageView( drawBgToCanvas(null) } else windowedCanvas.drawColor(Color.WHITE) + log.d("loaded initial bitmap, drawing to canvas: ${windowedCanvas.hashCode()}, bitmap: ${windowedBitmap.hashCode()}") return false } @@ -666,7 +668,7 @@ class PageView( val redrawRect = Rect(0, 0, windowedBitmap.width, windowedBitmap.height) log.d("Redrawing full logical rect: $redrawRect") - windowedCanvas.drawColor(Color.BLACK) + windowedCanvas.drawColor(Color.GREEN) drawBgToCanvas(redrawRect) pageDataManager.cacheBitmap(currentPageId, windowedBitmap) 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 84154c87..95168cbf 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 @@ -35,6 +35,8 @@ class CanvasObserverRegistry( private val pageDataManager = page.pageDataManager fun registerAll() { + // NOTE: Be careful with the dispatchers, choose them wisely. + ImageHandler(drawCanvas.context, page, viewModel, coroutineScope).observeImageUri() observeRefreshUiImmediately() @@ -58,7 +60,7 @@ class CanvasObserverRegistry( } private fun observeRefreshUiImmediately() { - coroutineScope.launch { + coroutineScope.launch(Dispatchers.Main) { CanvasEventBus.refreshUiImmediately.collect { log.v("Refreshing UI!") val zoneToRedraw = Rect(0, 0, page.viewWidth, page.viewHeight) @@ -71,7 +73,7 @@ class CanvasObserverRegistry( // observe forceUpdate, takes rect in screen coordinates // given null it will redraw whole page // BE CAREFUL: partial update is not tested fairly -- might not work in some situations. - coroutineScope.launch(Dispatchers.Main.immediate) { + coroutineScope.launch(Dispatchers.Main) { CanvasEventBus.forceUpdate.collect { dirtyRectangle -> // On loading, make sure that the loaded strokes are visible to it. log.v("Force update, zone: $dirtyRectangle, Strokes to draw: ${page.strokes.size}") @@ -103,6 +105,7 @@ class CanvasObserverRegistry( if (hasFocus) { inputHandler.updatePenAndStroke() // The setting might been changed by other app. drawCanvas.drawCanvasToView(null) + viewModel.updateDrawingState() } else { CanvasEventBus.isDrawing.emit(false) } diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt index 74c2e506..780114d7 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/CanvasRefreshManager.kt @@ -66,9 +66,10 @@ class CanvasRefreshManager( resetScreenFreeze(touchHelper) } - + // private val renderScope = CoroutineScope(Dispatchers.Main) fun drawCanvasToView(dirtyRect: Rect?) { - log.v("Canvas refresh started, dirtyRect: $dirtyRect") +// renderScope.launch { + log.v("Canvas refresh started, dirtyRect: $dirtyRect, bitmap: ${page.windowedBitmap.hashCode()}, thread: ${Thread.currentThread().name}") val zoneToRedraw = dirtyRect ?: Rect(0, 0, page.viewWidth, page.viewHeight) var canvas: Canvas? = null @@ -103,6 +104,7 @@ class CanvasRefreshManager( log.w("Surface released during unlock", e) } } +// } } diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt index c7ad73d4..7933e0a4 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/DrawCanvas.kt @@ -26,6 +26,7 @@ import com.onyx.android.sdk.api.device.epd.EpdController import io.shipbook.shipbooksdk.Log import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.CoroutineScope +import java.lang.Thread.sleep val pressure = EpdController.getMaxTouchPressure() diff --git a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt index 21140d8f..120ee1c1 100644 --- a/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt +++ b/app/src/main/java/com/ethran/notable/editor/canvas/OnyxInputHandler.kt @@ -11,7 +11,6 @@ import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.state.History -import com.ethran.notable.editor.state.SelectionState import com.ethran.notable.editor.utils.DeviceCompat import com.ethran.notable.editor.utils.Eraser import com.ethran.notable.editor.utils.Pen @@ -167,6 +166,7 @@ class OnyxInputHandler( if(touchHelper == null) return log.i("Update is drawing: $toolbarState.isDrawing") if (toolbarState.isDrawing) { +// DeviceCompat.delayBeforeResumingDrawing() touchHelper!!.setRawDrawingEnabled(true) } else { // Check if drawing is completed 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 8a5ef57c..56f46c51 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 @@ -193,10 +193,13 @@ fun drawOnCanvasFromPage( drawStroke(this, stroke, -page.scroll) } } catch (e: Exception) { - val error = DomainError.DrawingError("Strokes failed: ${e.message ?: e.toString()}") + val error = DomainError.DrawingError("Strokes failed: ${e.message ?: e.toString()}") pageDrawingLog.e("PageView.kt: ${error.userMessage}", e) persistentError = persistentError?.let { it + error } ?: error } } + pageDrawingLog.d( + "drawOnCanvasFromPage, finished drawing to canvas: ${canvas.hashCode()}" + ) return persistentError?.let { AppResult.Error(it) } ?: AppResult.Success(Unit) } \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PenToolbarButton.kt b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PenToolbarButton.kt index 5cc6565e..6c947bd0 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PenToolbarButton.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/toolbar/PenToolbarButton.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.Color import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.PenSetting +import io.shipbook.shipbooksdk.Log @Composable fun PenToolbarButton( @@ -20,15 +21,16 @@ fun PenToolbarButton( sizes: List>, penSetting: PenSetting, onChangeSetting: (PenSetting) -> Unit, - onStrokeMenuOpenChange: ((Boolean) -> Unit)? = null +// onStrokeMenuOpenChange: ((Boolean) -> Unit)? = null ) { var isStrokeMenuOpen by remember { mutableStateOf(false) } - if (onStrokeMenuOpenChange != null) { - LaunchedEffect(isStrokeMenuOpen) { - onStrokeMenuOpenChange(isStrokeMenuOpen) - } - } +// if (onStrokeMenuOpenChange != null) { +// LaunchedEffect(isStrokeMenuOpen) { +// Log.d("PenToolbarButton", "isStrokeMenuOpen: $isStrokeMenuOpen") +// onStrokeMenuOpenChange(isStrokeMenuOpen) +// } +// } Box { 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 68144c44..bc6a7221 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 @@ -273,7 +273,7 @@ fun ToolbarContent( value = uiState.eraser, onChange = { onAction(ToolbarAction.ChangeEraser(it)) }, toggleScribbleToErase = { onAction(ToolbarAction.ToggleScribbleToErase(it)) }, - onMenuOpenChange = { onAction(ToolbarAction.UpdateMenuOpenTo(it)) }, + onMenuOpenChange = { onAction(ToolbarAction.ToggleEraserManu(it)) }, isMenuOpen = uiState.isStrokeSelectionOpen ) 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 8fd67021..ad064ef9 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 @@ -25,9 +25,9 @@ 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.state.Mode 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 @@ -41,13 +41,15 @@ import com.ethran.notable.ui.noRippleClickable @Composable fun ToolbarMenu( uiState: ToolbarUiState, - onAction: (ToolbarAction) -> Unit + onAction: (ToolbarAction) -> Unit, ) { val context = LocalContext.current Popup( alignment = Alignment.TopEnd, - onDismissRequest = { onAction(ToolbarAction.ToggleMenu) }, + onDismissRequest = { + onAction(ToolbarAction.ToggleMenu) + }, offset = IntOffset( convertDpToPixel((-10).dp, context).toInt(), convertDpToPixel(50.dp, context).toInt() diff --git a/app/src/main/java/com/ethran/notable/editor/utils/PreviewBitmapStore.kt b/app/src/main/java/com/ethran/notable/editor/utils/PreviewBitmapStore.kt index 5720fef8..9544fcd6 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/PreviewBitmapStore.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/PreviewBitmapStore.kt @@ -150,21 +150,30 @@ fun saveHQPagePreview( val scrollYInt = scroll!!.y.roundToInt() val fileName = buildPreviewFileName(pageID, scrollYInt) val dir = ensurePreviewsFullFolder(context) - val file = File(dir, fileName) + val finalFile = File(dir, fileName) + val tempFile = File(dir, "$fileName.tmp") val optimized = optimizeBitmapForStorage(bitmap, mode, isThumbnail = false) try { - file.outputStream().buffered().use { os -> + tempFile.outputStream().buffered().use { os -> val success = optimized.bitmap.compress(optimized.format, optimized.quality, os) if (!success) { log.e("saveHQPagePreview: Failed to compress bitmap") + tempFile.delete() return@use } + } + if (tempFile.exists() && tempFile.length() > 0) { + tempFile.renameTo(finalFile) log.d("saveHQPagePreview: cached preview saved as $fileName (scrollY=$scrollYInt)") + removeOldBitmaps(dir, fileName, pageID) + } else { + log.e("saveHQPagePreview: temp file missing or empty, aborting rename") + tempFile.delete() } - removeOldBitmaps(dir, fileName, pageID) } catch (e: Exception) { + tempFile.delete() log.e("saveHQPagePreview: Exception while saving preview: ${e.message}") logCallStack("saveHQPagePreview") } finally { @@ -301,24 +310,29 @@ private fun createPlaceholderPreview( } private fun decodeBitmapFromFile(file: File): Bitmap? { + if (!file.exists()) { + log.w("decodeBitmapFromFile: file does not exist: ${file.name}") + return null + } + if (file.length() == 0L) { + log.w("decodeBitmapFromFile: file is zero bytes, deleting: ${file.name}") + file.delete() + return null + } return try { val imgBitmap = BitmapFactory.decodeFile(file.absolutePath) if (imgBitmap != null) { log.d("decodeBitmapFromFile: loaded cached preview '${file.name}'") imgBitmap } else { - log.w("decodeBitmapFromFile: failed to decode bitmap from ${file.name}") - log.d( - $$""" - exists=$${file.exists()} - size=$${file.length()} - name=${file.name} - """.trimIndent() - ) + log.w("decodeBitmapFromFile: failed to decode bitmap from ${file.name}, deleting") + log.d("exists=${file.exists()} size=${file.length()} name=${file.name}") + file.delete() null } } catch (e: Exception) { log.e("decodeBitmapFromFile: Exception while loading bitmap: ${e.message}") + file.delete() null } } @@ -330,27 +344,37 @@ fun savePageThumbnail( context: Context, bitmap: Bitmap, pageID: String, mode: PreviewSaveMode = PreviewSaveMode.REGULAR ) { ensureNotMainThread("savePageThumbnail") - val file = getThumbnailFile(context, pageID) - file.parentFile?.mkdirs() + val finalFile = getThumbnailFile(context, pageID) + finalFile.parentFile?.mkdirs() + val tempFile = File(finalFile.parentFile, "${finalFile.name}.tmp") val ratio = bitmap.height.toFloat() / bitmap.width.toFloat() val scaledBitmap = bitmap.scale(THUMBNAIL_WIDTH, (THUMBNAIL_WIDTH * ratio).toInt(), false) val optimized = optimizeBitmapForStorage(scaledBitmap, mode, isThumbnail = true) try { - file.outputStream().buffered().use { os -> - optimized.bitmap.compress(optimized.format, optimized.quality, os) + tempFile.outputStream().buffered().use { os -> + val success = optimized.bitmap.compress(optimized.format, optimized.quality, os) + if (!success) { + log.e("savePageThumbnail: Failed to compress bitmap") + tempFile.delete() + return + } + } + if (tempFile.exists() && tempFile.length() > 0) { + tempFile.renameTo(finalFile) + log.d("savePageThumbnail: thumbnail saved for $pageID") + } else { + log.e("savePageThumbnail: temp file missing or empty, aborting rename") + tempFile.delete() } } catch (e: Exception) { + tempFile.delete() log.e("savePageThumbnail: Exception while saving thumbnail: ${e.message}") logCallStack("savePageThumbnail") - } - - if (optimized.bitmap != scaledBitmap) { - optimized.bitmap.recycle() - } - if (scaledBitmap != bitmap) { - scaledBitmap.recycle() + } finally { + if (optimized.bitmap != scaledBitmap) optimized.bitmap.recycle() + if (scaledBitmap != bitmap) scaledBitmap.recycle() } } diff --git a/app/src/main/java/com/ethran/notable/editor/utils/einkHelper.kt b/app/src/main/java/com/ethran/notable/editor/utils/einkHelper.kt index 43158f61..3e51ec59 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/einkHelper.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/einkHelper.kt @@ -29,6 +29,8 @@ import com.onyx.android.sdk.device.Device import com.onyx.android.sdk.pen.TouchHelper import com.onyx.android.sdk.utils.DeviceInfoUtil import io.shipbook.shipbooksdk.ShipBook +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -319,6 +321,7 @@ fun restoreDefaults(view: View) { fun partialRefreshRegionOnce(view: View, dirtyRect: Rect, touchHelper: TouchHelper?) { if(touchHelper == null) return +// touchHelper.isRawDrawingRenderEnabled = false refreshScreenRegion(view, dirtyRect) resetScreenFreeze(touchHelper) // we need to wait before refreshing, as onyx library has its own buffer that needs to be updated. Otherwise we will refresh to correct, then incorrect and then correct state. @@ -332,8 +335,11 @@ fun resetScreenFreeze(touchHelper: TouchHelper?, view: View? = null) { log.w("touchHelper is null") return } - touchHelper.isRawDrawingRenderEnabled = false - touchHelper.isRawDrawingRenderEnabled = true + CoroutineScope(Dispatchers.Default).launch { + touchHelper.isRawDrawingRenderEnabled = false + DeviceCompat.delayBeforeResumingDrawing() + touchHelper.isRawDrawingRenderEnabled = true + } // setRawDrawingEnabled(false) // setRawDrawingEnabled(true) } @@ -454,7 +460,8 @@ object DeviceCompat { if (!isOnyxDevice) return // 500ms for Kaleido Color e-ink, 300ms for monochrome val delayMs = if (isColorDevice()) 500L else 300L - log.d("Delaying raw drawing resume for ${delayMs}ms to allow Android UI to settle") + log.d("delayBeforeResumingDrawing: Delaying raw drawing resume for ${delayMs}ms to allow Android UI to settle") delay(delayMs) + log.d("delayBeforeResumingDrawing: Resuming raw drawing") } } \ No newline at end of file diff --git a/app/src/main/java/com/ethran/notable/ui/SnackBar.kt b/app/src/main/java/com/ethran/notable/ui/SnackBar.kt index c67302f2..16a174da 100644 --- a/app/src/main/java/com/ethran/notable/ui/SnackBar.kt +++ b/app/src/main/java/com/ethran/notable/ui/SnackBar.kt @@ -81,7 +81,7 @@ class SnackState { task: suspend () -> T, ): T { val dismissSnack = - displaySnack(SnackConf(text = text, duration = null)) // Ensure it doesn't timeout + displaySnack(SnackConf(text = text, duration = null)) return try { task() @@ -183,6 +183,16 @@ private fun SnackItem(snack: SnackConf, onDismiss: (String) -> Unit) { .padding(horizontal = 12.dp, vertical = 6.dp)) } } + + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "x", + color = Color.White, + fontWeight = FontWeight.Bold, + modifier = Modifier + .noRippleClickable { onDismiss(snack.id) } + .padding(1.dp) + ) } } else { snack.content?.invoke()