Epub reader themes (#97)

* Implemented a custom theme engine for the EPUB reader, allowing users to switch between built-in themes (System, Light, Dark, Sepia, Slate, and OLED).

Key changes include:
- Added `ReaderThemePanel` and theme selection logic to `EpubReaderScreen`.
- Introduced a theme persistence layer using SharedPreferences.
- Updated `ChapterWebView` and `epub_reader.js` to dynamically apply background and text colors via JavaScript, including improved contrast adjustment for inline styles.
- Enhanced `CssParser` and `ContentStyler` in the paginated reader to adapt CSS colors based on the active theme's background and text luminance.
- Integrated theme-specific background and text colors into `PaginatedReaderScreen` and its layout components.
- Added a theme palette icon to the reader controls.

* Added custom theme builder and texture support to EPUB reader

* Refactored color picker(PDF) into shared components and enhanced EPUB theme editor

* Updated theme customization UI and improved WebView theme application efficiency

* Updated page flip animation to use paper color with a theme-based tint

* Improved state restoration during paginated reader reconfiguration
This commit is contained in:
Aryan 2026-03-19 19:59:51 +05:30 committed by GitHub
parent eaf0d4af00
commit c9dc3ce8c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 1817 additions and 847 deletions

View file

@ -107,6 +107,8 @@ class BookPaginator(
private val density: Density,
private val fontFamilyMap: Map<String, FontFamily>,
private val isDarkTheme: Boolean,
private val themeBackgroundColor: Color,
private val themeTextColor: Color,
private val bookId: String,
private val initialChapterToPaginate: Int,
private val bookCss: Map<String, String>,
@ -127,7 +129,7 @@ class BookPaginator(
override var generation by mutableIntStateOf(0)
private set
override val pageShiftRequest = MutableSharedFlow<Int>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val pageShiftRequest = MutableSharedFlow<Int>(extraBufferCapacity = 100, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val currentUserChapterIndex = MutableStateFlow(initialChapterToPaginate)
internal val chapterPageCounts = ConcurrentHashMap<Int, Int>()
@ -404,6 +406,8 @@ class BookPaginator(
fontFamilyMap = fontFamilyMap,
density = density,
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor,
chapterAbsPath = chapter.absPath,
extractionBasePath = extractionBasePath,
userTextAlign = userTextAlign
@ -475,10 +479,10 @@ class BookPaginator(
val processedHtml = document.outerHtml()
var parsingCssRules = OptimizedCssRules()
val uaResult = CssParser.parse(cssContent = userAgentStylesheet, cssPath = null, baseFontSizeSp = textStyle.fontSize.value, density = density.density, constraints = constraints, isDarkTheme = false)
val uaResult = CssParser.parse(cssContent = userAgentStylesheet, cssPath = null, baseFontSizeSp = textStyle.fontSize.value, density = density.density, constraints = constraints, isDarkTheme = false, themeBackgroundColor = themeBackgroundColor, themeTextColor = themeTextColor)
parsingCssRules = parsingCssRules.merge(uaResult.rules)
bookCss.forEach { (path, content) ->
val bookCssResult = CssParser.parse(cssContent = content, cssPath = path, baseFontSizeSp = textStyle.fontSize.value, density = density.density, constraints = constraints, isDarkTheme = false)
val bookCssResult = CssParser.parse(cssContent = content, cssPath = path, baseFontSizeSp = textStyle.fontSize.value, density = density.density, constraints = constraints, isDarkTheme = false, themeBackgroundColor = themeBackgroundColor, themeTextColor = themeTextColor)
parsingCssRules = parsingCssRules.merge(bookCssResult.rules)
}
@ -950,8 +954,8 @@ class BookPaginator(
return@launch
}
val chapterStartPage = calculateAccurateStartIndex(targetChapterIndex)
val chapterPages = pageCache[targetChapterIndex] ?: paginateChapter(targetChapterIndex)
val chapterStartPage = calculateAccurateStartIndex(targetChapterIndex)
if (chapterPages == null) {
Timber.e("Href Navigation failed: Could not paginate target chapter $targetChapterIndex.")
@ -983,8 +987,8 @@ class BookPaginator(
val targetChapterIndex = result.locationInSource
Timber.i("Finding page for search result: '${result.query}' in chapter $targetChapterIndex")
val chapterStartPage = calculateAccurateStartIndex(targetChapterIndex)
val chapterPages = pageCache[targetChapterIndex] ?: paginateChapter(targetChapterIndex)
val chapterStartPage = calculateAccurateStartIndex(targetChapterIndex)
if (chapterPages == null) {
Timber.e("Search result navigation failed: Could not paginate target chapter $targetChapterIndex.")
@ -1055,8 +1059,8 @@ class BookPaginator(
val targetChapterIndex = locator.chapterIndex
Timber.i("Finding page for locator: Chapter $targetChapterIndex, Block ${locator.blockIndex}, Offset ${locator.charOffset}")
val chapterStartPage = chapterStartPageIndices[targetChapterIndex] ?: 0
val chapterPages = pageCache[targetChapterIndex] ?: paginateChapter(targetChapterIndex)
val chapterStartPage = chapterStartPageIndices[targetChapterIndex] ?: 0
if (chapterPages.isNullOrEmpty()) {
Timber.e("Locator navigation failed: Could not paginate target chapter $targetChapterIndex.")
@ -1068,6 +1072,8 @@ class BookPaginator(
for ((pageIndex, page) in chapterPages.withIndex()) {
for (block in page.content) {
if (block.blockIndex == locator.blockIndex) {
Timber.tag("ThemeReconfig").d("Block Index Match: Found block ${locator.blockIndex} on page $pageIndex of Chapter $targetChapterIndex")
if (fallbackPageInChapter == -1) {
fallbackPageInChapter = pageIndex
}
@ -1077,19 +1083,20 @@ class BookPaginator(
val startOffsetOnPage = textBlock.startCharOffsetInSource
val endOffsetOnPage = startOffsetOnPage + textBlock.content.length
if (locator.charOffset in startOffsetOnPage..<endOffsetOnPage) {
val isInside = locator.charOffset in startOffsetOnPage..<endOffsetOnPage
Timber.tag("ThemeReconfig").d("Offset Check: Target ${locator.charOffset} vs Range [$startOffsetOnPage, $endOffsetOnPage]. Inside: $isInside")
if (isInside) {
val finalPageIndex = chapterStartPage + pageIndex
Timber.i("Locator navigation SUCCEEDED with offset. Final page index: $finalPageIndex")
return finalPageIndex
}
} else {
val finalPageIndex = chapterStartPage + pageIndex
Timber.i("Locator navigation SUCCEEDED for non-text block. Final page index: $finalPageIndex")
return finalPageIndex
return chapterStartPage + pageIndex
}
}
}
}
Timber.tag("ThemeReconfig").e("Block Index NOT FOUND: Could not find block ${locator.blockIndex} in any page of Chapter $targetChapterIndex")
if (fallbackPageInChapter != -1) {
val finalPageIndex = chapterStartPage + fallbackPageInChapter
@ -1120,9 +1127,9 @@ class BookPaginator(
coroutineScope.launch(Dispatchers.IO) {
Timber.i("findPageForCfi: Starting search for CFI: '$cfi' in chapter: '$chapterIndex'")
val chapterPages = pageCache[chapterIndex] ?: paginateChapter(chapterIndex)
val chapterStartPage = calculateAccurateStartIndex(chapterIndex)
Timber.d("findPageForCfi: Chapter $chapterIndex starts at absolute page $chapterStartPage.")
val chapterPages = pageCache[chapterIndex] ?: paginateChapter(chapterIndex)
if (chapterPages == null) {
Timber.e("CFI Navigation failed: Could not paginate target chapter $chapterIndex.")

View file

@ -50,6 +50,8 @@ class ContentStyler(
private val fontFamilyMap: Map<String, FontFamily>,
private val density: Density,
private val isDarkTheme: Boolean,
private val themeBackgroundColor: Color,
private val themeTextColor: Color,
private val chapterAbsPath: String,
private val extractionBasePath: String,
private val userTextAlign: TextAlign?
@ -59,7 +61,6 @@ class ContentStyler(
return groupFloatingBlocks(semanticBlocks.mapNotNull { styleBlock(it) })
}
// ADD this function to group floating blocks, similar to the original parser
private fun groupFloatingBlocks(blocks: List<ContentBlock>): List<ContentBlock> {
if (blocks.isEmpty()) return emptyList()
@ -71,7 +72,6 @@ class ContentStyler(
val floatDirection = (currentBlock as? ImageBlock)?.style?.float
if (currentBlock is ImageBlock && floatDirection in listOf("left", "right")) {
val floatedImage = currentBlock
val paragraphsToWrap = mutableListOf<ParagraphBlock>()
while (processingQueue.isNotEmpty()) {
@ -86,11 +86,11 @@ class ContentStyler(
}
}
val wrappingBlock = WrappingContentBlock(
floatedImage,
currentBlock,
paragraphsToWrap,
elementId = floatedImage.elementId,
cfi = floatedImage.cfi,
blockIndex = floatedImage.blockIndex
elementId = currentBlock.elementId,
cfi = currentBlock.cfi,
blockIndex = currentBlock.blockIndex
)
result.add(wrappingBlock)
} else {
@ -208,7 +208,7 @@ class ContentStyler(
private fun applyThemeToStyle(style: CssStyle): CssStyle {
val newSpanStyle = style.spanStyle.let { original ->
val newColor = if (original.color.isSpecified) {
CssParser.adaptColorForTheme(original.color, isDarkTheme, isBackground = false)
CssParser.adaptColorForTheme(original.color, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor)
} else {
original.color
}
@ -217,14 +217,14 @@ class ContentStyler(
val newBlockStyle = style.blockStyle.let { original ->
val newBgColor = if (original.backgroundColor.isSpecified) {
CssParser.adaptColorForTheme(original.backgroundColor, isDarkTheme, isBackground = true)
CssParser.adaptColorForTheme(original.backgroundColor, isDarkTheme, isBackground = true, themeBackgroundColor, themeTextColor)
} else {
original.backgroundColor
}
fun themeBorder(b: BorderStyle?): BorderStyle? {
if (b == null) return null
val newColor = CssParser.adaptColorForTheme(b.color, isDarkTheme, isBackground = false)
val newColor = CssParser.adaptColorForTheme(b.color, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor)
return b.copy(color = newColor)
}
@ -474,7 +474,7 @@ class ContentStyler(
}
private fun toRoman(number: Int): String {
if (number < 1 || number > 3999) return number.toString()
if (number !in 1..3999) return number.toString()
val values = listOf(1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
val symbols = listOf("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I")
val result = StringBuilder()

View file

@ -19,9 +19,6 @@
*/
package com.aryan.reader.paginatedreader
import android.os.Build
import timber.log.Timber
import androidx.annotation.RequiresApi
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.toArgb
@ -38,6 +35,7 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import timber.log.Timber
import java.io.File
import java.util.regex.Pattern
import kotlin.math.roundToInt
@ -67,24 +65,64 @@ object CssParser {
"thick" to 5.dp
)
internal fun adaptColorForTheme(color: Color, isDarkTheme: Boolean, isBackground: Boolean): Color {
internal fun adaptColorForTheme(
color: Color,
isDarkTheme: Boolean,
isBackground: Boolean,
themeBackground: Color = Color.Unspecified,
themeText: Color = Color.Unspecified
): Color {
if (!color.isSpecified) return color
if (color.alpha < 0.9f) return color
if (color.alpha < 0.9f && color != Color.Transparent) return color
if (color == Color.Transparent) return color
val luminance = color.luminance()
return if (isDarkTheme) {
if (isBackground) {
if (luminance > 0.9) Color.Transparent else color
if (!themeBackground.isSpecified || !themeText.isSpecified) {
val luminance = color.luminance()
return if (isDarkTheme) {
if (isBackground) {
if (luminance > 0.9) Color.Transparent else color
} else {
if (luminance < 0.2) Color.White.copy(alpha = 0.87f) else color
}
} else {
if (luminance < 0.2) Color.White.copy(alpha = 0.87f) else color
if (isBackground) {
if (luminance < 0.1) Color.Transparent else color
} else {
if (luminance > 0.8) Color.Black.copy(alpha = 0.87f) else color
}
}
}
val bgLuminance = themeBackground.luminance()
val colorLuminance = color.luminance()
val l1 = maxOf(bgLuminance, colorLuminance)
val l2 = minOf(bgLuminance, colorLuminance)
val contrast = (l1 + 0.05f) / (l2 + 0.05f)
if (isBackground) {
return if (isDarkTheme && colorLuminance > 0.5f) {
Color.Transparent
} else if (!isDarkTheme && colorLuminance < 0.2f) {
Color.Transparent
} else {
color
}
} else {
if (isBackground) {
if (luminance < 0.1) Color.Transparent else color
} else {
if (luminance > 0.8) Color.Black.copy(alpha = 0.87f) else color
if (contrast >= 4.5f) {
return color
}
val hsl = FloatArray(3)
androidx.core.graphics.ColorUtils.colorToHSL(color.toArgb(), hsl)
if (bgLuminance < 0.5f) {
hsl[2] = hsl[2].coerceAtLeast(0.7f)
} else {
hsl[2] = hsl[2].coerceAtMost(0.3f)
}
return Color(androidx.core.graphics.ColorUtils.HSLToColor(hsl))
}
}
@ -138,7 +176,9 @@ object CssParser {
baseFontSizeSp: Float,
density: Float,
constraints: Constraints,
isDarkTheme: Boolean
isDarkTheme: Boolean,
themeBackgroundColor: Color = Color.Unspecified,
themeTextColor: Color = Color.Unspecified
): OptimizedCssParseResult {
val byTag = mutableMapOf<String, MutableList<CssRule>>()
val byClass = mutableMapOf<String, MutableList<CssRule>>()
@ -192,11 +232,11 @@ object CssParser {
val specificity = calculateSpecificity(originalSelector)
val normalStyle = parseProperties(
propertiesGroup, baseFontSizeSp, density, constraints, onlyImportant = false,
isDarkTheme
isDarkTheme, themeBackgroundColor, themeTextColor
)
val importantStyle = parseProperties(
propertiesGroup, baseFontSizeSp, density, constraints, onlyImportant = true,
isDarkTheme
isDarkTheme, themeBackgroundColor, themeTextColor
)
fun addRule(style: CssStyle, spec: Int) {
@ -335,7 +375,9 @@ object CssParser {
density: Float,
constraints: Constraints,
onlyImportant: Boolean,
isDarkTheme: Boolean
isDarkTheme: Boolean,
themeBackgroundColor: Color = Color.Unspecified,
themeTextColor: Color = Color.Unspecified
): CssStyle {
var spanStyle = SpanStyle()
var paragraphStyle = ParagraphStyle()
@ -427,7 +469,7 @@ object CssParser {
styleStr: String?
) {
val parsedWidth = widthStr?.let { parseCssSizeToDp(it, baseFontSizeSp, density, containerWidthPx) } ?: 0.dp
val parsedColor = colorStr?.let { parseColor(it) }?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false) }
val parsedColor = colorStr?.let { parseColor(it) }?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor) }
val isExplicitWidth = widthStr != null
@ -482,7 +524,7 @@ object CssParser {
}
"color" -> {
parseColor(value)?.let {
spanStyle = spanStyle.copy(color = this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false))
spanStyle = spanStyle.copy(color = this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor))
}
}
"text-align" -> {
@ -587,7 +629,7 @@ object CssParser {
"background-color" -> {
val originalColor = parseColor(value) ?: Color.Unspecified
backgroundColor = this@CssParser.adaptColorForTheme(originalColor, isDarkTheme, isBackground = true)
backgroundColor = this@CssParser.adaptColorForTheme(originalColor, isDarkTheme, isBackground = true, themeBackgroundColor, themeTextColor)
}
// Border Properties
@ -727,7 +769,7 @@ object CssParser {
textEmphasisStyleString = value
}
"text-emphasis-color", "-epub-text-emphasis-color" -> {
textEmphasisColor = parseColor(value)?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false) }
textEmphasisColor = parseColor(value)?.let { this@CssParser.adaptColorForTheme(it, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor) }
}
"text-emphasis-position", "-epub-text-emphasis-position" -> {
if (value in listOf("over", "under")) {
@ -785,7 +827,7 @@ object CssParser {
val finalStyle = style ?: "none"
val finalColor = color ?: spanStyle.color.takeIf { it.isSpecified } ?: Color.Black
val adaptedColor = this@CssParser.adaptColorForTheme(finalColor, isDarkTheme, isBackground = false)
val adaptedColor = this@CssParser.adaptColorForTheme(finalColor, isDarkTheme, isBackground = false, themeBackgroundColor, themeTextColor)
if (finalWidth > 0.dp && finalStyle != "none" && finalStyle != "hidden") {
return BorderStyle(finalWidth, adaptedColor, finalStyle)

View file

@ -121,6 +121,7 @@ import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
import androidx.compose.ui.res.imageResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.PlatformTextStyle
@ -490,6 +491,8 @@ private fun WrappingContentLayout(
fun PaginatedReaderScreen(
book: EpubBook,
isDarkTheme: Boolean,
effectiveBg: Color,
effectiveText: Color,
pagerState: PagerState,
isPageTurnAnimationEnabled: Boolean,
searchQuery: String,
@ -513,7 +516,8 @@ fun PaginatedReaderScreen(
onHighlightCreated: (String, String, String) -> Unit,
onHighlightDeleted: (String) -> Unit,
activeHighlightPalette: List<HighlightColor>,
onUpdatePalette: (Int, HighlightColor) -> Unit
onUpdatePalette: (Int, HighlightColor) -> Unit,
activeTextureId: String? = null
) {
LaunchedEffect(userHighlights) {
Timber.d("PaginatedReaderScreen: Received ${userHighlights.size} highlights.")
@ -522,10 +526,26 @@ fun PaginatedReaderScreen(
}
}
BoxWithConstraints(modifier = modifier.fillMaxSize()) {
val context = LocalContext.current
val textureBitmap = remember(activeTextureId) {
activeTextureId?.let { id ->
com.aryan.reader.epubreader.ReaderTexture.entries.find { it.id == id }?.resId?.let { resId ->
androidx.compose.ui.graphics.ImageBitmap.imageResource(context.resources, resId)
}
}
}
val textureModifier = if (textureBitmap != null) {
Modifier.drawBehind {
val brush = androidx.compose.ui.graphics.ShaderBrush(
androidx.compose.ui.graphics.ImageShader(textureBitmap, androidx.compose.ui.graphics.TileMode.Repeated, androidx.compose.ui.graphics.TileMode.Repeated)
)
drawRect(brush = brush, blendMode = BlendMode.Multiply, alpha = 0.6f)
}
} else Modifier
BoxWithConstraints(modifier = modifier.fillMaxSize().background(effectiveBg).then(textureModifier)) {
val textMeasurer = rememberTextMeasurer()
val textColor = if (isDarkTheme) MaterialTheme.colorScheme.onBackground
else MaterialTheme.colorScheme.onSurface
val baseTextStyle = MaterialTheme.typography.bodyLarge
var debouncedFontSizeMult by remember { mutableFloatStateOf(fontSizeMultiplier) }
@ -536,22 +556,36 @@ fun PaginatedReaderScreen(
var anchorLocatorForReconfig by remember { mutableStateOf<Locator?>(null) }
val currentPaginatorRef = remember { mutableStateOf<IPaginator?>(null) }
val previousConstraints = remember { arrayOf(this.constraints) }
if (previousConstraints[0] != this.constraints) {
val previousState = remember {
arrayOf<Any>(this.constraints, isDarkTheme, effectiveBg, effectiveText)
}
if (previousState[0] != this.constraints ||
previousState[1] != isDarkTheme ||
previousState[2] != effectiveBg ||
previousState[3] != effectiveText
) {
val activePaginator = currentPaginatorRef.value
if (activePaginator is BookPaginator) {
val currentPage = pagerState.currentPage
val locator = activePaginator.getLocatorForPage(currentPage)
if (locator != null) {
anchorLocatorForReconfig = locator
}
anchorLocatorForReconfig = locator
Timber.tag("ThemeReconfig").d("""
RECONFIG DETECTED
- Reason: ${if (previousState[0] != this.constraints) "Constraints" else "Theme/Colors"}
- Current Page: $currentPage
- Saved Locator: $locator
""".trimIndent())
}
previousConstraints[0] = this.constraints
previousState[0] = this.constraints
previousState[1] = isDarkTheme
previousState[2] = effectiveBg
previousState[3] = effectiveText
}
val textStyle = remember(
baseTextStyle,
textColor,
baseTextStyle, effectiveText,
debouncedFontSizeMult,
debouncedLineHeightMult,
debouncedFontFamily
@ -560,7 +594,7 @@ fun PaginatedReaderScreen(
val adjustedLineHeight = adjustedFontSize * debouncedLineHeightMult
baseTextStyle.copy(
color = textColor,
color = effectiveText,
fontSize = adjustedFontSize,
lineHeight = adjustedLineHeight,
fontFamily = debouncedFontFamily,
@ -636,7 +670,7 @@ fun PaginatedReaderScreen(
remember(initialChapterIndexInBook, anchorLocatorForReconfig) {
anchorLocatorForReconfig?.chapterIndex ?: initialChapterIndexInBook ?: 0
}
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign) {
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText) {
val userAgentStylesheet = UserAgentStylesheet.default
var allRules = OptimizedCssRules()
val allFontFaces = mutableListOf<FontFaceInfo>()
@ -647,7 +681,9 @@ fun PaginatedReaderScreen(
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
themeBackgroundColor = effectiveBg,
themeTextColor = effectiveText
)
allRules = allRules.merge(uaResult.rules)
allFontFaces.addAll(uaResult.fontFaces)
@ -659,7 +695,9 @@ fun PaginatedReaderScreen(
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
themeBackgroundColor = effectiveBg,
themeTextColor = effectiveText
)
allRules = allRules.merge(bookCssResult.rules)
allFontFaces.addAll(bookCssResult.fontFaces)
@ -687,6 +725,8 @@ fun PaginatedReaderScreen(
density = density,
fontFamilyMap = fontFamilyMap,
isDarkTheme = isDarkTheme,
themeBackgroundColor = effectiveBg,
themeTextColor = effectiveText,
bookId = uniqueBookId,
bookCacheDao = bookCacheDao,
proto = proto,
@ -707,31 +747,28 @@ fun PaginatedReaderScreen(
LaunchedEffect(paginator) {
if (anchorLocatorForReconfig != null) {
Timber.d("Waiting for paginator to initialize before restoring anchor...")
Timber.tag("ThemeReconfig").d("Restoration Effect Triggered for Locator: $anchorLocatorForReconfig")
// Suspend until isLoading becomes false
snapshotFlow { paginator.isLoading }.filter { !it }.first()
val targetLocator = anchorLocatorForReconfig
if (targetLocator != null) {
Timber.d(
"Paginator initialized. Restoring anchor: Chapter=${targetLocator.chapterIndex}, Block=${targetLocator.blockIndex}, Offset=${targetLocator.charOffset}"
)
val page = paginator.findPageForLocator(targetLocator)
Timber.tag("ThemeReconfig").d("""
Restoration Progress:
- Target Locator: $targetLocator
- Paginator found Page: $page
- Chapter Start Page: ${paginator.chapterStartPageIndices[targetLocator.chapterIndex]}
""".trimIndent())
if (page != null) {
pagerState.scrollToPage(page)
Timber.d("Restored to page: $page")
} else {
val startPage =
paginator.chapterStartPageIndices[targetLocator.chapterIndex]
val startPage = paginator.chapterStartPageIndices[targetLocator.chapterIndex]
if (startPage != null) {
Timber.w(
"Exact locator not found. Falling back to start of chapter at page $startPage"
)
Timber.tag("ThemeReconfig").w("Precise page not found, falling back to chapter start: $startPage")
pagerState.scrollToPage(startPage)
} else {
Timber.e("Failed to restore position. Chapter start index not found.")
}
}
anchorLocatorForReconfig = null
@ -780,6 +817,7 @@ fun PaginatedReaderScreen(
uiState = uiState,
pagerState = pagerState,
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
effectiveBg = effectiveBg,
searchQuery = searchQuery,
ttsHighlightInfo = ttsHighlightInfo,
textStyle = textStyle,
@ -814,7 +852,8 @@ fun PaginatedReaderScreen(
onHighlightDeleted = onHighlightDeleted,
isDarkTheme = isDarkTheme,
activeHighlightPalette = activeHighlightPalette,
onUpdatePalette = onUpdatePalette
onUpdatePalette = onUpdatePalette,
effectiveText = effectiveText
)
}
}
@ -1366,6 +1405,8 @@ internal fun PaginatedReaderContent(
uiState: PaginatedReaderUiState,
pagerState: PagerState,
isPageTurnAnimationEnabled: Boolean,
effectiveBg: Color,
effectiveText: Color,
searchQuery: String,
ttsHighlightInfo: TtsHighlightInfo?,
textStyle: TextStyle,
@ -1526,7 +1567,7 @@ internal fun PaginatedReaderContent(
val pageModifier = if (isPageTurnAnimationEnabled) {
Modifier
.zIndex(zIndex)
.realisticBookPage(pagerState, pageIndex, isDarkTheme, pageTurnTouchY) // UPDATED
.realisticBookPage(pagerState, pageIndex, effectiveBg, isDarkTheme, pageTurnTouchY)
} else {
Modifier
}
@ -3396,6 +3437,7 @@ private fun RenderFlexChildBlock(
private fun Modifier.realisticBookPage(
pagerState: PagerState,
pageIndex: Int,
paperColor: Color,
isDarkTheme: Boolean,
touchY: Float?
): Modifier = composed {
@ -3419,7 +3461,6 @@ private fun Modifier.realisticBookPage(
}
.drawWithContent {
val pageOffset = (pageIndex - pagerState.currentPage) - pagerState.currentPageOffsetFraction
val paperColor = if (isDarkTheme) Color(0xFF121212) else Color(0xFFFFFFFF)
if (abs(pageOffset) < 0.001f) {
drawRect(color = paperColor)
@ -3507,8 +3548,10 @@ private fun Modifier.realisticBookPage(
clipRect(0f, 0f, w, h) {
clipPath(frontPath) {
val flapColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF0F0F0)
drawPath(reflectedScreenPath, color = flapColor)
drawPath(reflectedScreenPath, color = paperColor)
val flapTint = if (isDarkTheme) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f)
drawPath(reflectedScreenPath, color = flapTint)
val innerShadowWidth = shadowWidth * 0.7f
val innerShadowBrush = Brush.linearGradient(

View file

@ -72,6 +72,8 @@ class PaginatedReaderViewModel : ViewModel() {
textStyle: TextStyle,
density: Density,
isDarkTheme: Boolean,
themeBackgroundColor: androidx.compose.ui.graphics.Color,
themeTextColor: androidx.compose.ui.graphics.Color,
context: Context,
initialChapterToPaginate: Int?,
mathMLRenderer: MathMLRenderer
@ -83,7 +85,7 @@ class PaginatedReaderViewModel : ViewModel() {
// CSS Parsing and Font Loading
val userAgentStylesheet = UserAgentStylesheet.default
var allRules = OptimizedCssRules() // CHANGED from: mutableListOf<CssRule>()
var allRules = OptimizedCssRules()
val allFontFaces = mutableListOf<FontFaceInfo>()
val uaResult = CssParser.parse(
@ -92,9 +94,11 @@ class PaginatedReaderViewModel : ViewModel() {
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor
)
allRules = allRules.merge(uaResult.rules) // CHANGED
allRules = allRules.merge(uaResult.rules)
allFontFaces.addAll(uaResult.fontFaces)
book.css.forEach { (path, content) ->
@ -104,9 +108,11 @@ class PaginatedReaderViewModel : ViewModel() {
baseFontSizeSp = textStyle.fontSize.value,
density = density.density,
constraints = textConstraints,
isDarkTheme = isDarkTheme
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor
)
allRules = allRules.merge(bookCssResult.rules) // CHANGED
allRules = allRules.merge(bookCssResult.rules)
allFontFaces.addAll(bookCssResult.fontFaces)
}
val fontFamilyMap = loadFontFamilies(
@ -125,6 +131,8 @@ class PaginatedReaderViewModel : ViewModel() {
density = density,
fontFamilyMap = fontFamilyMap,
isDarkTheme = isDarkTheme,
themeBackgroundColor = themeBackgroundColor,
themeTextColor = themeTextColor,
bookId = bookId,
bookCacheDao = bookCacheDao,
proto = proto,