diff --git a/app/src/main/java/com/ethran/notable/MainActivity.kt b/app/src/main/java/com/ethran/notable/MainActivity.kt index 6cfc2a91..95cdde4e 100644 --- a/app/src/main/java/com/ethran/notable/MainActivity.kt +++ b/app/src/main/java/com/ethran/notable/MainActivity.kt @@ -60,7 +60,7 @@ var SCREEN_HEIGHT = EpdController.getEpdWidth().toInt() @AndroidEntryPoint class MainActivity : ComponentActivity() { - // Delay the init till we have the permisions required + // Delay the init till we have the permissions required @Inject lateinit var kvProxy: dagger.Lazy @@ -70,13 +70,15 @@ class MainActivity : ComponentActivity() { @Inject lateinit var editorSettingCacheManager: dagger.Lazy - // 1. Use dagger.Lazy to defer DB initialization until after permissions @Inject lateinit var appRepositoryLazy: dagger.Lazy @Inject lateinit var exportEngineLazy: dagger.Lazy + @Inject + lateinit var pageDataManager: dagger.Lazy + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableFullScreen() @@ -92,7 +94,6 @@ class MainActivity : ComponentActivity() { val snackState = SnackState() snackState.registerGlobalSnackObserver() snackState.registerCancelGlobalSnackObserver() - PageDataManager.registerComponentCallbacks(this) setContent { var isInitialized by remember { mutableStateOf(false) } @@ -106,9 +107,10 @@ class MainActivity : ComponentActivity() { ?: AppSettings(version = 1) GlobalAppSettings.update(savedSettings) - - editorSettingCacheManager.get().init() strokeMigrationHelper.get().reencodeStrokePointsToSB1() + pageDataManager.get() + .registerComponentCallbacks(this@MainActivity.applicationContext) + editorSettingCacheManager.get().init() } } isInitialized = true @@ -120,7 +122,6 @@ class MainActivity : ComponentActivity() { NotableApp( // Call .get() here so they are only instantiated AFTER the permission check runs exportEngine = exportEngineLazy.get(), - editorSettingCacheManager = editorSettingCacheManager.get(), snackState = snackState, appRepository = appRepositoryLazy.get() ) diff --git a/app/src/main/java/com/ethran/notable/data/PageDataManager.kt b/app/src/main/java/com/ethran/notable/data/PageDataManager.kt index 49115a2f..097f0936 100644 --- a/app/src/main/java/com/ethran/notable/data/PageDataManager.kt +++ b/app/src/main/java/com/ethran/notable/data/PageDataManager.kt @@ -3,6 +3,7 @@ package com.ethran.notable.data import android.content.ComponentCallbacks2 import android.content.Context import android.content.res.Configuration +import android.database.sqlite.SQLiteConstraintException import android.graphics.Bitmap import android.graphics.Rect import android.os.FileObserver @@ -11,6 +12,7 @@ import androidx.compose.ui.geometry.Offset import com.ethran.notable.SCREEN_HEIGHT import com.ethran.notable.SCREEN_WIDTH import com.ethran.notable.data.db.Image +import com.ethran.notable.data.db.Page import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.db.getBackgroundType import com.ethran.notable.data.model.BackgroundType @@ -26,6 +28,7 @@ import com.ethran.notable.io.loadBackgroundBitmap import com.ethran.notable.io.waitForFileAvailable import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState +import com.ethran.notable.ui.SnackState.Companion.logAndShowError import com.ethran.notable.ui.showHint import com.ethran.notable.utils.chunked import com.onyx.android.sdk.extension.isNotNull @@ -43,6 +46,8 @@ import kotlinx.coroutines.sync.withLock import java.io.File import java.lang.ref.SoftReference import java.security.MessageDigest +import javax.inject.Inject +import javax.inject.Singleton import kotlin.coroutines.cancellation.CancellationException import kotlin.math.max @@ -66,8 +71,15 @@ data class CachedBackground(val path: String, val pageNumber: Int, val scale: Fl } // Cache manager companion object -object PageDataManager { +@Singleton +class PageDataManager @Inject constructor( + private val appRepository: AppRepository +) { val log = ShipBook.getLogger("PageDataManager") + private val dataScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + var pageFromDb: Page? = null + + private val strokes = LinkedHashMap>() private var strokesById = LinkedHashMap>() @@ -96,6 +108,13 @@ object PageDataManager { @Volatile private var currentPage = "" + @Volatile + private var currentPageNumber = -1 + + fun getCurrentPageId(): String { + return currentPage + } + private val accessLock = Any() // Lock for accessing Images, Strokes, Backgrounds & derived private var entrySizeMB = LinkedHashMap() @@ -128,7 +147,7 @@ object PageDataManager { * Locking is handled internally. */ private suspend fun getOrStartLoadingJob( - appRepository: AppRepository, pageId: String, bookId: String? + pageId: String, bookId: String? ): Job { log.d("getOrStartLoadingJob($pageId)") // PageDataManager.ensureMemoryAvailable(15) @@ -148,11 +167,11 @@ object PageDataManager { existing == null || existing.isCancelled -> { // Cancel any previous job, without current, next and previous page if (bookId.isNotNull()) - cancelUnnecessaryLoading(appRepository, pageId, bookId) + cancelUnnecessaryLoading(pageId, bookId) log.d("starting loading of the Page($pageId)") if (existing.isNull() && areListInitialized(pageId)) log.e("Illegal state: Page($pageId) already in memory, but job is null.") val newJob = dataLoadingScope.launch { - loadPageFromDb(appRepository, this, pageId) + loadPageFromDb(this, pageId) } dataLoadingJobs[pageId] = newJob newJob @@ -166,15 +185,15 @@ object PageDataManager { /** * Ensures that the page is loaded; suspends until load is finished. */ - suspend fun requestPageLoadJoin( - appRepository: AppRepository, pageId: String, bookId: String? + suspend fun requestCurrentPageLoadJoin( ) { - log.d("requestPageLoadJoin($pageId)") - getOrStartLoadingJob(appRepository, pageId, bookId).join() + assert(currentPage == pageFromDb?.id) + val bookId = pageFromDb?.notebookId + log.d("requestCurrentPageLoadJoin($currentPage)") + getOrStartLoadingJob(currentPage, bookId).join() } private suspend fun cancelUnnecessaryLoading( - appRepository: AppRepository, pageId: String, bookId: String ) { @@ -190,30 +209,37 @@ object PageDataManager { ) } - suspend fun cacheNeighbors(appRepository: AppRepository, pageId: String, bookId: String) { + suspend fun cacheNeighbors() { + assert(currentPage == pageFromDb?.id) + val bookId = pageFromDb?.notebookId ?: return + + log.d("cacheNeighbors($currentPage)") // Only attempt to cache neighbors if we have memory to spare. if (!hasEnoughMemory(15)) return try { // Cache next page if not already cached val nextPageId = - appRepository.getNextPageIdFromBookAndPage(pageId = pageId, notebookId = bookId) + appRepository.getNextPageIdFromBookAndPage( + pageId = currentPage, + notebookId = bookId + ) log.d("Caching next page $nextPageId") nextPageId?.let { nextPage -> - requestPageLoad(appRepository, nextPage) + requestPageLoad(nextPage) } if (hasEnoughMemory(15)) { // Cache previous page if not already cached val prevPageId = appRepository.getPreviousPageIdFromBookAndPage( - pageId = pageId, + pageId = currentPage, notebookId = bookId ) log.d("Caching prev page $prevPageId") prevPageId?.let { prevPage -> - requestPageLoad(appRepository, prevPage) + requestPageLoad(prevPage) } } } catch (e: CancellationException) { @@ -231,22 +257,24 @@ object PageDataManager { * Requests that the given page is loaded, but doesn't wait. * If already loading, is a no-op. */ - fun requestPageLoad( - appRepository: AppRepository, pageId: String - ) { + fun requestPageLoad(pageId: String) { dataLoadingScope.launch { - getOrStartLoadingJob(appRepository, pageId, null) + getOrStartLoadingJob(pageId, null) } } - private suspend fun preLoadBackground(appRepository: AppRepository, pageId: String) { - val pageFromDb = appRepository.pageRepository.getById(pageId) ?: return - val backgroundType = pageFromDb.getBackgroundType() - val background = pageFromDb.background + private suspend fun preLoadBackground(pageId: String) { + val pageDataFromDb = appRepository.pageRepository.getById(pageId) + if (pageDataFromDb == null) { + log.e("Background not found for page $pageId") + return + } + val backgroundType = pageDataFromDb.getBackgroundType() + val background = pageDataFromDb.background val pageNumber = when (backgroundType) { is BackgroundType.Pdf -> backgroundType.page is BackgroundType.AutoPdf -> backgroundType.getPage( - appRepository, pageFromDb.notebookId, pageId + appRepository, pageDataFromDb.notebookId, pageId ) ?: return BackgroundType.Native -> return @@ -254,20 +282,18 @@ object PageDataManager { } val value = CachedBackground(background, pageNumber, 1f) log.i("Preloaded background: $value") - val observeBg = appRepository.isObservable(pageFromDb.notebookId) - setBackground(pageId, value, observeBg) + setBackground(pageId, value) } private suspend fun loadPageFromDb( - appRepository: AppRepository, coroutineScope: CoroutineScope, pageId: String + coroutineScope: CoroutineScope, pageId: String ) { try { log.d("Loading page $pageId") // sleep(5000) - coroutineScope.launch { - log.d("Preloading background for page $pageId") - preLoadBackground(appRepository, pageId) - } + log.d("Preloading background for page $pageId") + preLoadBackground(pageId) + val pageWithStrokes = appRepository.pageRepository.getWithStrokeById(pageId) // What will happened if page isn't in repository? @@ -320,8 +346,11 @@ object PageDataManager { // 3) Reconcile: if they disagree, warn and clear if (jobSnapshot.isNotNull() && dataLoaded != jobDone) { - log.e("Inconsistent state for page($pageId): dataLoaded=$dataLoaded, jobDone=$jobDone, job=$jobSnapshot") - showHint("Fixing inconsistent page state: $pageId") + SnackState.logAndShowError( + "PageDataManager.validatePageDataLoaded", + "Inconsistent state for page($pageId): dataLoaded=$dataLoaded," + + " jobDone=$jobDone, job=$jobSnapshot, trying to fix." + ) dataLoadingScope.launch { // Cancel/remove any job for this page jobLock.withLock { @@ -393,11 +422,22 @@ object PageDataManager { } } - - fun setPage(pageId: String) { + /* + * Sets current page, and starts loading it from db. + */ + suspend fun setPage(pageId: String) { + pageFromDb = appRepository.pageRepository.getById(pageId) + pageFromDb?.notebookId?.let { notebookId -> + currentPageNumber = appRepository.getPageNumber(notebookId, pageId) + } currentPage = pageId } + suspend fun refreshPageFromDb() { + pageFromDb = appRepository.pageRepository.getById(currentPage) + log.i("Refresh current page, background: ${pageFromDb?.background}") + } + fun getCachedBitmap(pageId: String): Bitmap? { return bitmapCache[pageId]?.get()?.takeIf { !it.isRecycled && it.isMutable @@ -434,7 +474,11 @@ object PageDataManager { } } - fun getPageScroll(pageId: String): Offset? = pageScroll[pageId] + fun getPageScroll(pageId: String): Offset { + return pageScroll.getOrPut(pageId) { + Offset(0f, pageFromDb?.scroll?.toFloat() ?: 0f) + } + } fun setPageScroll(pageId: String, scroll: Offset) { pageScroll[pageId] = scroll } @@ -445,6 +489,20 @@ object PageDataManager { } + fun isTransformationAllowedForCurrentPage(): Boolean { + return when (pageFromDb?.backgroundType) { + "native", null -> true + "coverImage" -> false + else -> true + } + } + + fun getCurrentPageNumber(): Int { + if (currentPageNumber == -1) + log.d("Current page number: $currentPageNumber") + return currentPageNumber + } + fun getStrokes(pageId: String): List = strokes[pageId] ?: emptyList() @@ -508,6 +566,63 @@ object PageDataManager { } } + fun updateStrokesInDb(strokes: List) { + dataScope.launch { + appRepository.strokeRepository.update(strokes) + } + } + + fun saveStrokesToDb(strokes: List) { + dataScope.launch { + try { + appRepository.strokeRepository.create(strokes) + } catch (_: SQLiteConstraintException) { + // There were some rare bugs when strokes weren't unique when inserting from history + // I'm not sure if it's still a problem, let's just show the message + logAndShowError( + "saveStrokesToPersistLayer", + "Attempted to create strokes that already exist" + ) + appRepository.strokeRepository.update(strokes) + } + } + } + + fun saveImagesToDb(images: List) { + dataScope.launch { + appRepository.imageRepository.create(images) + } + } + + fun removeStrokesFromDb(strokes: List) { + dataScope.launch { + appRepository.strokeRepository.deleteAll(strokes) + } + } + + fun removeImagesFromDb(images: List) { + dataScope.launch { + appRepository.imageRepository.deleteAll(images) + } + } + + fun setScrollInDb() { + dataScope.launch { + appRepository.pageRepository.updateScroll( + currentPage, + getPageScroll(currentPage).y.toInt() + ) + } + } + + fun getBackgroundType(): BackgroundType? { + return pageFromDb?.getBackgroundType() + } + + fun getBackgroundName(): String { + return pageFromDb?.background ?: "blank" + } + private fun cacheStrokes(pageId: String, strokes: List) { synchronized(accessLock) { @@ -531,23 +646,33 @@ object PageDataManager { } } - fun setBackground(pageId: String, background: CachedBackground, observe: Boolean) { - synchronized(accessLock) { + fun setCurrentBackground(background: CachedBackground){ + setBackground(currentPage, background) + } - // Merge/upgrade cache: if we already have an entry for this background, - // keep the one with higher scale (higher quality). - val existing = backgroundCache[background.id] - if (existing == null || background.scale > existing.scale) { - backgroundCache[background.id] = background - log.d("Cached background set: id=${background.id} scale=${background.scale}") - } else { - log.d("Cached background exists with equal/higher scale; reusing id=${existing.id} scale=${existing.scale}") - } + fun setBackground(pageId: String, background: CachedBackground) { + dataScope.launch { + // we assume that the pageId is in current notebook. + val observeBg = appRepository.isObservable(pageFromDb?.notebookId) - // Link this page to the background key - pageToBackgroundKey[pageId] = background.id + synchronized(accessLock) { - if (observe) observeBackgroundFile(pageId, background.path) + // Merge/upgrade cache: if we already have an entry for this background, + // keep the one with higher scale (higher quality). + val existing = backgroundCache[background.id] + if (existing == null || background.scale > existing.scale) { + backgroundCache[background.id] = background + log.d("Cached background set: id=${background.id} scale=${background.scale}") + } else { + log.d("Cached background exists with equal/higher scale; reusing id=${existing.id} scale=${existing.scale}") + } + + // Link this page to the background key + pageToBackgroundKey[pageId] = background.id + + if (observeBg) + observeBackgroundFile(pageId, background.path) + } } } @@ -563,15 +688,22 @@ object PageDataManager { * @param pageId The unique identifier of the page for which to retrieve the background. * @return The [CachedBackground] associated with the page, or a default empty instance if not found. */ - fun getBackground(pageId: String): CachedBackground { + fun getCurrentBackground(): CachedBackground { return synchronized(accessLock) { - val key = pageToBackgroundKey[pageId] + val key = pageToBackgroundKey[currentPage] val bg = if (key != null) backgroundCache[key] else null - log.d("Background for page $pageId: $bg") + log.d("Background for page $currentPage (no. $currentPageNumber): $bg") bg ?: CachedBackground("", 0, 1.0f) } } + suspend fun getPageNumberInCurrentNotebook(pageId: String): Int { + val pageNumber = + appRepository.getPageNumber(pageFromDb?.notebookId!!, pageId) + log.d("Page number for page($pageNumber): $pageId") + return pageNumber + } + /** * Start observing a background file for changes. * Registers the pageId to the file, and launches a FileObserver if not already present. @@ -707,12 +839,9 @@ object PageDataManager { fun removePage(pageId: String): Boolean { log.d("Removing page $pageId") if (pageId == currentPage) { - log.e("Removing current page!") - SnackState.globalSnackFlow.tryEmit( - SnackConf( - text = "Cannot remove current page, there is a bug in code", - duration = 3000 - ) + SnackState.logAndShowError( + "PageDataManager.removePage", + "Cannot remove current page, there is a bug in code", ) return false } diff --git a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt index fdaff115..2d0075d0 100644 --- a/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt +++ b/app/src/main/java/com/ethran/notable/data/datastore/EditorSettingCacheManager.kt @@ -9,12 +9,15 @@ import com.ethran.notable.editor.utils.Pen import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import javax.inject.Inject import javax.inject.Singleton + const val persistVersion = 2 @Singleton @@ -34,17 +37,34 @@ class EditorSettingCacheManager ) private val scope = CoroutineScope(Dispatchers.IO) + private val initMutex = Mutex() + + @Volatile + private var isInitialized = false + suspend fun init() { - val settingsJSon = withContext(Dispatchers.IO) { - kvRepository.get("EDITOR_SETTINGS") - } - if (settingsJSon != null) { - val settings = Json.decodeFromString(settingsJSon.value) - if (settings.version == persistVersion) setEditorSettings(settings, false) + if (isInitialized) return + + initMutex.withLock { + if (isInitialized) return + + val settingsJson = withContext(Dispatchers.IO) { + kvRepository.get("EDITOR_SETTINGS")?.value + } + + val settings = settingsJson + ?.let { runCatching { Json.decodeFromString(it) }.getOrNull() } + + if (settings?.version == persistVersion) { + setEditorSettings(settings, shouldPersist = false) + } + + isInitialized = true } } + private fun persist(settings: EditorSettings) { val settingsJson = Json.encodeToString(settings) scope.launch { 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 f458f694..50fab573 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -39,20 +39,27 @@ class EditorControlTower( private var scrollInProgress = Mutex() private var scrollJob: Job? = null private val logEditorControlTower = ShipBook.getLogger("EditorControlTower") - - + private var changePageObserverJob: Job? = null fun registerObservers() { - scope.launch { + if (changePageObserverJob?.isActive == true) return + + changePageObserverJob = scope.launch { CanvasEventBus.changePage.collect { pageId -> logEditorControlTower.d("Change to page $pageId") switchPage(pageId) - page.changePage(pageId) +// page.changePage(pageId) refreshScreen() } } } + // TODO: remove it, change to proper solution + fun unregisterObservers() { + changePageObserverJob?.cancel() + changePageObserverJob = 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 { @@ -96,10 +103,10 @@ class EditorControlTower( history.cleanHistory() } - // Switch to (or ensure we are on) IO thread for Database operations - withContext(Dispatchers.IO) { - page.changePage(id) - } +// // Switch to (or ensure we are on) IO thread for Database operations +// withContext(Dispatchers.IO) { +// page.changePage(id) +// } } fun setIsDrawing(value: Boolean) { 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 50608bdd..58089066 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorView.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorView.kt @@ -9,18 +9,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavController -import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History @@ -30,24 +25,15 @@ import com.ethran.notable.editor.ui.ScrollIndicator import com.ethran.notable.editor.ui.SelectedBitmap 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 import com.ethran.notable.navigation.NavigationDestination import com.ethran.notable.ui.LocalSnackContext 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.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext private val log = ShipBook.getLogger("EditorView") @@ -69,47 +55,30 @@ object EditorDestination : NavigationDestination { } + @Composable fun EditorView( - editorSettingCacheManager: EditorSettingCacheManager, - exportEngine: ExportEngine, - navController: NavController, - appRepository: AppRepository, + initialPageId: String, bookId: String?, - pageId: String, isQuickNavOpen: Boolean, + + // navigation callbacks onPageChange: (String) -> Unit, + goToLibrary: (folderId: String?) -> Unit, + goToPages: (bookId: String) -> Unit, + goToBugReport: () -> Unit, + viewModel: EditorViewModel = hiltViewModel() ) { val context = LocalContext.current val snackManager = LocalSnackContext.current val scope = rememberCoroutineScope() - var pageExists by remember(pageId) { mutableStateOf(null) } - LaunchedEffect(pageId) { - viewModel.loadBookData(bookId, pageId) - val exists = withContext(Dispatchers.IO) { - appRepository.pageRepository.getById(pageId) != null - } - pageExists = exists - - if (!exists) { - // TODO: check if it is correct, and remove exeption throwing - throw Exception("Page does not exist") - if (bookId != null) { - // clean the book - log.i("Could not find page, Cleaning book") - SnackState.globalSnackFlow.tryEmit( - SnackConf( - text = "Could not find page, cleaning book", duration = 4000 - ) - ) - scope.launch(Dispatchers.IO) { - appRepository.bookRepository.removePage(bookId, pageId) - } - } - navController.navigate(LibraryDestination.route) - } + // Single point of entry for loading book data based on the pageId from Navigation + // Should not be used for regular page switching + LaunchedEffect(initialPageId) { + log.v("EditorView: pageId changed to $initialPageId, loading data") + viewModel.loadToolbarState(bookId, initialPageId) } // Sync isQuickNavOpen to ViewModel @@ -117,18 +86,18 @@ fun EditorView( viewModel.onToolbarAction(ToolbarAction.UpdateQuickNavOpen(isQuickNavOpen)) } - if (pageExists == null) return BoxWithConstraints { val height = convertDpToPixel(this.maxHeight, context).toInt() val width = convertDpToPixel(this.maxWidth, context).toInt() + // Here we load initial page into the memory val page = remember { PageView( context = context, coroutineScope = scope, - appRepository = appRepository, - currentPageId = pageId, + pageDataManager = viewModel.pageDataManager, + initialPageId = initialPageId, viewWidth = width, viewHeight = height, snackManager = snackManager, @@ -144,34 +113,38 @@ fun EditorView( EditorState(viewModel) } + val editorControlTower = remember { + EditorControlTower(scope, page, history, editorState) + } + + // Initialize ViewModel with persisted settings on first composition LaunchedEffect(Unit) { - viewModel.initFromPersistedSettings(editorSettingCacheManager.getEditorSettings()) + viewModel.initFromPersistedSettings() viewModel.updateDrawingState() } - val editorControlTower = remember { - EditorControlTower(scope, page, history, editorState).apply { registerObservers() } + DisposableEffect(editorControlTower) { + editorControlTower.registerObservers() + onDispose { + editorControlTower.unregisterObservers() + } } - // Collect UI Events from ViewModel (navigation and snackbars) + // Collect UI Events from ViewModel (navigation ) LaunchedEffect(Unit) { viewModel.uiEvents.collect { event -> when (event) { is EditorUiEvent.NavigateToLibrary -> { - navController.navigate(LibraryDestination.createRoute(event.folderId)) + goToLibrary(event.folderId) } is EditorUiEvent.NavigateToPages -> { - navController.navigate(PagesDestination.createRoute(event.bookId)) + goToPages(event.bookId) } EditorUiEvent.NavigateToBugReport -> { - navController.navigate(BugReportDestination.route) - } - - is EditorUiEvent.ShowSnackbar -> { - snackManager.displaySnack(SnackConf(text = event.message, duration = 2000)) + goToBugReport() } } } @@ -230,6 +203,11 @@ fun EditorView( .distinctUntilChanged() .drop(1) // Skip initial emission from loadBookData .collect { newPageId -> + log.v("EditorView: snapshotFlow detected pageId change to $newPageId, triggering onPageChange") + // update the PageView + page.changePage(newPageId) + + // update the navigation state onPageChange(newPageId) } } @@ -248,42 +226,16 @@ fun EditorView( DisposableEffect(Unit) { onDispose { - // finish selection operation - viewModel.selectionState.applySelectionDisplace(page) - if (bookId != null) exportToLinkedFile( - exportEngine, - bookId, - appRepository.bookRepository - ) - page.disposeOldPage() + viewModel.onDispose(page) } } - // Persist editor settings when they change - LaunchedEffect( - toolbarState.isToolbarOpen, - toolbarState.pen, - toolbarState.penSettings, - toolbarState.mode, - toolbarState.eraser - ) { - log.i("EditorView: saving editor settings") - editorSettingCacheManager.setEditorSettings( - EditorSettingCacheManager.EditorSettings( - isToolbarOpen = toolbarState.isToolbarOpen, - mode = toolbarState.mode, - pen = toolbarState.pen, - eraser = toolbarState.eraser, - penSettings = toolbarState.penSettings - ) - ) - } InkaTheme { EditorGestureReceiver(controlTower = editorControlTower) EditorSurface( - appRepository = appRepository, state = editorState, page = page, history = history + state = editorState, page = page, history = history ) SelectedBitmap( context = context, controlTower = editorControlTower 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 d94bce31..9250a222 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorViewModel.kt @@ -7,6 +7,7 @@ import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.ethran.notable.data.AppRepository +import com.ethran.notable.data.PageDataManager import com.ethran.notable.data.copyImageToDatabase import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.data.datastore.GlobalAppSettings @@ -23,6 +24,10 @@ import com.ethran.notable.editor.utils.PenSetting import com.ethran.notable.io.ExportEngine import com.ethran.notable.io.ExportFormat import com.ethran.notable.io.ExportTarget +import com.ethran.notable.io.exportToLinkedFile +import com.ethran.notable.ui.SnackConf +import com.ethran.notable.ui.SnackState +import com.ethran.notable.ui.SnackState.Companion.logAndShowError import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import io.shipbook.shipbooksdk.Log @@ -141,7 +146,6 @@ sealed class CanvasCommand { // -------------------------------------------------------- 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() @@ -154,8 +158,10 @@ sealed class EditorUiEvent { @HiltViewModel class EditorViewModel @Inject constructor( @param:ApplicationContext private val context: Context, - private val appRepository: AppRepository, - private val exportEngine: ExportEngine + val appRepository: AppRepository, + var editorSettingCacheManager: EditorSettingCacheManager, + private val exportEngine: ExportEngine, + val pageDataManager: PageDataManager ) : ViewModel() { // ---- Toolbar / UI State (single flat flow) ---- @@ -187,9 +193,9 @@ class EditorViewModel @Inject constructor( * Restores editor settings from the persisted cache. * Idempotent: only applies settings on first call; subsequent calls are no-ops. */ - fun initFromPersistedSettings(settings: EditorSettingCacheManager.EditorSettings?) { + fun initFromPersistedSettings() { if (!didInitSettings.compareAndSet(false, true)) return - + val settings = editorSettingCacheManager.getEditorSettings() _toolbarState.update { it.copy( mode = settings?.mode ?: Mode.Draw, @@ -201,6 +207,19 @@ class EditorViewModel @Inject constructor( } } + fun onDispose(page: PageView) { + // finish selection operation + selectionState.applySelectionDisplace(page) + bookId?.let { bookId -> + exportToLinkedFile( + exportEngine, + bookId, + appRepository.bookRepository + ) + } + page.disposeOldPage() + } + // -------------------------------------------------------- // Toolbar Action Dispatch // -------------------------------------------------------- @@ -211,11 +230,13 @@ class EditorViewModel @Inject constructor( is ToolbarAction.ToggleToolbar -> { _toolbarState.update { it.copy(isToolbarOpen = !it.isToolbarOpen) } updateDrawingState() + saveToolbarState() } is ToolbarAction.ChangeMode -> { _toolbarState.update { it.copy(mode = action.mode) } updateDrawingState() + saveToolbarState() } is ToolbarAction.ChangePen -> handlePenChange(action.pen) @@ -280,6 +301,7 @@ class EditorViewModel @Inject constructor( _toolbarState.update { it.copy(pen = pen, mode = Mode.Draw) } + saveToolbarState() } updateDrawingState() } @@ -287,12 +309,14 @@ class EditorViewModel @Inject constructor( private fun handleEraserChange(eraser: Eraser) { _toolbarState.update { it.copy(eraser = eraser) } updateDrawingState() + saveToolbarState() } private fun handlePenSettingChange(pen: Pen, setting: PenSetting) { val newSettings = _toolbarState.value.penSettings.toMutableMap() newSettings[pen.penName] = setting _toolbarState.update { it.copy(penSettings = newSettings) } + saveToolbarState() } private fun handleCloseAllMenus() { @@ -321,7 +345,7 @@ class EditorViewModel @Inject constructor( val copiedFile = copyImageToDatabase(context, uri) sendCanvasCommand(CanvasCommand.CopyImageToCanvas(copiedFile.toUri())) } catch (e: Exception) { - sendUiEvent(EditorUiEvent.ShowSnackbar("Image import failed: ${e.message}")) + logAndShowError("EditorViewModel", "Image import failed: ${e.message}") } } } @@ -330,9 +354,10 @@ class EditorViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { try { val result = exportEngine.export(target, format) - sendUiEvent(EditorUiEvent.ShowSnackbar(result)) + val snack = SnackConf(text = result, duration = 4000) + SnackState.globalSnackFlow.emit(snack) } catch (e: Exception) { - sendUiEvent(EditorUiEvent.ShowSnackbar("Export failed: ${e.message}")) + logAndShowError("EditorViewModel", "Export failed: ${e.message}") } } } @@ -412,40 +437,76 @@ class EditorViewModel @Inject constructor( /** * Loads context data for the toolbar (page number, background info, etc.) */ - fun loadBookData(bookId: String?, pageId: String) { + suspend fun loadToolbarState(bookId: String?, pageId: String) { log.v("loadBookData: bookId=$bookId, pageId=$pageId") this.bookId = bookId - viewModelScope.launch(Dispatchers.IO) { - val page = appRepository.pageRepository.getById(pageId) - val book = bookId?.let { appRepository.bookRepository.getById(it) } + val page = appRepository.pageRepository.getById(pageId) + if (page == null) { + logAndShowError( + reason = "EditorViewModel", + message = "Could not find page", + ) + fixNotebook(bookId, pageId) + return + } + val book = bookId?.let { appRepository.bookRepository.getById(it) } - val pageIndex = book?.getPageIndex(pageId) ?: 0 - val totalPages = book?.pageIds?.size ?: 1 + 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 + val backgroundTypeObj = BackgroundType.fromKey(page.backgroundType) + val bgPageNumber = when (backgroundTypeObj) { + is BackgroundType.Pdf -> backgroundTypeObj.page + is BackgroundType.AutoPdf -> { + bookId?.let { appRepository.getPageNumber(it, pageId) } ?: 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 - ) - } + 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, + backgroundPath = page.background, + backgroundPageNumber = bgPageNumber + ) + } + } + + private fun saveToolbarState() { + val currentState = _toolbarState.value + editorSettingCacheManager.setEditorSettings( + EditorSettingCacheManager.EditorSettings( + isToolbarOpen = currentState.isToolbarOpen, + mode = currentState.mode, + pen = currentState.pen, + eraser = currentState.eraser, + penSettings = currentState.penSettings + ) + ) + } + + /** + * Attempts to repair potential inconsistencies in the notebook's data structure. + */ + suspend fun fixNotebook(bookId: String?, pageId: String) { + TODO("""I'm not confident in the code below.""" ) +// if (bookId != null) { +// log.i("Could not find page, Cleaning book") +// SnackState.globalSnackFlow.tryEmit( +// SnackConf( +// text = "Could not find page, cleaning book", duration = 4000 +// ) +// ) +// appRepository.bookRepository.removePage(bookId, pageId) +// +// } } // -------------------------------------------------------- @@ -482,6 +543,15 @@ class EditorViewModel @Inject constructor( } } + /** + * Updates the persistence layer and UI state to reflect a change in the currently opened page. + * + * This method saves the [newPageId] as the last opened page for the current notebook in the + * repository. If the page ID has changed, it updates the toolbar state; otherwise, it + * triggers a UI event to notify the user that the target page is already active. + * + * @param newPageId The unique identifier of the page to be set as open. + */ private suspend fun updateOpenedPage(newPageId: String) { log.v("updateOpenedPage: $newPageId") Log.d("EditorView", "Update open page to $newPageId") @@ -489,11 +559,14 @@ class EditorViewModel @Inject constructor( appRepository.bookRepository.setOpenPageId(bookId!!, newPageId) } if (newPageId != currentPageId) { + // The View's LaunchedEffect will handle the full load once navigation syncs. Log.d("EditorView", "Page changed") - loadBookData(bookId, newPageId) +// loadBookData(bookId, newPageId) + _toolbarState.update { it.copy(pageId = newPageId) } } else { Log.d("EditorView", "Tried to change to same page!") - sendUiEvent(EditorUiEvent.ShowSnackbar("Tried to change to same page!")) + val snack = SnackConf(text = "Tried to change to same page!", duration = 4000) + SnackState.globalSnackFlow.emit(snack) } } @@ -503,10 +576,17 @@ class EditorViewModel @Inject constructor( * @param id The unique identifier of the page to switch to. */ fun changePage(id: String) { - log.v("changePage: $id") log.d("Changing page to $id, from $currentPageId") viewModelScope.launch(Dispatchers.IO) { + // 1. Notify the PageView about the change + + // 2. Update the persistent layer + + // 3. Update the UI state updateOpenedPage(id) + + + // 4. Clean the selection state selectionState.reset() } } 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 dfa36343..78cb9ee3 100644 --- a/app/src/main/java/com/ethran/notable/editor/PageView.kt +++ b/app/src/main/java/com/ethran/notable/editor/PageView.kt @@ -2,7 +2,6 @@ package com.ethran.notable.editor import android.content.Context -import android.database.sqlite.SQLiteConstraintException import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color @@ -16,14 +15,11 @@ import androidx.core.graphics.createBitmap import androidx.core.graphics.toRect import com.ethran.notable.SCREEN_HEIGHT import com.ethran.notable.SCREEN_WIDTH -import com.ethran.notable.data.AppRepository import com.ethran.notable.data.CachedBackground import com.ethran.notable.data.PageDataManager import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.Image -import com.ethran.notable.data.db.Page import com.ethran.notable.data.db.Stroke -import com.ethran.notable.data.db.getBackgroundType import com.ethran.notable.data.model.BackgroundType import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.drawing.drawBg @@ -36,8 +32,6 @@ import com.ethran.notable.editor.utils.times import com.ethran.notable.editor.utils.toIntOffset import com.ethran.notable.gestures.ZOOM_SNAP_THRESHOLD import com.ethran.notable.ui.SnackState -import com.ethran.notable.ui.SnackState.Companion.logAndShowError -import com.onyx.android.sdk.extension.isNotNull import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -57,13 +51,14 @@ const val OVERLAP = 2 /** * Manages the state and rendering of a single page within the editor. - * @param currentPageId Id of page assigned to it. + * It delegates task to PageDataManager, which is responsible for loading data from db, + * and caching it. */ class PageView( val context: Context, val coroutineScope: CoroutineScope, - val appRepository: AppRepository, - var currentPageId: String, + val pageDataManager: PageDataManager, + val initialPageId: String, var viewWidth: Int, var viewHeight: Int, val snackManager: SnackState, @@ -85,68 +80,59 @@ class PageView( // var strokes = listOf() var strokes: List - get() = PageDataManager.getStrokes(currentPageId) - set(value) = PageDataManager.setStrokes(currentPageId, value) + get() = pageDataManager.getStrokes(currentPageId) + set(value) = pageDataManager.setStrokes(currentPageId, value) val strokesById: HashMap - get() = PageDataManager.getStrokesById(currentPageId) + get() = pageDataManager.getStrokesById(currentPageId) var images: List - get() = PageDataManager.getImages(currentPageId) - set(value) = PageDataManager.setImages(currentPageId, value) + 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) { - coroutineScope.launch { - val observeBg = appRepository.isObservable(pageFromDb?.notebookId) - PageDataManager.setBackground(currentPageId, value, observeBg) - } - } + get() = pageDataManager.getCurrentBackground() + set(value) = pageDataManager.setCurrentBackground(value) + + val currentPageId: String + get() = pageDataManager.getCurrentPageId() + + + // scroll is observed by ui, represents top left corner var scroll: Offset - get() = PageDataManager.getPageScroll(currentPageId) ?: run { - val value = Offset(0f, pageFromDb?.scroll?.toFloat() ?: 0f) - PageDataManager.setPageScroll(currentPageId, value) - value - } - set(value) { - PageDataManager.setPageScroll(currentPageId, value) - } + get() = pageDataManager.getPageScroll(currentPageId) + set(value) = pageDataManager.setPageScroll(currentPageId, value) + val isTransformationAllowed: Boolean - get() = when (pageFromDb?.backgroundType) { - "native", null -> true - "coverImage" -> false - else -> true - } + get() = pageDataManager.isTransformationAllowedForCurrentPage() + // we need to observe zoom level, to adjust strokes size. val zoomLevel: MutableStateFlow = - MutableStateFlow(PageDataManager.getPageZoom(currentPageId)) + MutableStateFlow(pageDataManager.getPageZoom(currentPageId)) var height: Int - get() = PageDataManager.getPageHeight(currentPageId) ?: viewHeight + get() = pageDataManager.getPageHeight(currentPageId) ?: viewHeight set(value) { - PageDataManager.setPageHeight(currentPageId, value) + pageDataManager.setPageHeight(currentPageId, value) } - var pageFromDb: Page? = null - - private var dbStrokes = appRepository.strokeRepository - private var dbImages = appRepository.imageRepository +// private var dbStrokes = appRepository.strokeRepository +// private var dbImages = appRepository.imageRepository - private var currentPageNumberVal: Int = -1 val currentPageNumber: Int - get() = currentPageNumberVal + get() = pageDataManager.getCurrentPageNumber() /* If pageNumber is -1, its assumed that the background is image type. */ fun getOrLoadBackground(filePath: String, pageNumber: Int, scale: Float): Bitmap? { + log.i("getOrLoadBackground") val cached = currentBackground if (cached.matches(filePath, pageNumber, scale)) { log.i("Background bitmap (cached): ${cached.bitmap}") @@ -166,30 +152,29 @@ class PageView( init { - PageDataManager.setPage(currentPageId) - log.i("PageView init") - zoomLevel.value = PageDataManager.getPageZoom(currentPageId) - PageDataManager.getCachedBitmap(currentPageId)?.let { cached -> - log.i("PageView: using cached bitmap") - windowedBitmap = cached - windowedCanvas = Canvas(windowedBitmap) - } ?: run { - log.i("PageView.init: creating new bitmap") - recreateCanvas() - PageDataManager.cacheBitmap(currentPageId, windowedBitmap) - } + coroutineScope.launch(Dispatchers.IO) { + // set page, and retrieve page data from db + pageDataManager.setPage(initialPageId) + log.i("PageView init") - coroutineScope.launch { - withContext(Dispatchers.IO) { - pageFromDb = appRepository.pageRepository.getById(currentPageId) - pageFromDb?.notebookId?.let { notebookId -> - currentPageNumberVal = appRepository.getPageNumber(notebookId, currentPageId) - } + zoomLevel.value = pageDataManager.getPageZoom(currentPageId) + pageDataManager.getCachedBitmap(currentPageId)?.let { cached -> + log.i("PageView: using cached bitmap") + windowedBitmap = cached + windowedCanvas = Canvas(windowedBitmap) + } ?: run { + log.i("PageView.init: creating new bitmap") + recreateCanvas() + pageDataManager.cacheBitmap(currentPageId, windowedBitmap) + } + + coroutineScope.launch(Dispatchers.Main) { + // If we do it with main.immediate then it wont work. + CanvasEventBus.refreshUiImmediately.emit(Unit) } - CanvasEventBus.refreshUiImmediately.emit(Unit) loadPage() log.d("Page loaded (Init with id: $currentPageId)") - PageDataManager.collectAndPersistBitmapsBatch(context, coroutineScope) + pageDataManager.collectAndPersistBitmapsBatch(context, coroutineScope) } } @@ -202,7 +187,7 @@ class PageView( * 1. Saves the state of the old page, including persisting its bitmap representation to disk. * 2. Updates the internal `currentPageId` to `newPageId`. * 3. Fetches the new page's data from the repository. - * 4. Updates the `PageDataManager` to the new page context. + * 4. Updates the `pageDataManager` to the new page context. * 5. Restores the zoom level for the new page. * 6. Attempts to load a cached bitmap for the new page. If a cached bitmap exists and its dimensions * match the current view, it's used directly. If dimensions differ, the canvas is resized. @@ -213,21 +198,14 @@ class PageView( */ fun changePage(newPageId: String) { val oldId = currentPageId - currentPageId = newPageId - coroutineScope.launch { - PageDataManager.updateOnExit(oldId) + coroutineScope.launch(Dispatchers.IO) { + pageDataManager.updateOnExit(oldId) persistBitmapDebounced(oldId) } - coroutineScope.launch { - withContext(Dispatchers.IO) { - pageFromDb = appRepository.pageRepository.getById(currentPageId) - pageFromDb?.notebookId?.let { notebookId -> - currentPageNumberVal = appRepository.getPageNumber(notebookId, currentPageId) - } - } - PageDataManager.setPage(newPageId) - zoomLevel.value = PageDataManager.getPageZoom(currentPageId) - PageDataManager.getCachedBitmap(newPageId)?.let { cached -> + coroutineScope.launch(Dispatchers.IO) { + pageDataManager.setPage(newPageId) + zoomLevel.value = pageDataManager.getPageZoom(currentPageId) + pageDataManager.getCachedBitmap(newPageId)?.let { cached -> log.i("PageView: using cached bitmap") windowedBitmap = cached windowedCanvas = Canvas(windowedBitmap) @@ -237,7 +215,7 @@ class PageView( } ?: run { log.i("PageView.changePage: creating new bitmap") recreateCanvas() - PageDataManager.cacheBitmap(newPageId, windowedBitmap) + pageDataManager.cacheBitmap(newPageId, windowedBitmap) } log.d("New bitmap hash: ${windowedBitmap.hashCode()}, ID: $currentPageId") @@ -263,7 +241,7 @@ class PageView( */ fun disposeOldPage() { log.d("Dispose old page") - PageDataManager.updateOnExit(currentPageId) + pageDataManager.updateOnExit(currentPageId) persistBitmapDebounced(currentPageId) cleanJob() } @@ -282,11 +260,10 @@ class PageView( windowedCanvas.scale(zoomLevel.value, zoomLevel.value) loadingJob = coroutineScope.launch(Dispatchers.IO) { try { - val bookId = pageFromDb?.notebookId snackManager.showSnackDuring(text = "Loading strokes...") { val timeToLoad = measureTimeMillis { - logCache.d("Start page, id $currentPageId") - PageDataManager.requestPageLoadJoin(appRepository, currentPageId, bookId) + logCache.d("Start page loading, id $currentPageId") + pageDataManager.requestCurrentPageLoadJoin() logCache.d("Got page data (PageView.loadPage). id $currentPageId") } logCache.d("All strokes loaded in $timeToLoad ms") @@ -296,20 +273,23 @@ class PageView( coroutineScope.launch(Dispatchers.Main.immediate) { CanvasEventBus.forceUpdate.emit(null) } +// sleep(5000) + logCache.d("Loaded page from persistent layer $currentPageId") - if (!PageDataManager.validatePageDataLoaded(currentPageId)) + if (!pageDataManager.validatePageDataLoaded(currentPageId)) logCache.e("Page should be loaded, but it is not. $currentPageId") coroutineScope.launch(Dispatchers.Default) { delay(10) - PageDataManager.reduceCache(20) - if (bookId.isNotNull()) - PageDataManager.cacheNeighbors(appRepository, currentPageId, bookId) + pageDataManager.reduceCache(20) + pageDataManager.cacheNeighbors() } +// sleep(10000) + } catch (_: CancellationException) { - val dataStatus = PageDataManager.validatePageDataLoaded(currentPageId) + val dataStatus = pageDataManager.validatePageDataLoaded(currentPageId) logCache.d("Page loading cancelled, data was loaded correctly: $dataStatus") } catch (e: Exception) { - val dataStatus = PageDataManager.validatePageDataLoaded(currentPageId) + val dataStatus = pageDataManager.validatePageDataLoaded(currentPageId) logCache.e("Page loading cancelled, data was loaded correctly: $dataStatus", e) } } @@ -321,36 +301,35 @@ class PageView( updateHeightForChange(strokesToAdd) saveStrokesToPersistLayer(strokesToAdd) - PageDataManager.indexStrokes(coroutineScope, currentPageId) + pageDataManager.indexStrokes(coroutineScope, currentPageId) persistBitmapDebounced() } // Completely updates strokes fun updateStrokes(strokesToUpdate: List) { + // TODO: Clean it up, move some logic to pageDataManager val strokeUpdateById = strokesToUpdate.associateBy { it.id } strokes = strokes.map { stroke -> strokeUpdateById[stroke.id] ?: stroke } updateHeightForChange(strokesToUpdate) - coroutineScope.launch(Dispatchers.IO) { - appRepository.strokeRepository.update(strokesToUpdate) - } - PageDataManager.indexStrokes(coroutineScope, currentPageId) + pageDataManager.updateStrokesInDb(strokesToUpdate) + pageDataManager.indexStrokes(coroutineScope, currentPageId) persistBitmapDebounced() } fun removeStrokes(strokeIds: List) { strokes = strokes.filter { s -> !strokeIds.contains(s.id) } removeStrokesFromPersistLayer(strokeIds) - PageDataManager.indexStrokes(coroutineScope, currentPageId) - PageDataManager.recomputeHeight(currentPageId) + pageDataManager.indexStrokes(coroutineScope, currentPageId) + pageDataManager.recomputeHeight(currentPageId) persistBitmapDebounced() } fun getStrokes(strokeIds: List): List { - return PageDataManager.getStrokes(strokeIds, currentPageId) + return pageDataManager.getStrokes(strokeIds, currentPageId) } fun updateHeightForChange(strokesChanged: List) { @@ -360,28 +339,11 @@ class PageView( } } - private fun saveStrokesToPersistLayer(strokes: List) { - coroutineScope.launch(Dispatchers.IO) { - try { - dbStrokes.create(strokes) - } catch (_: SQLiteConstraintException) { - // There were some rare bugs when strokes weren't unique when inserting from history - // I'm not sure if it's still a problem, let's just show the message - logAndShowError( - "saveStrokesToPersistLayer", - "Attempted to create strokes that already exist" - ) - dbStrokes.update(strokes) - } - } - } + private fun saveStrokesToPersistLayer(strokes: List) = + pageDataManager.saveStrokesToDb(strokes) - private fun saveImagesToPersistLayer(image: List) { - coroutineScope.launch(Dispatchers.IO) { - dbImages.create(image) - } - } + private fun saveImagesToPersistLayer(image: List) = pageDataManager.saveImagesToDb(image) fun addImage(imageToAdd: Image) { images += listOf(imageToAdd) @@ -389,7 +351,7 @@ class PageView( if (bottomPlusPadding > height) height = bottomPlusPadding saveImagesToPersistLayer(listOf(imageToAdd)) - PageDataManager.indexImages(coroutineScope, currentPageId) + pageDataManager.indexImages(coroutineScope, currentPageId) persistBitmapDebounced() } @@ -401,7 +363,7 @@ class PageView( if (bottomPlusPadding > height) height = bottomPlusPadding } saveImagesToPersistLayer(imageToAdd) - PageDataManager.indexImages(coroutineScope, currentPageId) + pageDataManager.indexImages(coroutineScope, currentPageId) persistBitmapDebounced() } @@ -409,28 +371,22 @@ class PageView( fun removeImages(imageIds: List) { images = images.filter { s -> !imageIds.contains(s.id) } removeImagesFromPersistLayer(imageIds) - PageDataManager.indexImages(coroutineScope, currentPageId) - PageDataManager.recomputeHeight(currentPageId) + pageDataManager.indexImages(coroutineScope, currentPageId) + pageDataManager.recomputeHeight(currentPageId) persistBitmapDebounced() } - fun getImage(imageId: String): Image? = PageDataManager.getImage(imageId, currentPageId) + fun getImage(imageId: String): Image? = pageDataManager.getImage(imageId, currentPageId) + fun getImages(imageIds: List): List = + pageDataManager.getImages(imageIds, currentPageId) - fun getImages(imageIds: List): List = PageDataManager.getImages(imageIds, currentPageId) + private fun removeStrokesFromPersistLayer(strokeIds: List) = + pageDataManager.removeStrokesFromDb(strokeIds) + private fun removeImagesFromPersistLayer(imageIds: List) = + pageDataManager.removeImagesFromDb(imageIds) - private fun removeStrokesFromPersistLayer(strokeIds: List) { - coroutineScope.launch(Dispatchers.IO) { - appRepository.strokeRepository.deleteAll(strokeIds) - } - } - - private fun removeImagesFromPersistLayer(imageIds: List) { - coroutineScope.launch(Dispatchers.IO) { - appRepository.imageRepository.deleteAll(imageIds) - } - } // load background, fast, if it is accurate enough. private fun loadInitialBitmap(): Boolean { @@ -447,12 +403,13 @@ class PageView( log.d("Drawing initial background.") // draw just background. - val backgroundType = pageFromDb?.getBackgroundType() - if (backgroundType == BackgroundType.Native) + val backgroundType = pageDataManager.getBackgroundType() + if (backgroundType == BackgroundType.Native) { + val bg = pageDataManager.getBackgroundName() drawBg( - context, windowedCanvas, backgroundType, - pageFromDb?.background ?: "blank", scroll, 1f, this + context, windowedCanvas, backgroundType, bg, scroll, 1f, this ) + } else windowedCanvas.drawColor(Color.WHITE) return false @@ -462,7 +419,7 @@ class PageView( private fun cleanJob() { //ensure that snack is canceled, even on dispose of the page. coroutineScope.launch(Dispatchers.IO) { - PageDataManager.cancelLoadingPage(pageId = currentPageId) + pageDataManager.cancelLoadingPage(pageId = currentPageId) } loadingJob?.cancel() if (loadingJob?.isActive == true) { @@ -658,18 +615,19 @@ class PageView( log.d("Redrawing full logical rect: $redrawRect") windowedCanvas.drawColor(Color.BLACK) - + val backgroundType = pageDataManager.getBackgroundType() ?: BackgroundType.Native + val bg = pageDataManager.getBackgroundName() drawBg( context, windowedCanvas, - pageFromDb?.getBackgroundType() ?: BackgroundType.Native, - pageFromDb?.background ?: "blank", + backgroundType, + bg, scroll, zoomLevel.value, this, redrawRect ) - PageDataManager.cacheBitmap(currentPageId, windowedBitmap) + pageDataManager.cacheBitmap(currentPageId, windowedBitmap) drawAreaScreenCoordinates(redrawRect) @@ -806,8 +764,7 @@ class PageView( // updates page setting in db, (for instance type of background) // and redraws page to view. suspend fun refreshCurrentPage() { - pageFromDb = appRepository.pageRepository.getById(currentPageId) - log.i("Refresh current page, background: ${pageFromDb?.background}") + pageDataManager.refreshPageFromDb() withContext(Dispatchers.Main) { drawAreaScreenCoordinates(Rect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)) persistBitmapDebounced() @@ -844,20 +801,12 @@ class PageView( coroutineScope.launch { // Make sure that persisting bitmap gets the newest possible bitmap // TODO: There might still be some nasty race conditions. - PageDataManager.cacheBitmap(currentPageId, windowedBitmap) - PageDataManager.saveTopic.emit(pageId) - } - } - - private fun saveToPersistLayer() { - coroutineScope.launch { - withContext(Dispatchers.IO) { - appRepository.pageRepository.updateScroll(currentPageId, scroll.y.toInt()) - pageFromDb = appRepository.pageRepository.getById(currentPageId) - } + pageDataManager.cacheBitmap(currentPageId, windowedBitmap) + pageDataManager.saveTopic.emit(pageId) } } + private fun saveToPersistLayer() = pageDataManager.setScrollInDb() fun applyZoom(point: IntOffset): IntOffset { return point * zoomLevel.value 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 a19af78b..68df327a 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 @@ -2,8 +2,6 @@ package com.ethran.notable.editor.canvas 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.editor.PageView import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History @@ -14,7 +12,6 @@ import com.ethran.notable.editor.utils.partialRefreshRegionOnce import com.ethran.notable.editor.utils.selectRectangle import com.ethran.notable.editor.utils.waitForEpdRefresh import com.onyx.android.sdk.extension.isNull -import io.shipbook.shipbooksdk.Log import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -26,7 +23,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class CanvasObserverRegistry( - private val appRepository: AppRepository, private val coroutineScope: CoroutineScope, private val drawCanvas: DrawCanvas, private val page: PageView, @@ -35,7 +31,9 @@ class CanvasObserverRegistry( private val inputHandler: OnyxInputHandler, private val refreshManager: CanvasRefreshManager ) { - private val logCanvasObserver = ShipBook.getLogger("CanvasObservers") + private val log = ShipBook.getLogger("CanvasObservers") + private val pageDataManager = page.pageDataManager + fun registerAll() { ImageHandler(drawCanvas.context, page, state, coroutineScope).observeImageUri() @@ -62,7 +60,7 @@ class CanvasObserverRegistry( private fun observeRefreshUiImmediately() { coroutineScope.launch { CanvasEventBus.refreshUiImmediately.collect { - logCanvasObserver.v("Refreshing UI!") + log.v("Refreshing UI!") val zoneToRedraw = Rect(0, 0, page.viewWidth, page.viewHeight) refreshManager.refreshUi(zoneToRedraw) } @@ -76,7 +74,7 @@ class CanvasObserverRegistry( coroutineScope.launch(Dispatchers.Main.immediate) { CanvasEventBus.forceUpdate.collect { dirtyRectangle -> // On loading, make sure that the loaded strokes are visible to it. - logCanvasObserver.v("Force update, zone: $dirtyRectangle, Strokes to draw: ${page.strokes.size}") + log.v("Force update, zone: $dirtyRectangle, Strokes to draw: ${page.strokes.size}") val zoneToRedraw = dirtyRectangle ?: Rect(0, 0, page.viewWidth, page.viewHeight) page.drawAreaScreenCoordinates(zoneToRedraw) launch(Dispatchers.Default) { @@ -92,7 +90,7 @@ class CanvasObserverRegistry( private fun observeRefreshUi() { coroutineScope.launch(Dispatchers.Default) { CanvasEventBus.refreshUi.collect { - logCanvasObserver.v("Refreshing UI!") + log.v("Refreshing UI!") refreshManager.refreshUiSuspend() } } @@ -101,7 +99,7 @@ class CanvasObserverRegistry( private fun observeFocusChange() { coroutineScope.launch { CanvasEventBus.onFocusChange.collect { hasFocus -> - logCanvasObserver.v("App has focus: $hasFocus") + log.v("App has focus: $hasFocus") if (hasFocus) { inputHandler.updatePenAndStroke() // The setting might been changed by other app. drawCanvas.drawCanvasToView(null) @@ -115,8 +113,8 @@ class CanvasObserverRegistry( private fun observeZoomLevel() { coroutineScope.launch { page.zoomLevel.drop(1).collect { - logCanvasObserver.v("zoom level change: ${page.zoomLevel.value}") - PageDataManager.setPageZoom(page.currentPageId, page.zoomLevel.value) + log.v("zoom level change: ${page.zoomLevel.value}") + pageDataManager.setPageZoom(page.currentPageId, page.zoomLevel.value) inputHandler.updatePenAndStroke() } } @@ -125,7 +123,7 @@ class CanvasObserverRegistry( private fun observeDrawingState() { coroutineScope.launch { CanvasEventBus.isDrawing.collect { - logCanvasObserver.v("drawing state changed to $it!") + log.v("drawing state changed to $it!") state.isDrawing = it } } @@ -135,7 +133,7 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.rectangleToSelectByGesture.drop(1).collect { if (it != null) { - logCanvasObserver.v("Area to Select (screen): $it") + log.v("Area to Select (screen): $it") selectRectangle(page, drawCanvas.coroutineScope, state, it) } } @@ -146,7 +144,7 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.clearPageSignal.collect { require(!state.isDrawing) { "Cannot clear page in drawing mode" } - logCanvasObserver.v("Clear page signal!") + log.v("Clear page signal!") cleanAllStrokes(page, history) } } @@ -155,7 +153,7 @@ class CanvasObserverRegistry( private fun observeRestartAfterConfChange() { coroutineScope.launch { CanvasEventBus.reinitSignal.collect { - logCanvasObserver.v("Configuration changed!") + log.v("Configuration changed!") drawCanvas.init() drawCanvas.drawCanvasToView(null) } @@ -172,23 +170,24 @@ class CanvasObserverRegistry( } private fun observePenChanges() { - coroutineScope.launch { + coroutineScope.launch(Dispatchers.Default) { snapshotFlow { state.pen }.drop(0).collect { - logCanvasObserver.v("pen change: ${state.pen}") + log.v("pen change: ${state.pen}") inputHandler.updatePenAndStroke() - refreshManager.refreshUiSuspend() + //I think we don't need to refresh the screen here. +// refreshManager.refreshUiSuspend() } } coroutineScope.launch { snapshotFlow { state.penSettings.toMap() }.drop(1).collect { - logCanvasObserver.v("pen settings change: ${state.penSettings}") + log.v("pen settings change: ${state.penSettings}") inputHandler.updatePenAndStroke() refreshManager.refreshUiSuspend() } } coroutineScope.launch { snapshotFlow { state.eraser }.drop(1).collect { - logCanvasObserver.v("eraser change: ${state.eraser}") + log.v("eraser change: ${state.eraser}") inputHandler.updatePenAndStroke() refreshManager.refreshUiSuspend() } @@ -198,7 +197,7 @@ class CanvasObserverRegistry( private fun observeIsDrawingSnapshot() { coroutineScope.launch { snapshotFlow { state.isDrawing }.drop(1).collect { - logCanvasObserver.v("isDrawing change to $it") + log.v("isDrawing change to $it") // We need to close all menus if (it) { CanvasEventBus.closeMenusSignal.emit(Unit) @@ -212,7 +211,7 @@ class CanvasObserverRegistry( private fun observeToolbar() { coroutineScope.launch { snapshotFlow { state.isToolbarOpen }.drop(1).collect { - logCanvasObserver.v("istoolbaropen change: ${state.isToolbarOpen}") + log.v("istoolbaropen change: ${state.isToolbarOpen}") inputHandler.updateActiveSurface() inputHandler.updatePenAndStroke() refreshManager.refreshUi(null) @@ -223,7 +222,7 @@ class CanvasObserverRegistry( private fun observeMode() { coroutineScope.launch { snapshotFlow { drawCanvas.getActualState().mode }.drop(1).collect { - logCanvasObserver.v("mode change: ${drawCanvas.getActualState().mode}") + log.v("mode change: ${drawCanvas.getActualState().mode}") inputHandler.updatePenAndStroke() refreshManager.refreshUiSuspend() } @@ -235,7 +234,7 @@ class CanvasObserverRegistry( coroutineScope.launch { // After 500ms add to history strokes CanvasEventBus.commitHistorySignal.debounce(500).collect { - logCanvasObserver.v("Commiting to history") + log.v("Commiting to history") drawCanvas.commitToHistory() } } @@ -252,8 +251,8 @@ class CanvasObserverRegistry( coroutineScope.launch { CanvasEventBus.saveCurrent.collect { // Push current bitmap to persist layer so preview has something to load - PageDataManager.cacheBitmap(page.currentPageId, page.windowedBitmap) - PageDataManager.saveTopic.tryEmit(page.currentPageId) + pageDataManager.cacheBitmap(page.currentPageId, page.windowedBitmap) + pageDataManager.saveTopic.tryEmit(page.currentPageId) } } } @@ -262,9 +261,7 @@ class CanvasObserverRegistry( private fun observeQuickNav() { coroutineScope.launch { CanvasEventBus.previewPage.debounce(50).collectLatest { pageId -> - val pageNumber = - appRepository.getPageNumber(page.pageFromDb?.notebookId!!, pageId) - Log.d("QuickNav", "Previewing page($pageNumber): $pageId") + val pageNumber = pageDataManager.getPageNumberInCurrentNotebook(pageId) val previewBitmap = withContext(Dispatchers.IO) { loadPreview( @@ -277,7 +274,7 @@ class CanvasObserverRegistry( } if (previewBitmap.isRecycled) { - Log.e("QuickNav", "Failed to preview page for $pageId, skipping draw") + log.e("Failed to preview page for $pageId, skipping draw") return@collectLatest } @@ -290,8 +287,10 @@ class CanvasObserverRegistry( private fun observeRestoreCanvas() { coroutineScope.launch { CanvasEventBus.restoreCanvas.collect { + log.d("Restoring canvas") val zoneToRedraw = Rect(0, 0, page.viewWidth, page.viewHeight) drawCanvas.restoreCanvas(zoneToRedraw) + log.v("Restored canvas") } } } 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 88260440..e06d9960 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 @@ -6,10 +6,10 @@ import android.graphics.Paint import android.graphics.Rect import android.os.Looper import com.ethran.notable.data.model.SimplePointF -import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.selectPaint import com.ethran.notable.editor.state.EditorState +import com.ethran.notable.editor.state.Mode import com.ethran.notable.editor.utils.pointsToPath import com.ethran.notable.editor.utils.refreshScreenRegion import com.ethran.notable.editor.utils.resetScreenFreeze @@ -31,7 +31,8 @@ class CanvasRefreshManager( // post what page drawn to visible surface drawCanvasToView(dirtyRect) - if (CanvasEventBus.drawingInProgress.isLocked) log.w("Drawing is still in progress there might be a bug.") + if (CanvasEventBus.drawingInProgress.isLocked) + log.w("Drawing is still in progress there might be a bug.") // Use only if you have confidence that there are no strokes being drawn at the moment if (!state.isDrawing) { @@ -40,6 +41,7 @@ class CanvasRefreshManager( } // reset screen freeze resetScreenFreeze(touchHelper) + log.d("refreshUi: done") } suspend fun refreshUiSuspend() { @@ -66,6 +68,8 @@ class CanvasRefreshManager( fun drawCanvasToView(dirtyRect: Rect?) { + log.v("Canvas refresh started, dirtyRect: $dirtyRect") + val zoneToRedraw = dirtyRect ?: Rect(0, 0, page.viewWidth, page.viewHeight) var canvas: Canvas? = null try { @@ -94,6 +98,7 @@ class CanvasRefreshManager( if (canvas != null) { drawCanvas.holder.unlockCanvasAndPost(canvas) } + log.v("Canvas refreshed") } catch (e: IllegalStateException) { 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 75cae6ab..2c13d48e 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 @@ -37,7 +37,6 @@ var referencedSurfaceView: String = "" @SuppressLint("ViewConstructor") // we never execute constructor from XML class DrawCanvas( context: Context, - appRepository: AppRepository, val coroutineScope: CoroutineScope, val state: EditorState, val page: PageView, @@ -90,7 +89,6 @@ class DrawCanvas( private val observers = CanvasObserverRegistry( - appRepository, coroutineScope, this, page, state, history, inputHandler, refreshManager ) @@ -98,7 +96,6 @@ class DrawCanvas( fun init() { log.i("Initializing Canvas") - glRenderer.release() glRenderer = OpenGLRenderer(this@DrawCanvas) glRenderer.attachSurfaceView(this) diff --git a/app/src/main/java/com/ethran/notable/editor/drawing/backgrounds.kt b/app/src/main/java/com/ethran/notable/editor/drawing/backgrounds.kt index 5ab4b688..c98af775 100644 --- a/app/src/main/java/com/ethran/notable/editor/drawing/backgrounds.kt +++ b/app/src/main/java/com/ethran/notable/editor/drawing/backgrounds.kt @@ -34,7 +34,7 @@ import kotlin.math.min import kotlin.math.sin import kotlin.math.sqrt -private val backgroundsLog = ShipBook.getLogger("BackgroundsLog") +private val log = ShipBook.getLogger("BackgroundsLog") const val padding = 0 const val lineHeight = 80 @@ -213,10 +213,10 @@ fun drawBackgroundImages( if (imageBitmap != null) { drawBitmapToCanvas(canvas, imageBitmap, scroll, scale, repeat) } else { - backgroundsLog.e("Failed to load image from $backgroundImage") + log.e("Failed to load image from $backgroundImage") } } catch (e: Exception) { - backgroundsLog.e("Error loading background image: ${e.message}", e) + log.e("Error loading background image: ${e.message}", e) } } @@ -266,7 +266,7 @@ fun drawPdfPage( scale: Float = 1.0f ) { if (pageNumber < 0) { - backgroundsLog.e("Page number should not be ${pageNumber}, uri: $pdfUriString") + log.e("Page number should not be ${pageNumber}, uri: $pdfUriString") logCallStack("DrawPdfPage") return } @@ -284,7 +284,7 @@ fun drawPdfPage( } } catch (e: Exception) { - backgroundsLog.e("drawPdfPage: Failed to render PDF", e) + log.e("drawPdfPage: Failed to render PDF", e) } } @@ -351,6 +351,7 @@ fun drawBg( page: PageView? = null, clipRect: Rect? = null // before the scaling ) { + log.v("Loading the background") clipRect?.let { canvas.save() canvas.clipRect(scaleRect(it, scale)) @@ -376,7 +377,7 @@ fun drawBg( canvas, background, pageNumber, scroll, page, scale ) else { - backgroundsLog.w("Page number $pageNumber is out of bounds") + log.w("Page number $pageNumber is out of bounds") canvas.drawColor(Color.WHITE) } } @@ -452,7 +453,7 @@ fun drawPaginationLine(canvas: Canvas, scroll: Offset, scale: Float) { "Subpage ${pageNum + 1}", 20f - scroll.x, yPosScaled + 30f, textPaint ) } else { - backgroundsLog.d("Skipping line at $yPos (above visible area)") + log.d("Skipping line at $yPos (above visible area)") } yPos += pageHeight pageNum++ 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 72fa62d9..7b70041c 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 @@ -15,10 +15,9 @@ import androidx.core.graphics.withClip import androidx.core.net.toUri import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.data.db.Image -import com.ethran.notable.data.db.getBackgroundType import com.ethran.notable.data.model.BackgroundType -import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.utils.imageBounds import com.ethran.notable.editor.utils.plus import com.ethran.notable.editor.utils.strokeBounds @@ -131,8 +130,8 @@ fun drawOnCanvasFromPage( ignoredImageIds: List = listOf(), ) { val zoomLevel = page.zoomLevel.value - val backgroundType = page.pageFromDb?.getBackgroundType() ?: BackgroundType.Native - val background = page.pageFromDb?.background ?: "blank" + val backgroundType = page.pageDataManager.getBackgroundType() ?: BackgroundType.Native + val background = page.pageDataManager.getBackgroundName() pageDrawingLog.d("drawOnCanvasFromPage, zoom: $zoomLevel, background: $background, type: $backgroundType") // Canvas is scaled, it will scale page area. diff --git a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt index 66b4211b..6849d838 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/EditorSurface.kt @@ -16,7 +16,6 @@ private val log = ShipBook.getLogger("EditorSurface") @Composable fun EditorSurface( - appRepository: AppRepository, state: EditorState, page: PageView, history: History @@ -28,7 +27,6 @@ fun EditorSurface( factory = { ctx -> DrawCanvas( context = ctx, - appRepository = appRepository, coroutineScope = coroutineScope, state = state, page = page, diff --git a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt index 8448623a..0c10346f 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/Select.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/Select.kt @@ -7,7 +7,6 @@ import android.graphics.RectF import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.toOffset import androidx.core.graphics.createBitmap -import com.ethran.notable.data.PageDataManager import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke import com.ethran.notable.data.model.SimplePointF @@ -257,9 +256,9 @@ suspend fun selectRectangle(page: PageView, coroutineScope: CoroutineScope, stat val inPageCoordinates = toPageCoordinates(rectToSelect, page.zoomLevel.value, page.scroll) val imagesToSelect = - PageDataManager.getImagesInRectangle(inPageCoordinates, page.currentPageId) + page.pageDataManager.getImagesInRectangle(inPageCoordinates, page.currentPageId) val strokesToSelect = - PageDataManager.getStrokesInRectangle(inPageCoordinates, page.currentPageId) + page.pageDataManager.getStrokesInRectangle(inPageCoordinates, page.currentPageId) if (imagesToSelect != null && strokesToSelect != null) { CanvasEventBus.rectangleToSelectByGesture.value = null if (imagesToSelect.isNotEmpty() || strokesToSelect.isNotEmpty()) { 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 262d2a2e..35d0a7e8 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 @@ -325,7 +325,11 @@ fun partialRefreshRegionOnce(view: View, dirtyRect: Rect, touchHelper: TouchHelp } fun resetScreenFreeze(touchHelper: TouchHelper?, view: View? = null) { - if(touchHelper == null) return + if(touchHelper == null) + { + log.e("touchHelper is null") + return + } touchHelper.isRawDrawingRenderEnabled = false touchHelper.isRawDrawingRenderEnabled = true // setRawDrawingEnabled(false) diff --git a/app/src/main/java/com/ethran/notable/editor/utils/persistBitmap.kt b/app/src/main/java/com/ethran/notable/editor/utils/persistBitmap.kt index b5d535e8..6582612e 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/persistBitmap.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/persistBitmap.kt @@ -266,6 +266,13 @@ private fun decodePreview(file: File, expectedNameForLog: String): Bitmap? { imgBitmap } else { log.w("loadPersistBitmap: failed to decode bitmap from ${file.name}") + log.d( + """ + exists=${file.exists()} + size=${file.length()} + name=${'$'}{file.name} + """.trimIndent() + ) null } } catch (e: Exception) { diff --git a/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt b/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt index be732c5d..959a5304 100644 --- a/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt +++ b/app/src/main/java/com/ethran/notable/navigation/NotableNavHost.kt @@ -11,7 +11,6 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.editor.EditorDestination import com.ethran.notable.editor.EditorView import com.ethran.notable.io.ExportEngine @@ -27,13 +26,14 @@ import com.ethran.notable.ui.views.SystemInformationDestination import com.ethran.notable.ui.views.SystemInformationView import com.ethran.notable.ui.views.WelcomeDestination import com.ethran.notable.ui.views.WelcomeView +import io.shipbook.shipbooksdk.ShipBook +private val log = ShipBook.getLogger("NotableNavHost") @Composable fun NotableNavHost( exportEngine: ExportEngine, appRepository: AppRepository, - editorSettingCacheManager: EditorSettingCacheManager, modifier: Modifier = Modifier, appNavigator: NotableNavigator ) { @@ -99,14 +99,14 @@ fun NotableNavHost( val currentPageId = appNavigator.resolveAndSyncPageId(backStackEntry) EditorView( - exportEngine = exportEngine, - editorSettingCacheManager = editorSettingCacheManager, - appRepository = appRepository, - navController = appNavigator.navController, + goToLibrary = {appNavigator.goToLibrary(it)}, + goToPages = { bookId -> appNavigator.goToPages(bookId) }, + goToBugReport = { appNavigator.goToBugReport() }, bookId = bookId, - pageId = currentPageId, + initialPageId = currentPageId, isQuickNavOpen = appNavigator.isQuickNavOpen, onPageChange = { newPageId -> + log.d("onPageChange: $newPageId") appNavigator.onPageChange( backStackEntry, newPageId diff --git a/app/src/main/java/com/ethran/notable/navigation/NotableNavigator.kt b/app/src/main/java/com/ethran/notable/navigation/NotableNavigator.kt index 133de57b..93634838 100644 --- a/app/src/main/java/com/ethran/notable/navigation/NotableNavigator.kt +++ b/app/src/main/java/com/ethran/notable/navigation/NotableNavigator.kt @@ -16,7 +16,9 @@ import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.EditorDestination import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.editor.utils.refreshScreen +import com.ethran.notable.ui.views.BugReportDestination import com.ethran.notable.ui.views.LibraryDestination +import com.ethran.notable.ui.views.PagesDestination import com.ethran.notable.ui.views.SystemInformationDestination import com.ethran.notable.ui.views.WelcomeDestination import com.ethran.notable.utils.hasFilePermission @@ -127,6 +129,14 @@ class NotableNavigator( navController.navigate(SystemInformationDestination.route) } + fun goToBugReport(){ + navController.navigate(BugReportDestination.route) + } + + fun goToPages(bookId: String) { + navController.navigate(PagesDestination.createRoute(bookId)) + } + fun goToPage(appRepository: AppRepository, pageId: String) { coroutineScope.launch { val bookId = runCatching { diff --git a/app/src/main/java/com/ethran/notable/ui/components/NotableApp.kt b/app/src/main/java/com/ethran/notable/ui/components/NotableApp.kt index fc98301b..9e4cc7b3 100644 --- a/app/src/main/java/com/ethran/notable/ui/components/NotableApp.kt +++ b/app/src/main/java/com/ethran/notable/ui/components/NotableApp.kt @@ -8,10 +8,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import com.ethran.notable.data.AppRepository -import com.ethran.notable.data.datastore.EditorSettingCacheManager import com.ethran.notable.gestures.quickNavGesture import com.ethran.notable.io.ExportEngine import com.ethran.notable.navigation.NotableNavHost @@ -23,7 +21,6 @@ import com.ethran.notable.ui.SnackState @Composable fun NotableApp( exportEngine: ExportEngine, - editorSettingCacheManager: EditorSettingCacheManager, snackState: SnackState, appRepository: AppRepository ) { @@ -36,7 +33,6 @@ fun NotableApp( ) { NotableNavHost( exportEngine = exportEngine, - editorSettingCacheManager = editorSettingCacheManager, appRepository = appRepository, appNavigator = appNavState ) diff --git a/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportGenerator.kt b/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportGenerator.kt index 36a53787..dd564652 100644 --- a/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportGenerator.kt +++ b/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportGenerator.kt @@ -1,4 +1,4 @@ -package com.ethran.notable.ui.views +package com.ethran.notable.ui.viewmodels import android.content.Context import android.content.Intent @@ -26,7 +26,8 @@ import kotlin.math.pow class BugReportGenerator( context: Context, selectedTags: Map, - includeLibrariesLogs: Boolean + includeLibrariesLogs: Boolean, + private val pageDataManager: PageDataManager ) { val deviceInfo: String = buildDeviceInfo(context) private val logs: String = getRecentLogs(selectedTags, includeLibrariesLogs) @@ -202,7 +203,7 @@ class BugReportGenerator( // Memory val maxHeap = runtime.maxMemory().toHumanReadable() val appUsed = (runtime.totalMemory() - runtime.freeMemory()).toHumanReadable() - val pageMemoryMB = PageDataManager.getUsedMemory() + val pageMemoryMB = pageDataManager.getUsedMemory() // Storage val dbDir = getDbDir() diff --git a/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportViewModel.kt b/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportViewModel.kt index ccc6f01d..3435f626 100644 --- a/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportViewModel.kt +++ b/app/src/main/java/com/ethran/notable/ui/viewmodels/BugReportViewModel.kt @@ -3,7 +3,8 @@ package com.ethran.notable.ui.viewmodels import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.ethran.notable.ui.views.BugReportGenerator +import com.ethran.notable.data.PageDataManager +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -11,6 +12,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject data class BugReportUiState( val description: String = "", @@ -26,8 +28,12 @@ data class BugReportUiState( val finalMarkdown: String = "" ) -// Using AndroidViewModel because we need the Context to read Battery, Storage, etc. -class BugReportViewModel(application: Application) : AndroidViewModel(application) { +// Using HiltViewModel to inject PageDataManager +@HiltViewModel +class BugReportViewModel @Inject constructor( + application: Application, + private val pageDataManager: PageDataManager +) : AndroidViewModel(application) { private val _uiState = MutableStateFlow( BugReportUiState( @@ -75,7 +81,7 @@ class BugReportViewModel(application: Application) : AndroidViewModel(applicatio val context = getApplication() // Generate the raw data (Logs, Device Info) - val generator = BugReportGenerator(context, state.selectedTags, state.includeLibrariesLogs) + val generator = BugReportGenerator(context, state.selectedTags, state.includeLibrariesLogs, pageDataManager) // Format the final markdown string needed for Clipboard/GitHub val markdown = generator.rapportMarkdown( diff --git a/app/src/main/java/com/ethran/notable/ui/viewmodels/LibraryViewModel.kt b/app/src/main/java/com/ethran/notable/ui/viewmodels/LibraryViewModel.kt index 1a2afcea..9ec43fc5 100644 --- a/app/src/main/java/com/ethran/notable/ui/viewmodels/LibraryViewModel.kt +++ b/app/src/main/java/com/ethran/notable/ui/viewmodels/LibraryViewModel.kt @@ -54,6 +54,7 @@ class LibraryViewModel @Inject constructor( val appRepository: AppRepository, val importEngine: ImportEngine, val exportEngine: ExportEngine, + val pageDataManager: PageDataManager, @param:ApplicationContext private val context: Context // Kept strictly for ImportEngine ) : ViewModel() { @@ -109,7 +110,7 @@ class LibraryViewModel @Inject constructor( } fun loadFolder(folderId: String?) { - PageDataManager.cancelLoadingPages() + pageDataManager.cancelLoadingPages() _folderId.value = folderId // Resolve breadcrumbs in background thread diff --git a/app/src/main/java/com/ethran/notable/ui/viewmodels/QuickNavViewModel.kt b/app/src/main/java/com/ethran/notable/ui/viewmodels/QuickNavViewModel.kt index 8477aa01..e9e0504e 100644 --- a/app/src/main/java/com/ethran/notable/ui/viewmodels/QuickNavViewModel.kt +++ b/app/src/main/java/com/ethran/notable/ui/viewmodels/QuickNavViewModel.kt @@ -11,6 +11,7 @@ import com.ethran.notable.editor.canvas.CanvasEventBus import com.ethran.notable.ui.SnackConf import com.ethran.notable.ui.SnackState import com.ethran.notable.ui.components.getFolderList +import io.shipbook.shipbooksdk.ShipBook import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -42,10 +43,11 @@ class QuickNavViewModel( private val pageRepository = appRepository.pageRepository private val bookRepository = appRepository.bookRepository private val kv = appRepository.kvProxy + private val log = ShipBook.getLogger("QuickNavViewModel") private val _uiState = MutableStateFlow(QuickNavUiState()) val uiState: StateFlow = _uiState.asStateFlow() - + private var lastScrubEndTargetPageId: String? = null // Initialize data when the ViewModel is created or when a new page is opened fun loadPageData(currentPageId: String?) { @@ -131,6 +133,7 @@ class QuickNavViewModel( // --- Scrubber Actions --- fun onScrubStart() { + lastScrubEndTargetPageId = null CanvasEventBus.saveCurrent.tryEmit(Unit) } @@ -142,11 +145,17 @@ class QuickNavViewModel( } fun onScrubEnd(index: Int) { + log.v("onScrubEnd: $index") CanvasEventBus.restoreCanvas.tryEmit(Unit) + val pageIds = _uiState.value.bookPageIds - if (index in pageIds.indices) { - CanvasEventBus.changePage.tryEmit(pageIds[index]) - } + val targetPageId = pageIds.getOrNull(index) ?: return + + // Gesture end callbacks can fire more than once; ignore repeated commit for same target. + if (targetPageId == lastScrubEndTargetPageId) return + lastScrubEndTargetPageId = targetPageId + + CanvasEventBus.changePage.tryEmit(targetPageId) } fun onReturnClick(quickNavSourcePageId: String?) {