Added customizable highlight color palette in epub (#6)
This introduces a user-configurable palette for text highlighting. The four default highlight colors can now be customized by the user from an expanded set of predefined colors. Changes include: - Added a `PaletteManagerDialog` to allow users to select and save their preferred highlight colors. - Implemented `saveHighlightPalette` and `loadHighlightPalette` to persist the user's choices. - The highlighting UI in both vertical and paginated readers now displays the active custom palette. - Added new `HighlightColor` options. - Updated JavaScript to support the expanded color set and improve highlight interaction logic.
This commit is contained in:
parent
1b048c9073
commit
1f111034f1
5 changed files with 3044 additions and 1753 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@
|
||||||
*
|
*
|
||||||
* mail: epistemereader@gmail.com
|
* mail: epistemereader@gmail.com
|
||||||
*/
|
*/
|
||||||
|
// ChapterWebView.kt
|
||||||
package com.aryan.reader.epubreader
|
package com.aryan.reader.epubreader
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -27,7 +28,6 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import timber.log.Timber
|
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
|
|
@ -36,13 +36,14 @@ import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -69,6 +70,7 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -86,6 +88,7 @@ import com.aryan.reader.countWords
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import timber.log.Timber
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
|
|
||||||
|
|
@ -326,6 +329,8 @@ fun ChapterWebView(
|
||||||
currentTextAlign: ReaderTextAlign,
|
currentTextAlign: ReaderTextAlign,
|
||||||
onHighlightClicked: () -> Unit,
|
onHighlightClicked: () -> Unit,
|
||||||
onAutoScrollChapterEnd: () -> Unit = {},
|
onAutoScrollChapterEnd: () -> Unit = {},
|
||||||
|
activeHighlightPalette: List<HighlightColor>,
|
||||||
|
onUpdatePalette: (Int, HighlightColor) -> Unit
|
||||||
) {
|
) {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"RenderChapterViaWebView for '$chapterTitle', Key: $key, isDarkTheme: $isDarkTheme, initialScrollTarget: $initialScrollTarget"
|
"RenderChapterViaWebView for '$chapterTitle', Key: $key, isDarkTheme: $isDarkTheme, initialScrollTarget: $initialScrollTarget"
|
||||||
|
|
@ -340,6 +345,8 @@ fun ChapterWebView(
|
||||||
|
|
||||||
val jsToInject = remember(context) { getJsToInject(context) }
|
val jsToInject = remember(context) { getJsToInject(context) }
|
||||||
|
|
||||||
|
var showPaletteManager by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(currentFontSize, currentLineHeight) {
|
LaunchedEffect(currentFontSize, currentLineHeight) {
|
||||||
localWebViewRef?.evaluateJavascript(
|
localWebViewRef?.evaluateJavascript(
|
||||||
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
"javascript:if(window.getSelection) window.getSelection().removeAllRanges();",
|
||||||
|
|
@ -542,9 +549,9 @@ fun ChapterWebView(
|
||||||
}
|
}
|
||||||
addJavascriptInterface(
|
addJavascriptInterface(
|
||||||
CfiJsBridge(
|
CfiJsBridge(
|
||||||
onCfiReady = { cfi -> onCfiGenerated(cfi) },
|
onCfiReady = { cfi -> onCfiGenerated(cfi) },
|
||||||
onCfiForBookmarkReady = { cfi -> onBookmarkCfiGenerated(cfi) }
|
onCfiForBookmarkReady = { cfi -> onBookmarkCfiGenerated(cfi) }
|
||||||
), "CfiBridge")
|
), "CfiBridge")
|
||||||
addJavascriptInterface(SnippetJsBridge { cfi, snippet ->
|
addJavascriptInterface(SnippetJsBridge { cfi, snippet ->
|
||||||
onSnippetForBookmarkReady(
|
onSnippetForBookmarkReady(
|
||||||
cfi,
|
cfi,
|
||||||
|
|
@ -812,45 +819,51 @@ fun ChapterWebView(
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.width(IntrinsicSize.Max)
|
modifier = Modifier.width(IntrinsicSize.Max)
|
||||||
) {
|
) {
|
||||||
// 1. Color Row (Improved sizing and gaps)
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.Center, // Centered colors
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
HighlightColor.entries.forEach { colorEnum ->
|
activeHighlightPalette.forEachIndexed { index, colorEnum ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 6.dp)
|
||||||
.size(24.dp)
|
.size(32.dp)
|
||||||
.background(colorEnum.color, CircleShape)
|
.background(colorEnum.color, CircleShape)
|
||||||
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.5f), CircleShape)
|
.pointerInput(colorEnum) {
|
||||||
.clickable {
|
detectTapGestures(
|
||||||
Timber.d("Kotlin: Color clicked. Existing? ${state.isExistingHighlight}")
|
onTap = {
|
||||||
|
Timber.d("Kotlin: Color clicked. Existing? ${state.isExistingHighlight}")
|
||||||
if (state.isExistingHighlight && state.cfi != null) {
|
if (state.isExistingHighlight && state.cfi != null) {
|
||||||
// UPDATE EXISTING HIGHLIGHT
|
localWebViewRef?.evaluateJavascript(
|
||||||
Timber.d("Kotlin: Requesting UPDATE via JS for CFI: ${state.cfi}")
|
"javascript:window.HighlightBridgeHelper.updateHighlightStyle('${state.cfi}', '${colorEnum.cssClass}', '${colorEnum.id}');",
|
||||||
localWebViewRef?.evaluateJavascript(
|
null
|
||||||
"javascript:window.HighlightBridgeHelper.updateHighlightStyle('${state.cfi}', '${colorEnum.cssClass}', '${colorEnum.id}');",
|
)
|
||||||
null
|
} else {
|
||||||
)
|
localWebViewRef?.evaluateJavascript(
|
||||||
} else {
|
"javascript:window.HighlightBridgeHelper.createUserHighlight('${colorEnum.cssClass}', '${colorEnum.id}');",
|
||||||
// CREATE NEW HIGHLIGHT
|
null
|
||||||
Timber.d("Kotlin: Requesting CREATE via JS")
|
)
|
||||||
localWebViewRef?.evaluateJavascript(
|
}
|
||||||
"javascript:window.HighlightBridgeHelper.createUserHighlight('${colorEnum.cssClass}', '${colorEnum.id}');",
|
state.finishActionModeCallback()
|
||||||
null
|
localWebViewRef?.clearFocus()
|
||||||
)
|
customMenuState = null
|
||||||
}
|
},
|
||||||
state.finishActionModeCallback()
|
onLongPress = {
|
||||||
localWebViewRef?.clearFocus()
|
showPaletteManager = true
|
||||||
customMenuState = null
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
SpectrumButton(
|
||||||
|
onClick = { showPaletteManager = true },
|
||||||
|
size = 32.dp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete Option (Only for existing highlights)
|
// 2. Delete Option (Only for existing highlights)
|
||||||
|
|
@ -979,5 +992,17 @@ fun ChapterWebView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showPaletteManager) {
|
||||||
|
PaletteManagerDialog(
|
||||||
|
currentPalette = activeHighlightPalette,
|
||||||
|
onDismiss = { showPaletteManager = false },
|
||||||
|
onSave = { newPalette ->
|
||||||
|
newPalette.forEachIndexed { index, color ->
|
||||||
|
onUpdatePalette(index, color)
|
||||||
|
}
|
||||||
|
showPaletteManager = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,6 +38,18 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -65,7 +77,17 @@ enum class HighlightColor(val id: String, val color: Color, val cssClass: String
|
||||||
YELLOW("yellow", Color(0xFFFBC02D), "user-highlight-yellow"),
|
YELLOW("yellow", Color(0xFFFBC02D), "user-highlight-yellow"),
|
||||||
GREEN("green", Color(0xFF388E3C), "user-highlight-green"),
|
GREEN("green", Color(0xFF388E3C), "user-highlight-green"),
|
||||||
BLUE("blue", Color(0xFF1976D2), "user-highlight-blue"),
|
BLUE("blue", Color(0xFF1976D2), "user-highlight-blue"),
|
||||||
RED("red", Color(0xFFD32F2F), "user-highlight-red")
|
RED("red", Color(0xFFD32F2F), "user-highlight-red"),
|
||||||
|
PURPLE("purple", Color(0xFF7B1FA2), "user-highlight-purple"),
|
||||||
|
ORANGE("orange", Color(0xFFF57C00), "user-highlight-orange"),
|
||||||
|
CYAN("cyan", Color(0xFF0097A7), "user-highlight-cyan"),
|
||||||
|
MAGENTA("magenta", Color(0xFFC2185B), "user-highlight-magenta"),
|
||||||
|
LIME("lime", Color(0xFFAFB42B), "user-highlight-lime"),
|
||||||
|
PINK("pink", Color(0xFFE91E63), "user-highlight-pink"),
|
||||||
|
TEAL("teal", Color(0xFF00796B), "user-highlight-teal"),
|
||||||
|
INDIGO("indigo", Color(0xFF303F9F), "user-highlight-indigo"),
|
||||||
|
BLACK("black", Color(0xFF424242), "user-highlight-black"),
|
||||||
|
WHITE("white", Color(0xFFF5F5F5), "user-highlight-white");
|
||||||
}
|
}
|
||||||
|
|
||||||
data class UserHighlight(
|
data class UserHighlight(
|
||||||
|
|
@ -88,6 +110,24 @@ fun escapeJsString(value: String): String {
|
||||||
.replace("\u2029", "\\u2029")
|
.replace("\u2029", "\\u2029")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun saveHighlightPalette(context: Context, palette: List<HighlightColor>) {
|
||||||
|
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val ids = palette.joinToString(",") { it.id }
|
||||||
|
prefs.edit { putString("highlight_palette_ids", ids) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadHighlightPalette(context: Context): List<HighlightColor> {
|
||||||
|
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
val savedIds = prefs.getString("highlight_palette_ids", null)
|
||||||
|
if (savedIds != null) {
|
||||||
|
val list = savedIds.split(",").mapNotNull { id ->
|
||||||
|
HighlightColor.entries.find { it.id == id }
|
||||||
|
}
|
||||||
|
if (list.size == 4) return list
|
||||||
|
}
|
||||||
|
return listOf(HighlightColor.YELLOW, HighlightColor.GREEN, HighlightColor.BLUE, HighlightColor.RED)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Persistence Helpers ---
|
// --- Persistence Helpers ---
|
||||||
|
|
||||||
fun loadBookmarks(context: Context, bookTitle: String, chapters: List<EpubChapter>, bookmarksJson: String?): Set<Bookmark> {
|
fun loadBookmarks(context: Context, bookTitle: String, chapters: List<EpubChapter>, bookmarksJson: String?): Set<Bookmark> {
|
||||||
|
|
@ -285,3 +325,110 @@ fun BookmarkButton(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SpectrumButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
size: androidx.compose.ui.unit.Dp = 32.dp
|
||||||
|
) {
|
||||||
|
val rainbowColors = listOf(
|
||||||
|
Color.Red, Color(0xFFFF7F00), Color.Yellow, Color.Green,
|
||||||
|
Color.Blue, Color(0xFF4B0082), Color(0xFF8B00FF)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(size)
|
||||||
|
.background(
|
||||||
|
brush = Brush.sweepGradient(rainbowColors),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PaletteManagerDialog(
|
||||||
|
currentPalette: List<HighlightColor>,
|
||||||
|
onSave: (List<HighlightColor>) -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
var tempPalette by remember { mutableStateOf(currentPalette.toMutableList()) }
|
||||||
|
var selectedSlotIndex by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text("Customize Palette", style = MaterialTheme.typography.titleMedium) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text("Tap a slot to edit:", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
tempPalette.forEachIndexed { index, colorEnum ->
|
||||||
|
val isSelected = index == selectedSlotIndex
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(colorEnum.color, CircleShape)
|
||||||
|
.border(
|
||||||
|
width = if (isSelected) 3.dp else 1.dp,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary else Color.Transparent, // Thin ring if selected
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { selectedSlotIndex = index }
|
||||||
|
) {
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = "Selected Slot",
|
||||||
|
tint = if (colorEnum == HighlightColor.WHITE) Color.Black else Color.White,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
// 2. Bottom Grid: Available Colors
|
||||||
|
Text("Select a color for the slot:", style = MaterialTheme.typography.bodySmall)
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 40.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.height(200.dp)
|
||||||
|
) {
|
||||||
|
items(HighlightColor.entries) { colorOption ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.background(colorOption.color, CircleShape)
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f), CircleShape)
|
||||||
|
.clickable {
|
||||||
|
val newList = tempPalette.toMutableList()
|
||||||
|
newList[selectedSlotIndex] = colorOption
|
||||||
|
tempPalette = newList
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { onSave(tempPalette) }) { Text("Save") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -283,6 +283,19 @@ fun EpubReaderHost(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentHighlightPalette by remember {
|
||||||
|
mutableStateOf(loadHighlightPalette(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
val onUpdateHighlightPalette: (Int, HighlightColor) -> Unit = { index, newColor ->
|
||||||
|
val newList = currentHighlightPalette.toMutableList()
|
||||||
|
if (index in newList.indices) {
|
||||||
|
newList[index] = newColor
|
||||||
|
currentHighlightPalette = newList
|
||||||
|
saveHighlightPalette(context, newList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(userHighlights.size, userHighlights.toList()) {
|
LaunchedEffect(userHighlights.size, userHighlights.toList()) {
|
||||||
saveHighlightsToPrefs(context, epubBook.title, userHighlights)
|
saveHighlightsToPrefs(context, epubBook.title, userHighlights)
|
||||||
}
|
}
|
||||||
|
|
@ -1541,6 +1554,8 @@ fun EpubReaderHost(
|
||||||
initialCfi = cfiToLoad,
|
initialCfi = cfiToLoad,
|
||||||
initialFragmentId = fragmentToLoad.also { },
|
initialFragmentId = fragmentToLoad.also { },
|
||||||
userHighlights = userHighlights.filter { it.chapterIndex == targetChapterIndex },
|
userHighlights = userHighlights.filter { it.chapterIndex == targetChapterIndex },
|
||||||
|
activeHighlightPalette = currentHighlightPalette,
|
||||||
|
onUpdatePalette = onUpdateHighlightPalette,
|
||||||
onHighlightCreated = { cfi, text, colorId ->
|
onHighlightCreated = { cfi, text, colorId ->
|
||||||
Timber.d("Vertical Mode (Source): Creating Highlight. CFI: $cfi")
|
Timber.d("Vertical Mode (Source): Creating Highlight. CFI: $cfi")
|
||||||
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
Timber.d("Vertical Mode (Source): Text Snippet: '${text.take(50)}...'")
|
||||||
|
|
@ -2090,6 +2105,8 @@ fun EpubReaderHost(
|
||||||
lineHeightMultiplier = currentLineHeight,
|
lineHeightMultiplier = currentLineHeight,
|
||||||
fontFamily = activeFontFamily,
|
fontFamily = activeFontFamily,
|
||||||
textAlign = currentTextAlign,
|
textAlign = currentTextAlign,
|
||||||
|
activeHighlightPalette = currentHighlightPalette,
|
||||||
|
onUpdatePalette = onUpdateHighlightPalette,
|
||||||
ttsHighlightInfo = TtsHighlightInfo(
|
ttsHighlightInfo = TtsHighlightInfo(
|
||||||
text = ttsState.currentText ?: "",
|
text = ttsState.currentText ?: "",
|
||||||
cfi = ttsState.sourceCfi ?: "",
|
cfi = ttsState.sourceCfi ?: "",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
*
|
*
|
||||||
* mail: epistemereader@gmail.com
|
* mail: epistemereader@gmail.com
|
||||||
*/
|
*/
|
||||||
|
// PaginatedReader.kt
|
||||||
package com.aryan.reader.paginatedreader
|
package com.aryan.reader.paginatedreader
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
|
@ -51,6 +52,9 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
|
@ -143,7 +147,9 @@ import com.aryan.reader.R
|
||||||
import com.aryan.reader.countWords
|
import com.aryan.reader.countWords
|
||||||
import com.aryan.reader.epub.EpubBook
|
import com.aryan.reader.epub.EpubBook
|
||||||
import com.aryan.reader.epubreader.HighlightColor
|
import com.aryan.reader.epubreader.HighlightColor
|
||||||
|
import com.aryan.reader.epubreader.PaletteManagerDialog
|
||||||
import com.aryan.reader.epubreader.ReaderTextAlign
|
import com.aryan.reader.epubreader.ReaderTextAlign
|
||||||
|
import com.aryan.reader.epubreader.SpectrumButton
|
||||||
import com.aryan.reader.epubreader.TtsHighlightInfo
|
import com.aryan.reader.epubreader.TtsHighlightInfo
|
||||||
import com.aryan.reader.epubreader.UserHighlight
|
import com.aryan.reader.epubreader.UserHighlight
|
||||||
import com.aryan.reader.paginatedreader.data.BookCacheDatabase
|
import com.aryan.reader.paginatedreader.data.BookCacheDatabase
|
||||||
|
|
@ -360,10 +366,10 @@ private fun WrappingContentLayout(
|
||||||
|
|
||||||
val imagePlacable = if (imageRenderWidthPx > 0 && imageRenderHeightPx > 0) {
|
val imagePlacable = if (imageRenderWidthPx > 0 && imageRenderHeightPx > 0) {
|
||||||
measurables.first().measure(
|
measurables.first().measure(
|
||||||
Constraints.fixed(
|
Constraints.fixed(
|
||||||
imageRenderWidthPx.roundToInt(), imageRenderHeightPx.roundToInt()
|
imageRenderWidthPx.roundToInt(), imageRenderHeightPx.roundToInt()
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
@ -392,8 +398,8 @@ private fun WrappingContentLayout(
|
||||||
|
|
||||||
val styleForMeasure =
|
val styleForMeasure =
|
||||||
remainingText.spanStyles.firstOrNull { it.item.fontFamily != null }?.item?.fontFamily?.let {
|
remainingText.spanStyles.firstOrNull { it.item.fontFamily != null }?.item?.fontFamily?.let {
|
||||||
textStyle.copy(fontFamily = it)
|
textStyle.copy(fontFamily = it)
|
||||||
} ?: textStyle
|
} ?: textStyle
|
||||||
|
|
||||||
val layoutResult = textMeasurer.measure(
|
val layoutResult = textMeasurer.measure(
|
||||||
remainingText, style = styleForMeasure, constraints = lineConstraints
|
remainingText, style = styleForMeasure, constraints = lineConstraints
|
||||||
|
|
@ -488,7 +494,9 @@ fun PaginatedReaderScreen(
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
userHighlights: List<UserHighlight>,
|
userHighlights: List<UserHighlight>,
|
||||||
onHighlightCreated: (String, String, String) -> Unit,
|
onHighlightCreated: (String, String, String) -> Unit,
|
||||||
onHighlightDeleted: (String) -> Unit
|
onHighlightDeleted: (String) -> Unit,
|
||||||
|
activeHighlightPalette: List<HighlightColor>,
|
||||||
|
onUpdatePalette: (Int, HighlightColor) -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(userHighlights) {
|
LaunchedEffect(userHighlights) {
|
||||||
Timber.d("PaginatedReaderScreen: Received ${userHighlights.size} highlights.")
|
Timber.d("PaginatedReaderScreen: Received ${userHighlights.size} highlights.")
|
||||||
|
|
@ -757,7 +765,9 @@ fun PaginatedReaderScreen(
|
||||||
userHighlights = userHighlights,
|
userHighlights = userHighlights,
|
||||||
onHighlightCreated = onHighlightCreated,
|
onHighlightCreated = onHighlightCreated,
|
||||||
onHighlightDeleted = onHighlightDeleted,
|
onHighlightDeleted = onHighlightDeleted,
|
||||||
isDarkTheme = isDarkTheme
|
isDarkTheme = isDarkTheme,
|
||||||
|
activeHighlightPalette = activeHighlightPalette,
|
||||||
|
onUpdatePalette = onUpdatePalette
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1079,7 +1089,7 @@ private fun TextWithEmphasis(
|
||||||
activeSelection: PaginatedSelection?,
|
activeSelection: PaginatedSelection?,
|
||||||
@Suppress("unused") onSelectionChange: (PaginatedSelection?) -> Unit,
|
@Suppress("unused") onSelectionChange: (PaginatedSelection?) -> Unit,
|
||||||
onHighlightClick: (UserHighlight, Rect) -> Unit,
|
onHighlightClick: (UserHighlight, Rect) -> Unit,
|
||||||
isDarkTheme: Boolean,
|
@Suppress("unused") isDarkTheme: Boolean,
|
||||||
onRegisterLayout: ((TextLayoutResult, LayoutCoordinates) -> Unit)? = null
|
onRegisterLayout: ((TextLayoutResult, LayoutCoordinates) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
|
|
@ -1107,36 +1117,20 @@ private fun TextWithEmphasis(
|
||||||
range.first, range.last + 1
|
range.first, range.last + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
if (isDarkTheme) {
|
drawPath(
|
||||||
Timber.d(
|
path,
|
||||||
"DRAWING: Highlight '${highlight.text.take(10)}' on Block ${block.cfi} at range $range"
|
highlight.color.color.copy(alpha = 0.4f),
|
||||||
)
|
blendMode = BlendMode.SrcOver
|
||||||
}
|
)
|
||||||
|
|
||||||
if (isDarkTheme) {
|
|
||||||
drawPath(
|
|
||||||
path,
|
|
||||||
highlight.color.color.copy(alpha = 0.6f),
|
|
||||||
blendMode = BlendMode.SrcOver
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
drawPath(
|
|
||||||
path,
|
|
||||||
highlight.color.color.copy(alpha = 0.5f),
|
|
||||||
blendMode = BlendMode.Multiply
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (highlight.cfi == pressedHighlightCfi) {
|
if (highlight.cfi == pressedHighlightCfi) {
|
||||||
drawPath(
|
drawPath(
|
||||||
path,
|
path,
|
||||||
Color.White.copy(alpha = 0.3f),
|
Color.Black.copy(alpha = 0.1f),
|
||||||
blendMode = BlendMode.SrcOver
|
blendMode = BlendMode.SrcOver
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) { }
|
||||||
// Ignore layout errors during drawing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1174,8 +1168,7 @@ private fun TextWithEmphasis(
|
||||||
val lineLeft = layout.getLineLeft(lineIndex)
|
val lineLeft = layout.getLineLeft(lineIndex)
|
||||||
val lineRight = layout.getLineRight(lineIndex)
|
val lineRight = layout.getLineRight(lineIndex)
|
||||||
if (offset.x < minOf(lineLeft, lineRight) - 50 || offset.x > maxOf(
|
if (offset.x < minOf(lineLeft, lineRight) - 50 || offset.x > maxOf(
|
||||||
lineLeft,
|
lineLeft, lineRight
|
||||||
lineRight
|
|
||||||
) + 50
|
) + 50
|
||||||
) {
|
) {
|
||||||
return null
|
return null
|
||||||
|
|
@ -1316,6 +1309,8 @@ internal fun PaginatedReaderContent(
|
||||||
userHighlights: List<UserHighlight>,
|
userHighlights: List<UserHighlight>,
|
||||||
onHighlightCreated: (String, String, String) -> Unit,
|
onHighlightCreated: (String, String, String) -> Unit,
|
||||||
onHighlightDeleted: (String) -> Unit,
|
onHighlightDeleted: (String) -> Unit,
|
||||||
|
activeHighlightPalette: List<HighlightColor>,
|
||||||
|
onUpdatePalette: (Int, HighlightColor) -> Unit,
|
||||||
isDarkTheme: Boolean
|
isDarkTheme: Boolean
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
@ -1330,6 +1325,8 @@ internal fun PaginatedReaderContent(
|
||||||
val blockLayoutMap = remember {
|
val blockLayoutMap = remember {
|
||||||
androidx.compose.runtime.mutableStateMapOf<String, Triple<TextLayoutResult, LayoutCoordinates, Int>>()
|
androidx.compose.runtime.mutableStateMapOf<String, Triple<TextLayoutResult, LayoutCoordinates, Int>>()
|
||||||
}
|
}
|
||||||
|
var showColorPickerDialog by remember { mutableStateOf<Int?>(null) }
|
||||||
|
var showPaletteManager by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
if (showExternalLinkDialog != null) {
|
if (showExternalLinkDialog != null) {
|
||||||
val urlToShow = showExternalLinkDialog!!
|
val urlToShow = showExternalLinkDialog!!
|
||||||
|
|
@ -1433,8 +1430,7 @@ internal fun PaginatedReaderContent(
|
||||||
LocalTextToolbar provides textToolbar, LocalClipboard provides dictionaryClipboard
|
LocalTextToolbar provides textToolbar, LocalClipboard provides dictionaryClipboard
|
||||||
) {
|
) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState, modifier = Modifier.fillMaxSize()
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
) { pageIndex ->
|
) { pageIndex ->
|
||||||
var pageContent by remember { mutableStateOf<Page?>(null) }
|
var pageContent by remember { mutableStateOf<Page?>(null) }
|
||||||
var currentChapterPath by remember { mutableStateOf<String?>(null) }
|
var currentChapterPath by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
@ -1524,7 +1520,9 @@ internal fun PaginatedReaderContent(
|
||||||
if (block.style.backgroundColor.isSpecified) {
|
if (block.style.backgroundColor.isSpecified) {
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
block.style.backgroundColor,
|
block.style.backgroundColor,
|
||||||
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(block.style.borderRadius) else androidx.compose.ui.graphics.RectangleShape
|
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(
|
||||||
|
block.style.borderRadius
|
||||||
|
) else androidx.compose.ui.graphics.RectangleShape
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -1535,7 +1533,9 @@ internal fun PaginatedReaderContent(
|
||||||
BorderStroke(
|
BorderStroke(
|
||||||
border.width, border.color
|
border.width, border.color
|
||||||
),
|
),
|
||||||
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(block.style.borderRadius) else androidx.compose.ui.graphics.RectangleShape
|
shape = if (block.style.borderRadius > 0.dp) RoundedCornerShape(
|
||||||
|
block.style.borderRadius
|
||||||
|
) else androidx.compose.ui.graphics.RectangleShape
|
||||||
)
|
)
|
||||||
} ?: Modifier)
|
} ?: Modifier)
|
||||||
|
|
||||||
|
|
@ -1830,10 +1830,10 @@ internal fun PaginatedReaderContent(
|
||||||
if (block.itemMarkerImage != null) {
|
if (block.itemMarkerImage != null) {
|
||||||
val imageRequest =
|
val imageRequest =
|
||||||
Builder(LocalContext.current).data(
|
Builder(LocalContext.current).data(
|
||||||
File(
|
File(
|
||||||
block.itemMarkerImage
|
block.itemMarkerImage
|
||||||
)
|
)
|
||||||
).crossfade(true).build()
|
).crossfade(true).build()
|
||||||
val imageSize = with(density) {
|
val imageSize = with(density) {
|
||||||
(textStyle.fontSize.value * 0.8f).sp.toDp()
|
(textStyle.fontSize.value * 0.8f).sp.toDp()
|
||||||
}
|
}
|
||||||
|
|
@ -1842,8 +1842,8 @@ internal fun PaginatedReaderContent(
|
||||||
model = imageRequest,
|
model = imageRequest,
|
||||||
contentDescription = "List item marker",
|
contentDescription = "List item marker",
|
||||||
modifier = markerAreaModifier.height(
|
modifier = markerAreaModifier.height(
|
||||||
imageSize
|
imageSize
|
||||||
),
|
),
|
||||||
alignment = Alignment.CenterEnd,
|
alignment = Alignment.CenterEnd,
|
||||||
contentScale = ContentScale.FillHeight
|
contentScale = ContentScale.FillHeight
|
||||||
)
|
)
|
||||||
|
|
@ -2093,16 +2093,16 @@ internal fun PaginatedReaderContent(
|
||||||
|
|
||||||
val imageRequest =
|
val imageRequest =
|
||||||
Builder(LocalContext.current).data(
|
Builder(LocalContext.current).data(
|
||||||
SvgData(
|
SvgData(
|
||||||
block.svgContent
|
block.svgContent
|
||||||
|
)
|
||||||
|
).listener(
|
||||||
|
onError = { _, result ->
|
||||||
|
Timber.e(
|
||||||
|
result.throwable,
|
||||||
|
"Coil failed to load SVG for MathBlock."
|
||||||
)
|
)
|
||||||
).listener(
|
}).build()
|
||||||
onError = { _, result ->
|
|
||||||
Timber.e(
|
|
||||||
result.throwable,
|
|
||||||
"Coil failed to load SVG for MathBlock."
|
|
||||||
)
|
|
||||||
}).build()
|
|
||||||
|
|
||||||
val colorFilter =
|
val colorFilter =
|
||||||
if (block.isFromMathJax) ColorFilter.tint(
|
if (block.isFromMathJax) ColorFilter.tint(
|
||||||
|
|
@ -2361,8 +2361,8 @@ internal fun PaginatedReaderContent(
|
||||||
Text(
|
Text(
|
||||||
text = blockInCell.content,
|
text = blockInCell.content,
|
||||||
style = cellTextStyle.copy(
|
style = cellTextStyle.copy(
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
),
|
),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2416,11 +2416,10 @@ internal fun PaginatedReaderContent(
|
||||||
model = Builder(
|
model = Builder(
|
||||||
LocalContext.current
|
LocalContext.current
|
||||||
).data(
|
).data(
|
||||||
File(
|
File(
|
||||||
blockInCell.path
|
blockInCell.path
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.build(),
|
).build(),
|
||||||
contentDescription = blockInCell.altText,
|
contentDescription = blockInCell.altText,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
@ -2470,231 +2469,232 @@ internal fun PaginatedReaderContent(
|
||||||
}, onDismissRequest = { state.onHide() }) {
|
}, onDismissRequest = { state.onHide() }) {
|
||||||
PaginatedTextSelectionMenu(
|
PaginatedTextSelectionMenu(
|
||||||
onCopy = {
|
onCopy = {
|
||||||
isForDictionary = false
|
isForDictionary = false
|
||||||
isForHighlight = false
|
isForHighlight = false
|
||||||
state.onCopy()
|
state.onCopy()
|
||||||
state.onHide()
|
state.onHide()
|
||||||
}, onSelectAll = {
|
}, onSelectAll = {
|
||||||
state.onSelectAll?.invoke()
|
state.onSelectAll?.invoke()
|
||||||
state.onHide()
|
state.onHide()
|
||||||
}, onDictionary = {
|
}, onDictionary = {
|
||||||
isForDictionary = true
|
isForDictionary = true
|
||||||
state.onCopy()
|
state.onCopy()
|
||||||
isForDictionary = false
|
isForDictionary = false
|
||||||
state.onHide()
|
state.onHide()
|
||||||
}, onHighlight = { color ->
|
}, onHighlight = { color ->
|
||||||
Timber.d("Menu: Highlight option clicked. Color: ${color.id}")
|
Timber.d("Menu: Highlight option clicked. Color: ${color.id}")
|
||||||
isForHighlight = true
|
isForHighlight = true
|
||||||
state.onCopy()
|
state.onCopy()
|
||||||
isForHighlight = false
|
isForHighlight = false
|
||||||
|
|
||||||
capturedTextForAction?.let { text ->
|
capturedTextForAction?.let { text ->
|
||||||
val selectionRect = state.rect
|
val selectionRect = state.rect
|
||||||
Timber.d("Menu: Selection Rect: $selectionRect")
|
Timber.d("Menu: Selection Rect: $selectionRect")
|
||||||
|
|
||||||
// 1. Attempt Geometric Strategy (Hit Testing)
|
var geometricSuccess = false
|
||||||
var geometricSuccess = false
|
val candidates = blockLayoutMap.filter { (_, triple) ->
|
||||||
val candidates = blockLayoutMap.filter { (_, triple) ->
|
val (_, coords, _) = triple
|
||||||
val (_, coords, _) = triple // Deconstruct Triple
|
if (!coords.isAttached) return@filter false
|
||||||
if (!coords.isAttached) return@filter false
|
val pos = coords.positionInWindow()
|
||||||
val pos = coords.positionInWindow()
|
val size = coords.size.toSize()
|
||||||
val size = coords.size.toSize()
|
val blockRect = Rect(pos, size)
|
||||||
val blockRect = Rect(pos, size)
|
val overlaps = blockRect.overlaps(selectionRect)
|
||||||
val overlaps = blockRect.overlaps(selectionRect)
|
overlaps
|
||||||
overlaps
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.isNotEmpty()) {
|
if (candidates.isNotEmpty()) {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"Menu: Geometric candidates found: ${candidates.keys}"
|
"Menu: Geometric candidates found: ${candidates.keys}"
|
||||||
)
|
|
||||||
try {
|
|
||||||
val sorted = candidates.entries.sortedBy {
|
|
||||||
it.value.second.positionInWindow().y
|
|
||||||
}
|
|
||||||
val (startCfi, startTriple) = sorted.first()
|
|
||||||
val (endCfi, endTriple) = sorted.last()
|
|
||||||
val (startLayout, startCoords, startAbsOffset) = startTriple
|
|
||||||
val (endLayout, endCoords, endAbsOffset) = endTriple
|
|
||||||
val localStart =
|
|
||||||
startCoords.windowToLocal(selectionRect.topLeft)
|
|
||||||
val localEnd = endCoords.windowToLocal(
|
|
||||||
selectionRect.bottomRight
|
|
||||||
)
|
)
|
||||||
var finalStartOffset =
|
try {
|
||||||
startLayout.getOffsetForPosition(localStart)
|
val sorted = candidates.entries.sortedBy {
|
||||||
var finalEndOffset = endLayout.getOffsetForPosition(localEnd)
|
it.value.second.positionInWindow().y
|
||||||
var finalEndCfi = endCfi
|
|
||||||
val startText = startLayout.layoutInput.text.text
|
|
||||||
|
|
||||||
if (startCfi == endCfi) {
|
|
||||||
val matches = mutableListOf<Int>()
|
|
||||||
var idx = startText.indexOf(text)
|
|
||||||
while (idx != -1) {
|
|
||||||
matches.add(idx)
|
|
||||||
idx = startText.indexOf(text, idx + 1)
|
|
||||||
}
|
}
|
||||||
if (matches.isNotEmpty()) {
|
val (startCfi, startTriple) = sorted.first()
|
||||||
val bestMatch = matches.minBy {
|
val (endCfi, endTriple) = sorted.last()
|
||||||
abs(it - finalStartOffset)
|
val (startLayout, startCoords, startAbsOffset) = startTriple
|
||||||
}
|
val (endLayout, endCoords, endAbsOffset) = endTriple
|
||||||
finalStartOffset = bestMatch
|
val localStart =
|
||||||
finalEndOffset = bestMatch + text.length
|
startCoords.windowToLocal(selectionRect.topLeft)
|
||||||
Timber.d(
|
val localEnd = endCoords.windowToLocal(
|
||||||
"Refined Single-Block Offset: $finalStartOffset"
|
selectionRect.bottomRight
|
||||||
)
|
)
|
||||||
}
|
var finalStartOffset =
|
||||||
} else {
|
startLayout.getOffsetForPosition(localStart)
|
||||||
val endText = endLayout.layoutInput.text.text
|
var finalEndOffset = endLayout.getOffsetForPosition(localEnd)
|
||||||
|
var finalEndCfi = endCfi
|
||||||
|
val startText = startLayout.layoutInput.text.text
|
||||||
|
|
||||||
fun findBestMatch(
|
if (startCfi == endCfi) {
|
||||||
source: String,
|
val matches = mutableListOf<Int>()
|
||||||
query: String,
|
var idx = startText.indexOf(text)
|
||||||
targetOffset: Int,
|
|
||||||
isSuffix: Boolean
|
|
||||||
): Int {
|
|
||||||
if (query.isEmpty()) return -1
|
|
||||||
var bestIdx = -1
|
|
||||||
var minDiff = Int.MAX_VALUE
|
|
||||||
var idx = source.indexOf(query)
|
|
||||||
while (idx != -1) {
|
while (idx != -1) {
|
||||||
val cmpPoint = if (isSuffix) idx + query.length
|
matches.add(idx)
|
||||||
else idx
|
idx = startText.indexOf(text, idx + 1)
|
||||||
val diff = abs(cmpPoint - targetOffset)
|
|
||||||
if (diff < minDiff) {
|
|
||||||
minDiff = diff
|
|
||||||
bestIdx = idx
|
|
||||||
}
|
|
||||||
idx = source.indexOf(query, idx + 1)
|
|
||||||
}
|
}
|
||||||
return bestIdx
|
if (matches.isNotEmpty()) {
|
||||||
}
|
val bestMatch = matches.minBy {
|
||||||
|
abs(it - finalStartOffset)
|
||||||
var sMatch = -1
|
}
|
||||||
var eMatch = -1
|
finalStartOffset = bestMatch
|
||||||
var usedSuffixLen = 0
|
finalEndOffset = bestMatch + text.length
|
||||||
|
Timber.d(
|
||||||
val maxChunk = minOf(text.length, 50)
|
"Refined Single-Block Offset: $finalStartOffset"
|
||||||
for (len in maxChunk downTo 3) {
|
|
||||||
val prefix = text.take(len).trim()
|
|
||||||
if (prefix.isNotEmpty()) {
|
|
||||||
val idx = findBestMatch(
|
|
||||||
startText,
|
|
||||||
prefix,
|
|
||||||
finalStartOffset,
|
|
||||||
isSuffix = false
|
|
||||||
)
|
)
|
||||||
if (idx != -1) {
|
}
|
||||||
sMatch = idx
|
} else {
|
||||||
Timber.d(
|
val endText = endLayout.layoutInput.text.text
|
||||||
"Refined Start: Found prefix '$prefix' at $idx"
|
|
||||||
|
fun findBestMatch(
|
||||||
|
source: String,
|
||||||
|
query: String,
|
||||||
|
targetOffset: Int,
|
||||||
|
isSuffix: Boolean
|
||||||
|
): Int {
|
||||||
|
if (query.isEmpty()) return -1
|
||||||
|
var bestIdx = -1
|
||||||
|
var minDiff = Int.MAX_VALUE
|
||||||
|
var idx = source.indexOf(query)
|
||||||
|
while (idx != -1) {
|
||||||
|
val cmpPoint = if (isSuffix) idx + query.length
|
||||||
|
else idx
|
||||||
|
val diff = abs(cmpPoint - targetOffset)
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff
|
||||||
|
bestIdx = idx
|
||||||
|
}
|
||||||
|
idx = source.indexOf(query, idx + 1)
|
||||||
|
}
|
||||||
|
return bestIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
var sMatch = -1
|
||||||
|
var eMatch = -1
|
||||||
|
var usedSuffixLen = 0
|
||||||
|
|
||||||
|
val maxChunk = minOf(text.length, 50)
|
||||||
|
for (len in maxChunk downTo 3) {
|
||||||
|
val prefix = text.take(len).trim()
|
||||||
|
if (prefix.isNotEmpty()) {
|
||||||
|
val idx = findBestMatch(
|
||||||
|
startText,
|
||||||
|
prefix,
|
||||||
|
finalStartOffset,
|
||||||
|
isSuffix = false
|
||||||
)
|
)
|
||||||
break
|
if (idx != -1) {
|
||||||
|
sMatch = idx
|
||||||
|
Timber.d(
|
||||||
|
"Refined Start: Found prefix '$prefix' at $idx"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (len in maxChunk downTo 3) {
|
for (len in maxChunk downTo 3) {
|
||||||
val suffix = text.takeLast(len).trim()
|
val suffix = text.takeLast(len).trim()
|
||||||
if (suffix.isNotEmpty()) {
|
if (suffix.isNotEmpty()) {
|
||||||
val idx = findBestMatch(
|
val idx = findBestMatch(
|
||||||
endText,
|
endText,
|
||||||
suffix,
|
suffix,
|
||||||
finalEndOffset,
|
finalEndOffset,
|
||||||
isSuffix = true
|
isSuffix = true
|
||||||
|
)
|
||||||
|
if (idx != -1) {
|
||||||
|
eMatch = idx
|
||||||
|
usedSuffixLen = suffix.length
|
||||||
|
Timber.d(
|
||||||
|
"Refined End: Found suffix '$suffix' at $idx"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sMatch != -1 && eMatch != -1) {
|
||||||
|
finalStartOffset = sMatch
|
||||||
|
finalEndOffset = eMatch + usedSuffixLen
|
||||||
|
} else if (eMatch == -1) {
|
||||||
|
Timber.d(
|
||||||
|
"Refined: Suffix not found in end block. Checking single block fit."
|
||||||
)
|
)
|
||||||
if (idx != -1) {
|
val fitIdx = startText.indexOf(text)
|
||||||
eMatch = idx
|
if (fitIdx != -1) {
|
||||||
usedSuffixLen = suffix.length
|
finalEndCfi = startCfi
|
||||||
Timber.d(
|
finalStartOffset = fitIdx
|
||||||
"Refined End: Found suffix '$suffix' at $idx"
|
finalEndOffset = fitIdx + text.length
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sMatch != -1 && eMatch != -1) {
|
val absStart = finalStartOffset + startAbsOffset
|
||||||
finalStartOffset = sMatch
|
val absEnd =
|
||||||
finalEndOffset = eMatch + usedSuffixLen
|
finalEndOffset + if (startCfi == finalEndCfi) startAbsOffset
|
||||||
} else if (eMatch == -1) {
|
else endAbsOffset
|
||||||
Timber.d(
|
|
||||||
"Refined: Suffix not found in end block. Checking single block fit."
|
|
||||||
)
|
|
||||||
val fitIdx = startText.indexOf(text)
|
|
||||||
if (fitIdx != -1) {
|
|
||||||
finalEndCfi = startCfi
|
|
||||||
finalStartOffset = fitIdx
|
|
||||||
finalEndOffset = fitIdx + text.length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val absStart = finalStartOffset + startAbsOffset
|
val rangeCfi = if (startCfi == finalEndCfi) {
|
||||||
val absEnd =
|
val actualStart = minOf(absStart, absEnd)
|
||||||
finalEndOffset + if (startCfi == finalEndCfi) startAbsOffset
|
val actualEnd = maxOf(absStart, absEnd).coerceAtLeast(
|
||||||
else endAbsOffset
|
|
||||||
|
|
||||||
val rangeCfi = if (startCfi == finalEndCfi) {
|
|
||||||
val actualStart = minOf(absStart, absEnd)
|
|
||||||
val actualEnd = maxOf(absStart, absEnd).coerceAtLeast(
|
|
||||||
actualStart + 1
|
actualStart + 1
|
||||||
)
|
)
|
||||||
"$startCfi:$actualStart|$finalEndCfi:$actualEnd"
|
"$startCfi:$actualStart|$finalEndCfi:$actualEnd"
|
||||||
} else {
|
} else {
|
||||||
"$startCfi:$absStart|$finalEndCfi:$absEnd"
|
"$startCfi:$absStart|$finalEndCfi:$absEnd"
|
||||||
}
|
}
|
||||||
|
|
||||||
Timber.d("Menu: Geometric Success. CFI: $rangeCfi")
|
Timber.d("Menu: Geometric Success. CFI: $rangeCfi")
|
||||||
onHighlightCreated(rangeCfi, text, color.id)
|
onHighlightCreated(rangeCfi, text, color.id)
|
||||||
geometricSuccess = true
|
geometricSuccess = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e, "Menu: Geometric calculation failed.")
|
Timber.e(e, "Menu: Geometric calculation failed.")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!geometricSuccess) {
|
|
||||||
Timber.w("Menu: Falling back to text search.")
|
|
||||||
val pageContent = onGetPage(pagerState.currentPage)
|
|
||||||
val textBlocks =
|
|
||||||
pageContent?.content?.filterIsInstance<TextContentBlock>()
|
|
||||||
?.filter { it.cfi != null } ?: emptyList()
|
|
||||||
|
|
||||||
var startBlock: TextContentBlock? = null
|
|
||||||
var endBlock: TextContentBlock? = null
|
|
||||||
var startOffsetRel = -1
|
|
||||||
var endOffsetRel = -1
|
|
||||||
|
|
||||||
for (block in textBlocks) {
|
|
||||||
val content = block.content.text
|
|
||||||
val idx = content.indexOf(text)
|
|
||||||
if (idx != -1) {
|
|
||||||
startBlock = block
|
|
||||||
endBlock = block
|
|
||||||
startOffsetRel = idx
|
|
||||||
endOffsetRel = idx + text.length
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
endBlock = startBlock
|
if (!geometricSuccess) {
|
||||||
|
Timber.w("Menu: Falling back to text search.")
|
||||||
|
val pageContent = onGetPage(pagerState.currentPage)
|
||||||
|
val textBlocks =
|
||||||
|
pageContent?.content?.filterIsInstance<TextContentBlock>()
|
||||||
|
?.filter { it.cfi != null } ?: emptyList()
|
||||||
|
|
||||||
if (startBlock != null) {
|
var startBlock: TextContentBlock? = null
|
||||||
val startAbs =
|
var endBlock: TextContentBlock? = null
|
||||||
startBlock.startCharOffsetInSource + startOffsetRel
|
var startOffsetRel = -1
|
||||||
val endAbs =
|
var endOffsetRel = -1
|
||||||
endBlock.startCharOffsetInSource + (if (endOffsetRel != -1) endOffsetRel
|
|
||||||
else startOffsetRel + text.length)
|
for (block in textBlocks) {
|
||||||
onHighlightCreated(
|
val content = block.content.text
|
||||||
"${startBlock.cfi}:$startAbs|${endBlock.cfi}:$endAbs",
|
val idx = content.indexOf(text)
|
||||||
text,
|
if (idx != -1) {
|
||||||
color.id
|
startBlock = block
|
||||||
)
|
endBlock = block
|
||||||
|
startOffsetRel = idx
|
||||||
|
endOffsetRel = idx + text.length
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endBlock = startBlock
|
||||||
|
|
||||||
|
if (startBlock != null) {
|
||||||
|
val startAbs =
|
||||||
|
startBlock.startCharOffsetInSource + startOffsetRel
|
||||||
|
val endAbs =
|
||||||
|
endBlock.startCharOffsetInSource + (if (endOffsetRel != -1) endOffsetRel
|
||||||
|
else startOffsetRel + text.length)
|
||||||
|
onHighlightCreated(
|
||||||
|
"${startBlock.cfi}:$startAbs|${endBlock.cfi}:$endAbs",
|
||||||
|
text,
|
||||||
|
color.id
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
state.onHide()
|
||||||
state.onHide()
|
}, onDelete = null, isProUser = isProUser, isOss = isOss,
|
||||||
}, onDelete = null, isProUser = isProUser, isOss = isOss
|
activeHighlightPalette = activeHighlightPalette,
|
||||||
|
onOpenPaletteManager = { showPaletteManager = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2706,31 +2706,61 @@ internal fun PaginatedReaderContent(
|
||||||
}, onDismissRequest = { activeSelection = null }) {
|
}, onDismissRequest = { activeSelection = null }) {
|
||||||
PaginatedTextSelectionMenu(
|
PaginatedTextSelectionMenu(
|
||||||
onCopy = {
|
onCopy = {
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Copied Text", sel.text)
|
val clip = ClipData.newPlainText("Copied Text", sel.text)
|
||||||
clipboardManager.setPrimaryClip(clip)
|
clipboardManager.setPrimaryClip(clip)
|
||||||
activeSelection = null
|
activeSelection = null
|
||||||
}, onSelectAll = null, onDictionary = {
|
}, onSelectAll = null, onDictionary = {
|
||||||
if (isProUser || countWords(sel.text) <= 1) {
|
if (isProUser || countWords(sel.text) <= 1) {
|
||||||
onWordSelectedForAiDefinition(sel.text)
|
onWordSelectedForAiDefinition(sel.text)
|
||||||
} else {
|
} else {
|
||||||
onShowDictionaryUpsellDialog()
|
onShowDictionaryUpsellDialog()
|
||||||
}
|
}
|
||||||
activeSelection = null
|
activeSelection = null
|
||||||
}, onHighlight = { color ->
|
}, onHighlight = { color ->
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"CustomSelection: Highlight clicked. Text: '${sel.text}', BaseCFI: ${sel.baseCfi}, StartOffset: ${sel.startOffset}"
|
"CustomSelection: Highlight clicked. Text: '${sel.text}', BaseCFI: ${sel.baseCfi}, StartOffset: ${sel.startOffset}"
|
||||||
)
|
)
|
||||||
val finalCfi = if (sel.startOffset > 0) "${sel.baseCfi}:${sel.startOffset}"
|
val finalCfi = if (sel.startOffset > 0) "${sel.baseCfi}:${sel.startOffset}"
|
||||||
else sel.baseCfi
|
else sel.baseCfi
|
||||||
onHighlightCreated(finalCfi, sel.text, color.id)
|
onHighlightCreated(finalCfi, sel.text, color.id)
|
||||||
activeSelection = null
|
activeSelection = null
|
||||||
}, onDelete = null, isProUser = isProUser, isOss = isOss
|
}, onDelete = null, isProUser = isProUser, isOss = isOss,
|
||||||
|
activeHighlightPalette = activeHighlightPalette,
|
||||||
|
onOpenPaletteManager = { showPaletteManager = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showColorPickerDialog != null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showColorPickerDialog = null },
|
||||||
|
title = { Text("Select Color") },
|
||||||
|
text = {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(minSize = 48.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(HighlightColor.entries) { colorOption ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.background(colorOption.color, CircleShape)
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outline, CircleShape)
|
||||||
|
.clickable {
|
||||||
|
onUpdatePalette(showColorPickerDialog!!, colorOption)
|
||||||
|
showColorPickerDialog = null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = { TextButton(onClick = { showColorPickerDialog = null }) { Text("Close") } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Edit Menu (Delete)
|
// Edit Menu (Delete)
|
||||||
if (activeHighlightForMenu != null) {
|
if (activeHighlightForMenu != null) {
|
||||||
val (highlight, rect) = activeHighlightForMenu!!
|
val (highlight, rect) = activeHighlightForMenu!!
|
||||||
|
|
@ -2739,30 +2769,51 @@ internal fun PaginatedReaderContent(
|
||||||
}, onDismissRequest = { activeHighlightForMenu = null }) {
|
}, onDismissRequest = { activeHighlightForMenu = null }) {
|
||||||
PaginatedTextSelectionMenu(
|
PaginatedTextSelectionMenu(
|
||||||
onCopy = {
|
onCopy = {
|
||||||
val clipboardManager =
|
val clipboardManager =
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
val clip = ClipData.newPlainText("Copied Text", highlight.text)
|
val clip = ClipData.newPlainText("Copied Text", highlight.text)
|
||||||
clipboardManager.setPrimaryClip(clip)
|
clipboardManager.setPrimaryClip(clip)
|
||||||
activeHighlightForMenu = null
|
activeHighlightForMenu = null
|
||||||
}, onSelectAll = null, onDictionary = {
|
},
|
||||||
if (isProUser || countWords(highlight.text) <= 1) {
|
onSelectAll = null,
|
||||||
onWordSelectedForAiDefinition(highlight.text)
|
onDictionary = {
|
||||||
} else {
|
if (isProUser || countWords(highlight.text) <= 1) {
|
||||||
onShowDictionaryUpsellDialog()
|
onWordSelectedForAiDefinition(highlight.text)
|
||||||
}
|
} else {
|
||||||
activeHighlightForMenu = null
|
onShowDictionaryUpsellDialog()
|
||||||
}, onHighlight = { color ->
|
}
|
||||||
Timber.d("Menu: Updating highlight color to ${color.id}")
|
activeHighlightForMenu = null
|
||||||
onHighlightDeleted(highlight.cfi)
|
},
|
||||||
onHighlightCreated(highlight.cfi, highlight.text, color.id)
|
onHighlight = { color ->
|
||||||
activeHighlightForMenu = null
|
Timber.d("Menu: Updating highlight color to ${color.id}")
|
||||||
}, onDelete = {
|
onHighlightDeleted(highlight.cfi)
|
||||||
onHighlightDeleted(highlight.cfi)
|
onHighlightCreated(highlight.cfi, highlight.text, color.id)
|
||||||
activeHighlightForMenu = null
|
activeHighlightForMenu = null
|
||||||
}, isProUser = isProUser, isOss = isOss
|
},
|
||||||
|
onDelete = {
|
||||||
|
onHighlightDeleted(highlight.cfi)
|
||||||
|
activeHighlightForMenu = null
|
||||||
|
},
|
||||||
|
isProUser = isProUser,
|
||||||
|
isOss = isOss,
|
||||||
|
activeHighlightPalette = activeHighlightPalette,
|
||||||
|
onOpenPaletteManager = { showPaletteManager = true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showPaletteManager) {
|
||||||
|
PaletteManagerDialog(
|
||||||
|
currentPalette = activeHighlightPalette,
|
||||||
|
onDismiss = { showPaletteManager = false },
|
||||||
|
onSave = { newPalette ->
|
||||||
|
newPalette.forEachIndexed { index, color ->
|
||||||
|
onUpdatePalette(index, color)
|
||||||
|
}
|
||||||
|
showPaletteManager = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Timber.w("Book has no pages to display.")
|
Timber.w("Book has no pages to display.")
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
|
@ -2811,9 +2862,11 @@ private fun PaginatedTextSelectionMenu(
|
||||||
onSelectAll: (() -> Unit)?,
|
onSelectAll: (() -> Unit)?,
|
||||||
onDictionary: () -> Unit,
|
onDictionary: () -> Unit,
|
||||||
onHighlight: ((HighlightColor) -> Unit)?,
|
onHighlight: ((HighlightColor) -> Unit)?,
|
||||||
@Suppress("SameParameterValue") onDelete: (() -> Unit)?,
|
onDelete: (() -> Unit)?,
|
||||||
@Suppress("unused") isProUser: Boolean,
|
@Suppress("unused") isProUser: Boolean,
|
||||||
isOss: Boolean
|
isOss: Boolean,
|
||||||
|
activeHighlightPalette: List<HighlightColor> = emptyList(),
|
||||||
|
onOpenPaletteManager: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
|
@ -2822,7 +2875,7 @@ private fun PaginatedTextSelectionMenu(
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.width(IntrinsicSize.Max)) {
|
Column(modifier = Modifier.width(IntrinsicSize.Max)) {
|
||||||
// 1. Colors Row (Only show if highlighting is supported)
|
// 1. Colors Row
|
||||||
if (onHighlight != null) {
|
if (onHighlight != null) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -2831,18 +2884,22 @@ private fun PaginatedTextSelectionMenu(
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
HighlightColor.entries.forEach { colorEnum ->
|
activeHighlightPalette.forEach { colorEnum ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 8.dp)
|
.padding(horizontal = 6.dp)
|
||||||
.size(24.dp)
|
.size(32.dp)
|
||||||
.background(colorEnum.color, CircleShape)
|
.background(colorEnum.color, CircleShape)
|
||||||
.border(
|
.clickable { onHighlight(colorEnum) }
|
||||||
1.dp, MaterialTheme.colorScheme.outline.copy(
|
)
|
||||||
alpha = 0.5f
|
}
|
||||||
), CircleShape
|
|
||||||
)
|
if (onOpenPaletteManager != null) {
|
||||||
.clickable { onHighlight(colorEnum) })
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
SpectrumButton(
|
||||||
|
onClick = onOpenPaletteManager,
|
||||||
|
size = 32.dp
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -3137,9 +3194,11 @@ private fun RenderFlexChildBlock(
|
||||||
is TableBlock -> {
|
is TableBlock -> {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
childBlock.rows.forEach { tableRow ->
|
childBlock.rows.forEach { tableRow ->
|
||||||
Row(Modifier
|
Row(
|
||||||
.fillMaxWidth()
|
Modifier
|
||||||
.height(IntrinsicSize.Min)) {
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
) {
|
||||||
val hasFixedWidths =
|
val hasFixedWidths =
|
||||||
tableRow.any { it.style.blockStyle.width != Dp.Unspecified }
|
tableRow.any { it.style.blockStyle.width != Dp.Unspecified }
|
||||||
|
|
||||||
|
|
@ -3199,10 +3258,10 @@ private fun RenderFlexChildBlock(
|
||||||
} else if (blockInCell is ImageBlock) {
|
} else if (blockInCell is ImageBlock) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = Builder(LocalContext.current).data(
|
model = Builder(LocalContext.current).data(
|
||||||
File(
|
File(
|
||||||
blockInCell.path
|
blockInCell.path
|
||||||
)
|
)
|
||||||
).build(),
|
).build(),
|
||||||
contentDescription = blockInCell.altText,
|
contentDescription = blockInCell.altText,
|
||||||
contentScale = ContentScale.Fit,
|
contentScale = ContentScale.Fit,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue