fix: 朗读问题
This commit is contained in:
5 files changed
+115
-37
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(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp)
|
|
||||||
) {
|
|
||||||
SmallTonalButton(
|
SmallTonalButton(
|
||||||
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
|
onClick = { onIntent(ReadBookIntent.EditHttpTts()) },
|
||||||
icon = Icons.Default.Add
|
icon = Icons.Default.Add
|
||||||
)
|
)
|
||||||
SmallTonalButton(
|
|
||||||
onClick = {
|
|
||||||
clipboardManager.getText()?.text?.let { text ->
|
|
||||||
if (text.isNotBlank()) {
|
|
||||||
onIntent(ReadBookIntent.ImportHttpTtsJson(text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
icon = Icons.Default.Description
|
endAction = {
|
||||||
)
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Box {
|
||||||
SmallTonalButton(
|
SmallTonalButton(
|
||||||
onClick = { onIntent(ReadBookIntent.ExportAllHttpTts) },
|
onClick = { expanded = true },
|
||||||
icon = Icons.Default.FileDownload
|
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),
|
.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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user