General improvements (#316)
* Added support for right text alignment in epub reader * Added tabs management to PDF navigation drawer * Implemented unified selection menu placement logic * Added reset functionality for toolbar customization * Improved highlight filtering in epub pagination * Migrated hardcoded UI strings to string resources * Extracted desktop application logic from Main.kt into modular files * Refactored Main.kt by extracting PDF and EPUB logic into specialized files * Added Spanish language support * Added system default option to app language selection
This commit is contained in:
parent
759d4b73a0
commit
056485a140
77 changed files with 9184 additions and 5947 deletions
|
|
@ -38,6 +38,8 @@ import android.widget.Toast
|
|||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -46,6 +48,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
|
|
@ -69,6 +72,7 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -83,6 +87,10 @@ import androidx.compose.ui.window.PopupPositionProvider
|
|||
import androidx.core.net.toUri
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.getReaderTextureDataUri
|
||||
import com.aryan.reader.shared.ui.SharedSelectionMenuRect
|
||||
import com.aryan.reader.shared.ui.SharedSelectionMenuSize
|
||||
import com.aryan.reader.shared.ui.SharedSelectionMenuViewport
|
||||
import com.aryan.reader.shared.ui.sharedSelectionMenuPlacement
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
|
@ -1026,6 +1034,8 @@ fun ChapterWebView(
|
|||
|
||||
// Custom Selection Menu Popup
|
||||
customMenuState?.let { state ->
|
||||
val configuration = LocalConfiguration.current
|
||||
val selectionMenuMaxHeight = (configuration.screenHeightDp.dp - 32.dp).coerceAtLeast(160.dp)
|
||||
val popupPositionProvider =
|
||||
remember(state.selectionBounds, density, state.isExistingHighlight) {
|
||||
object : PopupPositionProvider {
|
||||
|
|
@ -1035,30 +1045,23 @@ fun ChapterWebView(
|
|||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize
|
||||
): IntOffset {
|
||||
val topMargin = with(density) { 16.dp.toPx() }.toInt()
|
||||
val bottomMargin = with(density) {
|
||||
val marginPx = with(density) { 16.dp.toPx() }
|
||||
val gapPx = with(density) {
|
||||
if (state.isExistingHighlight) 16.dp.toPx() else 60.dp.toPx()
|
||||
}.toInt()
|
||||
|
||||
var x = state.selectionBounds.centerX() - popupContentSize.width / 2
|
||||
|
||||
var y = state.selectionBounds.top - popupContentSize.height - topMargin
|
||||
if (y < with(density) { 24.dp.toPx() }.toInt()) {
|
||||
y = state.selectionBounds.bottom + bottomMargin
|
||||
}
|
||||
if (x < 0) x = 0
|
||||
if (x + popupContentSize.width > windowSize.width) {
|
||||
x = windowSize.width - popupContentSize.width
|
||||
}
|
||||
if (y + popupContentSize.height > windowSize.height) {
|
||||
y = windowSize.height - popupContentSize.height
|
||||
}
|
||||
if (y < 0) y = 0
|
||||
|
||||
return IntOffset(
|
||||
x.coerceIn(0, windowSize.width - popupContentSize.width),
|
||||
y.coerceIn(0, windowSize.height - popupContentSize.height)
|
||||
val placement = sharedSelectionMenuPlacement(
|
||||
viewport = SharedSelectionMenuViewport(windowSize.width, windowSize.height),
|
||||
popup = SharedSelectionMenuSize(popupContentSize.width, popupContentSize.height),
|
||||
selection = SharedSelectionMenuRect(
|
||||
left = state.selectionBounds.left.toFloat(),
|
||||
top = state.selectionBounds.top.toFloat(),
|
||||
right = state.selectionBounds.right.toFloat(),
|
||||
bottom = state.selectionBounds.bottom.toFloat()
|
||||
),
|
||||
marginPx = marginPx,
|
||||
gapPx = gapPx
|
||||
)
|
||||
return IntOffset(placement.x, placement.y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1075,11 +1078,14 @@ fun ChapterWebView(
|
|||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.width(IntrinsicSize.Max)
|
||||
modifier = Modifier
|
||||
.width(IntrinsicSize.Max)
|
||||
.heightIn(max = selectionMenuMaxHeight)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
.padding(vertical = 8.dp, horizontal = 10.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
|
@ -1087,8 +1093,8 @@ fun ChapterWebView(
|
|||
activeHighlightPalette.forEachIndexed { index, colorEnum ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(28.dp)
|
||||
.background(colorEnum.color, CircleShape)
|
||||
.pointerInput(colorEnum) {
|
||||
detectTapGestures(onTap = {
|
||||
|
|
@ -1115,7 +1121,7 @@ fun ChapterWebView(
|
|||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
SpectrumButton(
|
||||
onClick = { showPaletteManager = true }, size = 32.dp
|
||||
onClick = { showPaletteManager = true }, size = 28.dp
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1123,7 +1129,7 @@ fun ChapterWebView(
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ suspend fun summarizeBookContent(
|
|||
onFinish: () -> Unit
|
||||
) {
|
||||
if (content.isBlank()) {
|
||||
onError("The book content is empty.")
|
||||
onError(context.getString(R.string.ai_error_book_content_empty))
|
||||
onFinish()
|
||||
return
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ suspend fun summarizeBookContent(
|
|||
@Suppress("KotlinConstantConditions")
|
||||
if (BuildConfig.FLAVOR == "oss") {
|
||||
if (BuildConfig.IS_OFFLINE) {
|
||||
onError("AI features are unavailable in the offline OSS build.")
|
||||
onError(context.getString(R.string.ai_error_offline_oss))
|
||||
onFinish()
|
||||
return
|
||||
}
|
||||
|
|
@ -154,7 +154,7 @@ suspend fun summarizeBookContent(
|
|||
}
|
||||
}
|
||||
if (!hasReceivedData) {
|
||||
onError("Failed to parse summary from server response.")
|
||||
onError(context.getString(R.string.ai_error_parse_summary))
|
||||
}
|
||||
} else {
|
||||
val errorBody = try {
|
||||
|
|
@ -162,12 +162,12 @@ suspend fun summarizeBookContent(
|
|||
} catch (_: Exception) { null }
|
||||
val errorDetail = try {
|
||||
JSONObject(errorBody.toString()).getString("detail")
|
||||
} catch (_: Exception) { "Could not fetch summary." }
|
||||
onError("Error: $responseCode. $errorDetail")
|
||||
} catch (_: Exception) { context.getString(R.string.ai_error_fetch_summary) }
|
||||
onError(context.getString(R.string.ai_error_with_code, responseCode, errorDetail))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Network error during summarization: ${e.message}")
|
||||
onError("Network error. Please check connection and server status.")
|
||||
onError(context.getString(R.string.ai_error_network_server))
|
||||
} finally {
|
||||
connection?.disconnect()
|
||||
onFinish()
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ fun PaginatedTextSelectionMenu(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 180.dp)) {
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
activeHighlightPalette = activeHighlightPalette,
|
||||
|
|
@ -531,7 +531,7 @@ fun PaginatedTextSelectionMenu(
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
@ -539,23 +539,23 @@ fun PaginatedTextSelectionMenu(
|
|||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.width(56.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
Spacer(modifier = Modifier.width(56.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -582,7 +582,7 @@ fun HighlightColorRow(
|
|||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(vertical = 12.dp, horizontal = 12.dp)
|
||||
.padding(vertical = 8.dp, horizontal = 10.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
|
@ -591,8 +591,8 @@ fun HighlightColorRow(
|
|||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 6.dp)
|
||||
.size(32.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
.size(28.dp)
|
||||
.clip(CircleShape) // 1. Clip shape for ripple
|
||||
.background(colorEnum.color) // 2. Apply background
|
||||
.clickable {
|
||||
|
|
@ -610,17 +610,17 @@ fun HighlightColorRow(
|
|||
imageVector = Icons.Default.Check,
|
||||
contentDescription = stringResource(R.string.content_desc_selected),
|
||||
tint = if (colorEnum == HighlightColor.WHITE || colorEnum == HighlightColor.YELLOW) Color.Black else Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (onOpenPaletteManager != null) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
SpectrumButton(
|
||||
onClick = onOpenPaletteManager,
|
||||
size = 32.dp
|
||||
size = 28.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -650,7 +650,7 @@ fun PaginatedTextSelectionMenu(
|
|||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 200.dp)) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Max).widthIn(min = 180.dp)) {
|
||||
// 1. Colors Row
|
||||
if (onHighlight != null) {
|
||||
HighlightColorRow(
|
||||
|
|
@ -731,7 +731,7 @@ fun PaginatedTextSelectionMenu(
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
.padding(horizontal = 6.dp, vertical = 3.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
@ -739,22 +739,22 @@ fun PaginatedTextSelectionMenu(
|
|||
val tint = if (action.isError) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.width(56.dp)
|
||||
.clickable { action.onClick() }
|
||||
.padding(vertical = 8.dp),
|
||||
.padding(vertical = 6.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (action.imageVector != null) {
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
Icon(imageVector = action.imageVector, contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
|
||||
} else if (action.iconRes != null) {
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(24.dp))
|
||||
Icon(painter = painterResource(id = action.iconRes), contentDescription = action.label, tint = tint, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(text = action.label, style = MaterialTheme.typography.labelSmall, color = tint, maxLines = 1)
|
||||
}
|
||||
}
|
||||
repeat(3 - rowActions.size) {
|
||||
Spacer(modifier = Modifier.width(64.dp))
|
||||
Spacer(modifier = Modifier.width(56.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import android.graphics.Canvas
|
|||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -153,26 +154,26 @@ import kotlinx.coroutines.withContext
|
|||
import timber.log.Timber
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class ReaderTool(val title: String, val category: String) {
|
||||
DICTIONARY("External Apps", "Top Bar"),
|
||||
THEME("Theme Settings", "Top Bar"),
|
||||
SLIDER("Navigation Slider", "Bottom Bar"),
|
||||
TOC("Sidebar", "Bottom Bar"),
|
||||
FORMAT("Text Formatting", "Bottom Bar"),
|
||||
SEARCH("Search", "Bottom Bar"),
|
||||
AI_FEATURES("AI Features", "Bottom Bar"),
|
||||
TTS_CONTROLS("TTS Controls", "Bottom Bar"),
|
||||
READING_MODE("Reading Mode", "Overflow Menu"),
|
||||
BOOKMARK("Bookmark", "Overflow Menu"),
|
||||
TAP_TO_TURN("Tap to Turn Pages", "Overflow Menu"),
|
||||
VOLUME_SCROLL("Volume Button Scrolling", "Overflow Menu"),
|
||||
PAGE_TURN_ANIM("Realistic Page Turns", "Overflow Menu"),
|
||||
KEEP_SCREEN_ON("Keep Screen On", "Overflow Menu"),
|
||||
VISUAL_OPTIONS("Visual Options", "Overflow Menu"),
|
||||
SCREEN_ORIENTATION("Screen Orientation", "Top Bar"),
|
||||
AUTO_SCROLL("Auto Scroll", "Overflow Menu"),
|
||||
TTS_SETTINGS("TTS Settings", "Overflow Menu"),
|
||||
TTS_REPLACEMENTS("TTS Word Replacements", "Overflow Menu")
|
||||
enum class ReaderTool(@StringRes val titleRes: Int, val category: String) {
|
||||
DICTIONARY(R.string.tool_external_apps, "Top Bar"),
|
||||
THEME(R.string.tooltip_theme_desc, "Top Bar"),
|
||||
SLIDER(R.string.tool_navigation_slider, "Bottom Bar"),
|
||||
TOC(R.string.tool_sidebar, "Bottom Bar"),
|
||||
FORMAT(R.string.content_desc_text_formatting, "Bottom Bar"),
|
||||
SEARCH(R.string.action_search, "Bottom Bar"),
|
||||
AI_FEATURES(R.string.ai_features_title, "Bottom Bar"),
|
||||
TTS_CONTROLS(R.string.tool_tts_controls, "Bottom Bar"),
|
||||
READING_MODE(R.string.tool_reading_mode, "Overflow Menu"),
|
||||
BOOKMARK(R.string.content_desc_bookmark, "Overflow Menu"),
|
||||
TAP_TO_TURN(R.string.menu_tap_to_turn_pages, "Overflow Menu"),
|
||||
VOLUME_SCROLL(R.string.menu_volume_button_scrolling, "Overflow Menu"),
|
||||
PAGE_TURN_ANIM(R.string.menu_realistic_page_turns, "Overflow Menu"),
|
||||
KEEP_SCREEN_ON(R.string.menu_keep_screen_on, "Overflow Menu"),
|
||||
VISUAL_OPTIONS(R.string.menu_visual_options, "Overflow Menu"),
|
||||
SCREEN_ORIENTATION(R.string.menu_screen_orientation, "Top Bar"),
|
||||
AUTO_SCROLL(R.string.menu_auto_scroll, "Overflow Menu"),
|
||||
TTS_SETTINGS(R.string.menu_tts_settings, "Overflow Menu"),
|
||||
TTS_REPLACEMENTS(R.string.menu_tts_word_replacements, "Overflow Menu")
|
||||
}
|
||||
|
||||
enum class FlatItemType { SECTION_HEADER, TOOL, EMPTY_PLACEHOLDER, MORE_HEADER, MORE_TOOL }
|
||||
|
|
@ -182,7 +183,8 @@ data class FlatToolItem(
|
|||
val type: FlatItemType,
|
||||
val tool: ReaderTool? = null,
|
||||
val section: ToolbarSection? = null,
|
||||
val title: String? = null
|
||||
val title: String? = null,
|
||||
@StringRes val titleRes: Int? = null
|
||||
)
|
||||
|
||||
fun sanitizePlaceholders(list: List<FlatToolItem>): List<FlatToolItem> {
|
||||
|
|
@ -197,7 +199,7 @@ fun sanitizePlaceholders(list: List<FlatToolItem>): List<FlatToolItem> {
|
|||
}
|
||||
|
||||
ToolbarSection.entries.forEach { section ->
|
||||
result.add(FlatToolItem("header_${section.name}", FlatItemType.SECTION_HEADER, section = section, title = section.title))
|
||||
result.add(FlatToolItem("header_${section.name}", FlatItemType.SECTION_HEADER, section = section, titleRes = section.titleRes))
|
||||
|
||||
val tools = sectionMap[section] ?: emptyList()
|
||||
if (tools.isEmpty()) {
|
||||
|
|
@ -266,6 +268,51 @@ private val epubToolbarTools = setOf(
|
|||
ReaderTool.SCREEN_ORIENTATION
|
||||
)
|
||||
|
||||
internal fun defaultReaderHiddenTools(): Set<String> = setOf(ReaderTool.SCREEN_ORIENTATION.name)
|
||||
|
||||
internal fun defaultReaderToolOrder(): List<ReaderTool> = ReaderTool.entries.toList()
|
||||
|
||||
internal fun defaultReaderBottomTools(): Set<String> {
|
||||
return ReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
|
||||
}
|
||||
|
||||
internal fun buildReaderToolbarItems(
|
||||
hiddenTools: Set<String>,
|
||||
toolOrder: List<ReaderTool>,
|
||||
bottomTools: Set<String>
|
||||
): List<FlatToolItem> {
|
||||
val toolbarTools = toolOrder.filter { it in epubToolbarTools }
|
||||
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
|
||||
val moreTools = toolOrder.filter { it !in epubToolbarTools }
|
||||
|
||||
val list = mutableListOf<FlatToolItem>()
|
||||
|
||||
ToolbarSection.entries.forEach { section ->
|
||||
val tools = when (section) {
|
||||
ToolbarSection.TOP -> topTools
|
||||
ToolbarSection.BOTTOM -> bottomToolsList
|
||||
ToolbarSection.HIDDEN -> hiddenToolsList
|
||||
}
|
||||
list.add(FlatToolItem("header_${section.name}", FlatItemType.SECTION_HEADER, section = section, titleRes = section.titleRes))
|
||||
if (tools.isEmpty()) {
|
||||
list.add(FlatToolItem("empty_${section.name}", FlatItemType.EMPTY_PLACEHOLDER, section = section))
|
||||
} else {
|
||||
tools.forEach { tool ->
|
||||
list.add(FlatToolItem("tool_${tool.name}", FlatItemType.TOOL, tool = tool, section = section))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.add(FlatToolItem("more_header", FlatItemType.MORE_HEADER, titleRes = R.string.toolbar_more_menu))
|
||||
moreTools.forEach { tool ->
|
||||
list.add(FlatToolItem("more_${tool.name}", FlatItemType.MORE_TOOL, tool = tool))
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpubReaderTopBar(
|
||||
isVisible: Boolean,
|
||||
|
|
@ -478,7 +525,7 @@ fun EpubReaderTopBar(
|
|||
|
||||
if (hiddenToolbarTools.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Hidden tools") },
|
||||
text = { Text(stringResource(R.string.toolbar_hidden_tools_menu)) },
|
||||
onClick = { showHiddenToolsExpanded = !showHiddenToolsExpanded },
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
|
|
@ -1717,37 +1764,11 @@ fun CustomizeToolsSheet(
|
|||
|
||||
var flatItems by remember {
|
||||
mutableStateOf(
|
||||
run {
|
||||
val toolbarTools = toolOrder.filter { it in epubToolbarTools }
|
||||
val topTools = toolbarTools.filter { !bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val bottomToolsList = toolbarTools.filter { bottomTools.contains(it.name) && !hiddenTools.contains(it.name) }
|
||||
val hiddenToolsList = toolbarTools.filter { hiddenTools.contains(it.name) }
|
||||
val moreTools = toolOrder.filter { it !in epubToolbarTools }
|
||||
|
||||
val list = mutableListOf<FlatToolItem>()
|
||||
|
||||
ToolbarSection.entries.forEach { section ->
|
||||
val tools = when(section) {
|
||||
ToolbarSection.TOP -> topTools
|
||||
ToolbarSection.BOTTOM -> bottomToolsList
|
||||
ToolbarSection.HIDDEN -> hiddenToolsList
|
||||
}
|
||||
list.add(FlatToolItem("header_${section.name}", FlatItemType.SECTION_HEADER, section = section, title = section.title))
|
||||
if (tools.isEmpty()) {
|
||||
list.add(FlatToolItem("empty_${section.name}", FlatItemType.EMPTY_PLACEHOLDER, section = section))
|
||||
} else {
|
||||
tools.forEach { tool ->
|
||||
list.add(FlatToolItem("tool_${tool.name}", FlatItemType.TOOL, tool = tool, section = section))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.add(FlatToolItem("more_header", FlatItemType.MORE_HEADER, title = "More menu"))
|
||||
moreTools.forEach { tool ->
|
||||
list.add(FlatToolItem("more_${tool.name}", FlatItemType.MORE_TOOL, tool = tool))
|
||||
}
|
||||
list
|
||||
}
|
||||
buildReaderToolbarItems(
|
||||
hiddenTools = hiddenTools,
|
||||
toolOrder = toolOrder,
|
||||
bottomTools = bottomTools
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1812,6 +1833,22 @@ fun CustomizeToolsSheet(
|
|||
}
|
||||
}
|
||||
|
||||
val resetToDefault = {
|
||||
val defaultHiddenTools = defaultReaderHiddenTools()
|
||||
val defaultToolOrder = defaultReaderToolOrder()
|
||||
val defaultBottomTools = defaultReaderBottomTools()
|
||||
|
||||
localHiddenTools = defaultHiddenTools
|
||||
flatItems = buildReaderToolbarItems(
|
||||
hiddenTools = defaultHiddenTools,
|
||||
toolOrder = defaultToolOrder,
|
||||
bottomTools = defaultBottomTools
|
||||
)
|
||||
onUpdate(defaultHiddenTools)
|
||||
onPlacementUpdate(defaultBottomTools)
|
||||
onOrderUpdate(defaultToolOrder)
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
|
|
@ -1828,12 +1865,17 @@ fun CustomizeToolsSheet(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Customize Toolbar",
|
||||
text = stringResource(R.string.title_customize_toolbar),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(onClick = resetToDefault) {
|
||||
Icon(Icons.Default.Refresh, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.action_reset))
|
||||
}
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.Default.Close, contentDescription = stringResource(R.string.action_close))
|
||||
}
|
||||
|
|
@ -1866,8 +1908,9 @@ fun CustomizeToolsSheet(
|
|||
) {
|
||||
when (item.type) {
|
||||
FlatItemType.SECTION_HEADER -> {
|
||||
val titleRes = item.titleRes
|
||||
Text(
|
||||
text = item.title ?: "",
|
||||
text = if (titleRes != null) stringResource(titleRes) else item.title.orEmpty(),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
|
|
@ -1883,7 +1926,7 @@ fun CustomizeToolsSheet(
|
|||
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f), RoundedCornerShape(12.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Drop tools here", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(stringResource(R.string.toolbar_drop_tools_here), color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
FlatItemType.TOOL -> {
|
||||
|
|
@ -1900,8 +1943,9 @@ fun CustomizeToolsSheet(
|
|||
)
|
||||
}
|
||||
FlatItemType.MORE_HEADER -> {
|
||||
val titleRes = item.titleRes
|
||||
Text(
|
||||
text = item.title ?: "More menu",
|
||||
text = if (titleRes != null) stringResource(titleRes) else item.title ?: stringResource(R.string.toolbar_more_menu),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 24.dp, bottom = 8.dp, start = 4.dp)
|
||||
|
|
@ -1909,7 +1953,7 @@ fun CustomizeToolsSheet(
|
|||
}
|
||||
FlatItemType.MORE_TOOL -> {
|
||||
MoreToolVisibilityRow(
|
||||
title = item.tool!!.title,
|
||||
title = stringResource(item.tool!!.titleRes),
|
||||
visible = !localHiddenTools.contains(item.tool.name),
|
||||
onToggle = {
|
||||
localHiddenTools = if (localHiddenTools.contains(item.tool.name)) {
|
||||
|
|
@ -1952,14 +1996,14 @@ private fun ToolbarDragRow(
|
|||
ToolPreviewIcon(tool)
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Text(
|
||||
text = tool.title,
|
||||
text = stringResource(tool.titleRes),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
contentDescription = "Drag to reorder",
|
||||
contentDescription = stringResource(R.string.content_desc_drag_to_reorder),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
|
|
@ -2007,25 +2051,26 @@ private fun MoreToolVisibilityRow(
|
|||
}
|
||||
}
|
||||
|
||||
enum class ToolbarSection(val title: String) {
|
||||
TOP("Top Bar"),
|
||||
BOTTOM("Bottom Bar"),
|
||||
HIDDEN("Hidden Tools")
|
||||
enum class ToolbarSection(@StringRes val titleRes: Int) {
|
||||
TOP(R.string.toolbar_top_bar),
|
||||
BOTTOM(R.string.toolbar_bottom_bar),
|
||||
HIDDEN(R.string.toolbar_hidden_tools)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToolPreviewIcon(tool: ReaderTool) {
|
||||
val title = stringResource(tool.titleRes)
|
||||
when (tool) {
|
||||
ReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.FORMAT -> Icon(painterResource(id = R.drawable.format_size), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SCREEN_ORIENTATION -> Icon(Icons.Default.ScreenRotation, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
else -> Icon(Icons.Default.MoreVert, contentDescription = tool.title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.DICTIONARY -> Icon(painterResource(id = R.drawable.dictionary), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.THEME -> Icon(painterResource(id = R.drawable.palette), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SLIDER -> Icon(painterResource(id = R.drawable.slider), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.TOC -> Icon(Icons.Default.Menu, contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.FORMAT -> Icon(painterResource(id = R.drawable.format_size), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
ReaderTool.SCREEN_ORIENTATION -> Icon(Icons.Default.ScreenRotation, contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
else -> Icon(Icons.Default.MoreVert, contentDescription = title, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2050,7 +2095,7 @@ private fun HiddenEpubToolMenuItem(
|
|||
else -> true
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(tool.title) },
|
||||
text = { Text(stringResource(tool.titleRes)) },
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
showMoreMenu()
|
||||
|
|
@ -2198,7 +2243,11 @@ fun TtsOverlayControls(
|
|||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Text(
|
||||
if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) "✨ Cloud" else "📱 Device",
|
||||
if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
|
||||
stringResource(R.string.tts_mode_cloud_ai)
|
||||
} else {
|
||||
stringResource(R.string.tts_mode_device_native)
|
||||
},
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
|
|
@ -2211,7 +2260,7 @@ fun TtsOverlayControls(
|
|||
) {
|
||||
val voiceName = if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
|
||||
GEMINI_TTS_SPEAKERS.find { it.id == ttsState.speakerId }?.name ?: ttsState.speakerId
|
||||
} else loadNativeVoice(context)?.split("-")?.lastOrNull() ?: "Default"
|
||||
} else loadNativeVoice(context)?.split("-")?.lastOrNull() ?: stringResource(R.string.label_default)
|
||||
|
||||
Text(
|
||||
voiceName,
|
||||
|
|
|
|||
|
|
@ -253,6 +253,15 @@ private const val TTS_LOCATE_REASON_OVERLAY = "overlay"
|
|||
|
||||
private const val TAG_LINK_NAV = "LINK_NAV"
|
||||
private const val TAG_STABLE_PAGE_NAV = "StablePageNav"
|
||||
private const val TAG_PAGINATED_HIGHLIGHT_DIAG = "PaginatedHighlightDiag"
|
||||
|
||||
private fun epubHighlightDiagSnippet(text: String, maxLength: Int = 80): String {
|
||||
return text
|
||||
.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.replace('\t', ' ')
|
||||
.take(maxLength)
|
||||
}
|
||||
|
||||
private fun View.bottomRoundedCornerRadiusPx(): Int {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return 0
|
||||
|
|
@ -303,7 +312,7 @@ private fun loadHiddenTools(context: Context): Set<String> {
|
|||
val savedHiddenTools = prefs.getStringSet(HIDDEN_TOOLS_KEY, emptySet()).orEmpty()
|
||||
val defaultsVersion = prefs.getInt(HIDDEN_TOOLS_DEFAULTS_VERSION_KEY, 0)
|
||||
if (defaultsVersion < HIDDEN_TOOLS_DEFAULTS_VERSION) {
|
||||
val migratedHiddenTools = savedHiddenTools + ReaderTool.SCREEN_ORIENTATION.name
|
||||
val migratedHiddenTools = savedHiddenTools + defaultReaderHiddenTools()
|
||||
prefs.edit {
|
||||
putStringSet(HIDDEN_TOOLS_KEY, migratedHiddenTools)
|
||||
putInt(HIDDEN_TOOLS_DEFAULTS_VERSION_KEY, HIDDEN_TOOLS_DEFAULTS_VERSION)
|
||||
|
|
@ -325,7 +334,7 @@ private fun loadToolOrder(context: Context): List<ReaderTool> {
|
|||
?.filter { it.isNotBlank() }
|
||||
?.mapNotNull { name -> ReaderTool.entries.firstOrNull { it.name == name } }
|
||||
.orEmpty()
|
||||
return (savedTools + ReaderTool.entries.filterNot { it in savedTools }).distinct()
|
||||
return (savedTools + defaultReaderToolOrder().filterNot { it in savedTools }).distinct()
|
||||
}
|
||||
|
||||
private fun saveBottomTools(context: Context, bottomTools: Set<String>) {
|
||||
|
|
@ -337,8 +346,8 @@ private fun loadBottomTools(context: Context): Set<String> {
|
|||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getStringSet(
|
||||
BOTTOM_TOOLS_KEY,
|
||||
ReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
|
||||
) ?: ReaderTool.entries.filter { it.category == "Bottom Bar" }.map { it.name }.toSet()
|
||||
defaultReaderBottomTools()
|
||||
) ?: defaultReaderBottomTools()
|
||||
}
|
||||
|
||||
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
|
||||
|
|
@ -2413,13 +2422,13 @@ fun EpubReaderHost(
|
|||
val targetPageIndex = pageIndex
|
||||
val targetCfi = cfi.orEmpty()
|
||||
if (targetPageIndex != null && (targetCfi.isBlank() || targetCfi.startsWith("android-page:"))) {
|
||||
return "Page ${targetPageIndex + 1}"
|
||||
return context.getString(R.string.pdf_page_short, targetPageIndex + 1)
|
||||
}
|
||||
val chapter = chapterIndex
|
||||
return if (chapter != null) {
|
||||
chapters.getOrNull(chapter)?.title?.takeIf { it.isNotBlank() } ?: "Chapter ${chapter + 1}"
|
||||
chapters.getOrNull(chapter)?.title?.takeIf { it.isNotBlank() } ?: context.getString(R.string.chapter_number_format, chapter + 1)
|
||||
} else {
|
||||
"Location"
|
||||
context.getString(R.string.location_generic)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3262,7 +3271,7 @@ fun EpubReaderHost(
|
|||
} ?: run {
|
||||
isSummarizationLoading = false
|
||||
summarizationResult =
|
||||
SummarizationResult(error = "WebView not available.")
|
||||
SummarizationResult(error = context.getString(R.string.error_webview_not_available))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3333,7 +3342,7 @@ fun EpubReaderHost(
|
|||
if (fullSummary.isNotBlank()) {
|
||||
val chapterTitle =
|
||||
chapters.getOrNull(chapterIndex)?.title
|
||||
?: "Chapter ${chapterIndex + 1}"
|
||||
?: context.getString(R.string.chapter_number_format, chapterIndex + 1)
|
||||
summaryCacheManager.saveSummary(
|
||||
epubBook.title,
|
||||
chapterIndex,
|
||||
|
|
@ -3344,12 +3353,12 @@ fun EpubReaderHost(
|
|||
})
|
||||
} else {
|
||||
summarizationResult =
|
||||
SummarizationResult(error = "Could not get chapter content.")
|
||||
SummarizationResult(error = context.getString(R.string.error_could_not_get_chapter_content))
|
||||
isSummarizationLoading = false
|
||||
}
|
||||
} else {
|
||||
summarizationResult =
|
||||
SummarizationResult(error = "Could not determine current chapter.")
|
||||
SummarizationResult(error = context.getString(R.string.error_could_not_determine_chapter))
|
||||
isSummarizationLoading = false
|
||||
}
|
||||
}
|
||||
|
|
@ -4207,7 +4216,7 @@ fun EpubReaderHost(
|
|||
isSummarizationLoading = false
|
||||
val fullSummary = finalSummaryBuilder.toString()
|
||||
if (fullSummary.isNotBlank()) {
|
||||
val chapterTitle = chapters.getOrNull(chapterIndexToSave)?.title ?: "Chapter ${chapterIndexToSave + 1}"
|
||||
val chapterTitle = chapters.getOrNull(chapterIndexToSave)?.title ?: context.getString(R.string.chapter_number_format, chapterIndexToSave + 1)
|
||||
summaryCacheManager.saveSummary(bookTitleToSave, chapterIndexToSave, chapterTitle, fullSummary)
|
||||
}
|
||||
}
|
||||
|
|
@ -4362,7 +4371,7 @@ fun EpubReaderHost(
|
|||
)
|
||||
val chapterTitle =
|
||||
epubBook.chapters.getOrNull(currentChapterIndex)?.title
|
||||
?: "Unknown Chapter"
|
||||
?: context.getString(R.string.unknown_chapter)
|
||||
val newBookmark = Bookmark(
|
||||
cfi = cfi,
|
||||
chapterTitle = chapterTitle,
|
||||
|
|
@ -4571,15 +4580,33 @@ fun EpubReaderHost(
|
|||
highlight.chapterIndex in (currentChapter - 1)..(currentChapter + 1)
|
||||
},
|
||||
onHighlightCreated = { cfi, text, colorId ->
|
||||
val chapterIndex = currentChapterInPaginatedMode ?: 0
|
||||
Timber.tag(TAG_PAGINATED_HIGHLIGHT_DIAG).d(
|
||||
"persist_request cfi=$cfi colorId=$colorId chapter=$chapterIndex " +
|
||||
"existingCount=${userHighlights.size} textLen=${text.length} " +
|
||||
"text='${epubHighlightDiagSnippet(text)}'"
|
||||
)
|
||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
|
||||
val finalCfi = processAndAddHighlight(
|
||||
newCfi = cfi,
|
||||
newText = text,
|
||||
newColor = color,
|
||||
chapterIndex = currentChapterInPaginatedMode ?: 0,
|
||||
chapterIndex = chapterIndex,
|
||||
currentList = userHighlights
|
||||
)
|
||||
val savedHighlight = userHighlights.find {
|
||||
it.chapterIndex == chapterIndex && it.cfi == finalCfi
|
||||
}
|
||||
Timber.tag(TAG_PAGINATED_HIGHLIGHT_DIAG).d(
|
||||
"persist_result finalCfi=$finalCfi chapter=$chapterIndex " +
|
||||
"savedId=${savedHighlight?.id} totalCount=${userHighlights.size} " +
|
||||
"matchingCfiCount=${userHighlights.count { it.chapterIndex == chapterIndex && it.cfi == finalCfi }} " +
|
||||
"locatorStart=${savedHighlight?.locator?.startOffset} " +
|
||||
"locatorEnd=${savedHighlight?.locator?.endOffset} " +
|
||||
"locatorPage=${savedHighlight?.locator?.pageIndex} " +
|
||||
"locatorCfi=${savedHighlight?.locator?.cfi}"
|
||||
)
|
||||
if (pendingNoteForNewHighlight) {
|
||||
pendingNoteForNewHighlight = false
|
||||
highlightToNoteCfi = finalCfi
|
||||
|
|
@ -4616,9 +4643,24 @@ fun EpubReaderHost(
|
|||
)?.let { recordEpubJump(it) }
|
||||
},
|
||||
onHighlightDeleted = { cfi ->
|
||||
val beforeCount = userHighlights.size
|
||||
val toRemove = userHighlights.find { it.cfi == cfi }
|
||||
if (toRemove != null) {
|
||||
Timber.tag(TAG_PAGINATED_HIGHLIGHT_DIAG).d(
|
||||
"delete_request cfi=$cfi matchedId=${toRemove.id} " +
|
||||
"matchedChapter=${toRemove.chapterIndex} beforeCount=$beforeCount " +
|
||||
"locatorStart=${toRemove.locator.startOffset} " +
|
||||
"locatorEnd=${toRemove.locator.endOffset}"
|
||||
)
|
||||
userHighlights.remove(toRemove)
|
||||
Timber.tag(TAG_PAGINATED_HIGHLIGHT_DIAG).d(
|
||||
"delete_result cfi=$cfi removedId=${toRemove.id} " +
|
||||
"afterCount=${userHighlights.size}"
|
||||
)
|
||||
} else {
|
||||
Timber.tag(TAG_PAGINATED_HIGHLIGHT_DIAG).w(
|
||||
"delete_request cfi=$cfi matchedId=null beforeCount=$beforeCount"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -4745,7 +4787,7 @@ fun EpubReaderHost(
|
|||
val finalCfi = if (offset > 0) "$baseCfi:$offset" else baseCfi
|
||||
|
||||
val chapterIndex = paginator?.findChapterIndexForPage(paginatedPagerState.currentPage)
|
||||
val chapterTitle = chapterIndex?.let { epubBook.chapters.getOrNull(it)?.title } ?: "Unknown Chapter"
|
||||
val chapterTitle = chapterIndex?.let { epubBook.chapters.getOrNull(it)?.title } ?: context.getString(R.string.unknown_chapter)
|
||||
val snippet = (targetBlockForBookmark as? TextContentBlock)?.content?.text?.take(150) ?: ""
|
||||
|
||||
val pageInChapter: Int?
|
||||
|
|
@ -4862,7 +4904,7 @@ fun EpubReaderHost(
|
|||
val textToShow = if (bookPaginator != null && chapterIndex != null) {
|
||||
val chapterTitle =
|
||||
chapters.getOrNull(chapterIndex)?.title?.take(30)?.trim()
|
||||
?: "Chapter"
|
||||
?: stringResource(R.string.chapter)
|
||||
val totalPagesInChapter = bookPaginator.chapterPageCounts[chapterIndex]
|
||||
val chapterStartPage = bookPaginator.chapterStartPageIndices[chapterIndex]
|
||||
|
||||
|
|
@ -4874,7 +4916,7 @@ fun EpubReaderHost(
|
|||
chapterTitle
|
||||
}
|
||||
} else {
|
||||
"Page ${paginatedPagerState.currentPage + 1}/${paginatedPagerState.pageCount}"
|
||||
stringResource(R.string.page_number_of_total, paginatedPagerState.currentPage + 1, paginatedPagerState.pageCount)
|
||||
}
|
||||
|
||||
Text(
|
||||
|
|
@ -5568,7 +5610,7 @@ fun EpubReaderHost(
|
|||
onVerticalMarginChange = { currentVerticalMargin = it },
|
||||
currentFont = currentFontFamily,
|
||||
currentCustomFontName = if(currentCustomFontPath != null) {
|
||||
customFonts.find { it.path == currentCustomFontPath }?.displayName ?: "Custom Font"
|
||||
customFonts.find { it.path == currentCustomFontPath }?.displayName ?: stringResource(R.string.custom_font_fallback)
|
||||
} else null,
|
||||
onFontOptionClick = { showFontSelectionSheet = true },
|
||||
currentTextAlign = currentTextAlign,
|
||||
|
|
@ -5649,7 +5691,7 @@ fun EpubReaderHost(
|
|||
credits = credits,
|
||||
isProUser = isProUser,
|
||||
currentChapterIndex = effectiveCurrentChapterIndex,
|
||||
chapterTitle = chapters.getOrNull(effectiveCurrentChapterIndex)?.title ?: "Chapter ${effectiveCurrentChapterIndex + 1}",
|
||||
chapterTitle = chapters.getOrNull(effectiveCurrentChapterIndex)?.title ?: context.getString(R.string.chapter_number_format, effectiveCurrentChapterIndex + 1),
|
||||
showAiHubSheet = showAiHubSheet,
|
||||
onGenerateSummary = handleGenerateSummary,
|
||||
onGenerateRecap = handleGenerateRecap,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ package com.aryan.reader.epubreader
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
|
|
@ -161,27 +162,28 @@ enum class ReaderFont(val id: String, val displayName: String, val fontFamilyNam
|
|||
LEXEND("lexend", "Lexend", "Lexend")
|
||||
}
|
||||
|
||||
enum class ReaderTextAlign(val id: String, val cssValue: String, val iconResId: Int, val displayName: String) {
|
||||
DEFAULT("default", "", R.drawable.format_align_left, "Default"),
|
||||
LEFT("left", "left", R.drawable.format_align_left, "Left"),
|
||||
JUSTIFY("justify", "justify", R.drawable.format_align_justify, "Justify")
|
||||
enum class ReaderTextAlign(val id: String, val cssValue: String, val iconResId: Int, @StringRes val displayNameRes: Int) {
|
||||
DEFAULT("default", "", R.drawable.format_align_left, R.string.label_default),
|
||||
LEFT("left", "left", R.drawable.format_align_left, R.string.label_left),
|
||||
RIGHT("right", "right", R.drawable.format_align_right, R.string.label_right),
|
||||
JUSTIFY("justify", "justify", R.drawable.format_align_justify, R.string.label_justify)
|
||||
}
|
||||
|
||||
enum class SystemUiMode(val id: Int, val title: String) {
|
||||
DEFAULT(0, "Always Show"),
|
||||
SYNC(1, "Sync with Menus"),
|
||||
HIDDEN(2, "Always Hide")
|
||||
enum class SystemUiMode(val id: Int, @StringRes val titleRes: Int) {
|
||||
DEFAULT(0, R.string.label_always_show),
|
||||
SYNC(1, R.string.label_sync_with_menus),
|
||||
HIDDEN(2, R.string.label_always_hide)
|
||||
}
|
||||
|
||||
enum class PageInfoMode(val id: Int, val title: String) {
|
||||
DEFAULT(0, "Always Show"),
|
||||
SYNC(1, "Sync with Menus"),
|
||||
HIDDEN(2, "Always Hide")
|
||||
enum class PageInfoMode(val id: Int, @StringRes val titleRes: Int) {
|
||||
DEFAULT(0, R.string.label_always_show),
|
||||
SYNC(1, R.string.label_sync_with_menus),
|
||||
HIDDEN(2, R.string.label_always_hide)
|
||||
}
|
||||
|
||||
enum class PageInfoPosition(val id: Int, val title: String) {
|
||||
BOTTOM(0, "Bottom"),
|
||||
TOP(1, "Top")
|
||||
enum class PageInfoPosition(val id: Int, @StringRes val titleRes: Int) {
|
||||
BOTTOM(0, R.string.label_bottom),
|
||||
TOP(1, R.string.label_top)
|
||||
}
|
||||
|
||||
data class FormatSettings(
|
||||
|
|
@ -649,6 +651,7 @@ fun ReaderTextFormatPanel(
|
|||
Row {
|
||||
ReaderTextAlign.entries.forEach { align ->
|
||||
val isSelected = currentTextAlign == align
|
||||
val alignDisplayName = stringResource(align.displayNameRes)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
|
|
@ -661,12 +664,12 @@ fun ReaderTextFormatPanel(
|
|||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = align.iconResId),
|
||||
contentDescription = align.displayName,
|
||||
contentDescription = alignDisplayName,
|
||||
tint = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = align.displayName,
|
||||
text = alignDisplayName,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontSize = 11.sp,
|
||||
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
|
|
@ -923,7 +926,7 @@ fun VisualOptionsSheet(
|
|||
options = SystemUiMode.entries,
|
||||
selectedOption = systemUiMode,
|
||||
onOptionSelected = onSystemUiModeChange,
|
||||
getLabel = { it.title }
|
||||
getLabel = { stringResource(it.titleRes) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
|
@ -936,7 +939,7 @@ fun VisualOptionsSheet(
|
|||
options = PageInfoMode.entries,
|
||||
selectedOption = pageInfoMode,
|
||||
onOptionSelected = onPageInfoModeChange,
|
||||
getLabel = { it.title }
|
||||
getLabel = { stringResource(it.titleRes) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
|
@ -946,7 +949,7 @@ fun VisualOptionsSheet(
|
|||
options = PageInfoPosition.entries,
|
||||
selectedOption = pageInfoPosition,
|
||||
onOptionSelected = onPageInfoPositionChange,
|
||||
getLabel = { it.title }
|
||||
getLabel = { stringResource(it.titleRes) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
|
@ -1003,7 +1006,7 @@ fun <T> OptionSegmentedControl(
|
|||
options: List<T>,
|
||||
selectedOption: T,
|
||||
onOptionSelected: (T) -> Unit,
|
||||
getLabel: (T) -> String
|
||||
getLabel: @Composable (T) -> String
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import android.os.Handler
|
|||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import android.webkit.JavascriptInterface
|
||||
import org.json.JSONObject
|
||||
|
||||
enum class DragOperation { NONE, PULLING_DOWN_FROM_TOP, PULLING_UP_FROM_BOTTOM }
|
||||
|
|
@ -52,6 +53,9 @@ class InteractiveWebView(
|
|||
|
||||
companion object {
|
||||
private const val DRAG_SENSITIVITY_PX = 20f
|
||||
private const val SELECTION_MENU_INITIAL_DELAY_MS = 120L
|
||||
private const val SELECTION_MENU_RETRY_DELAY_MS = 140L
|
||||
private const val SELECTION_MENU_RETRY_COUNT = 8
|
||||
}
|
||||
|
||||
private var startY: Float = 0f
|
||||
|
|
@ -60,16 +64,26 @@ class InteractiveWebView(
|
|||
|
||||
private val scrollStopHandler = Handler(Looper.getMainLooper())
|
||||
private var scrollStopRunnable: Runnable? = null
|
||||
private var selectionMenuRunnable: Runnable? = null
|
||||
private var activeSelectionActionMode: ActionMode? = null
|
||||
private var selectionMenuShownForActiveMode = false
|
||||
|
||||
init {
|
||||
addJavascriptInterface(ReaderSelectionBridge(), "ReaderSelectionBridge")
|
||||
}
|
||||
|
||||
private fun clearPendingSelectionWork() {
|
||||
scrollStopRunnable?.let { scrollStopHandler.removeCallbacks(it) }
|
||||
scrollStopRunnable = null
|
||||
selectionMenuRunnable?.let { scrollStopHandler.removeCallbacks(it) }
|
||||
selectionMenuRunnable = null
|
||||
}
|
||||
|
||||
private fun startLocalSelectionActionMode(): ActionMode {
|
||||
private fun startLocalSelectionActionMode(scheduleMenu: Boolean = true): ActionMode {
|
||||
activeSelectionActionMode?.let { existingMode ->
|
||||
showCustomSelectionMenuFromCurrentSelection(existingMode)
|
||||
if (scheduleMenu) {
|
||||
scheduleCustomSelectionMenuFromCurrentSelection(existingMode)
|
||||
}
|
||||
return existingMode
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +92,14 @@ class InteractiveWebView(
|
|||
if (activeSelectionActionMode === localMode) {
|
||||
activeSelectionActionMode = null
|
||||
}
|
||||
selectionMenuShownForActiveMode = false
|
||||
onHideCustomSelectionMenu()
|
||||
}
|
||||
selectionMenuShownForActiveMode = false
|
||||
activeSelectionActionMode = localMode
|
||||
showCustomSelectionMenuFromCurrentSelection(localMode)
|
||||
if (scheduleMenu) {
|
||||
scheduleCustomSelectionMenuFromCurrentSelection(localMode)
|
||||
}
|
||||
return localMode
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +108,56 @@ class InteractiveWebView(
|
|||
activeSelectionActionMode = null
|
||||
}
|
||||
|
||||
private fun showCustomSelectionMenuFromCurrentSelection(mode: ActionMode) {
|
||||
private fun scheduleCustomSelectionMenuFromCurrentSelection(
|
||||
mode: ActionMode,
|
||||
delayMs: Long = SELECTION_MENU_INITIAL_DELAY_MS,
|
||||
remainingRetries: Int = SELECTION_MENU_RETRY_COUNT
|
||||
) {
|
||||
selectionMenuRunnable?.let { scrollStopHandler.removeCallbacks(it) }
|
||||
selectionMenuRunnable = Runnable {
|
||||
selectionMenuRunnable = null
|
||||
showCustomSelectionMenuFromCurrentSelection(mode, remainingRetries)
|
||||
}
|
||||
scrollStopHandler.postDelayed(selectionMenuRunnable!!, delayMs)
|
||||
}
|
||||
|
||||
private fun scheduleSelectionActionModeRefreshAfterTouch(
|
||||
delayMs: Long = SELECTION_MENU_INITIAL_DELAY_MS,
|
||||
remainingRetries: Int = SELECTION_MENU_RETRY_COUNT
|
||||
) {
|
||||
selectionMenuRunnable?.let { scrollStopHandler.removeCallbacks(it) }
|
||||
selectionMenuRunnable = Runnable {
|
||||
selectionMenuRunnable = null
|
||||
activeSelectionActionMode?.let { mode ->
|
||||
showCustomSelectionMenuFromCurrentSelection(mode, SELECTION_MENU_RETRY_COUNT)
|
||||
return@Runnable
|
||||
}
|
||||
evaluateJavascript("(function() { var s = window.getSelection && window.getSelection(); return s ? s.toString().trim() : ''; })();") { result ->
|
||||
val selectedText = result?.removeSurrounding("\"")
|
||||
if (!selectedText.isNullOrBlank() && activeSelectionActionMode == null) {
|
||||
Timber.d("CustomSelection: selection exists after touch-up. Starting local action mode.")
|
||||
startLocalSelectionActionMode()
|
||||
} else {
|
||||
activeSelectionActionMode?.let { mode ->
|
||||
showCustomSelectionMenuFromCurrentSelection(mode, SELECTION_MENU_RETRY_COUNT)
|
||||
return@evaluateJavascript
|
||||
}
|
||||
if (remainingRetries > 0) {
|
||||
scheduleSelectionActionModeRefreshAfterTouch(
|
||||
delayMs = SELECTION_MENU_RETRY_DELAY_MS,
|
||||
remainingRetries = remainingRetries - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scrollStopHandler.postDelayed(selectionMenuRunnable!!, delayMs)
|
||||
}
|
||||
|
||||
private fun showCustomSelectionMenuFromCurrentSelection(
|
||||
mode: ActionMode,
|
||||
remainingRetries: Int
|
||||
) {
|
||||
val jsToGetSelectionDetails = """
|
||||
(function() {
|
||||
var selection = window.getSelection();
|
||||
|
|
@ -99,20 +166,44 @@ class InteractiveWebView(
|
|||
return null;
|
||||
}
|
||||
var range = selection.getRangeAt(0);
|
||||
var rect = range.getBoundingClientRect();
|
||||
|
||||
// If getBoundingClientRect returns all zeros, try getClientRects()
|
||||
if (rect.width === 0 && rect.height === 0 && rect.top === 0 && rect.left === 0) {
|
||||
var clientRects = range.getClientRects();
|
||||
if (clientRects.length > 0) {
|
||||
rect = clientRects[0]; // Use the first rect
|
||||
} else {
|
||||
return null; // No valid rect found
|
||||
var viewportLeft = 0;
|
||||
var viewportTop = 0;
|
||||
var viewportRight = window.innerWidth || document.documentElement.clientWidth || 0;
|
||||
var viewportBottom = window.innerHeight || document.documentElement.clientHeight || 0;
|
||||
var rects = Array.prototype.slice.call(range.getClientRects ? range.getClientRects() : []);
|
||||
rects = rects.filter(function(rect) {
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
|
||||
return rect.right >= viewportLeft &&
|
||||
rect.left <= viewportRight &&
|
||||
rect.bottom >= viewportTop &&
|
||||
rect.top <= viewportBottom;
|
||||
});
|
||||
var rect = null;
|
||||
if (rects.length > 0) {
|
||||
var firstRect = rects[0];
|
||||
rect = rects.reduce(function(acc, item) {
|
||||
return {
|
||||
left: Math.min(acc.left, item.left),
|
||||
top: Math.min(acc.top, item.top),
|
||||
right: Math.max(acc.right, item.right),
|
||||
bottom: Math.max(acc.bottom, item.bottom)
|
||||
};
|
||||
}, {
|
||||
left: firstRect.left,
|
||||
top: firstRect.top,
|
||||
right: firstRect.right,
|
||||
bottom: firstRect.bottom
|
||||
});
|
||||
rect.width = rect.right - rect.left;
|
||||
rect.height = rect.bottom - rect.top;
|
||||
} else {
|
||||
rect = range.getBoundingClientRect ? range.getBoundingClientRect() : null;
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the rect has some dimension
|
||||
if (rect.width === 0 && rect.height === 0) {
|
||||
if (rect.width <= 0 || rect.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -129,13 +220,28 @@ class InteractiveWebView(
|
|||
""".trimIndent()
|
||||
|
||||
evaluateJavascript(jsToGetSelectionDetails) { jsonResult ->
|
||||
fun retryOrFinish(message: String) {
|
||||
Timber.d(message)
|
||||
if (selectionMenuShownForActiveMode) {
|
||||
return
|
||||
}
|
||||
if (activeSelectionActionMode === mode && remainingRetries > 0) {
|
||||
scheduleCustomSelectionMenuFromCurrentSelection(
|
||||
mode = mode,
|
||||
delayMs = SELECTION_MENU_RETRY_DELAY_MS,
|
||||
remainingRetries = remainingRetries - 1
|
||||
)
|
||||
} else {
|
||||
mode.finish()
|
||||
}
|
||||
}
|
||||
|
||||
if (activeSelectionActionMode !== mode) {
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
if (jsonResult == null || jsonResult == "null" || jsonResult.equals("\"null\"", ignoreCase = true)) {
|
||||
Timber.d("CustomSelection: JS returned null or invalid for selection details.")
|
||||
mode.finish()
|
||||
retryOrFinish("CustomSelection: JS returned null or invalid for selection details. Retries left: $remainingRetries")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +254,7 @@ class InteractiveWebView(
|
|||
val selectedText = selectionDetails.getString("text")
|
||||
|
||||
if (selectedText.isBlank()) {
|
||||
Timber.d("CustomSelection: Selected text is blank after JS processing.")
|
||||
mode.finish()
|
||||
retryOrFinish("CustomSelection: Selected text is blank after JS processing. Retries left: $remainingRetries")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
|
|
@ -161,8 +266,7 @@ class InteractiveWebView(
|
|||
val jsHeight = selectionDetails.getDouble("height")
|
||||
|
||||
if (jsWidth == 0.0 && jsHeight == 0.0) {
|
||||
Timber.d("CustomSelection: JS returned a zero-area rect (width=0, height=0). Left: $jsLeft, Top: $jsTop")
|
||||
mode.finish()
|
||||
retryOrFinish("CustomSelection: JS returned a zero-area rect. Retries left: $remainingRetries. Left: $jsLeft, Top: $jsTop")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
|
|
@ -181,20 +285,85 @@ class InteractiveWebView(
|
|||
)
|
||||
|
||||
if (selectionRectScreen.isEmpty || selectionRectScreen.width() <= 0 || selectionRectScreen.height() <= 0) {
|
||||
Timber.d("CustomSelection: Calculated selectionRectScreen is empty or invalid: $selectionRectScreen. JS LTRB: $jsLeft, $jsTop, $jsRight, $jsBottom. WebViewLoc: $webViewX, $webViewY")
|
||||
mode.finish()
|
||||
retryOrFinish("CustomSelection: Calculated selectionRectScreen is empty or invalid: $selectionRectScreen. Retries left: $remainingRetries. JS LTRB: $jsLeft, $jsTop, $jsRight, $jsBottom. WebViewLoc: $webViewX, $webViewY")
|
||||
return@evaluateJavascript
|
||||
}
|
||||
|
||||
Timber.d("CustomSelection: Selected text: '$selectedText', JS Rect: {L:$jsLeft, T:$jsTop, R:$jsRight, B:$jsBottom}, Screen Rect: $selectionRectScreen")
|
||||
|
||||
selectionMenuShownForActiveMode = true
|
||||
onShowCustomSelectionMenu(selectedText, selectionRectScreen) {
|
||||
mode.finish()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "CustomSelection: Error parsing selection details from JS: '$jsonResult', raw: '$jsonResult'")
|
||||
if (selectionMenuShownForActiveMode) {
|
||||
return@evaluateJavascript
|
||||
}
|
||||
if (remainingRetries > 0) {
|
||||
scheduleCustomSelectionMenuFromCurrentSelection(
|
||||
mode = mode,
|
||||
delayMs = SELECTION_MENU_RETRY_DELAY_MS,
|
||||
remainingRetries = remainingRetries - 1
|
||||
)
|
||||
} else {
|
||||
mode.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomSelectionMenuFromSelectionDetailsJson(
|
||||
mode: ActionMode,
|
||||
rawJson: String
|
||||
) {
|
||||
if (activeSelectionActionMode !== mode) return
|
||||
selectionMenuRunnable?.let { scrollStopHandler.removeCallbacks(it) }
|
||||
selectionMenuRunnable = null
|
||||
|
||||
try {
|
||||
val selectionDetails = JSONObject(rawJson)
|
||||
val selectedText = selectionDetails.getString("text")
|
||||
if (selectedText.isBlank()) {
|
||||
return
|
||||
}
|
||||
|
||||
val jsLeft = selectionDetails.getDouble("left")
|
||||
val jsTop = selectionDetails.getDouble("top")
|
||||
val jsRight = selectionDetails.getDouble("right")
|
||||
val jsBottom = selectionDetails.getDouble("bottom")
|
||||
val jsWidth = selectionDetails.getDouble("width")
|
||||
val jsHeight = selectionDetails.getDouble("height")
|
||||
|
||||
if (jsWidth <= 0.0 || jsHeight <= 0.0) {
|
||||
return
|
||||
}
|
||||
|
||||
val density = context.resources.displayMetrics.density
|
||||
val webViewLocation = IntArray(2)
|
||||
getLocationOnScreen(webViewLocation)
|
||||
val webViewX = webViewLocation[0]
|
||||
val webViewY = webViewLocation[1]
|
||||
|
||||
val selectionRectScreen = Rect(
|
||||
(webViewX + jsLeft * density).toInt(),
|
||||
(webViewY + jsTop * density).toInt(),
|
||||
(webViewX + jsRight * density).toInt(),
|
||||
(webViewY + jsBottom * density).toInt()
|
||||
)
|
||||
|
||||
if (selectionRectScreen.isEmpty || selectionRectScreen.width() <= 0 || selectionRectScreen.height() <= 0) {
|
||||
Timber.d("CustomSelection: Bridge supplied invalid rect: $selectionRectScreen. JS LTRB: $jsLeft, $jsTop, $jsRight, $jsBottom")
|
||||
return
|
||||
}
|
||||
|
||||
Timber.d("CustomSelection: Bridge selected text '$selectedText', Screen Rect: $selectionRectScreen")
|
||||
selectionMenuShownForActiveMode = true
|
||||
onShowCustomSelectionMenu(selectedText, selectionRectScreen) {
|
||||
mode.finish()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "CustomSelection: Error parsing bridge selection details: '$rawJson'")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -292,6 +461,8 @@ class InteractiveWebView(
|
|||
if (wasDragging) {
|
||||
Timber.d("Drag operation ended, enabling text selection.")
|
||||
evaluateJavascript("javascript:if(window.setTextSelectionEnabled) window.setTextSelectionEnabled(true);", null)
|
||||
} else if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||
scheduleSelectionActionModeRefreshAfterTouch()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -306,12 +477,14 @@ class InteractiveWebView(
|
|||
|
||||
// MIUI can crash inside FloatingToolbar when WindowInsets are null, so WebView
|
||||
// selections use the app's Compose popup without starting the platform toolbar.
|
||||
override fun startActionMode(originalCallback: ActionMode.Callback): ActionMode? {
|
||||
Timber.d("CustomSelection: handling primary action mode locally.")
|
||||
return startLocalSelectionActionMode()
|
||||
}
|
||||
|
||||
override fun startActionMode(originalCallback: ActionMode.Callback, type: Int): ActionMode? {
|
||||
if (type == ActionMode.TYPE_FLOATING) {
|
||||
Timber.d("CustomSelection: handling floating action mode locally.")
|
||||
return startLocalSelectionActionMode()
|
||||
}
|
||||
return super.startActionMode(originalCallback, type)
|
||||
Timber.d("CustomSelection: handling action mode locally. Type: $type")
|
||||
return startLocalSelectionActionMode()
|
||||
}
|
||||
|
||||
override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
|
||||
|
|
@ -392,4 +565,14 @@ class InteractiveWebView(
|
|||
|
||||
override fun getMenuInflater(): MenuInflater = menuInflater
|
||||
}
|
||||
|
||||
private inner class ReaderSelectionBridge {
|
||||
@JavascriptInterface
|
||||
fun onSelectionChanged(selectionJson: String) {
|
||||
post {
|
||||
val mode = startLocalSelectionActionMode(scheduleMenu = false)
|
||||
showCustomSelectionMenuFromSelectionDetailsJson(mode, selectionJson)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue