fix: 朗读引擎长按事件与选择事件

This commit is contained in:
HapeLee committed 2026-06-10 00:42:00 +08:00
1 parent 92614c2ca6
commit 1d47e29a18
8 files changed
+111 -18

No files matched your search

@@ -191,6 +191,7 @@ data class ReadMenuConfig(
data class ReadBookTtsEngineItem( data class ReadBookTtsEngineItem(
val title: String, val title: String,
val value: String?, val value: String?,
val loginUrl: String? = null,
) )
@Immutable @Immutable
@@ -308,6 +309,8 @@ sealed interface ReadBookIntent {
data class OpenSourceEditByUrl(val sourceUrl: String) : ReadBookIntent data class OpenSourceEditByUrl(val sourceUrl: String) : ReadBookIntent
data object OpenBookInfo : ReadBookIntent data object OpenBookInfo : ReadBookIntent
data object OpenChapterList : ReadBookIntent data object OpenChapterList : ReadBookIntent
data object OpenChapterUrl : ReadBookIntent
data class SetReadUrlInBrowser(val useBrowser: Boolean) : ReadBookIntent
// Content edit // Content edit
data object LoadContentEdit : ReadBookIntent data object LoadContentEdit : ReadBookIntent
@@ -422,6 +425,7 @@ sealed interface ReadBookIntent {
data class DeleteHttpTts(val engineId: Long) : ReadBookIntent data class DeleteHttpTts(val engineId: Long) : ReadBookIntent
data class SaveHttpTts(val httpTTS: HttpTTS) : ReadBookIntent data class SaveHttpTts(val httpTTS: HttpTTS) : ReadBookIntent
data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookIntent
data class ImportHttpTtsJson(val json: String) : ReadBookIntent data class ImportHttpTtsJson(val json: String) : ReadBookIntent
data object ExportAllHttpTts : ReadBookIntent data object ExportAllHttpTts : ReadBookIntent
data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent
@@ -558,6 +562,7 @@ sealed interface ReadBookEffect {
data class OpenMenuCustomIconPicker(val id: String) : ReadBookEffect data class OpenMenuCustomIconPicker(val id: String) : ReadBookEffect
data class OpenTitleBarCustomIconPicker(val id: String) : ReadBookEffect data class OpenTitleBarCustomIconPicker(val id: String) : ReadBookEffect
data object OpenSystemTtsSettings : ReadBookEffect data object OpenSystemTtsSettings : ReadBookEffect
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect
// Day/night toggle // Day/night toggle
data object ToggleDayNight : ReadBookEffect data object ToggleDayNight : ReadBookEffect
@@ -626,6 +631,7 @@ sealed interface ReadBookDialog {
data class SureSyncProgress(val progress: BookProgress) : ReadBookDialog data class SureSyncProgress(val progress: BookProgress) : ReadBookDialog
data object ConfirmSkipToChapter : ReadBookDialog data object ConfirmSkipToChapter : ReadBookDialog
data class ConfirmChapterPay(val chapterTitle: String) : ReadBookDialog data class ConfirmChapterPay(val chapterTitle: String) : ReadBookDialog
data object ConfirmReadUrlInBrowser : ReadBookDialog
} }
/** /**
@@ -771,6 +771,7 @@ class ReadBookController(
is ReadBookEffect.OpenMenuCustomIconPicker, is ReadBookEffect.OpenMenuCustomIconPicker,
is ReadBookEffect.OpenTitleBarCustomIconPicker, is ReadBookEffect.OpenTitleBarCustomIconPicker,
is ReadBookEffect.OpenSystemTtsSettings, is ReadBookEffect.OpenSystemTtsSettings,
is ReadBookEffect.OpenHttpTtsLogin,
is ReadBookEffect.TtsCacheCleared, is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson, is ReadBookEffect.ExportJson,
// DB query + bookmark effects — handled by ViewModel, ignored here // DB query + bookmark effects — handled by ViewModel, ignored here
@@ -262,6 +262,14 @@ fun ReadBookRouteScreen(
} }
) )
} }
is ReadBookEffect.OpenHttpTtsLogin -> {
context.startActivity(
Intent(context, SourceLoginActivity::class.java).apply {
putExtra("type", "httpTts")
putExtra("key", effect.engineId.toString())
}
)
}
is ReadBookEffect.OpenWebView -> { is ReadBookEffect.OpenWebView -> {
context.startActivity( context.startActivity(
Intent(context, WebViewActivity::class.java).apply { Intent(context, WebViewActivity::class.java).apply {
@@ -39,7 +39,10 @@ import io.legado.app.help.book.isLocalTxt
import io.legado.app.help.book.isMobi import io.legado.app.help.book.isMobi
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.help.book.simulatedTotalChapterNum import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.constant.AppPattern
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.utils.EncoderUtils
import io.legado.app.utils.openUrl
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.help.source.getSourceType import io.legado.app.help.source.getSourceType
@@ -338,6 +341,11 @@ class ReadBookViewModel(
_effects.tryEmit(ReadBookEffect.OpenChapterList(bookUrl)) _effects.tryEmit(ReadBookEffect.OpenChapterList(bookUrl))
} }
} }
is ReadBookIntent.OpenChapterUrl -> openChapterUrl()
is ReadBookIntent.SetReadUrlInBrowser -> {
AppConfig.readUrlInBrowser = intent.useBrowser
openChapterUrl()
}
is ReadBookIntent.LoadContentEdit -> loadContentEdit() is ReadBookIntent.LoadContentEdit -> loadContentEdit()
is ReadBookIntent.SaveContentEdit -> saveContentEdit(intent.content, intent.saveToSource) is ReadBookIntent.SaveContentEdit -> saveContentEdit(intent.content, intent.saveToSource)
is ReadBookIntent.ResetContentEdit -> resetContentEdit() is ReadBookIntent.ResetContentEdit -> resetContentEdit()
@@ -562,6 +570,7 @@ class ReadBookViewModel(
} }
is ReadBookIntent.ApplySpeakEngine -> { is ReadBookIntent.ApplySpeakEngine -> {
ReadBook.book?.setTtsEngine(null)
AppConfig.ttsEngine = intent.value AppConfig.ttsEngine = intent.value
_uiState.update { _uiState.update {
it.copy( it.copy(
@@ -653,6 +662,10 @@ class ReadBookViewModel(
} }
} }
is ReadBookIntent.OpenHttpTtsLogin -> {
_effects.tryEmit(ReadBookEffect.OpenHttpTtsLogin(intent.engineId))
}
is ReadBookIntent.ImportHttpTtsJson -> { is ReadBookIntent.ImportHttpTtsJson -> {
execute { execute {
HttpTTS.fromJsonArray(intent.json).getOrDefault(arrayListOf()) HttpTTS.fromJsonArray(intent.json).getOrDefault(arrayListOf())
@@ -976,7 +989,13 @@ class ReadBookViewModel(
buildList { buildList {
add(ReadBookTtsEngineItem(context.getString(R.string.system_tts), null)) add(ReadBookTtsEngineItem(context.getString(R.string.system_tts), null))
appDb.httpTTSDao.all.forEach { httpTts -> appDb.httpTTSDao.all.forEach { httpTts ->
add(ReadBookTtsEngineItem(httpTts.name, httpTts.id.toString())) add(
ReadBookTtsEngineItem(
title = httpTts.name,
value = httpTts.id.toString(),
loginUrl = httpTts.loginUrl,
)
)
} }
} }
}.onSuccess { items -> }.onSuccess { items ->
@@ -2947,6 +2966,51 @@ class ReadBookViewModel(
} }
} }
private fun openChapterUrl() {
val book = ReadBook.book ?: return
if (book.isLocal) return
val chapter = ReadBook.curTextChapter?.chapter
?: appDb.bookChapterDao.getChapter(book.bookUrl, ReadBook.durChapterIndex)
?: return
val rawUrl = chapter.getAbsoluteURL()
if (rawUrl.isBlank()) return
val url = resolveChapterUrl(rawUrl)
if (url.isBlank()) {
context.toastOnUi(R.string.cannot_open_url)
return
}
if (AppConfig.readUrlInBrowser) {
context.openUrl(url.substringBefore(",{"))
} else {
_effects.tryEmit(
ReadBookEffect.OpenWebView(
title = chapter.title,
url = url,
sourceOrigin = ReadBook.bookSource?.bookSourceUrl,
sourceName = ReadBook.bookSource?.bookSourceName,
sourceType = ReadBook.bookSource?.getSourceType(),
)
)
}
}
/**
* 解析章节URL,处理data:;base64,...格式
* 从base64编码的JSON中提取真实URL
*/
private fun resolveChapterUrl(url: String): String {
if (!url.startsWith("data:", ignoreCase = true)) return url
val match = AppPattern.dataUriRegex.matchEntire(url) ?: return url
return try {
val decoded = EncoderUtils.base64Decode(match.groupValues[1])
val json = GSON.fromJsonObject<Map<String, Any>>(decoded).getOrNull()
val realUrl = json?.get("url")?.toString() ?: ""
realUrl.ifBlank { url }
} catch (_: Exception) {
url
}
}
private fun showPayDialog() { private fun showPayDialog() {
val book = ReadBook.book ?: return val book = ReadBook.book ?: return
if (book.isLocal) return if (book.isLocal) return
@@ -43,6 +43,7 @@ import io.legado.app.ui.book.read.ReadBookIntent
import io.legado.app.ui.book.read.ReadBookTtsEngineItem import io.legado.app.ui.book.read.ReadBookTtsEngineItem
import io.legado.app.ui.book.read.ReadBookUiState import io.legado.app.ui.book.read.ReadBookUiState
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.series.SmallTonalButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -164,6 +165,24 @@ fun SpeakEngineConfigSheet(
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
val items = state.ttsEngineItems val items = state.ttsEngineItems
val selectedValue = state.selectedTtsEngine val selectedValue = state.selectedTtsEngine
var pendingEngineValue by remember { mutableStateOf<String?>(null) }
AppAlertDialog(
show = pendingEngineValue != null,
onDismissRequest = { pendingEngineValue = null },
title = stringResource(R.string.speak_engine),
text = stringResource(R.string.speak_engine_apply_scope),
confirmText = stringResource(R.string.general),
onConfirm = {
onIntent(ReadBookIntent.ApplySpeakEngine(pendingEngineValue))
pendingEngineValue = null
},
dismissText = stringResource(R.string.book),
onDismiss = {
onIntent(ReadBookIntent.ApplySpeakEnginePerBook(pendingEngineValue))
pendingEngineValue = null
},
)
AppModalBottomSheet( AppModalBottomSheet(
show = show, show = show,
@@ -177,7 +196,6 @@ fun SpeakEngineConfigSheet(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) }, onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
icon = Icons.Default.Add icon = Icons.Default.Add
) )
//TODO: 导入
SmallTonalButton( SmallTonalButton(
onClick = { onClick = {
clipboardManager.getText()?.text?.let { text -> clipboardManager.getText()?.text?.let { text ->
@@ -194,20 +212,6 @@ fun SpeakEngineConfigSheet(
) )
} }
}, },
endAction = {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ApplySpeakEnginePerBook(selectedValue)) },
text = stringResource(R.string.book)
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ApplySpeakEngine(selectedValue)) },
text = stringResource(R.string.general)
)
}
}
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
@@ -225,7 +229,10 @@ fun SpeakEngineConfigSheet(
} else { } else {
null null
}, },
onClick = { onIntent(ReadBookIntent.ApplySpeakEngine(item.value)) }, onClick = { pendingEngineValue = item.value },
onLongClick = if (isHttpTts && !item.loginUrl.isNullOrBlank()) {
{ onIntent(ReadBookIntent.OpenHttpTtsLogin(item.value!!.toLong())) }
} else null,
trailingContent = if (isHttpTts) { trailingContent = if (isHttpTts) {
{ {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -243,7 +250,6 @@ fun SpeakEngineConfigSheet(
) )
} }
} }
} }
} }
@@ -75,6 +75,7 @@ fun TinySettingItem(
expandContent: (@Composable ColumnScope.() -> Unit)? = null, expandContent: (@Composable ColumnScope.() -> Unit)? = null,
enabled: Boolean = true, enabled: Boolean = true,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) { ) {
val isExpandable = expandContent != null && onExpandChange != null val isExpandable = expandContent != null && onExpandChange != null
val alpha = if (enabled) 1f else 0.5f val alpha = if (enabled) 1f else 0.5f
@@ -88,6 +89,7 @@ fun TinySettingItem(
} }
} }
} else null, } else null,
onLongClick = if (enabled) onLongClick else null,
modifier = modifier modifier = modifier
.padding(bottom = 4.dp) .padding(bottom = 4.dp)
.heightIn(min = 56.dp) .heightIn(min = 56.dp)
@@ -329,6 +331,7 @@ fun TinyClickableSettingItem(
color: Color? = LegadoTheme.colorScheme.surfaceContainerLow, color: Color? = LegadoTheme.colorScheme.surfaceContainerLow,
trailingContent: (@Composable () -> Unit)? = null, trailingContent: (@Composable () -> Unit)? = null,
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) { ) {
TinySettingItem( TinySettingItem(
title = title, title = title,
@@ -345,6 +348,7 @@ fun TinyClickableSettingItem(
) )
}, },
onClick = onClick, onClick = onClick,
onLongClick = onLongClick,
) )
} }
@@ -649,6 +649,7 @@
<string name="brightness_auto">自动亮度 %s</string> <string name="brightness_auto">自动亮度 %s</string>
<string name="read_aloud_by_page">按页朗读</string> <string name="read_aloud_by_page">按页朗读</string>
<string name="speak_engine">朗读引擎</string> <string name="speak_engine">朗读引擎</string>
<string name="speak_engine_apply_scope">选择朗读引擎的应用范围</string>
<string name="bg_image">背景图片</string> <string name="bg_image">背景图片</string>
<string name="bg_color">背景颜色</string> <string name="bg_color">背景颜色</string>
<string name="use_bg_image">使用背景图片</string> <string name="use_bg_image">使用背景图片</string>
@@ -1072,6 +1073,7 @@
<string name="full_screen">全屏</string> <string name="full_screen">全屏</string>
<string name="open_fun">打开方式</string> <string name="open_fun">打开方式</string>
<string name="use_browser_open">是否使用外部浏览器打开?</string> <string name="use_browser_open">是否使用外部浏览器打开?</string>
<string name="cannot_open_url">该URL无法直接打开</string>
<string name="see">查看</string> <string name="see">查看</string>
<string name="open">打开</string> <string name="open">打开</string>
<string name="del_login_header">删除登录头</string> <string name="del_login_header">删除登录头</string>
+2
View File
@@ -670,6 +670,7 @@
<string name="brightness_auto">Auto-Brightness %s</string> <string name="brightness_auto">Auto-Brightness %s</string>
<string name="read_aloud_by_page">Speak by pages</string> <string name="read_aloud_by_page">Speak by pages</string>
<string name="speak_engine">Speak Engine</string> <string name="speak_engine">Speak Engine</string>
<string name="speak_engine_apply_scope">Choose where to apply the selected speak engine</string>
<string name="bg_image">Background Images</string> <string name="bg_image">Background Images</string>
<string name="bg_color">Background color</string> <string name="bg_color">Background color</string>
<string name="use_bg_image">Use Background Image</string> <string name="use_bg_image">Use Background Image</string>
@@ -1101,6 +1102,7 @@
<string name="full_screen">full screen</string> <string name="full_screen">full screen</string>
<string name="open_fun">open function</string> <string name="open_fun">open function</string>
<string name="use_browser_open">Is it opened in an external browser?</string> <string name="use_browser_open">Is it opened in an external browser?</string>
<string name="cannot_open_url">This URL cannot be opened directly</string>
<string name="see">see</string> <string name="see">see</string>
<string name="open">open</string> <string name="open">open</string>
<string name="del_login_header">del login header</string> <string name="del_login_header">del login header</string>