Folder import\sync rework (#18)
* feat: rework Folder Sync architecture to separate Managed vs Linked books - Introduced "Managed" (imported to app storage) vs "Linked" (SAF URI) book logic. - Disabled Google Drive/Metadata synchronization for Folder-Linked books to respect privacy and storage. - Updated deletion logic: Folder books are now marked as deleted in the DB to blacklist them from future auto-scans, while Managed books are permanently purged from local and cloud storage. - Enhanced folder sync discoverability by adding a direct "Sync Folder" navigation button in Home screen. - Implemented reactive pager navigation in MainScreen and LibraryScreen via LaunchedEffect to allow programmatic tab switching from the ViewModel. * Implemented a local folder synchronization system to track and update book metadata across devices. Key changes: - Added `FolderBookMetadata` and `LocalSyncUtils` for serializing and managing metadata files in a `.episteme` directory. - Implemented bidirectional sync in `FolderSyncWorker` to reconcile local database state with folder-based metadata using timestamps and Syncthing conflict resolution. - Updated `MainViewModel` to trigger folder metadata sync when closing a book and verify for remote updates upon opening. - Enhanced folder management with manual scan support via `WorkManager` and automatic cleanup of database entries when a folder is disconnected. - Added `RecentFileDao` methods to support bulk deletion and prefix-based file lookups. * Removed API 34 restriction for chapter processing and updated local folder sync directory name * Increased TTS timeouts, added edge-to-edge support in MainScreen, and improved TtsChunk mapping safety * Improved folder synchronization and metadata management. - Added metadata-only sync option and triggered it on app start. - Implemented hidden metadata filenames (prefixed with `.`) to reduce file clutter. - Added automatic creation of `.nomedia` files in the sync directory. - Enhanced sync conflict resolution by picking the latest metadata and cleaning up obsolete or orphaned files. - Added "Sync Metadata" button to the Library screen. - Updated UI labels for local folder sync to clarify Google Drive integration. - Refactored `FolderSyncWorker` to support targeted metadata-only synchronization. * Implement folder sync migration and refactoring - Added a migration dialog to inform users about the folder sync refactor, where books are now read directly from external storage. - Updated `MainViewModel` to handle migration state and trigger a full scan upon completion or dismissal of the dialog. - Modified `FolderSyncWorker` to support legacy book matching during migration, updating file URIs and cleaning up internal app storage for migrated books. - Improved synchronization logic between local and remote metadata, including cleanup of orphaned metadata files. - Refined `RecentFileItem` updates during sync to preserve progress and bookmarks based on last modified timestamps. * Implemented pull-to-refresh for library sync and enhanced folder file deletion. - Added `isRefreshing` state to `MainViewModel` and integrated `PullToRefreshBox` in `HomeScreen`. - Implemented `refreshLibrary` to trigger cloud and folder metadata synchronization. - Updated deletion logic to physically remove files and metadata from synced local folders. - Added a warning to the delete confirmation dialog when removing folder-synced items. - Improved folder sync reliability by performing lazy cleanup of missing files during interaction and sync. - Fixed a bug where sync loading indicators would not retract on failure or cancellation. * Improved bookmark navigation and scroll synchronization in EPUB reader - Refined bookmark navigation logic for vertical scroll mode to handle chunk injection more reliably. - Added `isNavigatingToBookmark` state to show a loading overlay during long jumps. - Implemented `onScrollFinished` callback in `CfiJsBridge` to synchronize UI state with WebView scroll completion. - Updated `ChapterWebView` to use `rememberUpdatedState` for JavaScript bridge callbacks to ensure data consistency. - Improved `getCurrentCfi` and `scrollToCfi` in `epub_reader.js` with better visibility probing and retry logic for detached nodes. * Implemented background metadata extraction for folder sync. Key changes: - Added `MetadataExtractionWorker` to handle heavy metadata and cover extraction for files in the background. - Refactored `FolderSyncWorker` to perform fast file discovery using placeholders, enqueuing the metadata worker upon completion. - Added `getFolderBooksWithoutCovers` query to `RecentFileDao`. - Updated UI components (`EmptyState`, `MainViewModel`) to improve user feedback during sync and provide clear setup actions. - Enhanced `EmptyState` composable to support secondary actions and custom button text. * Updated Library Screen UI
This commit is contained in:
parent
60f6566b31
commit
a81df6921d
21 changed files with 3378 additions and 2478 deletions
|
|
@ -67,6 +67,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -174,11 +175,11 @@ class ContentBridge(
|
|||
@Suppress("unused")
|
||||
class CfiJsBridge(
|
||||
private val onCfiReady: (String) -> Unit,
|
||||
private val onCfiForBookmarkReady: (String) -> Unit
|
||||
private val onCfiForBookmarkReady: (String) -> Unit,
|
||||
private val onScrollFinishedCallback: (Boolean) -> Unit
|
||||
) {
|
||||
@JavascriptInterface
|
||||
fun onCfiExtracted(jsonResponse: String) {
|
||||
// This is called from JavaScript with the generated CFI and diagnostics
|
||||
try {
|
||||
val json = JSONObject(jsonResponse)
|
||||
val cfi = json.optString("cfi", "/4")
|
||||
|
|
@ -200,13 +201,12 @@ class CfiJsBridge(
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Error parsing CFI JSON response: $jsonResponse")
|
||||
// Still call back with a fallback CFI so the app doesn't hang
|
||||
onCfiReady("/4")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun onCfiForBookmarkExtracted(jsonResponse: String) {
|
||||
// This is called from JavaScript with the generated CFI for a bookmark action
|
||||
try {
|
||||
val json = JSONObject(jsonResponse)
|
||||
val cfi = json.optString("cfi")
|
||||
|
|
@ -230,6 +230,12 @@ class CfiJsBridge(
|
|||
Timber.e(e, "Error parsing CFI JSON for bookmark: $jsonResponse")
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun onScrollFinished(success: Boolean) {
|
||||
Timber.tag("BookmarkDiagnosis").d("JS reported scroll finished. Success: $success")
|
||||
onScrollFinishedCallback(success)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
|
|
@ -303,6 +309,7 @@ fun ChapterWebView(
|
|||
currentFontSize: Float,
|
||||
currentLineHeight: Float,
|
||||
onChapterInitiallyScrolled: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onTap: () -> Unit,
|
||||
onPotentialScroll: () -> Unit,
|
||||
onOverScrollTop: (dragAmount: Float) -> Unit,
|
||||
|
|
@ -314,9 +321,9 @@ fun ChapterWebView(
|
|||
onCfiGenerated: (cfi: String) -> Unit,
|
||||
onBookmarkCfiGenerated: (cfi: String) -> Unit,
|
||||
onSnippetForBookmarkReady: (cfi: String, snippet: String) -> Unit,
|
||||
onScrollFinished: (Boolean) -> Unit = {},
|
||||
ttsScope: CoroutineScope,
|
||||
tocFragments: List<String>,
|
||||
modifier: Modifier = Modifier,
|
||||
initialFragmentId: String? = null,
|
||||
onTtsTextReady: suspend (String) -> Unit,
|
||||
isProUser: Boolean,
|
||||
|
|
@ -347,6 +354,11 @@ fun ChapterWebView(
|
|||
|
||||
var showPaletteManager by remember { mutableStateOf(false) }
|
||||
|
||||
val currentOnSnippetForBookmarkReady by rememberUpdatedState(onSnippetForBookmarkReady)
|
||||
val currentOnCfiGenerated by rememberUpdatedState(onCfiGenerated)
|
||||
val currentOnBookmarkCfiGenerated by rememberUpdatedState(onBookmarkCfiGenerated)
|
||||
val currentOnScrollFinished by rememberUpdatedState(onScrollFinished)
|
||||
|
||||
LaunchedEffect(currentFontSize, currentLineHeight) {
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||
|
|
@ -499,6 +511,10 @@ fun ChapterWebView(
|
|||
consoleMessage?.let {
|
||||
val message = it.message()
|
||||
when {
|
||||
message.startsWith("BookmarkDiagnosis") -> {
|
||||
Timber.tag("BookmarkDiagnosis").d("JS -> ${message.substringAfter("BookmarkDiagnosis: ")}")
|
||||
}
|
||||
|
||||
message.startsWith("CFI_DIAGNOSIS:") -> {
|
||||
Timber.d(
|
||||
"JS -> ${message.substringAfter("CFI_DIAGNOSIS: ")}"
|
||||
|
|
@ -549,15 +565,17 @@ fun ChapterWebView(
|
|||
}
|
||||
addJavascriptInterface(
|
||||
CfiJsBridge(
|
||||
onCfiReady = { cfi -> onCfiGenerated(cfi) },
|
||||
onCfiForBookmarkReady = { cfi -> onBookmarkCfiGenerated(cfi) }
|
||||
), "CfiBridge")
|
||||
addJavascriptInterface(SnippetJsBridge { cfi, snippet ->
|
||||
onSnippetForBookmarkReady(
|
||||
cfi,
|
||||
snippet
|
||||
)
|
||||
}, "SnippetBridge")
|
||||
onCfiReady = { cfi -> currentOnCfiGenerated(cfi) },
|
||||
onCfiForBookmarkReady = { cfi -> currentOnBookmarkCfiGenerated(cfi) },
|
||||
onScrollFinishedCallback = { success -> currentOnScrollFinished(success) }
|
||||
), "CfiBridge"
|
||||
)
|
||||
|
||||
addJavascriptInterface(
|
||||
SnippetJsBridge { cfi, snippet ->
|
||||
currentOnSnippetForBookmarkReady(cfi, snippet)
|
||||
}, "SnippetBridge"
|
||||
)
|
||||
addJavascriptInterface(TtsJsBridge(ttsScope, onTtsTextReady), "TtsBridge")
|
||||
addJavascriptInterface(
|
||||
AiJsBridge(ttsScope, onContentReadyForSummarization),
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import android.graphics.Bitmap
|
|||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import timber.log.Timber
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
|
|
@ -77,10 +76,8 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
|
|
@ -103,6 +100,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
|
|
@ -117,6 +115,7 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
|
|
@ -131,7 +130,6 @@ import com.aryan.reader.RenderMode
|
|||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.SummarizationResult
|
||||
import com.aryan.reader.SummaryCacheManager
|
||||
import com.aryan.reader.SyncUpdateInfo
|
||||
import com.aryan.reader.countWords
|
||||
import com.aryan.reader.data.CustomFontEntity
|
||||
import com.aryan.reader.epub.EpubBook
|
||||
|
|
@ -165,14 +163,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
|
|||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import androidx.compose.ui.BiasAlignment
|
||||
import androidx.core.content.edit
|
||||
|
||||
private const val AUTO_SCROLL_LOCKED_KEY = "auto_scroll_locked"
|
||||
private const val AUTO_SCROLL_USE_SLIDER_KEY = "auto_scroll_use_slider"
|
||||
|
|
@ -206,8 +203,6 @@ fun EpubReaderScreen(
|
|||
initialCfi: String?,
|
||||
initialBookmarksJson: String?,
|
||||
isProUser: Boolean,
|
||||
pendingSyncUpdate: SyncUpdateInfo?,
|
||||
onClearPendingSyncUpdate: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
onSavePosition: (locator: Locator, cfiForWebView: String?, progress: Float) -> Unit,
|
||||
onBookmarksChanged: (bookmarksJson: String) -> Unit,
|
||||
|
|
@ -230,8 +225,6 @@ fun EpubReaderScreen(
|
|||
onNavigateToPro = onNavigateToPro,
|
||||
coverImagePath = coverImagePath,
|
||||
onRenderModeChange = onRenderModeChange,
|
||||
pendingSyncUpdate = pendingSyncUpdate,
|
||||
onClearPendingSyncUpdate = onClearPendingSyncUpdate,
|
||||
customFonts = customFonts,
|
||||
onImportFont = onImportFont
|
||||
)
|
||||
|
|
@ -249,8 +242,6 @@ fun EpubReaderHost(
|
|||
initialCfi: String?,
|
||||
initialBookmarksJson: String?,
|
||||
isProUser: Boolean,
|
||||
pendingSyncUpdate: SyncUpdateInfo?,
|
||||
onClearPendingSyncUpdate: () -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
onSavePosition: (locator: Locator, cfiForWebView: String?, progress: Float) -> Unit,
|
||||
onBookmarksChanged: (bookmarksJson: String) -> Unit,
|
||||
|
|
@ -269,6 +260,7 @@ fun EpubReaderHost(
|
|||
val focusManager = LocalFocusManager.current
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
val containerFocusRequester = remember { FocusRequester() }
|
||||
var isNavigatingToBookmark by remember { mutableStateOf(false) }
|
||||
|
||||
var isPageSliderVisible by remember { mutableStateOf(false) }
|
||||
var sliderCurrentPage by remember { mutableFloatStateOf(0f) }
|
||||
|
|
@ -559,62 +551,6 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingSyncUpdate) {
|
||||
if (pendingSyncUpdate != null) {
|
||||
val locator = pendingSyncUpdate.locator
|
||||
val message = if (locator != null) {
|
||||
val chapterTitle = chapters.getOrNull(locator.chapterIndex)?.title ?: "another location"
|
||||
"Newer reading position found in '$chapterTitle'. Sync now?"
|
||||
} else {
|
||||
"Bookmarks updated on another device. Sync now?"
|
||||
}
|
||||
|
||||
val result = withTimeoutOrNull(10_000L) {
|
||||
snackbarHostState.showSnackbar(
|
||||
message = message,
|
||||
actionLabel = "Sync",
|
||||
withDismissAction = true,
|
||||
duration = SnackbarDuration.Indefinite
|
||||
)
|
||||
}
|
||||
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
if (locator != null) {
|
||||
when (currentRenderMode) {
|
||||
RenderMode.VERTICAL_SCROLL -> {
|
||||
val cfi = locatorConverter.getCfiFromLocator(epubBook.title, locator)
|
||||
if (cfi != null) {
|
||||
val targetChunk = locator.blockIndex / 20
|
||||
if (currentChapterIndex != locator.chapterIndex) {
|
||||
chunkTargetOverride = targetChunk
|
||||
currentChapterIndex = locator.chapterIndex
|
||||
} else {
|
||||
if (targetChunk >= loadedChunkCount) {
|
||||
loadUpToChunkIndex = targetChunk
|
||||
}
|
||||
}
|
||||
cfiToLoad = cfi
|
||||
} else {
|
||||
Timber.w("Could not get CFI from locator for sync.")
|
||||
}
|
||||
}
|
||||
RenderMode.PAGINATED -> {
|
||||
(paginator as? BookPaginator)?.findPageForLocator(locator)?.let { page ->
|
||||
scope.launch {
|
||||
paginatedPagerState.scrollToPage(page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingSyncUpdate.bookmarksJson?.let { newBookmarksJson ->
|
||||
bookmarks = loadBookmarks(context, epubBook.title, chapters, newBookmarksJson)
|
||||
}
|
||||
}
|
||||
onClearPendingSyncUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(skipChapterRequest) {
|
||||
if (skipChapterRequest) {
|
||||
skipChapterRequest = false
|
||||
|
|
@ -1261,32 +1197,95 @@ fun EpubReaderHost(
|
|||
|
||||
when (currentRenderMode) {
|
||||
RenderMode.VERTICAL_SCROLL -> {
|
||||
Timber.tag("BookmarkDiagnosis").d("Navigating to ${bookmark.cfi}")
|
||||
cfiToLoad = bookmark.cfi
|
||||
val locator = locatorConverter.getLocatorFromCfi(epubBook, bookmark.chapterIndex, bookmark.cfi)
|
||||
val targetChunk = locator?.let { it.blockIndex / 20 }
|
||||
|
||||
// FIX: Try to extract chunk index directly from CFI for Vertical Mode
|
||||
// Vertical Mode CFIs are relative to content-container, so the first number
|
||||
// usually represents the chunk (2->Chunk0, 4->Chunk1, 6->Chunk2...)
|
||||
val directChunkIndex = try {
|
||||
val parts = bookmark.cfi.split('/').mapNotNull { it.toIntOrNull() }
|
||||
if (parts.isNotEmpty()) {
|
||||
val firstIndex = parts[0]
|
||||
// Standard EPUB CFI: indices are 1-based steps (2, 4, 6...)
|
||||
(firstIndex - 2) / 2
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val locator = if (directChunkIndex == null) {
|
||||
locatorConverter.getLocatorFromCfi(epubBook, bookmark.chapterIndex, bookmark.cfi)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val targetChunk = directChunkIndex ?: locator?.let { it.blockIndex / 20 }
|
||||
|
||||
if (bookmark.chapterIndex != currentChapterIndex) {
|
||||
if (targetChunk != null) {
|
||||
chunkTargetOverride = targetChunk
|
||||
chunkTargetOverride = if (targetChunk != null && targetChunk >= 0) {
|
||||
targetChunk
|
||||
} else {
|
||||
chunkTargetOverride = 0
|
||||
Timber.w("Could not get locator for bookmark CFI, will navigate to start of chapter.")
|
||||
0
|
||||
}
|
||||
currentChapterIndex = bookmark.chapterIndex
|
||||
} else {
|
||||
if (targetChunk != null) {
|
||||
}
|
||||
else {
|
||||
if (targetChunk != null && targetChunk >= 0) {
|
||||
isNavigatingToBookmark = true
|
||||
|
||||
// FIX: Ensure we don't reload if we already have it,
|
||||
// but do ensure the WebView has the content injected.
|
||||
if (targetChunk >= loadedChunkCount) {
|
||||
Timber.tag("BookmarkDiagnosis").d("Manual Chunk Injection: Loading from $loadedChunkCount to $targetChunk")
|
||||
|
||||
val chunksToInject = (loadedChunkCount..targetChunk)
|
||||
chunksToInject.forEach { idx ->
|
||||
val content = chapterChunks.getOrNull(idx)
|
||||
if (content != null) {
|
||||
val escaped = escapeJsString(content)
|
||||
webViewRefForTts?.evaluateJavascript(
|
||||
"javascript:window.virtualization.appendChunk($idx, '$escaped');",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
loadUpToChunkIndex = targetChunk
|
||||
loadedChunkCount = max(loadedChunkCount, targetChunk + 1)
|
||||
} else {
|
||||
webViewRefForTts?.evaluateJavascript("javascript:window.scrollToCfi('${escapeJsString(bookmark.cfi)}');", null)
|
||||
// Even if loadedChunkCount is high enough in Kotlin state,
|
||||
// ensure the specific chunk for the bookmark is actually in the DOM.
|
||||
// (Sometimes rapid jumps might leave gaps if logic was loose)
|
||||
val content = chapterChunks.getOrNull(targetChunk)
|
||||
if (content != null) {
|
||||
val escaped = escapeJsString(content)
|
||||
webViewRefForTts?.evaluateJavascript(
|
||||
"javascript:window.virtualization.appendChunk($targetChunk, '$escaped');",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
webViewRefForTts?.evaluateJavascript(
|
||||
"javascript:window.scrollToCfi('${escapeJsString(bookmark.cfi)}');",
|
||||
null
|
||||
)
|
||||
|
||||
scope.launch {
|
||||
delay(3000)
|
||||
if (isNavigatingToBookmark) {
|
||||
isNavigatingToBookmark = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.w("Could not get locator for bookmark CFI in current chapter, loading all chunks as fallback.")
|
||||
loadUpToChunkIndex = if (chapterChunks.isNotEmpty()) chapterChunks.size - 1 else 0
|
||||
// Fallback if we couldn't determine chunk
|
||||
webViewRefForTts?.evaluateJavascript(
|
||||
"javascript:window.scrollToCfi('${escapeJsString(bookmark.cfi)}');",
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RenderMode.PAGINATED -> {
|
||||
Timber.d("P-Mode Click: Navigating to bookmark. Chapter: ${bookmark.chapterIndex}, CFI: '${bookmark.cfi}'")
|
||||
val locator = locatorConverter.getLocatorFromCfi(
|
||||
|
|
@ -1588,7 +1587,7 @@ fun EpubReaderHost(
|
|||
"ControlFlowWithEmptyBody"
|
||||
)
|
||||
ChapterWebView(
|
||||
key = "$chapterKeyForWebView-$loadUpToChunkIndex",
|
||||
key = "$chapterKeyForWebView",
|
||||
chapterTitle = chapterToRender.title,
|
||||
isDarkTheme = isDarkTheme,
|
||||
initialScrollTarget = initialScrollTargetForChapter,
|
||||
|
|
@ -1795,6 +1794,10 @@ fun EpubReaderHost(
|
|||
null
|
||||
)
|
||||
},
|
||||
onScrollFinished = { success ->
|
||||
Timber.tag("BookmarkDiagnosis").d("Scroll finished callback. Success: $success")
|
||||
isNavigatingToBookmark = false
|
||||
},
|
||||
ttsScope = scope,
|
||||
onTtsTextReady = { jsonString ->
|
||||
scope.launch {
|
||||
|
|
@ -2959,6 +2962,26 @@ fun EpubReaderHost(
|
|||
isTtsSessionActive = isTtsSessionActive
|
||||
)
|
||||
|
||||
if (isNavigatingToBookmark) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.6f))
|
||||
.clickable(enabled = true) {},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = "Navigating to bookmark...",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showPermissionRationaleDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showPermissionRationaleDialog = false },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue