Ver Fonte

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

HapeLee há 5 meses atrás
pai
commit
1d47e29a18

+ 6 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -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
 }
 }
 
 
 /**
 /**

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -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

+ 8 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -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 {

+ 65 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -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

+ 23 - 17
app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt

@@ -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(
                 )
                 )
             }
             }
         }
         }
-
     }
     }
 }
 }
 
 

+ 4 - 0
app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt

@@ -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,
     )
     )
 }
 }
 
 

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -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 - 0
app/src/main/res/values/strings.xml

@@ -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>