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
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Episteme Reader is an offline-first application designed for reading various document formats. It leverages native Android technologies and C++ libraries to provide a performant reading experience with customization capabilities.
|
Episteme Reader is an offline-first, privacy-focused document and e-book reader that supports PDF, EPUB, MOBI, AZW3, Markdown, HTML, and plain text.
|
||||||
|
|
||||||
> **Note:** This is the Open Source (OSS) edition of Episteme Reader. The version available on the Google Play Store is built from this core but includes additional proprietary features.
|
> **Note:** This is the Open Source (OSS) edition of Episteme Reader. The version available on the Google Play Store is built from this core but includes additional proprietary features.
|
||||||
|
|
||||||
|
|
@ -61,10 +61,14 @@ Episteme Reader is an offline-first application designed for reading various doc
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Build:**
|
2. **Build:**
|
||||||
Open in Android Studio and run the `ossDebug` variant.
|
Open in Android Studio and run the `ossDebug` variant, or build from the command line:
|
||||||
```bash
|
```bash
|
||||||
./gradlew assembleOssDebug
|
./gradlew assembleOssDebug
|
||||||
```
|
```
|
||||||
|
The APK will be generated at:
|
||||||
|
```
|
||||||
|
app/build/outputs/apk/oss/debug/Episteme-oss-v{version}-oss-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
## Open Source Libraries
|
## Open Source Libraries
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,19 @@
|
||||||
*
|
*
|
||||||
* mail: epistemereader@gmail.com
|
* mail: epistemereader@gmail.com
|
||||||
*/
|
*/
|
||||||
|
@file:kotlin.OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
package com.aryan.reader
|
package com.aryan.reader
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
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.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextLayoutResult
|
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
|
@Composable
|
||||||
fun SearchTopBar(
|
fun SearchTopBar(
|
||||||
searchState: SearchState,
|
searchState: SearchState,
|
||||||
|
|
@ -265,7 +357,11 @@ fun SearchTopBar(
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Close Search"
|
contentDescription = "Close Search"
|
||||||
|
|
@ -297,7 +393,11 @@ fun SearchTopBar(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (searchState.searchQuery.isNotEmpty()) {
|
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(
|
Icon(
|
||||||
Icons.Default.Close,
|
Icons.Default.Close,
|
||||||
contentDescription = "Clear Search"
|
contentDescription = "Clear Search"
|
||||||
|
|
@ -305,10 +405,20 @@ fun SearchTopBar(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
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
|
searchState.showSearchResultsPanel = !searchState.showSearchResultsPanel
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
|
imageVector = if (searchState.showSearchResultsPanel) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown,
|
||||||
contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results"
|
contentDescription = if (searchState.showSearchResultsPanel) "Hide Results" else "Show Results"
|
||||||
|
|
@ -333,7 +443,9 @@ fun SearchNavigationControls(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
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) },
|
onClick = { onNavigate(searchState.currentSearchResultIndex - 1) },
|
||||||
enabled = searchState.currentSearchResultIndex > 0
|
enabled = searchState.currentSearchResultIndex > 0
|
||||||
) {
|
) {
|
||||||
|
|
@ -346,7 +458,9 @@ fun SearchNavigationControls(
|
||||||
modifier = Modifier.padding(horizontal = 4.dp)
|
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) },
|
onClick = { onNavigate(searchState.currentSearchResultIndex + 1) },
|
||||||
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1
|
enabled = searchState.currentSearchResultIndex < searchState.searchResultsCount - 1
|
||||||
) {
|
) {
|
||||||
|
|
@ -1020,7 +1134,6 @@ suspend fun fetchRecap(
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@kotlin.OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TtsSettingsSheet(
|
fun TtsSettingsSheet(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
|
|
|
||||||
|
|
@ -471,7 +471,7 @@ fun LibraryScreenContent(
|
||||||
val isBookContextualModeActive = selectedItems.isNotEmpty()
|
val isBookContextualModeActive = selectedItems.isNotEmpty()
|
||||||
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
|
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
|
||||||
var showSortMenu by remember { mutableStateOf(false) }
|
var showSortMenu by remember { mutableStateOf(false) }
|
||||||
val tabTitles = listOf("All Books", "Shelves", "Folder")
|
val tabTitles = listOf("All Books", "Shelves", "Folders")
|
||||||
val searchFocusRequester = remember { FocusRequester() }
|
val searchFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
LaunchedEffect(isSearchActive) {
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import android.webkit.WebViewClient
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
|
@ -57,6 +56,7 @@ import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -85,7 +85,6 @@ import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupPositionProvider
|
import androidx.compose.ui.window.PopupPositionProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.aryan.reader.R
|
import com.aryan.reader.R
|
||||||
import com.aryan.reader.countWords
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
|
@ -333,6 +332,8 @@ fun ChapterWebView(
|
||||||
isOss: Boolean = false,
|
isOss: Boolean = false,
|
||||||
onShowDictionaryUpsellDialog: () -> Unit,
|
onShowDictionaryUpsellDialog: () -> Unit,
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
|
onTranslate: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
onContentReadyForSummarization: suspend (String) -> Unit,
|
onContentReadyForSummarization: suspend (String) -> Unit,
|
||||||
currentFontFamily: ReaderFont,
|
currentFontFamily: ReaderFont,
|
||||||
customFontPath: String? = null,
|
customFontPath: String? = null,
|
||||||
|
|
@ -887,31 +888,68 @@ fun ChapterWebView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete Option (Only for existing highlights)
|
|
||||||
if (state.isExistingHighlight && state.cfi != null) {
|
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable {
|
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||||
// LOGGING START
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
Timber.d("Kotlin: Popup Delete requested for clicked CFI: '${state.cfi}'")
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
// 1. IMPROVED LOOKUP: Check if the clicked CFI exists within any split CFI string
|
if (state.selectedText.length <= 2000) {
|
||||||
|
IconButton(onClick = {
|
||||||
|
val textToDefine = state.selectedText
|
||||||
|
if (textToDefine.isNotBlank()) {
|
||||||
|
onWordSelectedForAiDefinition(textToDefine)
|
||||||
|
}
|
||||||
|
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 ->
|
val highlightToDelete = userHighlights.find { h ->
|
||||||
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
h.cfi == state.cfi || h.cfi.split("|").contains(state.cfi)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (highlightToDelete == null) {
|
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 cssClassToDelete = highlightToDelete.color.cssClass
|
||||||
val allCfiParts = highlightToDelete.cfi.split("|")
|
val allCfiParts = highlightToDelete.cfi.split("|")
|
||||||
|
|
||||||
allCfiParts.forEach { partCfi ->
|
allCfiParts.forEach { partCfi ->
|
||||||
Timber.d("Kotlin: Requesting JS removal for part: '$partCfi'")
|
|
||||||
localWebViewRef?.evaluateJavascript(
|
localWebViewRef?.evaluateJavascript(
|
||||||
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClassToDelete');",
|
"javascript:window.HighlightBridgeHelper.removeHighlightByCfi('${escapeJsString(partCfi)}', '$cssClassToDelete');",
|
||||||
null
|
null
|
||||||
|
|
@ -923,85 +961,9 @@ fun ChapterWebView(
|
||||||
|
|
||||||
state.finishActionModeCallback()
|
state.finishActionModeCallback()
|
||||||
customMenuState = null
|
customMenuState = null
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Delete, contentDescription = "Remove", tint = MaterialTheme.colorScheme.error)
|
||||||
}
|
}
|
||||||
.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)
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
.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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,29 @@
|
||||||
// DictionarySettingsDialog.kt
|
|
||||||
package com.aryan.reader.epubreader
|
package com.aryan.reader.epubreader
|
||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
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.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -35,19 +34,16 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import com.aryan.reader.BuildConfig
|
import com.aryan.reader.BuildConfig
|
||||||
import com.aryan.reader.R
|
|
||||||
|
|
||||||
@Suppress("KotlinConstantConditions")
|
@Suppress("KotlinConstantConditions")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DictionarySettingsDialog(
|
fun DictionarySettingsDialog(
|
||||||
isVisible: Boolean,
|
isVisible: Boolean,
|
||||||
|
|
@ -55,244 +51,292 @@ fun DictionarySettingsDialog(
|
||||||
isProUser: Boolean,
|
isProUser: Boolean,
|
||||||
useOnlineDictionary: Boolean,
|
useOnlineDictionary: Boolean,
|
||||||
onToggleOnlineDictionary: (Boolean) -> Unit,
|
onToggleOnlineDictionary: (Boolean) -> Unit,
|
||||||
selectedPackageName: String?,
|
selectedDictionaryPackageName: String?,
|
||||||
onSelectPackage: (String) -> Unit
|
onSelectDictionaryPackage: (String) -> Unit,
|
||||||
|
selectedTranslatePackageName: String?,
|
||||||
|
onSelectTranslatePackage: (String) -> Unit,
|
||||||
|
selectedSearchPackageName: String?,
|
||||||
|
onSelectSearchPackage: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
if (!isVisible) return
|
if (!isVisible) return
|
||||||
|
|
||||||
val context = LocalContext.current
|
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) {
|
LaunchedEffect(Unit) {
|
||||||
availableApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
|
dictionaryApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
|
||||||
|
searchApps = ExternalDictionaryHelper.getAvailableSearchApps(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 6.dp,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp, vertical = 24.dp)
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(24.dp)
|
||||||
) {
|
) {
|
||||||
// Header
|
|
||||||
Text(
|
Text(
|
||||||
text = "Dictionary Settings",
|
text = "Lookup Settings",
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
modifier = Modifier.padding(bottom = 20.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Dictionary ──
|
||||||
if (BuildConfig.FLAVOR != "oss") {
|
if (BuildConfig.FLAVOR != "oss") {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
color = if (useOnlineDictionary) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
border = BorderStroke(
|
modifier = Modifier.fillMaxWidth()
|
||||||
1.dp,
|
|
||||||
if (useOnlineDictionary) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onToggleOnlineDictionary(true) }
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
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(
|
||||||
text = "AI Smart Dictionary",
|
text = "Dictionary Engine",
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
) {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!useOnlineDictionary) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Check,
|
|
||||||
contentDescription = "Selected",
|
|
||||||
tint = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = if (useOnlineDictionary) "Fallback External App (Used when offline)" else "Select External App",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(bottom = 8.dp)
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
} else {
|
SingleChoiceSegmentedButtonRow(
|
||||||
// OSS FLAVOR UI (Dedicated to external apps)
|
modifier = Modifier
|
||||||
Surface(
|
.fillMaxWidth()
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
.padding(bottom = 8.dp)
|
||||||
shape = RoundedCornerShape(12.dp),
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
) {
|
) {
|
||||||
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
|
SegmentedButton(
|
||||||
Icon(
|
selected = useOnlineDictionary,
|
||||||
painter = painterResource(id = R.drawable.dictionary),
|
onClick = { onToggleOnlineDictionary(true) },
|
||||||
contentDescription = null,
|
shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2)
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
) {
|
||||||
modifier = Modifier.size(32.dp)
|
Text("Smart (AI)")
|
||||||
)
|
}
|
||||||
Spacer(Modifier.width(12.dp))
|
SegmentedButton(
|
||||||
|
selected = !useOnlineDictionary,
|
||||||
|
onClick = { onToggleOnlineDictionary(false) },
|
||||||
|
shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2)
|
||||||
|
) {
|
||||||
|
Text("External App")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Choose an external app to define selected words.",
|
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,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "Dictionary",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
AppSelectionDropdown(
|
||||||
|
apps = dictionaryApps,
|
||||||
|
selectedPackageName = selectedDictionaryPackageName,
|
||||||
|
onSelect = onSelectDictionaryPackage,
|
||||||
|
placeholder = "Select an app"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// App List
|
@Composable
|
||||||
if (availableApps.isEmpty()) {
|
private fun SectionDivider() {
|
||||||
Box(
|
HorizontalDivider(
|
||||||
modifier = Modifier
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
.fillMaxWidth()
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||||
.padding(24.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
"No supported dictionary apps found.",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LazyColumn(
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
modifier = Modifier
|
@Composable
|
||||||
.fillMaxWidth()
|
private fun AppSelectionDropdown(
|
||||||
.heightIn(max = 300.dp) // Bound height so it doesn't take over screen
|
apps: List<ExternalDictionaryApp>,
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerLowest, RoundedCornerShape(12.dp))
|
selectedPackageName: String?,
|
||||||
|
onSelect: (String) -> Unit,
|
||||||
|
placeholder: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
items(availableApps) { app ->
|
var expanded by remember { mutableStateOf(false) }
|
||||||
val isSelected = app.packageName == selectedPackageName
|
val selectedApp = apps.find { it.packageName == selectedPackageName }
|
||||||
Row(
|
val hasSelection = !selectedPackageName.isNullOrEmpty() && selectedApp != null
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
ExposedDropdownMenuBox(
|
||||||
.clickable { onSelectPackage(app.packageName) }
|
expanded = expanded,
|
||||||
.padding(vertical = 12.dp, horizontal = 8.dp),
|
onExpandedChange = { expanded = it },
|
||||||
verticalAlignment = Alignment.CenterVertically
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
// Dynamic Icon handling
|
OutlinedTextField(
|
||||||
if (app.packageName == ExternalDictionaryHelper.GOOGLE_SEARCH_PKG) {
|
value = if (hasSelection) selectedApp.label else "",
|
||||||
Box(
|
onValueChange = {},
|
||||||
modifier = Modifier
|
readOnly = true,
|
||||||
.size(40.dp)
|
singleLine = true,
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer, RoundedCornerShape(8.dp)),
|
placeholder = { Text(placeholder) },
|
||||||
contentAlignment = Alignment.Center
|
leadingIcon = if (hasSelection && selectedApp.icon != null) {
|
||||||
) {
|
{
|
||||||
Icon(
|
Image(
|
||||||
imageVector = Icons.Default.Search,
|
bitmap = selectedApp.icon.toBitmap().asImageBitmap(),
|
||||||
contentDescription = "Search",
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if (app.icon != null) {
|
} else null,
|
||||||
Image(
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
bitmap = app.icon.toBitmap().asImageBitmap(),
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
contentDescription = null,
|
shape = RoundedCornerShape(12.dp),
|
||||||
modifier = Modifier.size(40.dp)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
|
.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
// None option
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = app.label,
|
"None",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
color = if (!hasSelection) MaterialTheme.colorScheme.primary
|
||||||
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
else MaterialTheme.colorScheme.onSurface
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
if (isSelected) {
|
trailingIcon = if (!hasSelection) {
|
||||||
|
{
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
contentDescription = "Selected",
|
contentDescription = "Selected",
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} else null,
|
||||||
|
onClick = {
|
||||||
|
onSelect("")
|
||||||
|
expanded = false
|
||||||
}
|
}
|
||||||
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
|
)
|
||||||
}
|
|
||||||
}
|
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.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.RenderMode
|
||||||
import com.aryan.reader.SearchState
|
import com.aryan.reader.SearchState
|
||||||
import com.aryan.reader.SearchTopBar
|
import com.aryan.reader.SearchTopBar
|
||||||
|
import com.aryan.reader.TooltipIconButton
|
||||||
import com.aryan.reader.epub.EpubChapter
|
import com.aryan.reader.epub.EpubChapter
|
||||||
import com.aryan.reader.paginatedreader.BookPaginator
|
import com.aryan.reader.paginatedreader.BookPaginator
|
||||||
import com.aryan.reader.paginatedreader.IPaginator
|
import com.aryan.reader.paginatedreader.IPaginator
|
||||||
|
|
@ -182,7 +184,11 @@ fun EpubReaderTopBar(
|
||||||
onCloseSearch = onCloseSearch
|
onCloseSearch = onCloseSearch
|
||||||
)
|
)
|
||||||
} else {
|
} 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")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
|
|
@ -193,7 +199,11 @@ fun EpubReaderTopBar(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
IconButton(onClick = onOpenDictionarySettings) {
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_dictionary),
|
||||||
|
description = stringResource(R.string.tooltip_dictionary_desc),
|
||||||
|
onClick = onOpenDictionarySettings
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.dictionary),
|
painter = painterResource(id = R.drawable.dictionary),
|
||||||
contentDescription = "Dictionary Settings"
|
contentDescription = "Dictionary Settings"
|
||||||
|
|
@ -201,7 +211,11 @@ fun EpubReaderTopBar(
|
||||||
}
|
}
|
||||||
Box {
|
Box {
|
||||||
var showMoreMenu by remember { mutableStateOf(false) }
|
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")
|
Icon(Icons.Default.MoreVert, contentDescription = "More Options")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -392,19 +406,33 @@ fun EpubReaderBottomBar(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.SpaceAround
|
horizontalArrangement = Arrangement.SpaceAround
|
||||||
) {
|
) {
|
||||||
IconButton(
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_slider),
|
||||||
|
description = stringResource(R.string.tooltip_slider_desc),
|
||||||
onClick = onOpenSlider,
|
onClick = onOpenSlider,
|
||||||
enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL
|
enabled = currentRenderMode != RenderMode.VERTICAL_SCROLL
|
||||||
) {
|
) {
|
||||||
Icon(painter = painterResource(id = R.drawable.slider), contentDescription = "Navigate with slider")
|
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")
|
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")
|
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")
|
Icon(imageVector = Icons.Default.Search, contentDescription = "Search")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -412,7 +440,11 @@ fun EpubReaderBottomBar(
|
||||||
if (BuildConfig.FLAVOR != "oss") {
|
if (BuildConfig.FLAVOR != "oss") {
|
||||||
Box {
|
Box {
|
||||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
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")
|
Icon(painter = painterResource(id = R.drawable.ai), contentDescription = "AI Features")
|
||||||
}
|
}
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
@ -441,14 +473,32 @@ fun EpubReaderBottomBar(
|
||||||
}
|
}
|
||||||
Box {
|
Box {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
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(
|
Icon(
|
||||||
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech),
|
painter = if (isTtsSessionActive) painterResource(id = R.drawable.close) else painterResource(id = R.drawable.text_to_speech),
|
||||||
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
|
contentDescription = if (isTtsSessionActive) "Stop TTS" else "Start TTS"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isTtsSessionActive) {
|
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,
|
onClick = onPlayPauseTts,
|
||||||
enabled = !ttsState.isLoading
|
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_USE_ONLINE_DICT = "use_online_dictionary"
|
||||||
private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
|
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 {
|
private fun loadUseOnlineDict(context: Context): Boolean {
|
||||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
|
@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) }
|
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)
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||||
@Composable
|
@Composable
|
||||||
fun EpubReaderScreen(
|
fun EpubReaderScreen(
|
||||||
|
|
@ -573,6 +595,12 @@ fun EpubReaderHost(
|
||||||
var selectedDictPackage by remember {
|
var selectedDictPackage by remember {
|
||||||
mutableStateOf(loadExternalDictPackage(context))
|
mutableStateOf(loadExternalDictPackage(context))
|
||||||
}
|
}
|
||||||
|
var selectedTranslatePackage by remember {
|
||||||
|
mutableStateOf(loadExternalTranslatePackage(context))
|
||||||
|
}
|
||||||
|
var selectedSearchPackage by remember {
|
||||||
|
mutableStateOf(loadExternalSearchPackage(context))
|
||||||
|
}
|
||||||
|
|
||||||
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
|
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
|
||||||
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
|
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
|
||||||
|
|
@ -605,7 +633,7 @@ fun EpubReaderHost(
|
||||||
showDictionaryUpsellDialog = true
|
showDictionaryUpsellDialog = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selectedDictPackage != null) {
|
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word)
|
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show()
|
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) }
|
val summaryCacheManager = remember(context) { SummaryCacheManager(context) }
|
||||||
var showRecapPopup by remember { mutableStateOf(false) }
|
var showRecapPopup by remember { mutableStateOf(false) }
|
||||||
var recapResult by remember { mutableStateOf<SummarizationResult?>(null) }
|
var recapResult by remember { mutableStateOf<SummarizationResult?>(null) }
|
||||||
|
|
@ -2256,6 +2302,12 @@ fun EpubReaderHost(
|
||||||
onWordSelectedForAiDefinition = { text ->
|
onWordSelectedForAiDefinition = { text ->
|
||||||
onDictionaryLookup(text)
|
onDictionaryLookup(text)
|
||||||
},
|
},
|
||||||
|
onTranslate = { text ->
|
||||||
|
onTranslateLookup(text)
|
||||||
|
},
|
||||||
|
onSearch = { text ->
|
||||||
|
onSearchLookup(text)
|
||||||
|
},
|
||||||
onContentReadyForSummarization = { content ->
|
onContentReadyForSummarization = { content ->
|
||||||
Timber.d("Content received for summarization")
|
Timber.d("Content received for summarization")
|
||||||
scope.launch {
|
scope.launch {
|
||||||
|
|
@ -2591,6 +2643,12 @@ fun EpubReaderHost(
|
||||||
onWordSelectedForAiDefinition = { text ->
|
onWordSelectedForAiDefinition = { text ->
|
||||||
onDictionaryLookup(text)
|
onDictionaryLookup(text)
|
||||||
},
|
},
|
||||||
|
onTranslate = { text ->
|
||||||
|
onTranslateLookup(text)
|
||||||
|
},
|
||||||
|
onSearch = { text ->
|
||||||
|
onSearchLookup(text)
|
||||||
|
},
|
||||||
userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) },
|
userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) },
|
||||||
onHighlightCreated = { cfi, text, colorId ->
|
onHighlightCreated = { cfi, text, colorId ->
|
||||||
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
Timber.d("EpubReaderScreen: onHighlightCreated. CFI: $cfi")
|
||||||
|
|
@ -3532,7 +3590,7 @@ fun EpubReaderHost(
|
||||||
onNavigateToPro = onNavigateToPro,
|
onNavigateToPro = onNavigateToPro,
|
||||||
isTtsSessionActive = isTtsSessionActive,
|
isTtsSessionActive = isTtsSessionActive,
|
||||||
onOpenExternalDictionary = { text ->
|
onOpenExternalDictionary = { text ->
|
||||||
if (selectedDictPackage != null) {
|
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
||||||
|
|
@ -3707,10 +3765,20 @@ fun EpubReaderHost(
|
||||||
useOnlineDictionary = newState
|
useOnlineDictionary = newState
|
||||||
saveUseOnlineDict(context, newState)
|
saveUseOnlineDict(context, newState)
|
||||||
},
|
},
|
||||||
selectedPackageName = selectedDictPackage,
|
selectedDictionaryPackageName = selectedDictPackage,
|
||||||
onSelectPackage = { pkg ->
|
onSelectDictionaryPackage = { pkg ->
|
||||||
selectedDictPackage = pkg
|
selectedDictPackage = pkg
|
||||||
saveExternalDictPackage(context, 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.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
data class ExternalDictionaryApp(
|
data class ExternalDictionaryApp(
|
||||||
val label: String,
|
val label: String,
|
||||||
|
|
@ -23,7 +25,6 @@ object ExternalDictionaryHelper {
|
||||||
"com.samsung.android.samsungpassautofill",
|
"com.samsung.android.samsungpassautofill",
|
||||||
"com.samsung.android.samsungpass",
|
"com.samsung.android.samsungpass",
|
||||||
"com.samsung.android.app.pass",
|
"com.samsung.android.app.pass",
|
||||||
"com.google.android.gms",
|
|
||||||
"com.truecaller",
|
"com.truecaller",
|
||||||
"com.adobe.reader",
|
"com.adobe.reader",
|
||||||
"com.reddit.frontpage"
|
"com.reddit.frontpage"
|
||||||
|
|
@ -77,18 +78,15 @@ object ExternalDictionaryHelper {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return sortedApps
|
return apps.sortedBy { it.label }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun launchDictionary(context: Context, packageName: String, query: String) {
|
fun launchDictionary(context: Context, packageName: String, query: String) {
|
||||||
|
if (packageName.isEmpty()) return
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
try {
|
try {
|
||||||
if (packageName == GOOGLE_SEARCH_PKG) {
|
if (packageName == GOOGLE_SEARCH_PKG) {
|
||||||
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
launchSearch(context, packageName, query)
|
||||||
putExtra(SearchManager.QUERY, query)
|
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
}
|
|
||||||
context.startActivity(searchIntent)
|
|
||||||
return
|
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) {
|
private fun launchGenericSend(context: Context, packageName: String, query: String) {
|
||||||
val sendIntent = Intent(Intent.ACTION_SEND)
|
val sendIntent = Intent(Intent.ACTION_SEND)
|
||||||
sendIntent.type = "text/plain"
|
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.CircularProgressIndicator
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -505,6 +506,8 @@ fun PaginatedReaderScreen(
|
||||||
isOss: Boolean = false,
|
isOss: Boolean = false,
|
||||||
onShowDictionaryUpsellDialog: () -> Unit,
|
onShowDictionaryUpsellDialog: () -> Unit,
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
|
onTranslate: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
userHighlights: List<UserHighlight>,
|
userHighlights: List<UserHighlight>,
|
||||||
onHighlightCreated: (String, String, String) -> Unit,
|
onHighlightCreated: (String, String, String) -> Unit,
|
||||||
onHighlightDeleted: (String) -> Unit,
|
onHighlightDeleted: (String) -> Unit,
|
||||||
|
|
@ -802,6 +805,8 @@ fun PaginatedReaderScreen(
|
||||||
isOss = isOss,
|
isOss = isOss,
|
||||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
||||||
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
||||||
|
onTranslate = onTranslate,
|
||||||
|
onSearch = onSearch,
|
||||||
userHighlights = userHighlights,
|
userHighlights = userHighlights,
|
||||||
onHighlightCreated = onHighlightCreated,
|
onHighlightCreated = onHighlightCreated,
|
||||||
onHighlightDeleted = onHighlightDeleted,
|
onHighlightDeleted = onHighlightDeleted,
|
||||||
|
|
@ -1372,6 +1377,8 @@ internal fun PaginatedReaderContent(
|
||||||
isOss: Boolean,
|
isOss: Boolean,
|
||||||
onShowDictionaryUpsellDialog: () -> Unit,
|
onShowDictionaryUpsellDialog: () -> Unit,
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
|
onTranslate: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
onGetChapterInfo: (Int) -> Pair<String, Int?>?,
|
onGetChapterInfo: (Int) -> Pair<String, Int?>?,
|
||||||
userHighlights: List<UserHighlight>,
|
userHighlights: List<UserHighlight>,
|
||||||
onHighlightCreated: (String, String, String) -> Unit,
|
onHighlightCreated: (String, String, String) -> Unit,
|
||||||
|
|
@ -2531,6 +2538,14 @@ internal fun PaginatedReaderContent(
|
||||||
state.onCopy()
|
state.onCopy()
|
||||||
isForDictionary = false
|
isForDictionary = false
|
||||||
state.onHide()
|
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 ->
|
}, onHighlight = { color ->
|
||||||
Timber.d("Menu: Highlight option clicked. Color: ${color.id}")
|
Timber.d("Menu: Highlight option clicked. Color: ${color.id}")
|
||||||
isForHighlight = true
|
isForHighlight = true
|
||||||
|
|
@ -2768,6 +2783,12 @@ internal fun PaginatedReaderContent(
|
||||||
onShowDictionaryUpsellDialog()
|
onShowDictionaryUpsellDialog()
|
||||||
}
|
}
|
||||||
activeSelection = null
|
activeSelection = null
|
||||||
|
}, onTranslate = {
|
||||||
|
onTranslate(sel.text)
|
||||||
|
activeSelection = null
|
||||||
|
}, onSearch = {
|
||||||
|
onSearch(sel.text)
|
||||||
|
activeSelection = null
|
||||||
}, onHighlight = { color ->
|
}, onHighlight = { color ->
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"CustomSelection: Highlight clicked. Text: '${sel.text}', BaseCFI: ${sel.baseCfi}, StartOffset: ${sel.startOffset}"
|
"CustomSelection: Highlight clicked. Text: '${sel.text}', BaseCFI: ${sel.baseCfi}, StartOffset: ${sel.startOffset}"
|
||||||
|
|
@ -2834,6 +2855,14 @@ internal fun PaginatedReaderContent(
|
||||||
}
|
}
|
||||||
activeHighlightForMenu = null
|
activeHighlightForMenu = null
|
||||||
},
|
},
|
||||||
|
onTranslate = {
|
||||||
|
onTranslate(highlight.text)
|
||||||
|
activeHighlightForMenu = null
|
||||||
|
},
|
||||||
|
onSearch = {
|
||||||
|
onSearch(highlight.text)
|
||||||
|
activeHighlightForMenu = null
|
||||||
|
},
|
||||||
onHighlight = { color ->
|
onHighlight = { color ->
|
||||||
Timber.d("Menu: Updating highlight color to ${color.id}")
|
Timber.d("Menu: Updating highlight color to ${color.id}")
|
||||||
onHighlightDeleted(highlight.cfi)
|
onHighlightDeleted(highlight.cfi)
|
||||||
|
|
@ -2911,6 +2940,8 @@ private fun PaginatedTextSelectionMenu(
|
||||||
onCopy: () -> Unit,
|
onCopy: () -> Unit,
|
||||||
onSelectAll: (() -> Unit)?,
|
onSelectAll: (() -> Unit)?,
|
||||||
onDictionary: () -> Unit,
|
onDictionary: () -> Unit,
|
||||||
|
onTranslate: () -> Unit,
|
||||||
|
onSearch: () -> Unit,
|
||||||
onHighlight: ((HighlightColor) -> Unit)?,
|
onHighlight: ((HighlightColor) -> Unit)?,
|
||||||
onDelete: (() -> Unit)?,
|
onDelete: (() -> Unit)?,
|
||||||
@Suppress("unused") isProUser: Boolean,
|
@Suppress("unused") isProUser: Boolean,
|
||||||
|
|
@ -2955,99 +2986,71 @@ private fun PaginatedTextSelectionMenu(
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete Option
|
// 2. Action Icons Row (Horizontal)
|
||||||
if (onDelete != null) {
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onDelete)
|
.padding(horizontal = 8.dp, vertical = 8.dp),
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
) {
|
||||||
|
IconButton(onClick = onCopy) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.copy),
|
||||||
|
contentDescription = "Copy",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
Icon(
|
||||||
imageVector = Icons.Default.Delete,
|
imageVector = Icons.Default.Delete,
|
||||||
contentDescription = "Remove",
|
contentDescription = "Remove",
|
||||||
tint = MaterialTheme.colorScheme.error,
|
tint = MaterialTheme.colorScheme.error,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Remove",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalDivider()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Copy Option
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onCopy)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = R.drawable.select_all),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
"Select All",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.Icons
|
||||||
import androidx.compose.material.icons.filled.CopyAll
|
import androidx.compose.material.icons.filled.CopyAll
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -188,6 +190,8 @@ internal fun PdfSelectionMenuPopup(
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onCopy: (String) -> Unit,
|
onCopy: (String) -> Unit,
|
||||||
onAiDefine: (String) -> Unit,
|
onAiDefine: (String) -> Unit,
|
||||||
|
onTranslate: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onColorSelected: (PdfHighlightColor) -> Unit,
|
onColorSelected: (PdfHighlightColor) -> Unit,
|
||||||
onDelete: () -> Unit
|
onDelete: () -> Unit
|
||||||
|
|
@ -281,51 +285,74 @@ internal fun PdfSelectionMenuPopup(
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
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
|
||||||
|
) {
|
||||||
|
// Copy
|
||||||
|
androidx.compose.material3.IconButton(
|
||||||
|
onClick = { onCopy(menuState.selectedText) }
|
||||||
) {
|
) {
|
||||||
Box(
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
.clickable { onCopy(menuState.selectedText) }.padding(vertical = 12.dp),
|
|
||||||
contentAlignment = Alignment.Center) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CopyAll,
|
Icons.Default.CopyAll,
|
||||||
contentDescription = null,
|
contentDescription = "Copy",
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Text("Copy", style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dictionary
|
||||||
if (menuState.selectedText.length <= 2000) {
|
if (menuState.selectedText.length <= 2000) {
|
||||||
Box(
|
androidx.compose.material3.IconButton(
|
||||||
modifier = Modifier.weight(1f)
|
onClick = { onAiDefine(menuState.selectedText) }
|
||||||
.clickable { onAiDefine(menuState.selectedText) }
|
) {
|
||||||
.padding(vertical = 12.dp), contentAlignment = Alignment.Center) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.dictionary),
|
painter = painterResource(id = R.drawable.dictionary),
|
||||||
contentDescription = null,
|
contentDescription = "Dictionary",
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Text("Dictionary", style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
if (!menuState.isExistingHighlight) {
|
||||||
Box(modifier = Modifier.weight(1f).clickable { onSelectAll() }
|
androidx.compose.material3.IconButton(
|
||||||
.padding(vertical = 12.dp), contentAlignment = Alignment.Center) {
|
onClick = { onSelectAll() }
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.select_all),
|
painter = painterResource(id = R.drawable.select_all),
|
||||||
contentDescription = null,
|
contentDescription = "Select All",
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
Text("Select All", style = MaterialTheme.typography.labelSmall)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,8 @@ internal fun PdfPageComposable(
|
||||||
isProUser: Boolean,
|
isProUser: Boolean,
|
||||||
onShowDictionaryUpsellDialog: () -> Unit,
|
onShowDictionaryUpsellDialog: () -> Unit,
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
|
onTranslateText: (String) -> Unit,
|
||||||
|
onSearchText: (String) -> Unit,
|
||||||
ttsHighlightData: TtsHighlightData?,
|
ttsHighlightData: TtsHighlightData?,
|
||||||
onLinkClicked: (String) -> Unit,
|
onLinkClicked: (String) -> Unit,
|
||||||
onInternalLinkClicked: (Int) -> 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 = {
|
onSelectAll = {
|
||||||
customMenuState = null
|
customMenuState = null
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
|
|
@ -4523,6 +4555,8 @@ private fun PdfPageRenderer(
|
||||||
onMenuDismiss: () -> Unit,
|
onMenuDismiss: () -> Unit,
|
||||||
onCopy: (String) -> Unit,
|
onCopy: (String) -> Unit,
|
||||||
onAiDefine: (String) -> Unit,
|
onAiDefine: (String) -> Unit,
|
||||||
|
onTranslate: (String) -> Unit,
|
||||||
|
onSearch: (String) -> Unit,
|
||||||
onSelectAll: () -> Unit,
|
onSelectAll: () -> Unit,
|
||||||
onShowUpsellDialog: () -> Unit,
|
onShowUpsellDialog: () -> Unit,
|
||||||
isProUser: Boolean,
|
isProUser: Boolean,
|
||||||
|
|
@ -4934,6 +4968,8 @@ private fun PdfPageRenderer(
|
||||||
onDismiss = onMenuDismiss,
|
onDismiss = onMenuDismiss,
|
||||||
onCopy = onCopy,
|
onCopy = onCopy,
|
||||||
onAiDefine = onAiDefine,
|
onAiDefine = onAiDefine,
|
||||||
|
onTranslate = onTranslate,
|
||||||
|
onSearch = onSearch,
|
||||||
onSelectAll = onSelectAll,
|
onSelectAll = onSelectAll,
|
||||||
onColorSelected = { color ->
|
onColorSelected = { color ->
|
||||||
Timber.tag("PdfHighlightDebug").d("PdfSelectionMenuPopup onColorSelected: $color, isExisting=${menuState.isExistingHighlight}")
|
Timber.tag("PdfHighlightDebug").d("PdfSelectionMenuPopup onColorSelected: $color, isExisting=${menuState.isExistingHighlight}")
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,8 @@ internal fun PdfVerticalReader(
|
||||||
isProUser: Boolean,
|
isProUser: Boolean,
|
||||||
onShowDictionaryUpsellDialog: () -> Unit,
|
onShowDictionaryUpsellDialog: () -> Unit,
|
||||||
onWordSelectedForAiDefinition: (String) -> Unit,
|
onWordSelectedForAiDefinition: (String) -> Unit,
|
||||||
|
onTranslateText: (String) -> Unit,
|
||||||
|
onSearchText: (String) -> Unit,
|
||||||
ttsHighlightData: TtsHighlightData?,
|
ttsHighlightData: TtsHighlightData?,
|
||||||
ttsReadingPage: Int?,
|
ttsReadingPage: Int?,
|
||||||
onLinkClicked: (String) -> Unit,
|
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) {
|
val onDoubleTapLambda = remember(page, screenWidth, screenHeight) {
|
||||||
{ localOffset: Offset ->
|
{ localOffset: Offset ->
|
||||||
Timber.tag("PdfZoomDebug").d(
|
Timber.tag("PdfZoomDebug").d(
|
||||||
|
|
@ -1456,6 +1466,8 @@ internal fun PdfVerticalReader(
|
||||||
isProUser = isProUser,
|
isProUser = isProUser,
|
||||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialog,
|
||||||
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
onWordSelectedForAiDefinition = onWordSelectedForAiDefinition,
|
||||||
|
onTranslateText = onTranslateTextLambda,
|
||||||
|
onSearchText = onSearchTextLambda,
|
||||||
ttsHighlightData = pageTtsData,
|
ttsHighlightData = pageTtsData,
|
||||||
onLinkClicked = onLinkClicked,
|
onLinkClicked = onLinkClicked,
|
||||||
onInternalLinkClicked = onInternalLinkClicked,
|
onInternalLinkClicked = onInternalLinkClicked,
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,7 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
|
@ -244,6 +245,7 @@ import com.aryan.reader.R
|
||||||
import com.aryan.reader.SearchResult
|
import com.aryan.reader.SearchResult
|
||||||
import com.aryan.reader.SearchTopBar
|
import com.aryan.reader.SearchTopBar
|
||||||
import com.aryan.reader.SummarizationPopup
|
import com.aryan.reader.SummarizationPopup
|
||||||
|
import com.aryan.reader.TooltipIconButton
|
||||||
import com.aryan.reader.SummarizationResult
|
import com.aryan.reader.SummarizationResult
|
||||||
import com.aryan.reader.TtsSettingsSheet
|
import com.aryan.reader.TtsSettingsSheet
|
||||||
import com.aryan.reader.countWords
|
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 PDF_MUSICIAN_MODE_KEY = "pdf_musician_mode_enabled"
|
||||||
private const val PREF_USE_ONLINE_DICT = "use_online_dictionary"
|
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_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 {
|
private fun loadUseOnlineDict(context: Context): Boolean {
|
||||||
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
|
@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) }
|
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) {
|
private fun savePdfMusicianMode(context: Context, isEnabled: Boolean) {
|
||||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
prefs.edit { putBoolean(PDF_MUSICIAN_MODE_KEY, isEnabled) }
|
prefs.edit { putBoolean(PDF_MUSICIAN_MODE_KEY, isEnabled) }
|
||||||
|
|
@ -780,6 +804,8 @@ fun PdfViewerScreen(
|
||||||
var showDictionarySettingsSheet by remember { mutableStateOf(false) }
|
var showDictionarySettingsSheet by remember { mutableStateOf(false) }
|
||||||
var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
|
var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
|
||||||
var selectedDictPackage by remember { mutableStateOf(loadExternalDictPackage(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) }
|
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
@ -1127,7 +1153,7 @@ fun PdfViewerScreen(
|
||||||
withContext(NonCancellable) {
|
withContext(NonCancellable) {
|
||||||
saveMutex.withLock {
|
saveMutex.withLock {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
var didSave = false
|
@Suppress("VariableNeverRead") var didSave = false
|
||||||
|
|
||||||
if (force || annotsHash != lastSavedHashes[0]) {
|
if (force || annotsHash != lastSavedHashes[0]) {
|
||||||
annotationRepository.saveAnnotations(bookId, annots)
|
annotationRepository.saveAnnotations(bookId, annots)
|
||||||
|
|
@ -2131,7 +2157,7 @@ fun PdfViewerScreen(
|
||||||
showDictionaryUpsellDialog = true
|
showDictionaryUpsellDialog = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (selectedDictPackage != null) {
|
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show()
|
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 onLinkClickedStable = remember { { url: String -> clickedLinkUrl = url } }
|
||||||
|
|
||||||
val onInternalLinkNavStable = remember(displayMode) {
|
val onInternalLinkNavStable = remember(displayMode) {
|
||||||
|
|
@ -3874,6 +3922,8 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||||
|
onTranslateText = onTranslateTextStable,
|
||||||
|
onSearchText = onSearchTextStable,
|
||||||
onOcrStateChange = onOcrStateChange,
|
onOcrStateChange = onOcrStateChange,
|
||||||
onLinkClicked = { url -> clickedLinkUrl = url },
|
onLinkClicked = { url -> clickedLinkUrl = url },
|
||||||
onInternalLinkClicked = onInternalLinkNav,
|
onInternalLinkClicked = onInternalLinkNav,
|
||||||
|
|
@ -4244,6 +4294,8 @@ fun PdfViewerScreen(
|
||||||
isProUser = isProUser,
|
isProUser = isProUser,
|
||||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable,
|
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable,
|
||||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||||
|
onTranslateText = onTranslateTextStable,
|
||||||
|
onSearchText = onSearchTextStable,
|
||||||
ttsHighlightData = ttsHighlightData,
|
ttsHighlightData = ttsHighlightData,
|
||||||
ttsReadingPage = ttsPageData?.pageIndex,
|
ttsReadingPage = ttsPageData?.pageIndex,
|
||||||
userHighlights = userHighlights,
|
userHighlights = userHighlights,
|
||||||
|
|
@ -4774,7 +4826,11 @@ fun PdfViewerScreen(
|
||||||
focusManager.clearFocus()
|
focusManager.clearFocus()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
IconButton(onClick = { saveStateAndExit() }) {
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_back),
|
||||||
|
description = stringResource(R.string.tooltip_back_desc),
|
||||||
|
onClick = { saveStateAndExit() }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
contentDescription = "Back"
|
contentDescription = "Back"
|
||||||
|
|
@ -4804,7 +4860,17 @@ fun PdfViewerScreen(
|
||||||
.testTag("PageNumberIndicator")
|
.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(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.dark_mode),
|
painter = painterResource(id = R.drawable.dark_mode),
|
||||||
contentDescription = if (isPdfDarkMode) "Disable Dark Mode"
|
contentDescription = if (isPdfDarkMode) "Disable Dark Mode"
|
||||||
|
|
@ -4814,10 +4880,20 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
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
|
isScrollLocked = !isScrollLocked
|
||||||
savePdfScrollLocked(context, bookId, isScrollLocked)
|
savePdfScrollLocked(context, bookId, isScrollLocked)
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
imageVector = if (isScrollLocked) Icons.Default.Lock else Icons.Default.LockOpen,
|
||||||
contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning",
|
contentDescription = if (isScrollLocked) "Unlock Panning" else "Lock Panning",
|
||||||
|
|
@ -4825,10 +4901,14 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(onClick = {
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_fullscreen),
|
||||||
|
description = stringResource(R.string.tooltip_fullscreen_desc),
|
||||||
|
onClick = {
|
||||||
isFullScreen = true
|
isFullScreen = true
|
||||||
savePdfFullScreen(context, bookId, true)
|
savePdfFullScreen(context, bookId, true)
|
||||||
}) {
|
}
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Fullscreen,
|
imageVector = Icons.Default.Fullscreen,
|
||||||
contentDescription = "Enter Full Screen",
|
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(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.dictionary),
|
painter = painterResource(id = R.drawable.dictionary),
|
||||||
contentDescription = "Dictionary Settings",
|
contentDescription = "Dictionary Settings",
|
||||||
|
|
@ -4845,7 +4929,7 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
IconButton(onClick = { showPenPlayground = true }) {
|
TooltipIconButton(text = "Pen Playground", onClick = { showPenPlayground = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Star,
|
imageVector = Icons.Default.Star,
|
||||||
contentDescription = "Open Pen Playground",
|
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
|
val page = if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
|
||||||
|
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
|
|
@ -4890,7 +4974,11 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
var showMoreMenu by remember { mutableStateOf(false) }
|
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(
|
Icon(
|
||||||
imageVector = Icons.Default.MoreVert,
|
imageVector = Icons.Default.MoreVert,
|
||||||
contentDescription = "More Options"
|
contentDescription = "More Options"
|
||||||
|
|
@ -5350,7 +5438,9 @@ fun PdfViewerScreen(
|
||||||
horizontalArrangement = Arrangement.SpaceAround
|
horizontalArrangement = Arrangement.SpaceAround
|
||||||
) {
|
) {
|
||||||
// Slider Navigation Trigger
|
// Slider Navigation Trigger
|
||||||
IconButton(
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_slider),
|
||||||
|
description = stringResource(R.string.tooltip_slider_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
val currentPage = if (displayMode == DisplayMode.PAGINATION) {
|
val currentPage = if (displayMode == DisplayMode.PAGINATION) {
|
||||||
pagerState.currentPage
|
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() } },
|
onClick = { coroutineScope.launch { drawerState.open() } },
|
||||||
enabled = !(ttsState.isPlaying || ttsState.isLoading),
|
enabled = !(ttsState.isPlaying || ttsState.isLoading),
|
||||||
modifier = Modifier.testTag("TocButton")
|
modifier = Modifier.testTag("TocButton")
|
||||||
|
|
@ -5381,7 +5473,9 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search Button
|
// Search Button
|
||||||
IconButton(
|
TooltipIconButton(
|
||||||
|
text = stringResource(R.string.tooltip_search),
|
||||||
|
description = stringResource(R.string.tooltip_search_desc),
|
||||||
onClick = {
|
onClick = {
|
||||||
executeWithOcrCheck {
|
executeWithOcrCheck {
|
||||||
searchState.isSearchActive = true
|
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 = {
|
onClick = {
|
||||||
val newState = !showAllTextHighlights
|
val newState = !showAllTextHighlights
|
||||||
if (newState) {
|
if (newState) {
|
||||||
if (isHighlightingLoading) return@IconButton
|
if (isHighlightingLoading) return@TooltipIconButton
|
||||||
showAllTextHighlights = true
|
showAllTextHighlights = true
|
||||||
isHighlightingLoading = true
|
isHighlightingLoading = true
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -5424,7 +5526,11 @@ fun PdfViewerScreen(
|
||||||
// AI feat
|
// AI feat
|
||||||
Box {
|
Box {
|
||||||
var showAiFeaturesMenu by remember { mutableStateOf(false) }
|
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(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.ai),
|
painter = painterResource(id = R.drawable.ai),
|
||||||
contentDescription = "AI Features"
|
contentDescription = "AI Features"
|
||||||
|
|
@ -5458,7 +5564,15 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Edit Button
|
// 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 = {
|
onClick = {
|
||||||
val newEditMode = !isEditMode
|
val newEditMode = !isEditMode
|
||||||
val currentActivePage = richTextController?.activePageIndex ?: -1
|
val currentActivePage = richTextController?.activePageIndex ?: -1
|
||||||
|
|
@ -5486,7 +5600,15 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
// TTS
|
// 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 = {
|
onClick = {
|
||||||
if (isTtsSessionActive) {
|
if (isTtsSessionActive) {
|
||||||
Timber.d("TTS button clicked: Stopping TTS")
|
Timber.d("TTS button clicked: Stopping TTS")
|
||||||
|
|
@ -5526,7 +5648,15 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
// TTS Pause/Resume Button
|
// TTS Pause/Resume Button
|
||||||
if (isTtsSessionActive) {
|
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 = {
|
onClick = {
|
||||||
if (ttsState.isPlaying) {
|
if (ttsState.isPlaying) {
|
||||||
ttsController.pause()
|
ttsController.pause()
|
||||||
|
|
@ -6264,7 +6394,7 @@ fun PdfViewerScreen(
|
||||||
isMainTtsActive = isTtsSessionActive,
|
isMainTtsActive = isTtsSessionActive,
|
||||||
onOpenExternalDictionary = {
|
onOpenExternalDictionary = {
|
||||||
selectedTextForAi?.let { text ->
|
selectedTextForAi?.let { text ->
|
||||||
if (selectedDictPackage != null) {
|
if (!selectedDictPackage.isNullOrEmpty()) {
|
||||||
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
|
||||||
|
|
@ -6413,10 +6543,20 @@ fun PdfViewerScreen(
|
||||||
useOnlineDictionary = newState
|
useOnlineDictionary = newState
|
||||||
saveUseOnlineDict(context, newState)
|
saveUseOnlineDict(context, newState)
|
||||||
},
|
},
|
||||||
selectedPackageName = selectedDictPackage,
|
selectedDictionaryPackageName = selectedDictPackage,
|
||||||
onSelectPackage = { pkg ->
|
onSelectDictionaryPackage = { pkg ->
|
||||||
selectedDictPackage = pkg
|
selectedDictPackage = pkg
|
||||||
saveExternalDictPackage(context, pkg)
|
saveExternalDictPackage(context, pkg)
|
||||||
|
},
|
||||||
|
selectedTranslatePackageName = selectedTranslatePackage,
|
||||||
|
onSelectTranslatePackage = { pkg ->
|
||||||
|
selectedTranslatePackage = pkg
|
||||||
|
saveExternalTranslatePackage(context, pkg)
|
||||||
|
},
|
||||||
|
selectedSearchPackageName = selectedSearchPackage,
|
||||||
|
onSelectSearchPackage = { pkg ->
|
||||||
|
selectedSearchPackage = pkg
|
||||||
|
saveExternalSearchPackage(context, pkg)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable-nodpi/search.xml
Normal file
10
app/src/main/res/drawable-nodpi/search.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M784,840L532,588Q502,612 463,626Q424,640 380,640Q271,640 195.5,564.5Q120,489 120,380Q120,271 195.5,195.5Q271,120 380,120Q489,120 564.5,195.5Q640,271 640,380Q640,424 626,463Q612,502 588,532L840,784L784,840ZM380,560Q455,560 507.5,507.5Q560,455 560,380Q560,305 507.5,252.5Q455,200 380,200Q305,200 252.5,252.5Q200,305 200,380Q200,455 252.5,507.5Q305,560 380,560Z"/>
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable-nodpi/translate.xml
Normal file
10
app/src/main/res/drawable-nodpi/translate.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M476,880L658,400L742,400L924,880L840,880L797,758L603,758L560,880L476,880ZM160,760L104,704L306,502Q271,467 242.5,422Q214,377 190,320L274,320Q294,359 314,388Q334,417 362,446Q395,413 430.5,353.5Q466,294 484,240L40,240L40,160L320,160L320,80L400,80L400,160L680,160L680,240L564,240Q543,312 501,388Q459,464 418,504L514,602L484,684L362,559L160,760ZM628,688L772,688L700,484L628,688Z"/>
|
||||||
|
</vector>
|
||||||
|
|
@ -1,3 +1,61 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Episteme</string>
|
<string name="app_name">Episteme</string>
|
||||||
|
|
||||||
|
<!-- Tooltip titles -->
|
||||||
|
<string name="tooltip_back">Back</string>
|
||||||
|
<string name="tooltip_dictionary">Dictionary</string>
|
||||||
|
<string name="tooltip_more_options">More Options</string>
|
||||||
|
<string name="tooltip_slider">Page Slider</string>
|
||||||
|
<string name="tooltip_toc">Table of Contents</string>
|
||||||
|
<string name="tooltip_format">Text Format</string>
|
||||||
|
<string name="tooltip_search">Search</string>
|
||||||
|
<string name="tooltip_ai">AI Features</string>
|
||||||
|
<string name="tooltip_tts_start">Start Text-to-Speech</string>
|
||||||
|
<string name="tooltip_tts_stop">Stop Text-to-Speech</string>
|
||||||
|
<string name="tooltip_tts_pause">Pause</string>
|
||||||
|
<string name="tooltip_tts_resume">Resume</string>
|
||||||
|
<string name="tooltip_dark_mode_on">Enable Dark Mode</string>
|
||||||
|
<string name="tooltip_dark_mode_off">Disable Dark Mode</string>
|
||||||
|
<string name="tooltip_lock_pan">Lock Panning</string>
|
||||||
|
<string name="tooltip_unlock_pan">Unlock Panning</string>
|
||||||
|
<string name="tooltip_fullscreen">Full Screen</string>
|
||||||
|
<string name="tooltip_highlights">Show Highlights</string>
|
||||||
|
<string name="tooltip_highlights_off">Hide Highlights</string>
|
||||||
|
<string name="tooltip_edit_mode">Annotation Mode</string>
|
||||||
|
<string name="tooltip_edit_mode_exit">Exit Annotation Mode</string>
|
||||||
|
<string name="tooltip_close_search">Close Search</string>
|
||||||
|
<string name="tooltip_clear_search">Clear Search</string>
|
||||||
|
<string name="tooltip_show_results">Show Results</string>
|
||||||
|
<string name="tooltip_hide_results">Hide Results</string>
|
||||||
|
<string name="tooltip_prev_result">Previous Result</string>
|
||||||
|
<string name="tooltip_next_result">Next Result</string>
|
||||||
|
|
||||||
|
<!-- Tooltip descriptions -->
|
||||||
|
<string name="tooltip_back_desc">Exit the reader and return to the home screen</string>
|
||||||
|
<string name="tooltip_dictionary_desc">Choose your preferred app for word lookups</string>
|
||||||
|
<string name="tooltip_more_options_desc">Access reading mode, bookmarks, and advanced settings</string>
|
||||||
|
<string name="tooltip_slider_desc">Drag to jump quickly to any page in the document</string>
|
||||||
|
<string name="tooltip_toc_desc">Browse chapters and navigate to any section</string>
|
||||||
|
<string name="tooltip_format_desc">Adjust font, size, line height, alignment, and custom fonts</string>
|
||||||
|
<string name="tooltip_search_desc">Find any word or phrase in this book</string>
|
||||||
|
<string name="tooltip_ai_desc">Summarize the current chapter or page using AI</string>
|
||||||
|
<string name="tooltip_tts_start_desc">Read the book aloud using your device\'s voice engine</string>
|
||||||
|
<string name="tooltip_tts_stop_desc">Stop the current read-aloud session</string>
|
||||||
|
<string name="tooltip_tts_pause_desc">Pause the current read-aloud playback</string>
|
||||||
|
<string name="tooltip_tts_resume_desc">Resume paused read-aloud playback</string>
|
||||||
|
<string name="tooltip_dark_mode_on_desc">Invert PDF colors for dark mode</string>
|
||||||
|
<string name="tooltip_dark_mode_off_desc">Disable dark mode and restore the original PDF colors</string>
|
||||||
|
<string name="tooltip_lock_pan_desc">Lock horizontal panning on the page</string>
|
||||||
|
<string name="tooltip_unlock_pan_desc">Unlock panning to re-enable pinch-to-zoom and drag gestures</string>
|
||||||
|
<string name="tooltip_fullscreen_desc">Hide all UI controls for an immersive, distraction-free reading view</string>
|
||||||
|
<string name="tooltip_highlights_desc">Visually mark selectable text regions across the current page</string>
|
||||||
|
<string name="tooltip_highlights_off_desc">Remove the selectable text overlay from the page</string>
|
||||||
|
<string name="tooltip_edit_mode_desc">Add ink or text annotations</string>
|
||||||
|
<string name="tooltip_edit_mode_exit_desc">Finish editing and return to normal reading view</string>
|
||||||
|
<string name="tooltip_close_search_desc">Exit search and go back to the reader</string>
|
||||||
|
<string name="tooltip_clear_search_desc">Erase your current search query and start over</string>
|
||||||
|
<string name="tooltip_show_results_desc">Expand the panel to see all search matches</string>
|
||||||
|
<string name="tooltip_hide_results_desc">Collapse the search results panel</string>
|
||||||
|
<string name="tooltip_prev_result_desc">Jump to the previous search match in the document</string>
|
||||||
|
<string name="tooltip_next_result_desc">Jump to the next search match in the document</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue