Offline dictionary (#45)

* Added support for external dictionary apps and a dictionary settings sheet.

* Refactored external dictionary integration and improved package visibility

 - Updated `AndroidManifest.xml` with `<queries>` for common dictionary apps and intent actions (`PROCESS_TEXT`, `SEARCH`, `SEND`) to support Android 11+ package visibility.
 - Refactored `ExternalDictionaryHelper` to streamline dictionary discovery using `Intent.ACTION_PROCESS_TEXT` and `colordict.intent.action.SEARCH`.
 - Added a package blocklist in `ExternalDictionaryHelper` to filter out system services (e.g., Samsung Pass, GMS) from dictionary lists.
  - Simplified `launchDictionary` logic to prioritize `PROCESS_TEXT` intents.
 - Fixed a bug in `PdfViewerScreen` to only show the dictionary upsell dialog when `useOnlineDictionary` is enabled.

* Refactored dictionary settings into a dialog and updated dictionary selection logic.

- Renamed `DictionarySettingsSheet` to `DictionarySettingsDialog` and converted it from a bottom sheet to a formal dialog.
- Redesigned the dictionary settings UI with a clearer distinction between AI and external app modes.
- Added a "Search" option to the external dictionary list that triggers a web search.
- Updated `ExternalDictionaryHelper` to include a package blocklist and support for generic web search intents.
- Simplified `PdfHelper` by removing Pro user checks for the "Dictionary" action.
- Updated icons for dictionary settings across the EPUB and PDF readers.
This commit is contained in:
Aryan 2026-03-08 15:19:52 +05:30 committed by GitHub
parent dff236fa7f
commit 76780bff31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 759 additions and 171 deletions

View file

@ -11,6 +11,26 @@
</intent> </intent>
</queries> </queries>
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
<intent>
<action android:name="colordict.intent.action.SEARCH" />
</intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="text/plain" />
</intent>
<package android:name="it.t_arn.aard2" />
<package android:name="io.github.mvasilev.dictionary" />
<package android:name="com.github.tngande.ossdict" />
<package android:name="com.gaurav.lookup" />
<package android:name="com.randallbaltazar.lookup" />
</queries>
<application <application
android:name=".MyApplication" android:name=".MyApplication"
android:allowBackup="true" android:allowBackup="true"

View file

@ -104,6 +104,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager 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.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
@ -531,7 +532,8 @@ fun AiDefinitionPopup(
result: AiDefinitionResult?, result: AiDefinitionResult?,
isLoading: Boolean, isLoading: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
isMainTtsActive: Boolean = false isMainTtsActive: Boolean = false,
onOpenExternalDictionary: () -> Unit
) { ) {
val ttsController = rememberTtsController() val ttsController = rememberTtsController()
val ttsState by ttsController.ttsState.collectAsState() val ttsState by ttsController.ttsState.collectAsState()
@ -642,6 +644,13 @@ fun AiDefinitionPopup(
contentDescription = "Copy" contentDescription = "Copy"
) )
} }
Spacer(modifier = Modifier.width(8.dp))
IconButton(onClick = onOpenExternalDictionary) {
Icon(
painter = painterResource(id = R.drawable.dictionary),
contentDescription = "Open in Dictionary App"
)
}
} }
} }

View file

