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