* Add performance and stylus debugging logs

* Refactor and decouple UI models from `MainViewModel`

* Refactor library state management and projection logic

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Implement desktop shell using Compose Multiplatform

* Introduce ReaderEngine and enhance EPUB reader features in windows app

* Move core paginated reader logic to a Kotlin Multiplatform `shared` module and introduce experimental desktop support.

* Implement PDF rendering and text extraction for desktop using Pdfium

* Add `NonReaderScreens.kt` and UI dependencies

* Refactor and centralize library state management and models to improve cross-platform consistency

* Implement JSON persistence for desktop library and enhance library management features including shelf CRUD, tagging, and metadata editing

* Implement PDF annotation system and enhanced zoom controls for the desktop viewer

* Implement WebView-based EPUB rendering for desktop using CEF and embedded resources

* Optimize UI state projection, navigation state handling, and main screen pager performance

* Implement Bring Your Own Key (BYOK) support for AI features in OSS version

* Support Gemini-based Cloud TTS with BYOK support for OSS builds

* Refactor table cell image sizing in `PaginatedReader` and improve `MobiParser` native library loading and error handling.

* crash fixes

* Enhance navigation stability with lifecycle-aware safety checks and update `navigation-compose` to 2.9.6

* Implement dynamic bottom padding for the page info bar to account for device rounded corners

* Implement bidirectional jump history navigation and replace the jump-back pill with a dedicated `PdfJumpHistoryBar`

* Optimize PDF tiling performance and refine pan-and-fling gesture handling

* Implement customizable toolbars with drag-and-drop reordering and placement for PDF and EPUB readers

* Updated UI for customize toolbar

* Refine drag-and-drop reordering and section assignment for PDF and EPUB reader controls

* restructure PDF viewer UI component hierarchy to fix verifier crash

* Implement separate text dimming factors for light and dark themes

* Synchronize Pdfium access and improve resource lifecycle safety across Kotlin and native layers

* Enhance image alignment in paginated and EPUB readers through anchor detection and style-based positioning

* Centralize file type resolution logic and implement HTML sanitization during import

* Introduce vertical margin customization and configurable progress bar positioning

* texture support in epub reader

* Enhance TTS session management, progress tracking, and diagnostic logging

* Optimize library state projection and folder synchronization performance by refactoring collection lookups and refining metadata extraction logic.

* Refine TTS page mapping for PDF and overhaul TTS control UI

* Implement natural session completion logic in `TtsPlaybackManager` for cloud tts

* Replace Snackbar with `CustomTopBanner` for notifications in `PdfViewerScreen`

* Refine TTS playback continuity across PDF pages and improve state management for session transitions

* Implement global texture transparency and enhance textured theme support across PDF and EPUB readers.

* Update reader themes and improve texture rendering in page animations, EPUB UI, and immersive mode

* Add Support Project screen

* Optimize library performance via projection caching, batch database updates, and scoped folder synchronization.

* Enhance folder synchronization with fallback query mechanisms and refactor annotation sidecar importing logic

* Bump version to 1.0.47 (51)
This commit is contained in:
Aryan 2026-05-04 21:55:38 +05:30 committed by GitHub
parent f42de6b462
commit d7a9cae9e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
126 changed files with 15287 additions and 3154 deletions

View file

@ -19,7 +19,7 @@
*/
// EpubReaderScreen.kt
@file:OptIn(ExperimentalSerializationApi::class) @file:Suppress("VariableNeverRead",
"UnusedVariable", "Unused", "SimplifyBooleanWithConstants"
"UnusedVariable", "Unused", "SimplifyBooleanWithConstants", "KotlinConstantConditions"
)
package com.aryan.reader.epubreader
@ -35,6 +35,8 @@ import android.graphics.Bitmap
import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.view.RoundedCorner
import android.view.View
import android.webkit.WebView
import android.widget.Toast
import androidx.activity.compose.BackHandler
@ -119,9 +121,14 @@ 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.draw.drawBehind
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageShader
import androidx.compose.ui.graphics.ShaderBrush
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
@ -134,6 +141,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.content.edit
@ -158,12 +166,17 @@ import com.aryan.reader.SearchResult
import com.aryan.reader.SummarizationResult
import com.aryan.reader.SummaryCacheManager
import com.aryan.reader.TtsSettingsSheet
import com.aryan.reader.areReaderAiFeaturesEnabled
import com.aryan.reader.countWords
import com.aryan.reader.isByokCloudTtsAvailable
import com.aryan.reader.data.CustomFontEntity
import com.aryan.reader.epub.EpubBook
import com.aryan.reader.epub.hasReadableExtractedContent
import com.aryan.reader.fetchAiDefinition
import com.aryan.reader.loadCustomThemes
import com.aryan.reader.loadGlobalTextureTransparency
import com.aryan.reader.loadReaderThemeId
import com.aryan.reader.loadReaderTextureBitmap
import com.aryan.reader.paginatedreader.BookPaginator
import com.aryan.reader.paginatedreader.CfiUtils
import com.aryan.reader.paginatedreader.HeaderBlock
@ -180,11 +193,11 @@ import com.aryan.reader.paginatedreader.data.BookCacheDatabase
import com.aryan.reader.paginatedreader.semanticBlockModule
import com.aryan.reader.rememberSearchState
import com.aryan.reader.saveCustomThemes
import com.aryan.reader.saveGlobalTextureTransparency
import com.aryan.reader.saveReaderThemeId
import com.aryan.reader.tts.SpeakerSamplePlayer
import com.aryan.reader.tts.TtsPlaybackManager
import com.aryan.reader.tts.loadTtsMode
import com.aryan.reader.tts.rememberTtsController
import com.aryan.reader.tts.splitTextIntoChunks
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -218,12 +231,50 @@ private const val AUTO_SCROLL_LOCAL_MAX_PREFIX = "auto_scroll_local_max_"
private const val MUSICIAN_MODE_KEY = "musician_mode_enabled"
private const val KEEP_SCREEN_ON_KEY = "keep_screen_on_enabled"
private const val HIDDEN_TOOLS_KEY = "hidden_reader_tools"
private const val TOOL_ORDER_KEY = "reader_tool_order"
private const val BOTTOM_TOOLS_KEY = "reader_bottom_tools"
private const val TTS_LOCATE_REASON_INITIAL_RESTORE = "initial_restore"
private const val TTS_LOCATE_REASON_LIFECYCLE_RESUME = "lifecycle_resume"
private const val TTS_LOCATE_REASON_OVERLAY = "overlay"
private const val TAG_LINK_NAV = "LINK_NAV"
private fun View.bottomRoundedCornerRadiusPx(): Int {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return 0
val insets = rootWindowInsets ?: return 0
return max(
insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_LEFT)?.radius ?: 0,
insets.getRoundedCorner(RoundedCorner.POSITION_BOTTOM_RIGHT)?.radius ?: 0
)
}
@Composable
private fun rememberBottomRoundedCornerPadding(view: View): Dp {
val density = LocalDensity.current
val configuration = LocalConfiguration.current
var radiusPx by remember(view) { mutableIntStateOf(view.bottomRoundedCornerRadiusPx()) }
DisposableEffect(
view,
configuration.orientation,
configuration.screenWidthDp,
configuration.screenHeightDp
) {
val listener = View.OnLayoutChangeListener { updatedView, _, _, _, _, _, _, _, _ ->
radiusPx = updatedView.bottomRoundedCornerRadiusPx()
}
view.addOnLayoutChangeListener(listener)
radiusPx = view.bottomRoundedCornerRadiusPx()
onDispose {
view.removeOnLayoutChangeListener(listener)
}
}
return with(density) { radiusPx.toDp() }
}
private fun saveHiddenTools(context: Context, hiddenTools: Set<String>) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putStringSet(HIDDEN_TOOLS_KEY, hiddenTools) }
@ -234,6 +285,34 @@ private fun loadHiddenTools(context: Context): Set<String> {
return prefs.getStringSet(HIDDEN_TOOLS_KEY, emptySet()) ?: emptySet()
}
private fun saveToolOrder(context: Context, toolOrder: List<ReaderTool>) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putString(TOOL_ORDER_KEY, toolOrder.joinToString(",") { it.name }) }
}
private fun loadToolOrder(context: Context): List<ReaderTool> {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
val savedTools = prefs.getString(TOOL_ORDER_KEY, null)
?.split(',')
?.filter { it.isNotBlank() }
?.mapNotNull { name -> ReaderTool.entries.firstOrNull { it.name == name } }
.orEmpty()
return (savedTools + ReaderTool.entries.filterNot { it in savedTools }).distinct()
}
private fun saveBottomTools(context: Context, bottomTools: Set<String>) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putStringSet(BOTTOM_TOOLS_KEY, bottomTools) }
}
private fun loadBottomTools(context: Context): Set<String> {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getStringSet(
BOTTOM_TOOLS_KEY,
ReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
) ?: ReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
}
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(KEEP_SCREEN_ON_KEY, isEnabled) }
@ -338,7 +417,7 @@ private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package"
private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
private fun loadUseOnlineDict(context: Context): Boolean {
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE) return false
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(PREF_USE_ONLINE_DICT, true)
}
@ -381,6 +460,7 @@ private fun saveExternalSearchPackage(context: Context, packageName: String) {
const val PREF_READER_THEME = "reader_theme_id"
const val PREF_CUSTOM_THEMES = "custom_themes_json"
@UnstableApi
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable
fun EpubReaderScreen(
@ -416,8 +496,8 @@ fun EpubReaderScreen(
}
} else null
val hasValidExtractionBasePath = remember(epubBook.extractionBasePath) {
epubBook.extractionBasePath.isNotBlank() && File(epubBook.extractionBasePath).exists()
val hasValidExtractionBasePath = remember(epubBook.extractionBasePath, epubBook.chapters) {
epubBook.hasReadableExtractedContent()
}
var requestedContentRecovery by remember(epubBook.extractionBasePath, uiState.selectedBookId) {
mutableStateOf(false)
@ -563,6 +643,7 @@ fun EpubReaderHost(
var systemUiMode by remember { mutableStateOf(loadSystemUiMode(context)) }
var pageInfoMode by remember { mutableStateOf(loadPageInfoMode(context)) }
var pageInfoPosition by remember { mutableStateOf(loadPageInfoPosition(context)) }
var pullToTurnEnabled by remember { mutableStateOf(loadPullToTurn(context)) }
var pullToTurnMultiplier by remember { mutableFloatStateOf(loadPullToTurnMultiplier(context)) }
var showVisualOptionsSheet by remember { mutableStateOf(false) }
@ -582,7 +663,7 @@ fun EpubReaderHost(
var currentTtsMode by remember {
mutableStateOf(
loadTtsMode(context).let {
if (BuildConfig.FLAVOR == "oss") TtsPlaybackManager.TtsMode.BASE else it
if (BuildConfig.FLAVOR == "oss" && !isByokCloudTtsAvailable(context)) TtsPlaybackManager.TtsMode.BASE else it
}
)
}
@ -738,18 +819,19 @@ fun EpubReaderHost(
}
var hiddenTools by remember { mutableStateOf(loadHiddenTools(context)) }
var toolOrder by remember { mutableStateOf(loadToolOrder(context)) }
var bottomTools by remember { mutableStateOf(loadBottomTools(context)) }
var showCustomizeToolsSheet by remember { mutableStateOf(false) }
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
@Suppress("KotlinConstantConditions") val onDictionaryLookup = { word: String ->
val isOss = BuildConfig.FLAVOR == "oss"
val effectiveUseOnline = !isOss && useOnlineDictionary
val effectiveUseOnline = areReaderAiFeaturesEnabled(context) && useOnlineDictionary
if (effectiveUseOnline) {
val wordCount = countWords(word)
if (wordCount > 1 && !isProUser) {
if (BuildConfig.FLAVOR != "oss" && wordCount > 1 && !isProUser) {
showDictionaryUpsellDialog = true
} else {
selectedTextForAi = word
@ -815,6 +897,8 @@ fun EpubReaderHost(
var lastKnownLocator by remember(initialLocator) { mutableStateOf(initialLocator) }
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val roundedCornerBottomPadding = rememberBottomRoundedCornerPadding(view)
val pageInfoCornerBottomPadding = roundedCornerBottomPadding.coerceAtMost(8.dp)
var bookmarks by remember(epubBook.title) {
mutableStateOf(
@ -991,6 +1075,7 @@ fun EpubReaderHost(
var currentParagraphGap by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.paragraphGap) }
var currentImageSize by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.imageSize) }
var currentHorizontalMargin by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.horizontalMargin) }
var currentVerticalMargin by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.verticalMargin) }
var currentTextAlign by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.textAlign) }
var currentFontFamily by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.font) }
var currentCustomFontPath by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.customPath) }
@ -1006,14 +1091,14 @@ fun EpubReaderHost(
var showFontSelectionSheet by remember { mutableStateOf(false) }
val fontSheetState = rememberModalBottomSheetState()
LaunchedEffect(currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) {
LaunchedEffect(currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentVerticalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) {
if (isFormatLocal) {
saveLocalReaderSettings(
context, bookId, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign
context, bookId, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentVerticalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign
)
} else {
saveReaderSettings(
context, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign
context, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentImageSize, currentHorizontalMargin, currentVerticalMargin, currentFontFamily, currentCustomFontPath, currentTextAlign
)
}
}
@ -1130,6 +1215,7 @@ fun EpubReaderHost(
var currentThemeId by remember { mutableStateOf(loadReaderThemeId(context)) }
var customThemes by remember { mutableStateOf(loadCustomThemes(context)) }
var globalTextureTransparency by remember { mutableFloatStateOf(loadGlobalTextureTransparency(context)) }
val activeTheme = remember(currentThemeId, customThemes) {
BuiltInThemes.find { it.id == currentThemeId }
@ -1151,6 +1237,19 @@ fun EpubReaderHost(
} else activeTheme.textColor
}
val activeTextureId = activeTheme.textureId
val activeTextureAlpha = 1f - globalTextureTransparency
val activeTextureBitmap = remember(activeTextureId) {
loadReaderTextureBitmap(context, activeTextureId)
}
val activeTextureModifier = activeTextureBitmap?.let { bitmap ->
Modifier.drawBehind {
drawRect(
brush = ShaderBrush(ImageShader(bitmap, TileMode.Repeated, TileMode.Repeated)),
blendMode = BlendMode.SrcOver,
alpha = activeTextureAlpha.coerceIn(0f, 1f)
)
}
} ?: Modifier
val infoBarBgColor = remember(effectiveBg, isDarkTheme) {
val overlayAlpha = if (isDarkTheme) 0.08f else 0.06f
@ -1401,7 +1500,7 @@ fun EpubReaderHost(
}
fun startTts() {
if (currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
if (BuildConfig.FLAVOR != "oss" && currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
showInsufficientCreditsDialog = true
return
}
@ -1441,6 +1540,7 @@ fun EpubReaderHost(
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
chapterIndex = chapterIndex,
totalChapters = chapters.size,
ttsMode = currentTtsMode,
playbackSource = "READER",
authToken = token
@ -1460,7 +1560,7 @@ fun EpubReaderHost(
)
fun startTtsFromSelectionPaginated(baseCfi: String, startOffset: Int) {
if (currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
if (BuildConfig.FLAVOR != "oss" && currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
showInsufficientCreditsDialog = true
return
}
@ -1504,6 +1604,7 @@ fun EpubReaderHost(
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
chapterIndex = chapterIndex,
totalChapters = chapters.size,
ttsMode = currentTtsMode,
playbackSource = "READER",
authToken = token
@ -1582,18 +1683,6 @@ fun EpubReaderHost(
focusRequester = searchFocusRequester
)
if (epubBook.extractionBasePath.isBlank() || !File(epubBook.extractionBasePath).exists()) {
Timber.e("Extraction base path is blank or does not exist: ${epubBook.extractionBasePath}"
)
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(
"Error: Book content not found. Path: ${epubBook.extractionBasePath}",
color = MaterialTheme.colorScheme.error
)
}
return
}
val totalPagesInCurrentChapter = remember(currentScrollHeightValue, currentClientHeightValue) {
if (currentClientHeightValue > 0) {
max(
@ -1989,10 +2078,7 @@ fun EpubReaderHost(
}
}
val pageInfoBottomPadding by animateDpAsState(
targetValue = if (showBars && pageInfoMode == PageInfoMode.SYNC) 45.dp else 0.dp,
label = "PageInfoBottomPadding"
)
val pageInfoBarHeight = PAGE_INFO_BAR_HEIGHT + pageInfoCornerBottomPadding
val isPageInfoVisible = when (pageInfoMode) {
PageInfoMode.DEFAULT -> !showBars
@ -2631,7 +2717,7 @@ fun EpubReaderHost(
}
val handleGenerateSummary: (Boolean) -> Unit = { force ->
if (!isProUser && credits <= 0) {
if (BuildConfig.FLAVOR != "oss" && !isProUser && credits <= 0) {
showInsufficientCreditsDialog = true
showAiHubSheet = false
} else {
@ -2688,6 +2774,7 @@ fun EpubReaderHost(
val finalSummaryBuilder = StringBuilder()
summarizeBookContent(
content = text,
context = context,
authToken = token,
onUsageReceived = { cost, freeRemaining ->
currentCost = cost
@ -2750,7 +2837,7 @@ fun EpubReaderHost(
}
val handleGenerateRecap: () -> Unit = {
if (credits <= 0) {
if (BuildConfig.FLAVOR != "oss" && credits <= 0) {
showInsufficientCreditsDialog = true
showAiHubSheet = false
} else {
@ -2820,6 +2907,7 @@ fun EpubReaderHost(
modifier = Modifier
.fillMaxSize()
.background(effectiveBg)
.then(activeTextureModifier)
.padding(top = effectiveTopPadding)
.focusRequester(containerFocusRequester)
.focusable()
@ -2879,13 +2967,15 @@ fun EpubReaderHost(
) {
when (currentRenderMode) {
RenderMode.VERTICAL_SCROLL -> {
val contentBottomPadding = if (pageInfoMode != PageInfoMode.HIDDEN) PAGE_INFO_BAR_HEIGHT else 0.dp
val pageInfoReserve = if (pageInfoMode != PageInfoMode.HIDDEN) pageInfoBarHeight else 0.dp
val contentTopPadding = if (pageInfoPosition == PageInfoPosition.TOP) pageInfoReserve else 0.dp
val contentBottomPadding = if (pageInfoPosition == PageInfoPosition.BOTTOM) pageInfoReserve else 0.dp
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = contentTopPadding)
.padding(bottom = contentBottomPadding)
.padding(top = 16.dp)
.testTag("ReaderContainer")
) {
if (chapters.isEmpty()) {
@ -3292,10 +3382,12 @@ fun EpubReaderHost(
currentParagraphGap = currentParagraphGap,
currentImageSize = currentImageSize,
currentHorizontalMargin = currentHorizontalMargin,
currentVerticalMargin = currentVerticalMargin,
currentFontFamily = currentFontFamily,
customFontPath = currentCustomFontPath,
currentTextAlign = currentTextAlign,
activeTextureId = activeTextureId,
activeTextureAlpha = activeTextureAlpha,
onHighlightClicked = {
lastHighlightClickTime = System.currentTimeMillis()
showBars = false
@ -3462,7 +3554,7 @@ fun EpubReaderHost(
if (ttsChunks.isNotEmpty()) {
logTtsChapterDiag("Vertical TTS extraction produced ${ttsChunks.size} chunks for chapter $targetChapterIndex")
if (currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
if (BuildConfig.FLAVOR != "oss" && currentTtsMode == TtsPlaybackManager.TtsMode.CLOUD && credits <= 0) {
showInsufficientCreditsDialog = true
ttsShouldStartOnChapterLoad = false
return@launch
@ -3483,6 +3575,7 @@ fun EpubReaderHost(
chapterTitle = chapterTitle,
coverImageUri = coverUriString,
chapterIndex = targetChapterIndex,
totalChapters = chapters.size,
ttsMode = currentTtsMode,
playbackSource = "READER",
authToken = token
@ -3528,6 +3621,7 @@ fun EpubReaderHost(
summarizeBookContent(
content = content,
context = context,
authToken = token,
onUsageReceived = { cost: Double?, freeRemaining: Int? ->
currentCost = cost
@ -3779,11 +3873,14 @@ fun EpubReaderHost(
}
RenderMode.PAGINATED -> {
val contentBottomPadding = if (pageInfoMode != PageInfoMode.HIDDEN) PAGE_INFO_BAR_HEIGHT else 0.dp
val pageInfoReserve = if (pageInfoMode != PageInfoMode.HIDDEN) pageInfoBarHeight else 0.dp
val contentTopPadding = if (pageInfoPosition == PageInfoPosition.TOP) pageInfoReserve else 0.dp
val contentBottomPadding = if (pageInfoPosition == PageInfoPosition.BOTTOM) pageInfoReserve else 0.dp
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.padding(top = contentTopPadding)
.padding(bottom = contentBottomPadding)
.testTag("ReaderContainer")
) {
@ -3799,6 +3896,7 @@ fun EpubReaderHost(
paragraphGapMultiplier = currentParagraphGap,
imageSizeMultiplier = currentImageSize,
horizontalMarginMultiplier = currentHorizontalMargin,
verticalMarginMultiplier = currentVerticalMargin,
fontFamily = activeFontFamily,
textAlign = currentTextAlign,
activeHighlightPalette = currentHighlightPalette,
@ -3810,6 +3908,7 @@ fun EpubReaderHost(
offset = ttsState.startOffsetInSource
).takeIf { ttsState.currentText != null && ttsState.sourceCfi != null && ttsState.startOffsetInSource != -1 },
activeTextureId = activeTextureId,
activeTextureAlpha = activeTextureAlpha,
initialChapterIndexInBook = lastKnownLocator?.chapterIndex,
modifier = Modifier.alpha(if (isPagerInitialized) 1f else 0f),
onPaginatorReady = { newPaginator ->
@ -4094,17 +4193,19 @@ fun EpubReaderHost(
// Page Info Bar (Vertical)
AnimatedVisibility(
visible = renderMode == RenderMode.VERTICAL_SCROLL && isPageInfoVisible,
visible = currentRenderMode == RenderMode.VERTICAL_SCROLL && isPageInfoVisible,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.BottomCenter)
modifier = Modifier.align(
if (pageInfoPosition == PageInfoPosition.TOP) Alignment.TopCenter else Alignment.BottomCenter
)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT)
.height(pageInfoBarHeight)
.background(infoBarBgColor)
.padding(bottom = bottomPadding + pageInfoBottomPadding)
.then(activeTextureModifier)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
@ -4140,17 +4241,19 @@ fun EpubReaderHost(
// Page Info Bar (Paginated)
AnimatedVisibility(
visible = renderMode == RenderMode.PAGINATED && paginator != null && isPageInfoVisible && paginatedPagerState.pageCount > 0,
visible = currentRenderMode == RenderMode.PAGINATED && paginator != null && isPageInfoVisible && paginatedPagerState.pageCount > 0,
enter = fadeIn(animationSpec = tween(200)),
exit = fadeOut(animationSpec = tween(200)),
modifier = Modifier.align(Alignment.BottomCenter)
modifier = Modifier.align(
if (pageInfoPosition == PageInfoPosition.TOP) Alignment.TopCenter else Alignment.BottomCenter
)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT)
.height(pageInfoBarHeight)
.background(infoBarBgColor)
.padding(bottom = bottomPadding + pageInfoBottomPadding)
.then(activeTextureModifier)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
) {
@ -4442,6 +4545,8 @@ fun EpubReaderHost(
volumeScrollEnabled = volumeScrollEnabled,
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
hiddenTools = hiddenTools,
toolOrder = toolOrder,
bottomTools = bottomTools,
onCustomizeTools = { showCustomizeToolsSheet = true },
onNavigateBack = { triggerSaveAndExit() },
isKeepScreenOn = isKeepScreenOn,
@ -4522,6 +4627,71 @@ fun EpubReaderHost(
onOpenDictionarySettings = { showDictionarySettingsSheet = true },
onOpenThemeSettings = { showThemePanel = true },
onOpenVisualOptions = { showVisualOptionsSheet = true },
onOpenAiHub = { showAiHubSheet = true },
onOpenSlider = {
when (currentRenderMode) {
RenderMode.VERTICAL_SCROLL -> {
sliderStartPage = currentPageInChapter
sliderCurrentPage = currentPageInChapter.toFloat()
isPageSliderVisible = true
showBars = false
scope.launch {
webViewRefForTts?.let { webView ->
startPageThumbnail = captureWebViewVisibleArea(webView)
}
}
}
RenderMode.PAGINATED -> {
if (paginatedPagerState.pageCount > 0) {
sliderStartPage = paginatedPagerState.currentPage + 1
sliderCurrentPage = (paginatedPagerState.currentPage + 1).toFloat()
isPageSliderVisible = true
showBars = false
startPageThumbnail = null
} else {
bannerMessage = BannerMessage("Book is not paginated yet.")
}
}
}
},
onOpenDrawer = {
scope.launch { drawerState.open() }
},
onToggleFormat = {
showFormatAdjustmentBars = !showFormatAdjustmentBars
if (showFormatAdjustmentBars) {
searchState.showSearchResultsPanel = false
isPageSliderVisible = false
}
},
onToggleSearch = {
searchState.isSearchActive = true
searchState.showSearchResultsPanel = true
showBars = true
showFormatAdjustmentBars = false
},
onToggleTts = {
if (isTtsSessionActive) {
Timber.d("TTS button clicked: Stopping TTS")
userStoppedTts = true
ttsController.stop()
} else {
when {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED -> {
startTts()
}
activity?.shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) == true -> {
showPermissionRationaleDialog = true
}
else -> {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}
},
onToggleReflow = if (onToggleReflow != null) {
{
val activeChapter = if (currentRenderMode == RenderMode.PAGINATED) {
@ -4687,8 +4857,12 @@ fun EpubReaderHost(
ttsState = ttsState,
isProUser = isProUser,
hiddenTools = hiddenTools,
toolOrder = toolOrder,
bottomTools = bottomTools,
currentTtsMode = currentTtsMode,
onOpenAiHub = { showAiHubSheet = true },
onOpenDictionarySettings = { showDictionarySettingsSheet = true },
onOpenThemeSettings = { showThemePanel = true },
onOpenSlider = {
when (currentRenderMode) {
RenderMode.VERTICAL_SCROLL -> {
@ -4770,6 +4944,8 @@ fun EpubReaderHost(
onImageSizeChange = { currentImageSize = it },
currentHorizontalMargin = currentHorizontalMargin,
onHorizontalMarginChange = { currentHorizontalMargin = it },
currentVerticalMargin = currentVerticalMargin,
onVerticalMarginChange = { currentVerticalMargin = it },
currentFont = currentFontFamily,
currentCustomFontName = if(currentCustomFontPath != null) {
customFonts.find { it.path == currentCustomFontPath }?.displayName ?: "Custom Font"
@ -4788,6 +4964,7 @@ fun EpubReaderHost(
currentParagraphGap = DEFAULT_PARAGRAPH_GAP_VAL
currentImageSize = DEFAULT_IMAGE_SIZE_VAL
currentHorizontalMargin = DEFAULT_HORIZONTAL_MARGIN_VAL
currentVerticalMargin = DEFAULT_VERTICAL_MARGIN_VAL
currentFontFamily = ReaderFont.ORIGINAL
currentCustomFontPath = null
currentTextAlign = ReaderTextAlign.DEFAULT
@ -5085,10 +5262,20 @@ fun EpubReaderHost(
if (showCustomizeToolsSheet) {
CustomizeToolsSheet(
hiddenTools = hiddenTools,
toolOrder = toolOrder,
bottomTools = bottomTools,
onUpdate = { newHiddenSet ->
hiddenTools = newHiddenSet
saveHiddenTools(context, newHiddenSet)
},
onOrderUpdate = { newOrder ->
toolOrder = newOrder
saveToolOrder(context, newOrder)
},
onPlacementUpdate = { newBottomTools ->
bottomTools = newBottomTools
saveBottomTools(context, newBottomTools)
},
onDismiss = { showCustomizeToolsSheet = false }
)
}
@ -5133,6 +5320,11 @@ fun EpubReaderHost(
pageInfoMode = it
savePageInfoMode(context, it)
},
pageInfoPosition = pageInfoPosition,
onPageInfoPositionChange = {
pageInfoPosition = it
savePageInfoPosition(context, it)
},
pullToTurnEnabled = pullToTurnEnabled,
onPullToTurnChange = {
pullToTurnEnabled = it
@ -5173,6 +5365,11 @@ fun EpubReaderHost(
ReaderThemePanel(
isVisible = true,
currentThemeId = currentThemeId,
globalTextureTransparency = globalTextureTransparency,
onGlobalTextureTransparencyChange = {
globalTextureTransparency = it
saveGlobalTextureTransparency(context, it)
},
onThemeSelected = {
currentThemeId = it
saveReaderThemeId(context, it)