@ -975,7 +975,7 @@ fun ChapterWebView(
} }
// 3. Dictionary Option (Preserving Logic) // 3. Dictionary Option (Preserving Logic)
if (!isOss && state.selectedText.length <= 2000) { if (state.selectedText.length <= 2000) {
HorizontalDivider() HorizontalDivider()
Row( Row(
modifier = Modifier modifier = Modifier
@ -983,12 +983,7 @@ fun ChapterWebView(
.clickable { .clickable {
val textToDefine = state.selectedText val textToDefine = state.selectedText
if (textToDefine.isNotBlank()) { if (textToDefine.isNotBlank()) {
val wordCount = countWords(textToDefine)
if (isProUser || wordCount <= 1) {
onWordSelectedForAiDefinition(textToDefine) onWordSelectedForAiDefinition(textToDefine)
} else {
onShowDictionaryUpsellDialog()
}
} }
customMenuState = null customMenuState = null
} }

View file

@ -0,0 +1,299 @@
// DictionarySettingsDialog.kt
package com.aryan.reader.epubreader
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.toBitmap
import com.aryan.reader.BuildConfig
import com.aryan.reader.R
@Suppress("KotlinConstantConditions")
@Composable
fun DictionarySettingsDialog(
isVisible: Boolean,
onDismiss: () -> Unit,
isProUser: Boolean,
useOnlineDictionary: Boolean,
onToggleOnlineDictionary: (Boolean) -> Unit,
selectedPackageName: String?,
onSelectPackage: (String) -> Unit
) {
if (!isVisible) return
val context = LocalContext.current
var availableApps by remember { mutableStateOf<List<ExternalDictionaryApp>>(emptyList()) }
LaunchedEffect(Unit) {
availableApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
}
Dialog(onDismissRequest = onDismiss) {
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.surface,
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 24.dp)
) {
// Header
Text(
text = "Dictionary Settings",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 16.dp)
)
if (BuildConfig.FLAVOR != "oss") {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
Surface(
shape = RoundedCornerShape(12.dp),
color = if (useOnlineDictionary) MaterialTheme.colorScheme.primaryContainer else Color.Transparent,
border = BorderStroke(
1.dp,
if (useOnlineDictionary) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant
),
modifier = Modifier
.fillMaxWidth()
.clickable { onToggleOnlineDictionary(true) }
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(id = R.drawable.ai),
contentDescription = null,
tint = if (useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = "AI Smart Dictionary",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = if (useOnlineDictionary) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface
)
Text(
text = "Contextual 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,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(bottom = 8.dp)
)
} else {
// OSS FLAVOR UI (Dedicated to external apps)
Surface(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.padding(bottom = 16.dp)
) {
Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(
painter = painterResource(id = R.drawable.dictionary),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(32.dp)
)
Spacer(Modifier.width(12.dp))
Text(
text = "Choose an external app to define selected words.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
}
}
// App List
if (availableApps.isEmpty()) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
"No supported dictionary apps found.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center
)
}
} else {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 300.dp) // Bound height so it doesn't take over screen
.background(MaterialTheme.colorScheme.surfaceContainerLowest, RoundedCornerShape(12.dp))
) {
items(availableApps) { app ->
val isSelected = app.packageName == selectedPackageName
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onSelectPackage(app.packageName) }
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Dynamic Icon handling
if (app.packageName == ExternalDictionaryHelper.GOOGLE_SEARCH_PKG) {
Box(
modifier = Modifier
.size(40.dp)
.background(MaterialTheme.colorScheme.secondaryContainer, RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Search,
contentDescription = "Search",
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(24.dp)
)
}
} else if (app.icon != null) {
Image(
bitmap = app.icon.toBitmap().asImageBitmap(),
contentDescription = null,
modifier = Modifier.size(40.dp)
)
} else {
Box(
modifier = Modifier
.size(40.dp)
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = app.label,
style = MaterialTheme.typography.bodyLarge,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
modifier = Modifier.weight(1f)
)
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Selected",
tint = MaterialTheme.colorScheme.primary
)
}
}
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f))
}
}
}
}
}
}
}

View file

