fix(read): 修复阅读菜单与高亮规则功能
This commit is contained in:
@@ -181,6 +181,7 @@ data class HighlightRuleConfigUiState(
|
||||
val editingRule: HighlightRule? = null,
|
||||
val showNewRule: Boolean = false,
|
||||
val deleteRule: HighlightRule? = null,
|
||||
val importState: BaseImportUiState<HighlightRule> = 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<ConfigUpdateAction>()
|
||||
}
|
||||
data class TextFullJustify(val value: Boolean) : ConfigUpdate {
|
||||
override val actions = setOf(ConfigUpdateAction.ReloadContent)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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<HighlightRule> {
|
||||
return when {
|
||||
text.isJsonArray() -> GSON.fromJsonArray<HighlightRule>(text).getOrThrow()
|
||||
text.isJsonObject() -> listOf(
|
||||
GSON.fromJsonObject<HighlightRule>(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<String?> {
|
||||
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<HighlightRule> ?: 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<HighlightRule> ?: 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<HighlightRule> ?: 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<HighlightRule> ?: 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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user