fix: 朗读问题
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user