diff --git a/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt b/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt index 33a965588..7c42022ff 100644 --- a/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/ReadAloudSettingsRepository.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt index 1da24e8f4..d46fd57d7 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt @@ -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 = 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 } /** diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt index 8342a8434..4f91d79a3 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt @@ -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) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt index fe11ad156..8cdf8ef8b 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt @@ -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 { 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() diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt index 3658b0d6a..3bae8d23e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt @@ -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 diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 8b408e8d3..167e0bd51 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -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(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 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 { 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 { + 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) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index 6f78393f8..ef709ffaf 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt @@ -1079,7 +1079,7 @@ object ChapterProvider { viewWidth = width viewHeight = height upLayout() - postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5)) + postEvent(EventBus.UP_CONFIG, arrayListOf(5)) } /** diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt index 09fa8480e..78983e74a 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ContentEditSheet.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt index 390dfaed1..f11fd678b 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudConfigSheet.kt @@ -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(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( diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt index 0b223d453..31afb06f5 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/ReadAloudSheet.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt index 3d299907f..2220b6c51 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt @@ -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( diff --git a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt index 1ce39a1da..3258a775c 100644 --- a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt @@ -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 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 prefDelegate( } // 同步写入 DataStore,确保持久化后再返回 runCatching { - runBlocking(Dispatchers.IO) { + runBlocking { when (value) { is String? -> DsSync.putString(key, value) is Int -> DsSync.putInt(key, value) diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt index 381d21a8c..844be94ce 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt @@ -262,7 +262,7 @@ fun MainActivity.mainEntryProvider( entry { route -> val readBookViewModel = koinViewModel( - 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 { route -> val viewModel = koinViewModel( - key = route.bookUrl, + key = "SearchContent:${route.bookUrl}", parameters = { parametersOf(route) } ) SearchContentScreen( @@ -549,7 +549,7 @@ fun MainActivity.mainEntryProvider( } else null } ) { route -> - val bookInfoViewModel = koinViewModel(key = route.bookUrl) + val bookInfoViewModel = koinViewModel(key = "BookInfo:${route.bookUrl}") BookInfoRouteScreen( bookUrl = route.bookUrl, name = route.name,