Text lookup upgrade (#66)
* Added `TooltipIconButton` and integrated tooltips for tool icons in app bars * Added external translate and search support for EPUB and PDF readers * docs: refine project overview and update build instructions in readme
This commit is contained in:
parent
74e2cec415
commit
f2c5ae25d7
16 changed files with 1172 additions and 506 deletions
|
|
@ -17,12 +17,19 @@
|
|||
*
|
||||
* mail: epistemereader@gmail.com
|
||||
*/
|
||||
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
|
||||
package com.aryan.reader
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.RichTooltip
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -105,6 +112,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
|
@ -242,6 +250,90 @@ fun rememberSearchState(
|
|||
}
|
||||
}
|
||||
|
||||
private val activeTooltipState = mutableStateOf<androidx.compose.material3.TooltipState?>(null)
|
||||
|
||||
@Composable
|
||||
fun TooltipIconButton(
|
||||
text: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
description: String? = null,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val tooltipState = rememberTooltipState(isPersistent = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(tooltipState.isVisible) {
|
||||
if (tooltipState.isVisible) {
|
||||
val previous = activeTooltipState.value
|
||||
if (previous != null && previous !== tooltipState) {
|
||||
previous.dismiss()
|
||||
}
|
||||
activeTooltipState.value = tooltipState
|
||||
} else {
|
||||
if (activeTooltipState.value === tooltipState) {
|
||||
activeTooltipState.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TooltipBox(
|
||||
positionProvider = if (description != null)
|
||||
TooltipDefaults.rememberRichTooltipPositionProvider()
|
||||
else
|
||||
TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
if (description != null) {
|
||||
RichTooltip(
|
||||
title = {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
content()
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = TooltipDefaults.richTooltipColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
titleContentColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
)
|
||||
}
|
||||
} else {
|
||||
PlainTooltip {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
content()
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
state = tooltipState
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchTopBar(
|
||||
searchState: SearchState,
|
||||
|
|
@ -265,7 +357,11 @@ fun SearchTopBar(
|
|||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
IconButton(onClick = onCloseSearch) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_close_search),
|
||||
description = stringResource(R.string.tooltip_close_search_desc),
|
||||
onClick = onCloseSearch
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Close Search"
|
||||
|
|
@ -297,7 +393,11 @@ fun SearchTopBar(
|
|||
)
|
||||
|
||||
if (searchState.searchQuery.isNotEmpty()) {
|
||||
IconButton(onClick = { searchState.onQueryChange("") }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_clear_search),
|
||||
description = stringResource(R.string.tooltip_clear_search_desc),
|
||||
onClick = { searchState.onQueryChange("") }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = "Clear Search"
|
||||
|
|
@ -305,10 +405,20 @@ fun SearchTopBar(
|
|||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
searchState.showSearchResultsPanel = !searchState.showSearchResultsPanel
|
||||
focusManager.clearFocus()
|
||||
}) {
|
||||
TooltipIconButton(
|
||||
text = if (searchState.showSearchResultsPanel)
|
||||
stringResource(R.string.tooltip_hide_results)
|
||||
else
|
||||
stringResource(R.string.tooltip_show_results),
|
||||
description = if (searchState.showSearchResultsPanel)
|
||||
stringResource(R.string.tooltip_hide_results_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_show_results_desc),
|
||||
onClick = {
|
||||
searchState.showSearchResultsPanel = !searchState.showSearchResultsPanel
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
|
||||
contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results"
|
||||
|
|
@ -333,7 +443,9 @@ fun SearchNavigationControls(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
) {
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_prev_result),
|
||||
description = stringResource(R.string.tooltip_prev_result_desc),
|
||||
onClick = { onNavigate(searchState.currentSearchResultIndex - 1) },
|
||||
enabled = searchState.currentSearchResultIndex > 0
|
||||
) {
|
||||
|
|
@ -346,7 +458,9 @@ fun SearchNavigationControls(
|
|||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_next_result),
|
||||
description = stringResource(R.string.tooltip_next_result_desc),
|
||||
onClick = { onNavigate(searchState.currentSearchResultIndex + 1) },
|
||||
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1
|
||||
) {
|
||||
|
|
@ -1020,7 +1134,6 @@ suspend fun fetchRecap(
|
|||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TtsSettingsSheet(
|
||||
isVisible: Boolean,
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ fun LibraryScreenContent(
|
|||
val isBookContextualModeActive = selectedItems.isNotEmpty()
|
||||
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
|
||||
var showSortMenu by remember { mutableStateOf(false) }
|
||||
val tabTitles = listOf("All Books", "Shelves", "Folder")
|
||||
val tabTitles = listOf("All Books", "Shelves", "Folders")
|
||||
val searchFocusRequester = remember { FocusRequester() }
|
||||
|
||||
LaunchedEffect(isSearchActive) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import android.webkit.WebViewClient
|
|||
import android.widget.Toast
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -57,6 +56,7 @@ import androidx.compose.material.icons.filled.Delete
|
|||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -85,7 +85,6 @@ import androidx.compose.ui.window.Popup
|
|||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.aryan.reader.R
|
||||
import com.aryan.reader.countWords
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
|
@ -333,6 +332,8 @@ fun ChapterWebView(
|
|||
isOss: Boolean = false,
|
||||
onShowDictionaryUpsellDialog: () -> Unit,
|
||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onContentReadyForSummarization: suspend (String) -> Unit,
|
||||
currentFontFamily: ReaderFont,
|
||||
customFontPath: String? = null,
|
||||
|
|
@ -887,121 +888,82 @@ fun ChapterWebView(
|
|||
)
|
||||
}
|
||||
|
||||
// 2. Delete Option (Only for existing highlights)
|
||||
if (state.isExistingHighlight && state.cfi != null) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
// LOGGING START
|
||||
Timber.d("Kotlin: Popup Delete requested for clicked CFI: '${state.cfi}'")
|
||||
|
||||
// 1. IMPROVED LOOKUP: Check if the clicked CFI exists within any split CFI string
|
||||
val highlightToDelete = userHighlights.find { h ->
|
||||
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||
}
|
||||
|
||||
if (highlightToDelete == null) {
|
||||
Timber.e("Kotlin: ERROR - Lookup failed. CFI '${state.cfi}' not found in any highlight.")
|
||||
} else {
|
||||
Timber.d("Kotlin: SUCCESS - Found highlight object. Full CFI: '${highlightToDelete.cfi}', Color: ${highlightToDelete.color.id}")
|
||||
|
||||
val cssClassToDelete = highlightToDelete.color.cssClass
|
||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
||||
|
||||
allCfiParts.forEach { partCfi ->
|
||||
Timber.d("Kotlin: Requesting JS removal for part: '$partCfi'")
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClassToDelete');",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
onHighlightDeleted(highlightToDelete.cfi)
|
||||
}
|
||||
|
||||
state.finishActionModeCallback()
|
||||
customMenuState = null
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Remove",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Remove",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// 2. Copy Option
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Copied Text", state.selectedText)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
state.finishActionModeCallback()
|
||||
localWebViewRef?.clearFocus()
|
||||
localWebViewRef?.evaluateJavascript("javascript:if(window.getSelection) window.getSelection().removeAllRanges();", null)
|
||||
customMenuState = null
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CopyAll,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Copy",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Copied Text", state.selectedText)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
state.finishActionModeCallback()
|
||||
localWebViewRef?.clearFocus()
|
||||
localWebViewRef?.evaluateJavascript("javascript:if(window.getSelection) window.getSelection().removeAllRanges();", null)
|
||||
customMenuState = null
|
||||
}) {
|
||||
Icon(Icons.Default.CopyAll, contentDescription = "Copy")
|
||||
}
|
||||
|
||||
// 3. Dictionary Option (Preserving Logic)
|
||||
if (state.selectedText.length <= 2000) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
val textToDefine = state.selectedText
|
||||
if (textToDefine.isNotBlank()) {
|
||||
onWordSelectedForAiDefinition(textToDefine)
|
||||
}
|
||||
customMenuState = null
|
||||
if (state.selectedText.length <= 2000) {
|
||||
IconButton(onClick = {
|
||||
val textToDefine = state.selectedText
|
||||
if (textToDefine.isNotBlank()) {
|
||||
onWordSelectedForAiDefinition(textToDefine)
|
||||
}
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Dictionary",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
customMenuState = null
|
||||
}) {
|
||||
Icon(painterResource(id = R.drawable.dictionary), contentDescription = "Dictionary")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val textToDefine = state.selectedText
|
||||
if (textToDefine.isNotBlank()) {
|
||||
onTranslate(textToDefine)
|
||||
}
|
||||
customMenuState = null
|
||||
}) {
|
||||
Icon(painterResource(id = R.drawable.translate), contentDescription = "Translate")
|
||||
}
|
||||
IconButton(onClick = {
|
||||
val textToDefine = state.selectedText
|
||||
if (textToDefine.isNotBlank()) {
|
||||
onSearch(textToDefine)
|
||||
}
|
||||
customMenuState = null
|
||||
}) {
|
||||
Icon(painterResource(id = R.drawable.search), contentDescription = "Search")
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isExistingHighlight && state.cfi != null) {
|
||||
IconButton(onClick = {
|
||||
Timber.d("Kotlin: Popup Delete requested for clicked CFI: '${state.cfi}'")
|
||||
val highlightToDelete = userHighlights.find { h ->
|
||||
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||
}
|
||||
|
||||
if (highlightToDelete != null) {
|
||||
val cssClassToDelete = highlightToDelete.color.cssClass
|
||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
||||
|
||||
allCfiParts.forEach { partCfi ->
|
||||
localWebViewRef?.evaluateJavascript(
|
||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClassToDelete');",
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
onHighlightDeleted(highlightToDelete.cfi)
|
||||
}
|
||||
|
||||
state.finishActionModeCallback()
|
||||
customMenuState = null
|
||||
}) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,29 @@
|
|||
// DictionarySettingsDialog.kt
|
||||
package com.aryan.reader.epubreader
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.MenuAnchorType
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -35,19 +34,16 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import com.aryan.reader.BuildConfig
|
||||
import com.aryan.reader.R
|
||||
|
||||
@Suppress("KotlinConstantConditions")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun DictionarySettingsDialog(
|
||||
isVisible: Boolean,
|
||||
|
|
@ -55,244 +51,292 @@ fun DictionarySettingsDialog(
|
|||
isProUser: Boolean,
|
||||
useOnlineDictionary: Boolean,
|
||||
onToggleOnlineDictionary: (Boolean) -> Unit,
|
||||
selectedPackageName: String?,
|
||||
onSelectPackage: (String) -> Unit
|
||||
selectedDictionaryPackageName: String?,
|
||||
onSelectDictionaryPackage: (String) -> Unit,
|
||||
selectedTranslatePackageName: String?,
|
||||
onSelectTranslatePackage: (String) -> Unit,
|
||||
selectedSearchPackageName: String?,
|
||||
onSelectSearchPackage: (String) -> Unit
|
||||
) {
|
||||
if (!isVisible) return
|
||||
|
||||
val context = LocalContext.current
|
||||
var availableApps by remember { mutableStateOf<List<ExternalDictionaryApp>>(emptyList()) }
|
||||
var dictionaryApps by remember { mutableStateOf<List<ExternalDictionaryApp>>(emptyList()) }
|
||||
var searchApps by remember { mutableStateOf<List<ExternalDictionaryApp>>(emptyList()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
availableApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
|
||||
dictionaryApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
|
||||
searchApps = ExternalDictionaryHelper.getAvailableSearchApps(context)
|
||||
}
|
||||
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
tonalElevation = 6.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp, vertical = 24.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(24.dp)
|
||||
) {
|
||||
// Header
|
||||
Text(
|
||||
text = "Dictionary Settings",
|
||||
text = "Lookup Settings",
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
modifier = Modifier.padding(bottom = 20.dp)
|
||||
)
|
||||
|
||||
// ── Dictionary ──
|
||||
if (BuildConfig.FLAVOR != "oss") {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (useOnlineDictionary) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
if (useOnlineDictionary) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleOnlineDictionary(true) }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ai),
|
||||
contentDescription = null,
|
||||
tint = if (useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "AI Smart Dictionary",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Definitions powered by AI.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
if (useOnlineDictionary) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
text = "Dictionary Engine",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
// External App Option
|
||||
Surface(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
color = if (!useOnlineDictionary) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
if (!useOnlineDictionary) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onToggleOnlineDictionary(false) }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = null,
|
||||
tint = if (!useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "External App",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = if (!useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = "Launch an offline dictionary or search app.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (!useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
SegmentedButton(
|
||||
selected = useOnlineDictionary,
|
||||
onClick = { onToggleOnlineDictionary(true) },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
|
||||
) {
|
||||
Text("Smart (AI)")
|
||||
}
|
||||
if (!useOnlineDictionary) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
SegmentedButton(
|
||||
selected = !useOnlineDictionary,
|
||||
onClick = { onToggleOnlineDictionary(false) },
|
||||
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
|
||||
) {
|
||||
Text("External App")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = if (useOnlineDictionary)
|
||||
"Uses AI for definitions. Will fallback to the external app below if offline or if the selected phrase is too long."
|
||||
else
|
||||
"Uses the selected app for dictionary lookups.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = if (useOnlineDictionary) "Fallback App" else "Dictionary App",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
AppSelectionDropdown(
|
||||
apps = dictionaryApps,
|
||||
selectedPackageName = selectedDictionaryPackageName,
|
||||
onSelect = onSelectDictionaryPackage,
|
||||
placeholder = "Select an app"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
} else {
|
||||
Text(
|
||||
text = if (useOnlineDictionary) "Fallback External App (Used when offline)" else "Select External App",
|
||||
text = "Dictionary",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
} else {
|
||||
// OSS FLAVOR UI (Dedicated to external apps)
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Choose an external app to define selected words.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
AppSelectionDropdown(
|
||||
apps = dictionaryApps,
|
||||
selectedPackageName = selectedDictionaryPackageName,
|
||||
onSelect = onSelectDictionaryPackage,
|
||||
placeholder = "Select an app"
|
||||
)
|
||||
}
|
||||
|
||||
// App List
|
||||
if (availableApps.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
"No supported dictionary apps found.",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
textAlign = TextAlign.Center
|
||||
SectionDivider()
|
||||
|
||||
// ── Translate ──
|
||||
Text(
|
||||
text = "Translate",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "App used for translating selected text.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
AppSelectionDropdown(
|
||||
apps = dictionaryApps,
|
||||
selectedPackageName = selectedTranslatePackageName,
|
||||
onSelect = onSelectTranslatePackage,
|
||||
placeholder = "Select an app"
|
||||
)
|
||||
|
||||
SectionDivider()
|
||||
|
||||
// ── Search ──
|
||||
Text(
|
||||
text = "Search",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
)
|
||||
Text(
|
||||
text = "App used for web searches.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp)
|
||||
)
|
||||
|
||||
AppSelectionDropdown(
|
||||
apps = searchApps,
|
||||
selectedPackageName = selectedSearchPackageName,
|
||||
onSelect = onSelectSearchPackage,
|
||||
placeholder = "Select an app"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionDivider() {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppSelectionDropdown(
|
||||
apps: List<ExternalDictionaryApp>,
|
||||
selectedPackageName: String?,
|
||||
onSelect: (String) -> Unit,
|
||||
placeholder: String,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
val selectedApp = apps.find { it.packageName == selectedPackageName }
|
||||
val hasSelection = !selectedPackageName.isNullOrEmpty() && selectedApp != null
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = modifier
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = if (hasSelection) selectedApp.label else "",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
placeholder = { Text(placeholder) },
|
||||
leadingIcon = if (hasSelection && selectedApp.icon != null) {
|
||||
{
|
||||
Image(
|
||||
bitmap = selectedApp.icon.toBitmap().asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
// None option
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
"None",
|
||||
color = if (!hasSelection) MaterialTheme.colorScheme.primary
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
trailingIcon = if (!hasSelection) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 300.dp) // Bound height so it doesn't take over screen
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest, RoundedCornerShape(12.dp))
|
||||
) {
|
||||
items(availableApps) { app ->
|
||||
val isSelected = app.packageName == selectedPackageName
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelectPackage(app.packageName) }
|
||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Dynamic Icon handling
|
||||
if (app.packageName == ExternalDictionaryHelper.GOOGLE_SEARCH_PKG) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(MaterialTheme.colorScheme.secondaryContainer, RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
} else if (app.icon != null) {
|
||||
Image(
|
||||
bitmap = app.icon.toBitmap().asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = app.label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
onClick = {
|
||||
onSelect("")
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
|
||||
if (apps.isNotEmpty()) {
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
apps.forEach { app ->
|
||||
val isSelected = app.packageName == selectedPackageName
|
||||
DropdownMenuItem(
|
||||
text = { Text(app.label) },
|
||||
leadingIcon = {
|
||||
if (app.icon != null) {
|
||||
Image(
|
||||
bitmap = app.icon.toBitmap().asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.background(
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
RoundedCornerShape(4.dp)
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
content = {}
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingIcon = if (isSelected) {
|
||||
{
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "Selected",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
onClick = {
|
||||
onSelect(app.packageName)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ import androidx.compose.ui.graphics.asImageBitmap
|
|||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -120,6 +121,7 @@ import com.aryan.reader.R
|
|||
import com.aryan.reader.RenderMode
|
||||
import com.aryan.reader.SearchState
|
||||
import com.aryan.reader.SearchTopBar
|
||||
import com.aryan.reader.TooltipIconButton
|
||||
import com.aryan.reader.epub.EpubChapter
|
||||
import com.aryan.reader.paginatedreader.BookPaginator
|
||||
import com.aryan.reader.paginatedreader.IPaginator
|
||||
|
|
@ -182,7 +184,11 @@ fun EpubReaderTopBar(
|
|||
onCloseSearch = onCloseSearch
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = onNavigateBack) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_back),
|
||||
description = stringResource(R.string.tooltip_back_desc),
|
||||
onClick = onNavigateBack
|
||||
) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
|
@ -193,7 +199,11 @@ fun EpubReaderTopBar(
|
|||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
IconButton(onClick = onOpenDictionarySettings) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
onClick = onOpenDictionarySettings
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = "Dictionary Settings"
|
||||
|
|
@ -201,7 +211,11 @@ fun EpubReaderTopBar(
|
|||
}
|
||||
Box {
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showMoreMenu = true }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_more_options),
|
||||
description = stringResource(R.string.tooltip_more_options_desc),
|
||||
onClick = { showMoreMenu = true }
|
||||
) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
|
||||
}
|
||||
|
||||
|
|
@ -392,19 +406,33 @@ fun EpubReaderBottomBar(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = onOpenSlider,
|
||||
enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.slider), contentDescription = "Navigate with slider")
|
||||
}
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = onOpenDrawer
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Menu, contentDescription = "Chapters Menu")
|
||||
}
|
||||
IconButton(onClick = onToggleFormat) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_format),
|
||||
description = stringResource(R.string.tooltip_format_desc),
|
||||
onClick = onToggleFormat
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.format_size), contentDescription = "Text Formatting")
|
||||
}
|
||||
IconButton(onClick = onToggleSearch) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = onToggleSearch
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Search, contentDescription = "Search")
|
||||
}
|
||||
|
||||
|
|
@ -412,7 +440,11 @@ fun EpubReaderBottomBar(
|
|||
if (BuildConfig.FLAVOR != "oss") {
|
||||
Box {
|
||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showAiFeaturesMenu = true }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = { showAiFeaturesMenu = true }
|
||||
) {
|
||||
Icon(painter = painterResource(id = R.drawable.ai), contentDescription = "AI Features")
|
||||
}
|
||||
DropdownMenu(
|
||||
|
|
@ -441,14 +473,32 @@ fun EpubReaderBottomBar(
|
|||
}
|
||||
Box {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = onToggleTts) {
|
||||
TooltipIconButton(
|
||||
text = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = onToggleTts
|
||||
) {
|
||||
Icon(
|
||||
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech),
|
||||
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
|
||||
)
|
||||
}
|
||||
if (isTtsSessionActive) {
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume),
|
||||
description = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume_desc),
|
||||
onClick = onPlayPauseTts,
|
||||
enabled = !ttsState.isLoading
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -291,6 +291,8 @@ private fun saveTtsMode(context: Context, modeName: String) {
|
|||
|
||||
private const val PREF_USE_ONLINE_DICT = "use_online_dictionary"
|
||||
private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
|
||||
private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package"
|
||||
private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
|
||||
|
||||
private fun loadUseOnlineDict(context: Context): Boolean {
|
||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
|
||||
|
|
@ -313,6 +315,26 @@ private fun saveExternalDictPackage(context: Context, packageName: String) {
|
|||
prefs.edit { putString(PREF_EXTERNAL_DICT_PKG, packageName) }
|
||||
}
|
||||
|
||||
private fun loadExternalTranslatePackage(context: Context): String? {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getString(PREF_EXTERNAL_TRANSLATE_PKG, null)
|
||||
}
|
||||
|
||||
private fun saveExternalTranslatePackage(context: Context, packageName: String) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(PREF_EXTERNAL_TRANSLATE_PKG, packageName) }
|
||||
}
|
||||
|
||||
private fun loadExternalSearchPackage(context: Context): String? {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
return prefs.getString(PREF_EXTERNAL_SEARCH_PKG, null)
|
||||
}
|
||||
|
||||
private fun saveExternalSearchPackage(context: Context, packageName: String) {
|
||||
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(PREF_EXTERNAL_SEARCH_PKG, packageName) }
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
@Composable
|
||||
fun EpubReaderScreen(
|
||||
|
|
@ -573,6 +595,12 @@ fun EpubReaderHost(
|
|||
var selectedDictPackage by remember {
|
||||
mutableStateOf(loadExternalDictPackage(context))
|
||||
}
|
||||
var selectedTranslatePackage by remember {
|
||||
mutableStateOf(loadExternalTranslatePackage(context))
|
||||
}
|
||||
var selectedSearchPackage by remember {
|
||||
mutableStateOf(loadExternalSearchPackage(context))
|
||||
}
|
||||
|
||||
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
|
||||
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
|
||||
|
|
@ -605,7 +633,7 @@ fun EpubReaderHost(
|
|||
showDictionaryUpsellDialog = true
|
||||
}
|
||||
} else {
|
||||
if (selectedDictPackage != null) {
|
||||
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -614,6 +642,24 @@ fun EpubReaderHost(
|
|||
}
|
||||
}
|
||||
|
||||
val onTranslateLookup = { text: String ->
|
||||
if (!selectedTranslatePackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a translate app first.", Toast.LENGTH_SHORT).show()
|
||||
showDictionarySettingsSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
val onSearchLookup = { text: String ->
|
||||
if (!selectedSearchPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a search app first.", Toast.LENGTH_SHORT).show()
|
||||
showDictionarySettingsSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
val summaryCacheManager = remember(context) { SummaryCacheManager(context) }
|
||||
var showRecapPopup by remember { mutableStateOf(false) }
|
||||
var recapResult by remember { mutableStateOf<SummarizationResult?>(null) }
|
||||
|
|
@ -2256,6 +2302,12 @@ fun EpubReaderHost(
|
|||
onWordSelectedForAiDefinition = { text ->
|
||||
onDictionaryLookup(text)
|
||||
},
|
||||
onTranslate = { text ->
|
||||
onTranslateLookup(text)
|
||||
},
|
||||
onSearch = { text ->
|
||||
onSearchLookup(text)
|
||||
},
|
||||
onContentReadyForSummarization = { content ->
|
||||
Timber.d("Content received for summarization")
|
||||
scope.launch {
|
||||
|
|
@ -2591,6 +2643,12 @@ fun EpubReaderHost(
|
|||
onWordSelectedForAiDefinition = { text ->
|
||||
onDictionaryLookup(text)
|
||||
},
|
||||
onTranslate = { text ->
|
||||
onTranslateLookup(text)
|
||||
},
|
||||
onSearch = { text ->
|
||||
onSearchLookup(text)
|
||||
},
|
||||
userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) },
|
||||
onHighlightCreated = { cfi, text, colorId ->
|
||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||
|
|
@ -3532,7 +3590,7 @@ fun EpubReaderHost(
|
|||
onNavigateToPro = onNavigateToPro,
|
||||
isTtsSessionActive = isTtsSessionActive,
|
||||
onOpenExternalDictionary = { text ->
|
||||
if (selectedDictPackage != null) {
|
||||
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -3707,10 +3765,20 @@ fun EpubReaderHost(
|
|||
useOnlineDictionary = newState
|
||||
saveUseOnlineDict(context, newState)
|
||||
},
|
||||
selectedPackageName = selectedDictPackage,
|
||||
onSelectPackage = { pkg ->
|
||||
selectedDictionaryPackageName = selectedDictPackage,
|
||||
onSelectDictionaryPackage = { pkg ->
|
||||
selectedDictPackage = pkg
|
||||
saveExternalDictPackage(context, pkg)
|
||||
},
|
||||
selectedTranslatePackageName = selectedTranslatePackage,
|
||||
onSelectTranslatePackage = { pkg ->
|
||||
selectedTranslatePackage = pkg
|
||||
saveExternalTranslatePackage(context, pkg)
|
||||
},
|
||||
selectedSearchPackageName = selectedSearchPackage,
|
||||
onSelectSearchPackage = { pkg ->
|
||||
selectedSearchPackage = pkg
|
||||
saveExternalSearchPackage(context, pkg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.widget.Toast
|
||||
import timber.log.Timber
|
||||
import androidx.core.net.toUri
|
||||
|
||||
data class ExternalDictionaryApp(
|
||||
val label: String,
|
||||
|
|
@ -23,7 +25,6 @@ object ExternalDictionaryHelper {
|
|||
"com.samsung.android.samsungpassautofill",
|
||||
"com.samsung.android.samsungpass",
|
||||
"com.samsung.android.app.pass",
|
||||
"com.google.android.gms",
|
||||
"com.truecaller",
|
||||
"com.adobe.reader",
|
||||
"com.reddit.frontpage"
|
||||
|
|
@ -77,18 +78,15 @@ object ExternalDictionaryHelper {
|
|||
)
|
||||
)
|
||||
|
||||
return sortedApps
|
||||
return apps.sortedBy { it.label }
|
||||
}
|
||||
|
||||
fun launchDictionary(context: Context, packageName: String, query: String) {
|
||||
if (packageName.isEmpty()) return
|
||||
val pm = context.packageManager
|
||||
try {
|
||||
if (packageName == GOOGLE_SEARCH_PKG) {
|
||||
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(searchIntent)
|
||||
launchSearch(context, packageName, query)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -133,6 +131,89 @@ object ExternalDictionaryHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun launchTranslate(context: Context, packageName: String, query: String) {
|
||||
if (packageName.isEmpty()) return
|
||||
val pm = context.packageManager
|
||||
try {
|
||||
if (packageName == GOOGLE_SEARCH_PKG) {
|
||||
launchSearch(context, packageName, query)
|
||||
return
|
||||
}
|
||||
|
||||
// Google Translate specific intent
|
||||
if (packageName == "com.google.android.apps.translate") {
|
||||
val translateIntent = Intent(Intent.ACTION_PROCESS_TEXT).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT, query)
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
setPackage(packageName)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
if (translateIntent.resolveActivity(pm) != null) {
|
||||
context.startActivity(translateIntent)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generic text processing intent
|
||||
val processTextIntent = Intent(Intent.ACTION_PROCESS_TEXT).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT, query)
|
||||
putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true)
|
||||
setPackage(packageName)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
if (processTextIntent.resolveActivity(pm) != null) {
|
||||
context.startActivity(processTextIntent)
|
||||
return
|
||||
}
|
||||
|
||||
launchGenericSend(context, packageName, query)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to launch translate app: $packageName")
|
||||
Toast.makeText(context, "Error opening translate app", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun launchSearch(context: Context, packageName: String, query: String) {
|
||||
try {
|
||||
if (packageName == GOOGLE_SEARCH_PKG) {
|
||||
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(searchIntent)
|
||||
return
|
||||
}
|
||||
|
||||
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
setPackage(packageName)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
if (searchIntent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(searchIntent)
|
||||
return
|
||||
}
|
||||
|
||||
val viewIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = "https://www.google.com/search?q=${Uri.encode(query)}".toUri()
|
||||
setPackage(packageName)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
if (viewIntent.resolveActivity(context.packageManager) != null) {
|
||||
context.startActivity(viewIntent)
|
||||
return
|
||||
}
|
||||
|
||||
launchGenericSend(context, packageName, query)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to launch search app: $packageName")
|
||||
Toast.makeText(context, "Error opening search app", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchGenericSend(context: Context, packageName: String, query: String) {
|
||||
val sendIntent = Intent(Intent.ACTION_SEND)
|
||||
sendIntent.type = "text/plain"
|
||||
|
|
@ -151,4 +232,52 @@ object ExternalDictionaryHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAvailableSearchApps(context: Context): List<ExternalDictionaryApp> {
|
||||
val pm = context.packageManager
|
||||
val apps = mutableListOf<ExternalDictionaryApp>()
|
||||
val addedPackages = mutableSetOf<String>()
|
||||
|
||||
val webSearchIntent = Intent(Intent.ACTION_WEB_SEARCH)
|
||||
val searchResolvers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.queryIntentActivities(webSearchIntent, PackageManager.ResolveInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") pm.queryIntentActivities(webSearchIntent, 0)
|
||||
}
|
||||
searchResolvers.forEach { ri ->
|
||||
val pkg = ri.activityInfo.packageName
|
||||
if (!PACKAGE_BLOCKLIST.contains(pkg) && addedPackages.add(pkg)) {
|
||||
apps.add(
|
||||
ExternalDictionaryApp(
|
||||
label = ri.loadLabel(pm).toString(),
|
||||
packageName = pkg,
|
||||
icon = ri.loadIcon(pm)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri()).apply {
|
||||
addCategory(Intent.CATEGORY_BROWSABLE)
|
||||
}
|
||||
val browserResolvers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
pm.queryIntentActivities(browserIntent, PackageManager.ResolveInfoFlags.of(0))
|
||||
} else {
|
||||
@Suppress("DEPRECATION") pm.queryIntentActivities(browserIntent, 0)
|
||||
}
|
||||
browserResolvers.forEach { ri ->
|
||||
val pkg = ri.activityInfo.packageName
|
||||
if (!PACKAGE_BLOCKLIST.contains(pkg) && addedPackages.add(pkg)) {
|
||||
apps.add(
|
||||
ExternalDictionaryApp(
|
||||
label = ri.loadLabel(pm).toString(),
|
||||
packageName = pkg,
|
||||
icon = ri.loadIcon(pm)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return apps.sortedBy { it.label }
|
||||
}
|
||||
}
|
||||
|
|
@ -69,6 +69,7 @@ import androidx.compose.material3.AlertDialog
|
|||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -505,6 +506,8 @@ fun PaginatedReaderScreen(
|
|||
isOss: Boolean = false,
|
||||
onShowDictionaryUpsellDialog: () -> Unit,
|
||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
userHighlights: List<UserHighlight>,
|
||||
onHighlightCreated: (String, String, String) -> Unit,
|
||||
onHighlightDeleted: (String) -> Unit,
|
||||
|
|
@ -802,6 +805,8 @@ fun PaginatedReaderScreen(
|
|||
isOss = isOss,
|
||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
||||
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
||||
onTranslate = onTranslate,
|
||||
onSearch = onSearch,
|
||||
userHighlights = userHighlights,
|
||||
onHighlightCreated = onHighlightCreated,
|
||||
onHighlightDeleted = onHighlightDeleted,
|
||||
|
|
@ -1372,6 +1377,8 @@ internal fun PaginatedReaderContent(
|
|||
isOss: Boolean,
|
||||
onShowDictionaryUpsellDialog: () -> Unit,
|
||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onGetChapterInfo: (Int) -> Pair<String, Int?>?,
|
||||
userHighlights: List<UserHighlight>,
|
||||
onHighlightCreated: (String, String, String) -> Unit,
|
||||
|
|
@ -2531,6 +2538,14 @@ internal fun PaginatedReaderContent(
|
|||
state.onCopy()
|
||||
isForDictionary = false
|
||||
state.onHide()
|
||||
}, onTranslate = {
|
||||
state.onCopy() // we don't necessarily need copy to get text, but follow dictionary pattern if needed, wait menuState has selectedText!
|
||||
// Actually PaginatedMenuState has `selectedText`? Let's check.
|
||||
onTranslate(capturedTextForAction ?: "")
|
||||
state.onHide()
|
||||
}, onSearch = {
|
||||
onSearch(capturedTextForAction ?: "")
|
||||
state.onHide()
|
||||
}, onHighlight = { color ->
|
||||
Timber.d("Menu: Highlight option clicked. Color: ${color.id}")
|
||||
isForHighlight = true
|
||||
|
|
@ -2768,6 +2783,12 @@ internal fun PaginatedReaderContent(
|
|||
onShowDictionaryUpsellDialog()
|
||||
}
|
||||
activeSelection = null
|
||||
}, onTranslate = {
|
||||
onTranslate(sel.text)
|
||||
activeSelection = null
|
||||
}, onSearch = {
|
||||
onSearch(sel.text)
|
||||
activeSelection = null
|
||||
}, onHighlight = { color ->
|
||||
Timber.d(
|
||||
"CustomSelection: Highlight clicked. Text: '${sel.text}', BaseCFI: ${sel.baseCfi}, StartOffset: ${sel.startOffset}"
|
||||
|
|
@ -2834,6 +2855,14 @@ internal fun PaginatedReaderContent(
|
|||
}
|
||||
activeHighlightForMenu = null
|
||||
},
|
||||
onTranslate = {
|
||||
onTranslate(highlight.text)
|
||||
activeHighlightForMenu = null
|
||||
},
|
||||
onSearch = {
|
||||
onSearch(highlight.text)
|
||||
activeHighlightForMenu = null
|
||||
},
|
||||
onHighlight = { color ->
|
||||
Timber.d("Menu: Updating highlight color to ${color.id}")
|
||||
onHighlightDeleted(highlight.cfi)
|
||||
|
|
@ -2911,6 +2940,8 @@ private fun PaginatedTextSelectionMenu(
|
|||
onCopy: () -> Unit,
|
||||
onSelectAll: (() -> Unit)?,
|
||||
onDictionary: () -> Unit,
|
||||
onTranslate: () -> Unit,
|
||||
onSearch: () -> Unit,
|
||||
onHighlight: ((HighlightColor) -> Unit)?,
|
||||
onDelete: (() -> Unit)?,
|
||||
@Suppress("unused") isProUser: Boolean,
|
||||
|
|
@ -2955,99 +2986,71 @@ private fun PaginatedTextSelectionMenu(
|
|||
HorizontalDivider()
|
||||
}
|
||||
|
||||
// 2. Delete Option
|
||||
if (onDelete != null) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onDelete)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Remove",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
"Remove",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
// 3. Copy Option
|
||||
// 2. Action Icons Row (Horizontal)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onCopy)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.copy),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
"Copy",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
// 4. Select All Option
|
||||
if (onSelectAll != null) {
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onSelectAll)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
IconButton(onClick = onCopy) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.select_all),
|
||||
contentDescription = null,
|
||||
painter = painterResource(id = R.drawable.copy),
|
||||
contentDescription = "Copy",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
"Select All",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Dictionary Option
|
||||
HorizontalDivider()
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onDictionary)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
"Dictionary",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
IconButton(onClick = onDictionary) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = "Dictionary",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onTranslate) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.translate),
|
||||
contentDescription = "Translate",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onSearch) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.search),
|
||||
contentDescription = "Search",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
if (onSelectAll != null) {
|
||||
IconButton(onClick = onSelectAll) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.select_all),
|
||||
contentDescription = "Select All",
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (onDelete != null) {
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Remove",
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
|
|
@ -45,6 +46,7 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CopyAll
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -188,6 +190,8 @@ internal fun PdfSelectionMenuPopup(
|
|||
onDismiss: () -> Unit,
|
||||
onCopy: (String) -> Unit,
|
||||
onAiDefine: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
onColorSelected: (PdfHighlightColor) -> Unit,
|
||||
onDelete: () -> Unit
|
||||
|
|
@ -281,51 +285,74 @@ internal fun PdfSelectionMenuPopup(
|
|||
}
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(48.dp) // Fixed height for the sleek row
|
||||
.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
.clickable { onCopy(menuState.selectedText) }.padding(vertical = 12.dp),
|
||||
contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
Icons.Default.CopyAll,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text("Copy", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
// Copy
|
||||
androidx.compose.material3.IconButton(
|
||||
onClick = { onCopy(menuState.selectedText) }
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.CopyAll,
|
||||
contentDescription = "Copy",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
|
||||
// Dictionary
|
||||
if (menuState.selectedText.length <= 2000) {
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
.clickable { onAiDefine(menuState.selectedText) }
|
||||
.padding(vertical = 12.dp), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text("Dictionary", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
androidx.compose.material3.IconButton(
|
||||
onClick = { onAiDefine(menuState.selectedText) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = "Dictionary",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Translate
|
||||
if (menuState.selectedText.length <= 2000) {
|
||||
androidx.compose.material3.IconButton(
|
||||
onClick = { onTranslate(menuState.selectedText) }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.translate),
|
||||
contentDescription = "Translate",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
if (menuState.selectedText.length <= 2000) {
|
||||
androidx.compose.material3.IconButton(
|
||||
onClick = { onSearch(menuState.selectedText) }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Select All
|
||||
if (!menuState.isExistingHighlight) {
|
||||
Box(modifier = Modifier.weight(1f).clickable { onSelectAll() }
|
||||
.padding(vertical = 12.dp), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.select_all),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text("Select All", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
androidx.compose.material3.IconButton(
|
||||
onClick = { onSelectAll() }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.select_all),
|
||||
contentDescription = "Select All",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -389,6 +389,8 @@ internal fun PdfPageComposable(
|
|||
isProUser: Boolean,
|
||||
onShowDictionaryUpsellDialog: () -> Unit,
|
||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslateText: (String) -> Unit,
|
||||
onSearchText: (String) -> Unit,
|
||||
ttsHighlightData: TtsHighlightData?,
|
||||
onLinkClicked: (String) -> Unit,
|
||||
onInternalLinkClicked: (Int) -> Unit,
|
||||
|
|
@ -3685,6 +3687,36 @@ internal fun PdfPageComposable(
|
|||
)
|
||||
}
|
||||
},
|
||||
onTranslate = { textToTranslate ->
|
||||
onTranslateText(textToTranslate.trim())
|
||||
customMenuState = null
|
||||
selectionCharRange.value = null
|
||||
coroutineScope.launch {
|
||||
updateSelectionVisuals(
|
||||
pdfDocumentItem,
|
||||
pdfPageIndex,
|
||||
null,
|
||||
actualBitmapWidthPx,
|
||||
actualBitmapHeightPx,
|
||||
currentPageRotation
|
||||
)
|
||||
}
|
||||
},
|
||||
onSearch = { textToSearch ->
|
||||
onSearchText(textToSearch.trim())
|
||||
customMenuState = null
|
||||
selectionCharRange.value = null
|
||||
coroutineScope.launch {
|
||||
updateSelectionVisuals(
|
||||
pdfDocumentItem,
|
||||
pdfPageIndex,
|
||||
null,
|
||||
actualBitmapWidthPx,
|
||||
actualBitmapHeightPx,
|
||||
currentPageRotation
|
||||
)
|
||||
}
|
||||
},
|
||||
onSelectAll = {
|
||||
customMenuState = null
|
||||
coroutineScope.launch {
|
||||
|
|
@ -4523,6 +4555,8 @@ private fun PdfPageRenderer(
|
|||
onMenuDismiss: () -> Unit,
|
||||
onCopy: (String) -> Unit,
|
||||
onAiDefine: (String) -> Unit,
|
||||
onTranslate: (String) -> Unit,
|
||||
onSearch: (String) -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
onShowUpsellDialog: () -> Unit,
|
||||
isProUser: Boolean,
|
||||
|
|
@ -4934,6 +4968,8 @@ private fun PdfPageRenderer(
|
|||
onDismiss = onMenuDismiss,
|
||||
onCopy = onCopy,
|
||||
onAiDefine = onAiDefine,
|
||||
onTranslate = onTranslate,
|
||||
onSearch = onSearch,
|
||||
onSelectAll = onSelectAll,
|
||||
onColorSelected = { color ->
|
||||
Timber.tag("PdfHighlightDebug").d("PdfSelectionMenuPopup onColorSelected: $color, isExisting=${menuState.isExistingHighlight}")
|
||||
|
|
|
|||
|
|
@ -193,6 +193,8 @@ internal fun PdfVerticalReader(
|
|||
isProUser: Boolean,
|
||||
onShowDictionaryUpsellDialog: () -> Unit,
|
||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||
onTranslateText: (String) -> Unit,
|
||||
onSearchText: (String) -> Unit,
|
||||
ttsHighlightData: TtsHighlightData?,
|
||||
ttsReadingPage: Int?,
|
||||
onLinkClicked: (String) -> Unit,
|
||||
|
|
@ -1332,6 +1334,14 @@ internal fun PdfVerticalReader(
|
|||
}
|
||||
}
|
||||
|
||||
val onTranslateTextLambda = remember(onTranslateText) {
|
||||
{ text: String -> onTranslateText(text) }
|
||||
}
|
||||
|
||||
val onSearchTextLambda = remember(onSearchText) {
|
||||
{ text: String -> onSearchText(text) }
|
||||
}
|
||||
|
||||
val onDoubleTapLambda = remember(page, screenWidth, screenHeight) {
|
||||
{ localOffset: Offset ->
|
||||
Timber.tag("PdfZoomDebug").d(
|
||||
|
|
@ -1456,6 +1466,8 @@ internal fun PdfVerticalReader(
|
|||
isProUser = isProUser,
|
||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
||||
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
||||
onTranslateText = onTranslateTextLambda,
|
||||
onSearchText = onSearchTextLambda,
|
||||
ttsHighlightData = pageTtsData,
|
||||
onLinkClicked = onLinkClicked,
|
||||
onInternalLinkClicked = onInternalLinkClicked,
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
|||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
|
|
@ -244,6 +245,7 @@ import com.aryan.reader.R
|
|||
import com.aryan.reader.SearchResult
|
||||
import com.aryan.reader.SearchTopBar
|
||||
import com.aryan.reader.SummarizationPopup
|
||||
import com.aryan.reader.TooltipIconButton
|
||||
import com.aryan.reader.SummarizationResult
|
||||
import com.aryan.reader.TtsSettingsSheet
|
||||
import com.aryan.reader.countWords
|
||||
|
|
@ -322,6 +324,8 @@ private const val PDF_FULL_SCREEN_PREFIX = "pdf_fs_local_"
|
|||
private const val PDF_MUSICIAN_MODE_KEY = "pdf_musician_mode_enabled"
|
||||
private const val PREF_USE_ONLINE_DICT = "use_online_dictionary"
|
||||
private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
|
||||
private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package"
|
||||
private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
|
||||
|
||||
private fun loadUseOnlineDict(context: Context): Boolean {
|
||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
|
||||
|
|
@ -344,6 +348,26 @@ private fun saveExternalDictPackage(context: Context, packageName: String) {
|
|||
prefs.edit { putString(PREF_EXTERNAL_DICT_PKG, packageName) }
|
||||
}
|
||||
|
||||
private fun loadExternalTranslatePackage(context: Context): String? {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getString(PREF_EXTERNAL_TRANSLATE_PKG, null)
|
||||
}
|
||||
|
||||
private fun saveExternalTranslatePackage(context: Context, packageName: String) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(PREF_EXTERNAL_TRANSLATE_PKG, packageName) }
|
||||
}
|
||||
|
||||
private fun loadExternalSearchPackage(context: Context): String? {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
return prefs.getString(PREF_EXTERNAL_SEARCH_PKG, null)
|
||||
}
|
||||
|
||||
private fun saveExternalSearchPackage(context: Context, packageName: String) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putString(PREF_EXTERNAL_SEARCH_PKG, packageName) }
|
||||
}
|
||||
|
||||
private fun savePdfMusicianMode(context: Context, isEnabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit { putBoolean(PDF_MUSICIAN_MODE_KEY, isEnabled) }
|
||||
|
|
@ -780,6 +804,8 @@ fun PdfViewerScreen(
|
|||
var showDictionarySettingsSheet by remember { mutableStateOf(false) }
|
||||
var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
|
||||
var selectedDictPackage by remember { mutableStateOf(loadExternalDictPackage(context)) }
|
||||
var selectedTranslatePackage by remember { mutableStateOf(loadExternalTranslatePackage(context)) }
|
||||
var selectedSearchPackage by remember { mutableStateOf(loadExternalSearchPackage(context)) }
|
||||
|
||||
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||
|
||||
|
|
@ -1127,7 +1153,7 @@ fun PdfViewerScreen(
|
|||
withContext(NonCancellable) {
|
||||
saveMutex.withLock {
|
||||
withContext(Dispatchers.IO) {
|
||||
var didSave = false
|
||||
@Suppress("VariableNeverRead") var didSave = false
|
||||
|
||||
if (force || annotsHash != lastSavedHashes[0]) {
|
||||
annotationRepository.saveAnnotations(bookId, annots)
|
||||
|
|
@ -2131,7 +2157,7 @@ fun PdfViewerScreen(
|
|||
showDictionaryUpsellDialog = true
|
||||
}
|
||||
} else {
|
||||
if (selectedDictPackage != null) {
|
||||
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -2142,6 +2168,28 @@ fun PdfViewerScreen(
|
|||
}
|
||||
}
|
||||
|
||||
val onTranslateTextStable = remember(selectedTranslatePackage) {
|
||||
{ text: String ->
|
||||
if (!selectedTranslatePackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchTranslate(context, selectedTranslatePackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a translate app first.", Toast.LENGTH_SHORT).show()
|
||||
showDictionarySettingsSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onSearchTextStable = remember(selectedSearchPackage) {
|
||||
{ text: String ->
|
||||
if (!selectedSearchPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchSearch(context, selectedSearchPackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Please select a search app first.", Toast.LENGTH_SHORT).show()
|
||||
showDictionarySettingsSheet = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val onLinkClickedStable = remember { { url: String -> clickedLinkUrl = url } }
|
||||
|
||||
val onInternalLinkNavStable = remember(displayMode) {
|
||||
|
|
@ -3874,6 +3922,8 @@ fun PdfViewerScreen(
|
|||
}
|
||||
},
|
||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||
onTranslateText = onTranslateTextStable,
|
||||
onSearchText = onSearchTextStable,
|
||||
onOcrStateChange = onOcrStateChange,
|
||||
onLinkClicked = { url -> clickedLinkUrl = url },
|
||||
onInternalLinkClicked = onInternalLinkNav,
|
||||
|
|
@ -4244,6 +4294,8 @@ fun PdfViewerScreen(
|
|||
isProUser = isProUser,
|
||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable,
|
||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||
onTranslateText = onTranslateTextStable,
|
||||
onSearchText = onSearchTextStable,
|
||||
ttsHighlightData = ttsHighlightData,
|
||||
ttsReadingPage = ttsPageData?.pageIndex,
|
||||
userHighlights = userHighlights,
|
||||
|
|
@ -4774,7 +4826,11 @@ fun PdfViewerScreen(
|
|||
focusManager.clearFocus()
|
||||
})
|
||||
} else {
|
||||
IconButton(onClick = { saveStateAndExit() }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_back),
|
||||
description = stringResource(R.string.tooltip_back_desc),
|
||||
onClick = { saveStateAndExit() }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "Back"
|
||||
|
|
@ -4804,7 +4860,17 @@ fun PdfViewerScreen(
|
|||
.testTag("PageNumberIndicator")
|
||||
)
|
||||
|
||||
IconButton(onClick = { isPdfDarkMode = !isPdfDarkMode }) {
|
||||
TooltipIconButton(
|
||||
text = if (isPdfDarkMode)
|
||||
stringResource(R.string.tooltip_dark_mode_off)
|
||||
else
|
||||
stringResource(R.string.tooltip_dark_mode_on),
|
||||
description = if (isPdfDarkMode)
|
||||
stringResource(R.string.tooltip_dark_mode_off_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_dark_mode_on_desc),
|
||||
onClick = { isPdfDarkMode = !isPdfDarkMode }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dark_mode),
|
||||
contentDescription = if (isPdfDarkMode) "Disable Dark Mode"
|
||||
|
|
@ -4814,10 +4880,20 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
isScrollLocked = !isScrollLocked
|
||||
savePdfScrollLocked(context, bookId, isScrollLocked)
|
||||
}) {
|
||||
TooltipIconButton(
|
||||
text = if (isScrollLocked)
|
||||
stringResource(R.string.tooltip_unlock_pan)
|
||||
else
|
||||
stringResource(R.string.tooltip_lock_pan),
|
||||
description = if (isScrollLocked)
|
||||
stringResource(R.string.tooltip_unlock_pan_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_lock_pan_desc),
|
||||
onClick = {
|
||||
isScrollLocked = !isScrollLocked
|
||||
savePdfScrollLocked(context, bookId, isScrollLocked)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
||||
contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning",
|
||||
|
|
@ -4825,10 +4901,14 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
isFullScreen = true
|
||||
savePdfFullScreen(context, bookId, true)
|
||||
}) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_fullscreen),
|
||||
description = stringResource(R.string.tooltip_fullscreen_desc),
|
||||
onClick = {
|
||||
isFullScreen = true
|
||||
savePdfFullScreen(context, bookId, true)
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Fullscreen,
|
||||
contentDescription = "Enter Full Screen",
|
||||
|
|
@ -4836,7 +4916,11 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = { showDictionarySettingsSheet = true }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_dictionary),
|
||||
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||
onClick = { showDictionarySettingsSheet = true }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.dictionary),
|
||||
contentDescription = "Dictionary Settings",
|
||||
|
|
@ -4845,7 +4929,7 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
IconButton(onClick = { showPenPlayground = true }) {
|
||||
TooltipIconButton(text = "Pen Playground", onClick = { showPenPlayground = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Open Pen Playground",
|
||||
|
|
@ -4853,7 +4937,7 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
TooltipIconButton(text = "Import SVG", onClick = {
|
||||
val page = if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
|
|
@ -4890,7 +4974,11 @@ fun PdfViewerScreen(
|
|||
|
||||
Box {
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showMoreMenu = true }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_more_options),
|
||||
description = stringResource(R.string.tooltip_more_options_desc),
|
||||
onClick = { showMoreMenu = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More Options"
|
||||
|
|
@ -5350,7 +5438,9 @@ fun PdfViewerScreen(
|
|||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
// Slider Navigation Trigger
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_slider),
|
||||
description = stringResource(R.string.tooltip_slider_desc),
|
||||
onClick = {
|
||||
val currentPage = if (displayMode == DisplayMode.PAGINATION) {
|
||||
pagerState.currentPage
|
||||
|
|
@ -5369,7 +5459,9 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_toc),
|
||||
description = stringResource(R.string.tooltip_toc_desc),
|
||||
onClick = { coroutineScope.launch { drawerState.open() } },
|
||||
enabled = !(ttsState.isPlaying || ttsState.isLoading),
|
||||
modifier = Modifier.testTag("TocButton")
|
||||
|
|
@ -5381,7 +5473,9 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
// Search Button
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_search),
|
||||
description = stringResource(R.string.tooltip_search_desc),
|
||||
onClick = {
|
||||
executeWithOcrCheck {
|
||||
searchState.isSearchActive = true
|
||||
|
|
@ -5397,11 +5491,19 @@ fun PdfViewerScreen(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = if (showAllTextHighlights)
|
||||
stringResource(R.string.tooltip_highlights_off)
|
||||
else
|
||||
stringResource(R.string.tooltip_highlights),
|
||||
description = if (showAllTextHighlights)
|
||||
stringResource(R.string.tooltip_highlights_off_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_highlights_desc),
|
||||
onClick = {
|
||||
val newState = !showAllTextHighlights
|
||||
if (newState) {
|
||||
if (isHighlightingLoading) return@IconButton
|
||||
if (isHighlightingLoading) return@TooltipIconButton
|
||||
showAllTextHighlights = true
|
||||
isHighlightingLoading = true
|
||||
} else {
|
||||
|
|
@ -5424,7 +5526,11 @@ fun PdfViewerScreen(
|
|||
// AI feat
|
||||
Box {
|
||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { showAiFeaturesMenu = true }) {
|
||||
TooltipIconButton(
|
||||
text = stringResource(R.string.tooltip_ai),
|
||||
description = stringResource(R.string.tooltip_ai_desc),
|
||||
onClick = { showAiFeaturesMenu = true }
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ai),
|
||||
contentDescription = "AI Features"
|
||||
|
|
@ -5458,7 +5564,15 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
// Edit Button
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = if (isEditMode)
|
||||
stringResource(R.string.tooltip_edit_mode_exit)
|
||||
else
|
||||
stringResource(R.string.tooltip_edit_mode),
|
||||
description = if (isEditMode)
|
||||
stringResource(R.string.tooltip_edit_mode_exit_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_edit_mode_desc),
|
||||
onClick = {
|
||||
val newEditMode = !isEditMode
|
||||
val currentActivePage = richTextController?.activePageIndex ?: -1
|
||||
|
|
@ -5486,7 +5600,15 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
// TTS
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start),
|
||||
description = if (isTtsSessionActive)
|
||||
stringResource(R.string.tooltip_tts_stop_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_start_desc),
|
||||
onClick = {
|
||||
if (isTtsSessionActive) {
|
||||
Timber.d("TTS button clicked: Stopping TTS")
|
||||
|
|
@ -5526,7 +5648,15 @@ fun PdfViewerScreen(
|
|||
|
||||
// TTS Pause/Resume Button
|
||||
if (isTtsSessionActive) {
|
||||
IconButton(
|
||||
TooltipIconButton(
|
||||
text = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume),
|
||||
description = if (ttsState.isPlaying)
|
||||
stringResource(R.string.tooltip_tts_pause_desc)
|
||||
else
|
||||
stringResource(R.string.tooltip_tts_resume_desc),
|
||||
onClick = {
|
||||
if (ttsState.isPlaying) {
|
||||
ttsController.pause()
|
||||
|
|
@ -6264,7 +6394,7 @@ fun PdfViewerScreen(
|
|||
isMainTtsActive = isTtsSessionActive,
|
||||
onOpenExternalDictionary = {
|
||||
selectedTextForAi?.let { text ->
|
||||
if (selectedDictPackage != null) {
|
||||
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||
} else {
|
||||
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -6413,10 +6543,20 @@ fun PdfViewerScreen(
|
|||
useOnlineDictionary = newState
|
||||
saveUseOnlineDict(context, newState)
|
||||
},
|
||||
selectedPackageName = selectedDictPackage,
|
||||
onSelectPackage = { pkg ->
|
||||
selectedDictionaryPackageName = selectedDictPackage,
|
||||
onSelectDictionaryPackage = { pkg ->
|
||||
selectedDictPackage = pkg
|
||||
saveExternalDictPackage(context, pkg)
|
||||
},
|
||||
selectedTranslatePackageName = selectedTranslatePackage,
|
||||
onSelectTranslatePackage = { pkg ->
|
||||
selectedTranslatePackage = pkg
|
||||
saveExternalTranslatePackage(context, pkg)
|
||||
},
|
||||
selectedSearchPackageName = selectedSearchPackage,
|
||||
onSelectSearchPackage = { pkg ->
|
||||
selectedSearchPackage = pkg
|
||||
saveExternalSearchPackage(context, pkg)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue