fix: 书签问题与高亮规则代码优化
This commit is contained in:
@@ -41,8 +41,8 @@ import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.data.entities.Cache
|
||||
import io.legado.app.data.entities.Cookie
|
||||
import io.legado.app.data.entities.DictRule
|
||||
import io.legado.app.data.entities.HomepageCustomSet
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.entities.HomepageCustomSet
|
||||
import io.legado.app.data.entities.HomepageModule
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.data.entities.KeyboardAssist
|
||||
@@ -75,7 +75,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 92,
|
||||
version = 91,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -134,8 +134,7 @@ val appDb by lazy {
|
||||
AutoMigration(from = 87, to = 88),
|
||||
AutoMigration(from = 88, to = 89),
|
||||
AutoMigration(from = 89, to = 90),
|
||||
AutoMigration(from = 90, to = 91),
|
||||
AutoMigration(from = 91, to = 92)
|
||||
AutoMigration(from = 90, to = 91)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
+83
-56
@@ -1,9 +1,10 @@
|
||||
package io.legado.app.ui.book.read.config
|
||||
package io.legado.app.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.dao.HighlightRuleDao
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
@@ -11,9 +12,14 @@ import io.legado.app.utils.putPrefBoolean
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
object HighlightRuleStore {
|
||||
class HighlightRuleRepository(
|
||||
private val dao: HighlightRuleDao = appDb.highlightRuleDao,
|
||||
private val context: Context = appCtx,
|
||||
) {
|
||||
|
||||
const val backupFileName = "highlightRule.json"
|
||||
companion object {
|
||||
const val backupFileName = "highlightRule.json"
|
||||
}
|
||||
|
||||
data class BackupData(
|
||||
val rules: List<HighlightRule> = emptyList(),
|
||||
@@ -22,24 +28,19 @@ object HighlightRuleStore {
|
||||
val bracketNoteEnabled: Boolean = true,
|
||||
)
|
||||
|
||||
private val dao get() = appDb.highlightRuleDao
|
||||
|
||||
fun load(): List<HighlightRule> {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
fun load(configName: String): List<HighlightRule> {
|
||||
return dao.getAll().filter { it.matchesConfig(configName) }
|
||||
}
|
||||
|
||||
fun loadEnabled(): List<HighlightRule> {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
fun loadEnabled(configName: String): List<HighlightRule> {
|
||||
return dao.getEnabled().filter { it.matchesConfig(configName) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前排版的规则。
|
||||
* 保存指定排版的规则。
|
||||
* 仅替换当前排版绑定的规则,不影响其他排版的规则。
|
||||
*/
|
||||
fun save(rules: List<HighlightRule>) {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
fun save(configName: String, rules: List<HighlightRule>) {
|
||||
saveForConfig(rules, configName.ifBlank { null })
|
||||
}
|
||||
|
||||
@@ -59,11 +60,16 @@ object HighlightRuleStore {
|
||||
}
|
||||
val newIds = sanitized.map { it.id }.toSet()
|
||||
// 被移除的旧规则:从 configName 列表中去掉当前排版
|
||||
// 未绑定任何排版时保留规则(configName="[]"),不删除
|
||||
// 未绑定任何排版时删除,避免留下无法在管理界面看到的规则
|
||||
for (old in oldBound) {
|
||||
if (old.id !in newIds) {
|
||||
val remaining = old.configName.orEmpty().configNames().filter { it != configName }
|
||||
dao.update(old.copy(configName = remaining.toJsonArray()))
|
||||
val remaining =
|
||||
old.configName.orEmpty().configNames().filter { it != configName }
|
||||
if (remaining.isEmpty()) {
|
||||
dao.delete(old)
|
||||
} else {
|
||||
dao.update(old.copy(configName = remaining.toJsonArray()))
|
||||
}
|
||||
}
|
||||
}
|
||||
// 插入所有规则(全局规则也一起,否则会被 replaceGlobal 删掉)
|
||||
@@ -72,31 +78,46 @@ object HighlightRuleStore {
|
||||
cleanupUnusedBgImages()
|
||||
}
|
||||
|
||||
fun update(rule: HighlightRule) {
|
||||
dao.update(sanitizeRule(rule))
|
||||
}
|
||||
|
||||
fun delete(rule: HighlightRule) {
|
||||
dao.delete(rule)
|
||||
cleanupUnusedBgImages()
|
||||
}
|
||||
|
||||
fun reset(): List<HighlightRule> {
|
||||
val defaults = createDefaultRules()
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
if (configName.isBlank()) {
|
||||
dao.replaceGlobal(defaults)
|
||||
} else {
|
||||
saveForConfig(defaults, configName)
|
||||
fun removeConfigBinding(configName: String) {
|
||||
if (configName.isBlank()) return
|
||||
dao.getAll().forEach { rule ->
|
||||
val names = rule.configName.orEmpty().configNames()
|
||||
if (configName in names) {
|
||||
val remaining = names.filter { it != configName }
|
||||
val updatedConfigName = remaining.takeIf { it.isNotEmpty() }?.toJsonArray()
|
||||
dao.update(rule.copy(configName = updatedConfigName))
|
||||
}
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
fun createBackupData(): BackupData {
|
||||
fun reset(configName: String): List<HighlightRule> {
|
||||
val defaults = createDefaultRules()
|
||||
val rules = if (configName.isBlank()) {
|
||||
defaults
|
||||
} else {
|
||||
defaults.map {
|
||||
it.copyWithNewId().copy(configName = listOf(configName).toJsonArray())
|
||||
}
|
||||
}
|
||||
if (configName.isBlank()) {
|
||||
dao.replaceGlobal(rules)
|
||||
} else {
|
||||
saveForConfig(rules, configName)
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
fun createBackupData(configName: String): BackupData {
|
||||
return BackupData(
|
||||
rules = load(),
|
||||
dialogEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleDialog, true),
|
||||
bookTitleEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleBookTitle, true),
|
||||
bracketNoteEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleBracketNote, true),
|
||||
rules = load(configName),
|
||||
dialogEnabled = context.getPrefBoolean(PreferKey.highlightRuleDialog, true),
|
||||
bookTitleEnabled = context.getPrefBoolean(PreferKey.highlightRuleBookTitle, true),
|
||||
bracketNoteEnabled = context.getPrefBoolean(PreferKey.highlightRuleBracketNote, true),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,9 +130,9 @@ object HighlightRuleStore {
|
||||
// 备份恢复是全量替换
|
||||
dao.replaceAll(rules)
|
||||
cleanupUnusedBgImages()
|
||||
appCtx.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled)
|
||||
appCtx.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled)
|
||||
appCtx.putPrefBoolean(PreferKey.highlightRuleBracketNote, backupData.bracketNoteEnabled)
|
||||
context.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled)
|
||||
context.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled)
|
||||
context.putPrefBoolean(PreferKey.highlightRuleBracketNote, backupData.bracketNoteEnabled)
|
||||
}
|
||||
|
||||
fun sanitizeRule(rule: HighlightRule): HighlightRule {
|
||||
@@ -119,22 +140,30 @@ object HighlightRuleStore {
|
||||
val pattern = runCatching { rule.pattern }.getOrNull().orEmpty()
|
||||
val sampleText = runCatching { rule.sampleText }.getOrNull().orEmpty()
|
||||
val id = runCatching { rule.id }.getOrNull().orEmpty().ifBlank {
|
||||
"${System.currentTimeMillis()}_${listOf(name, pattern).joinToString("|").hashCode().toUInt().toString(16)}"
|
||||
"${System.currentTimeMillis()}_${
|
||||
listOf(name, pattern).joinToString("|").hashCode().toUInt().toString(16)
|
||||
}"
|
||||
}
|
||||
return HighlightRule(
|
||||
id = id,
|
||||
name = name,
|
||||
pattern = pattern,
|
||||
sampleText = sampleText,
|
||||
targetScope = normalizeTargetScope(runCatching { rule.targetScope }.getOrDefault(HighlightRule.TARGET_ALL)),
|
||||
targetScope = normalizeTargetScope(
|
||||
runCatching { rule.targetScope }.getOrDefault(
|
||||
HighlightRule.TARGET_ALL
|
||||
)
|
||||
),
|
||||
enabled = runCatching { rule.enabled }.getOrDefault(true),
|
||||
position = runCatching { rule.position }.getOrDefault(0),
|
||||
textColor = runCatching { rule.textColor }.getOrNull(),
|
||||
bgColor = runCatching { rule.bgColor }.getOrNull(),
|
||||
underlineMode = runCatching { rule.underlineMode }.getOrDefault(0).coerceIn(0, 5),
|
||||
underlineColor = runCatching { rule.underlineColor }.getOrNull(),
|
||||
underlineWidth = runCatching { rule.underlineWidth }.getOrDefault(1f).coerceIn(0.1f, 10f),
|
||||
underlineOffset = runCatching { rule.underlineOffset }.getOrDefault(2f).coerceIn(0f, 20f),
|
||||
underlineWidth = runCatching { rule.underlineWidth }.getOrDefault(1f)
|
||||
.coerceIn(0.1f, 10f),
|
||||
underlineOffset = runCatching { rule.underlineOffset }.getOrDefault(2f)
|
||||
.coerceIn(0f, 20f),
|
||||
underlineSvgPath = runCatching { rule.underlineSvgPath }.getOrNull(),
|
||||
bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
|
||||
bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2),
|
||||
@@ -149,12 +178,13 @@ object HighlightRuleStore {
|
||||
HighlightRule.TARGET_ALL,
|
||||
HighlightRule.TARGET_TITLE,
|
||||
HighlightRule.TARGET_BODY -> value
|
||||
|
||||
else -> fallback
|
||||
}
|
||||
}
|
||||
|
||||
fun createDefaultRules(): List<HighlightRule> {
|
||||
val ctx = appCtx
|
||||
val ctx = context
|
||||
return listOf(
|
||||
HighlightRule(
|
||||
id = "dialog_default",
|
||||
@@ -288,7 +318,7 @@ object HighlightRuleStore {
|
||||
val usedPaths = allRules.mapNotNull { it.bgImage }
|
||||
.filter { it.isNotBlank() && !it.startsWith("assets://") }
|
||||
.toSet()
|
||||
val dir = File(appCtx.filesDir, "bg_images")
|
||||
val dir = File(context.filesDir, "bg_images")
|
||||
if (!dir.exists()) return
|
||||
dir.listFiles()?.forEach { file ->
|
||||
if (file.absolutePath !in usedPaths) {
|
||||
@@ -304,7 +334,7 @@ object HighlightRuleStore {
|
||||
val backupFile = File(rootPath, "highlightRuleBg${File.separator}${File(path).name}")
|
||||
.takeIf { it.exists() && it.isFile }
|
||||
?: return path
|
||||
val dir = File(appCtx.filesDir, "bg_images")
|
||||
val dir = File(context.filesDir, "bg_images")
|
||||
if (!dir.exists()) dir.mkdirs()
|
||||
val targetFile = File(dir, backupFile.name)
|
||||
if (!targetFile.exists() || targetFile.length() != backupFile.length()) {
|
||||
@@ -329,18 +359,15 @@ object HighlightRuleStore {
|
||||
/**
|
||||
* 解析 configName JSON 数组为列表。
|
||||
*/
|
||||
fun String.configNames(): List<String> {
|
||||
return runCatching {
|
||||
GSON.fromJsonArray<String>(this).getOrNull() ?: emptyList()
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 将排版名列表转为 JSON 数组字符串。
|
||||
*/
|
||||
fun List<String>.toJsonArray(): String {
|
||||
return GSON.toJson(this)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
fun String.configNames(): List<String> {
|
||||
return runCatching {
|
||||
GSON.fromJsonArray<String>(this).getOrNull() ?: emptyList()
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
fun List<String>.toJsonArray(): String {
|
||||
return GSON.toJson(this)
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import java.io.InputStream
|
||||
* keeps a full layout/background/text configuration. This wrapper centralizes
|
||||
* file mutations so UI and ViewModel code do not call persistence helpers directly.
|
||||
*/
|
||||
class ReadBookStyleConfigRepository {
|
||||
class ReadBookStyleConfigRepository(
|
||||
private val readStyleRepository: ReadStyleRepository,
|
||||
private val highlightRuleRepository: HighlightRuleRepository,
|
||||
) {
|
||||
|
||||
fun save() {
|
||||
ReadBookConfig.save()
|
||||
@@ -23,20 +26,25 @@ class ReadBookStyleConfigRepository {
|
||||
}
|
||||
|
||||
fun deleteCurrentStyle(): Boolean {
|
||||
val deletedConfigName = ReadBookConfig.durConfig.name
|
||||
val deleted = ReadBookConfig.deleteDur()
|
||||
if (deleted) {
|
||||
highlightRuleRepository.removeConfigBinding(deletedConfigName)
|
||||
save()
|
||||
}
|
||||
return deleted
|
||||
}
|
||||
|
||||
fun importCurrentStyle(bytes: ByteArray) {
|
||||
ReadBookConfig.durConfig = ReadBookConfig.import(bytes)
|
||||
ReadBookConfig.durConfig = readStyleRepository.import(bytes)
|
||||
save()
|
||||
}
|
||||
|
||||
fun exportCurrentStyle(): ByteArray {
|
||||
return ReadBookConfig.export()
|
||||
val config = ReadBookConfig.getExportConfig().copy(
|
||||
highlightRules = ArrayList(highlightRuleRepository.load(ReadBookConfig.durConfig.name))
|
||||
)
|
||||
return readStyleRepository.export(config)
|
||||
}
|
||||
|
||||
fun saveBackgroundImage(inputStream: InputStream, displayName: String?): String {
|
||||
|
||||
@@ -5,7 +5,6 @@ import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.DefaultData
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.config.ReadStyleResolver
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.compress.ZipUtils
|
||||
@@ -20,7 +19,9 @@ import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
class ReadStyleRepository {
|
||||
class ReadStyleRepository(
|
||||
private val highlightRuleRepository: HighlightRuleRepository,
|
||||
) {
|
||||
|
||||
val configFilePath: String =
|
||||
FileUtils.getPath(appCtx.filesDir, ReadBookConfig.configFileName)
|
||||
@@ -199,7 +200,15 @@ class ReadStyleRepository {
|
||||
config.curTextShadowColor()
|
||||
if (config.highlightRules.isNotEmpty()) {
|
||||
val targetConfigName = config.name.ifBlank { null }
|
||||
HighlightRuleStore.saveForConfig(config.highlightRules, targetConfigName)
|
||||
val highlightRules = config.highlightRules.map { rule ->
|
||||
if (targetConfigName.isNullOrBlank()) {
|
||||
rule.copy(configName = null)
|
||||
} else {
|
||||
rule.copyWithNewId().copy(configName = listOf(targetConfigName).toJsonArray())
|
||||
}
|
||||
}
|
||||
config.highlightRules = ArrayList(highlightRules)
|
||||
highlightRuleRepository.saveForConfig(highlightRules, targetConfigName)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ import io.legado.app.data.repository.DictionaryRepositoryImpl
|
||||
import io.legado.app.data.repository.DirectLinkUploadRepository
|
||||
import io.legado.app.data.repository.ExploreRepository
|
||||
import io.legado.app.data.repository.ExploreRepositoryImpl
|
||||
import io.legado.app.data.repository.HighlightRuleRepository
|
||||
import io.legado.app.data.repository.HomepageModulesRepository
|
||||
import io.legado.app.data.repository.LlmTranslateRepositoryImpl
|
||||
import io.legado.app.data.repository.LocalBookRepository
|
||||
import io.legado.app.data.repository.ReadBookStyleConfigRepository
|
||||
import io.legado.app.data.repository.ReadAloudSettingsRepository
|
||||
import io.legado.app.data.repository.ReadBookStyleConfigRepository
|
||||
import io.legado.app.data.repository.ReadRecordRepository
|
||||
import io.legado.app.data.repository.ReadSettingsRepository
|
||||
import io.legado.app.data.repository.ReadStyleRepository
|
||||
import io.legado.app.data.repository.RemoteBookRepository
|
||||
import io.legado.app.data.repository.RssRepository
|
||||
import io.legado.app.data.repository.SearchContentRepository
|
||||
@@ -155,6 +157,8 @@ val appModule = module {
|
||||
singleOf(::SettingsRepository)
|
||||
singleOf(::ReadSettingsRepository)
|
||||
singleOf(::ReadAloudSettingsRepository)
|
||||
singleOf(::HighlightRuleRepository)
|
||||
singleOf(::ReadStyleRepository)
|
||||
singleOf(::ReadBookStyleConfigRepository)
|
||||
singleOf(::LocalPreferencesRepository)
|
||||
singleOf(::ExploreBooksUseCase)
|
||||
@@ -267,7 +271,8 @@ val appModule = module {
|
||||
readSettingsRepository = get(),
|
||||
readBookStyleConfigRepository = get(),
|
||||
readAloudSettingsRepository = get(),
|
||||
localPreferencesRepository = get()
|
||||
localPreferencesRepository = get(),
|
||||
highlightRuleRepository = get()
|
||||
)
|
||||
}
|
||||
viewModelOf(::ChangeCoverViewModel)
|
||||
|
||||
@@ -6,16 +6,15 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.core.graphics.toColorInt
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.PageAnim
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.ReadMenuBlurMode
|
||||
import io.legado.app.constant.ReadMenuBlurStyle
|
||||
import androidx.compose.runtime.State
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.repository.ReadStyleRepository
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.help.DefaultData
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.ui.config.PrefDelegate
|
||||
@@ -24,6 +23,7 @@ import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.getMeanColor
|
||||
import io.legado.app.utils.hexString
|
||||
import org.koin.core.context.GlobalContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.InputStream
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
@@ -35,7 +35,8 @@ import kotlin.reflect.KProperty
|
||||
@Suppress("ConstPropertyName")
|
||||
@Keep
|
||||
object ReadBookConfig {
|
||||
private val readStyleRepository = ReadStyleRepository()
|
||||
private val readStyleRepository: ReadStyleRepository
|
||||
get() = GlobalContext.get().get()
|
||||
|
||||
// region prefDelegate helpers
|
||||
|
||||
@@ -773,7 +774,7 @@ object ReadBookConfig {
|
||||
// endregion
|
||||
|
||||
fun getExportConfig(): Config {
|
||||
val exportConfig = durConfig.copy(highlightRules = ArrayList(HighlightRuleStore.load()))
|
||||
val exportConfig = durConfig.copy(highlightRules = arrayListOf())
|
||||
if (shareLayout) {
|
||||
exportConfig.textFont = shareConfig.textFont
|
||||
exportConfig.titleFont = shareConfig.titleFont
|
||||
|
||||
@@ -5,7 +5,6 @@ object ReadBookColorPickerIds {
|
||||
const val TEXT_COLOR = 121
|
||||
const val TEXT_ACCENT_COLOR = 125
|
||||
const val BG_COLOR = 122
|
||||
const val HIGHLIGHT_RULE_COLOR = 7900
|
||||
const val TITLE_COLOR = 7896
|
||||
const val TIP_HEADER_COLOR = 7897
|
||||
const val TIP_DIVIDER_COLOR = 7898
|
||||
@@ -13,5 +12,4 @@ object ReadBookColorPickerIds {
|
||||
const val MENU_BG_COLOR = 114
|
||||
const val MENU_ACCENT_COLOR = 514
|
||||
const val UNDERLINE_COLOR = 810
|
||||
var pendingHighlightRulePosition = -1
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||
@@ -153,11 +155,20 @@ data class ReadBookUiState(
|
||||
val styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
|
||||
// Menu config (from ReadBookConfig via repository)
|
||||
val menuConfig: ReadMenuConfig = ReadMenuConfig(),
|
||||
val highlightRuleConfig: HighlightRuleConfigUiState = HighlightRuleConfigUiState(),
|
||||
) {
|
||||
val menuVisible: Boolean
|
||||
get() = menuState.visible
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class HighlightRuleConfigUiState(
|
||||
val rules: ImmutableList<HighlightRule> = persistentListOf(),
|
||||
val editingRule: HighlightRule? = null,
|
||||
val showNewRule: Boolean = false,
|
||||
val deleteRule: HighlightRule? = null,
|
||||
)
|
||||
|
||||
@Stable
|
||||
data class ReadMenuConfig(
|
||||
val titleBarIconPosition: Int = 0,
|
||||
@@ -381,6 +392,16 @@ sealed interface ReadBookIntent {
|
||||
// Typed config mutation — single entry point for all ReadBookConfig changes
|
||||
data class UpdateConfig(val update: ConfigUpdate) : ReadBookIntent
|
||||
|
||||
// Highlight rules
|
||||
data object AddHighlightRule : ReadBookIntent
|
||||
data class EditHighlightRule(val rule: HighlightRule) : ReadBookIntent
|
||||
data class ToggleHighlightRule(val rule: HighlightRule, val enabled: Boolean) : ReadBookIntent
|
||||
data class SaveHighlightRule(val rule: HighlightRule) : ReadBookIntent
|
||||
data object DismissHighlightRuleEdit : ReadBookIntent
|
||||
data class RequestDeleteHighlightRule(val rule: HighlightRule) : ReadBookIntent
|
||||
data object ConfirmDeleteHighlightRule : ReadBookIntent
|
||||
data object DismissDeleteHighlightRule : ReadBookIntent
|
||||
|
||||
// Icon picker — file IO handled by ViewModel
|
||||
data class SaveMenuCustomIcon(val id: String, val uri: Uri) : ReadBookIntent
|
||||
data class SaveTitleBarCustomIcon(val id: String, val uri: Uri) : ReadBookIntent
|
||||
@@ -398,7 +419,7 @@ sealed interface ReadBookIntent {
|
||||
// Default font picker (needs Activity for AlertDialog)
|
||||
// Text action menu (moved from Activity)
|
||||
data class TextActionAloud(val text: String) : ReadBookIntent
|
||||
data class TextActionBookmark(val text: String) : ReadBookIntent
|
||||
data class TextActionBookmark(val bookmark: Bookmark) : ReadBookIntent
|
||||
data class TextActionReplace(val text: String) : ReadBookIntent
|
||||
data class TextActionSearchContent(val text: String) : ReadBookIntent
|
||||
data class TextActionDict(val text: String) : ReadBookIntent
|
||||
@@ -1032,11 +1053,6 @@ sealed interface ConfigUpdate {
|
||||
override val actions = emptySet<ConfigUpdateAction>()
|
||||
}
|
||||
|
||||
// --- Highlight rules ---
|
||||
data class HighlightRules(val rules: List<io.legado.app.data.entities.HighlightRule>) : ConfigUpdate {
|
||||
override val actions = setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
|
||||
}
|
||||
|
||||
// --- Auto read ---
|
||||
data class AutoReadSpeed(val value: Int) : ConfigUpdate {
|
||||
override val actions = emptySet<ConfigUpdateAction>()
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.legado.app.ui.book.read
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
@@ -43,7 +42,6 @@ import io.legado.app.ui.book.read.page.ReadView
|
||||
import io.legado.app.ui.book.read.page.entities.PageDirection
|
||||
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.searchContent.SearchResult
|
||||
import io.legado.app.ui.login.SourceLoginJsExtensions
|
||||
import io.legado.app.ui.widget.PopupAction
|
||||
import io.legado.app.utils.ColorUtils
|
||||
@@ -529,7 +527,9 @@ class ReadBookController(
|
||||
}
|
||||
|
||||
R.id.menu_bookmark -> {
|
||||
viewModel.onIntent(ReadBookIntent.TextActionBookmark(selectedText))
|
||||
refs?.readView?.curPage?.createBookmark()?.let {
|
||||
viewModel.onIntent(ReadBookIntent.TextActionBookmark(it))
|
||||
} ?: activity.toastOnUi(R.string.create_bookmark_error)
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.widget.components.log.AppLogSheet
|
||||
import io.legado.app.ui.book.read.sheet.BgTextConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.ChangeChapterSourceSheet
|
||||
import io.legado.app.ui.book.read.sheet.CharsetConfigSheet
|
||||
@@ -20,22 +19,23 @@ import io.legado.app.ui.book.read.sheet.DictSheet
|
||||
import io.legado.app.ui.book.read.sheet.DownloadSheet
|
||||
import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
|
||||
import io.legado.app.ui.book.read.sheet.FontSelectSheet
|
||||
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.HttpTtsEditSheet
|
||||
import io.legado.app.ui.book.read.sheet.MoreConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.PageAnimConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.PageKeyConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.PhotoSheet
|
||||
import io.legado.app.ui.book.read.sheet.ReadAloudConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.ShadowSetSheet
|
||||
import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet
|
||||
import io.legado.app.ui.book.read.sheet.HttpTtsEditSheet
|
||||
import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
|
||||
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
|
||||
import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.changeSource.ChangeSourceSheet
|
||||
import io.legado.app.ui.widget.components.log.AppLogSheet
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
||||
@@ -169,6 +169,7 @@ fun ReadBookScreen(
|
||||
)
|
||||
HighlightRuleConfigSheet(
|
||||
show = state.activeSheet is ReadBookSheet.HighlightRuleConfig,
|
||||
state = state.highlightRuleConfig,
|
||||
onDismissRequest = dismissSheet,
|
||||
onIntent = onIntent,
|
||||
)
|
||||
|
||||
@@ -17,12 +17,14 @@ import io.legado.app.constant.ReadMenuBlurMode
|
||||
import io.legado.app.constant.Status
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesKeys
|
||||
import io.legado.app.data.local.preferences.LocalPreferencesRepository
|
||||
import io.legado.app.data.repository.HighlightRuleRepository
|
||||
import io.legado.app.data.repository.ReadAloudSettingsRepository
|
||||
import io.legado.app.data.repository.ReadBookStyleConfigRepository
|
||||
import io.legado.app.data.repository.ReadPreferences
|
||||
@@ -42,7 +44,6 @@ import io.legado.app.help.book.isMobi
|
||||
import io.legado.app.help.book.removeType
|
||||
import io.legado.app.help.book.simulatedTotalChapterNum
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.source.getSourceType
|
||||
@@ -57,7 +58,6 @@ import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.service.BaseReadAloudService
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||
@@ -74,6 +74,7 @@ import io.legado.app.utils.hexString
|
||||
import io.legado.app.utils.isAbsUrl
|
||||
import io.legado.app.utils.isTrue
|
||||
import io.legado.app.utils.mapParallelSafe
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.putPrefInt
|
||||
import io.legado.app.utils.toStringArray
|
||||
@@ -120,7 +121,8 @@ class ReadBookViewModel(
|
||||
private val readSettingsRepository: ReadSettingsRepository,
|
||||
private val readBookStyleConfigRepository: ReadBookStyleConfigRepository,
|
||||
private val readAloudSettingsRepository: ReadAloudSettingsRepository,
|
||||
private val localPreferencesRepository: LocalPreferencesRepository
|
||||
private val localPreferencesRepository: LocalPreferencesRepository,
|
||||
private val highlightRuleRepository: HighlightRuleRepository,
|
||||
) : BaseViewModel(application), ReadBook.CallBack {
|
||||
|
||||
// --- MVI State ---
|
||||
@@ -296,6 +298,9 @@ class ReadBookViewModel(
|
||||
if (intent.sheet is ReadBookSheet.Bookmark) {
|
||||
// Bookmark is shown as a menu route, not a sheet
|
||||
openReadMenuRoute(ReadBookMenuRoute.Bookmark(intent.sheet.bookmark))
|
||||
} else if (intent.sheet is ReadBookSheet.HighlightRuleConfig) {
|
||||
loadHighlightRules()
|
||||
_uiState.update { it.copy(activeSheet = intent.sheet) }
|
||||
} else {
|
||||
_uiState.update { it.copy(activeSheet = intent.sheet) }
|
||||
}
|
||||
@@ -309,6 +314,15 @@ class ReadBookViewModel(
|
||||
contentEditLoading = false,
|
||||
contentEditSaveToSource = false,
|
||||
)
|
||||
} else if (it.activeSheet is ReadBookSheet.HighlightRuleConfig) {
|
||||
it.copy(
|
||||
activeSheet = null,
|
||||
highlightRuleConfig = it.highlightRuleConfig.copy(
|
||||
editingRule = null,
|
||||
showNewRule = false,
|
||||
deleteRule = null,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
it.copy(activeSheet = null)
|
||||
}
|
||||
@@ -506,6 +520,48 @@ class ReadBookViewModel(
|
||||
is ReadBookIntent.UpdateConfig -> {
|
||||
handleConfigUpdate(intent.update)
|
||||
}
|
||||
is ReadBookIntent.AddHighlightRule -> _uiState.update {
|
||||
it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(showNewRule = true))
|
||||
}
|
||||
|
||||
is ReadBookIntent.EditHighlightRule -> _uiState.update {
|
||||
it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(editingRule = intent.rule))
|
||||
}
|
||||
|
||||
is ReadBookIntent.ToggleHighlightRule -> {
|
||||
val rules = _uiState.value.highlightRuleConfig.rules.map {
|
||||
if (it.id == intent.rule.id) it.copy(enabled = intent.enabled) else it
|
||||
}
|
||||
saveHighlightRules(rules)
|
||||
}
|
||||
|
||||
is ReadBookIntent.SaveHighlightRule -> {
|
||||
val currentRules = _uiState.value.highlightRuleConfig.rules
|
||||
val updatedRules = if (currentRules.any { it.id == intent.rule.id }) {
|
||||
currentRules.map { if (it.id == intent.rule.id) intent.rule else it }
|
||||
} else {
|
||||
currentRules + intent.rule
|
||||
}
|
||||
saveHighlightRules(updatedRules)
|
||||
}
|
||||
|
||||
is ReadBookIntent.DismissHighlightRuleEdit -> _uiState.update {
|
||||
it.copy(
|
||||
highlightRuleConfig = it.highlightRuleConfig.copy(
|
||||
editingRule = null,
|
||||
showNewRule = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
is ReadBookIntent.RequestDeleteHighlightRule -> _uiState.update {
|
||||
it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(deleteRule = intent.rule))
|
||||
}
|
||||
|
||||
is ReadBookIntent.ConfirmDeleteHighlightRule -> deletePendingHighlightRule()
|
||||
is ReadBookIntent.DismissDeleteHighlightRule -> _uiState.update {
|
||||
it.copy(highlightRuleConfig = it.highlightRuleConfig.copy(deleteRule = null))
|
||||
}
|
||||
is ReadBookIntent.SaveMenuCustomIcon -> saveMenuCustomIcon(intent.id, intent.uri)
|
||||
is ReadBookIntent.SaveTitleBarCustomIcon -> saveTitleBarCustomIcon(intent.id, intent.uri)
|
||||
is ReadBookIntent.OpenMenuCustomIconPicker -> {
|
||||
@@ -842,28 +898,16 @@ class ReadBookViewModel(
|
||||
}
|
||||
|
||||
is ReadBookIntent.TextActionBookmark -> {
|
||||
val book = ReadBook.book
|
||||
val page = ReadBook.curTextChapter?.getPage(ReadBook.durPageIndex)
|
||||
if (book != null && page != null) {
|
||||
val bookmark = book.createBookMark().apply {
|
||||
chapterIndex = ReadBook.durChapterIndex
|
||||
chapterPos = ReadBook.durChapterPos
|
||||
chapterName = page.title
|
||||
bookText = page.text.replace(Regex("[袮꧁]"), "").trim()
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
menuState = ReadBookMenuState(
|
||||
visible = true,
|
||||
routeStack = kotlinx.collections.immutable.persistentListOf(
|
||||
ReadBookMenuRoute.Main,
|
||||
ReadBookMenuRoute.Bookmark(bookmark),
|
||||
),
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
menuState = ReadBookMenuState(
|
||||
visible = true,
|
||||
routeStack = kotlinx.collections.immutable.persistentListOf(
|
||||
ReadBookMenuRoute.Main,
|
||||
ReadBookMenuRoute.Bookmark(intent.bookmark),
|
||||
),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
context.toastOnUi(R.string.create_bookmark_error)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2685,12 +2729,6 @@ class ReadBookViewModel(
|
||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
||||
}
|
||||
|
||||
// --- Highlight rules ---
|
||||
is ConfigUpdate.HighlightRules -> {
|
||||
HighlightRuleStore.save(update.rules)
|
||||
TextChapterLayout.invalidateRegexCache()
|
||||
}
|
||||
|
||||
// --- Auto read ---
|
||||
is ConfigUpdate.AutoReadSpeed -> {
|
||||
ReadBookConfig.autoReadSpeed = update.value
|
||||
@@ -2709,6 +2747,62 @@ class ReadBookViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadHighlightRules() {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
highlightRuleConfig = it.highlightRuleConfig.copy(
|
||||
rules = highlightRuleRepository.load(configName).toImmutableList(),
|
||||
editingRule = null,
|
||||
showNewRule = false,
|
||||
deleteRule = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveHighlightRules(rules: List<HighlightRule>) {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
val sanitizedRules = rules.map(highlightRuleRepository::sanitizeRule)
|
||||
highlightRuleRepository.save(configName, sanitizedRules)
|
||||
TextChapterLayout.invalidateRegexCache()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
highlightRuleConfig = it.highlightRuleConfig.copy(
|
||||
rules = highlightRuleRepository.load(configName).toImmutableList(),
|
||||
editingRule = null,
|
||||
showNewRule = false,
|
||||
deleteRule = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
_effects.tryEmit(
|
||||
ReadBookEffect.UpdateReadViewConfig(
|
||||
setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun deletePendingHighlightRule() {
|
||||
val rule = _uiState.value.highlightRuleConfig.deleteRule ?: return
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
highlightRuleRepository.delete(rule)
|
||||
TextChapterLayout.invalidateRegexCache()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
highlightRuleConfig = it.highlightRuleConfig.copy(
|
||||
rules = highlightRuleRepository.load(configName).toImmutableList(),
|
||||
deleteRule = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
_effects.tryEmit(
|
||||
ReadBookEffect.UpdateReadViewConfig(
|
||||
setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveMenuCustomIcon(id: String, uri: Uri) {
|
||||
execute {
|
||||
val iconFile = java.io.File(context.filesDir, "read_menu_icons/$id.png")
|
||||
@@ -2925,18 +3019,6 @@ class ReadBookViewModel(
|
||||
))
|
||||
}
|
||||
|
||||
ReadBookColorPickerIds.HIGHLIGHT_RULE_COLOR -> {
|
||||
val pos = ReadBookColorPickerIds.pendingHighlightRulePosition
|
||||
val rules = HighlightRuleStore.load()
|
||||
if (pos in rules.indices) {
|
||||
HighlightRuleStore.update(rules[pos].copy(textColor = color))
|
||||
TextChapterLayout.invalidateRegexCache()
|
||||
_effects.tryEmit(ReadBookEffect.UpdateReadViewConfig(
|
||||
setOf(ConfigUpdateAction.UpdateChapterStyle, ConfigUpdateAction.ReloadContent)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
ReadBookColorPickerIds.MENU_BG_COLOR -> {
|
||||
ReadBookConfig.readMenuBgColor = color
|
||||
viewModelScope.launch {
|
||||
|
||||
@@ -22,13 +22,13 @@ import io.legado.app.constant.PageAnim
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.repository.HighlightRuleRepository
|
||||
import io.legado.app.help.book.BookContent
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.getBookSource
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.ImageProvider
|
||||
import io.legado.app.model.ReadBook
|
||||
@@ -60,6 +60,7 @@ import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.context.GlobalContext
|
||||
import java.util.LinkedList
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@@ -75,20 +76,36 @@ class TextChapterLayout(
|
||||
@Volatile
|
||||
private var cachedHighlightRules: List<CompiledHighlightRule>? = null
|
||||
|
||||
@Volatile
|
||||
private var cachedHighlightRulesConfigName: String? = null
|
||||
|
||||
fun invalidateRegexCache() {
|
||||
cachedHighlightRules = null
|
||||
cachedHighlightRulesConfigName = null
|
||||
}
|
||||
}
|
||||
|
||||
private val compiledHighlightRules: List<CompiledHighlightRule>
|
||||
get() = cachedHighlightRules ?: HighlightRuleStore.loadEnabled().mapNotNull { rule ->
|
||||
runCatching {
|
||||
CompiledHighlightRule(
|
||||
rule = rule,
|
||||
regex = Regex(rule.pattern)
|
||||
)
|
||||
}.getOrNull()
|
||||
}.also { cachedHighlightRules = it }
|
||||
get() {
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
cachedHighlightRules?.takeIf {
|
||||
cachedHighlightRulesConfigName == configName
|
||||
}?.let { return it }
|
||||
return highlightRuleRepository.loadEnabled(configName).mapNotNull { rule ->
|
||||
runCatching {
|
||||
CompiledHighlightRule(
|
||||
rule = rule,
|
||||
regex = Regex(rule.pattern)
|
||||
)
|
||||
}.getOrNull()
|
||||
}.also {
|
||||
cachedHighlightRulesConfigName = configName
|
||||
cachedHighlightRules = it
|
||||
}
|
||||
}
|
||||
|
||||
private val highlightRuleRepository: HighlightRuleRepository
|
||||
get() = GlobalContext.get().get()
|
||||
|
||||
@Volatile
|
||||
private var listener: LayoutProgressListener? = textChapter
|
||||
|
||||
@@ -3,37 +3,27 @@ package io.legado.app.ui.book.read.sheet
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
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.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.repository.configNames
|
||||
import io.legado.app.ui.book.read.HighlightRuleConfigUiState
|
||||
import io.legado.app.ui.book.read.ReadBookIntent
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore.configNames
|
||||
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.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.settingItem.TinySettingItem
|
||||
@@ -41,19 +31,20 @@ import io.legado.app.ui.widget.components.settingItem.TinySettingItem
|
||||
@Composable
|
||||
fun HighlightRuleConfigSheet(
|
||||
show: Boolean,
|
||||
state: HighlightRuleConfigUiState,
|
||||
onDismissRequest: () -> Unit,
|
||||
onIntent: (ReadBookIntent) -> Unit,
|
||||
) {
|
||||
var rules by remember { mutableStateOf(HighlightRuleStore.load()) }
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var deleteIndex by remember { mutableIntStateOf(-1) }
|
||||
var editingRule by remember { mutableStateOf<HighlightRule?>(null) }
|
||||
var showNewRule by remember { mutableStateOf(false) }
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = stringResource(R.string.highlight_rule_config),
|
||||
endAction = {
|
||||
SmallTonalButton(
|
||||
onClick = { onIntent(ReadBookIntent.AddHighlightRule) },
|
||||
icon = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -64,100 +55,63 @@ fun HighlightRuleConfigSheet(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.weight(1f, fill = false),
|
||||
) {
|
||||
itemsIndexed(rules, key = { _, rule -> rule.id }) { index, rule ->
|
||||
itemsIndexed(state.rules, key = { _, rule -> rule.id }) { _, rule ->
|
||||
HighlightRuleItem(
|
||||
rule = rule,
|
||||
onToggle = { enabled ->
|
||||
rules = rules.toMutableList().also {
|
||||
it[index] = it[index].copy(enabled = enabled)
|
||||
}
|
||||
saveRules(rules, onIntent)
|
||||
onIntent(ReadBookIntent.ToggleHighlightRule(rule, enabled))
|
||||
},
|
||||
onEditClick = { editingRule = rule },
|
||||
onEditClick = { onIntent(ReadBookIntent.EditHighlightRule(rule)) },
|
||||
onDeleteClick = {
|
||||
deleteIndex = index
|
||||
showDeleteConfirm = true
|
||||
onIntent(ReadBookIntent.RequestDeleteHighlightRule(rule))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
TextButton(
|
||||
onClick = { showNewRule = true },
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(stringResource(R.string.add))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit existing rule
|
||||
val editingRuleValue = editingRule
|
||||
val editingRuleValue = state.editingRule
|
||||
|
||||
if (show && editingRuleValue != null) {
|
||||
HighlightRuleEditSheet(
|
||||
show = true,
|
||||
rule = editingRuleValue,
|
||||
onDismissRequest = { editingRule = null },
|
||||
onDismissRequest = { onIntent(ReadBookIntent.DismissHighlightRuleEdit) },
|
||||
onSave = { updated ->
|
||||
rules = rules.map { if (it.id == updated.id) updated else it }
|
||||
saveRules(rules, onIntent)
|
||||
editingRule = null
|
||||
onIntent(ReadBookIntent.SaveHighlightRule(updated))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Add new rule
|
||||
if (show && showNewRule) {
|
||||
if (show && state.showNewRule) {
|
||||
HighlightRuleEditSheet(
|
||||
show = true,
|
||||
rule = null,
|
||||
onDismissRequest = { showNewRule = false },
|
||||
onDismissRequest = { onIntent(ReadBookIntent.DismissHighlightRuleEdit) },
|
||||
onSave = { newRule ->
|
||||
rules = rules + newRule
|
||||
saveRules(rules, onIntent)
|
||||
showNewRule = false
|
||||
onIntent(ReadBookIntent.SaveHighlightRule(newRule))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Delete confirmation
|
||||
if (showDeleteConfirm && deleteIndex in rules.indices) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showDeleteConfirm = false
|
||||
deleteIndex = -1
|
||||
},
|
||||
containerColor = io.legado.app.ui.theme.LegadoTheme.colorScheme.surfaceContainer,
|
||||
title = { Text(stringResource(R.string.delete)) },
|
||||
text = { Text(stringResource(R.string.sure_delete) + " \"${rules[deleteIndex].name}\"?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
rules = rules.toMutableList().also { it.removeAt(deleteIndex) }
|
||||
saveRules(rules, onIntent)
|
||||
showDeleteConfirm = false
|
||||
deleteIndex = -1
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showDeleteConfirm = false
|
||||
deleteIndex = -1
|
||||
}) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
val deletingRule = state.deleteRule
|
||||
val sureDeleteText = stringResource(R.string.sure_delete)
|
||||
AppAlertDialog(
|
||||
show = show && deletingRule != null,
|
||||
onDismissRequest = { onIntent(ReadBookIntent.DismissDeleteHighlightRule) },
|
||||
title = stringResource(R.string.delete),
|
||||
text = deletingRule?.let { "$sureDeleteText \"${it.name}\"?" },
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = { onIntent(ReadBookIntent.ConfirmDeleteHighlightRule) },
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { onIntent(ReadBookIntent.DismissDeleteHighlightRule) },
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -191,7 +145,3 @@ private fun HighlightRuleItem(
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveRules(rules: List<HighlightRule>, onIntent: (ReadBookIntent) -> Unit) {
|
||||
onIntent(ReadBookIntent.UpdateConfig(io.legado.app.ui.book.read.ConfigUpdate.HighlightRules(rules)))
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ 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.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -39,13 +40,13 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.HighlightRule
|
||||
import io.legado.app.data.repository.configNames
|
||||
import io.legado.app.data.repository.toJsonArray
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore.configNames
|
||||
import io.legado.app.ui.book.read.config.HighlightRuleStore.toJsonArray
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.SectionTitle
|
||||
import io.legado.app.ui.widget.components.card.NormalCard
|
||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
|
||||
@@ -147,7 +148,7 @@ fun HighlightRuleEditSheet(
|
||||
}
|
||||
}
|
||||
patternError = null
|
||||
val sanitized = HighlightRuleStore.sanitizeRule(
|
||||
onSave(
|
||||
HighlightRule(
|
||||
id = initial.id,
|
||||
name = name,
|
||||
@@ -170,7 +171,6 @@ fun HighlightRuleEditSheet(
|
||||
fontPath = if (hasFont) fontPath.ifBlank { null } else null,
|
||||
)
|
||||
)
|
||||
onSave(sanitized)
|
||||
}) {
|
||||
androidx.compose.material3.Icon(
|
||||
Icons.Default.Done,
|
||||
@@ -372,33 +372,51 @@ fun HighlightRuleEditSheet(
|
||||
// === Section 3: Config Binding ===
|
||||
if (allConfigNames.isNotEmpty()) {
|
||||
SectionTitle("应用排版")
|
||||
AppText(
|
||||
text = if (configNames.isEmpty()) "全局(所有排版生效)"
|
||||
else "已选: ${configNames.joinToString("、")}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
)
|
||||
androidx.compose.foundation.layout.FlowRow(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Global toggle
|
||||
FilterChip(
|
||||
selected = configNames.isEmpty(),
|
||||
onClick = { configNames = emptySet() },
|
||||
label = { AppText("全局") },
|
||||
)
|
||||
allConfigNames.forEach { cn ->
|
||||
FilterChip(
|
||||
selected = cn in configNames,
|
||||
item {
|
||||
val selected = configNames.isEmpty()
|
||||
val bg = if (selected) LegadoTheme.colorScheme.secondaryContainer
|
||||
else LegadoTheme.colorScheme.surfaceContainerLow
|
||||
val fg = if (selected) LegadoTheme.colorScheme.onSecondaryContainer
|
||||
else LegadoTheme.colorScheme.onSurfaceVariant
|
||||
NormalCard(
|
||||
onClick = { configNames = emptySet() },
|
||||
containerColor = bg,
|
||||
cornerRadius = 8.dp,
|
||||
) {
|
||||
AppText(
|
||||
"全局",
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
color = fg,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(allConfigNames) { _, cn ->
|
||||
val selected = cn in configNames
|
||||
val bg = if (selected) LegadoTheme.colorScheme.secondaryContainer
|
||||
else LegadoTheme.colorScheme.surfaceContainerLow
|
||||
val fg = if (selected) LegadoTheme.colorScheme.onSecondaryContainer
|
||||
else LegadoTheme.colorScheme.onSurfaceVariant
|
||||
NormalCard(
|
||||
onClick = {
|
||||
configNames = if (cn in configNames) configNames - cn
|
||||
configNames = if (selected) configNames - cn
|
||||
else configNames + cn
|
||||
},
|
||||
label = { AppText(cn) },
|
||||
)
|
||||
containerColor = bg,
|
||||
cornerRadius = 8.dp,
|
||||
) {
|
||||
AppText(
|
||||
cn,
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
color = fg,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user