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:
parent
dff236fa7f
commit
76780bff31
12 changed files with 759 additions and 171 deletions
|
|
@ -129,9 +129,7 @@ internal fun PdfSelectionMenuPopup(
|
|||
popupPositionProvider: PopupPositionProvider,
|
||||
onCopy: (String) -> Unit,
|
||||
onAiDefine: (String) -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
isProUser: Boolean,
|
||||
onShowUpsellDialog: () -> Unit,
|
||||
onSelectAll: () -> Unit
|
||||
) {
|
||||
Popup(
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
|
|
@ -157,11 +155,7 @@ internal fun PdfSelectionMenuPopup(
|
|||
}
|
||||
if (menuState.selectedText.length <= 2000) {
|
||||
TextButton(onClick = {
|
||||
if (isProUser || countWords(menuState.selectedText) <= 1) {
|
||||
onAiDefine(menuState.selectedText)
|
||||
} else {
|
||||
onShowUpsellDialog()
|
||||
}
|
||||
onAiDefine(menuState.selectedText)
|
||||
}) {
|
||||
Text("Dictionary")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4557,9 +4557,7 @@ private fun PdfPageRenderer(
|
|||
popupPositionProvider = popupPositionProvider,
|
||||
onCopy = onCopy,
|
||||
onAiDefine = onAiDefine,
|
||||
onSelectAll = onSelectAll,
|
||||
isProUser = isProUser,
|
||||
onShowUpsellDialog = onShowUpsellDialog
|
||||
onSelectAll = onSelectAll
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,12 @@ import android.content.Context
|
|||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.RectF
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.provider.OpenableColumns
|
||||
import androidx.compose.foundation.border
|
||||
import android.util.Base64
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -55,6 +53,7 @@ import androidx.compose.foundation.BorderStroke
|
|||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectDragGestures
|
||||
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.material.icons.Icons
|
||||
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.ArrowUpward
|
||||
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.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SnackbarDuration
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.SnackbarResult
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.TabRow
|
||||
|
|
@ -236,7 +238,10 @@ import com.aryan.reader.SearchTopBar
|
|||
import com.aryan.reader.SummarizationPopup
|
||||
import com.aryan.reader.SummarizationResult
|
||||
import com.aryan.reader.TtsSettingsSheet
|
||||
import com.aryan.reader.countWords
|
||||
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.paginatedreader.TtsChunk
|
||||
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_FULL_SCREEN_PREFIX = "pdf_fs_local_"
|
||||
private const val PDF_MUSICIAN_MODE_KEY = "pdf_musician_mode_enabled"
|
||||
private const val PREF_USE_ONLINE_DICT = "use_online_dictionary"
|
||||
private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
|
||||
|
||||
private 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) {
|
||||
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
|
@ -736,6 +764,10 @@ fun PdfViewerScreen(
|
|||
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
|
||||
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) }
|
||||
|
||||
fun triggerAutoScrollTempPause(durationMs: Long) {
|
||||
|
|
@ -1840,24 +1872,54 @@ fun PdfViewerScreen(
|
|||
{ 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 ->
|
||||
executeWithOcrCheck {
|
||||
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 })
|
||||
val isOss = BuildConfig.FLAVOR == "oss"
|
||||
val effectiveUseOnline = !isOss && useOnlineDictionary
|
||||
|
||||
if (effectiveUseOnline) {
|
||||
val wordCount = countWords(text)
|
||||
if (isProUser || wordCount <= 1) {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
} 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,
|
||||
isProUser = isProUser,
|
||||
onShowDictionaryUpsellDialog = {
|
||||
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
|
||||
})
|
||||
if (useOnlineDictionary) {
|
||||
showDictionaryUpsellDialog = true
|
||||
}
|
||||
},
|
||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||
onOcrStateChange = onOcrStateChange,
|
||||
onLinkClicked = { url -> clickedLinkUrl = url },
|
||||
onInternalLinkClicked = onInternalLinkNav,
|
||||
|
|
@ -3846,7 +3886,7 @@ fun PdfViewerScreen(
|
|||
searchResultToHighlight = searchHighlightTarget,
|
||||
isProUser = isProUser,
|
||||
onShowDictionaryUpsellDialog = onShowDictionaryUpsellDialogStable,
|
||||
onWordSelectedForAiDefinition = onWordSelectedForAiDefinitionStable,
|
||||
onWordSelectedForAiDefinition = onDictionaryLookupStable,
|
||||
ttsHighlightData = ttsHighlightData,
|
||||
ttsReadingPage = ttsPageData?.pageIndex,
|
||||
onLinkClicked = onLinkClickedStable,
|
||||
|
|
@ -3940,7 +3980,7 @@ fun PdfViewerScreen(
|
|||
}
|
||||
|
||||
if (isMusicianMode && isAutoScrollModeActive) {
|
||||
val density = LocalDensity.current
|
||||
@Suppress("UnusedVariable", "Unused") val density = LocalDensity.current
|
||||
|
||||
var leftPulseTrigger 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) {
|
||||
IconButton(onClick = { showPenPlayground = true }) {
|
||||
Icon(
|
||||
|
|
@ -4453,11 +4501,7 @@ fun PdfViewerScreen(
|
|||
showMoreMenu = false
|
||||
isAutoScrollModeActive = true
|
||||
isAutoScrollPlaying = true
|
||||
showBars = if (isMusicianMode) {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
showBars = !isMusicianMode
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -5741,7 +5785,17 @@ fun PdfViewerScreen(
|
|||
selectedTextForAi = 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) {
|
||||
|
|
@ -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) {
|
||||
DeviceVoiceSettingsSheet(
|
||||
isVisible = true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue