fix(read): 修复阅读菜单与高亮规则功能

This commit is contained in:
HapeLee
2026-07-01 23:43:14 +08:00
parent 6e4917fb5d
commit da00f712bf
8 changed files with 471 additions and 21 deletions
@@ -181,6 +181,7 @@ data class HighlightRuleConfigUiState(
val editingRule: HighlightRule? = null, val editingRule: HighlightRule? = null,
val showNewRule: Boolean = false, val showNewRule: Boolean = false,
val deleteRule: HighlightRule? = null, val deleteRule: HighlightRule? = null,
val importState: BaseImportUiState<HighlightRule> = BaseImportUiState.Idle,
) )
@Stable @Stable
@@ -429,6 +430,21 @@ sealed interface ReadBookIntent {
data class RequestDeleteHighlightRule(val rule: HighlightRule) : ReadBookIntent data class RequestDeleteHighlightRule(val rule: HighlightRule) : ReadBookIntent
data object ConfirmDeleteHighlightRule : ReadBookIntent data object ConfirmDeleteHighlightRule : ReadBookIntent
data object DismissDeleteHighlightRule : 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 // Icon picker — file IO handled by ViewModel
data class SaveMenuCustomIcon(val id: String, val uri: Uri) : ReadBookIntent data class SaveMenuCustomIcon(val id: String, val uri: Uri) : ReadBookIntent
@@ -645,6 +661,8 @@ sealed interface ReadBookEffect {
data object OpenHttpTtsImportPicker : ReadBookEffect data object OpenHttpTtsImportPicker : ReadBookEffect
data object OpenHttpTtsExportPicker : ReadBookEffect data object OpenHttpTtsExportPicker : ReadBookEffect
data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect data class OpenHttpTtsLogin(val engineId: Long) : ReadBookEffect
data object OpenHighlightRuleImportPicker : ReadBookEffect
data object OpenHighlightRuleExportPicker : ReadBookEffect
// Day/night toggle // Day/night toggle
data object ToggleDayNight : ReadBookEffect data object ToggleDayNight : ReadBookEffect
@@ -1128,6 +1146,9 @@ sealed interface ConfigUpdate {
data class ReadBodyToLh(val value: Boolean) : ConfigUpdate { data class ReadBodyToLh(val value: Boolean) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.ReloadContent) 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 { data class TextFullJustify(val value: Boolean) : ConfigUpdate {
override val actions = setOf(ConfigUpdateAction.ReloadContent) override val actions = setOf(ConfigUpdateAction.ReloadContent)
} }
@@ -177,6 +177,16 @@ class ReadBookController(
newRefs.readView.upBattery(activity.sysBattery) 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() { fun onRouteInitialized() {
applyReadBrightness() applyReadBrightness()
upScreenTimeOut() upScreenTimeOut()
@@ -261,7 +271,10 @@ class ReadBookController(
ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud) 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) state.isShowingSearchResult -> viewModel.onIntent(ReadBookIntent.ShowSearchMenu)
else -> viewModel.onIntent(ReadBookIntent.ShowMenu) else -> viewModel.onIntent(ReadBookIntent.ShowMenu)
} }
@@ -833,6 +846,8 @@ class ReadBookController(
is ReadBookEffect.OpenHttpTtsImportPicker, is ReadBookEffect.OpenHttpTtsImportPicker,
is ReadBookEffect.OpenHttpTtsExportPicker, is ReadBookEffect.OpenHttpTtsExportPicker,
is ReadBookEffect.OpenHttpTtsLogin, is ReadBookEffect.OpenHttpTtsLogin,
is ReadBookEffect.OpenHighlightRuleImportPicker,
is ReadBookEffect.OpenHighlightRuleExportPicker,
is ReadBookEffect.TtsCacheCleared, is ReadBookEffect.TtsCacheCleared,
is ReadBookEffect.ExportJson, is ReadBookEffect.ExportJson,
// DB query + bookmark effects — handled by ViewModel, ignored here // DB query + bookmark effects — handled by ViewModel, ignored here
@@ -908,6 +923,9 @@ class ReadBookController(
stopAutoPage() stopAutoPage()
} else { } else {
refs?.readView?.autoPager?.start() refs?.readView?.autoPager?.start()
if (viewModel.uiState.value.menuVisible) {
refs?.readView?.autoPager?.pause()
}
viewModel.setAutoPage(true) viewModel.setAutoPage(true)
onScreenOffTimerStart?.invoke() onScreenOffTimerStart?.invoke()
} }
@@ -122,6 +122,10 @@ fun ReadBookRouteScreen(
state.menuConfig.readMenuBottomBarBlurMode == ReadMenuBlurMode.LiquidGlass state.menuConfig.readMenuBottomBarBlurMode == ReadMenuBlurMode.LiquidGlass
) )
LaunchedEffect(state.menuVisible) {
controller.onMenuVisibilityChanged(state.menuVisible)
}
// ── ActivityResult Launchers ────────────────────────────────────── // ── ActivityResult Launchers ──────────────────────────────────────
val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result -> val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result ->
@@ -235,6 +239,18 @@ fun ReadBookRouteScreen(
uri?.let { viewModel.onIntent(ReadBookIntent.ExportHttpTtsToFile(it)) } 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( val bookInfoLauncher = rememberLauncherForActivityResult(
StartActivityContract(BookInfoActivity::class.java) StartActivityContract(BookInfoActivity::class.java)
) { result -> ) { result ->
@@ -408,6 +424,19 @@ fun ReadBookRouteScreen(
exportHttpTtsPicker.launch("httpTTS.json") 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) // All other effects — delegate to bridge (View/Window/Activity operations)
else -> controller.handleEffect(effect) 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.ImageSaveUtils
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.StringUtils import io.legado.app.utils.StringUtils
import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.hexString import io.legado.app.utils.hexString
import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isAbsUrl
@@ -410,6 +411,7 @@ class ReadBookViewModel(
editingRule = null, editingRule = null,
showNewRule = false, showNewRule = false,
deleteRule = null, deleteRule = null,
importState = BaseImportUiState.Idle,
), ),
) )
} else { } else {
@@ -658,6 +660,36 @@ class ReadBookViewModel(
is ReadBookIntent.DismissDeleteHighlightRule -> _uiState.update { is ReadBookIntent.DismissDeleteHighlightRule -> _uiState.update {
it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(deleteRule = null)) 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.SaveMenuCustomIcon -> saveMenuCustomIcon(intent.id, intent.uri)
is ReadBookIntent.SaveTitleBarCustomIcon -> saveTitleBarCustomIcon(intent.id, intent.uri) is ReadBookIntent.SaveTitleBarCustomIcon -> saveTitleBarCustomIcon(intent.id, intent.uri)
is ReadBookIntent.OpenMenuCustomIconPicker -> { is ReadBookIntent.OpenMenuCustomIconPicker -> {
@@ -3306,6 +3338,12 @@ class ReadBookViewModel(
readSettingsRepository.setReadBodyToLh(update.value) readSettingsRepository.setReadBodyToLh(update.value)
} }
} }
is ConfigUpdate.DefaultSourceChangeAll -> {
ReadConfig.defaultSourceChangeAll = update.value
viewModelScope.launch {
readSettingsRepository.setDefaultSourceChangeAll(update.value)
}
}
is ConfigUpdate.TextFullJustify -> { is ConfigUpdate.TextFullJustify -> {
ReadBookConfig.textFullJustify = update.value ReadBookConfig.textFullJustify = update.value
viewModelScope.launch { 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() { private fun loadHighlightRules() {
val configName = ReadBookConfig.durConfig.name val configName = ReadBookConfig.durConfig.name
_uiState.update { _uiState.update {
@@ -3535,6 +3790,7 @@ class ReadBookViewModel(
editingRule = null, editingRule = null,
showNewRule = false, showNewRule = false,
deleteRule = null, 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.ChapterProvider
import io.legado.app.ui.book.read.page.provider.TextPageFactory import io.legado.app.ui.book.read.page.provider.TextPageFactory
import io.legado.app.ui.config.readConfig.ReadConfig 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.ui.widget.dialog.PhotoDialog
import io.legado.app.utils.activity import io.legado.app.utils.activity
import io.legado.app.utils.dpToPx 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) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh) super.onSizeChanged(w, h, oldw, oldh)
if (!isMainView) return if (!isMainView || MainNavigator.backNavigationInProgress) return
ChapterProvider.upViewSize(w, h) ChapterProvider.upViewSize(w, h)
textPage.format() textPage.format()
} }
@@ -1,22 +1,33 @@
package io.legado.app.ui.book.read.sheet package io.legado.app.ui.book.read.sheet
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn 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.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.HighlightRule import io.legado.app.data.entities.HighlightRule
import io.legado.app.data.repository.configNames 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.TinySwitch
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.series.SmallTonalButton import io.legado.app.ui.widget.components.button.series.SmallTonalButton
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.importComponents.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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.TinySettingItem import io.legado.app.ui.widget.components.settingItem.TinySettingItem
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable @Composable
fun HighlightRuleConfigSheet( fun HighlightRuleConfigSheet(
@@ -35,16 +53,105 @@ fun HighlightRuleConfigSheet(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onIntent: (ReadBookIntent) -> 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( AppModalBottomSheet(
show = show, show = show,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = stringResource(R.string.highlight_rule_config), title = stringResource(R.string.highlight_rule_config),
endAction = { startAction = {
SmallTonalButton( SmallTonalButton(
onClick = { onIntent(ReadBookIntent.AddHighlightRule) }, onClick = { onIntent(ReadBookIntent.AddHighlightRule) },
icon = Icons.Default.Add 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( Column(
modifier = Modifier modifier = Modifier
@@ -52,26 +159,34 @@ fun HighlightRuleConfigSheet(
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
) { ) {
LazyColumn( LazyColumn(
state = listState,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.weight(1f, fill = false), modifier = Modifier.weight(1f, fill = false),
) { ) {
itemsIndexed(state.rules, key = { _, rule -> rule.id }) { _, rule -> items(state.rules, key = { it.id }) { rule ->
HighlightRuleItem( ReorderableItem(reorderableState, key = rule.id) { isDragging ->
rule = rule, HighlightRuleItem(
onToggle = { enabled -> rule = rule,
onIntent(ReadBookIntent.ToggleHighlightRule(rule, enabled)) onToggle = { enabled ->
}, onIntent(ReadBookIntent.ToggleHighlightRule(rule, enabled))
onEditClick = { onIntent(ReadBookIntent.EditHighlightRule(rule)) }, },
onDeleteClick = { onEditClick = {
onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule)) 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 val editingRuleValue = state.editingRule
HighlightRuleEditSheet( HighlightRuleEditSheet(
@@ -83,7 +198,6 @@ fun HighlightRuleConfigSheet(
}, },
) )
// Add new rule
HighlightRuleEditSheet( HighlightRuleEditSheet(
show = show && state.showNewRule, show = show && state.showNewRule,
rule = null, rule = null,
@@ -93,7 +207,6 @@ fun HighlightRuleConfigSheet(
}, },
) )
// Delete confirmation
val deletingRule = state.deleteRule val deletingRule = state.deleteRule
val sureDeleteText = stringResource(R.string.sure_delete) val sureDeleteText = stringResource(R.string.sure_delete)
AppAlertDialog( AppAlertDialog(
@@ -106,7 +219,6 @@ fun HighlightRuleConfigSheet(
dismissText = stringResource(android.R.string.cancel), dismissText = stringResource(android.R.string.cancel),
onDismiss = { onIntent(ReadBookIntent.DismissDeleteHighlightRule) }, onDismiss = { onIntent(ReadBookIntent.DismissDeleteHighlightRule) },
) )
} }
@Composable @Composable
@@ -115,12 +227,14 @@ private fun HighlightRuleItem(
onToggle: (Boolean) -> Unit, onToggle: (Boolean) -> Unit,
onEditClick: () -> Unit, onEditClick: () -> Unit,
onDeleteClick: () -> Unit, onDeleteClick: () -> Unit,
modifier: Modifier = Modifier,
) { ) {
val configLabel = rule.configName?.configNames()?.joinToString("") ?: "全局" val configLabel = rule.configName?.configNames()?.joinToString("") ?: "全局"
TinySettingItem( TinySettingItem(
title = rule.name.ifBlank { rule.displayPattern() }, title = rule.name.ifBlank { rule.displayPattern() },
description = "${rule.styleSummary()} · $configLabel", description = "${rule.styleSummary()} · $configLabel",
onClick = onEditClick, onClick = onEditClick,
modifier = modifier,
trailingContent = { trailingContent = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
TinySwitch( TinySwitch(
@@ -125,6 +125,9 @@ fun MoreConfigSheet(
onAutoChangeSourceChange = { onAutoChangeSourceChange = {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoChangeSource(it))) onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoChangeSource(it)))
}, },
onDefaultSourceChangeAllChange = {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.DefaultSourceChangeAll(it)))
},
onAutoSuggestDayNightChange = { onAutoSuggestDayNightChange = {
onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoSuggestDayNight(it))) onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AutoSuggestDayNight(it)))
}, },
@@ -301,6 +304,7 @@ private fun OtherSettings(
onSliderVibratorChange: (Boolean) -> Unit, onSliderVibratorChange: (Boolean) -> Unit,
onSelectVibratorChange: (Boolean) -> Unit, onSelectVibratorChange: (Boolean) -> Unit,
onAutoChangeSourceChange: (Boolean) -> Unit, onAutoChangeSourceChange: (Boolean) -> Unit,
onDefaultSourceChangeAllChange: (Boolean) -> Unit,
onAutoSuggestDayNightChange: (Boolean) -> Unit, onAutoSuggestDayNightChange: (Boolean) -> Unit,
onSelectTextChange: (Boolean) -> Unit, onSelectTextChange: (Boolean) -> Unit,
onNoAnimScrollPageChange: (Boolean) -> Unit, onNoAnimScrollPageChange: (Boolean) -> Unit,
@@ -333,6 +337,12 @@ private fun OtherSettings(
checked = preferences.autoChangeSource, checked = preferences.autoChangeSource,
onCheckedChange = onAutoChangeSourceChange, onCheckedChange = onAutoChangeSourceChange,
) )
TinySwitchSettingItem(
title = stringResource(R.string.read_change_all),
description = stringResource(R.string.read_change_all_s),
checked = preferences.defaultSourceChangeAll,
onCheckedChange = onDefaultSourceChangeAllChange,
)
TinySwitchSettingItem( TinySwitchSettingItem(
title = stringResource(R.string.auto_switch_theme_reminder_title), title = stringResource(R.string.auto_switch_theme_reminder_title),
description = stringResource(R.string.auto_switch_theme_reminder_desc), description = stringResource(R.string.auto_switch_theme_reminder_desc),
@@ -14,7 +14,8 @@ import kotlinx.coroutines.launch
object MainNavigator { object MainNavigator {
private var backNavigationInProgress = false var backNavigationInProgress = false
private set
private val navigationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) private val navigationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private var backNavigationResetJob: Job? = null private var backNavigationResetJob: Job? = null