@ -227,21 +227,16 @@ suspend fun executeRecapLogic(
*/ */
@Composable @Composable
fun EpubReaderAiOverlays( fun EpubReaderAiOverlays(
// Summarization State
showSummarizationPopup: Boolean, showSummarizationPopup: Boolean,
summarizationResult: SummarizationResult?, summarizationResult: SummarizationResult?,
isSummarizationLoading: Boolean, isSummarizationLoading: Boolean,
onDismissSummarization: () -> Unit, onDismissSummarization: () -> Unit,
showSummarizationUpsellDialog: Boolean, showSummarizationUpsellDialog: Boolean,
onDismissSummarizationUpsell: () -> Unit, onDismissSummarizationUpsell: () -> Unit,
// Recap State
showRecapPopup: Boolean, showRecapPopup: Boolean,
recapResult: SummarizationResult?, recapResult: SummarizationResult?,
isRecapLoading: Boolean, isRecapLoading: Boolean,
onDismissRecap: () -> Unit, onDismissRecap: () -> Unit,
// Dictionary State
showAiDefinitionPopup: Boolean, showAiDefinitionPopup: Boolean,
selectedTextForAi: String?, selectedTextForAi: String?,
aiDefinitionResult: AiDefinitionResult?, aiDefinitionResult: AiDefinitionResult?,
@ -249,10 +244,9 @@ fun EpubReaderAiOverlays(
onDismissAiDefinition: () -> Unit, onDismissAiDefinition: () -> Unit,
showDictionaryUpsellDialog: Boolean, showDictionaryUpsellDialog: Boolean,
onDismissDictionaryUpsell: () -> Unit, onDismissDictionaryUpsell: () -> Unit,
// Navigation
onNavigateToPro: () -> Unit, onNavigateToPro: () -> Unit,
isTtsSessionActive: Boolean isTtsSessionActive: Boolean,
onOpenExternalDictionary: (String) -> Unit
) { ) {
if (showSummarizationPopup) { if (showSummarizationPopup) {
SummarizationPopup( SummarizationPopup(
@ -298,7 +292,11 @@ fun EpubReaderAiOverlays(
result = aiDefinitionResult, result = aiDefinitionResult,
isLoading = isAiDefinitionLoading, isLoading = isAiDefinitionLoading,
onDismiss = onDismissAiDefinition, onDismiss = onDismissAiDefinition,
isMainTtsActive = isTtsSessionActive isMainTtsActive = isTtsSessionActive,
// Pass it down
onOpenExternalDictionary = {
selectedTextForAi?.let { text -> onOpenExternalDictionary(text) }
}
) )
} }

View file

@ -67,6 +67,7 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
@ -148,6 +149,7 @@ fun EpubReaderTopBar(
onStartAutoScroll: () -> Unit, onStartAutoScroll: () -> Unit,
onOpenTtsSettings: () -> Unit, onOpenTtsSettings: () -> Unit,
onOpenDeviceVoiceSettings: () -> Unit, onOpenDeviceVoiceSettings: () -> Unit,
onOpenDictionarySettings: () -> Unit,
searchFocusRequester: androidx.compose.ui.focus.FocusRequester, searchFocusRequester: androidx.compose.ui.focus.FocusRequester,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onToggleReflow: (() -> Unit)? = null, onToggleReflow: (() -> Unit)? = null,
@ -190,6 +192,12 @@ fun EpubReaderTopBar(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
IconButton(onClick = onOpenDictionarySettings) {
Icon(
painter = painterResource(id = R.drawable.dictionary),
contentDescription = "Dictionary Settings"
)
}
Box { Box {
var showMoreMenu by remember { mutableStateOf(false) } var showMoreMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMoreMenu = true }) { IconButton(onClick = { showMoreMenu = true }) {

View file

@ -32,6 +32,7 @@ import android.media.AudioManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -280,6 +281,30 @@ private fun saveTtsMode(context: Context, modeName: String) {
prefs.edit { putString(TTS_MODE_KEY, modeName) } prefs.edit { putString(TTS_MODE_KEY, modeName) }
} }
private const val PREF_USE_ONLINE_DICT = "use_online_dictionary"
private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
private fun loadUseOnlineDict(context: Context): Boolean {
if (BuildConfig.FLAVOR == "oss") return false
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(PREF_USE_ONLINE_DICT, true)
}
private fun saveUseOnlineDict(context: Context, useOnline: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(PREF_USE_ONLINE_DICT, useOnline) }
}
private fun loadExternalDictPackage(context: Context): String? {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getString(PREF_EXTERNAL_DICT_PKG, null)
}
private fun saveExternalDictPackage(context: Context, packageName: String) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putString(PREF_EXTERNAL_DICT_PKG, packageName) }
}
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@Composable @Composable
fun EpubReaderScreen( fun EpubReaderScreen(
@ -334,6 +359,7 @@ fun EpubReaderScreen(
) )
} }
@Suppress("ControlFlowWithEmptyBody")
@SuppressLint("UnusedBoxWithConstraintsScope", "ObsoleteSdkInt") @SuppressLint("UnusedBoxWithConstraintsScope", "ObsoleteSdkInt")
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
@ -516,6 +542,61 @@ fun EpubReaderHost(
saveHighlightsToPrefs(context, epubBook.title, userHighlights) saveHighlightsToPrefs(context, epubBook.title, userHighlights)
} }
// Dictionary
var showAiDefinitionPopup by remember { mutableStateOf(false) }
var selectedTextForAi by remember { mutableStateOf<String?>(null) }
var aiDefinitionResult by remember { mutableStateOf<AiDefinitionResult?>(null) }
var isAiDefinitionLoading by remember { mutableStateOf(false) }
var showDictionarySettingsSheet by remember { mutableStateOf(false) }
var useOnlineDictionary by remember {
mutableStateOf(loadUseOnlineDict(context))
}
var selectedDictPackage by remember {
mutableStateOf(loadExternalDictPackage(context))
}
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
val onDictionaryLookup = { word: String ->
val isOss = BuildConfig.FLAVOR == "oss"
val effectiveUseOnline = !isOss && useOnlineDictionary
if (effectiveUseOnline) {
val wordCount = countWords(word)
if (isProUser || wordCount <= 1) {
selectedTextForAi = word
showAiDefinitionPopup = true
scope.launch {
isAiDefinitionLoading = true
aiDefinitionResult = null
fetchAiDefinition(
text = word,
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult(definition = currentDefinition + chunk)
},
onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error)
},
onFinish = { isAiDefinitionLoading = false }
)
}
} else {
showDictionaryUpsellDialog = true
}
} else {
if (selectedDictPackage != null) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, word)
} else {
Toast.makeText(context, "Please select a dictionary 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) }
@ -585,17 +666,9 @@ fun EpubReaderHost(
var webViewRefForTts by remember { mutableStateOf<WebView?>(null) } var webViewRefForTts by remember { mutableStateOf<WebView?>(null) }
// Dictionary
var showAiDefinitionPopup by remember { mutableStateOf(false) }
var selectedTextForAi by remember { mutableStateOf<String?>(null) }
var aiDefinitionResult by remember { mutableStateOf<AiDefinitionResult?>(null) }
var isAiDefinitionLoading by remember { mutableStateOf(false) }
var showSummarizationPopup by remember { mutableStateOf(false) } var showSummarizationPopup by remember { mutableStateOf(false) }
var summarizationResult by remember { mutableStateOf<SummarizationResult?>(null) } var summarizationResult by remember { mutableStateOf<SummarizationResult?>(null) }
var isSummarizationLoading by remember { mutableStateOf(false) } var isSummarizationLoading by remember { mutableStateOf(false) }
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
val epubSearcher = remember(epubBook) { createEpubSearcher(epubBook) } val epubSearcher = remember(epubBook) { createEpubSearcher(epubBook) }
@ -2101,32 +2174,7 @@ fun EpubReaderHost(
} }
}, },
onWordSelectedForAiDefinition = { text -> onWordSelectedForAiDefinition = { text ->
val wordCount = countWords(text) onDictionaryLookup(text)
if (isProUser || wordCount <= 1) {
Timber.d("Text selected for AI definition: $text"
)
selectedTextForAi = text
showAiDefinitionPopup = true
scope.launch {
isAiDefinitionLoading = true
aiDefinitionResult = null
fetchAiDefinition(
text = text,
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult(definition = currentDefinition + chunk)
},
onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error)
},
onFinish = {
isAiDefinitionLoading = false
}
)
}
} else {
showDictionaryUpsellDialog = true
}
}, },
onContentReadyForSummarization = { content -> onContentReadyForSummarization = { content ->
Timber.d("Content received for summarization") Timber.d("Content received for summarization")
@ -2461,32 +2509,7 @@ fun EpubReaderHost(
showDictionaryUpsellDialog = true showDictionaryUpsellDialog = true
}, },
onWordSelectedForAiDefinition = { text -> onWordSelectedForAiDefinition = { text ->
val wordCount = countWords(text) onDictionaryLookup(text)
if (isProUser || wordCount <= 1) {
Timber.d("Text selected for AI definition: $text"
)
selectedTextForAi = text
showAiDefinitionPopup = true
scope.launch {
isAiDefinitionLoading = true
aiDefinitionResult = null
fetchAiDefinition(
text = text,
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult(definition = currentDefinition + chunk)
},
onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error)
},
onFinish = {
isAiDefinitionLoading = false
}
)
}
} else {
showDictionaryUpsellDialog = true
}
}, },
userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) }, userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) },
onHighlightCreated = { cfi, text, colorId -> onHighlightCreated = { cfi, text, colorId ->
@ -2992,6 +3015,7 @@ fun EpubReaderHost(
searchFocusRequester = searchFocusRequester, searchFocusRequester = searchFocusRequester,
modifier = Modifier.align(Alignment.TopCenter), modifier = Modifier.align(Alignment.TopCenter),
onOpenTtsSettings = { showTtsSettingsSheet = true }, onOpenTtsSettings = { showTtsSettingsSheet = true },
onOpenDictionarySettings = { showDictionarySettingsSheet = true },
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true }, onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true },
onToggleReflow = onToggleReflow, onToggleReflow = onToggleReflow,
) )
@ -3322,9 +3346,16 @@ fun EpubReaderHost(
}, },
showDictionaryUpsellDialog = showDictionaryUpsellDialog, showDictionaryUpsellDialog = showDictionaryUpsellDialog,
onDismissDictionaryUpsell = { showDictionaryUpsellDialog = false }, onDismissDictionaryUpsell = { showDictionaryUpsellDialog = false },
onNavigateToPro = onNavigateToPro, onNavigateToPro = onNavigateToPro,
isTtsSessionActive = isTtsSessionActive isTtsSessionActive = isTtsSessionActive,
onOpenExternalDictionary = { text ->
if (selectedDictPackage != null) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} else {
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true
}
}
) )
if (isNavigatingToBookmark) { if (isNavigatingToBookmark) {
@ -3483,6 +3514,24 @@ fun EpubReaderHost(
) )
} }
if (showDictionarySettingsSheet) {
DictionarySettingsDialog(
isVisible = true,
onDismiss = { showDictionarySettingsSheet = false },
isProUser = isProUser,
useOnlineDictionary = useOnlineDictionary,
onToggleOnlineDictionary = { newState ->
useOnlineDictionary = newState
saveUseOnlineDict(context, newState)
},
selectedPackageName = selectedDictPackage,
onSelectPackage = { pkg ->
selectedDictPackage = pkg
saveExternalDictPackage(context, pkg)
}
)
}
if (showDeviceVoiceSettingsSheet) { if (showDeviceVoiceSettingsSheet) {
DeviceVoiceSettingsSheet( DeviceVoiceSettingsSheet(
isVisible = true, isVisible = true,

View file

@ -0,0 +1,154 @@
// ExternalDictionaryHelper.kt
package com.aryan.reader.epubreader
import android.app.SearchManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Build
import android.widget.Toast
import timber.log.Timber
data class ExternalDictionaryApp(
val label: String,
val packageName: String,
val icon: Drawable?
)
object ExternalDictionaryHelper {
const val GOOGLE_SEARCH_PKG = "app.internal.google_search"
private val PACKAGE_BLOCKLIST = setOf(
"com.samsung.android.samsungpassautofill",
"com.samsung.android.samsungpass",
"com.samsung.android.app.pass",
"com.google.android.gms",
"com.truecaller",
"com.adobe.reader",
"com.reddit.frontpage"
)
fun getAvailableDictionaries(context: Context): List<ExternalDictionaryApp> {
val pm = context.packageManager
val apps = mutableListOf<ExternalDictionaryApp>()
val addedPackages = mutableSetOf<String>()
val processTextIntent = Intent(Intent.ACTION_PROCESS_TEXT).setType("text/plain")
val textResolvers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pm.queryIntentActivities(processTextIntent, PackageManager.ResolveInfoFlags.of(0))
} else {
@Suppress("DEPRECATION") pm.queryIntentActivities(processTextIntent, 0)
}
textResolvers.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 colorDictIntent = Intent("colordict.intent.action.SEARCH")
val colorResolvers = pm.queryIntentActivities(colorDictIntent, 0)
colorResolvers.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 sortedApps = apps.sortedBy { it.label }.toMutableList()
// Inject Google Search at the top
sortedApps.add(
0,
ExternalDictionaryApp(
label = "Search",
packageName = GOOGLE_SEARCH_PKG,
icon = null
)
)
return sortedApps
}
fun launchDictionary(context: Context, packageName: String, query: String) {
val pm = context.packageManager
try {
if (packageName == GOOGLE_SEARCH_PKG) {
val searchIntent = Intent(Intent.ACTION_WEB_SEARCH).apply {
putExtra(SearchManager.QUERY, query)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(searchIntent)
return
}
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
}
val dictIntent = Intent("colordict.intent.action.SEARCH").apply {
putExtra("EXTRA_QUERY", query)
setPackage(packageName)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
if (dictIntent.resolveActivity(pm) != null) {
context.startActivity(dictIntent)
return
}
if (packageName == "it.t_arn.aard2") {
val aardIntent = Intent("aard2.lookup").apply {
putExtra("query", query)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(aardIntent)
return
}
launchGenericSend(context, packageName, query)
} catch (e: Exception) {
Timber.e(e, "Failed to launch dictionary app: $packageName")
Toast.makeText(context, "Error opening dictionary", Toast.LENGTH_SHORT).show()
}
}
private fun launchGenericSend(context: Context, packageName: String, query: String) {
val sendIntent = Intent(Intent.ACTION_SEND)
sendIntent.type = "text/plain"
sendIntent.putExtra(Intent.EXTRA_TEXT, query)
sendIntent.setPackage(packageName)
sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
context.startActivity(sendIntent)
} catch (e: Exception) {
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) {
context.startActivity(launchIntent)
} else {
throw e
}
}
}
}

View file

@ -1467,14 +1467,8 @@ internal fun PaginatedReaderContent(
if (isForDictionary) { if (isForDictionary) {
if (!text.isNullOrBlank()) { if (!text.isNullOrBlank()) {
if (isProUser || countWords(text) <= 1) {
if (text.length <= 2000) {
onWordSelectedForAiDefinition(text) onWordSelectedForAiDefinition(text)
} }
} else {
onShowDictionaryUpsellDialog()
}
}
} else if (isForHighlight) { } else if (isForHighlight) {
// Do not copy to real clipboard // Do not copy to real clipboard
} else { } else {
@ -3022,7 +3016,6 @@ private fun PaginatedTextSelectionMenu(
} }
// 5. Dictionary Option // 5. Dictionary Option
if (!isOss) {
HorizontalDivider() HorizontalDivider()
Row( Row(
modifier = Modifier modifier = Modifier
@ -3046,7 +3039,6 @@ private fun PaginatedTextSelectionMenu(
} }
} }
} }
}
} }
@Composable @Composable

View file

@ -129,9 +129,7 @@ internal fun PdfSelectionMenuPopup(
popupPositionProvider: PopupPositionProvider, popupPositionProvider: PopupPositionProvider,
onCopy: (String) -> Unit, onCopy: (String) -> Unit,
onAiDefine: (String) -> Unit, onAiDefine: (String) -> Unit,
onSelectAll: () -> Unit, onSelectAll: () -> Unit
isProUser: Boolean,
onShowUpsellDialog: () -> Unit,
) { ) {
Popup( Popup(
popupPositionProvider = popupPositionProvider, popupPositionProvider = popupPositionProvider,
@ -157,11 +155,7 @@ internal fun PdfSelectionMenuPopup(
} }
if (menuState.selectedText.length <= 2000) { if (menuState.selectedText.length <= 2000) {
TextButton(onClick = { TextButton(onClick = {
if (isProUser || countWords(menuState.selectedText) <= 1) {
onAiDefine(menuState.selectedText) onAiDefine(menuState.selectedText)
} else {
onShowUpsellDialog()
}
}) { }) {
Text("Dictionary") Text("Dictionary")
} }

View file

@ -4557,9 +4557,7 @@ private fun PdfPageRenderer(
popupPositionProvider = popupPositionProvider, popupPositionProvider = popupPositionProvider,
onCopy = onCopy, onCopy = onCopy,
onAiDefine = onAiDefine, onAiDefine = onAiDefine,
onSelectAll = onSelectAll, onSelectAll = onSelectAll
isProUser = isProUser,
onShowUpsellDialog = onShowUpsellDialog
) )
} }
} }

