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
|
|
@ -32,6 +32,7 @@ import android.media.AudioManager
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.webkit.WebView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -280,6 +281,30 @@ private fun saveTtsMode(context: Context, modeName: String) {
|
|||
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)
|
||||
@Composable
|
||||
fun EpubReaderScreen(
|
||||
|
|
@ -334,6 +359,7 @@ fun EpubReaderScreen(
|
|||
)
|
||||
}
|
||||
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope", "ObsoleteSdkInt")
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
||||
|
|
@ -516,6 +542,61 @@ fun EpubReaderHost(
|
|||
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) }
|
||||
var showRecapPopup by remember { mutableStateOf(false) }
|
||||
var recapResult by remember { mutableStateOf<SummarizationResult?>(null) }
|
||||
|
|
@ -585,17 +666,9 @@ fun EpubReaderHost(
|
|||
|
||||
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 summarizationResult by remember { mutableStateOf<SummarizationResult?>(null) }
|
||||
var isSummarizationLoading by remember { mutableStateOf(false) }
|
||||
var showDictionaryUpsellDialog by remember { mutableStateOf(false) }
|
||||
var showSummarizationUpsellDialog by remember { mutableStateOf(false) }
|
||||
|
||||
val epubSearcher = remember(epubBook) { createEpubSearcher(epubBook) }
|
||||
|
||||
|
|
@ -2101,32 +2174,7 @@ fun EpubReaderHost(
|
|||
}
|
||||
},
|
||||
onWordSelectedForAiDefinition = { text ->
|
||||
val wordCount = countWords(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
|
||||
}
|
||||
onDictionaryLookup(text)
|
||||
},
|
||||
onContentReadyForSummarization = { content ->
|
||||
Timber.d("Content received for summarization")
|
||||
|
|
@ -2461,32 +2509,7 @@ fun EpubReaderHost(
|
|||
showDictionaryUpsellDialog = true
|
||||
},
|
||||
onWordSelectedForAiDefinition = { text ->
|
||||
val wordCount = countWords(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
|
||||
}
|
||||
onDictionaryLookup(text)
|
||||
},
|
||||
userHighlights = userHighlights.filter { it.chapterIndex == (currentChapterInPaginatedMode ?: -1) },
|
||||
onHighlightCreated = { cfi, text, colorId ->
|
||||
|
|
@ -2992,6 +3015,7 @@ fun EpubReaderHost(
|
|||
searchFocusRequester = searchFocusRequester,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
onOpenTtsSettings = { showTtsSettingsSheet = true },
|
||||
onOpenDictionarySettings = { showDictionarySettingsSheet = true },
|
||||
onOpenDeviceVoiceSettings = { showDeviceVoiceSettingsSheet = true },
|
||||
onToggleReflow = onToggleReflow,
|
||||
)
|
||||
|
|
@ -3322,9 +3346,16 @@ fun EpubReaderHost(
|
|||
},
|
||||
showDictionaryUpsellDialog = showDictionaryUpsellDialog,
|
||||
onDismissDictionaryUpsell = { showDictionaryUpsellDialog = false },
|
||||
|
||||
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) {
|
||||
|
|
@ -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) {
|
||||
DeviceVoiceSettingsSheet(
|
||||
isVisible = true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue