Epub improvements (#162)
* Added a "Remove Edge Padding" option in "Visual Options" * Added support for paragraph gap settings in the EPUB vertical reader. * Added support for adjustable paragraph gaps in the paginated reader. * fix(epub-pagination): ensure custom fonts override embedded epub fonts * feat(epub): allow overlapping highlights and resolve gesture selection conflicts
This commit is contained in:
parent
291504fd90
commit
49e08cc9f1
11 changed files with 315 additions and 264 deletions
|
|
@ -381,6 +381,10 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
function handleHighlightInteraction(e) {
|
function handleHighlightInteraction(e) {
|
||||||
|
if (window.getSelection && window.getSelection().toString().trim().length > 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var target = e.target;
|
var target = e.target;
|
||||||
var highlightSpan = null;
|
var highlightSpan = null;
|
||||||
|
|
||||||
|
|
@ -426,7 +430,6 @@
|
||||||
if (rawCfi && rawCfi.includes(";;")) {
|
if (rawCfi && rawCfi.includes(";;")) {
|
||||||
var cfiParts = rawCfi.split(";;");
|
var cfiParts = rawCfi.split(";;");
|
||||||
cfiToReport = cfiParts[cfiParts.length - 1];
|
cfiToReport = cfiParts[cfiParts.length - 1];
|
||||||
console.log("HandleInteraction: Multi-CFI detected on single span. Reporting top layer: " + cfiToReport);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.HighlightBridge) {
|
if (window.HighlightBridge) {
|
||||||
|
|
@ -445,25 +448,10 @@
|
||||||
function (e) {
|
function (e) {
|
||||||
handleHighlightInteraction(e);
|
handleHighlightInteraction(e);
|
||||||
},
|
},
|
||||||
|
true
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Handle Long Press (Context Menu) - "Atomic" Behavior
|
window.updateReaderStyles = function (fontSizeEm, lineHeight, fontFamily, textAlign, paragraphGap) {
|
||||||
// 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) {
|
|
||||||
var logTag = "ReaderFontDiagnosis";
|
var logTag = "ReaderFontDiagnosis";
|
||||||
console.log(
|
console.log(
|
||||||
logTag +
|
logTag +
|
||||||
|
|
@ -475,7 +463,8 @@
|
||||||
fontFamily +
|
fontFamily +
|
||||||
"', Align: '" +
|
"', Align: '" +
|
||||||
textAlign +
|
textAlign +
|
||||||
"'",
|
"', Gap: " +
|
||||||
|
paragraphGap
|
||||||
);
|
);
|
||||||
|
|
||||||
var dynamicStyleId = "dynamicReaderStyles";
|
var dynamicStyleId = "dynamicReaderStyles";
|
||||||
|
|
@ -489,9 +478,11 @@
|
||||||
|
|
||||||
var newFontSize = parseFloat(fontSizeEm);
|
var newFontSize = parseFloat(fontSizeEm);
|
||||||
var newLineHeight = parseFloat(lineHeight);
|
var newLineHeight = parseFloat(lineHeight);
|
||||||
|
var newGap = parseFloat(paragraphGap);
|
||||||
|
|
||||||
if (isNaN(newFontSize) || newFontSize < 0.5 || newFontSize > 5.0) newFontSize = 1.0;
|
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(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 fontCss = "";
|
||||||
var selector = "body";
|
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 =
|
dynamicStyleElement.innerHTML =
|
||||||
` body {
|
` body {
|
||||||
font-size: ` +
|
font-size: ` +
|
||||||
|
|
@ -555,7 +557,7 @@
|
||||||
|
|
||||||
` +
|
` +
|
||||||
alignCss +
|
alignCss +
|
||||||
` `;
|
gapCss;
|
||||||
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
function () {
|
function () {
|
||||||
|
|
|
||||||
|
|
@ -321,6 +321,7 @@ fun ChapterWebView(
|
||||||
onTopChunkUpdated: (Int) -> Unit,
|
onTopChunkUpdated: (Int) -> Unit,
|
||||||
currentFontSize: Float,
|
currentFontSize: Float,
|
||||||
currentLineHeight: Float,
|
currentLineHeight: Float,
|
||||||
|
currentParagraphGap: Float,
|
||||||
onChapterInitiallyScrolled: () -> Unit,
|
onChapterInitiallyScrolled: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onTap: () -> Unit,
|
onTap: () -> Unit,
|
||||||
|
|
@ -456,6 +457,7 @@ fun ChapterWebView(
|
||||||
key,
|
key,
|
||||||
currentFontSize,
|
currentFontSize,
|
||||||
currentLineHeight,
|
currentLineHeight,
|
||||||
|
currentParagraphGap,
|
||||||
currentFontFamily,
|
currentFontFamily,
|
||||||
currentTextAlign
|
currentTextAlign
|
||||||
) {
|
) {
|
||||||
|
|
@ -714,7 +716,7 @@ fun ChapterWebView(
|
||||||
}
|
}
|
||||||
|
|
||||||
view?.evaluateJavascript(
|
view?.evaluateJavascript(
|
||||||
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}', $currentParagraphGap);",
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -829,7 +831,7 @@ fun ChapterWebView(
|
||||||
)
|
)
|
||||||
|
|
||||||
webView.evaluateJavascript(
|
webView.evaluateJavascript(
|
||||||
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}');",
|
"javascript:window.updateReaderStyles($currentFontSize, $currentLineHeight, '$fontNameForJs', '${currentTextAlign.cssValue}', $currentParagraphGap);",
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -287,76 +287,28 @@ fun processAndAddHighlight(
|
||||||
chapterIndex: Int,
|
chapterIndex: Int,
|
||||||
currentList: MutableList<UserHighlight>
|
currentList: MutableList<UserHighlight>
|
||||||
): String {
|
): String {
|
||||||
val newParts = newCfi.split('|')
|
// Scenario: Exact match -> Update color and text instead of stacking identical spans
|
||||||
val newStartFull = newParts.first()
|
val exactMatchIndex = currentList.indexOfFirst {
|
||||||
val newEndFull = newParts.last()
|
it.chapterIndex == chapterIndex && it.cfi == newCfi
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalCfi = "$finalStartPath:$finalStartOffset|$finalEndPath:$finalEndOffset"
|
if (exactMatchIndex != -1) {
|
||||||
currentList.add(UserHighlight(
|
val existing = currentList[exactMatchIndex]
|
||||||
cfi = finalCfi,
|
currentList[exactMatchIndex] = existing.copy(color = newColor, text = newText)
|
||||||
text = finalText,
|
return existing.cfi
|
||||||
color = newColor,
|
}
|
||||||
chapterIndex = chapterIndex,
|
|
||||||
note = finalNote
|
// Scenarios: Partial overlap or subsumption -> Add independently
|
||||||
))
|
currentList.add(
|
||||||
return finalCfi
|
UserHighlight(
|
||||||
|
cfi = newCfi,
|
||||||
|
text = newText,
|
||||||
|
color = newColor,
|
||||||
|
chapterIndex = chapterIndex,
|
||||||
|
note = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return newCfi
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- UI Components ---
|
// --- UI Components ---
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,7 @@ fun EpubReaderHost(
|
||||||
var pageInfoMode by remember { mutableStateOf(loadPageInfoMode(context)) }
|
var pageInfoMode by remember { mutableStateOf(loadPageInfoMode(context)) }
|
||||||
var pullToTurnEnabled by remember { mutableStateOf(loadPullToTurn(context)) }
|
var pullToTurnEnabled by remember { mutableStateOf(loadPullToTurn(context)) }
|
||||||
var showVisualOptionsSheet by remember { mutableStateOf(false) }
|
var showVisualOptionsSheet by remember { mutableStateOf(false) }
|
||||||
|
var removeEdgePadding by remember { mutableStateOf(loadRemoveEdgePadding(context)) }
|
||||||
|
|
||||||
var volumeScrollEnabled by remember {
|
var volumeScrollEnabled by remember {
|
||||||
mutableStateOf(loadVolumeScrollSetting(context))
|
mutableStateOf(loadVolumeScrollSetting(context))
|
||||||
|
|
@ -899,6 +900,7 @@ fun EpubReaderHost(
|
||||||
|
|
||||||
var currentFontSizeEm by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.fontSize) }
|
var currentFontSizeEm by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.fontSize) }
|
||||||
var currentLineHeight by remember(initialFormatSettings) { mutableFloatStateOf(initialFormatSettings.lineHeight) }
|
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 currentTextAlign by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.textAlign) }
|
||||||
var currentFontFamily by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.font) }
|
var currentFontFamily by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.font) }
|
||||||
var currentCustomFontPath by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.customPath) }
|
var currentCustomFontPath by remember(initialFormatSettings) { mutableStateOf(initialFormatSettings.customPath) }
|
||||||
|
|
@ -914,14 +916,14 @@ fun EpubReaderHost(
|
||||||
var showFontSelectionSheet by remember { mutableStateOf(false) }
|
var showFontSelectionSheet by remember { mutableStateOf(false) }
|
||||||
val fontSheetState = rememberModalBottomSheetState()
|
val fontSheetState = rememberModalBottomSheetState()
|
||||||
|
|
||||||
LaunchedEffect(currentFontSizeEm, currentLineHeight, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) {
|
LaunchedEffect(currentFontSizeEm, currentLineHeight, currentParagraphGap, currentFontFamily, currentCustomFontPath, currentTextAlign, isFormatLocal) {
|
||||||
if (isFormatLocal) {
|
if (isFormatLocal) {
|
||||||
saveLocalReaderSettings(
|
saveLocalReaderSettings(
|
||||||
context, bookId, currentFontSizeEm, currentLineHeight, currentFontFamily, currentCustomFontPath, currentTextAlign
|
context, bookId, currentFontSizeEm, currentLineHeight, currentParagraphGap, currentFontFamily, currentCustomFontPath, currentTextAlign
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
saveReaderSettings(
|
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
|
if (pageInfoMode == PageInfoMode.DEFAULT) PAGE_INFO_BAR_HEIGHT else 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val horizontalPadding = if (removeEdgePadding) 0.dp else 16.dp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = contentBottomPadding)
|
.padding(bottom = contentBottomPadding)
|
||||||
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
|
.padding(top = 16.dp, start = horizontalPadding, end = horizontalPadding)
|
||||||
.testTag("ReaderContainer")
|
.testTag("ReaderContainer")
|
||||||
) {
|
) {
|
||||||
if (chapters.isEmpty()) {
|
if (chapters.isEmpty()) {
|
||||||
|
|
@ -2521,6 +2525,7 @@ fun EpubReaderHost(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
currentFontSize = currentFontSizeEm,
|
currentFontSize = currentFontSizeEm,
|
||||||
currentLineHeight = currentLineHeight,
|
currentLineHeight = currentLineHeight,
|
||||||
|
currentParagraphGap = currentParagraphGap,
|
||||||
currentFontFamily = currentFontFamily,
|
currentFontFamily = currentFontFamily,
|
||||||
customFontPath = currentCustomFontPath,
|
customFontPath = currentCustomFontPath,
|
||||||
currentTextAlign = currentTextAlign,
|
currentTextAlign = currentTextAlign,
|
||||||
|
|
@ -2870,6 +2875,7 @@ fun EpubReaderHost(
|
||||||
searchQuery = searchState.searchQuery,
|
searchQuery = searchState.searchQuery,
|
||||||
fontSizeMultiplier = currentFontSizeEm,
|
fontSizeMultiplier = currentFontSizeEm,
|
||||||
lineHeightMultiplier = currentLineHeight,
|
lineHeightMultiplier = currentLineHeight,
|
||||||
|
paragraphGapMultiplier = currentParagraphGap,
|
||||||
fontFamily = activeFontFamily,
|
fontFamily = activeFontFamily,
|
||||||
textAlign = currentTextAlign,
|
textAlign = currentTextAlign,
|
||||||
activeHighlightPalette = currentHighlightPalette,
|
activeHighlightPalette = currentHighlightPalette,
|
||||||
|
|
@ -2881,6 +2887,7 @@ fun EpubReaderHost(
|
||||||
offset = ttsState.startOffsetInSource
|
offset = ttsState.startOffsetInSource
|
||||||
).takeIf { ttsState.currentText != null && ttsState.sourceCfi != null && ttsState.startOffsetInSource != -1 },
|
).takeIf { ttsState.currentText != null && ttsState.sourceCfi != null && ttsState.startOffsetInSource != -1 },
|
||||||
activeTextureId = activeTextureId,
|
activeTextureId = activeTextureId,
|
||||||
|
removeEdgePadding = removeEdgePadding,
|
||||||
initialChapterIndexInBook = lastKnownLocator?.chapterIndex,
|
initialChapterIndexInBook = lastKnownLocator?.chapterIndex,
|
||||||
modifier = Modifier.alpha(if (isPagerInitialized) 1f else 0f),
|
modifier = Modifier.alpha(if (isPagerInitialized) 1f else 0f),
|
||||||
onPaginatorReady = { newPaginator ->
|
onPaginatorReady = { newPaginator ->
|
||||||
|
|
@ -3877,6 +3884,8 @@ fun EpubReaderHost(
|
||||||
onFontSizeChange = { currentFontSizeEm = it },
|
onFontSizeChange = { currentFontSizeEm = it },
|
||||||
currentLineHeight = currentLineHeight,
|
currentLineHeight = currentLineHeight,
|
||||||
onLineHeightChange = { currentLineHeight = it },
|
onLineHeightChange = { currentLineHeight = it },
|
||||||
|
currentParagraphGap = currentParagraphGap,
|
||||||
|
onParagraphGapChange = { currentParagraphGap = it },
|
||||||
currentFont = currentFontFamily,
|
currentFont = currentFontFamily,
|
||||||
currentCustomFontName = if(currentCustomFontPath != null) {
|
currentCustomFontName = if(currentCustomFontPath != null) {
|
||||||
customFonts.find { it.path == currentCustomFontPath }?.displayName ?: "Custom Font"
|
customFonts.find { it.path == currentCustomFontPath }?.displayName ?: "Custom Font"
|
||||||
|
|
@ -3892,6 +3901,7 @@ fun EpubReaderHost(
|
||||||
onReset = {
|
onReset = {
|
||||||
currentFontSizeEm = DEFAULT_FONT_SIZE_VAL
|
currentFontSizeEm = DEFAULT_FONT_SIZE_VAL
|
||||||
currentLineHeight = DEFAULT_LINE_HEIGHT_VAL
|
currentLineHeight = DEFAULT_LINE_HEIGHT_VAL
|
||||||
|
currentParagraphGap = DEFAULT_PARAGRAPH_GAP_VAL
|
||||||
currentFontFamily = ReaderFont.ORIGINAL
|
currentFontFamily = ReaderFont.ORIGINAL
|
||||||
currentCustomFontPath = null
|
currentCustomFontPath = null
|
||||||
currentTextAlign = ReaderTextAlign.DEFAULT
|
currentTextAlign = ReaderTextAlign.DEFAULT
|
||||||
|
|
@ -4229,6 +4239,11 @@ fun EpubReaderHost(
|
||||||
pullToTurnEnabled = it
|
pullToTurnEnabled = it
|
||||||
savePullToTurn(context, it)
|
savePullToTurn(context, it)
|
||||||
},
|
},
|
||||||
|
removeEdgePadding = removeEdgePadding,
|
||||||
|
onRemoveEdgePaddingChange = {
|
||||||
|
removeEdgePadding = it
|
||||||
|
saveRemoveEdgePadding(context, it)
|
||||||
|
},
|
||||||
onDismiss = { showVisualOptionsSheet = false }
|
onDismiss = { showVisualOptionsSheet = false }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,12 +97,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
||||||
const val SETTINGS_PREFS_NAME = "epub_reader_settings"
|
const val SETTINGS_PREFS_NAME = "epub_reader_settings"
|
||||||
private const val TEXT_ALIGN_KEY = "reader_text_align"
|
private const val TEXT_ALIGN_KEY = "reader_text_align"
|
||||||
private const val FONT_SIZE_KEY = "reader_font_size"
|
private const val FONT_SIZE_KEY = "reader_font_size"
|
||||||
private const val LINE_HEIGHT_KEY = "reader_line_height"
|
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 AUTO_SCROLL_SPEED_KEY = "reader_auto_scroll_speed"
|
||||||
private const val FONT_FAMILY_KEY = "reader_font_family"
|
private const val FONT_FAMILY_KEY = "reader_font_family"
|
||||||
private const val TAP_TO_NAVIGATE_ENABLED_KEY = "tap_to_navigate_enabled"
|
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_FONT_SIZE_VAL = 1.0f
|
||||||
const val DEFAULT_LINE_HEIGHT_VAL = 1.6f
|
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) {
|
enum class ReaderFont(val id: String, val displayName: String, val fontFamilyName: String) {
|
||||||
ORIGINAL("original", "Original", "Original"),
|
ORIGINAL("original", "Original", "Original"),
|
||||||
|
|
@ -144,6 +147,7 @@ enum class PageInfoMode(val id: Int, val title: String) {
|
||||||
data class FormatSettings(
|
data class FormatSettings(
|
||||||
val fontSize: Float,
|
val fontSize: Float,
|
||||||
val lineHeight: Float,
|
val lineHeight: Float,
|
||||||
|
val paragraphGap: Float,
|
||||||
val font: ReaderFont,
|
val font: ReaderFont,
|
||||||
val customPath: String?,
|
val customPath: String?,
|
||||||
val textAlign: ReaderTextAlign
|
val textAlign: ReaderTextAlign
|
||||||
|
|
@ -152,6 +156,7 @@ data class FormatSettings(
|
||||||
private const val FORMAT_IS_LOCAL_PREFIX = "format_is_local_"
|
private const val FORMAT_IS_LOCAL_PREFIX = "format_is_local_"
|
||||||
private const val LOCAL_FONT_SIZE_PREFIX = "local_font_size_"
|
private const val LOCAL_FONT_SIZE_PREFIX = "local_font_size_"
|
||||||
private const val LOCAL_LINE_HEIGHT_PREFIX = "local_line_height_"
|
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_FONT_FAMILY_PREFIX = "local_font_family_"
|
||||||
private const val LOCAL_TEXT_ALIGN_PREFIX = "local_text_align_"
|
private const val LOCAL_TEXT_ALIGN_PREFIX = "local_text_align_"
|
||||||
|
|
||||||
|
|
@ -170,6 +175,7 @@ fun saveLocalReaderSettings(
|
||||||
bookId: String,
|
bookId: String,
|
||||||
fontSize: Float,
|
fontSize: Float,
|
||||||
lineHeight: Float,
|
lineHeight: Float,
|
||||||
|
paragraphGap: Float,
|
||||||
fontFamily: ReaderFont,
|
fontFamily: ReaderFont,
|
||||||
customFontPath: String?,
|
customFontPath: String?,
|
||||||
textAlign: ReaderTextAlign
|
textAlign: ReaderTextAlign
|
||||||
|
|
@ -178,6 +184,7 @@ fun saveLocalReaderSettings(
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putFloat(LOCAL_FONT_SIZE_PREFIX + bookId, fontSize)
|
putFloat(LOCAL_FONT_SIZE_PREFIX + bookId, fontSize)
|
||||||
putFloat(LOCAL_LINE_HEIGHT_PREFIX + bookId, lineHeight)
|
putFloat(LOCAL_LINE_HEIGHT_PREFIX + bookId, lineHeight)
|
||||||
|
putFloat(LOCAL_PARAGRAPH_GAP_PREFIX + bookId, paragraphGap)
|
||||||
if (customFontPath != null) {
|
if (customFontPath != null) {
|
||||||
putString(LOCAL_FONT_FAMILY_PREFIX + bookId, "custom|$customFontPath")
|
putString(LOCAL_FONT_FAMILY_PREFIX + bookId, "custom|$customFontPath")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -234,6 +241,12 @@ fun loadFormatSettings(context: Context, bookId: String, isLocal: Boolean): Form
|
||||||
prefs.getFloat(LINE_HEIGHT_KEY, DEFAULT_LINE_HEIGHT_VAL)
|
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)) {
|
val savedFontVal = if (isLocal && prefs.contains(LOCAL_FONT_FAMILY_PREFIX + bookId)) {
|
||||||
prefs.getString(LOCAL_FONT_FAMILY_PREFIX + bookId, ReaderFont.ORIGINAL.id) ?: ReaderFont.ORIGINAL.id
|
prefs.getString(LOCAL_FONT_FAMILY_PREFIX + bookId, ReaderFont.ORIGINAL.id) ?: ReaderFont.ORIGINAL.id
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -253,7 +266,7 @@ fun loadFormatSettings(context: Context, bookId: String, isLocal: Boolean): Form
|
||||||
}
|
}
|
||||||
val textAlign = ReaderTextAlign.entries.find { it.id == alignId } ?: ReaderTextAlign.DEFAULT
|
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(
|
fun getComposeFontFamily(
|
||||||
|
|
@ -291,6 +304,7 @@ fun saveReaderSettings(
|
||||||
context: Context,
|
context: Context,
|
||||||
fontSize: Float,
|
fontSize: Float,
|
||||||
lineHeight: Float,
|
lineHeight: Float,
|
||||||
|
paragraphGap: Float,
|
||||||
fontFamily: ReaderFont,
|
fontFamily: ReaderFont,
|
||||||
customFontPath: String?,
|
customFontPath: String?,
|
||||||
textAlign: ReaderTextAlign
|
textAlign: ReaderTextAlign
|
||||||
|
|
@ -299,6 +313,7 @@ fun saveReaderSettings(
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putFloat(FONT_SIZE_KEY, fontSize)
|
putFloat(FONT_SIZE_KEY, fontSize)
|
||||||
putFloat(LINE_HEIGHT_KEY, lineHeight)
|
putFloat(LINE_HEIGHT_KEY, lineHeight)
|
||||||
|
putFloat(PARAGRAPH_GAP_KEY, paragraphGap)
|
||||||
if (customFontPath != null) {
|
if (customFontPath != null) {
|
||||||
putString(FONT_FAMILY_KEY, "custom|$customFontPath")
|
putString(FONT_FAMILY_KEY, "custom|$customFontPath")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -345,6 +360,8 @@ fun ReaderTextFormatPanel(
|
||||||
onFontSizeChange: (Float) -> Unit,
|
onFontSizeChange: (Float) -> Unit,
|
||||||
currentLineHeight: Float,
|
currentLineHeight: Float,
|
||||||
onLineHeightChange: (Float) -> Unit,
|
onLineHeightChange: (Float) -> Unit,
|
||||||
|
currentParagraphGap: Float, // NEW
|
||||||
|
onParagraphGapChange: (Float) -> Unit, // NEW
|
||||||
currentFont: ReaderFont,
|
currentFont: ReaderFont,
|
||||||
currentCustomFontName: String?,
|
currentCustomFontName: String?,
|
||||||
onFontOptionClick: () -> Unit,
|
onFontOptionClick: () -> Unit,
|
||||||
|
|
@ -363,10 +380,10 @@ fun ReaderTextFormatPanel(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(28.dp),
|
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.95f),
|
color = MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = 0.98f),
|
||||||
tonalElevation = 0.dp,
|
tonalElevation = 0.dp,
|
||||||
shadowElevation = 0.dp,
|
shadowElevation = 8.dp,
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f)),
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f)),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -375,6 +392,7 @@ fun ReaderTextFormatPanel(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
|
// Header Row (Local/Global + Close/Reset)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|
@ -439,82 +457,109 @@ fun ReaderTextFormatPanel(
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(
|
// FONT & ALIGNMENT SECTION
|
||||||
modifier = Modifier.fillMaxWidth(),
|
Text(
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
text = "FONT & ALIGNMENT",
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Row(
|
||||||
onClick = onFontOptionClick,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
shape = RoundedCornerShape(12.dp),
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
modifier = Modifier
|
|
||||||
.weight(0.45f)
|
|
||||||
.height(48.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
Text(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
text = "Aa",
|
||||||
modifier = Modifier.padding(horizontal = 12.dp)
|
style = MaterialTheme.typography.titleMedium,
|
||||||
) {
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
modifier = Modifier.padding(end = 12.dp)
|
||||||
|
)
|
||||||
Text(
|
Text(
|
||||||
text = currentCustomFontName ?: currentFont.displayName,
|
text = currentCustomFontName ?: currentFont.displayName,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
maxLines = 1, overflow = TextOverflow.Ellipsis
|
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(
|
Spacer(Modifier.height(8.dp))
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
// Alignment Button (Full width Segmented)
|
||||||
modifier = Modifier
|
Surface(
|
||||||
.weight(0.55f)
|
shape = RoundedCornerShape(12.dp),
|
||||||
.height(48.dp)
|
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
) {
|
modifier = Modifier
|
||||||
Row {
|
.fillMaxWidth()
|
||||||
ReaderTextAlign.entries.forEach { align ->
|
.height(48.dp)
|
||||||
val isSelected = currentTextAlign == align
|
) {
|
||||||
Column(
|
Row {
|
||||||
modifier = Modifier
|
ReaderTextAlign.entries.forEach { align ->
|
||||||
.fillMaxHeight()
|
val isSelected = currentTextAlign == align
|
||||||
.weight(1f)
|
Column(
|
||||||
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
|
modifier = Modifier
|
||||||
.clickable { onTextAlignChange(align) },
|
.fillMaxHeight()
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
.weight(1f)
|
||||||
verticalArrangement = Arrangement.Center
|
.clip(RoundedCornerShape(12.dp))
|
||||||
) {
|
.background(if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent)
|
||||||
Icon(
|
.clickable { onTextAlignChange(align) },
|
||||||
painter = androidx.compose.ui.res.painterResource(id = align.iconResId),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
contentDescription = align.displayName,
|
verticalArrangement = Arrangement.Center
|
||||||
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
) {
|
||||||
modifier = Modifier.size(18.dp)
|
Icon(
|
||||||
)
|
painter = painterResource(id = align.iconResId),
|
||||||
Text(
|
contentDescription = align.displayName,
|
||||||
text = align.displayName,
|
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
modifier = Modifier.size(20.dp)
|
||||||
fontSize = 10.sp,
|
)
|
||||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
|
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
|
// Sliders
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
// Size
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
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(
|
Slider(
|
||||||
value = currentFontSize,
|
value = currentFontSize,
|
||||||
onValueChange = onFontSizeChange,
|
onValueChange = onFontSizeChange,
|
||||||
|
|
@ -522,10 +567,11 @@ fun ReaderTextFormatPanel(
|
||||||
steps = 24,
|
steps = 24,
|
||||||
modifier = Modifier.weight(1f)
|
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)) {
|
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(
|
Slider(
|
||||||
value = currentLineHeight,
|
value = currentLineHeight,
|
||||||
onValueChange = onLineHeightChange,
|
onValueChange = onLineHeightChange,
|
||||||
|
|
@ -533,9 +579,22 @@ fun ReaderTextFormatPanel(
|
||||||
steps = 14,
|
steps = 14,
|
||||||
modifier = Modifier.weight(1f)
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun VisualOptionsSheet(
|
fun VisualOptionsSheet(
|
||||||
|
|
@ -656,6 +727,8 @@ fun VisualOptionsSheet(
|
||||||
onPageInfoModeChange: (PageInfoMode) -> Unit,
|
onPageInfoModeChange: (PageInfoMode) -> Unit,
|
||||||
pullToTurnEnabled: Boolean,
|
pullToTurnEnabled: Boolean,
|
||||||
onPullToTurnChange: (Boolean) -> Unit,
|
onPullToTurnChange: (Boolean) -> Unit,
|
||||||
|
removeEdgePadding: Boolean,
|
||||||
|
onRemoveEdgePaddingChange: (Boolean) -> Unit,
|
||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
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))
|
Spacer(modifier = Modifier.height(32.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,8 @@ class BookPaginator(
|
||||||
private val allFontFaces: List<FontFaceInfo>,
|
private val allFontFaces: List<FontFaceInfo>,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mathMLRenderer: MathMLRenderer,
|
private val mathMLRenderer: MathMLRenderer,
|
||||||
private val userTextAlign: TextAlign?
|
private val userTextAlign: TextAlign?,
|
||||||
|
private val paragraphGapMultiplier: Float
|
||||||
) : IPaginator {
|
) : IPaginator {
|
||||||
override var totalPageCount by mutableIntStateOf(0)
|
override var totalPageCount by mutableIntStateOf(0)
|
||||||
private set
|
private set
|
||||||
|
|
@ -271,7 +272,7 @@ class BookPaginator(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun generateConfigurationHash(): Int {
|
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()
|
val hash = configString.hashCode()
|
||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
@ -323,6 +324,8 @@ class BookPaginator(
|
||||||
)
|
)
|
||||||
bookCacheDao.insertConfigurationCache(newCache)
|
bookCacheDao.insertConfigurationCache(newCache)
|
||||||
|
|
||||||
|
bookCacheDao.cleanupOldConfigurations(bookId)
|
||||||
|
|
||||||
if (finalizedChapterCounts.size >= chapters.size) {
|
if (finalizedChapterCounts.size >= chapters.size) {
|
||||||
pageCountsAreAccurate = true
|
pageCountsAreAccurate = true
|
||||||
}
|
}
|
||||||
|
|
@ -418,7 +421,8 @@ class BookPaginator(
|
||||||
themeTextColor = themeTextColor,
|
themeTextColor = themeTextColor,
|
||||||
chapterAbsPath = chapter.absPath,
|
chapterAbsPath = chapter.absPath,
|
||||||
extractionBasePath = extractionBasePath,
|
extractionBasePath = extractionBasePath,
|
||||||
userTextAlign = userTextAlign
|
userTextAlign = userTextAlign,
|
||||||
|
paragraphGapMultiplier = paragraphGapMultiplier
|
||||||
)
|
)
|
||||||
|
|
||||||
bookCacheDao.getProcessedChapter(bookId, chapterIndex)?.let { cachedChapter ->
|
bookCacheDao.getProcessedChapter(bookId, chapterIndex)?.let { cachedChapter ->
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ class ContentStyler(
|
||||||
private val themeTextColor: Color,
|
private val themeTextColor: Color,
|
||||||
private val chapterAbsPath: String,
|
private val chapterAbsPath: String,
|
||||||
private val extractionBasePath: String,
|
private val extractionBasePath: String,
|
||||||
private val userTextAlign: TextAlign?
|
private val userTextAlign: TextAlign?,
|
||||||
|
private val paragraphGapMultiplier: Float
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun style(semanticBlocks: List<SemanticBlock>): List<ContentBlock> {
|
fun style(semanticBlocks: List<SemanticBlock>): List<ContentBlock> {
|
||||||
|
|
@ -102,6 +103,18 @@ class ContentStyler(
|
||||||
|
|
||||||
private fun styleBlock(block: SemanticBlock): ContentBlock? {
|
private fun styleBlock(block: SemanticBlock): ContentBlock? {
|
||||||
val themedStyle = applyThemeToStyle(block.style)
|
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) {
|
return when (block) {
|
||||||
is SemanticParagraph -> {
|
is SemanticParagraph -> {
|
||||||
val computedTextAlign = userTextAlign ?: themedStyle.paragraphStyle.textAlign
|
val computedTextAlign = userTextAlign ?: themedStyle.paragraphStyle.textAlign
|
||||||
|
|
@ -109,7 +122,7 @@ class ContentStyler(
|
||||||
ParagraphBlock(
|
ParagraphBlock(
|
||||||
content = buildAnnotatedString(block, themedStyle),
|
content = buildAnnotatedString(block, themedStyle),
|
||||||
textAlign = computedTextAlign,
|
textAlign = computedTextAlign,
|
||||||
style = themedStyle.blockStyle,
|
style = finalBlockStyle,
|
||||||
elementId = block.elementId,
|
elementId = block.elementId,
|
||||||
cfi = block.cfi,
|
cfi = block.cfi,
|
||||||
startCharOffsetInSource = block.startCharOffsetInSource,
|
startCharOffsetInSource = block.startCharOffsetInSource,
|
||||||
|
|
@ -353,14 +366,20 @@ class ContentStyler(
|
||||||
textMotion = mergedParagraphStyle.textMotion
|
textMotion = mergedParagraphStyle.textMotion
|
||||||
)
|
)
|
||||||
|
|
||||||
var initialSpanStyle = baseTextStyle.toSpanStyle()
|
val isCustomFont = baseTextStyle.fontFamily != null && baseTextStyle.fontFamily != FontFamily.Default
|
||||||
.merge(blockStyle.spanStyle)
|
|
||||||
.copy(fontFamily = baseTextStyle.fontFamily)
|
|
||||||
|
|
||||||
if (rootFontFamily == FontFamily.Monospace) {
|
val effectiveBlockFontFamily = if (rootFontFamily == FontFamily.Monospace) {
|
||||||
initialSpanStyle = initialSpanStyle.copy(fontFamily = rootFontFamily)
|
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}")
|
Timber.d("ContentStyler: InitialSpanStyle. BaseFontSize=${baseTextStyle.fontSize}, BlockFontSize=${blockStyle.spanStyle.fontSize} -> Merged=${initialSpanStyle.fontSize}")
|
||||||
|
|
||||||
withStyle(finalParagraphStyle) {
|
withStyle(finalParagraphStyle) {
|
||||||
|
|
@ -368,14 +387,23 @@ class ContentStyler(
|
||||||
append(block.text)
|
append(block.text)
|
||||||
block.spans.sortedBy { it.start }.forEach { span ->
|
block.spans.sortedBy { it.start }.forEach { span ->
|
||||||
val themedSpanStyle = applyThemeToStyle(span.style)
|
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) {
|
val baselineShift = when (span.tag) {
|
||||||
"sub" -> BaselineShift.Subscript
|
"sub" -> BaselineShift.Subscript
|
||||||
"sup" -> BaselineShift.Superscript
|
"sup" -> BaselineShift.Superscript
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
val finalSpanStyle = themedSpanStyle.spanStyle.copy(
|
val finalSpanStyle = themedSpanStyle.spanStyle.copy(
|
||||||
fontFamily = fontFamily,
|
fontFamily = effectiveSpanFontFamily,
|
||||||
baselineShift = baselineShift
|
baselineShift = baselineShift
|
||||||
)
|
)
|
||||||
addStyle(initialSpanStyle.merge(finalSpanStyle), span.start, span.end)
|
addStyle(initialSpanStyle.merge(finalSpanStyle), span.start, span.end)
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,7 @@ private fun WrappingContentLayout(
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalSerializationApi::class, FlowPreview::class)
|
@OptIn(ExperimentalFoundationApi::class, ExperimentalSerializationApi::class, FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PaginatedReaderScreen(
|
fun PaginatedReaderScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
book: EpubBook,
|
book: EpubBook,
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
effectiveBg: Color,
|
effectiveBg: Color,
|
||||||
|
|
@ -500,11 +501,12 @@ fun PaginatedReaderScreen(
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
fontSizeMultiplier: Float,
|
fontSizeMultiplier: Float,
|
||||||
lineHeightMultiplier: Float,
|
lineHeightMultiplier: Float,
|
||||||
|
paragraphGapMultiplier: Float,
|
||||||
fontFamily: FontFamily,
|
fontFamily: FontFamily,
|
||||||
textAlign: ReaderTextAlign,
|
textAlign: ReaderTextAlign,
|
||||||
ttsHighlightInfo: TtsHighlightInfo?,
|
ttsHighlightInfo: TtsHighlightInfo?,
|
||||||
initialChapterIndexInBook: Int?,
|
initialChapterIndexInBook: Int?,
|
||||||
modifier: Modifier = Modifier,
|
removeEdgePadding: Boolean = false,
|
||||||
onPaginatorReady: (IPaginator) -> Unit,
|
onPaginatorReady: (IPaginator) -> Unit,
|
||||||
onTap: (Offset?) -> Unit,
|
onTap: (Offset?) -> Unit,
|
||||||
isProUser: Boolean,
|
isProUser: Boolean,
|
||||||
|
|
@ -553,6 +555,7 @@ fun PaginatedReaderScreen(
|
||||||
|
|
||||||
var debouncedFontSizeMult by remember { mutableFloatStateOf(fontSizeMultiplier) }
|
var debouncedFontSizeMult by remember { mutableFloatStateOf(fontSizeMultiplier) }
|
||||||
var debouncedLineHeightMult by remember { mutableFloatStateOf(lineHeightMultiplier) }
|
var debouncedLineHeightMult by remember { mutableFloatStateOf(lineHeightMultiplier) }
|
||||||
|
var debouncedParagraphGapMult by remember { mutableFloatStateOf(paragraphGapMultiplier) }
|
||||||
var debouncedFontFamily by remember { mutableStateOf(fontFamily) }
|
var debouncedFontFamily by remember { mutableStateOf(fontFamily) }
|
||||||
var debouncedTextAlign by remember { mutableStateOf(textAlign) }
|
var debouncedTextAlign by remember { mutableStateOf(textAlign) }
|
||||||
|
|
||||||
|
|
@ -611,8 +614,8 @@ fun PaginatedReaderScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, fontFamily, textAlign) {
|
LaunchedEffect(fontSizeMultiplier, lineHeightMultiplier, paragraphGapMultiplier, fontFamily, textAlign) {
|
||||||
if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) {
|
if (fontSizeMultiplier != debouncedFontSizeMult || lineHeightMultiplier != debouncedLineHeightMult || paragraphGapMultiplier != debouncedParagraphGapMult || fontFamily != debouncedFontFamily || textAlign != debouncedTextAlign) {
|
||||||
Timber.d("Formatting changed. Waiting for debounce.")
|
Timber.d("Formatting changed. Waiting for debounce.")
|
||||||
delay(400L)
|
delay(400L)
|
||||||
|
|
||||||
|
|
@ -627,6 +630,7 @@ fun PaginatedReaderScreen(
|
||||||
|
|
||||||
debouncedFontSizeMult = fontSizeMultiplier
|
debouncedFontSizeMult = fontSizeMultiplier
|
||||||
debouncedLineHeightMult = lineHeightMultiplier
|
debouncedLineHeightMult = lineHeightMultiplier
|
||||||
|
debouncedParagraphGapMult = paragraphGapMultiplier
|
||||||
debouncedFontFamily = fontFamily
|
debouncedFontFamily = fontFamily
|
||||||
debouncedTextAlign = textAlign
|
debouncedTextAlign = textAlign
|
||||||
Timber.d("Debounce complete. Applying new format settings.")
|
Timber.d("Debounce complete. Applying new format settings.")
|
||||||
|
|
@ -642,7 +646,7 @@ fun PaginatedReaderScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val horizontalPadding = 16.dp
|
val horizontalPadding = if (removeEdgePadding) 0.dp else 16.dp
|
||||||
val verticalPadding = 16.dp
|
val verticalPadding = 16.dp
|
||||||
|
|
||||||
val textConstraints =
|
val textConstraints =
|
||||||
|
|
@ -673,8 +677,8 @@ fun PaginatedReaderScreen(
|
||||||
remember(initialChapterIndexInBook, anchorLocatorForReconfig) {
|
remember(initialChapterIndexInBook, anchorLocatorForReconfig) {
|
||||||
anchorLocatorForReconfig?.chapterIndex ?: initialChapterIndexInBook ?: 0
|
anchorLocatorForReconfig?.chapterIndex ?: initialChapterIndexInBook ?: 0
|
||||||
}
|
}
|
||||||
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText) {
|
val paginator = remember(book, textConstraints, isDarkTheme, textStyle, userTextAlign, effectiveBg, effectiveText, debouncedParagraphGapMult) {
|
||||||
val userAgentStylesheet = UserAgentStylesheet.default
|
val userAgentStylesheet = UserAgentStylesheet.default
|
||||||
var allRules = OptimizedCssRules()
|
var allRules = OptimizedCssRules()
|
||||||
val allFontFaces = mutableListOf<FontFaceInfo>()
|
val allFontFaces = mutableListOf<FontFaceInfo>()
|
||||||
|
|
||||||
|
|
@ -739,7 +743,8 @@ fun PaginatedReaderScreen(
|
||||||
allFontFaces = allFontFaces,
|
allFontFaces = allFontFaces,
|
||||||
context = context.applicationContext,
|
context = context.applicationContext,
|
||||||
mathMLRenderer = mathMLRenderer,
|
mathMLRenderer = mathMLRenderer,
|
||||||
userTextAlign = userTextAlign
|
userTextAlign = userTextAlign,
|
||||||
|
paragraphGapMultiplier = debouncedParagraphGapMult
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1171,7 +1176,6 @@ private fun TextWithEmphasis(
|
||||||
is HeaderBlock -> block.startCharOffsetInSource
|
is HeaderBlock -> block.startCharOffsetInSource
|
||||||
is QuoteBlock -> block.startCharOffsetInSource
|
is QuoteBlock -> block.startCharOffsetInSource
|
||||||
is ListItemBlock -> block.startCharOffsetInSource
|
is ListItemBlock -> block.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val isStart = block.blockIndex == activeSelection.startBlockIndex && currentBlockAbs == activeSelection.startBlockCharOffset
|
val isStart = block.blockIndex == activeSelection.startBlockIndex && currentBlockAbs == activeSelection.startBlockCharOffset
|
||||||
|
|
@ -1276,76 +1280,6 @@ private fun TextWithEmphasis(
|
||||||
}
|
}
|
||||||
.then(customDrawer)
|
.then(customDrawer)
|
||||||
.pointerInput(userHighlights, text) {
|
.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(
|
detectTapGestures(
|
||||||
onLongPress = { offset ->
|
onLongPress = { offset ->
|
||||||
textLayoutResult?.let { layout ->
|
textLayoutResult?.let { layout ->
|
||||||
|
|
@ -1355,7 +1289,6 @@ private fun TextWithEmphasis(
|
||||||
var start = wordBoundary.start
|
var start = wordBoundary.start
|
||||||
var end = wordBoundary.end
|
var end = wordBoundary.end
|
||||||
|
|
||||||
// Trim trailing/leading punctuations for a cleaner word selection
|
|
||||||
val textStr = text.text
|
val textStr = text.text
|
||||||
while (start < end && start < textStr.length && !textStr[start].isLetterOrDigit()) start++
|
while (start < end && start < textStr.length && !textStr[start].isLetterOrDigit()) start++
|
||||||
while (end > start && end <= textStr.length && !textStr[end - 1].isLetterOrDigit()) end--
|
while (end > start && end <= textStr.length && !textStr[end - 1].isLetterOrDigit()) end--
|
||||||
|
|
@ -1377,7 +1310,6 @@ private fun TextWithEmphasis(
|
||||||
is HeaderBlock -> block.startCharOffsetInSource
|
is HeaderBlock -> block.startCharOffsetInSource
|
||||||
is QuoteBlock -> block.startCharOffsetInSource
|
is QuoteBlock -> block.startCharOffsetInSource
|
||||||
is ListItemBlock -> block.startCharOffsetInSource
|
is ListItemBlock -> block.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectionChange(
|
onSelectionChange(
|
||||||
|
|
@ -1402,14 +1334,27 @@ private fun TextWithEmphasis(
|
||||||
},
|
},
|
||||||
onTap = { offset ->
|
onTap = { offset ->
|
||||||
textLayoutResult?.let { layout ->
|
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 charOffset = layout.getOffsetForPosition(offset)
|
||||||
val urlAnnotation = text.getStringAnnotations(
|
val urlAnnotation = text.getStringAnnotations("URL", charOffset, charOffset).firstOrNull()
|
||||||
"URL", charOffset, charOffset
|
|
||||||
).firstOrNull()
|
|
||||||
if (urlAnnotation != null) onLinkClick(urlAnnotation.item)
|
if (urlAnnotation != null) onLinkClick(urlAnnotation.item)
|
||||||
else onGeneralTap(offset)
|
else onGeneralTap(offset)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}, onTextLayout = {
|
}, onTextLayout = {
|
||||||
textLayoutResult = it
|
textLayoutResult = it
|
||||||
if (layoutCoordinates != null && block.cfi != null) {
|
if (layoutCoordinates != null && block.cfi != null) {
|
||||||
|
|
@ -1713,7 +1658,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> firstTextBlock.startCharOffsetInSource
|
is HeaderBlock -> firstTextBlock.startCharOffsetInSource
|
||||||
is QuoteBlock -> firstTextBlock.startCharOffsetInSource
|
is QuoteBlock -> firstTextBlock.startCharOffsetInSource
|
||||||
is ListItemBlock -> firstTextBlock.startCharOffsetInSource
|
is ListItemBlock -> firstTextBlock.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val newTextPerBlock = (previousSel?.textPerBlock ?: emptyMap()).toMutableMap()
|
val newTextPerBlock = (previousSel?.textPerBlock ?: emptyMap()).toMutableMap()
|
||||||
|
|
@ -2853,7 +2797,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> block.startCharOffsetInSource
|
is HeaderBlock -> block.startCharOffsetInSource
|
||||||
is QuoteBlock -> block.startCharOffsetInSource
|
is QuoteBlock -> block.startCharOffsetInSource
|
||||||
is ListItemBlock -> block.startCharOffsetInSource
|
is ListItemBlock -> block.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
val isStartBlockPart = block.blockIndex == sel.startBlockIndex && currentBlockAbs == sel.startBlockCharOffset
|
val isStartBlockPart = block.blockIndex == sel.startBlockIndex && currentBlockAbs == sel.startBlockCharOffset
|
||||||
val isEndBlockPart = block.blockIndex == sel.endBlockIndex && currentBlockAbs == sel.endBlockCharOffset
|
val isEndBlockPart = block.blockIndex == sel.endBlockIndex && currentBlockAbs == sel.endBlockCharOffset
|
||||||
|
|
@ -3008,7 +2951,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> block.startCharOffsetInSource
|
is HeaderBlock -> block.startCharOffsetInSource
|
||||||
is QuoteBlock -> block.startCharOffsetInSource
|
is QuoteBlock -> block.startCharOffsetInSource
|
||||||
is ListItemBlock -> block.startCharOffsetInSource
|
is ListItemBlock -> block.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
var newStartBlockAbs = if (isStartHandle) currentBlockAbs else sel.startBlockCharOffset
|
var newStartBlockAbs = if (isStartHandle) currentBlockAbs else sel.startBlockCharOffset
|
||||||
var newEndBlockAbs = if (!isStartHandle) currentBlockAbs else sel.endBlockCharOffset
|
var newEndBlockAbs = if (!isStartHandle) currentBlockAbs else sel.endBlockCharOffset
|
||||||
|
|
@ -3044,7 +2986,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> (b.third as HeaderBlock).startCharOffsetInSource
|
is HeaderBlock -> (b.third as HeaderBlock).startCharOffsetInSource
|
||||||
is QuoteBlock -> (b.third as QuoteBlock).startCharOffsetInSource
|
is QuoteBlock -> (b.third as QuoteBlock).startCharOffsetInSource
|
||||||
is ListItemBlock -> (b.third as ListItemBlock).startCharOffsetInSource
|
is ListItemBlock -> (b.third as ListItemBlock).startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -3061,7 +3002,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> b.third.startCharOffsetInSource
|
is HeaderBlock -> b.third.startCharOffsetInSource
|
||||||
is QuoteBlock -> b.third.startCharOffsetInSource
|
is QuoteBlock -> b.third.startCharOffsetInSource
|
||||||
is ListItemBlock -> b.third.startCharOffsetInSource
|
is ListItemBlock -> b.third.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
val isStartBlockPart = b.third.blockIndex == newStartIdx && bAbs == newStartBlockAbs
|
val isStartBlockPart = b.third.blockIndex == newStartIdx && bAbs == newStartBlockAbs
|
||||||
val isEndBlockPart = b.third.blockIndex == newEndIdx && bAbs == newEndBlockAbs
|
val isEndBlockPart = b.third.blockIndex == newEndIdx && bAbs == newEndBlockAbs
|
||||||
|
|
@ -3092,7 +3032,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> it.third.startCharOffsetInSource
|
is HeaderBlock -> it.third.startCharOffsetInSource
|
||||||
is QuoteBlock -> it.third.startCharOffsetInSource
|
is QuoteBlock -> it.third.startCharOffsetInSource
|
||||||
is ListItemBlock -> it.third.startCharOffsetInSource
|
is ListItemBlock -> it.third.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
abs == newStartBlockAbs
|
abs == newStartBlockAbs
|
||||||
}
|
}
|
||||||
|
|
@ -3187,7 +3126,6 @@ internal fun PaginatedReaderContent(
|
||||||
is HeaderBlock -> block.startCharOffsetInSource
|
is HeaderBlock -> block.startCharOffsetInSource
|
||||||
is QuoteBlock -> block.startCharOffsetInSource
|
is QuoteBlock -> block.startCharOffsetInSource
|
||||||
is ListItemBlock -> block.startCharOffsetInSource
|
is ListItemBlock -> block.startCharOffsetInSource
|
||||||
else -> 0
|
|
||||||
}
|
}
|
||||||
blockAbs == targetBlockAbs
|
blockAbs == targetBlockAbs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@ class PaginatedReaderViewModel : ViewModel() {
|
||||||
themeTextColor: androidx.compose.ui.graphics.Color,
|
themeTextColor: androidx.compose.ui.graphics.Color,
|
||||||
context: Context,
|
context: Context,
|
||||||
initialChapterToPaginate: Int?,
|
initialChapterToPaginate: Int?,
|
||||||
mathMLRenderer: MathMLRenderer
|
mathMLRenderer: MathMLRenderer,
|
||||||
|
paragraphGapMultiplier: Float
|
||||||
) {
|
) {
|
||||||
if (paginator != null) return
|
if (paginator != null) return
|
||||||
|
|
||||||
|
|
@ -142,7 +143,8 @@ class PaginatedReaderViewModel : ViewModel() {
|
||||||
allFontFaces = allFontFaces,
|
allFontFaces = allFontFaces,
|
||||||
context = context.applicationContext,
|
context = context.applicationContext,
|
||||||
mathMLRenderer = mathMLRenderer,
|
mathMLRenderer = mathMLRenderer,
|
||||||
userTextAlign = null
|
userTextAlign = null,
|
||||||
|
paragraphGapMultiplier = paragraphGapMultiplier
|
||||||
)
|
)
|
||||||
paginator = newPaginator
|
paginator = newPaginator
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,16 @@ abstract class BookCacheDao {
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
abstract suspend fun insertConfigurationCache(cache: ConfigurationCache)
|
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(
|
@Database(
|
||||||
|
|
|
||||||
|
|
@ -631,6 +631,8 @@
|
||||||
<string name="visual_options_progress_bar_desc">The reading progress and chapter indicator at the bottom of the screen.</string>
|
<string name="visual_options_progress_bar_desc">The reading progress and chapter indicator at the bottom of the screen.</string>
|
||||||
<string name="visual_options_seamless_chapter">Seamless Chapter Transition</string>
|
<string name="visual_options_seamless_chapter">Seamless Chapter Transition</string>
|
||||||
<string name="visual_options_seamless_chapter_desc">Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation.</string>
|
<string name="visual_options_seamless_chapter_desc">Instantly load the next/previous chapter when scrolling past the end, without the pull-to-refresh animation.</string>
|
||||||
|
<string name="visual_options_edge_padding">Remove Edge Padding</string>
|
||||||
|
<string name="visual_options_edge_padding_desc">Removes the horizontal gap on the left and right edges.</string>
|
||||||
|
|
||||||
<!-- ExternalDictionaryHelper.kt -->
|
<!-- ExternalDictionaryHelper.kt -->
|
||||||
<string name="dict_app_label_search">Search</string>
|
<string name="dict_app_label_search">Search</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue