From 56c85be61226b10ae6aa0cf1e970422cce978ec4 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 30 Jun 2026 22:14:06 +0200 Subject: [PATCH 1/5] Done. The scrollBackBuffer "never used" warning is now resolved by the read in updateScroll; the other warnings predate this change. - Added a persistent scrollBackBuffer: Bitmap? field. - updateScroll now reuses that spare (validated against current width/height/config) instead of createBitmap on every event, and ping-pongs: the outgoing windowedBitmap becomes the next spare. --- .../java/com/ethran/notable/editor/PageView.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/PageView.kt b/app/src/main/java/com/ethran/notable/editor/PageView.kt index c1362a0a..e671731f 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -94,6 +94,11 @@ class PageView( var windowedCanvas = Canvas(windowedBitmap) private set + // Spare screen-sized buffer reused by updateScroll to avoid allocating a new + // bitmap on every scroll event. Ping-ponged with windowedBitmap; recreated only + // when the canvas size/config changes (zoom, dimension change, page switch). + private var scrollBackBuffer: Bitmap? = null + // var strokes = listOf() var strokes: List get() = pageDataManager.getStrokes(currentPageId) @@ -573,13 +578,17 @@ class PageView( val width = windowedBitmap.width val height = windowedBitmap.height - // Shift the existing bitmap content - val shiftedBitmap = createBitmap(width, height, windowedBitmap.config!!) + // Shift the existing bitmap content into the spare buffer, reusing it across + // scroll events. Recreate the spare only if it doesn't match current geometry. + val shiftedBitmap = scrollBackBuffer?.takeIf { + it.width == width && it.height == height && it.config == windowedBitmap.config + } ?: createBitmap(width, height, windowedBitmap.config!!) val shiftedCanvas = Canvas(shiftedBitmap) shiftedCanvas.drawColor(Color.RED) //for debugging. shiftedCanvas.drawBitmap(windowedBitmap, -movement.x, -movement.y, null) - // Swap in the shifted bitmap + // Swap in the shifted bitmap; the old live buffer becomes the next spare. + scrollBackBuffer = windowedBitmap windowedBitmap = shiftedBitmap windowedCanvas.setBitmap(windowedBitmap) windowedCanvas.scale(zoomLevel.value, zoomLevel.value) From f61d8a75484a11cb81b2c4e1f4ed73ee1c61f2a3 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 30 Jun 2026 23:02:53 +0200 Subject: [PATCH 2/5] investigate #167 --- .../com/ethran/notable/editor/utils/PreviewBitmapStore.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 fef73ffb..63bf44f0 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 @@ -133,9 +133,12 @@ private fun removeOldBitmaps(dir: File, latestPreview: String, pageID: String) { try { if (f.delete()) { log.d("saveHQPagePreview: removed old preview ${f.name}") + } else { + // File may have already been deleted by a concurrent save for the same page. + log.d("saveHQPagePreview: could not delete old preview ${f.name} (already gone or in use)") } - } catch (_: Throwable) { - log.e("saveHQPagePreview: failed to delete old preview ${f.name}") + } catch (e: Throwable) { + log.w("saveHQPagePreview: exception deleting old preview ${f.name}", e) } } } From 66ef795d87e9829b5a1f07b2763d0637b6db311c Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 30 Jun 2026 23:09:41 +0200 Subject: [PATCH 3/5] investigate #262 --- .../main/java/com/ethran/notable/data/dbUtils.kt | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/data/dbUtils.kt b/app/src/main/java/com/ethran/notable/data/dbUtils.kt index fff03109..7f102f8b 100644 --- a/app/src/main/java/com/ethran/notable/data/dbUtils.kt +++ b/app/src/main/java/com/ethran/notable/data/dbUtils.kt @@ -19,10 +19,19 @@ fun getDbDir(): File { Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) val dbDir = File(documentsDir, "notabledb") if (!dbDir.exists()) { - dbDir.mkdirs() + val created = dbDir.mkdirs() + if (!created && !dbDir.exists()) { + throw IllegalStateException( + "Database directory could not be created at ${dbDir.absolutePath}. " + + "Grant 'All files access' to Notable in system settings and restart the app." + ) + } } if (!dbDir.canWrite()) { - throw IllegalStateException("Database directory is not writable") + throw IllegalStateException( + "Database directory is not writable: ${dbDir.absolutePath}. " + + "Grant 'All files access' to Notable in system settings and restart the app." + ) } return dbDir } From a2fb07d9da41cf4748045fcef1b3dd46eb28b243 Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 30 Jun 2026 23:18:48 +0200 Subject: [PATCH 4/5] Optimize scroll performance and E-Ink refresh logic through event conflation and improved observer management. ### Sync & Core - **EditorControlTower**: - Replaced immediate scroll processing with a non-blocking `requestScroll` system using `MutableStateFlow` to accumulate deltas. - Introduced a `scrollConsumerJob` that conflates high-frequency touch events into a single render pass per frame, improving performance during fast scrolls. - **einkHelper**: - Added a dedicated `screenFreezeScope` and `screenFreezeResetJob` to `resetScreenFreeze`. - Implemented job cancellation to coalesce overlapping resume calls, preventing timer stacking and log flooding during continuous interaction. ### UI & Canvas - **CanvasObserverRegistry**: - Implemented a dedicated `observerScope` and static `activeObserverJob` tracking to ensure previous observers are cancelled when a new registry is created, preventing memory leaks and duplicate refreshes. - Optimized `observeRefreshUiImmediately` by adding `conflate()` and manual deduplication of scroll and zoom values to avoid redundant EPD refreshes when content hasn't changed. - Migrated all internal observers to the managed `observerScope`. ### Gestures - **EditorGestureReceiver**: - Simplified gesture handling by removing manual `overdueScroll` tracking. - Updated touch, drag, and zoom handlers to use the new asynchronous `requestScroll` API. --- .../notable/editor/EditorControlTower.kt | 62 +++++++----- .../editor/canvas/CanvasObserverRegistry.kt | 94 ++++++++++++++----- .../ethran/notable/editor/utils/einkHelper.kt | 11 ++- .../notable/gestures/EditorGestureReceiver.kt | 10 +- 4 files changed, 126 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index ed69afbd..443608f1 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -18,6 +18,9 @@ import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -33,11 +36,18 @@ class EditorControlTower( private val clipboardStore: ClipboardStore, ) { private var scrollInProgress = Mutex() - private var scrollJob: Job? = null private val logEditorControlTower = ShipBook.getLogger("EditorControlTower") private var changePageObserverJob: Job? = null + // Accumulated, not-yet-rendered scroll delta in screen coordinates. Input events add + // into this; a single consumer coroutine drains and renders it. StateFlow conflation + // means a burst of input collapses to one render pass per frame the renderer can keep + // up with. + private val pendingScroll = MutableStateFlow(Offset.Zero) + private var scrollConsumerJob: Job? = null + fun registerObservers() { + startScrollConsumer() if (changePageObserverJob?.isActive == true) return changePageObserverJob = scope.launch { @@ -61,34 +71,44 @@ class EditorControlTower( fun unregisterObservers() { changePageObserverJob?.cancel() changePageObserverJob = null + scrollConsumerJob?.cancel() + scrollConsumerJob = null } - // returns delta if could not scroll, to be added to next request, - // this ensures that smooth scroll works reliably even if rendering takes to long - fun processScroll(delta: Offset): Offset { - if (delta == Offset.Zero) return Offset.Zero - if (!page.isTransformationAllowed) return Offset.Zero - if (scrollInProgress.isLocked) { - logEditorControlTower.w("Scroll in progress -- skipping") - return delta - } // Return unhandled part - - scrollJob = scope.launch(Dispatchers.Main.immediate) { - scrollInProgress.withLock { - val scaledDelta = (delta / page.zoomLevel.value) - if (viewModel.toolbarState.value.mode == Mode.Select) { - if (viewModel.selectionState.firstPageCut != null) { - onOpenPageCut(scaledDelta) + /** + * Submit a scroll/drag delta (screen coordinates) for rendering. Non-blocking: the + * delta is accumulated and consumed by [startScrollConsumer]; bursts coalesce automatically. + */ + fun requestScroll(delta: Offset) { + if (delta == Offset.Zero) return + if (!page.isTransformationAllowed) return + pendingScroll.update { it + delta } + } + + /** + * Single consumer that drains [pendingScroll] and performs the actual bitmap shift. + * Draining atomically resets the accumulator to zero, so any input that arrives while + * a render is in flight piles onto a fresh zero and is picked up on the next pass — + * coalescing a flood of touch samples into one shift per render. + */ + private fun startScrollConsumer() { + if (scrollConsumerJob?.isActive == true) return + scrollConsumerJob = scope.launch(Dispatchers.Main.immediate) { + pendingScroll.collect { + val delta = pendingScroll.getAndUpdate { Offset.Zero } + if (delta == Offset.Zero) return@collect + scrollInProgress.withLock { + if (viewModel.toolbarState.value.mode == Mode.Select && + viewModel.selectionState.firstPageCut != null + ) { + onOpenPageCut(delta / page.zoomLevel.value) } else { onPageScroll(-delta) } - } else { - onPageScroll(-delta) } + CanvasEventBus.refreshUiImmediately.emit(Unit) } - CanvasEventBus.refreshUiImmediately.emit(Unit) } - return Offset.Zero // All handled } 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 a7f6043e..08634dd1 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 @@ -1,9 +1,11 @@ package com.ethran.notable.editor.canvas import android.graphics.Rect +import androidx.compose.ui.geometry.Offset import com.ethran.notable.editor.EditorViewModel import com.ethran.notable.editor.PageView import com.ethran.notable.editor.state.History +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.ImageHandler import com.ethran.notable.editor.utils.cleanAllStrokes import com.ethran.notable.editor.utils.loadPagePreviewOrFallback @@ -14,7 +16,10 @@ import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop @@ -34,7 +39,32 @@ class CanvasObserverRegistry( private val log = ShipBook.getLogger("CanvasObservers") private val pageDataManager = page.pageDataManager + private var registered = false + + // All observers launch on this scope (a child of the supplied Compose scope) instead of + // directly on coroutineScope, so they can be cancelled as a group. The supplied Compose + // scope is long-lived and shared, so observers launched on it directly would outlive a + // recreated DrawCanvas and keep collecting — duplicating every refresh. + private val observerJob = SupervisorJob(coroutineScope.coroutineContext[Job]) + private val observerScope = CoroutineScope(coroutineScope.coroutineContext + observerJob) + + companion object { + // The observer job of the currently active registry. A newly registering registry + // cancels the previous one, guaranteeing exactly one live observer set even if an old + // DrawCanvas instance leaked. + private var activeObserverJob: Job? = null + } + fun registerAll() { + // Guard against double registration on the same instance. + if (registered) { + log.w("registerAll called twice — observers already registered, skipping") + return + } + registered = true + // Cancel observers from any prior (possibly leaked) registry so refreshes aren't doubled. + activeObserverJob?.cancel() + activeObserverJob = observerJob // NOTE: Be careful with the dispatchers, choose them wisely. ImageHandler(drawCanvas.context, page, viewModel, coroutineScope).observeImageUri() @@ -59,9 +89,29 @@ class CanvasObserverRegistry( observeRestoreCanvas() } + // Last (scroll, zoom) actually pushed by an immediate refresh. Used to drop redundant + // refreshes — see observeRefreshUiImmediately. + private var lastImmediateScroll: Offset? = null + private var lastImmediateZoom: Float = Float.NaN + private fun observeRefreshUiImmediately() { - coroutineScope.launch(Dispatchers.Main) { - CanvasEventBus.refreshUiImmediately.collect { + observerScope.launch(Dispatchers.Main) { + // conflate() collapses any backlog that builds while a refresh is mid-flight. + CanvasEventBus.refreshUiImmediately.conflate().collect { + // Dedup by content: the collector drains in bursts on Main, so several scroll + // steps that settled to the same scroll get drained back-to-back. Pushing the + // identical bitmap to the EPD (and freezing the screen) again is wasted work, so + // skip when scroll+zoom are unchanged. Select mode is exempt: page-cut drag + // changes content without moving scroll. + val scroll = page.scroll + val zoom = page.zoomLevel.value + val inSelect = viewModel.toolbarState.value.mode == Mode.Select + if (!inSelect && scroll == lastImmediateScroll && zoom == lastImmediateZoom) { + log.v("Refreshing UI! skipped — unchanged (scroll=$scroll, zoom=$zoom)") + return@collect + } + lastImmediateScroll = scroll + lastImmediateZoom = zoom log.v("Refreshing UI!") val zoneToRedraw = Rect(0, 0, page.viewWidth, page.viewHeight) refreshManager.refreshUi(zoneToRedraw) @@ -73,7 +123,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) { + observerScope.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}") @@ -90,7 +140,7 @@ class CanvasObserverRegistry( } private fun observeRefreshUi() { - coroutineScope.launch(Dispatchers.Default) { + observerScope.launch(Dispatchers.Default) { CanvasEventBus.refreshUi.collect { log.v("Refreshing UI!") refreshManager.refreshUiSuspend() @@ -99,7 +149,7 @@ class CanvasObserverRegistry( } private fun observeFocusChange() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.onFocusChange.collect { hasFocus -> log.v("App has focus: $hasFocus") if (hasFocus) { @@ -114,7 +164,7 @@ class CanvasObserverRegistry( } private fun observeZoomLevel() { - coroutineScope.launch { + observerScope.launch { page.zoomLevel.drop(1).collect { log.v("zoom level change: ${page.zoomLevel.value}") pageDataManager.setPageZoom(page.currentPageId, page.zoomLevel.value) @@ -124,7 +174,7 @@ class CanvasObserverRegistry( } private fun observeDrawingState() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.isDrawing.collect { log.v("drawing state changed to $it!") viewModel.setDrawingStateFromCanvas(it) @@ -133,7 +183,7 @@ class CanvasObserverRegistry( } private fun observeSelectionGesture() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.rectangleToSelectByGesture.drop(1).collect { if (it != null) { log.v("Area to Select (screen): $it") @@ -144,7 +194,7 @@ class CanvasObserverRegistry( } private fun observeClearPage() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.clearPageSignal.collect { log.v("Clear page signal!") cleanAllStrokes(page, history) @@ -154,7 +204,7 @@ class CanvasObserverRegistry( } private fun observeRestartAfterConfChange() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.reinitSignal.collect { log.v("Configuration changed!") drawCanvas.init() @@ -164,7 +214,7 @@ class CanvasObserverRegistry( } private fun observeReloadFromDb() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.reloadFromDb.collect { page.refreshCurrentPage() refreshManager.refreshUiSuspend() @@ -173,7 +223,7 @@ class CanvasObserverRegistry( } private fun observePenChanges() { - coroutineScope.launch(Dispatchers.Default) { + observerScope.launch(Dispatchers.Default) { viewModel.toolbarState .map { it.pen } .distinctUntilChanged() @@ -184,7 +234,7 @@ class CanvasObserverRegistry( // refreshManager.refreshUiSuspend() } } - coroutineScope.launch { + observerScope.launch { viewModel.toolbarState .map { it.penSettings } .distinctUntilChanged() @@ -195,7 +245,7 @@ class CanvasObserverRegistry( refreshManager.refreshUiSuspend() } } - coroutineScope.launch { + observerScope.launch { viewModel.toolbarState .map { it.eraser } .distinctUntilChanged() @@ -209,7 +259,7 @@ class CanvasObserverRegistry( } private fun observeIsDrawingSnapshot() { - coroutineScope.launch { + observerScope.launch { viewModel.toolbarState .map { it.isDrawing } .distinctUntilChanged() @@ -227,7 +277,7 @@ class CanvasObserverRegistry( } private fun observeToolbar() { - coroutineScope.launch { + observerScope.launch { viewModel.toolbarState .map { it.isToolbarOpen } .distinctUntilChanged() @@ -242,7 +292,7 @@ class CanvasObserverRegistry( } private fun observeMode() { - coroutineScope.launch { + observerScope.launch { viewModel.toolbarState .map { it.mode } .distinctUntilChanged() @@ -257,14 +307,14 @@ class CanvasObserverRegistry( @OptIn(FlowPreview::class) private fun observeHistory() { - coroutineScope.launch { + observerScope.launch { // After 500ms add to history strokes CanvasEventBus.commitHistorySignal.debounce(500).collect { log.v("Commiting to history") drawCanvas.commitToHistory() } } - coroutineScope.launch { + observerScope.launch { CanvasEventBus.commitHistorySignalImmediately.collect { drawCanvas.commitToHistory() CanvasEventBus.commitCompletion.complete(Unit) @@ -274,7 +324,7 @@ class CanvasObserverRegistry( private fun observeSaveCurrent() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.saveCurrent.collect { // Push current bitmap to persist layer so preview has something to load pageDataManager.cacheBitmap(page.currentPageId, page.windowedBitmap) @@ -285,7 +335,7 @@ class CanvasObserverRegistry( @OptIn(FlowPreview::class) private fun observeQuickNav() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.previewPage.debounce(50).collectLatest { pageId -> if (!CanvasEventBus.isScrubbing.value) return@collectLatest // dropped — scrub already ended val pageNumber = pageDataManager.getPageNumberInCurrentNotebook(pageId) @@ -317,7 +367,7 @@ class CanvasObserverRegistry( } private fun observeRestoreCanvas() { - coroutineScope.launch { + observerScope.launch { CanvasEventBus.restoreCanvas.collect { log.d("Restoring canvas") val zoneToRedraw = Rect(0, 0, page.viewWidth, page.viewHeight) 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 28468118..3931b45b 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 @@ -241,12 +241,21 @@ fun partialRefreshRegionOnce(view: View, dirtyRect: Rect, touchHelper: TouchHelp resetScreenFreeze(touchHelper) } +// Single coroutine scope + job for the raw-drawing resume so overlapping calls coalesce. +// Without this, a continuous scroll fires resetScreenFreeze on every frame, each arming a +// fresh 500 ms resume timer — the timers stack and "Resuming raw drawing" floods at the end. +private val screenFreezeScope = CoroutineScope(Dispatchers.Default) +private var screenFreezeResetJob: Job? = null + fun resetScreenFreeze(touchHelper: TouchHelper?, view: View? = null) { if(touchHelper == null) { log.w("touchHelper is null") return } - CoroutineScope(Dispatchers.Default).launch { + // Cancel any pending resume and re-arm. While calls keep arriving (e.g. during a scroll) + // raw drawing stays disabled; only the last call's delay completes and re-enables it once. + screenFreezeResetJob?.cancel() + screenFreezeResetJob = screenFreezeScope.launch { touchHelper.isRawDrawingRenderEnabled = false DeviceCompat.delayBeforeResumingDrawing() touchHelper.isRawDrawingRenderEnabled = true diff --git a/app/src/main/java/com/ethran/notable/gestures/EditorGestureReceiver.kt b/app/src/main/java/com/ethran/notable/gestures/EditorGestureReceiver.kt index 86dda136..d4322c5b 100644 --- a/app/src/main/java/com/ethran/notable/gestures/EditorGestureReceiver.kt +++ b/app/src/main/java/com/ethran/notable/gestures/EditorGestureReceiver.kt @@ -67,7 +67,6 @@ fun EditorGestureReceiver( if (!view.hasWindowFocus()) return@awaitEachGesture val gestureState = GestureState(scope = coroutineScope) - var overdueScroll = Offset.Zero // Ignore non-touch input if (down.type != PointerType.Touch) { @@ -132,9 +131,7 @@ fun EditorGestureReceiver( } if (gestureState.gestureMode == GestureMode.Scroll) { val delta = gestureState.getVerticalDragDelta() - overdueScroll = controlTower.processScroll( - delta = Offset(overdueScroll.x, overdueScroll.y + delta) - ) + controlTower.requestScroll(Offset(0f, delta.toFloat())) } if (gestureState.gestureMode == GestureMode.Zoom) { val delta = gestureState.getPinchDelta() @@ -143,8 +140,7 @@ fun EditorGestureReceiver( if (gestureState.gestureMode == GestureMode.Drag) { val delta = gestureState.getTotalDragDelta() - overdueScroll = - controlTower.processScroll(delta = overdueScroll + delta) + controlTower.requestScroll(delta) } } while (true) @@ -266,7 +262,7 @@ fun EditorGestureReceiver( && abs(verticalDrag) > SWIPE_THRESHOLD ) { log.d("Discrete scrolling, verticalDrag: $verticalDrag") - controlTower.processScroll(Offset(0f, verticalDrag)) + controlTower.requestScroll(Offset(0f, verticalDrag)) } } catch (e: CancellationException) { log.w("Gesture coroutine canceled", e) From a1efa4e9d59913e477cf3127c16a6f303bfcaf4e Mon Sep 17 00:00:00 2001 From: Wiktor Wichrowski Date: Tue, 30 Jun 2026 23:30:53 +0200 Subject: [PATCH 5/5] Bump version to 0.2.2 and disable Sync settings tab. ### Build - **app/build.gradle**: Updated `versionCode` to 36 and `versionName` to 0.2.2. ### UI & UX - **Settings.kt**: - Commented out the "Sync" tab from the settings menu. - Updated tab navigation logic to skip the Sync settings view and re-indexed the Debug settings tab. --- app/build.gradle | 4 ++-- .../java/com/ethran/notable/ui/views/Settings.kt | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d58024c5..333083e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -16,8 +16,8 @@ android { minSdk 29 targetSdk 35 - versionCode 35 - versionName '0.2.1' + versionCode 36 + versionName '0.2.2' if (project.hasProperty('IS_NEXT') && project.property('IS_NEXT').toBoolean()) { def timestamp = new Date().format('dd.MM.YYYY-HH:mm') versionName = "${versionName}-next-${timestamp}" diff --git a/app/src/main/java/com/ethran/notable/ui/views/Settings.kt b/app/src/main/java/com/ethran/notable/ui/views/Settings.kt index 795426b5..14eba962 100644 --- a/app/src/main/java/com/ethran/notable/ui/views/Settings.kt +++ b/app/src/main/java/com/ethran/notable/ui/views/Settings.kt @@ -146,7 +146,7 @@ fun SettingsContent( val tabs = listOf( stringResource(R.string.settings_tab_general_name), stringResource(R.string.settings_tab_gestures_name), - stringResource(R.string.settings_tab_sync_name), +// stringResource(R.string.settings_tab_sync_name), stringResource(R.string.settings_tab_debug_name) ) @@ -176,12 +176,12 @@ fun SettingsContent( settings, onUpdateSettings, listOfGestures, availableGestures ) - 2 -> SyncSettings( - state = syncUiState, - callbacks = syncCallbacks, - ) +// 2 -> SyncSettings( +// state = syncUiState, +// callbacks = syncCallbacks, +// ) - 3 -> DebugSettings(settings, onUpdateSettings, goToWelcome, goToSystemInfo) + 2 -> DebugSettings(settings, onUpdateSettings, goToWelcome, goToSystemInfo) } }