fix: 朗读引擎长按事件与选择事件
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user