fix: 修复 justInitData 在 initData 完成前被设置
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user