fix: 朗读问题

This commit is contained in:
HapeLee
2026-06-10 21:17:03 +08:00
parent b1f4b207cc
commit 3a8c4af968
5 changed files with 120 additions and 42 deletions
@@ -449,7 +449,10 @@ sealed interface ReadBookIntent {
data class ApplySpeakEnginePerBook(val value: String?) : ReadBookIntent
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookIntent
data class ImportHttpTtsJson(val json: String) : ReadBookIntent
data object ImportHttpTtsFile : ReadBookIntent
data class ImportHttpTtsFileSelected(val uri: Uri) : ReadBookIntent
data object ExportAllHttpTts : ReadBookIntent
data class ExportHttpTtsToFile(val uri: Uri) : ReadBookIntent
data class SetReadAloudIgnoreAudioFocus(val value: Boolean) : ReadBookIntent
data class SetReadAloudPauseOnPhoneCall(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 OpenTitleBarCustomIconPicker(val id: String) : ReadBookEffect
data object OpenSystemTtsSettings : ReadBookEffect
data object OpenHttpTtsImportPicker : ReadBookEffect
data object OpenHttpTtsExportPicker : ReadBookEffect
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect
// Day/night toggle
@@ -771,6 +771,8 @@ class ReadBookController(
is ReadBookEffect.OpenMenuCustomIconPicker,
is ReadBookEffect.OpenTitleBarCustomIconPicker,
is ReadBookEffect.OpenSystemTtsSettings,
is ReadBookEffect.OpenHttpTtsImportPicker,
is ReadBookEffect.OpenHttpTtsExportPicker,
is ReadBookEffect.OpenHttpTtsLogin,
is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson,
@@ -29,9 +29,6 @@ import io.legado.app.constant.AppLog
import io.legado.app.constant.ReadMenuBlurMode
import io.legado.app.help.IntentData
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.read.page.ContentTextView
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.toc.TocActivityResult
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.replace.ReplaceEditRoute
import io.legado.app.ui.replace.ReplaceRuleActivity
import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.takePersistablePermissionSafely
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.flow.onSubscription
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(
StartActivityContract(BookInfoActivity::class.java)
) { result ->
@@ -353,6 +364,18 @@ fun ReadBookRouteScreen(
is ReadBookEffect.TtsCacheCleared -> {
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)
else -> controller.handleEffect(effect)
@@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import android.speech.tts.TextToSpeech
import androidx.lifecycle.viewModelScope
import io.legado.app.BuildConfig
import io.legado.app.R
@@ -133,6 +134,13 @@ class ReadBookViewModel(
private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16)
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())
val readPreferences = _readPreferences.asStateFlow()
@@ -737,10 +745,39 @@ class ReadBookViewModel(
}
is ReadBookIntent.ExportAllHttpTts -> {
_effects.tryEmit(ReadBookEffect.OpenHttpTtsExportPicker)
}
is ReadBookIntent.ExportHttpTtsToFile -> {
execute {
GSON.toJson(appDb.httpTTSDao.all)
}.onSuccess { json ->
_effects.tryEmit(ReadBookEffect.ExportJson(json))
val json = GSON.toJson(appDb.httpTTSDao.all)
context.contentResolver.openOutputStream(intent.uri)?.use { os ->
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 {
buildList {
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 ->
add(
ReadBookTtsEngineItem(
@@ -8,30 +8,21 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
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.Edit
import androidx.compose.material.icons.filled.FileDownload
import androidx.compose.material.icons.filled.MoreVert
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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
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.data.entities.HttpTTS
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
@@ -162,7 +152,6 @@ fun SpeakEngineConfigSheet(
onIntent: (ReadBookIntent) -> Unit,
onDismissRequest: () -> Unit,
) {
val clipboardManager = LocalClipboardManager.current
val items = state.ttsEngineItems
val selectedValue = state.selectedTtsEngine
var pendingEngineValue by remember { mutableStateOf<String?>(null) }
@@ -189,27 +178,41 @@ fun SpeakEngineConfigSheet(
onDismissRequest = onDismissRequest,
title = stringResource(R.string.speak_engine),
startAction = {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
icon = Icons.Default.Add
)
},
endAction = {
var expanded by remember { mutableStateOf(false) }
Box {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
icon = Icons.Default.Add
)
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
onClick = { expanded = true },
icon = Icons.Default.MoreVert
)
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),
) {
items(items) { item ->
val isHttpTts = item.value != null
val httpTtsId = item.value?.toLongOrNull()
val isSelected = item.value == selectedValue
TinyClickableSettingItem(
title = item.title,
@@ -230,18 +233,18 @@ fun SpeakEngineConfigSheet(
null
},
onClick = { pendingEngineValue = item.value },
onLongClick = if (isHttpTts && !item.loginUrl.isNullOrBlank()) {
{ onIntent(ReadBookIntent.OpenHttpTtsLogin(item.value!!.toLong())) }
onLongClick = if (httpTtsId != null && !item.loginUrl.isNullOrBlank()) {
{ onIntent(ReadBookIntent.OpenHttpTtsLogin(httpTtsId)) }
} else null,
trailingContent = if (isHttpTts) {
trailingContent = if (httpTtsId != null) {
{
Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) {
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.EditHttpTts(item.value!!.toLong())) },
onClick = { onIntent(ReadBookIntent.EditHttpTts(httpTtsId)) },
icon = Icons.Default.Edit,
)
SmallTonalButton(
onClick = { onIntent(ReadBookIntent.DeleteHttpTts(item.value!!.toLong())) },
onClick = { onIntent(ReadBookIntent.DeleteHttpTts(httpTtsId)) },
icon = Icons.Default.Delete,
)
}