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