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.provider.OpenableColumns
import android.speech.tts.TextToSpeech
import android.util.Base64
import androidx.lifecycle.viewModelScope
import io.legado.app.BuildConfig
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppLog
import io.legado.app.constant.AppPattern
import io.legado.app.constant.BookType
import io.legado.app.constant.EventBus
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.readConfig.ReadConfig
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.ImageSaveUtils
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.hexString
import io.legado.app.utils.isAbsUrl
import io.legado.app.utils.isDataUrl
import io.legado.app.utils.isJsonArray
import io.legado.app.utils.isJsonObject
import io.legado.app.utils.isTrue
import io.legado.app.utils.mapParallelSafe
import io.legado.app.utils.openUrl
import io.legado.app.utils.postEvent
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toStringArray
import io.legado.app.utils.toastOnUi
import kotlinx.collections.immutable.toImmutableList
@@ -111,6 +118,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
import java.net.URLEncoder
import kotlin.coroutines.coroutineContext
/**
@@ -181,8 +189,8 @@ class ReadBookViewModel(
fun onIntent(intent: ReadBookIntent) {
when (intent) {
is ReadBookIntent.InitData -> {
justInitData = true
initData(intent.intent)
justInitData = true
}
is ReadBookIntent.InitReadBookConfig -> initReadBookConfig(intent.intent)
is ReadBookIntent.NextPage -> ReadBook.moveToNextPage()
@@ -685,13 +693,7 @@ class ReadBookViewModel(
is ReadBookIntent.TtsProgress -> _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(intent.chapterStart))
is ReadBookIntent.ReadAloudAction -> {
_uiState.update {
it.copy(
speakEngineName = computeSpeakEngineName(),
activeSheet = ReadBookSheet.ReadAloudConfig,
)
}
loadTtsEngineItems()
openReadMenuRoute(ReadBookMenuRoute.ReadAloud)
}
is ReadBookIntent.ConfirmAddCurrentBookToBookshelf -> addCurrentBookToBookshelfAndFinish()
is ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf -> removeCurrentNotShelfBookAndFinish()
@@ -839,9 +841,23 @@ class ReadBookViewModel(
_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 -> {
execute {
val json = GSON.toJson(appDb.httpTTSDao.all)
val json = exportHttpTtsJson()
context.contentResolver.openOutputStream(intent.uri)?.use { os ->
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 -> {
viewModelScope.launch { readAloudSettingsRepository.setIgnoreAudioFocus(intent.value) }
}
@@ -1030,7 +1062,9 @@ class ReadBookViewModel(
// Text action menu
is ReadBookIntent.TextActionAloud -> {
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))
}
}
@@ -1094,7 +1128,7 @@ class ReadBookViewModel(
is ReadBookIntent.OnResume -> handleOnResume()
is ReadBookIntent.OnPause -> handleOnPause()
is ReadBookIntent.OnDispose -> handleOnDispose()
is ReadBookIntent.CloseReadBook -> closeReadBook()
is ReadBookIntent.CloseReadBook -> closeReadBook(intent.keepReadAloud)
is ReadBookIntent.OpenBooksDirPicker -> requestBooksDirPicker(reloadChapterList = false)
is ReadBookIntent.BooksDirSelected -> onBooksDirSelected(intent.uri)
}
@@ -1157,10 +1191,6 @@ class ReadBookViewModel(
private fun handleOnDispose() {
backupJob?.cancel()
ReadBook.cancelPreDownloadTask()
if (BaseReadAloudService.isRun) {
ReadAloud.stop(context)
_uiState.update { it.copy(isReadAloudRunning = false, isReadAloudPaused = false) }
}
}
private fun showSpeakEngineConfig() {
@@ -1203,26 +1233,63 @@ class ReadBookViewModel(
}
private fun importHttpTtsSource(text: String) {
_uiState.update {
it.copy(httpTtsImportState = BaseImportUiState.Loading)
}
execute {
importHttpTtsSourceAwait(text.trim()).also { list ->
if (list.isNotEmpty()) {
appDb.httpTTSDao.insert(*list.toTypedArray())
val list = importHttpTtsSourceAwait(text.trim())
val items = list.map { httpTTS ->
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 (list.isNotEmpty()) {
loadTtsEngineItems()
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.success)))
if (items.isEmpty()) {
throw NoStackTraceException(context.getString(R.string.wrong_format))
}
BaseImportUiState.Success(
source = text,
items = items,
)
}.onSuccess { importState ->
_uiState.update {
it.copy(httpTtsImportState = importState)
}
}.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 suspend fun importHttpTtsSourceAwait(text: String): List<HttpTTS> {
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.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() -> {
val body = okHttpClient.newCallResponseBody {
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 {
val ttsEngine = ReadAloud.ttsEngine
?: return context.getString(R.string.system_tts)
@@ -1578,9 +1730,6 @@ class ReadBookViewModel(
private fun setReadAloudTtsTimer(value: Int) {
val timer = value.coerceIn(0, 180)
viewModelScope.launch {
readAloudSettingsRepository.setTtsTimer(timer)
}
ReadAloud.setTimer(context, timer)
_uiState.update { it.copy(readAloudTtsTimer = timer) }
}
@@ -1858,6 +2007,10 @@ class ReadBookViewModel(
}
}
fun markJustInitData() {
justInitData = true
}
private suspend fun initBook(book: Book) {
val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
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
if (!ReadBook.inBookshelf && book != null && OtherConfig.showAddToShelfAlert) {
_uiState.update {
@@ -2168,10 +2324,19 @@ class ReadBookViewModel(
} else if (!ReadBook.inBookshelf) {
removeCurrentNotShelfBookAndFinish()
} else {
stopReadAloudForClose()
_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() {
val book = ReadBook.book ?: return removeCurrentNotShelfBookAndFinish()
execute {
@@ -2187,6 +2352,7 @@ class ReadBookViewModel(
ReadBook.inBookshelf = true
}.onSuccess {
_uiState.update { it.copy(activeDialog = null) }
stopReadAloudForClose()
_effects.tryEmit(ReadBookEffect.Finish)
}.onError {
AppLog.put("添加书籍到书架失败", it)
@@ -2197,6 +2363,7 @@ class ReadBookViewModel(
private fun removeCurrentNotShelfBookAndFinish() {
_uiState.update { it.copy(activeDialog = null) }
removeFromBookshelf {
stopReadAloudForClose()
_effects.tryEmit(ReadBookEffect.Finish)
}
}
@@ -3786,3 +3953,10 @@ private data class SearchTextPoint(
private fun Int.coerceSearchResultIndex(resultSize: Int): Int {
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,10 +351,12 @@ fun MainActivity.mainEntryProvider(
effectsReady.await()
collectorReady[0] = true
readBookViewModel.initReadBookConfig(readIntent)
readBookViewModel.initData(readIntent)
controller.onRouteInitialized()
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
resumeReader()
readBookViewModel.initData(readIntent) {
readBookViewModel.markJustInitData()
controller.onRouteInitialized()
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
resumeReader()
}
}
}
}