fix: 修复 justInitData 在 initData 完成前被设置

This commit is contained in:
HapeLee
2026-06-19 00:20:05 +08:00
parent ad29e5aad2
commit ce5cd3ade7
2 changed files with 206 additions and 30 deletions
@@ -6,12 +6,14 @@ 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 android.speech.tts.TextToSpeech
import android.util.Base64
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
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
@@ -70,6 +72,9 @@ import io.legado.app.ui.book.searchContent.SearchResult
import io.legado.app.ui.config.otherConfig.OtherConfig import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.ui.config.readConfig.ReadConfig import io.legado.app.ui.config.readConfig.ReadConfig
import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
import io.legado.app.ui.widget.components.importComponents.ImportItemWrapper
import io.legado.app.ui.widget.components.importComponents.ImportStatus
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.ImageSaveUtils import io.legado.app.utils.ImageSaveUtils
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
@@ -77,12 +82,14 @@ import io.legado.app.utils.StringUtils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.hexString import io.legado.app.utils.hexString
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isDataUrl
import io.legado.app.utils.isJsonArray import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isTrue import io.legado.app.utils.isTrue
import io.legado.app.utils.mapParallelSafe import io.legado.app.utils.mapParallelSafe
import io.legado.app.utils.openUrl import io.legado.app.utils.openUrl
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toStringArray import io.legado.app.utils.toStringArray
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -111,6 +118,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.net.URLEncoder
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
/** /**
@@ -181,8 +189,8 @@ class ReadBookViewModel(
fun onIntent(intent: ReadBookIntent) { fun onIntent(intent: ReadBookIntent) {
when (intent) { when (intent) {
is ReadBookIntent.InitData -> { is ReadBookIntent.InitData -> {
justInitData = true
initData(intent.intent) initData(intent.intent)
justInitData = true
} }
is ReadBookIntent.InitReadBookConfig -> initReadBookConfig(intent.intent) is ReadBookIntent.InitReadBookConfig -> initReadBookConfig(intent.intent)
is ReadBookIntent.NextPage -> ReadBook.moveToNextPage() is ReadBookIntent.NextPage -> ReadBook.moveToNextPage()
@@ -685,13 +693,7 @@ class ReadBookViewModel(
is ReadBookIntent.TtsProgress -> _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(intent.chapterStart)) is ReadBookIntent.TtsProgress -> _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(intent.chapterStart))
is ReadBookIntent.ReadAloudAction -> { is ReadBookIntent.ReadAloudAction -> {
_uiState.update { openReadMenuRoute(ReadBookMenuRoute.ReadAloud)
it.copy(
speakEngineName = computeSpeakEngineName(),
activeSheet = ReadBookSheet.ReadAloudConfig,
)
}
loadTtsEngineItems()
} }
is ReadBookIntent.ConfirmAddCurrentBookToBookshelf -> addCurrentBookToBookshelfAndFinish() is ReadBookIntent.ConfirmAddCurrentBookToBookshelf -> addCurrentBookToBookshelfAndFinish()
is ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf -> removeCurrentNotShelfBookAndFinish() is ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf -> removeCurrentNotShelfBookAndFinish()
@@ -839,9 +841,23 @@ class ReadBookViewModel(
_effects.tryEmit(ReadBookEffect.OpenHttpTtsExportPicker) _effects.tryEmit(ReadBookEffect.OpenHttpTtsExportPicker)
} }
is ReadBookIntent.ExportAllHttpTtsAsUrl -> {
execute {
val json = exportHttpTtsJson()
val dataUrl = "data:application/json;base64," + Base64.encodeToString(
json.toByteArray(Charsets.UTF_8),
Base64.NO_WRAP
)
"legado://import/httpTTS?src=" + URLEncoder.encode(dataUrl, "UTF-8")
}.onSuccess { url ->
context.sendToClip(url)
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.copy_url)))
}
}
is ReadBookIntent.ExportHttpTtsToFile -> { is ReadBookIntent.ExportHttpTtsToFile -> {
execute { execute {
val json = GSON.toJson(appDb.httpTTSDao.all) val json = exportHttpTtsJson()
context.contentResolver.openOutputStream(intent.uri)?.use { os -> context.contentResolver.openOutputStream(intent.uri)?.use { os ->
os.write(json.toByteArray()) os.write(json.toByteArray())
} }
@@ -864,6 +880,22 @@ class ReadBookViewModel(
} }
} }
ReadBookIntent.CancelHttpTtsImport -> cancelHttpTtsImport()
is ReadBookIntent.ToggleHttpTtsImportSelection -> {
toggleHttpTtsImportSelection(intent.index)
}
is ReadBookIntent.ToggleHttpTtsImportAll -> {
toggleHttpTtsImportAll(intent.isSelected)
}
is ReadBookIntent.UpdateHttpTtsImportItem -> {
updateHttpTtsImportItem(intent.index, intent.httpTTS)
}
ReadBookIntent.SaveImportedHttpTts -> saveImportedHttpTts()
is ReadBookIntent.SetReadAloudIgnoreAudioFocus -> { is ReadBookIntent.SetReadAloudIgnoreAudioFocus -> {
viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) } viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) }
} }
@@ -1030,7 +1062,9 @@ class ReadBookViewModel(
// Text action menu // Text action menu
is ReadBookIntent.TextActionAloud -> { is ReadBookIntent.TextActionAloud -> {
when (ReadConfig.contentSelectSpeakMod) { when (ReadConfig.contentSelectSpeakMod) {
1 -> _effects.tryEmit(ReadBookEffect.TextActionAloudSelect) 1 -> intent.selectStartPos?.let {
_effects.tryEmit(ReadBookEffect.TextActionAloudSelect(it.copy()))
} ?: _effects.tryEmit(ReadBookEffect.TextActionSpeak(intent.text))
else -> _effects.tryEmit(ReadBookEffect.TextActionSpeak(intent.text)) else -> _effects.tryEmit(ReadBookEffect.TextActionSpeak(intent.text))
} }
} }
@@ -1094,7 +1128,7 @@ class ReadBookViewModel(
is ReadBookIntent.OnResume -> handleOnResume() is ReadBookIntent.OnResume -> handleOnResume()
is ReadBookIntent.OnPause -> handleOnPause() is ReadBookIntent.OnPause -> handleOnPause()
is ReadBookIntent.OnDispose -> handleOnDispose() is ReadBookIntent.OnDispose -> handleOnDispose()
is ReadBookIntent.CloseReadBook -> closeReadBook() is ReadBookIntent.CloseReadBook -> closeReadBook(intent.keepReadAloud)
is ReadBookIntent.OpenBooksDirPicker -> requestBooksDirPicker(reloadChapterList = false) is ReadBookIntent.OpenBooksDirPicker -> requestBooksDirPicker(reloadChapterList = false)
is ReadBookIntent.BooksDirSelected -> onBooksDirSelected(intent.uri) is ReadBookIntent.BooksDirSelected -> onBooksDirSelected(intent.uri)
} }
@@ -1157,10 +1191,6 @@ class ReadBookViewModel(
private fun handleOnDispose() { private fun handleOnDispose() {
backupJob?.cancel() backupJob?.cancel()
ReadBook.cancelPreDownloadTask() ReadBook.cancelPreDownloadTask()
if (BaseReadAloudService.isRun) {
ReadAloud.stop(context)
_uiState.update { it.copy(isReadAloudRunning = false, isReadAloudPaused = false) }
}
} }
private fun showSpeakEngineConfig() { private fun showSpeakEngineConfig() {
@@ -1203,26 +1233,63 @@ class ReadBookViewModel(
} }
private fun importHttpTtsSource(text: String) { private fun importHttpTtsSource(text: String) {
_uiState.update {
it.copy(httpTtsImportState = BaseImportUiState.Loading)
}
execute { execute {
importHttpTtsSourceAwait(text.trim()).also { list -> val list = importHttpTtsSourceAwait(text.trim())
if (list.isNotEmpty()) { val items = list.map { httpTTS ->
appDb.httpTTSDao.insert(*list.toTypedArray()) val old = appDb.httpTTSDao.get(httpTTS.id)
val status = when {
old == null -> ImportStatus.New
httpTTS.lastUpdateTime > old.lastUpdateTime -> ImportStatus.Update
else -> ImportStatus.Existing
} }
ImportItemWrapper(
data = httpTTS,
oldData = old,
status = status,
isSelected = status != ImportStatus.Existing,
)
} }
}.onSuccess { list -> if (items.isEmpty()) {
if (list.isNotEmpty()) { throw NoStackTraceException(context.getString(R.string.wrong_format))
loadTtsEngineItems() }
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.success))) BaseImportUiState.Success(
source = text,
items = items,
)
}.onSuccess { importState ->
_uiState.update {
it.copy(httpTtsImportState = importState)
} }
}.onError { }.onError {
AppLog.put("导入朗读引擎失败\n${it.localizedMessage}", it, true) AppLog.put("导入朗读引擎失败\n${it.localizedMessage}", it, true)
_uiState.update { state ->
state.copy(
httpTtsImportState = BaseImportUiState.Error(
it.localizedMessage ?: context.getString(R.string.wrong_format)
)
)
}
} }
} }
private suspend fun importHttpTtsSourceAwait(text: String): List<HttpTTS> { private suspend fun importHttpTtsSourceAwait(text: String): List<HttpTTS> {
return when { return when {
text.isHttpTtsImportUri() -> {
val src = Uri.parse(text).getQueryParameter("src")
?: throw NoStackTraceException(context.getString(R.string.wrong_format))
importHttpTtsSourceAwait(src)
}
text.isJsonObject() -> listOf(HttpTTS.fromJson(text).getOrThrow()) text.isJsonObject() -> listOf(HttpTTS.fromJson(text).getOrThrow())
text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow() text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow()
text.isDataUrl() -> {
val data = AppPattern.dataUriRegex.find(text)?.groupValues?.getOrNull(1)
?: throw NoStackTraceException(context.getString(R.string.wrong_format))
val body = Base64.decode(data, Base64.DEFAULT).toString(Charsets.UTF_8)
importHttpTtsSourceAwait(body)
}
text.isAbsUrl() -> { text.isAbsUrl() -> {
val body = okHttpClient.newCallResponseBody { val body = okHttpClient.newCallResponseBody {
if (text.endsWith("#requestWithoutUA")) { if (text.endsWith("#requestWithoutUA")) {
@@ -1238,6 +1305,91 @@ class ReadBookViewModel(
} }
} }
private fun cancelHttpTtsImport() {
_uiState.update {
it.copy(
httpTtsImportState = BaseImportUiState.Idle,
activeSheet = ReadBookSheet.SpeakEngineConfig,
)
}
}
private fun toggleHttpTtsImportSelection(index: Int) {
val importState = _uiState.value.httpTtsImportState
as? BaseImportUiState.Success<HttpTTS> ?: return
if (index !in importState.items.indices) return
val items = importState.items.toMutableList()
val item = items[index]
items[index] = item.copy(isSelected = !item.isSelected)
_uiState.update {
it.copy(httpTtsImportState = importState.copy(items = items))
}
}
private fun toggleHttpTtsImportAll(isSelected: Boolean) {
val importState = _uiState.value.httpTtsImportState
as? BaseImportUiState.Success<HttpTTS> ?: return
_uiState.update {
it.copy(
httpTtsImportState = importState.copy(
items = importState.items.map { item ->
item.copy(isSelected = isSelected)
}
)
)
}
}
private fun updateHttpTtsImportItem(index: Int, httpTTS: HttpTTS) {
val importState = _uiState.value.httpTtsImportState
as? BaseImportUiState.Success<HttpTTS> ?: return
if (index !in importState.items.indices) return
val items = importState.items.toMutableList()
items[index] = items[index].copy(data = httpTTS)
_uiState.update {
it.copy(
httpTtsImportState = importState.copy(
items = items,
version = importState.version + 1,
)
)
}
}
private fun saveImportedHttpTts() {
val importState = _uiState.value.httpTtsImportState
as? BaseImportUiState.Success<HttpTTS> ?: return
val selected = importState.items
.filter { it.isSelected }
.map { it.data }
if (selected.isEmpty()) return
execute {
appDb.httpTTSDao.insert(*selected.toTypedArray())
}.onSuccess {
loadTtsEngineItems()
_uiState.update {
it.copy(
httpTtsImportState = BaseImportUiState.Idle,
activeSheet = ReadBookSheet.SpeakEngineConfig,
)
}
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.success)))
}.onError {
AppLog.put("保存朗读引擎失败\n${it.localizedMessage}", it, true)
_uiState.update { state ->
state.copy(
httpTtsImportState = BaseImportUiState.Error(
it.localizedMessage ?: context.getString(R.string.wrong_format)
)
)
}
}
}
private fun exportHttpTtsJson(): String {
return GSON.toJson(appDb.httpTTSDao.all)
}
private fun computeSpeakEngineName(): String { private fun computeSpeakEngineName(): String {
val ttsEngine = ReadAloud.ttsEngine val ttsEngine = ReadAloud.ttsEngine
?: return context.getString(R.string.system_tts) ?: return context.getString(R.string.system_tts)
@@ -1578,9 +1730,6 @@ class ReadBookViewModel(
private fun setReadAloudTtsTimer(value: Int) { private fun setReadAloudTtsTimer(value: Int) {
val timer = value.coerceIn(0, 180) val timer = value.coerceIn(0, 180)
viewModelScope.launch {
readAloudSettingsRepository.setTtsTimer(timer)
}
ReadAloud.setTimer(context, timer) ReadAloud.setTimer(context, timer)
_uiState.update { it.copy(readAloudTtsTimer = timer) } _uiState.update { it.copy(readAloudTtsTimer = timer) }
} }
@@ -1858,6 +2007,10 @@ class ReadBookViewModel(
} }
} }
fun markJustInitData() {
justInitData = true
}
private suspend fun initBook(book: Book) { private suspend fun initBook(book: Book) {
val isSameBook = ReadBook.book?.bookUrl == book.bookUrl val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
if (isSameBook) { if (isSameBook) {
@@ -2159,7 +2312,10 @@ class ReadBookViewModel(
} }
} }
private fun closeReadBook() { private var closeReadBookKeepReadAloud = false
private fun closeReadBook(keepReadAloud: Boolean = false) {
closeReadBookKeepReadAloud = keepReadAloud
val book = ReadBook.book val book = ReadBook.book
if (!ReadBook.inBookshelf && book != null && OtherConfig.showAddToShelfAlert) { if (!ReadBook.inBookshelf && book != null && OtherConfig.showAddToShelfAlert) {
_uiState.update { _uiState.update {
@@ -2168,10 +2324,19 @@ class ReadBookViewModel(
} else if (!ReadBook.inBookshelf) { } else if (!ReadBook.inBookshelf) {
removeCurrentNotShelfBookAndFinish() removeCurrentNotShelfBookAndFinish()
} else { } else {
stopReadAloudForClose()
_effects.tryEmit(ReadBookEffect.Finish) _effects.tryEmit(ReadBookEffect.Finish)
} }
} }
private fun stopReadAloudForClose() {
if (closeReadBookKeepReadAloud || !BaseReadAloudService.isRun) {
return
}
ReadAloud.stop(context)
_uiState.update { it.copy(isReadAloudRunning = false, isReadAloudPaused = false) }
}
private fun addCurrentBookToBookshelfAndFinish() { private fun addCurrentBookToBookshelfAndFinish() {
val book = ReadBook.book ?: return removeCurrentNotShelfBookAndFinish() val book = ReadBook.book ?: return removeCurrentNotShelfBookAndFinish()
execute { execute {
@@ -2187,6 +2352,7 @@ class ReadBookViewModel(
ReadBook.inBookshelf = true ReadBook.inBookshelf = true
}.onSuccess { }.onSuccess {
_uiState.update { it.copy(activeDialog = null) } _uiState.update { it.copy(activeDialog = null) }
stopReadAloudForClose()
_effects.tryEmit(ReadBookEffect.Finish) _effects.tryEmit(ReadBookEffect.Finish)
}.onError { }.onError {
AppLog.put("添加书籍到书架失败", it) AppLog.put("添加书籍到书架失败", it)
@@ -2197,6 +2363,7 @@ class ReadBookViewModel(
private fun removeCurrentNotShelfBookAndFinish() { private fun removeCurrentNotShelfBookAndFinish() {
_uiState.update { it.copy(activeDialog = null) } _uiState.update { it.copy(activeDialog = null) }
removeFromBookshelf { removeFromBookshelf {
stopReadAloudForClose()
_effects.tryEmit(ReadBookEffect.Finish) _effects.tryEmit(ReadBookEffect.Finish)
} }
} }
@@ -3786,3 +3953,10 @@ private data class SearchTextPoint(
private fun Int.coerceSearchResultIndex(resultSize: Int): Int { private fun Int.coerceSearchResultIndex(resultSize: Int): Int {
return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1) return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1)
} }
private fun String.isHttpTtsImportUri(): Boolean {
val uri = runCatching { Uri.parse(this) }.getOrNull() ?: return false
return uri.scheme in setOf("legado", "yuedu")
&& uri.host == "import"
&& uri.path.equals("/httpTTS", ignoreCase = true)
}
@@ -351,13 +351,15 @@ fun MainActivity.mainEntryProvider(
effectsReady.await() effectsReady.await()
collectorReady[0] = true collectorReady[0] = true
readBookViewModel.initReadBookConfig(readIntent) readBookViewModel.initReadBookConfig(readIntent)
readBookViewModel.initData(readIntent) readBookViewModel.initData(readIntent) {
readBookViewModel.markJustInitData()
controller.onRouteInitialized() controller.onRouteInitialized()
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) { if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
resumeReader() resumeReader()
} }
} }
} }
}
entry<MainRouteSearchContent> { route -> entry<MainRouteSearchContent> { route ->
val viewModel = koinViewModel<SearchContentViewModel>( val viewModel = koinViewModel<SearchContentViewModel>(