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

This commit is contained in:
HapeLee
2026-06-10 00:42:00 +08:00
parent 92614c2ca6
commit 1d47e29a18
8 changed files with 111 additions and 18 deletions
@@ -191,6 +191,7 @@ data class ReadMenuConfig(
data class ReadBookTtsEngineItem(
val title: String,
val value: String?,
val loginUrl: String? = null,
)
@Immutable
@@ -308,6 +309,8 @@ sealed interface ReadBookIntent {
data class OpenSourceEditByUrl(val sourceUrl: String) : ReadBookIntent
data object OpenBookInfo : ReadBookIntent
data object OpenChapterList : ReadBookIntent
data object OpenChapterUrl : ReadBookIntent
data class SetReadUrlInBrowser(val useBrowser: Boolean) : ReadBookIntent
// Content edit
data object LoadContentEdit : ReadBookIntent
@@ -422,6 +425,7 @@ sealed interface ReadBookIntent {
data class DeleteHttpTts(val engineId: Long) : ReadBookIntent
data class SaveHttpTts(val httpTTS: HttpTTS) : ReadBookIntent
data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookIntent
data class ImportHttpTtsJson(val json: String) : ReadBookIntent
data object ExportAllHttpTts : ReadBookIntent
data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent
@@ -558,6 +562,7 @@ sealed interface ReadBookEffect {
data class OpenMenuCustomIconPicker(val id: String) : ReadBookEffect
data class OpenTitleBarCustomIconPicker(val id: String) : ReadBookEffect
data object OpenSystemTtsSettings : ReadBookEffect
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect
// Day/night toggle
data object ToggleDayNight : ReadBookEffect
@@ -626,6 +631,7 @@ sealed interface ReadBookDialog {
data class SureSyncProgress(val progress: BookProgress) : ReadBookDialog
data object ConfirmSkipToChapter : ReadBookDialog
data class ConfirmChapterPay(val chapterTitle: String) : ReadBookDialog
data object ConfirmReadUrlInBrowser : ReadBookDialog
}
/**
@@ -771,6 +771,7 @@ class ReadBookController(
is ReadBookEffect.OpenMenuCustomIconPicker,
is ReadBookEffect.OpenTitleBarCustomIconPicker,
is ReadBookEffect.OpenSystemTtsSettings,
is ReadBookEffect.OpenHttpTtsLogin,
is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson,
// 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 -> {
context.startActivity(
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.removeType
import io.legado.app.help.book.simulatedTotalChapterNum
import io.legado.app.constant.AppPattern
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.coroutine.Coroutine
import io.legado.app.help.source.getSourceType
@@ -338,6 +341,11 @@ class ReadBookViewModel(
_effects.tryEmit(ReadBookEffect.OpenChapterList(bookUrl))
}
}
is ReadBookIntent.OpenChapterUrl -> openChapterUrl()
is ReadBookIntent.SetReadUrlInBrowser -> {
AppConfig.readUrlInBrowser = intent.useBrowser
openChapterUrl()
}
is ReadBookIntent.LoadContentEdit -> loadContentEdit()
is ReadBookIntent.SaveContentEdit -> saveContentEdit(intent.content, intent.saveToSource)
is ReadBookIntent.ResetContentEdit -> resetContentEdit()
@@ -562,6 +570,7 @@ class ReadBookViewModel(
}
is ReadBookIntent.ApplySpeakEngine -> {
ReadBook.book?.setTtsEngine(null)
AppConfig.ttsEngine = intent.value
_uiState.update {
it.copy(
@@ -653,6 +662,10 @@ class ReadBookViewModel(
}
}
is ReadBookIntent.OpenHttpTtsLogin -> {
_effects.tryEmit(ReadBookEffect.OpenHttpTtsLogin(intent.engineId))
}
is ReadBookIntent.ImportHttpTtsJson -> {
execute {
HttpTTS.fromJsonArray(intent.json).getOrDefault(arrayListOf())
@@ -976,7 +989,13 @@ class ReadBookViewModel(
buildList {
add(ReadBookTtsEngineItem(context.getString(R.string.system_tts), null))
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 ->
@@ -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() {
val book = ReadBook.book ?: 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.ReadBookUiState
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.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -164,6 +165,24 @@ fun SpeakEngineConfigSheet(
val clipboardManager = LocalClipboardManager.current
val items = state.ttsEngineItems
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(
show = show,
@@ -177,7 +196,6 @@ fun SpeakEngineConfigSheet(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
icon = Icons.Default.Add
)
//TODO: 导入
SmallTonalButton(
onClick = {
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(
modifier = Modifier
@@ -225,7 +229,10 @@ fun SpeakEngineConfigSheet(
} else {
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) {
{
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
@@ -243,7 +250,6 @@ fun SpeakEngineConfigSheet(
)
}
}
}
}
@@ -75,6 +75,7 @@ fun TinySettingItem(
expandContent: (@Composable ColumnScope.() -> Unit)? = null,
enabled: Boolean = true,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
) {
val isExpandable = expandContent != null && onExpandChange != null
val alpha = if (enabled) 1f else 0.5f
@@ -88,6 +89,7 @@ fun TinySettingItem(
}
}
} else null,
onLongClick = if (enabled) onLongClick else null,
modifier = modifier
.padding(bottom = 4.dp)
.heightIn(min = 56.dp)
@@ -329,6 +331,7 @@ fun TinyClickableSettingItem(
color: Color? = LegadoTheme.colorScheme.surfaceContainerLow,
trailingContent: (@Composable () -> Unit)? = null,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
) {
TinySettingItem(
title = title,
@@ -345,6 +348,7 @@ fun TinyClickableSettingItem(
)
},
onClick = onClick,
onLongClick = onLongClick,
)
}
@@ -649,6 +649,7 @@
<string name="brightness_auto">自动亮度 %s</string>
<string name="read_aloud_by_page">按页朗读</string>
<string name="speak_engine">朗读引擎</string>
<string name="speak_engine_apply_scope">选择朗读引擎的应用范围</string>
<string name="bg_image">背景图片</string>
<string name="bg_color">背景颜色</string>
<string name="use_bg_image">使用背景图片</string>
@@ -1072,6 +1073,7 @@
<string name="full_screen">全屏</string>
<string name="open_fun">打开方式</string>
<string name="use_browser_open">是否使用外部浏览器打开?</string>
<string name="cannot_open_url">该URL无法直接打开</string>
<string name="see">查看</string>
<string name="open">打开</string>
<string name="del_login_header">删除登录头</string>
+2
View File
@@ -670,6 +670,7 @@
<string name="brightness_auto">Auto-Brightness %s</string>
<string name="read_aloud_by_page">Speak by pages</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_color">Background color</string>
<string name="use_bg_image">Use Background Image</string>
@@ -1101,6 +1102,7 @@
<string name="full_screen">full screen</string>
<string name="open_fun">open function</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="open">open</string>
<string name="del_login_header">del login header</string>