fix: 修复阅读界面交互问题

This commit is contained in:
HapeLee
2026-06-18 00:07:42 +08:00
parent 300922f96e
commit 4f6ee460ae
13 changed files with 287 additions and 54 deletions
@@ -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,