diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js index e7b7be1..fd7071a 100644 --- a/app/src/main/assets/epub_reader.js +++ b/app/src/main/assets/epub_reader.js @@ -381,6 +381,10 @@ }; function handleHighlightInteraction(e) { + if (window.getSelection && window.getSelection().toString().trim().length > 0) { + return false; + } + var target = e.target; var highlightSpan = null; @@ -426,7 +430,6 @@ if (rawCfi && rawCfi.includes(";;")) { var cfiParts = rawCfi.split(";;"); cfiToReport = cfiParts[cfiParts.length - 1]; - console.log("HandleInteraction: Multi-CFI detected on single span. Reporting top layer: " + cfiToReport); } if (window.HighlightBridge) { @@ -445,25 +448,10 @@ function (e) { handleHighlightInteraction(e); }, - - true, + true ); - // 2. Handle Long Press (Context Menu) - "Atomic" Behavior - // This prevents the native Android selection handles from appearing inside the highlight - document.addEventListener( - "contextmenu", - function (e) { - if (handleHighlightInteraction(e)) { - e.preventDefault(); // Ensure menu doesn't show - return false; - } - }, - - true, - ); - - window.updateReaderStyles = function (fontSizeEm, lineHeight, fontFamily, textAlign) { + window.updateReaderStyles = function (fontSizeEm, lineHeight, fontFamily, textAlign, paragraphGap) { var logTag = "ReaderFontDiagnosis"; console.log( logTag + @@ -475,7 +463,8 @@ fontFamily + "', Align: '" + textAlign + - "'", + "', Gap: " + + paragraphGap ); var dynamicStyleId = "dynamicReaderStyles"; @@ -489,9 +478,11 @@ var newFontSize = parseFloat(fontSizeEm); var newLineHeight = parseFloat(lineHeight); + var newGap = parseFloat(paragraphGap); if (isNaN(newFontSize) || newFontSize < 0.5 || newFontSize > 5.0) newFontSize = 1.0; if (isNaN(newLineHeight) || newLineHeight < 1.0 || newLineHeight > 3.0) newLineHeight = 1.6; + if (isNaN(newGap) || newGap < 0.0 || newGap > 3.0) newGap = 1.0; var fontCss = ""; var selector = "body"; @@ -535,6 +526,17 @@ `; } + // --- GAP LOGIC --- + var gapCss = ` + body p, body ul, body ol, body blockquote { + margin-top: ` + (0.5 * newGap) + `em !important; + margin-bottom: ` + (0.5 * newGap) + `em !important; + } + body li { + margin-bottom: ` + (0.25 * newGap) + `em !important; + } + `; + dynamicStyleElement.innerHTML = ` body { font-size: ` + @@ -555,7 +557,7 @@ ` + alignCss + - ` `; + gapCss; setTimeout( function () { diff --git a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt index 00f42c0..23ae79b 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt @@ -321,6 +321,7 @@ fun ChapterWebView( onTopChunkUpdated: (Int) -> Unit, currentFontSize: Float, currentLineHeight: Float, + currentParagraphGap: Float, onChapterInitiallyScrolled: () -> Unit, modifier: Modifier = Modifier, onTap: () -> Unit, @@ -456,6 +457,7 @@ fun ChapterWebView( key, currentFontSize, currentLineHeight, + currentParagraphGap, currentFontFamily, currentTextAlign ) { @@ -714,7 +716,7 @@ fun ChapterWebView( } view?.evaluateJavascript( - "javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');", + "javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}', $currentParagraphGap);", null ) @@ -829,7 +831,7 @@ fun ChapterWebView( ) webView.evaluateJavascript( - "javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');", + "javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}', $currentParagraphGap);", null ) diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt index d4d8695..3c67a73 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt @@ -287,76 +287,28 @@ fun processAndAddHighlight( chapterIndex: Int, currentList: MutableList ): String { - val newParts = newCfi.split('|') - val newStartFull = newParts.first() - val newEndFull = newParts.last() - val newStartPath = newStartFull.split(':').first() - val newStartOffset = newStartFull.substringAfter(':', "0").toInt() - val newEndPath = newEndFull.split(':').first() - val newEndOffset = newEndFull.substringAfter(':', "0").toInt() - - val iterator = currentList.iterator() - var finalStartPath = newStartPath - var finalStartOffset = newStartOffset - var finalEndPath = newEndPath - var finalEndOffset = newEndOffset - var finalText = newText - var finalNote: String? = null - - while (iterator.hasNext()) { - val existing = iterator.next() - if (existing.chapterIndex != chapterIndex) continue - - val exParts = existing.cfi.split('|') - val exStartFull = exParts.first() - val exEndFull = exParts.last() - val exStartPath = exStartFull.split(':').first() - val exStartOffset = exStartFull.substringAfter(':', "0").toInt() - val exEndPath = exEndFull.split(':').first() - val exEndOffset = exEndFull.substringAfter(':', "0").toInt() - - fun comparePaths(p1: String, p2: String): Int { - val parts1 = p1.split('/').filter { it.isNotEmpty() }.mapNotNull { it.toIntOrNull() } - val parts2 = p2.split('/').filter { it.isNotEmpty() }.mapNotNull { it.toIntOrNull() } - val len = min(parts1.size, parts2.size) - for (i in 0 until len) { - if (parts1[i] != parts2[i]) return parts1[i] - parts2[i] - } - return parts1.size - parts2.size - } - - val startCmp = comparePaths(exStartPath, newEndPath) - val endCmp = comparePaths(exEndPath, newStartPath) - - val isDisjoint = (startCmp > 0) || (startCmp == 0 && exStartOffset > newEndOffset) || - (endCmp < 0) || (endCmp == 0 && exEndOffset < newStartOffset) - - if (!isDisjoint) { - iterator.remove() - if (existing.note != null && finalNote == null) finalNote = existing.note - val unionStartCmp = comparePaths(finalStartPath, exStartPath) - if (unionStartCmp > 0 || (unionStartCmp == 0 && finalStartOffset > exStartOffset)) { - finalStartPath = exStartPath - finalStartOffset = exStartOffset - } - val unionEndCmp = comparePaths(finalEndPath, exEndPath) - if (unionEndCmp < 0 || (unionEndCmp == 0 && finalEndOffset < exEndOffset)) { - finalEndPath = exEndPath - finalEndOffset = exEndOffset - } - if (existing.text.length > finalText.length) finalText = existing.text - } + // Scenario: Exact match -> Update color and text instead of stacking identical spans + val exactMatchIndex = currentList.indexOfFirst { + it.chapterIndex == chapterIndex && it.cfi == newCfi } - val finalCfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset" - currentList.add(UserHighlight( - cfi = finalCfi, - text = finalText, - color = newColor, - chapterIndex = chapterIndex, - note = finalNote - )) - return finalCfi + if (exactMatchIndex != -1) { + val existing = currentList[exactMatchIndex] + currentList[exactMatchIndex] = existing.copy(color = newColor, text = newText) + return existing.cfi + } + + // Scenarios: Partial overlap or subsumption -> Add independently + currentList.add( + UserHighlight( + cfi = newCfi, + text = newText, + color = newColor, + chapterIndex = chapterIndex, + note = null + ) + ) + return newCfi } // --- UI Components --- diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt index 4d08e5d..b1a9cfb 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt @@ -502,6 +502,7 @@ fun EpubReaderHost( var pageInfoMode by remember { mutableStateOf(loadPageInfoMode(context)) } var pullToTurnEnabled by remember { mutableStateOf(loadPullToTurn(context)) } var showVisualOptionsSheet by remember { mutableStateOf(false) } + var removeEdgePadding by remember { mutableStateOf(loadRemoveEdgePadding(context)) } var volumeScrollEnabled by remember { mutableStateOf(loadVolumeScrollSetting(context)) @@ -899,6 +900,7 @@ fun EpubReaderHost( var currentFontSizeEm by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.fontSize) } var currentLineHeight by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.lineHeight) } + var currentParagraphGap by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.paragraphGap) } var currentTextAlign by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.textAlign) } var currentFontFamily by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.font) } var currentCustomFontPath by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.customPath) } @@ -914,14 +916,14 @@ fun EpubReaderHost( var showFontSelectionSheet by remember { mutableStateOf(false) } val fontSheetState = rememberModalBottomSheetState() - LaunchedEffect(currentFontSizeEm, currentLineHeight, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) { + LaunchedEffect(currentFontSizeEm, currentLineHeight, currentParagraphGap, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) { if (isFormatLocal) { saveLocalReaderSettings( - context, bookId, currentFontSizeEm, currentLineHeight, currentFontFamily, currentCustomFontPath, currentTextAlign + context, bookId, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentFontFamily, currentCustomFontPath, currentTextAlign ) } else { saveReaderSettings( - context, currentFontSizeEm, currentLineHeight, currentFontFamily, currentCustomFontPath, currentTextAlign + context, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentFontFamily, currentCustomFontPath, currentTextAlign ) } } @@ -2144,11 +2146,13 @@ fun EpubReaderHost( if (pageInfoMode == PageInfoMode.DEFAULT) PAGE_INFO_BAR_HEIGHT else 0.dp } + val horizontalPadding = if (removeEdgePadding) 0.dp else 16.dp + Box( modifier = Modifier .fillMaxSize() .padding(bottom = contentBottomPadding) - .padding(top = 16.dp, start = 16.dp, end = 16.dp) + .padding(top = 16.dp, start = horizontalPadding, end = horizontalPadding) .testTag("ReaderContainer") ) { if (chapters.isEmpty()) { @@ -2521,6 +2525,7 @@ fun EpubReaderHost( modifier = Modifier.fillMaxSize(), currentFontSize = currentFontSizeEm, currentLineHeight = currentLineHeight, + currentParagraphGap = currentParagraphGap, currentFontFamily = currentFontFamily, customFontPath = currentCustomFontPath, currentTextAlign = currentTextAlign, @@ -2870,6 +2875,7 @@ fun EpubReaderHost( searchQuery = searchState.searchQuery, fontSizeMultiplier = currentFontSizeEm, lineHeightMultiplier = currentLineHeight, + paragraphGapMultiplier = currentParagraphGap, fontFamily = activeFontFamily, textAlign = currentTextAlign, activeHighlightPalette = currentHighlightPalette, @@ -2881,6 +2887,7 @@ fun EpubReaderHost( offset = ttsState.startOffsetInSource ).takeIf { ttsState.currentText != null && ttsState.sourceCfi != null && ttsState.startOffsetInSource != -1 }, activeTextureId = activeTextureId, + removeEdgePadding = removeEdgePadding, initialChapterIndexInBook = lastKnownLocator?.chapterIndex, modifier = Modifier.alpha(if (isPagerInitialized) 1f else 0f), onPaginatorReady = { newPaginator -> @@ -3877,6 +3884,8 @@ fun EpubReaderHost( onFontSizeChange = { currentFontSizeEm = it }, currentLineHeight = currentLineHeight, onLineHeightChange = { currentLineHeight = it }, + currentParagraphGap = currentParagraphGap, + onParagraphGapChange = { currentParagraphGap = it }, currentFont = currentFontFamily, currentCustomFontName = if(currentCustomFontPath != null) { customFonts.find { it.path == currentCustomFontPath }?.displayName ?: "Custom Font" @@ -3892,6 +3901,7 @@ fun EpubReaderHost( onReset = { currentFontSizeEm = DEFAULT_FONT_SIZE_VAL currentLineHeight = DEFAULT_LINE_HEIGHT_VAL + currentParagraphGap = DEFAULT_PARAGRAPH_GAP_VAL currentFontFamily = ReaderFont.ORIGINAL currentCustomFontPath = null currentTextAlign = ReaderTextAlign.DEFAULT @@ -4229,6 +4239,11 @@ fun EpubReaderHost( pullToTurnEnabled = it savePullToTurn(context, it) }, + removeEdgePadding = removeEdgePadding, + onRemoveEdgePaddingChange = { + removeEdgePadding = it + saveRemoveEdgePadding(context, it) + }, onDismiss = { showVisualOptionsSheet = false } ) } diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt index 87dc785..aa18c24 100644 --- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt +++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderSettings.kt @@ -97,12 +97,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource const val SETTINGS_PREFS_NAME = "epub_reader_settings" private const val TEXT_ALIGN_KEY = "reader_text_align" private const val FONT_SIZE_KEY = "reader_font_size" private const val LINE_HEIGHT_KEY = "reader_line_height" +private const val PARAGRAPH_GAP_KEY = "reader_paragraph_gap" private const val AUTO_SCROLL_SPEED_KEY = "reader_auto_scroll_speed" private const val FONT_FAMILY_KEY = "reader_font_family" private const val TAP_TO_NAVIGATE_ENABLED_KEY = "tap_to_navigate_enabled" @@ -113,6 +115,7 @@ private const val PULL_TO_TURN_ENABLED_KEY = "reader_pull_to_turn_enabled" const val DEFAULT_FONT_SIZE_VAL = 1.0f const val DEFAULT_LINE_HEIGHT_VAL = 1.6f +const val DEFAULT_PARAGRAPH_GAP_VAL = 1.0f enum class ReaderFont(val id: String, val displayName: String, val fontFamilyName: String) { ORIGINAL("original", "Original", "Original"), @@ -144,6 +147,7 @@ enum class PageInfoMode(val id: Int, val title: String) { data class FormatSettings( val fontSize: Float, val lineHeight: Float, + val paragraphGap: Float, val font: ReaderFont, val customPath: String?, val textAlign: ReaderTextAlign @@ -152,6 +156,7 @@ data class FormatSettings( private const val FORMAT_IS_LOCAL_PREFIX = "format_is_local_" private const val LOCAL_FONT_SIZE_PREFIX = "local_font_size_" private const val LOCAL_LINE_HEIGHT_PREFIX = "local_line_height_" +private const val LOCAL_PARAGRAPH_GAP_PREFIX = "local_paragraph_gap_" private const val LOCAL_FONT_FAMILY_PREFIX = "local_font_family_" private const val LOCAL_TEXT_ALIGN_PREFIX = "local_text_align_" @@ -170,6 +175,7 @@ fun saveLocalReaderSettings( bookId: String, fontSize: Float, lineHeight: Float, + paragraphGap: Float, fontFamily: ReaderFont, customFontPath: String?, textAlign: ReaderTextAlign @@ -178,6 +184,7 @@ fun saveLocalReaderSettings( prefs.edit { putFloat(LOCAL_FONT_SIZE_PREFIX + bookId, fontSize) putFloat(LOCAL_LINE_HEIGHT_PREFIX + bookId, lineHeight) + putFloat(LOCAL_PARAGRAPH_GAP_PREFIX + bookId, paragraphGap) if (customFontPath != null) { putString(LOCAL_FONT_FAMILY_PREFIX + bookId, "custom|$customFontPath") } else { @@ -234,6 +241,12 @@ fun loadFormatSettings(context: Context, bookId: String, isLocal: Boolean): Form prefs.getFloat(LINE_HEIGHT_KEY, DEFAULT_LINE_HEIGHT_VAL) } + val paragraphGap = if (isLocal && prefs.contains(LOCAL_PARAGRAPH_GAP_PREFIX + bookId)) { + prefs.getFloat(LOCAL_PARAGRAPH_GAP_PREFIX + bookId, DEFAULT_PARAGRAPH_GAP_VAL) + } else { + prefs.getFloat(PARAGRAPH_GAP_KEY, DEFAULT_PARAGRAPH_GAP_VAL) + } + val savedFontVal = if (isLocal && prefs.contains(LOCAL_FONT_FAMILY_PREFIX + bookId)) { prefs.getString(LOCAL_FONT_FAMILY_PREFIX + bookId, ReaderFont.ORIGINAL.id) ?: ReaderFont.ORIGINAL.id } else { @@ -253,7 +266,7 @@ fun loadFormatSettings(context: Context, bookId: String, isLocal: Boolean): Form } val textAlign = ReaderTextAlign.entries.find { it.id == alignId } ?: ReaderTextAlign.DEFAULT - return FormatSettings(fontSize, lineHeight, font, customPath, textAlign) + return FormatSettings(fontSize, lineHeight, paragraphGap, font, customPath, textAlign) } fun getComposeFontFamily( @@ -291,6 +304,7 @@ fun saveReaderSettings( context: Context, fontSize: Float, lineHeight: Float, + paragraphGap: Float, fontFamily: ReaderFont, customFontPath: String?, textAlign: ReaderTextAlign @@ -299,6 +313,7 @@ fun saveReaderSettings( prefs.edit { putFloat(FONT_SIZE_KEY, fontSize) putFloat(LINE_HEIGHT_KEY, lineHeight) + putFloat(PARAGRAPH_GAP_KEY, paragraphGap) if (customFontPath != null) { putString(FONT_FAMILY_KEY, "custom|$customFontPath") } else { @@ -345,6 +360,8 @@ fun ReaderTextFormatPanel( onFontSizeChange: (Float) -> Unit, currentLineHeight: Float, onLineHeightChange: (Float) -> Unit, + currentParagraphGap: Float, // NEW + onParagraphGapChange: (Float) -> Unit, // NEW currentFont: ReaderFont, currentCustomFontName: String?, onFontOptionClick: () -> Unit, @@ -363,10 +380,10 @@ fun ReaderTextFormatPanel( modifier = modifier ) { Surface( - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.95f), + shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.98f), tonalElevation = 0.dp, - shadowElevation = 0.dp, + shadowElevation = 8.dp, border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f)), modifier = Modifier .fillMaxWidth() @@ -375,6 +392,7 @@ fun ReaderTextFormatPanel( Column( modifier = Modifier.padding(16.dp) ) { + // Header Row (Local/Global + Close/Reset) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -439,82 +457,109 @@ fun ReaderTextFormatPanel( Spacer(Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically + // FONT & ALIGNMENT SECTION + Text( + text = "FONT & ALIGNMENT", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) + + // Font Button (Full width) + Surface( + onClick = onFontOptionClick, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) ) { - Surface( - onClick = onFontOptionClick, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier - .weight(0.45f) - .height(48.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.padding(horizontal = 16.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.padding(horizontal = 12.dp) - ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Aa", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.padding(end = 12.dp) + ) Text( text = currentCustomFontName ?: currentFont.displayName, - style = MaterialTheme.typography.labelLarge, + style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.onSecondaryContainer, maxLines = 1, overflow = TextOverflow.Ellipsis ) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer, - modifier = Modifier.size(16.dp) - ) } + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(20.dp) + ) } + } - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier - .weight(0.55f) - .height(48.dp) - ) { - Row { - ReaderTextAlign.entries.forEach { align -> - val isSelected = currentTextAlign == align - Column( - modifier = Modifier - .fillMaxHeight() - .weight(1f) - .background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent) - .clickable { onTextAlignChange(align) }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - painter = androidx.compose.ui.res.painterResource(id = align.iconResId), - contentDescription = align.displayName, - tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) - ) - Text( - text = align.displayName, - style = MaterialTheme.typography.labelSmall, - fontSize = 10.sp, - color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Spacer(Modifier.height(8.dp)) + + // Alignment Button (Full width Segmented) + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Row { + ReaderTextAlign.entries.forEach { align -> + val isSelected = currentTextAlign == align + Column( + modifier = Modifier + .fillMaxHeight() + .weight(1f) + .clip(RoundedCornerShape(12.dp)) + .background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent) + .clickable { onTextAlignChange(align) }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + painter = painterResource(id = align.iconResId), + contentDescription = align.displayName, + tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp) + ) + Text( + text = align.displayName, + style = MaterialTheme.typography.labelSmall, + fontSize = 11.sp, + color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant + ) } } } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(24.dp)) + + // LAYOUT & SPACING SECTION + Text( + text = "LAYOUT & SPACING", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(start = 4.dp, bottom = 8.dp) + ) // Sliders - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + // Size Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Size", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(55.dp)) + Text("Size", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp)) Slider( value = currentFontSize, onValueChange = onFontSizeChange, @@ -522,10 +567,11 @@ fun ReaderTextFormatPanel( steps = 24, modifier = Modifier.weight(1f) ) - Text("%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(35.dp), textAlign = TextAlign.End) + Text("%.1fx".format(currentFontSize), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) } + // Lines Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("Spacing", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(55.dp)) + Text("Lines", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp)) Slider( value = currentLineHeight, onValueChange = onLineHeightChange, @@ -533,9 +579,22 @@ fun ReaderTextFormatPanel( steps = 14, modifier = Modifier.weight(1f) ) - Text("%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(35.dp), textAlign = TextAlign.End) + Text("%.1fx".format(currentLineHeight), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) + } + // Paragraph Gap + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { + Text("Gap", style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(60.dp)) + Slider( + value = currentParagraphGap, + onValueChange = onParagraphGapChange, + valueRange = 0.0f..3.0f, + steps = 12, + modifier = Modifier.weight(1f) + ) + Text("%.1fx".format(currentParagraphGap), style = MaterialTheme.typography.labelMedium, modifier = Modifier.width(40.dp), textAlign = TextAlign.End) } } + Spacer(Modifier.height(8.dp)) } } } @@ -647,6 +706,18 @@ fun FontSelectionSheetContent( } } +private const val REMOVE_EDGE_PADDING_KEY = "reader_remove_edge_padding" + +fun saveRemoveEdgePadding(context: Context, enabled: Boolean) { + val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit { putBoolean(REMOVE_EDGE_PADDING_KEY, enabled) } +} + +fun loadRemoveEdgePadding(context: Context): Boolean { + val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) + return prefs.getBoolean(REMOVE_EDGE_PADDING_KEY, false) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun VisualOptionsSheet( @@ -656,6 +727,8 @@ fun VisualOptionsSheet( onPageInfoModeChange: (PageInfoMode) -> Unit, pullToTurnEnabled: Boolean, onPullToTurnChange: (Boolean) -> Unit, + removeEdgePadding: Boolean, + onRemoveEdgePaddingChange: (Boolean) -> Unit, onDismiss: () -> Unit ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -730,6 +803,29 @@ fun VisualOptionsSheet( } } + Spacer(modifier = Modifier.height(24.dp)) + + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + modifier = Modifier + .fillMaxWidth() + .clickable { onRemoveEdgePaddingChange(!removeEdgePadding) } + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text(stringResource(R.string.visual_options_edge_padding), style = MaterialTheme.typography.titleMedium) + Text(stringResource(R.string.visual_options_edge_padding_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Spacer(modifier = Modifier.width(16.dp)) + Switch(checked = removeEdgePadding, onCheckedChange = { onRemoveEdgePaddingChange(it) }) + } + } + Spacer(modifier = Modifier.height(32.dp)) } } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt index 1491347..c00412e 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/BookPaginator.kt @@ -118,7 +118,8 @@ class BookPaginator( private val allFontFaces: List, private val context: Context, private val mathMLRenderer: MathMLRenderer, - private val userTextAlign: TextAlign? + private val userTextAlign: TextAlign?, + private val paragraphGapMultiplier: Float ) : IPaginator { override var totalPageCount by mutableIntStateOf(0) private set @@ -271,7 +272,7 @@ class BookPaginator( } private fun generateConfigurationHash(): Int { - val configString = "w:${constraints.maxWidth}-h:${constraints.maxHeight}-fs:${textStyle.fontSize.value}-ta:$userTextAlign" + val configString = "w:${constraints.maxWidth}-h:${constraints.maxHeight}-fs:${textStyle.fontSize.value}-ta:$userTextAlign-pg:$paragraphGapMultiplier" val hash = configString.hashCode() return hash } @@ -323,6 +324,8 @@ class BookPaginator( ) bookCacheDao.insertConfigurationCache(newCache) + bookCacheDao.cleanupOldConfigurations(bookId) + if (finalizedChapterCounts.size >= chapters.size) { pageCountsAreAccurate = true } @@ -418,7 +421,8 @@ class BookPaginator( themeTextColor = themeTextColor, chapterAbsPath = chapter.absPath, extractionBasePath = extractionBasePath, - userTextAlign = userTextAlign + userTextAlign = userTextAlign, + paragraphGapMultiplier = paragraphGapMultiplier ) bookCacheDao.getProcessedChapter(bookId, chapterIndex)?.let { cachedChapter -> diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt b/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt index 68a39c3..776696a 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/ContentStyler.kt @@ -54,7 +54,8 @@ class ContentStyler( private val themeTextColor: Color, private val chapterAbsPath: String, private val extractionBasePath: String, - private val userTextAlign: TextAlign? + private val userTextAlign: TextAlign?, + private val paragraphGapMultiplier: Float ) { fun style(semanticBlocks: List): List { @@ -102,6 +103,18 @@ class ContentStyler( private fun styleBlock(block: SemanticBlock): ContentBlock? { val themedStyle = applyThemeToStyle(block.style) + + val finalBlockStyle = if (block is SemanticParagraph) { + val originalMargin = themedStyle.blockStyle.margin + val newMargin = originalMargin.copy( + top = originalMargin.top * paragraphGapMultiplier, + bottom = originalMargin.bottom * paragraphGapMultiplier + ) + themedStyle.blockStyle.copy(margin = newMargin) + } else { + themedStyle.blockStyle + } + return when (block) { is SemanticParagraph -> { val computedTextAlign = userTextAlign ?: themedStyle.paragraphStyle.textAlign @@ -109,7 +122,7 @@ class ContentStyler( ParagraphBlock( content = buildAnnotatedString(block, themedStyle), textAlign = computedTextAlign, - style = themedStyle.blockStyle, + style = finalBlockStyle, elementId = block.elementId, cfi = block.cfi, startCharOffsetInSource = block.startCharOffsetInSource, @@ -353,14 +366,20 @@ class ContentStyler( textMotion = mergedParagraphStyle.textMotion ) - var initialSpanStyle = baseTextStyle.toSpanStyle() - .merge(blockStyle.spanStyle) - .copy(fontFamily = baseTextStyle.fontFamily) + val isCustomFont = baseTextStyle.fontFamily != null && baseTextStyle.fontFamily != FontFamily.Default - if (rootFontFamily == FontFamily.Monospace) { - initialSpanStyle = initialSpanStyle.copy(fontFamily = rootFontFamily) + val effectiveBlockFontFamily = if (rootFontFamily == FontFamily.Monospace) { + FontFamily.Monospace + } else if (isCustomFont) { + baseTextStyle.fontFamily + } else { + rootFontFamily ?: baseTextStyle.fontFamily } + val initialSpanStyle = baseTextStyle.toSpanStyle() + .merge(blockStyle.spanStyle) + .copy(fontFamily = effectiveBlockFontFamily) + Timber.d("ContentStyler: InitialSpanStyle. BaseFontSize=${baseTextStyle.fontSize}, BlockFontSize=${blockStyle.spanStyle.fontSize} -> Merged=${initialSpanStyle.fontSize}") withStyle(finalParagraphStyle) { @@ -368,14 +387,23 @@ class ContentStyler( append(block.text) block.spans.sortedBy { it.start }.forEach { span -> val themedSpanStyle = applyThemeToStyle(span.style) - val fontFamily = findFirstAvailableFontFamily(themedSpanStyle.fontFamilies, fontFamilyMap) + val spanFontFamily = findFirstAvailableFontFamily(themedSpanStyle.fontFamilies, fontFamilyMap) + val effectiveSpanFontFamily = if (spanFontFamily == FontFamily.Monospace) { + FontFamily.Monospace + } else if (isCustomFont) { + baseTextStyle.fontFamily + } else { + spanFontFamily + } + val baselineShift = when (span.tag) { "sub" -> BaselineShift.Subscript "sup" -> BaselineShift.Superscript else -> null } + val finalSpanStyle = themedSpanStyle.spanStyle.copy( - fontFamily = fontFamily, + fontFamily = effectiveSpanFontFamily, baselineShift = baselineShift ) addStyle(initialSpanStyle.merge(finalSpanStyle), span.start, span.end) diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt index 73aa349..a17bcae 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReader.kt @@ -491,6 +491,7 @@ private fun WrappingContentLayout( @OptIn(ExperimentalFoundationApi::class, ExperimentalSerializationApi::class, FlowPreview::class) @Composable fun PaginatedReaderScreen( + modifier: Modifier = Modifier, book: EpubBook, isDarkTheme: Boolean, effectiveBg: Color, @@ -500,11 +501,12 @@ fun PaginatedReaderScreen( searchQuery: String, fontSizeMultiplier: Float, lineHeightMultiplier: Float, + paragraphGapMultiplier: Float, fontFamily: FontFamily, textAlign: ReaderTextAlign, ttsHighlightInfo: TtsHighlightInfo?, initialChapterIndexInBook: Int?, - modifier: Modifier = Modifier, + removeEdgePadding: Boolean = false, onPaginatorReady: (IPaginator) -> Unit, onTap: (Offset?) -> Unit, isProUser: Boolean, @@ -553,6 +555,7 @@ fun PaginatedReaderScreen( var debouncedFontSizeMult by remember { mutableFloatStateOf(fontSizeMultiplier) } var debouncedLineHeightMult by remember { mutableFloatStateOf(lineHeightMultiplier) } + var debouncedParagraphGapMult by remember { mutableFloatStateOf(paragraphGapMultiplier) } var debouncedFontFamily by remember { mutableStateOf(fontFamily) } var debouncedTextAlign by remember { mutableStateOf(textAlign) } @@ -611,8 +614,8 @@ fun PaginatedReaderScreen( ) } - LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, fontFamily, textAlign) { - if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) { + LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, paragraphGapMultiplier, fontFamily, textAlign) { + if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || paragraphGapMultiplier != debouncedParagraphGapMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) { Timber.d("Formatting changed. Waiting for debounce.") delay(400L) @@ -627,6 +630,7 @@ fun PaginatedReaderScreen( debouncedFontSizeMult = fontSizeMultiplier debouncedLineHeightMult = lineHeightMultiplier + debouncedParagraphGapMult = paragraphGapMultiplier debouncedFontFamily = fontFamily debouncedTextAlign = textAlign Timber.d("Debounce complete. Applying new format settings.") @@ -642,7 +646,7 @@ fun PaginatedReaderScreen( } val density = LocalDensity.current - val horizontalPadding = 16.dp + val horizontalPadding = if (removeEdgePadding) 0.dp else 16.dp val verticalPadding = 16.dp val textConstraints = @@ -673,8 +677,8 @@ fun PaginatedReaderScreen( remember(initialChapterIndexInBook, anchorLocatorForReconfig) { anchorLocatorForReconfig?.chapterIndex ?: initialChapterIndexInBook ?: 0 } - val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText) { - val userAgentStylesheet = UserAgentStylesheet.default + val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText, debouncedParagraphGapMult) { + val userAgentStylesheet = UserAgentStylesheet.default var allRules = OptimizedCssRules() val allFontFaces = mutableListOf() @@ -739,7 +743,8 @@ fun PaginatedReaderScreen( allFontFaces = allFontFaces, context = context.applicationContext, mathMLRenderer = mathMLRenderer, - userTextAlign = userTextAlign + userTextAlign = userTextAlign, + paragraphGapMultiplier = debouncedParagraphGapMult ) } @@ -1171,7 +1176,6 @@ private fun TextWithEmphasis( is HeaderBlock -> block.startCharOffsetInSource is QuoteBlock -> block.startCharOffsetInSource is ListItemBlock -> block.startCharOffsetInSource - else -> 0 } val isStart = block.blockIndex == activeSelection.startBlockIndex && currentBlockAbs == activeSelection.startBlockCharOffset @@ -1276,76 +1280,6 @@ private fun TextWithEmphasis( } .then(customDrawer) .pointerInput(userHighlights, text) { - awaitEachGesture { - val down = awaitFirstDown( - pass = PointerEventPass.Initial, requireUnconsumed = false - ) - val layout = textLayoutResult - if (layout != null) { - val hit = getHighlightAt(down.position, layout) - if (hit != null) { - down.consume() - val startPosition = down.position - var dragDistance = 0f - var isFinished = false - - val longPressJob = scope.launch { - delay(500) - pressedHighlightCfi = hit.first.cfi - } - - try { - while (true) { - val event = awaitPointerEvent( - pass = PointerEventPass.Initial - ) - val change = event.changes.firstOrNull { - it.id == down.id - } - if (change == null) { - longPressJob.cancel() - pressedHighlightCfi = null - break - } - change.consume() - if (change.pressed) { - dragDistance = (change.position - startPosition).getDistance() - if (dragDistance >= viewConfiguration.touchSlop) { - longPressJob.cancel() - pressedHighlightCfi = null - } - } else { - isFinished = true - longPressJob.cancel() - pressedHighlightCfi = null - break - } - } - } catch (_: Exception) { - longPressJob.cancel() - pressedHighlightCfi = null - } - - if (isFinished && dragDistance < viewConfiguration.touchSlop) { - val (highlight, localRect) = hit - val globalRect = layoutCoordinates?.let { coords -> - if (coords.isAttached) { - val topLeft = coords.localToWindow( - localRect.topLeft - ) - val bottomRight = coords.localToWindow( - localRect.bottomRight - ) - Rect(topLeft, bottomRight) - } else null - } ?: localRect - onHighlightClick(highlight, globalRect) - } - } - } - } - } - .pointerInput(text) { detectTapGestures( onLongPress = { offset -> textLayoutResult?.let { layout -> @@ -1355,7 +1289,6 @@ private fun TextWithEmphasis( var start = wordBoundary.start var end = wordBoundary.end - // Trim trailing/leading punctuations for a cleaner word selection val textStr = text.text while (start < end && start < textStr.length && !textStr[start].isLetterOrDigit()) start++ while (end > start && end <= textStr.length && !textStr[end - 1].isLetterOrDigit()) end-- @@ -1377,7 +1310,6 @@ private fun TextWithEmphasis( is HeaderBlock -> block.startCharOffsetInSource is QuoteBlock -> block.startCharOffsetInSource is ListItemBlock -> block.startCharOffsetInSource - else -> 0 } onSelectionChange( @@ -1402,14 +1334,27 @@ private fun TextWithEmphasis( }, onTap = { offset -> textLayoutResult?.let { layout -> + val hit = getHighlightAt(offset, layout) + if (hit != null) { + val (highlight, localRect) = hit + val globalRect = layoutCoordinates?.let { coords -> + if (coords.isAttached) { + val topLeft = coords.localToWindow(localRect.topLeft) + val bottomRight = coords.localToWindow(localRect.bottomRight) + Rect(topLeft, bottomRight) + } else null + } ?: localRect + onHighlightClick(highlight, globalRect) + return@detectTapGestures + } + val charOffset = layout.getOffsetForPosition(offset) - val urlAnnotation = text.getStringAnnotations( - "URL", charOffset, charOffset - ).firstOrNull() + val urlAnnotation = text.getStringAnnotations("URL", charOffset, charOffset).firstOrNull() if (urlAnnotation != null) onLinkClick(urlAnnotation.item) else onGeneralTap(offset) } - }) + } + ) }, onTextLayout = { textLayoutResult = it if (layoutCoordinates != null && block.cfi != null) { @@ -1713,7 +1658,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> firstTextBlock.startCharOffsetInSource is QuoteBlock -> firstTextBlock.startCharOffsetInSource is ListItemBlock -> firstTextBlock.startCharOffsetInSource - else -> 0 } val newTextPerBlock = (previousSel?.textPerBlock ?: emptyMap()).toMutableMap() @@ -2853,7 +2797,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> block.startCharOffsetInSource is QuoteBlock -> block.startCharOffsetInSource is ListItemBlock -> block.startCharOffsetInSource - else -> 0 } val isStartBlockPart = block.blockIndex == sel.startBlockIndex && currentBlockAbs == sel.startBlockCharOffset val isEndBlockPart = block.blockIndex == sel.endBlockIndex && currentBlockAbs == sel.endBlockCharOffset @@ -3008,7 +2951,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> block.startCharOffsetInSource is QuoteBlock -> block.startCharOffsetInSource is ListItemBlock -> block.startCharOffsetInSource - else -> 0 } var newStartBlockAbs = if (isStartHandle) currentBlockAbs else sel.startBlockCharOffset var newEndBlockAbs = if (!isStartHandle) currentBlockAbs else sel.endBlockCharOffset @@ -3044,7 +2986,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> (b.third as HeaderBlock).startCharOffsetInSource is QuoteBlock -> (b.third as QuoteBlock).startCharOffsetInSource is ListItemBlock -> (b.third as ListItemBlock).startCharOffsetInSource - else -> 0 } })) @@ -3061,7 +3002,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> b.third.startCharOffsetInSource is QuoteBlock -> b.third.startCharOffsetInSource is ListItemBlock -> b.third.startCharOffsetInSource - else -> 0 } val isStartBlockPart = b.third.blockIndex == newStartIdx && bAbs == newStartBlockAbs val isEndBlockPart = b.third.blockIndex == newEndIdx && bAbs == newEndBlockAbs @@ -3092,7 +3032,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> it.third.startCharOffsetInSource is QuoteBlock -> it.third.startCharOffsetInSource is ListItemBlock -> it.third.startCharOffsetInSource - else -> 0 } abs == newStartBlockAbs } @@ -3187,7 +3126,6 @@ internal fun PaginatedReaderContent( is HeaderBlock -> block.startCharOffsetInSource is QuoteBlock -> block.startCharOffsetInSource is ListItemBlock -> block.startCharOffsetInSource - else -> 0 } blockAbs == targetBlockAbs } diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModel.kt b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModel.kt index a507736..eab8b7e 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModel.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModel.kt @@ -76,7 +76,8 @@ class PaginatedReaderViewModel : ViewModel() { themeTextColor: androidx.compose.ui.graphics.Color, context: Context, initialChapterToPaginate: Int?, - mathMLRenderer: MathMLRenderer + mathMLRenderer: MathMLRenderer, + paragraphGapMultiplier: Float ) { if (paginator != null) return @@ -142,7 +143,8 @@ class PaginatedReaderViewModel : ViewModel() { allFontFaces = allFontFaces, context = context.applicationContext, mathMLRenderer = mathMLRenderer, - userTextAlign = null + userTextAlign = null, + paragraphGapMultiplier = paragraphGapMultiplier ) paginator = newPaginator diff --git a/app/src/main/java/com/aryan/reader/paginatedreader/data/BookCacheDatabase.kt b/app/src/main/java/com/aryan/reader/paginatedreader/data/BookCacheDatabase.kt index 36659a5..da0433d 100644 --- a/app/src/main/java/com/aryan/reader/paginatedreader/data/BookCacheDatabase.kt +++ b/app/src/main/java/com/aryan/reader/paginatedreader/data/BookCacheDatabase.kt @@ -178,6 +178,16 @@ abstract class BookCacheDao { @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun insertConfigurationCache(cache: ConfigurationCache) + + @Query(""" + DELETE FROM configuration_cache + WHERE bookId = :bookId AND configHash NOT IN ( + SELECT configHash FROM configuration_cache + WHERE bookId = :bookId + ORDER BY rowid DESC LIMIT 3 + ) + """) + abstract suspend fun cleanupOldConfigurations(bookId: String) } @Database( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 19b9482..886ac0d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -631,6 +631,8 @@ The reading progress and chapter indicator at the bottom of the screen. Seamless Chapter Transition Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation. + Remove Edge Padding + Removes the horizontal gap on the left and right edges. Search