fix: 高亮规则增加更多配置

This commit is contained in:
HapeLee committed 2026-06-10 01:45:32 +08:00
1 parent b27f068525
commit eef1f58f26
17 files changed
+5089 -116

No files matched your search

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -102,8 +102,6 @@ object PreferKey {
const val replaceEnableDefault = "replaceEnableDefault" const val replaceEnableDefault = "replaceEnableDefault"
const val showBrightnessView = "showBrightnessView" const val showBrightnessView = "showBrightnessView"
const val useUnderline = "useUnderline" const val useUnderline = "useUnderline"
const val regexColorRules = "regexColorRules"
const val highlightRuleItems = "highlightRuleItems"
const val highlightRuleDialog = "highlightRuleDialog" const val highlightRuleDialog = "highlightRuleDialog"
const val highlightRuleBookTitle = "highlightRuleBookTitle" const val highlightRuleBookTitle = "highlightRuleBookTitle"
const val highlightRuleBracketNote = "highlightRuleBracketNote" const val highlightRuleBracketNote = "highlightRuleBracketNote"
@@ -75,7 +75,7 @@ val appDb by lazy {
} }
@Database( @Database(
version = 90, version = 92,
exportSchema = true, exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -133,7 +133,9 @@ val appDb by lazy {
AutoMigration(from = 86, to = 87), AutoMigration(from = 86, to = 87),
AutoMigration(from = 87, to = 88), AutoMigration(from = 87, to = 88),
AutoMigration(from = 88, to = 89), AutoMigration(from = 88, to = 89),
AutoMigration(from = 89, to = 90) AutoMigration(from = 89, to = 90),
AutoMigration(from = 90, to = 91),
AutoMigration(from = 91, to = 92)
] ]
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@@ -30,6 +30,9 @@ interface HighlightRuleDao {
@Query("DELETE FROM highlightRules") @Query("DELETE FROM highlightRules")
fun deleteAll() fun deleteAll()
@Query("DELETE FROM highlightRules WHERE configName IS NULL")
fun deleteGlobal()
@Query("SELECT COUNT(*) FROM highlightRules") @Query("SELECT COUNT(*) FROM highlightRules")
fun count(): Int fun count(): Int
@@ -38,4 +41,10 @@ interface HighlightRuleDao {
deleteAll() deleteAll()
insertAll(rules) insertAll(rules)
} }
@Transaction
fun replaceGlobal(rules: List<HighlightRule>) {
deleteGlobal()
insertAll(rules)
}
} }
@@ -24,6 +24,8 @@ data class HighlightRule(
var bgImage: String? = null, var bgImage: String? = null,
var bgImageFit: Int = 0, var bgImageFit: Int = 0,
var bgImageScale: Float = 1f, var bgImageScale: Float = 1f,
var configName: String? = null,
var fontPath: String? = null,
) { ) {
fun styleSummary(): String { fun styleSummary(): String {
@@ -56,6 +58,9 @@ data class HighlightRule(
} }
) )
} }
if (!fontPath.isNullOrBlank()) {
parts.add("自定义字体")
}
if (parts.isEmpty()) { if (parts.isEmpty()) {
parts.add("无样式") parts.add("无样式")
} }
@@ -5,6 +5,7 @@ import io.legado.app.constant.AppLog
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadStyleResolver 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.FileUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.compress.ZipUtils import io.legado.app.utils.compress.ZipUtils
@@ -118,7 +119,7 @@ class ReadStyleRepository {
val exportDir = appCtx.externalCache.getFile("readConfigExport") val exportDir = appCtx.externalCache.getFile("readConfigExport")
exportDir.createFolderReplace() exportDir.createFolderReplace()
val exportConfig = config.copy( val exportConfig = config.copy(
regexColorRules = ArrayList(config.regexColorRules.map { it.copy() }) highlightRules = ArrayList(config.highlightRules.map { it.copy() })
) )
val exportFiles = arrayListOf<File>() val exportFiles = arrayListOf<File>()
@@ -196,6 +197,10 @@ class ReadStyleRepository {
config.curTextColor() config.curTextColor()
config.curTextAccentColor() config.curTextAccentColor()
config.curTextShadowColor() config.curTextShadowColor()
if (config.highlightRules.isNotEmpty()) {
val targetConfigName = config.name.ifBlank { null }
HighlightRuleStore.saveForConfig(config.highlightRules, targetConfigName)
}
return config return config
} }
@@ -13,7 +13,9 @@ import io.legado.app.constant.PreferKey
import io.legado.app.constant.ReadMenuBlurMode import io.legado.app.constant.ReadMenuBlurMode
import io.legado.app.constant.ReadMenuBlurStyle import io.legado.app.constant.ReadMenuBlurStyle
import androidx.compose.runtime.State import androidx.compose.runtime.State
import io.legado.app.data.entities.HighlightRule
import io.legado.app.data.repository.ReadStyleRepository 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.DefaultData
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
import io.legado.app.ui.config.PrefDelegate import io.legado.app.ui.config.PrefDelegate
@@ -329,11 +331,6 @@ object ReadBookConfig {
val resolvedMenuBorderColor: Int val resolvedMenuBorderColor: Int
get() = if (ReadStyleResolver.isNightTheme()) readMenuBorderColorNight else readMenuBorderColor get() = if (ReadStyleResolver.isNightTheme()) readMenuBorderColorNight else readMenuBorderColor
val regexColorRules: ArrayList<RegexColorRule> get() = durConfig.regexColorRules
fun saveRegexColorRules() {
save()
}
val config get() = if (shareLayout) shareConfig else durConfig val config get() = if (shareLayout) shareConfig else durConfig
@@ -776,7 +773,7 @@ object ReadBookConfig {
// endregion // endregion
fun getExportConfig(): Config { fun getExportConfig(): Config {
val exportConfig = durConfig.copy(regexColorRules = ArrayList(durConfig.regexColorRules.map { it.copy() })) val exportConfig = durConfig.copy(highlightRules = ArrayList(HighlightRuleStore.load()))
if (shareLayout) { if (shareLayout) {
exportConfig.textFont = shareConfig.textFont exportConfig.textFont = shareConfig.textFont
exportConfig.titleFont = shareConfig.titleFont exportConfig.titleFont = shareConfig.titleFont
@@ -963,7 +960,7 @@ object ReadBookConfig {
var menuBottomHorizontalMargin: Int = 0, var menuBottomHorizontalMargin: Int = 0,
@Transient @Transient
var menuBottomBottomMargin: Int = 0, var menuBottomBottomMargin: Int = 0,
var regexColorRules: ArrayList<RegexColorRule> = arrayListOf() var highlightRules: ArrayList<HighlightRule> = arrayListOf()
) { ) {
@Transient @Transient
@@ -1082,7 +1079,7 @@ object ReadBookConfig {
"tipDividerColor" to tipDividerColor, "tipDividerColor" to tipDividerColor,
"headerMode" to headerMode, "headerMode" to headerMode,
"footerMode" to footerMode, "footerMode" to footerMode,
"regexColorRules" to regexColorRules.map { mapOf("name" to it.name, "pattern" to it.pattern, "color" to it.color, "fontPath" to it.fontPath) } "highlightRules" to highlightRules.map { mapOf("id" to it.id, "name" to it.name, "pattern" to it.pattern, "sampleText" to it.sampleText, "targetScope" to it.targetScope, "enabled" to it.enabled, "position" to it.position, "textColor" to it.textColor, "bgColor" to it.bgColor, "underlineMode" to it.underlineMode, "underlineColor" to it.underlineColor, "underlineWidth" to it.underlineWidth, "underlineOffset" to it.underlineOffset, "underlineSvgPath" to it.underlineSvgPath, "bgImage" to it.bgImage, "bgImageFit" to it.bgImageFit, "bgImageScale" to it.bgImageScale, "configName" to it.configName, "fontPath" to it.fontPath) }
) )
fun getBgPath(bgIndex: Int): String? { fun getBgPath(bgIndex: Int): String? {
@@ -1,8 +0,0 @@
package io.legado.app.help.config
data class RegexColorRule(
var name: String,
var pattern: String,
var color: Int,
var fontPath: String = ""
)
@@ -7,9 +7,7 @@ import io.legado.app.help.config.ReadBookConfig
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonArray import io.legado.app.utils.fromJsonArray
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.getPrefString
import io.legado.app.utils.putPrefBoolean import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefString
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
@@ -27,21 +25,51 @@ object HighlightRuleStore {
private val dao get() = appDb.highlightRuleDao private val dao get() = appDb.highlightRuleDao
fun load(): List<HighlightRule> { fun load(): List<HighlightRule> {
migrateFromPrefsIfNeeded() val configName = ReadBookConfig.durConfig.name
return dao.getAll() return dao.getAll().filter { it.matchesConfig(configName) }
} }
fun loadEnabled(): List<HighlightRule> { fun loadEnabled(): List<HighlightRule> {
migrateFromPrefsIfNeeded() val configName = ReadBookConfig.durConfig.name
return dao.getEnabled() return dao.getEnabled().filter { it.matchesConfig(configName) }
} }
/**
* 保存当前排版的规则。
* 仅替换当前排版绑定的规则,不影响其他排版的规则。
*/
fun save(rules: List<HighlightRule>) { fun save(rules: List<HighlightRule>) {
val configName = ReadBookConfig.durConfig.name
saveForConfig(rules, configName.ifBlank { null })
}
fun saveForConfig(rules: List<HighlightRule>, configName: String?) {
val sanitized = rules.mapIndexed { index, rule -> val sanitized = rules.mapIndexed { index, rule ->
sanitizeRule(rule).copy(position = index) sanitizeRule(rule).copy(position = index)
} }
dao.replaceAll(sanitized) if (configName.isNullOrBlank()) {
cleanupUnusedBgImages(sanitized) // 全局规则:只替换 configName 为 null 的规则
dao.replaceGlobal(sanitized)
} else {
// 按排版保存:只处理绑定到当前排版的规则,不动全局规则
val allRules = dao.getAll()
// 旧的绑定到当前排版的规则(不含全局规则)
val oldBound = allRules.filter {
!it.configName.isNullOrBlank() && it.matchesConfig(configName)
}
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()))
}
}
// 插入所有规则(全局规则也一起,否则会被 replaceGlobal 删掉)
dao.insertAll(sanitized)
}
cleanupUnusedBgImages()
} }
fun update(rule: HighlightRule) { fun update(rule: HighlightRule) {
@@ -54,7 +82,12 @@ object HighlightRuleStore {
fun reset(): List<HighlightRule> { fun reset(): List<HighlightRule> {
val defaults = createDefaultRules() val defaults = createDefaultRules()
dao.replaceAll(defaults) val configName = ReadBookConfig.durConfig.name
if (configName.isBlank()) {
dao.replaceGlobal(defaults)
} else {
saveForConfig(defaults, configName)
}
return defaults return defaults
} }
@@ -73,70 +106,14 @@ object HighlightRuleStore {
val restoredBgImage = restoreRuleBgImage(backupRootPath, safeRule.bgImage) val restoredBgImage = restoreRuleBgImage(backupRootPath, safeRule.bgImage)
safeRule.copy(bgImage = restoredBgImage) safeRule.copy(bgImage = restoredBgImage)
} }
save(rules) // 备份恢复是全量替换
dao.replaceAll(rules)
cleanupUnusedBgImages()
appCtx.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled) appCtx.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled)
appCtx.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled) appCtx.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled)
appCtx.putPrefBoolean(PreferKey.highlightRuleBracketNote, backupData.bracketNoteEnabled) appCtx.putPrefBoolean(PreferKey.highlightRuleBracketNote, backupData.bracketNoteEnabled)
} }
/**
* 从旧版 SharedPreferences 迁移数据(一次性)
*/
private fun migrateFromPrefsIfNeeded() {
if (dao.count() > 0) return
// 尝试从 SharedPreferences 读取旧数据
val stored = appCtx.getPrefString(PreferKey.highlightRuleItems)
if (!stored.isNullOrBlank()) {
val oldRules = GSON.fromJsonArray<LegacyHighlightRule>(stored).getOrNull()
if (!oldRules.isNullOrEmpty()) {
val migrated = oldRules.mapIndexed { index, old ->
sanitizeRule(
HighlightRule(
id = old.id,
name = old.name,
pattern = old.pattern,
sampleText = old.sampleText,
targetScope = old.targetScope,
enabled = old.enabled,
position = index,
textColor = old.textColor,
underlineMode = old.underlineMode,
underlineColor = old.underlineColor,
underlineWidth = old.underlineWidth,
underlineOffset = old.underlineOffset,
underlineSvgPath = old.underlineSvgPath,
bgImage = old.bgImage,
bgImageFit = old.bgImageFit,
bgImageScale = old.bgImageScale,
)
).copy(position = index)
}
dao.insertAll(migrated)
// 清除旧 SharedPreferences 数据
appCtx.putPrefString(PreferKey.highlightRuleItems, null)
return
}
}
// 尝试从旧版 RegexColorRule 迁移
migrateFromRegexColorRules()
}
private fun migrateFromRegexColorRules() {
val oldRules = ReadBookConfig.regexColorRules
if (oldRules.isEmpty()) return
val migrated = oldRules.mapIndexed { index, old ->
HighlightRule(
name = old.name,
pattern = old.pattern,
position = index,
textColor = old.color,
)
}
dao.insertAll(migrated)
oldRules.clear()
ReadBookConfig.save()
}
fun sanitizeRule(rule: HighlightRule): HighlightRule { fun sanitizeRule(rule: HighlightRule): HighlightRule {
val name = runCatching { rule.name }.getOrNull().orEmpty() val name = runCatching { rule.name }.getOrNull().orEmpty()
val pattern = runCatching { rule.pattern }.getOrNull().orEmpty() val pattern = runCatching { rule.pattern }.getOrNull().orEmpty()
@@ -162,6 +139,8 @@ object HighlightRuleStore {
bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() }, bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2), bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2),
bgImageScale = runCatching { rule.bgImageScale }.getOrDefault(1f).coerceIn(0.1f, 5f), bgImageScale = runCatching { rule.bgImageScale }.getOrDefault(1f).coerceIn(0.1f, 5f),
configName = runCatching { rule.configName }.getOrNull()?.takeIf { it.isNotBlank() },
fontPath = runCatching { rule.fontPath }.getOrNull()?.takeIf { it.isNotBlank() },
) )
} }
@@ -304,8 +283,9 @@ object HighlightRuleStore {
) )
} }
private fun cleanupUnusedBgImages(rules: List<HighlightRule>) { private fun cleanupUnusedBgImages() {
val usedPaths = rules.mapNotNull { it.bgImage } val allRules = dao.getAll()
val usedPaths = allRules.mapNotNull { it.bgImage }
.filter { it.isNotBlank() && !it.startsWith("assets://") } .filter { it.isNotBlank() && !it.startsWith("assets://") }
.toSet() .toSet()
val dir = File(appCtx.filesDir, "bg_images") val dir = File(appCtx.filesDir, "bg_images")
@@ -333,24 +313,34 @@ object HighlightRuleStore {
return targetFile.absolutePath return targetFile.absolutePath
} }
// region configName helpers
/** /**
* 旧版 SharedPreferences 数据结构(用于迁移) * 判断规则是否适用于指定排版。
* configName 为 null 表示全局规则(适用于所有排版)。
* configName 为 JSON 数组字符串,如 '["日间","夜间"]'。
*/ */
private data class LegacyHighlightRule( private fun HighlightRule.matchesConfig(configName: String): Boolean {
val id: String = "", val cn = this.configName
val name: String = "", if (cn.isNullOrBlank()) return true // 全局规则
val pattern: String = "", return cn.configNames().contains(configName)
val sampleText: String = "", }
val targetScope: Int = 0,
val enabled: Boolean = true, /**
val textColor: Int? = null, * 解析 configName JSON 数组为列表。
val underlineMode: Int = 0, */
val underlineColor: Int? = null, fun String.configNames(): List<String> {
val underlineWidth: Float = 1f, return runCatching {
val underlineOffset: Float = 2f, GSON.fromJsonArray<String>(this).getOrNull() ?: emptyList()
val underlineSvgPath: String? = null, }.getOrElse { emptyList() }
val bgImage: String? = null, }
val bgImageFit: Int = 0,
val bgImageScale: Float = 1f, /**
) * 将排版名列表转为 JSON 数组字符串。
*/
fun List<String>.toJsonArray(): String {
return GSON.toJson(this)
}
// endregion
} }
@@ -17,6 +17,7 @@ interface TextBaseColumn : BaseColumn {
val bgImage: String val bgImage: String
val bgImageFit: Int val bgImageFit: Int
val bgImageScale: Float val bgImageScale: Float
val fontPath: String
var selected: Boolean var selected: Boolean
var isSearchResult: Boolean var isSearchResult: Boolean
} }
@@ -4,11 +4,15 @@ import android.graphics.Canvas
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build import android.os.Build
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.net.toUri
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ContentTextView
import io.legado.app.ui.book.read.page.entities.TextLine import io.legado.app.ui.book.read.page.entities.TextLine
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
import io.legado.app.ui.book.read.page.provider.ChapterProvider import io.legado.app.ui.book.read.page.provider.ChapterProvider
import io.legado.app.utils.isContentScheme
import splitties.init.appCtx
import java.io.File
@Keep @Keep
data class TextColumn( data class TextColumn(
@@ -25,6 +29,7 @@ data class TextColumn(
override val bgImage: String = "", override val bgImage: String = "",
override val bgImageFit: Int = 0, override val bgImageFit: Int = 0,
override val bgImageScale: Float = 1f, override val bgImageScale: Float = 1f,
override val fontPath: String = "",
) : TextBaseColumn { ) : TextBaseColumn {
override var textLine: TextLine = emptyTextLine override var textLine: TextLine = emptyTextLine
@@ -105,7 +110,29 @@ data class TextColumn(
} }
private fun getCustomTypeface(): Typeface? { private fun getCustomTypeface(): Typeface? {
// TODO: HighlightRule 不再存储 fontPath,需要重新设计自定义字体方案 if (fontPath.isEmpty()) return null
return null return typefaceCache.getOrPut(fontPath) {
loadTypeface(fontPath)
}
}
companion object {
private val typefaceCache = HashMap<String, Typeface?>()
private fun loadTypeface(fontPath: String): Typeface? {
return runCatching {
when {
fontPath.isContentScheme() -> {
appCtx.contentResolver
.openFileDescriptor(fontPath.toUri(), "r")!!
.use { Typeface.Builder(it.fileDescriptor).build() }
}
fontPath.isNotEmpty() -> {
Typeface.Builder(File(fontPath)).build()
}
else -> null
}
}.getOrNull()
}
} }
} }
@@ -30,6 +30,7 @@ data class TextHtmlColumn(
override val bgImage: String = "", override val bgImage: String = "",
override val bgImageFit: Int = 0, override val bgImageFit: Int = 0,
override val bgImageScale: Float = 1f, override val bgImageScale: Float = 1f,
override val fontPath: String = "",
) : TextBaseColumn { ) : TextBaseColumn {
override val textColor: Int? get() = mTextColor override val textColor: Int? get() = mTextColor
@@ -14,7 +14,8 @@ data class CharStyle(
val bgImage: String = "", val bgImage: String = "",
val bgImageFit: Int = 0, val bgImageFit: Int = 0,
val bgImageScale: Float = 1f, val bgImageScale: Float = 1f,
val fontPath: String = "",
) { ) {
val hasStyle: Boolean val hasStyle: Boolean
get() = textColor != null || bgColor != null || underlineMode != 0 || bgImage.isNotEmpty() get() = textColor != null || bgColor != null || underlineMode != 0 || bgImage.isNotEmpty() || fontPath.isNotEmpty()
} }
@@ -1240,7 +1240,8 @@ class TextChapterLayout(
underlineSvgPath = style?.underlineSvgPath ?: "", underlineSvgPath = style?.underlineSvgPath ?: "",
bgImage = style?.bgImage ?: "", bgImage = style?.bgImage ?: "",
bgImageFit = style?.bgImageFit ?: 0, bgImageFit = style?.bgImageFit ?: 0,
bgImageScale = style?.bgImageScale ?: 1f bgImageScale = style?.bgImageScale ?: 1f,
fontPath = style?.fontPath ?: ""
) )
} }
} }
@@ -1374,7 +1375,8 @@ class TextChapterLayout(
underlineSvgPath = rule.underlineSvgPath.orEmpty(), underlineSvgPath = rule.underlineSvgPath.orEmpty(),
bgImage = rule.bgImage.orEmpty(), bgImage = rule.bgImage.orEmpty(),
bgImageFit = rule.bgImageFit, bgImageFit = rule.bgImageFit,
bgImageScale = rule.bgImageScale bgImageScale = rule.bgImageScale,
fontPath = rule.fontPath.orEmpty()
) )
compiled.regex.findAll(text).forEach { match -> compiled.regex.findAll(text).forEach { match ->
for (i in match.range) { for (i in match.range) {
@@ -32,6 +32,7 @@ import io.legado.app.R
import io.legado.app.data.entities.HighlightRule import io.legado.app.data.entities.HighlightRule
import io.legado.app.ui.book.read.ReadBookIntent 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
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.TinySwitch
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.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -166,9 +167,10 @@ private fun HighlightRuleItem(
onEditClick: () -> Unit, onEditClick: () -> Unit,
onDeleteClick: () -> Unit, onDeleteClick: () -> Unit,
) { ) {
val configLabel = rule.configName?.configNames()?.joinToString("、") ?: "全局"
TinySettingItem( TinySettingItem(
title = rule.name.ifBlank { rule.displayPattern() }, title = rule.name.ifBlank { rule.displayPattern() },
description = rule.styleSummary(), description = "${rule.styleSummary()} · $configLabel",
onClick = onEditClick, onClick = onEditClick,
trailingContent = { trailingContent = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
@@ -1,9 +1,11 @@
package io.legado.app.ui.book.read.sheet package io.legado.app.ui.book.read.sheet
import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -11,6 +13,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.FilterChip
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Done
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -20,6 +23,7 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@@ -35,7 +39,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
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.help.config.ReadBookConfig
import io.legado.app.ui.book.read.config.HighlightRuleStore 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.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.SectionTitle import io.legado.app.ui.widget.components.SectionTitle
@@ -47,6 +54,7 @@ import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
@@ -87,10 +95,21 @@ fun HighlightRuleEditSheet(
var bgImageScale by remember { mutableFloatStateOf(initial.bgImageScale) } var bgImageScale by remember { mutableFloatStateOf(initial.bgImageScale) }
var hasBgImage by remember { mutableStateOf(initial.bgImage?.isNotBlank() == true) } var hasBgImage by remember { mutableStateOf(initial.bgImage?.isNotBlank() == true) }
// Config binding state — empty set = global (applies to all configs)
val allConfigNames = remember { ReadBookConfig.configList.map { it.name }.filter { it.isNotBlank() } }
var configNames by remember {
mutableStateOf(initial.configName.orEmpty().configNames().toSet())
}
// Font state
var hasFont by remember { mutableStateOf(initial.fontPath?.isNotBlank() == true) }
var fontPath by remember { mutableStateOf(initial.fontPath.orEmpty()) }
// Color picker state // Color picker state
var showTextColorPicker by remember { mutableStateOf(false) } var showTextColorPicker by remember { mutableStateOf(false) }
var showBgColorPicker by remember { mutableStateOf(false) } var showBgColorPicker by remember { mutableStateOf(false) }
var showUnderlineColorPicker by remember { mutableStateOf(false) } var showUnderlineColorPicker by remember { mutableStateOf(false) }
var showFontSelect by remember { mutableStateOf(false) }
// Validation // Validation
var patternError by remember { mutableStateOf<String?>(null) } var patternError by remember { mutableStateOf<String?>(null) }
@@ -147,6 +166,8 @@ fun HighlightRuleEditSheet(
bgImage = if (hasBgImage) bgImage.ifBlank { null } else null, bgImage = if (hasBgImage) bgImage.ifBlank { null } else null,
bgImageFit = bgImageFit, bgImageFit = bgImageFit,
bgImageScale = bgImageScale, bgImageScale = bgImageScale,
configName = if (configNames.isEmpty()) null else configNames.toList().toJsonArray(),
fontPath = if (hasFont) fontPath.ifBlank { null } else null,
) )
) )
onSave(sanitized) onSave(sanitized)
@@ -348,7 +369,56 @@ fun HighlightRuleEditSheet(
) )
} }
// === Section 3: Preview === // === 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),
) {
// Global toggle
FilterChip(
selected = configNames.isEmpty(),
onClick = { configNames = emptySet() },
label = { AppText("全局") },
)
allConfigNames.forEach { cn ->
FilterChip(
selected = cn in configNames,
onClick = {
configNames = if (cn in configNames) configNames - cn
else configNames + cn
},
label = { AppText(cn) },
)
}
}
}
// === Section 4: Font ===
SectionTitle("字体替换")
TinySwitchSettingItem(
title = "自定义字体",
checked = hasFont,
onCheckedChange = { hasFont = it },
)
if (hasFont) {
TinyClickableSettingItem(
title = stringResource(R.string.select_font),
description = fontPath.ifBlank { null }?.let { File(it).name },
onClick = { showFontSelect = true },
)
}
// === Section 5: Preview ===
SectionTitle(stringResource(R.string.preview_effect)) SectionTitle(stringResource(R.string.preview_effect))
AppTextField( AppTextField(
@@ -408,6 +478,32 @@ fun HighlightRuleEditSheet(
}, },
) )
} }
// Font selector
if (showFontSelect) {
val readSettingsRepository: io.legado.app.data.repository.ReadSettingsRepository =
org.koin.compose.koinInject()
val scope = rememberCoroutineScope()
val fontFolderLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocumentTree()
) { uri ->
uri?.let {
context.contentResolver.takePersistableUriPermission(
it, Intent.FLAG_GRANT_READ_URI_PERMISSION
)
scope.launch {
readSettingsRepository.setFontFolder(it.toString())
}
}
}
FontSelectSheet(
show = true,
onDismissRequest = { showFontSelect = false },
onSelectFont = { fontPath = it; showFontSelect = false },
onSelectSystemTypeface = { fontPath = ""; showFontSelect = false },
onOpenFolderPicker = { fontFolderLauncher.launch(null) },
)
}
} }
@Composable @Composable