View file

@ -29,14 +29,12 @@ import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.RectF import android.graphics.RectF
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarResult
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.OpenableColumns import android.provider.OpenableColumns
import androidx.compose.foundation.border
import android.util.Base64 import android.util.Base64
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -55,6 +53,7 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
@ -90,6 +89,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.ArrowDownward import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Brush
@ -133,8 +133,10 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
@ -236,7 +238,10 @@ import com.aryan.reader.SearchTopBar
import com.aryan.reader.SummarizationPopup import com.aryan.reader.SummarizationPopup
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.epubreader.AutoScrollControls import com.aryan.reader.epubreader.AutoScrollControls
import com.aryan.reader.epubreader.DictionarySettingsDialog
import com.aryan.reader.epubreader.ExternalDictionaryHelper
import com.aryan.reader.fetchAiDefinition import com.aryan.reader.fetchAiDefinition
import com.aryan.reader.paginatedreader.TtsChunk import com.aryan.reader.paginatedreader.TtsChunk
import com.aryan.reader.pdf.data.AnnotationSettingsRepository import com.aryan.reader.pdf.data.AnnotationSettingsRepository
@ -304,6 +309,29 @@ private const val PDF_AUTO_SCROLL_LOCAL_MAX_PREFIX = "pdf_as_local_max_"
private const val PDF_SCROLL_LOCKED_PREFIX = "pdf_sl_local_" private const val PDF_SCROLL_LOCKED_PREFIX = "pdf_sl_local_"
private const val PDF_FULL_SCREEN_PREFIX = "pdf_fs_local_" 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_EXTERNAL_DICT_PKG = "external_dictionary_package"
private fun loadUseOnlineDict(context: Context): Boolean {
@Suppress("KotlinConstantConditions") if (BuildConfig.FLAVOR == "oss") return false
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(PREF_USE_ONLINE_DICT, true)
}
private fun saveUseOnlineDict(context: Context, useOnline: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putBoolean(PREF_USE_ONLINE_DICT, useOnline) }
}
private fun loadExternalDictPackage(context: Context): String? {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getString(PREF_EXTERNAL_DICT_PKG, null)
}
private fun saveExternalDictPackage(context: Context, packageName: String) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putString(PREF_EXTERNAL_DICT_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)
@ -736,6 +764,10 @@ fun PdfViewerScreen(
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) } var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
var showTtsSettingsSheet by remember { mutableStateOf(false) } var showTtsSettingsSheet by remember { mutableStateOf(false) }
var showDictionarySettingsSheet by remember { mutableStateOf(false) }
var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
var selectedDictPackage by remember { mutableStateOf(loadExternalDictPackage(context)) }
var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) } var showDeviceVoiceSettingsSheet by remember { mutableStateOf(false) }
fun triggerAutoScrollTempPause(durationMs: Long) { fun triggerAutoScrollTempPause(durationMs: Long) {
@ -1840,24 +1872,54 @@ fun PdfViewerScreen(
{ isLoading: Boolean -> isHighlightingLoading = isLoading } { isLoading: Boolean -> isHighlightingLoading = isLoading }
} }
val onShowDictionaryUpsellDialogStable = remember { { showDictionaryUpsellDialog = true } } val onShowDictionaryUpsellDialogStable = remember(useOnlineDictionary) {
{
if (useOnlineDictionary) {
showDictionaryUpsellDialog = true
}
}
}
val onWordSelectedForAiDefinitionStable = remember(isProUser, executeWithOcrCheck) { val onDictionaryLookupStable = remember(isProUser, executeWithOcrCheck, useOnlineDictionary, selectedDictPackage) {
{ text: String -> { text: String ->
executeWithOcrCheck { executeWithOcrCheck {
val isOss = BuildConfig.FLAVOR == "oss"
val effectiveUseOnline = !isOss && useOnlineDictionary
if (effectiveUseOnline) {
val wordCount = countWords(text)
if (isProUser || wordCount <= 1) {
selectedTextForAi = text selectedTextForAi = text
showAiDefinitionPopup = true showAiDefinitionPopup = true
coroutineScope.launch { coroutineScope.launch {
isAiDefinitionLoading = true isAiDefinitionLoading = true
aiDefinitionResult = null aiDefinitionResult = null
fetchAiDefinition(text = text, onUpdate = { chunk -> fetchAiDefinition(
text = text,
onUpdate = { chunk ->
val currentDefinition = aiDefinitionResult?.definition ?: "" val currentDefinition = aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult( aiDefinitionResult = AiDefinitionResult(
definition = currentDefinition + chunk definition = currentDefinition + chunk
) )
}, onError = { error -> },
onError = { error ->
aiDefinitionResult = AiDefinitionResult(error = error) aiDefinitionResult = AiDefinitionResult(error = error)
}, onFinish = { isAiDefinitionLoading = false }) },
onFinish = {
isAiDefinitionLoading = false
}
)
}
} else {
showDictionaryUpsellDialog = true
}
} else {
if (selectedDictPackage != null) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} else {
Toast.makeText(context, "Please select a dictionary app first.", Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true
}
} }
} }
} }
@ -3458,33 +3520,11 @@ fun PdfViewerScreen(
onSingleTap = onSingleTapStable, onSingleTap = onSingleTapStable,
isProUser = isProUser, isProUser = isProUser,
onShowDictionaryUpsellDialog = { onShowDictionaryUpsellDialog = {
if (useOnlineDictionary) {
showDictionaryUpsellDialog = true showDictionaryUpsellDialog = true
},
onWordSelectedForAiDefinition = { text ->
selectedTextForAi = text
showAiDefinitionPopup = true
coroutineScope.launch {
isAiDefinitionLoading = true
aiDefinitionResult = null
fetchAiDefinition(
text = text,
onUpdate = { chunk ->
val currentDefinition =
aiDefinitionResult?.definition ?: ""
aiDefinitionResult = AiDefinitionResult(
definition = currentDefinition + chunk
)
},
onError = { error ->
aiDefinitionResult = AiDefinitionResult(
error = error
)
},
onFinish = {
isAiDefinitionLoading = false
})
} }
}, },
onWordSelectedForAiDefinition = onDictionaryLookupStable,
onOcrStateChange = onOcrStateChange, onOcrStateChange = onOcrStateChange,
onLinkClicked = { url -> clickedLinkUrl = url }, onLinkClicked = { url -> clickedLinkUrl = url },
onInternalLinkClicked = onInternalLinkNav, onInternalLinkClicked = onInternalLinkNav,
@ -3846,7 +3886,7 @@ fun PdfViewerScreen(
searchResultToHighlight = searchHighlightTarget, searchResultToHighlight = searchHighlightTarget,
isProUser = isProUser, isProUser = isProUser,
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable, onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable,
onWordSelectedForAiDefinition = onWordSelectedForAiDefinitionStable, onWordSelectedForAiDefinition = onDictionaryLookupStable,
ttsHighlightData = ttsHighlightData, ttsHighlightData = ttsHighlightData,
ttsReadingPage = ttsPageData?.pageIndex, ttsReadingPage = ttsPageData?.pageIndex,
onLinkClicked = onLinkClickedStable, onLinkClicked = onLinkClickedStable,
@ -3940,7 +3980,7 @@ fun PdfViewerScreen(
} }
if (isMusicianMode && isAutoScrollModeActive) { if (isMusicianMode && isAutoScrollModeActive) {
val density = LocalDensity.current @Suppress("UnusedVariable", "Unused") val density = LocalDensity.current
var leftPulseTrigger by remember { mutableLongStateOf(0L) } var leftPulseTrigger by remember { mutableLongStateOf(0L) }
var rightPulseTrigger by remember { mutableLongStateOf(0L) } var rightPulseTrigger by remember { mutableLongStateOf(0L) }
@ -4345,6 +4385,14 @@ fun PdfViewerScreen(
) )
} }
IconButton(onClick = { showDictionarySettingsSheet = true }) {
Icon(
painter = painterResource(id = R.drawable.dictionary),
contentDescription = "Dictionary Settings",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
IconButton(onClick = { showPenPlayground = true }) { IconButton(onClick = { showPenPlayground = true }) {
Icon( Icon(
@ -4453,11 +4501,7 @@ fun PdfViewerScreen(
showMoreMenu = false showMoreMenu = false
isAutoScrollModeActive = true isAutoScrollModeActive = true
isAutoScrollPlaying = true isAutoScrollPlaying = true
showBars = if (isMusicianMode) { showBars = !isMusicianMode
false
} else {
true
}
} }
) )
@ -5741,7 +5785,17 @@ fun PdfViewerScreen(
selectedTextForAi = null selectedTextForAi = null
aiDefinitionResult = null aiDefinitionResult = null
}, },
isMainTtsActive = isTtsSessionActive isMainTtsActive = isTtsSessionActive,
onOpenExternalDictionary = {
selectedTextForAi?.let { text ->
if (selectedDictPackage != null) {
ExternalDictionaryHelper.launchDictionary(context, selectedDictPackage!!, text)
} else {
Toast.makeText(context, "Select an offline dictionary first.", Toast.LENGTH_SHORT).show()
showDictionarySettingsSheet = true
}
}
}
) )
} }
if (showDictionaryUpsellDialog) { if (showDictionaryUpsellDialog) {
@ -5873,6 +5927,24 @@ fun PdfViewerScreen(
) )
} }
if (showDictionarySettingsSheet) {
DictionarySettingsDialog(
isVisible = true,
onDismiss = { showDictionarySettingsSheet = false },
isProUser = isProUser,
useOnlineDictionary = useOnlineDictionary,
onToggleOnlineDictionary = { newState ->
useOnlineDictionary = newState
saveUseOnlineDict(context, newState)
},
selectedPackageName = selectedDictPackage,
onSelectPackage = { pkg ->
selectedDictPackage = pkg
saveExternalDictPackage(context, pkg)
}
)
}
if (showDeviceVoiceSettingsSheet) { if (showDeviceVoiceSettingsSheet) {
DeviceVoiceSettingsSheet( DeviceVoiceSettingsSheet(
isVisible = true, isVisible = true,