fix: 修复阅读界面交互问题
This commit is contained in:
@@ -82,7 +82,10 @@ class ReadAloudSettingsRepository(
|
||||
suspend fun setTtsTimer(value: Int) {
|
||||
val timer = value.coerceIn(0, 180)
|
||||
ReadConfig.ttsTimer = timer
|
||||
settingsRepository.putInt(PreferKey.ttsTimer, timer)
|
||||
}
|
||||
|
||||
suspend fun saveTtsTimer(value: Int) {
|
||||
settingsRepository.putInt(PreferKey.ttsTimer, value.coerceIn(0, 180))
|
||||
}
|
||||
|
||||
suspend fun setTtsFollowSys(value: Boolean) {
|
||||
|
||||
@@ -133,6 +133,7 @@ data class ReadBookUiState(
|
||||
val contentEditLoading: Boolean = false,
|
||||
val contentEditText: String = "",
|
||||
val contentEditTitle: String = "",
|
||||
val contentEditCursorOffset: Int = 0,
|
||||
val contentEditIsLocalTxt: Boolean = false,
|
||||
val contentEditSaveToSource: Boolean = false,
|
||||
val ttsEngineItems: ImmutableList<ReadBookTtsEngineItem> = persistentListOf(),
|
||||
@@ -446,6 +447,8 @@ sealed interface ReadBookIntent {
|
||||
|
||||
// Dialog callback bridge
|
||||
data object ReadAloudAction : ReadBookIntent
|
||||
data object ConfirmAddCurrentBookToBookshelf : ReadBookIntent
|
||||
data object ExitWithoutAddingCurrentBookToBookshelf : ReadBookIntent
|
||||
|
||||
// Read aloud config (needs Activity for DialogFragment)
|
||||
data object ShowReadAloudConfig : ReadBookIntent
|
||||
@@ -461,6 +464,7 @@ 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 class ImportHttpTtsSource(val text: String) : ReadBookIntent
|
||||
data object ImportHttpTtsFile : ReadBookIntent
|
||||
data class ImportHttpTtsFileSelected(val uri: Uri) : ReadBookIntent
|
||||
data object ExportAllHttpTts : ReadBookIntent
|
||||
@@ -479,6 +483,7 @@ sealed interface ReadBookIntent {
|
||||
data object ReadAloudPrevChapter : ReadBookIntent
|
||||
data object ReadAloudNextChapter : ReadBookIntent
|
||||
data class SetReadAloudTtsTimer(val value: Int) : ReadBookIntent
|
||||
data class SaveReadAloudTtsTimer(val value: Int) : ReadBookIntent
|
||||
data class SetReadAloudTtsFollowSys(val value: Boolean) : ReadBookIntent
|
||||
data class SetReadAloudTtsSpeechRate(val value: Int) : ReadBookIntent
|
||||
data object OpenSystemTtsSettings : ReadBookIntent
|
||||
@@ -688,6 +693,7 @@ sealed interface ReadBookDialog {
|
||||
data object RestoreLastBookProgress : ReadBookDialog
|
||||
data object ConfirmSkipToChapter : ReadBookDialog
|
||||
data class ConfirmChapterPay(val chapterTitle: String) : ReadBookDialog
|
||||
data class ConfirmAddToBookshelf(val bookName: String) : ReadBookDialog
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,6 +117,10 @@ class ReadBookController(
|
||||
private val popupAction by lazy { PopupAction(activity) }
|
||||
private var screenTimeOut: Long = 0
|
||||
private var pendingSearchResultMark: IntArray? = null
|
||||
private val originalRequestedOrientation = activity.requestedOrientation
|
||||
private val originalScreenBrightness = activity.window.attributes.screenBrightness
|
||||
private val originalKeepScreenOn =
|
||||
(activity.window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
|
||||
// justInitData moved to ViewModel (set on InitData intent)
|
||||
|
||||
val isAutoPage: Boolean get() = refs?.readView?.isAutoPage == true
|
||||
@@ -136,6 +140,7 @@ class ReadBookController(
|
||||
refs?.readView?.onDestroy()
|
||||
networkChangedListener.unRegister()
|
||||
unregisterTimeBatteryReceiver()
|
||||
restoreActivityWindowState()
|
||||
}
|
||||
|
||||
// Phase 5: Key handling / page turn
|
||||
@@ -173,6 +178,7 @@ class ReadBookController(
|
||||
}
|
||||
|
||||
fun onRouteInitialized() {
|
||||
applyReadBrightness()
|
||||
upScreenTimeOut()
|
||||
}
|
||||
|
||||
@@ -1039,6 +1045,25 @@ class ReadBookController(
|
||||
screenOffTimerStartInternal()
|
||||
}
|
||||
|
||||
private fun applyReadBrightness() {
|
||||
val lp = activity.window.attributes
|
||||
lp.screenBrightness = if (ReadBookConfig.brightnessAuto) {
|
||||
WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
} else {
|
||||
ReadBookConfig.readBrightness / 100f
|
||||
}
|
||||
activity.window.attributes = lp
|
||||
}
|
||||
|
||||
private fun restoreActivityWindowState() {
|
||||
activity.requestedOrientation = originalRequestedOrientation
|
||||
val lp = activity.window.attributes
|
||||
lp.screenBrightness = originalScreenBrightness
|
||||
activity.window.attributes = lp
|
||||
keepScreenOn(originalKeepScreenOn)
|
||||
handler.removeCallbacks(screenOffRunnable)
|
||||
}
|
||||
|
||||
private fun screenOffTimerStartInternal() {
|
||||
handler.post {
|
||||
if (screenTimeOut < 0) {
|
||||
|
||||
@@ -1582,6 +1582,7 @@ private fun FloatingIconRow(
|
||||
),
|
||||
selected = iconDef.isActive,
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
onLongClick = iconDef.onLongClick,
|
||||
) {
|
||||
if (isCustom) {
|
||||
AsyncImage(
|
||||
@@ -2446,6 +2447,7 @@ private fun ToolButtonItem(
|
||||
menuConfig = state.menuConfig,
|
||||
glassEnabled = true,
|
||||
selected = button.isActive,
|
||||
onLongClick = button.onLongClick,
|
||||
) { tint ->
|
||||
ToolButtonContent(
|
||||
button = button,
|
||||
@@ -2470,6 +2472,7 @@ private fun ToolButtonItem(
|
||||
indication = LocalIndication.current,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
role = Role.Button,
|
||||
onLongClick = button.onLongClick,
|
||||
onClick = button.onClick,
|
||||
),
|
||||
) {
|
||||
@@ -2553,6 +2556,7 @@ private data class ToolButtonDef(
|
||||
val customIconPath: String?,
|
||||
val isActive: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
val onLongClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
private fun loadToolButtons(
|
||||
@@ -2561,8 +2565,12 @@ private fun loadToolButtons(
|
||||
onIntent: (ReadBookIntent) -> Unit,
|
||||
): List<ToolButtonDef> {
|
||||
val customIcons = state.menuConfig.readMenuCustomIcons
|
||||
fun ReadMenuButtonInfo.toButton(isActive: Boolean = false, onClick: () -> Unit): ToolButtonDef {
|
||||
return ToolButtonDef(id, icon, label, customIcons[id], isActive, onClick)
|
||||
fun ReadMenuButtonInfo.toButton(
|
||||
isActive: Boolean = false,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
onClick: () -> Unit,
|
||||
): ToolButtonDef {
|
||||
return ToolButtonDef(id, icon, label, customIcons[id], isActive, onClick, onLongClick)
|
||||
}
|
||||
val infoMap = readMenuButtonInfos(context).associateBy { it.id }
|
||||
val allButtons = listOf(
|
||||
@@ -2572,7 +2580,10 @@ private fun loadToolButtons(
|
||||
infoMap.getValue("catalog").toButton {
|
||||
onIntent(ReadBookIntent.OpenChapterList)
|
||||
},
|
||||
infoMap.getValue("read_aloud").toButton(isActive = state.isReadAloudRunning) {
|
||||
infoMap.getValue("read_aloud").toButton(
|
||||
isActive = state.isReadAloudRunning,
|
||||
onLongClick = { onIntent(ReadBookIntent.ShowReadAloudConfig) },
|
||||
) {
|
||||
if (state.isReadAloudRunning) {
|
||||
onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud))
|
||||
} else {
|
||||
@@ -2906,6 +2917,7 @@ private data class TitleBarIconDef(
|
||||
val label: String,
|
||||
val isActive: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
val onLongClick: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
private fun loadFloatingIcons(
|
||||
@@ -2961,6 +2973,11 @@ private fun loadFloatingIcons(
|
||||
label = info.label,
|
||||
isActive = id in activeIds,
|
||||
onClick = actionMap[id] ?: {},
|
||||
onLongClick = if (id == "read_aloud") {
|
||||
{ onIntent(ReadBookIntent.ShowReadAloudConfig) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
.toList()
|
||||
|
||||
@@ -59,7 +59,7 @@ fun ReadBookScreen(
|
||||
state.isShowingSearchResult -> onIntent(ReadBookIntent.ExitSearch)
|
||||
state.menuVisible -> onIntent(ReadBookIntent.ReadMenuBack)
|
||||
state.isAutoPage -> onIntent(ReadBookIntent.StopAutoPage)
|
||||
else -> onBack()
|
||||
else -> onIntent(ReadBookIntent.CloseReadBook)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ fun ReadBookScreen(
|
||||
val restoreLastProgressDialog = state.activeDialog as? ReadBookDialog.RestoreLastBookProgress
|
||||
val skipDialog = state.activeDialog as? ReadBookDialog.ConfirmSkipToChapter
|
||||
val payDialog = state.activeDialog as? ReadBookDialog.ConfirmChapterPay
|
||||
val addToBookshelfDialog = state.activeDialog as? ReadBookDialog.ConfirmAddToBookshelf
|
||||
|
||||
AppAlertDialog(
|
||||
show = restoreDialog != null,
|
||||
@@ -129,6 +130,19 @@ fun ReadBookScreen(
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = { onIntent(ReadBookIntent.DismissDialog) },
|
||||
)
|
||||
AppAlertDialog(
|
||||
show = addToBookshelfDialog != null,
|
||||
onDismissRequest = { onIntent(ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf) },
|
||||
title = stringResource(R.string.add_to_bookshelf),
|
||||
text = stringResource(
|
||||
R.string.check_add_bookshelf,
|
||||
addToBookshelfDialog?.bookName.orEmpty()
|
||||
),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
onConfirm = { onIntent(ReadBookIntent.ConfirmAddCurrentBookToBookshelf) },
|
||||
dismissText = stringResource(R.string.cancel),
|
||||
onDismiss = { onIntent(ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf) },
|
||||
)
|
||||
|
||||
// AppModalBottomSheet-based sheets — always composed, controlled by show flag
|
||||
// for proper enter/exit animations
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.speech.tts.TextToSpeech
|
||||
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.BookType
|
||||
@@ -46,6 +47,10 @@ import io.legado.app.help.book.removeType
|
||||
import io.legado.app.help.book.simulatedTotalChapterNum
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.http.decompressed
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.help.source.getSourceType
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
import io.legado.app.model.ImageProvider
|
||||
@@ -72,6 +77,8 @@ 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.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
|
||||
@@ -132,6 +139,15 @@ class ReadBookViewModel(
|
||||
private val _effects = MutableSharedFlow<ReadBookEffect>(extraBufferCapacity = 16)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
private suspend fun emitEffectWhenSubscribed(effect: ReadBookEffect) {
|
||||
_effects.subscriptionCount.first { it > 0 }
|
||||
_effects.emit(effect)
|
||||
}
|
||||
|
||||
private fun closeReadMenu() {
|
||||
_uiState.update { it.copy(menuState = ReadBookMenuState()) }
|
||||
}
|
||||
|
||||
private val sysEngines: List<TextToSpeech.EngineInfo> by lazy {
|
||||
val tts = TextToSpeech(context, null)
|
||||
val engines = tts.engines
|
||||
@@ -172,7 +188,7 @@ class ReadBookViewModel(
|
||||
is ReadBookIntent.NextPage -> ReadBook.moveToNextPage()
|
||||
is ReadBookIntent.PrevPage -> ReadBook.moveToPrevPage()
|
||||
is ReadBookIntent.NextChapter -> ReadBook.moveToNextChapter(upContent = true)
|
||||
is ReadBookIntent.PrevChapter -> ReadBook.moveToPrevChapter(upContent = true)
|
||||
is ReadBookIntent.PrevChapter -> ReadBook.moveToPrevChapter(upContent = true, toLast = false)
|
||||
is ReadBookIntent.OpenChapter -> openChapter(intent.index, intent.pos)
|
||||
is ReadBookIntent.SkipToPage -> ReadBook.skipToPage(intent.pageIndex)
|
||||
is ReadBookIntent.ToggleMenu -> _uiState.update {
|
||||
@@ -220,6 +236,7 @@ class ReadBookViewModel(
|
||||
}
|
||||
|
||||
is ReadBookIntent.OpenSearch -> {
|
||||
closeReadMenu()
|
||||
_uiState.update { it.copy(searchContentQuery = intent.word ?: "") }
|
||||
ReadBook.book?.bookUrl?.let { bookUrl ->
|
||||
_effects.tryEmit(ReadBookEffect.OpenSearchActivity(intent.word, bookUrl))
|
||||
@@ -368,6 +385,7 @@ class ReadBookViewModel(
|
||||
activeSheet = null,
|
||||
contentEditText = "",
|
||||
contentEditTitle = "",
|
||||
contentEditCursorOffset = 0,
|
||||
contentEditLoading = false,
|
||||
contentEditSaveToSource = false,
|
||||
)
|
||||
@@ -407,11 +425,13 @@ class ReadBookViewModel(
|
||||
}
|
||||
is ReadBookIntent.OpenBookInfo -> {
|
||||
ReadBook.book?.let { book ->
|
||||
closeReadMenu()
|
||||
_effects.tryEmit(ReadBookEffect.OpenBookInfo(book.name, book.author, book.bookUrl))
|
||||
}
|
||||
}
|
||||
is ReadBookIntent.OpenChapterList -> {
|
||||
ReadBook.book?.bookUrl?.let { bookUrl ->
|
||||
closeReadMenu()
|
||||
_effects.tryEmit(ReadBookEffect.OpenChapterList(bookUrl))
|
||||
}
|
||||
}
|
||||
@@ -487,8 +507,12 @@ class ReadBookViewModel(
|
||||
_uiState.update { it.copy(activeSheet = ReadBookSheet.ChangeBookSource) }
|
||||
}
|
||||
is ReadBookIntent.MenuChapterChangeSource -> handleChapterChangeSource()
|
||||
is ReadBookIntent.MenuSettingReplace -> _effects.tryEmit(ReadBookEffect.MenuSettingReplace)
|
||||
is ReadBookIntent.MenuSettingReplace -> {
|
||||
closeReadMenu()
|
||||
_effects.tryEmit(ReadBookEffect.MenuSettingReplace)
|
||||
}
|
||||
is ReadBookIntent.MenuTocRegex -> {
|
||||
closeReadMenu()
|
||||
_effects.tryEmit(ReadBookEffect.MenuTocRegex(ReadBook.book?.tocUrl))
|
||||
}
|
||||
is ReadBookIntent.TocRegexResult -> {
|
||||
@@ -661,8 +685,16 @@ class ReadBookViewModel(
|
||||
|
||||
is ReadBookIntent.TtsProgress -> _effects.tryEmit(ReadBookEffect.UpTtsAloudSpan(intent.chapterStart))
|
||||
is ReadBookIntent.ReadAloudAction -> {
|
||||
openReadMenuRoute(ReadBookMenuRoute.ReadAloud)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
speakEngineName = computeSpeakEngineName(),
|
||||
activeSheet = ReadBookSheet.ReadAloudConfig,
|
||||
)
|
||||
}
|
||||
loadTtsEngineItems()
|
||||
}
|
||||
is ReadBookIntent.ConfirmAddCurrentBookToBookshelf -> addCurrentBookToBookshelfAndFinish()
|
||||
is ReadBookIntent.ExitWithoutAddingCurrentBookToBookshelf -> removeCurrentNotShelfBookAndFinish()
|
||||
|
||||
is ReadBookIntent.ShowReadAloudConfig -> {
|
||||
_uiState.update {
|
||||
@@ -796,17 +828,11 @@ class ReadBookViewModel(
|
||||
}
|
||||
|
||||
is ReadBookIntent.ImportHttpTtsJson -> {
|
||||
execute {
|
||||
HttpTTS.fromJsonArray(intent.json).getOrDefault(arrayListOf())
|
||||
}.onSuccess { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
execute {
|
||||
appDb.httpTTSDao.insert(*list.toTypedArray())
|
||||
}.onSuccess {
|
||||
loadTtsEngineItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
importHttpTtsSource(intent.json)
|
||||
}
|
||||
|
||||
is ReadBookIntent.ImportHttpTtsSource -> {
|
||||
importHttpTtsSource(intent.text)
|
||||
}
|
||||
|
||||
is ReadBookIntent.ExportAllHttpTts -> {
|
||||
@@ -829,20 +855,12 @@ class ReadBookViewModel(
|
||||
}
|
||||
|
||||
is ReadBookIntent.ImportHttpTtsFileSelected -> {
|
||||
execute {
|
||||
execute<String?> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
text
|
||||
}.onSuccess { text ->
|
||||
if (!text.isNullOrBlank()) importHttpTtsSource(text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,7 +888,7 @@ class ReadBookViewModel(
|
||||
if (intent.value) postEvent(EventBus.MEDIA_BUTTON, false)
|
||||
}
|
||||
is ReadBookIntent.ReadAloudPrevParagraph -> ReadAloud.prevParagraph(context)
|
||||
is ReadBookIntent.ReadAloudTogglePause -> toggleReadAloudPause()
|
||||
is ReadBookIntent.ReadAloudTogglePause -> _effects.tryEmit(ReadBookEffect.ToggleReadAloud)
|
||||
is ReadBookIntent.ReadAloudStop -> {
|
||||
ReadAloud.stop(context)
|
||||
_uiState.update { it.copy(isReadAloudRunning = false, isReadAloudPaused = false) }
|
||||
@@ -882,6 +900,10 @@ class ReadBookViewModel(
|
||||
)
|
||||
is ReadBookIntent.ReadAloudNextChapter -> ReadBook.moveToNextChapter(true)
|
||||
is ReadBookIntent.SetReadAloudTtsTimer -> setReadAloudTtsTimer(intent.value)
|
||||
is ReadBookIntent.SaveReadAloudTtsTimer -> {
|
||||
viewModelScope.launch { readAloudSettingsRepository.saveTtsTimer(intent.value) }
|
||||
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.save_success)))
|
||||
}
|
||||
is ReadBookIntent.SetReadAloudTtsFollowSys -> {
|
||||
viewModelScope.launch { readAloudSettingsRepository.setTtsFollowSys(intent.value) }
|
||||
_uiState.update { it.copy(readAloudTtsFollowSys = intent.value) }
|
||||
@@ -1066,7 +1088,7 @@ class ReadBookViewModel(
|
||||
is ReadBookIntent.OnResume -> handleOnResume()
|
||||
is ReadBookIntent.OnPause -> handleOnPause()
|
||||
is ReadBookIntent.OnDispose -> handleOnDispose()
|
||||
is ReadBookIntent.CloseReadBook -> _effects.tryEmit(ReadBookEffect.Finish)
|
||||
is ReadBookIntent.CloseReadBook -> closeReadBook()
|
||||
is ReadBookIntent.OpenBooksDirPicker -> requestBooksDirPicker(reloadChapterList = false)
|
||||
is ReadBookIntent.BooksDirSelected -> onBooksDirSelected(intent.uri)
|
||||
}
|
||||
@@ -1127,9 +1149,12 @@ class ReadBookViewModel(
|
||||
}
|
||||
|
||||
private fun handleOnDispose() {
|
||||
// TTS and view cleanup — bridge handles via clearTts()
|
||||
backupJob?.cancel()
|
||||
ReadBook.cancelPreDownloadTask()
|
||||
if (BaseReadAloudService.isRun) {
|
||||
ReadAloud.stop(context)
|
||||
_uiState.update { it.copy(isReadAloudRunning = false, isReadAloudPaused = false) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSpeakEngineConfig() {
|
||||
@@ -1171,6 +1196,42 @@ class ReadBookViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun importHttpTtsSource(text: String) {
|
||||
execute {
|
||||
importHttpTtsSourceAwait(text.trim()).also { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
appDb.httpTTSDao.insert(*list.toTypedArray())
|
||||
}
|
||||
}
|
||||
}.onSuccess { list ->
|
||||
if (list.isNotEmpty()) {
|
||||
loadTtsEngineItems()
|
||||
_effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.success)))
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("导入朗读引擎失败\n${it.localizedMessage}", it, true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun importHttpTtsSourceAwait(text: String): List<HttpTTS> {
|
||||
return when {
|
||||
text.isJsonObject() -> listOf(HttpTTS.fromJson(text).getOrThrow())
|
||||
text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow()
|
||||
text.isAbsUrl() -> {
|
||||
val body = okHttpClient.newCallResponseBody {
|
||||
if (text.endsWith("#requestWithoutUA")) {
|
||||
url(text.substringBeforeLast("#requestWithoutUA"))
|
||||
header(AppConst.UA_NAME, "null")
|
||||
} else {
|
||||
url(text)
|
||||
}
|
||||
}.decompressed().text()
|
||||
importHttpTtsSourceAwait(body)
|
||||
}
|
||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeSpeakEngineName(): String {
|
||||
val ttsEngine = ReadAloud.ttsEngine
|
||||
?: return context.getString(R.string.system_tts)
|
||||
@@ -1399,7 +1460,7 @@ class ReadBookViewModel(
|
||||
}
|
||||
}.toSet()
|
||||
if (actions.isNotEmpty()) {
|
||||
_effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(actions))
|
||||
emitEffectWhenSubscribed(ReadBookEffect.UpdateReadViewConfig(actions))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2092,6 +2153,48 @@ class ReadBookViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeReadBook() {
|
||||
val book = ReadBook.book
|
||||
if (!ReadBook.inBookshelf && book != null && OtherConfig.showAddToShelfAlert) {
|
||||
_uiState.update {
|
||||
it.copy(activeDialog = ReadBookDialog.ConfirmAddToBookshelf(book.name))
|
||||
}
|
||||
} else if (!ReadBook.inBookshelf) {
|
||||
removeCurrentNotShelfBookAndFinish()
|
||||
} else {
|
||||
_effects.tryEmit(ReadBookEffect.Finish)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCurrentBookToBookshelfAndFinish() {
|
||||
val book = ReadBook.book ?: return removeCurrentNotShelfBookAndFinish()
|
||||
execute {
|
||||
val toc = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
||||
book.removeType(BookType.notShelf)
|
||||
if (book.order == 0) {
|
||||
book.order = appDb.bookDao.minOrder - 1
|
||||
}
|
||||
appDb.bookDao.insert(book)
|
||||
if (toc.isNotEmpty()) {
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
}
|
||||
ReadBook.inBookshelf = true
|
||||
}.onSuccess {
|
||||
_uiState.update { it.copy(activeDialog = null) }
|
||||
_effects.tryEmit(ReadBookEffect.Finish)
|
||||
}.onError {
|
||||
AppLog.put("添加书籍到书架失败", it)
|
||||
context.toastOnUi("添加书籍失败")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeCurrentNotShelfBookAndFinish() {
|
||||
_uiState.update { it.copy(activeDialog = null) }
|
||||
removeFromBookshelf {
|
||||
_effects.tryEmit(ReadBookEffect.Finish)
|
||||
}
|
||||
}
|
||||
|
||||
fun upBookSource(success: (() -> Unit)? = null) {
|
||||
execute {
|
||||
ReadBook.book?.let { book ->
|
||||
@@ -2179,6 +2282,7 @@ class ReadBookViewModel(
|
||||
it.copy(
|
||||
contentEditText = text,
|
||||
contentEditTitle = title,
|
||||
contentEditCursorOffset = ReadBook.durChapterPos.coerceIn(0, text.length),
|
||||
contentEditIsLocalTxt = book.isLocalTxt,
|
||||
)
|
||||
}
|
||||
@@ -2219,7 +2323,13 @@ class ReadBookViewModel(
|
||||
} else {
|
||||
""
|
||||
}
|
||||
_uiState.update { it.copy(contentEditText = text, contentEditLoading = false) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
contentEditText = text,
|
||||
contentEditCursorOffset = ReadBook.durChapterPos.coerceIn(0, text.length),
|
||||
contentEditLoading = false,
|
||||
)
|
||||
}
|
||||
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
||||
}.onError {
|
||||
_uiState.update { it.copy(contentEditLoading = false) }
|
||||
|
||||
@@ -1079,7 +1079,7 @@ object ChapterProvider {
|
||||
viewWidth = width
|
||||
viewHeight = height
|
||||
upLayout()
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,12 +7,17 @@ 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.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.setTextAndSelectAll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.book.read.ReadBookIntent
|
||||
@@ -34,12 +39,31 @@ fun ContentEditSheet(
|
||||
onDismissRequest: () -> Unit,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val editorState = remember { TextFieldState() }
|
||||
|
||||
LaunchedEffect(show) {
|
||||
if (!show) return@LaunchedEffect
|
||||
onIntent(ReadBookIntent.LoadContentEdit)
|
||||
}
|
||||
|
||||
LaunchedEffect(state.contentEditText, state.contentEditCursorOffset) {
|
||||
if (editorState.text.toString() != state.contentEditText) {
|
||||
editorState.setTextAndSelectAll(state.contentEditText)
|
||||
editorState.edit {
|
||||
selection = TextRange(state.contentEditCursorOffset.coerceIn(0, length))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(editorState) {
|
||||
snapshotFlow { editorState.text.toString() }
|
||||
.collect { text ->
|
||||
if (text != state.contentEditText) {
|
||||
onIntent(ReadBookIntent.SetContentEditText(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = {
|
||||
@@ -60,7 +84,7 @@ fun ContentEditSheet(
|
||||
onClick = {
|
||||
onIntent(
|
||||
ReadBookIntent.SaveContentEdit(
|
||||
state.contentEditText,
|
||||
editorState.text.toString(),
|
||||
state.contentEditSaveToSource
|
||||
)
|
||||
)
|
||||
@@ -76,7 +100,7 @@ fun ContentEditSheet(
|
||||
)
|
||||
SmallTonalButton(
|
||||
onClick = {
|
||||
context.sendToClip("${state.contentEditTitle}\n${state.contentEditText}")
|
||||
context.sendToClip("${state.contentEditTitle}\n${editorState.text}")
|
||||
}, text = stringResource(R.string.copy_all)
|
||||
)
|
||||
}
|
||||
@@ -94,8 +118,7 @@ fun ContentEditSheet(
|
||||
}
|
||||
} else {
|
||||
AppTextField(
|
||||
value = state.contentEditText,
|
||||
onValueChange = { onIntent(ReadBookIntent.SetContentEditText(it)) },
|
||||
state = editorState,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, fill = false)
|
||||
|
||||
@@ -35,6 +35,8 @@ 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
|
||||
import io.legado.app.ui.widget.components.button.series.SmallTonalButton
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -155,6 +157,8 @@ fun SpeakEngineConfigSheet(
|
||||
val items = state.ttsEngineItems
|
||||
val selectedValue = state.selectedTtsEngine
|
||||
var pendingEngineSelection by remember { mutableStateOf<PendingSpeakEngineSelection?>(null) }
|
||||
var showImportSheet by remember { mutableStateOf(false) }
|
||||
var showUrlInput by remember { mutableStateOf(false) }
|
||||
|
||||
AppAlertDialog(
|
||||
show = pendingEngineSelection != null,
|
||||
@@ -172,6 +176,29 @@ fun SpeakEngineConfigSheet(
|
||||
pendingEngineSelection = null
|
||||
},
|
||||
)
|
||||
SourceInputDialog(
|
||||
show = showUrlInput,
|
||||
title = stringResource(R.string.import_on_line),
|
||||
onDismissRequest = { showUrlInput = false },
|
||||
onConfirm = {
|
||||
showUrlInput = false
|
||||
onIntent(ReadBookIntent.ImportHttpTtsSource(it))
|
||||
},
|
||||
)
|
||||
FilePickerSheet(
|
||||
show = showImportSheet,
|
||||
onDismissRequest = { showImportSheet = false },
|
||||
title = stringResource(R.string.import_tts),
|
||||
onSelectSysFile = {
|
||||
showImportSheet = false
|
||||
onIntent(ReadBookIntent.ImportHttpTtsFile)
|
||||
},
|
||||
onManualInput = {
|
||||
showImportSheet = false
|
||||
showUrlInput = true
|
||||
},
|
||||
allowExtensions = arrayOf("json", "txt"),
|
||||
)
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
@@ -195,7 +222,7 @@ fun SpeakEngineConfigSheet(
|
||||
text = stringResource(R.string.import_tts),
|
||||
onClick = {
|
||||
expanded = false
|
||||
onIntent(ReadBookIntent.ImportHttpTtsFile)
|
||||
showImportSheet = true
|
||||
},
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
|
||||
@@ -107,8 +107,8 @@ fun ReadAloudContent(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
MediumTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.SetReadAloudTtsTimer(timerMinute)) },
|
||||
text = stringResource(R.string.timer_m, timerMinute),
|
||||
onClick = { onIntent(ReadBookIntent.SaveReadAloudTtsTimer(timerMinute)) },
|
||||
text = stringResource(R.string.action_save),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
MediumTonalButton(
|
||||
|
||||
@@ -363,16 +363,25 @@ fun TocScreen(
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.use_replace_rule),
|
||||
isSelected = useReplace,
|
||||
onClick = { viewModel.toggleUseReplace() }
|
||||
onClick = {
|
||||
dismiss()
|
||||
viewModel.toggleUseReplace()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.show_word_count),
|
||||
isSelected = showWordCount,
|
||||
onClick = { viewModel.toggleShowWordCount() }
|
||||
onClick = {
|
||||
dismiss()
|
||||
viewModel.toggleShowWordCount()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.reverse_toc),
|
||||
onClick = { viewModel.reverseToc() }
|
||||
onClick = {
|
||||
dismiss()
|
||||
viewModel.reverseToc()
|
||||
}
|
||||
)
|
||||
PillDivider()
|
||||
RoundDropdownMenuItem(
|
||||
|
||||
@@ -26,7 +26,6 @@ import io.legado.app.utils.putPrefLong
|
||||
import io.legado.app.utils.putPrefString
|
||||
import io.legado.app.utils.putPrefStringSync
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -61,12 +60,12 @@ fun <T> prefDelegate(
|
||||
|
||||
@Volatile
|
||||
private var currentValue: T = defaultValue
|
||||
private val scope = CoroutineScope(Dispatchers.IO)
|
||||
private val scope = CoroutineScope(kotlinx.coroutines.Dispatchers.IO)
|
||||
private var dsObserverJob: Job? = null
|
||||
|
||||
init {
|
||||
// 同步从 DataStore 读取初始值,确保构造完成后即为最新值
|
||||
val initialValue = runBlocking(Dispatchers.IO) { readFromDs() } ?: defaultValue
|
||||
val initialValue = runBlocking { readFromDs() } ?: defaultValue
|
||||
_value = mutableStateOf(initialValue)
|
||||
currentValue = initialValue
|
||||
|
||||
@@ -134,7 +133,7 @@ fun <T> prefDelegate(
|
||||
}
|
||||
// 同步写入 DataStore,确保持久化后再返回
|
||||
runCatching {
|
||||
runBlocking(Dispatchers.IO) {
|
||||
runBlocking {
|
||||
when (value) {
|
||||
is String? -> DsSync.putString(key, value)
|
||||
is Int -> DsSync.putInt(key, value)
|
||||
|
||||
@@ -262,7 +262,7 @@ fun MainActivity.mainEntryProvider(
|
||||
|
||||
entry<MainRouteReadBook> { route ->
|
||||
val readBookViewModel = koinViewModel<ReadBookViewModel>(
|
||||
key = route.bookUrl ?: "last-read"
|
||||
key = "ReadBook:${route.bookUrl ?: "last-read"}"
|
||||
)
|
||||
val controller = remember(readBookViewModel) {
|
||||
ReadBookController(this@mainEntryProvider, readBookViewModel)
|
||||
@@ -361,7 +361,7 @@ fun MainActivity.mainEntryProvider(
|
||||
|
||||
entry<MainRouteSearchContent> { route ->
|
||||
val viewModel = koinViewModel<SearchContentViewModel>(
|
||||
key = route.bookUrl,
|
||||
key = "SearchContent:${route.bookUrl}",
|
||||
parameters = { parametersOf(route) }
|
||||
)
|
||||
SearchContentScreen(
|
||||
@@ -549,7 +549,7 @@ fun MainActivity.mainEntryProvider(
|
||||
} else null
|
||||
}
|
||||
) { route ->
|
||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>(key = route.bookUrl)
|
||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>(key = "BookInfo:${route.bookUrl}")
|
||||
BookInfoRouteScreen(
|
||||
bookUrl = route.bookUrl,
|
||||
name = route.name,
|
||||
|
||||
Reference in New Issue
Block a user