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
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -287,76 +287,28 @@ fun processAndAddHighlight(
|
|||
chapterIndex: Int,
|
||||
currentList: MutableList<UserHighlight>
|
||||
): 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 ---
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue