From da00f712bf9039aab4cc6dd1b935cd5324288e53 Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Wed, 1 Jul 2026 23:12:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(read):=20=E4=BF=AE=E5=A4=8D=E9=98=85?= =?UTF-8?q?=E8=AF=BB=E8=8F=9C=E5=8D=95=E4=B8=8E=E9=AB=98=E4=BA=AE=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/book/read/ReadBookContract.kt | 21 ++ .../app/ui/book/read/ReadBookController.kt | 20 +- .../app/ui/book/read/ReadBookRouteScreen.kt | 29 ++ .../app/ui/book/read/ReadBookViewModel.kt | 256 ++++++++++++++++++ .../app/ui/book/read/page/ContentTextView.kt | 3 +- .../read/sheet/HighlightRuleConfigSheet.kt | 150 ++++++++-- .../app/ui/book/read/sheet/MoreConfigSheet.kt | 10 + .../io/legado/app/ui/main/MainNavigator.kt | 3 +- 8 files changed, 471 insertions(+), 21 deletions(-) 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 44f6b4a93..8a5b18d89 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 @@ -181,6 +181,7 @@ data class HighlightRuleConfigUiState( val editingRule: HighlightRule? = null, val showNewRule: Boolean = false, val deleteRule: HighlightRule? = null, + val importState: BaseImportUiState = BaseImportUiState.Idle, ) @Stable @@ -429,6 +430,21 @@ sealed interface ReadBookIntent { data class RequestDeleteHighlightRule(val rule: HighlightRule) : ReadBookIntent data object ConfirmDeleteHighlightRule : ReadBookIntent data object DismissDeleteHighlightRule : ReadBookIntent + data class MoveHighlightRule(val from: Int, val to: Int) : ReadBookIntent + data object SaveHighlightRuleOrder : ReadBookIntent + data class ImportHighlightRuleSource(val text: String) : ReadBookIntent + data object OpenHighlightRuleImportPicker : ReadBookIntent + data class HighlightRuleImportFileSelected(val uri: Uri) : ReadBookIntent + data object CancelHighlightRuleImport : ReadBookIntent + data class ToggleHighlightRuleImportSelection(val index: Int) : ReadBookIntent + data class ToggleHighlightRuleImportAll(val isSelected: Boolean) : ReadBookIntent + data class UpdateHighlightRuleImportItem( + val index: Int, + val rule: HighlightRule, + ) : ReadBookIntent + data object SaveImportedHighlightRules : ReadBookIntent + data object ExportHighlightRules : ReadBookIntent + data class ExportHighlightRulesToFile(val uri: Uri) : ReadBookIntent // Icon picker — file IO handled by ViewModel data class SaveMenuCustomIcon(val id: String, val uri: Uri) : ReadBookIntent @@ -645,6 +661,8 @@ sealed interface ReadBookEffect { data object OpenHttpTtsImportPicker : ReadBookEffect data object OpenHttpTtsExportPicker : ReadBookEffect data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect + data object OpenHighlightRuleImportPicker : ReadBookEffect + data object OpenHighlightRuleExportPicker : ReadBookEffect // Day/night toggle data object ToggleDayNight : ReadBookEffect @@ -1128,6 +1146,9 @@ sealed interface ConfigUpdate { data class ReadBodyToLh(val value: Boolean) : ConfigUpdate { override val actions = setOf(ConfigUpdateAction.ReloadContent) } + data class DefaultSourceChangeAll(val value: Boolean) : ConfigUpdate { + override val actions = emptySet() + } data class TextFullJustify(val value: Boolean) : ConfigUpdate { override val actions = setOf(ConfigUpdateAction.ReloadContent) } 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 6fdfc71b7..d6dd9b983 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 @@ -177,6 +177,16 @@ class ReadBookController( newRefs.readView.upBattery(activity.sysBattery) } + fun onMenuVisibilityChanged(visible: Boolean) { + val autoPager = refs?.readView?.autoPager ?: return + if (!autoPager.isRunning) return + if (visible) { + autoPager.pause() + } else { + autoPager.resume() + } + } + fun onRouteInitialized() { applyReadBrightness() upScreenTimeOut() @@ -261,7 +271,10 @@ class ReadBookController( ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud) ) - isAutoPage -> viewModel.onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead)) + isAutoPage -> { + refs?.readView?.autoPager?.pause() + viewModel.onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead)) + } state.isShowingSearchResult -> viewModel.onIntent(ReadBookIntent.ShowSearchMenu) else -> viewModel.onIntent(ReadBookIntent.ShowMenu) } @@ -833,6 +846,8 @@ class ReadBookController( is ReadBookEffect.OpenHttpTtsImportPicker, is ReadBookEffect.OpenHttpTtsExportPicker, is ReadBookEffect.OpenHttpTtsLogin, + is ReadBookEffect.OpenHighlightRuleImportPicker, + is ReadBookEffect.OpenHighlightRuleExportPicker, is ReadBookEffect.TtsCacheCleared, is ReadBookEffect.ExportJson, // DB query + bookmark effects — handled by ViewModel, ignored here @@ -908,6 +923,9 @@ class ReadBookController( stopAutoPage() } else { refs?.readView?.autoPager?.start() + if (viewModel.uiState.value.menuVisible) { + refs?.readView?.autoPager?.pause() + } viewModel.setAutoPage(true) onScreenOffTimerStart?.invoke() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt index b23c39d05..f2e1f2dce 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt @@ -122,6 +122,10 @@ fun ReadBookRouteScreen( state.menuConfig.readMenuBottomBarBlurMode == ReadMenuBlurMode.LiquidGlass ) + LaunchedEffect(state.menuVisible) { + controller.onMenuVisibilityChanged(state.menuVisible) + } + // ── ActivityResult Launchers ────────────────────────────────────── val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result -> @@ -235,6 +239,18 @@ fun ReadBookRouteScreen( uri?.let { viewModel.onIntent(ReadBookIntent.ExportHttpTtsToFile(it)) } } + val importHighlightRulePicker = rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { viewModel.onIntent(ReadBookIntent.HighlightRuleImportFileSelected(it)) } + } + + val exportHighlightRulePicker = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/json") + ) { uri -> + uri?.let { viewModel.onIntent(ReadBookIntent.ExportHighlightRulesToFile(it)) } + } + val bookInfoLauncher = rememberLauncherForActivityResult( StartActivityContract(BookInfoActivity::class.java) ) { result -> @@ -408,6 +424,19 @@ fun ReadBookRouteScreen( exportHttpTtsPicker.launch("httpTTS.json") } + is ReadBookEffect.OpenHighlightRuleImportPicker -> { + importHighlightRulePicker.launch( + arrayOf( + "application/json", + "text/plain" + ) + ) + } + + is ReadBookEffect.OpenHighlightRuleExportPicker -> { + exportHighlightRulePicker.launch("highlightRule.json") + } + // All other effects — delegate to bridge (View/Window/Activity operations) else -> controller.handleEffect(effect) } 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 8e414a0ce..975539fc4 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 @@ -83,6 +83,7 @@ import io.legado.app.utils.GSON import io.legado.app.utils.ImageSaveUtils import io.legado.app.utils.NetworkUtils import io.legado.app.utils.StringUtils +import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonObject import io.legado.app.utils.hexString import io.legado.app.utils.isAbsUrl @@ -410,6 +411,7 @@ class ReadBookViewModel( editingRule = null, showNewRule = false, deleteRule = null, + importState = BaseImportUiState.Idle, ), ) } else { @@ -658,6 +660,36 @@ class ReadBookViewModel( is ReadBookIntent.DismissDeleteHighlightRule -> _uiState.update { it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(deleteRule = null)) } + is ReadBookIntent.MoveHighlightRule -> moveHighlightRule(intent.from, intent.to) + is ReadBookIntent.SaveHighlightRuleOrder -> { + saveHighlightRules(_uiState.value.highlightRuleConfig.rules) + } + is ReadBookIntent.ImportHighlightRuleSource -> { + importHighlightRuleSource(intent.text) + } + is ReadBookIntent.OpenHighlightRuleImportPicker -> { + _effects.tryEmit(ReadBookEffect.OpenHighlightRuleImportPicker) + } + is ReadBookIntent.HighlightRuleImportFileSelected -> { + importHighlightRuleFile(intent.uri) + } + is ReadBookIntent.CancelHighlightRuleImport -> cancelHighlightRuleImport() + is ReadBookIntent.ToggleHighlightRuleImportSelection -> { + toggleHighlightRuleImportSelection(intent.index) + } + is ReadBookIntent.ToggleHighlightRuleImportAll -> { + toggleHighlightRuleImportAll(intent.isSelected) + } + is ReadBookIntent.UpdateHighlightRuleImportItem -> { + updateHighlightRuleImportItem(intent.index, intent.rule) + } + is ReadBookIntent.SaveImportedHighlightRules -> saveImportedHighlightRules() + is ReadBookIntent.ExportHighlightRules -> { + _effects.tryEmit(ReadBookEffect.OpenHighlightRuleExportPicker) + } + is ReadBookIntent.ExportHighlightRulesToFile -> { + exportHighlightRules(intent.uri) + } is ReadBookIntent.SaveMenuCustomIcon -> saveMenuCustomIcon(intent.id, intent.uri) is ReadBookIntent.SaveTitleBarCustomIcon -> saveTitleBarCustomIcon(intent.id, intent.uri) is ReadBookIntent.OpenMenuCustomIconPicker -> { @@ -3306,6 +3338,12 @@ class ReadBookViewModel( readSettingsRepository.setReadBodyToLh(update.value) } } + is ConfigUpdate.DefaultSourceChangeAll -> { + ReadConfig.defaultSourceChangeAll = update.value + viewModelScope.launch { + readSettingsRepository.setDefaultSourceChangeAll(update.value) + } + } is ConfigUpdate.TextFullJustify -> { ReadBookConfig.textFullJustify = update.value viewModelScope.launch { @@ -3526,6 +3564,223 @@ class ReadBookViewModel( } } + private fun moveHighlightRule(from: Int, to: Int) { + val rules = _uiState.value.highlightRuleConfig.rules + if (from !in rules.indices || to !in rules.indices) return + val reordered = rules.toMutableList().apply { + add(to, removeAt(from)) + } + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + rules = reordered.toImmutableList() + ) + ) + } + } + + private fun importHighlightRuleSource(text: String) { + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = BaseImportUiState.Loading + ) + ) + } + execute { + val importedRules = importHighlightRuleSourceAwait(text.trim()) + .map(highlightRuleRepository::sanitizeRule) + if (importedRules.isEmpty()) { + throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + val oldRules = highlightRuleRepository.load(ReadBookConfig.durConfig.name) + .associateBy { it.id } + BaseImportUiState.Success( + source = text, + items = importedRules.map { rule -> + val oldRule = oldRules[rule.id] + val status = when { + oldRule == null -> ImportStatus.New + oldRule != rule -> ImportStatus.Update + else -> ImportStatus.Existing + } + ImportItemWrapper( + data = rule, + oldData = oldRule, + status = status, + isSelected = status != ImportStatus.Existing, + ) + } + ) + }.onSuccess { importState -> + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = importState + ) + ) + } + }.onError { + AppLog.put("导入高亮规则失败\n${it.localizedMessage}", it, true) + _uiState.update { state -> + state.copy( + highlightRuleConfig = state.highlightRuleConfig.copy( + importState = BaseImportUiState.Error( + it.localizedMessage ?: context.getString(R.string.wrong_format) + ) + ) + ) + } + } + } + + private suspend fun importHighlightRuleSourceAwait(text: String): List { + return when { + text.isJsonArray() -> GSON.fromJsonArray(text).getOrThrow() + text.isJsonObject() -> listOf( + GSON.fromJsonObject(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() + importHighlightRuleSourceAwait(body) + } + else -> throw NoStackTraceException(context.getString(R.string.wrong_format)) + } + } + + private fun importHighlightRuleFile(uri: Uri) { + execute { + context.contentResolver.openInputStream(uri)?.use { + it.reader().readText() + } + }.onSuccess { text -> + if (text.isNullOrBlank()) { + _uiState.update { state -> + state.copy( + highlightRuleConfig = state.highlightRuleConfig.copy( + importState = BaseImportUiState.Error( + context.getString(R.string.wrong_format) + ) + ) + ) + } + } else { + importHighlightRuleSource(text) + } + }.onError { + _uiState.update { state -> + state.copy( + highlightRuleConfig = state.highlightRuleConfig.copy( + importState = BaseImportUiState.Error( + it.localizedMessage ?: context.getString(R.string.wrong_format) + ) + ) + ) + } + } + } + + private fun cancelHighlightRuleImport() { + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = BaseImportUiState.Idle + ) + ) + } + } + + private fun toggleHighlightRuleImportSelection(index: Int) { + val importState = _uiState.value.highlightRuleConfig.importState + as? BaseImportUiState.Success ?: return + if (index !in importState.items.indices) return + val items = importState.items.toMutableList() + val item = items[index] + items[index] = item.copy(isSelected = !item.isSelected) + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = importState.copy(items = items) + ) + ) + } + } + + private fun toggleHighlightRuleImportAll(isSelected: Boolean) { + val importState = _uiState.value.highlightRuleConfig.importState + as? BaseImportUiState.Success ?: return + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = importState.copy( + items = importState.items.map { item -> + item.copy(isSelected = isSelected) + } + ) + ) + ) + } + } + + private fun updateHighlightRuleImportItem(index: Int, rule: HighlightRule) { + val importState = _uiState.value.highlightRuleConfig.importState + as? BaseImportUiState.Success ?: return + if (index !in importState.items.indices) return + val items = importState.items.toMutableList() + items[index] = items[index].copy(data = rule) + _uiState.update { + it.copy( + highlightRuleConfig = it.highlightRuleConfig.copy( + importState = importState.copy( + items = items, + version = importState.version + 1, + ) + ) + ) + } + } + + private fun saveImportedHighlightRules() { + val state = _uiState.value.highlightRuleConfig + val importState = state.importState + as? BaseImportUiState.Success ?: return + val importedRules = importState.items + .filter { it.isSelected } + .map { highlightRuleRepository.sanitizeRule(it.data) } + if (importedRules.isEmpty()) return + val importedById = importedRules.associateBy { it.id } + val mergedRules = state.rules.map { importedById[it.id] ?: it } + + importedRules.filter { imported -> state.rules.none { it.id == imported.id } } + saveHighlightRules(mergedRules) + cancelHighlightRuleImport() + } + + private fun exportHighlightRules(uri: Uri) { + val rules = _uiState.value.highlightRuleConfig.rules + execute { + context.contentResolver.openOutputStream(uri)?.use { output -> + output.bufferedWriter().use { writer -> + writer.write(GSON.toJson(rules)) + } + } ?: throw NoStackTraceException(context.getString(R.string.error)) + }.onSuccess { + _effects.tryEmit(ReadBookEffect.ShowToast(context.getString(R.string.export_success))) + }.onError { + _effects.tryEmit( + ReadBookEffect.ShowToast( + it.localizedMessage ?: context.getString(R.string.error) + ) + ) + } + } + private fun loadHighlightRules() { val configName = ReadBookConfig.durConfig.name _uiState.update { @@ -3535,6 +3790,7 @@ class ReadBookViewModel( editingRule = null, showNewRule = false, deleteRule = null, + importState = BaseImportUiState.Idle, ), ) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 9f686b6fa..c3123f21c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -25,6 +25,7 @@ import io.legado.app.ui.book.read.page.entities.column.TextHtmlColumn import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.config.readConfig.ReadConfig +import io.legado.app.ui.main.MainNavigator import io.legado.app.ui.widget.dialog.PhotoDialog import io.legado.app.utils.activity import io.legado.app.utils.dpToPx @@ -96,7 +97,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - if (!isMainView) return + if (!isMainView || MainNavigator.backNavigationInProgress) return ChapterProvider.upViewSize(w, h) textPage.format() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt index 05682cb3b..aa9591778 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleConfigSheet.kt @@ -1,22 +1,33 @@ package io.legado.app.ui.book.read.sheet import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex import io.legado.app.R import io.legado.app.data.entities.HighlightRule import io.legado.app.data.repository.configNames @@ -25,8 +36,15 @@ import io.legado.app.ui.book.read.ReadBookIntent import io.legado.app.ui.widget.components.TinySwitch 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.BatchImportDialog +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 import io.legado.app.ui.widget.components.settingItem.TinySettingItem +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable fun HighlightRuleConfigSheet( @@ -35,16 +53,105 @@ fun HighlightRuleConfigSheet( onDismissRequest: () -> Unit, onIntent: (ReadBookIntent) -> Unit, ) { + var showImportSheet by remember { mutableStateOf(false) } + var showSourceInput by remember { mutableStateOf(false) } + val listState = rememberLazyListState() + val hapticFeedback = LocalHapticFeedback.current + var orderChanged by remember { mutableStateOf(false) } + val reorderableState = rememberReorderableLazyListState(listState) { from, to -> + orderChanged = true + onIntent(ReadBookIntent.MoveHighlightRule(from.index, to.index)) + hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick) + } + + LaunchedEffect(reorderableState.isAnyItemDragging) { + if (!reorderableState.isAnyItemDragging && orderChanged) { + orderChanged = false + onIntent(ReadBookIntent.SaveHighlightRuleOrder) + } + } + + SourceInputDialog( + show = showSourceInput, + title = stringResource(R.string.import_on_line), + onDismissRequest = { showSourceInput = false }, + onConfirm = { + showSourceInput = false + onIntent(ReadBookIntent.ImportHighlightRuleSource(it)) + }, + ) + FilePickerSheet( + show = showImportSheet, + onDismissRequest = { showImportSheet = false }, + title = stringResource(R.string.highlight_rule_config), + onSelectSysFile = { + showImportSheet = false + onIntent(ReadBookIntent.OpenHighlightRuleImportPicker) + }, + onManualInput = { + showImportSheet = false + showSourceInput = true + }, + allowExtensions = arrayOf("json", "txt"), + ) + BatchImportDialog( + title = stringResource(R.string.highlight_rule_config), + importState = state.importState, + onDismissRequest = { onIntent(ReadBookIntent.CancelHighlightRuleImport) }, + onToggleItem = { + onIntent(ReadBookIntent.ToggleHighlightRuleImportSelection(it)) + }, + onToggleAll = { + onIntent(ReadBookIntent.ToggleHighlightRuleImportAll(it)) + }, + onUpdateItem = { index, rule -> + onIntent(ReadBookIntent.UpdateHighlightRuleImportItem(index, rule)) + }, + onConfirm = { + onIntent(ReadBookIntent.SaveImportedHighlightRules) + }, + itemTitle = { it.name.ifBlank { it.displayPattern() } }, + itemSubtitle = { it.pattern.takeIf(String::isNotBlank) }, + ) + AppModalBottomSheet( show = show, onDismissRequest = onDismissRequest, title = stringResource(R.string.highlight_rule_config), - endAction = { + startAction = { SmallTonalButton( onClick = { onIntent(ReadBookIntent.AddHighlightRule) }, icon = Icons.Default.Add ) - } + }, + endAction = { + var expanded by remember { mutableStateOf(false) } + Box { + SmallTonalButton( + onClick = { expanded = true }, + icon = Icons.Default.MoreVert + ) + RoundDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + RoundDropdownMenuItem( + text = stringResource(R.string.import_action), + onClick = { + expanded = false + showImportSheet = true + }, + ) + RoundDropdownMenuItem( + text = stringResource(R.string.export), + onClick = { + expanded = false + onIntent(ReadBookIntent.ExportHighlightRules) + }, + ) + } + } + }, ) { Column( modifier = Modifier @@ -52,26 +159,34 @@ fun HighlightRuleConfigSheet( .padding(bottom = 16.dp), ) { LazyColumn( + state = listState, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f, fill = false), ) { - itemsIndexed(state.rules, key = { _, rule -> rule.id }) { _, rule -> - HighlightRuleItem( - rule = rule, - onToggle = { enabled -> - onIntent(ReadBookIntent.ToggleHighlightRule(rule, enabled)) - }, - onEditClick = { onIntent(ReadBookIntent.EditHighlightRule(rule)) }, - onDeleteClick = { - onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule)) - }, - ) + items(state.rules, key = { it.id }) { rule -> + ReorderableItem(reorderableState, key = rule.id) { isDragging -> + HighlightRuleItem( + rule = rule, + onToggle = { enabled -> + onIntent(ReadBookIntent.ToggleHighlightRule(rule, enabled)) + }, + onEditClick = { + onIntent(ReadBookIntent.EditHighlightRule(rule)) + }, + onDeleteClick = { + onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule)) + }, + modifier = Modifier + .longPressDraggableHandle() + .zIndex(if (isDragging) 1f else 0f) + .animateItem(), + ) + } } } } } - // Edit existing rule val editingRuleValue = state.editingRule HighlightRuleEditSheet( @@ -83,7 +198,6 @@ fun HighlightRuleConfigSheet( }, ) - // Add new rule HighlightRuleEditSheet( show = show && state.showNewRule, rule = null, @@ -93,7 +207,6 @@ fun HighlightRuleConfigSheet( }, ) - // Delete confirmation val deletingRule = state.deleteRule val sureDeleteText = stringResource(R.string.sure_delete) AppAlertDialog( @@ -106,7 +219,6 @@ fun HighlightRuleConfigSheet( dismissText = stringResource(android.R.string.cancel), onDismiss = { onIntent(ReadBookIntent.DismissDeleteHighlightRule) }, ) - } @Composable @@ -115,12 +227,14 @@ private fun HighlightRuleItem( onToggle: (Boolean) -> Unit, onEditClick: () -> Unit, onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, ) { val configLabel = rule.configName?.configNames()?.joinToString("、") ?: "全局" TinySettingItem( title = rule.name.ifBlank { rule.displayPattern() }, description = "${rule.styleSummary()} · $configLabel", onClick = onEditClick, + modifier = modifier, trailingContent = { Row(verticalAlignment = Alignment.CenterVertically) { TinySwitch( diff --git a/app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt b/app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt index aa15ebaa1..26e4e7741 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt @@ -125,6 +125,9 @@ fun MoreConfigSheet( onAutoChangeSourceChange = { onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoChangeSource(it))) }, + onDefaultSourceChangeAllChange = { + onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.DefaultSourceChangeAll(it))) + }, onAutoSuggestDayNightChange = { onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoSuggestDayNight(it))) }, @@ -301,6 +304,7 @@ private fun OtherSettings( onSliderVibratorChange: (Boolean) -> Unit, onSelectVibratorChange: (Boolean) -> Unit, onAutoChangeSourceChange: (Boolean) -> Unit, + onDefaultSourceChangeAllChange: (Boolean) -> Unit, onAutoSuggestDayNightChange: (Boolean) -> Unit, onSelectTextChange: (Boolean) -> Unit, onNoAnimScrollPageChange: (Boolean) -> Unit, @@ -333,6 +337,12 @@ private fun OtherSettings( checked = preferences.autoChangeSource, onCheckedChange = onAutoChangeSourceChange, ) + TinySwitchSettingItem( + title = stringResource(R.string.read_change_all), + description = stringResource(R.string.read_change_all_s), + checked = preferences.defaultSourceChangeAll, + onCheckedChange = onDefaultSourceChangeAllChange, + ) TinySwitchSettingItem( title = stringResource(R.string.auto_switch_theme_reminder_title), description = stringResource(R.string.auto_switch_theme_reminder_desc), diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt b/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt index 859b54081..447ea3ef9 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt @@ -14,7 +14,8 @@ import kotlinx.coroutines.launch object MainNavigator { - private var backNavigationInProgress = false + var backNavigationInProgress = false + private set private val navigationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private var backNavigationResetJob: Job? = null