fix: 朗读问题

This commit is contained in:
HapeLee committed 2026-06-10 21:17:03 +08:00
1 parent b1f4b207cc
commit 3a8c4af968
5 files changed
+120 -42

No files matched your search

@@ -449,7 +449,10 @@ sealed interface ReadBookIntent {
data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookIntent data class OpenHttpTtsLogin(val engineId: Long) : ReadBookIntent
data class ImportHttpTtsJson(val json: String) : ReadBookIntent data class ImportHttpTtsJson(val json: String) : ReadBookIntent
data object ImportHttpTtsFile : ReadBookIntent
data class ImportHttpTtsFileSelected(val uri: Uri) : ReadBookIntent
data object ExportAllHttpTts : ReadBookIntent data object ExportAllHttpTts : ReadBookIntent
data class ExportHttpTtsToFile(val uri: Uri) : ReadBookIntent
data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent
data class SetReadAloudPauseOnPhoneCall(val value: Boolean) : ReadBookIntent data class SetReadAloudPauseOnPhoneCall(val value: Boolean) : ReadBookIntent
data class SetReadAloudWakeLock(val value: Boolean) : ReadBookIntent data class SetReadAloudWakeLock(val value: Boolean) : ReadBookIntent
@@ -585,6 +588,8 @@ 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 object OpenHttpTtsImportPicker : ReadBookEffect
data object OpenHttpTtsExportPicker : ReadBookEffect
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect
// Day/night toggle // Day/night toggle
@@ -771,6 +771,8 @@ class ReadBookController(
is ReadBookEffect.OpenMenuCustomIconPicker, is ReadBookEffect.OpenMenuCustomIconPicker,
is ReadBookEffect.OpenTitleBarCustomIconPicker, is ReadBookEffect.OpenTitleBarCustomIconPicker,
is ReadBookEffect.OpenSystemTtsSettings, is ReadBookEffect.OpenSystemTtsSettings,
is ReadBookEffect.OpenHttpTtsImportPicker,
is ReadBookEffect.OpenHttpTtsExportPicker,
is ReadBookEffect.OpenHttpTtsLogin, is ReadBookEffect.OpenHttpTtsLogin,
is ReadBookEffect.TtsCacheCleared, is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson, is ReadBookEffect.ExportJson,
@@ -29,9 +29,6 @@ import io.legado.app.constant.AppLog
import io.legado.app.constant.ReadMenuBlurMode import io.legado.app.constant.ReadMenuBlurMode
import io.legado.app.help.IntentData import io.legado.app.help.IntentData
import io.legado.app.help.IntentHelp import io.legado.app.help.IntentHelp
import io.legado.app.model.ReadBook
import io.legado.app.utils.toastOnUi
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.info.BookInfoActivity
import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.ReadView import io.legado.app.ui.book.read.page.ReadView
@@ -41,11 +38,13 @@ import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.toc.TocActivityResult import io.legado.app.ui.book.toc.TocActivityResult
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
import io.legado.app.ui.browser.WebViewActivity
import io.legado.app.ui.login.SourceLoginActivity import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.replace.ReplaceEditRoute import io.legado.app.ui.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleActivity import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.takePersistablePermissionSafely import io.legado.app.utils.takePersistablePermissionSafely
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.flow.onSubscription import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -223,6 +222,18 @@ fun ReadBookRouteScreen(
} }
} }
val importHttpTtsPicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { viewModel.onIntent(ReadBookIntent.ImportHttpTtsFileSelected(it)) }
}
val exportHttpTtsPicker = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) { uri ->
uri?.let { viewModel.onIntent(ReadBookIntent.ExportHttpTtsToFile(it)) }
}
val bookInfoLauncher = rememberLauncherForActivityResult( val bookInfoLauncher = rememberLauncherForActivityResult(
StartActivityContract(BookInfoActivity::class.java) StartActivityContract(BookInfoActivity::class.java)
) { result -> ) { result ->
@@ -353,6 +364,18 @@ fun ReadBookRouteScreen(
is ReadBookEffect.TtsCacheCleared -> { is ReadBookEffect.TtsCacheCleared -> {
context.toastOnUi(effect.message) context.toastOnUi(effect.message)
} }
is ReadBookEffect.OpenHttpTtsImportPicker -> {
importHttpTtsPicker.launch(
arrayOf(
"application/json",
"text/plain"
)
)
}
is ReadBookEffect.OpenHttpTtsExportPicker -> {
exportHttpTtsPicker.launch("httpTTS.json")
}
// All other effects — delegate to bridge (View/Window/Activity operations) // All other effects — delegate to bridge (View/Window/Activity operations)
else -> controller.handleEffect(effect) else -> controller.handleEffect(effect)
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.speech.tts.TextToSpeech
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.BuildConfig import io.legado.app.BuildConfig
import io.legado.app.R import io.legado.app.R
@@ -133,6 +134,13 @@ class ReadBookViewModel(
private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16) private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16)
val effects = _effects.asSharedFlow() val effects = _effects.asSharedFlow()
private val sysEngines: List<TextToSpeech.EngineInfo> by lazy {
val tts = TextToSpeech(context, null)
val engines = tts.engines
tts.shutdown()
engines
}
private val _readPreferences = MutableStateFlow(ReadPreferences()) private val _readPreferences = MutableStateFlow(ReadPreferences())
val readPreferences = _readPreferences.asStateFlow() val readPreferences = _readPreferences.asStateFlow()
@@ -737,10 +745,39 @@ class ReadBookViewModel(
} }
is ReadBookIntent.ExportAllHttpTts -> { is ReadBookIntent.ExportAllHttpTts -> {
_effects.tryEmit(ReadBookEffect.OpenHttpTtsExportPicker)
}
is ReadBookIntent.ExportHttpTtsToFile -> {
execute { execute {
GSON.toJson(appDb.httpTTSDao.all) val json = GSON.toJson(appDb.httpTTSDao.all)
}.onSuccess { json -> context.contentResolver.openOutputStream(intent.uri)?.use { os ->
_effects.tryEmit(ReadBookEffect.ExportJson(json)) os.write(json.toByteArray())
}
}.onSuccess {
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.export_success)))
}
}
is ReadBookIntent.ImportHttpTtsFile -> {
_effects.tryEmit(ReadBookEffect.OpenHttpTtsImportPicker)
}
is ReadBookIntent.ImportHttpTtsFileSelected -> {
execute {
val text = context.contentResolver.openInputStream(intent.uri)
?.use { it.reader().readText() }
if (!text.isNullOrBlank()) {
HttpTTS.fromJsonArray(text).getOrDefault(arrayListOf())
} else arrayListOf()
}.onSuccess { list ->
if (list.isNotEmpty()) {
execute {
appDb.httpTTSDao.insert(*list.toTypedArray())
}.onSuccess {
loadTtsEngineItems()
}
}
} }
} }
@@ -1032,6 +1069,14 @@ class ReadBookViewModel(
execute { execute {
buildList { buildList {
add(ReadBookTtsEngineItem(context.getString(R.string.system_tts), null)) add(ReadBookTtsEngineItem(context.getString(R.string.system_tts), null))
sysEngines.forEach { engine ->
add(
ReadBookTtsEngineItem(
title = engine.label,
value = GSON.toJson(SelectItem(engine.label, engine.name)),
)
)
}
appDb.httpTTSDao.all.forEach { httpTts -> appDb.httpTTSDao.all.forEach { httpTts ->
add( add(
ReadBookTtsEngineItem( ReadBookTtsEngineItem(
@@ -8,30 +8,21 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -40,7 +31,6 @@ import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.HttpTTS import io.legado.app.data.entities.HttpTTS
import io.legado.app.ui.book.read.ReadBookIntent 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.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.alert.AppAlertDialog
@@ -162,7 +152,6 @@ fun SpeakEngineConfigSheet(
onIntent: (ReadBookIntent) -> Unit, onIntent: (ReadBookIntent) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
) { ) {
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) } var pendingEngineValue by remember { mutableStateOf<String?>(null) }
@@ -189,27 +178,41 @@ fun SpeakEngineConfigSheet(
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.speak_engine), title = stringResource(R.string.speak_engine),
startAction = { startAction = {
Row( SmallTonalButton(
horizontalArrangement = Arrangement.spacedBy(2.dp) onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
) { icon = Icons.Default.Add
)
},
endAction = {
var expanded by remember { mutableStateOf(false) }
Box {
SmallTonalButton( SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) }, onClick = { expanded = true },
icon = Icons.Default.Add icon = Icons.Default.MoreVert
)
SmallTonalButton(
onClick = {
clipboardManager.getText()?.text?.let { text ->
if (text.isNotBlank()) {
onIntent(ReadBookIntent.ImportHttpTtsJson(text))
}
}
},
icon = Icons.Default.Description
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.ExportAllHttpTts) },
icon = Icons.Default.FileDownload
) )
RoundDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
RoundDropdownMenuItem(
text = stringResource(R.string.import_tts),
onClick = {
expanded = false
onIntent(ReadBookIntent.ImportHttpTtsFile)
},
)
RoundDropdownMenuItem(
text = stringResource(R.string.export),
onClick = {
expanded = false
onIntent(ReadBookIntent.ExportAllHttpTts)
},
)
RoundDropdownMenuItem(
text = stringResource(R.string.clear_cache),
onClick = {
expanded = false
onIntent(ReadBookIntent.ClearTtsCache)
},
)
}
} }
}, },
) { ) {
@@ -220,7 +223,7 @@ fun SpeakEngineConfigSheet(
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) { ) {
items(items) { item -> items(items) { item ->
val isHttpTts = item.value != null val httpTtsId = item.value?.toLongOrNull()
val isSelected = item.value == selectedValue val isSelected = item.value == selectedValue
TinyClickableSettingItem( TinyClickableSettingItem(
title = item.title, title = item.title,
@@ -230,18 +233,18 @@ fun SpeakEngineConfigSheet(
null null
}, },
onClick = { pendingEngineValue = item.value }, onClick = { pendingEngineValue = item.value },
onLongClick = if (isHttpTts && !item.loginUrl.isNullOrBlank()) { onLongClick = if (httpTtsId != null && !item.loginUrl.isNullOrBlank()) {
{ onIntent(ReadBookIntent.OpenHttpTtsLogin(item.value!!.toLong())) } { onIntent(ReadBookIntent.OpenHttpTtsLogin(httpTtsId)) }
} else null, } else null,
trailingContent = if (isHttpTts) { trailingContent = if (httpTtsId != null) {
{ {
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
SmallTonalButton( SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts(item.value!!.toLong())) }, onClick = { onIntent(ReadBookIntent.EditHttpTts(httpTtsId)) },
icon = Icons.Default.Edit, icon = Icons.Default.Edit,
) )
SmallTonalButton( SmallTonalButton(
onClick = { onIntent(ReadBookIntent.DeleteHttpTts(item.value!!.toLong())) }, onClick = { onIntent(ReadBookIntent.DeleteHttpTts(httpTtsId)) },
icon = Icons.Default.Delete, icon = Icons.Default.Delete,
) )
} }