fix: 高亮规则增加更多配置
This commit is contained in:
@@ -102,8 +102,6 @@ object PreferKey {
|
||||
const val replaceEnableDefault = "replaceEnableDefault"
|
||||
const val showBrightnessView = "showBrightnessView"
|
||||
const val useUnderline = "useUnderline"
|
||||
const val regexColorRules = "regexColorRules"
|
||||
const val highlightRuleItems = "highlightRuleItems"
|
||||
const val highlightRuleDialog = "highlightRuleDialog"
|
||||
const val highlightRuleBookTitle = "highlightRuleBookTitle"
|
||||
const val highlightRuleBracketNote = "highlightRuleBracketNote"
|
||||
|
||||
@@ -75,7 +75,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 90,
|
||||
version = 92,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -133,7 +133,9 @@ val appDb by lazy {
|
||||
AutoMigration(from = 86, to = 87),
|
||||
AutoMigration(from = 87, to = 88),
|
||||
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() {
|
||||
|
||||
@@ -30,6 +30,9 @@ interface HighlightRuleDao {
|
||||
@Query("DELETE FROM highlightRules")
|
||||
fun deleteAll()
|
||||
|
||||
@Query("DELETE FROM highlightRules WHERE configName IS NULL")
|
||||
fun deleteGlobal()
|
||||
|
||||
@Query("SELECT COUNT(*) FROM highlightRules")
|
||||
fun count(): Int
|
||||
|
||||
@@ -38,4 +41,10 @@ interface HighlightRuleDao {
|
||||
deleteAll()
|
||||
insertAll(rules)
|
||||
}
|
||||
|
||||
@Transaction
|
||||
fun replaceGlobal(rules: List<HighlightRule>) {
|
||||
deleteGlobal()
|
||||
insertAll(rules)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ data class HighlightRule(
|
||||
var bgImage: String? = null,
|
||||
var bgImageFit: Int = 0,
|
||||
var bgImageScale: Float = 1f,
|
||||
var configName: String? = null,
|
||||
var fontPath: String? = null,
|
||||
) {
|
||||
|
||||
fun styleSummary(): String {
|
||||
@@ -56,6 +58,9 @@ data class HighlightRule(
|
||||
}
|
||||
)
|
||||
}
|
||||
if (!fontPath.isNullOrBlank()) {
|
||||
parts.add("自定义字体")
|
||||
}
|
||||
if (parts.isEmpty()) {
|
||||
parts.add("无样式")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -118,7 +119,7 @@ class ReadStyleRepository {
|
||||
val exportDir = appCtx.externalCache.getFile("readConfigExport")
|
||||
exportDir.createFolderReplace()
|
||||
val exportConfig = config.copy(
|
||||
regexColorRules = ArrayList(config.regexColorRules.map { it.copy() })
|
||||
highlightRules = ArrayList(config.highlightRules.map { it.copy() })
|
||||
)
|
||||
val exportFiles = arrayListOf<File>()
|
||||
|
||||
@@ -196,6 +197,10 @@ class ReadStyleRepository {
|
||||
config.curTextColor()
|
||||
config.curTextAccentColor()
|
||||
config.curTextShadowColor()
|
||||
if (config.highlightRules.isNotEmpty()) {
|
||||
val targetConfigName = config.name.ifBlank { null }
|
||||
HighlightRuleStore.saveForConfig(config.highlightRules, targetConfigName)
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ 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
|
||||
@@ -329,11 +331,6 @@ object ReadBookConfig {
|
||||
val resolvedMenuBorderColor: Int
|
||||
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
|
||||
|
||||
@@ -776,7 +773,7 @@ object ReadBookConfig {
|
||||
// endregion
|
||||
|
||||
fun getExportConfig(): Config {
|
||||
val exportConfig = durConfig.copy(regexColorRules = ArrayList(durConfig.regexColorRules.map { it.copy() }))
|
||||
val exportConfig = durConfig.copy(highlightRules = ArrayList(HighlightRuleStore.load()))
|
||||
if (shareLayout) {
|
||||
exportConfig.textFont = shareConfig.textFont
|
||||
exportConfig.titleFont = shareConfig.titleFont
|
||||
@@ -963,7 +960,7 @@ object ReadBookConfig {
|
||||
var menuBottomHorizontalMargin: Int = 0,
|
||||
@Transient
|
||||
var menuBottomBottomMargin: Int = 0,
|
||||
var regexColorRules: ArrayList<RegexColorRule> = arrayListOf()
|
||||
var highlightRules: ArrayList<HighlightRule> = arrayListOf()
|
||||
) {
|
||||
|
||||
@Transient
|
||||
@@ -1082,7 +1079,7 @@ object ReadBookConfig {
|
||||
"tipDividerColor" to tipDividerColor,
|
||||
"headerMode" to headerMode,
|
||||
"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? {
|
||||
|
||||
@@ -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.fromJsonArray
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import io.legado.app.utils.putPrefString
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
@@ -27,21 +25,51 @@ object HighlightRuleStore {
|
||||
private val dao get() = appDb.highlightRuleDao
|
||||
|
||||
fun load(): List<HighlightRule> {
|
||||
migrateFromPrefsIfNeeded()
|
||||
return dao.getAll()
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
return dao.getAll().filter { it.matchesConfig(configName) }
|
||||
}
|
||||
|
||||
fun loadEnabled(): List<HighlightRule> {
|
||||
migrateFromPrefsIfNeeded()
|
||||
return dao.getEnabled()
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
return dao.getEnabled().filter { it.matchesConfig(configName) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前排版的规则。
|
||||
* 仅替换当前排版绑定的规则,不影响其他排版的规则。
|
||||
*/
|
||||
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 ->
|
||||
sanitizeRule(rule).copy(position = index)
|
||||
}
|
||||
dao.replaceAll(sanitized)
|
||||
cleanupUnusedBgImages(sanitized)
|
||||
if (configName.isNullOrBlank()) {
|
||||
// 全局规则:只替换 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) {
|
||||
@@ -54,7 +82,12 @@ object HighlightRuleStore {
|
||||
|
||||
fun reset(): List<HighlightRule> {
|
||||
val defaults = createDefaultRules()
|
||||
dao.replaceAll(defaults)
|
||||
val configName = ReadBookConfig.durConfig.name
|
||||
if (configName.isBlank()) {
|
||||
dao.replaceGlobal(defaults)
|
||||
} else {
|
||||
saveForConfig(defaults, configName)
|
||||
}
|
||||
return defaults
|
||||
}
|
||||
|
||||
@@ -73,70 +106,14 @@ object HighlightRuleStore {
|
||||
val restoredBgImage = restoreRuleBgImage(backupRootPath, safeRule.bgImage)
|
||||
safeRule.copy(bgImage = restoredBgImage)
|
||||
}
|
||||
save(rules)
|
||||
// 备份恢复是全量替换
|
||||
dao.replaceAll(rules)
|
||||
cleanupUnusedBgImages()
|
||||
appCtx.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled)
|
||||
appCtx.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled)
|
||||
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 {
|
||||
val name = runCatching { rule.name }.getOrNull().orEmpty()
|
||||
val pattern = runCatching { rule.pattern }.getOrNull().orEmpty()
|
||||
@@ -162,6 +139,8 @@ object HighlightRuleStore {
|
||||
bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
|
||||
bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2),
|
||||
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>) {
|
||||
val usedPaths = rules.mapNotNull { it.bgImage }
|
||||
private fun cleanupUnusedBgImages() {
|
||||
val allRules = dao.getAll()
|
||||
val usedPaths = allRules.mapNotNull { it.bgImage }
|
||||
.filter { it.isNotBlank() && !it.startsWith("assets://") }
|
||||
.toSet()
|
||||
val dir = File(appCtx.filesDir, "bg_images")
|
||||
@@ -333,24 +313,34 @@ object HighlightRuleStore {
|
||||
return targetFile.absolutePath
|
||||
}
|
||||
|
||||
// region configName helpers
|
||||
|
||||
/**
|
||||
* 旧版 SharedPreferences 数据结构(用于迁移)
|
||||
* 判断规则是否适用于指定排版。
|
||||
* configName 为 null 表示全局规则(适用于所有排版)。
|
||||
* configName 为 JSON 数组字符串,如 '["日间","夜间"]'。
|
||||
*/
|
||||
private data class LegacyHighlightRule(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val pattern: String = "",
|
||||
val sampleText: String = "",
|
||||
val targetScope: Int = 0,
|
||||
val enabled: Boolean = true,
|
||||
val textColor: Int? = null,
|
||||
val underlineMode: Int = 0,
|
||||
val underlineColor: Int? = null,
|
||||
val underlineWidth: Float = 1f,
|
||||
val underlineOffset: Float = 2f,
|
||||
val underlineSvgPath: String? = null,
|
||||
val bgImage: String? = null,
|
||||
val bgImageFit: Int = 0,
|
||||
val bgImageScale: Float = 1f,
|
||||
)
|
||||
private fun HighlightRule.matchesConfig(configName: String): Boolean {
|
||||
val cn = this.configName
|
||||
if (cn.isNullOrBlank()) return true // 全局规则
|
||||
return cn.configNames().contains(configName)
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 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
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ interface TextBaseColumn : BaseColumn {
|
||||
val bgImage: String
|
||||
val bgImageFit: Int
|
||||
val bgImageScale: Float
|
||||
val fontPath: String
|
||||
var selected: Boolean
|
||||
var isSearchResult: Boolean
|
||||
}
|
||||
@@ -4,11 +4,15 @@ import android.graphics.Canvas
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.net.toUri
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
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.Companion.emptyTextLine
|
||||
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
|
||||
data class TextColumn(
|
||||
@@ -25,6 +29,7 @@ data class TextColumn(
|
||||
override val bgImage: String = "",
|
||||
override val bgImageFit: Int = 0,
|
||||
override val bgImageScale: Float = 1f,
|
||||
override val fontPath: String = "",
|
||||
) : TextBaseColumn {
|
||||
|
||||
override var textLine: TextLine = emptyTextLine
|
||||
@@ -105,7 +110,29 @@ data class TextColumn(
|
||||
}
|
||||
|
||||
private fun getCustomTypeface(): Typeface? {
|
||||
// TODO: HighlightRule 不再存储 fontPath,需要重新设计自定义字体方案
|
||||
return null
|
||||
if (fontPath.isEmpty()) 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 bgImageFit: Int = 0,
|
||||
override val bgImageScale: Float = 1f,
|
||||
override val fontPath: String = "",
|
||||
) : TextBaseColumn {
|
||||
|
||||
override val textColor: Int? get() = mTextColor
|
||||
|
||||
@@ -14,7 +14,8 @@ data class CharStyle(
|
||||
val bgImage: String = "",
|
||||
val bgImageFit: Int = 0,
|
||||
val bgImageScale: Float = 1f,
|
||||
val fontPath: String = "",
|
||||
) {
|
||||
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 ?: "",
|
||||
bgImage = style?.bgImage ?: "",
|
||||
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(),
|
||||
bgImage = rule.bgImage.orEmpty(),
|
||||
bgImageFit = rule.bgImageFit,
|
||||
bgImageScale = rule.bgImageScale
|
||||
bgImageScale = rule.bgImageScale,
|
||||
fontPath = rule.fontPath.orEmpty()
|
||||
)
|
||||
compiled.regex.findAll(text).forEach { match ->
|
||||
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.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.button.series.SmallTonalButton
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
@@ -166,9 +167,10 @@ private fun HighlightRuleItem(
|
||||
onEditClick: () -> Unit,
|
||||
onDeleteClick: () -> Unit,
|
||||
) {
|
||||
val configLabel = rule.configName?.configNames()?.joinToString("、") ?: "全局"
|
||||
TinySettingItem(
|
||||
title = rule.name.ifBlank { rule.displayPattern() },
|
||||
description = rule.styleSummary(),
|
||||
description = "${rule.styleSummary()} · $configLabel",
|
||||
onClick = onEditClick,
|
||||
trailingContent = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.ui.book.read.sheet
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.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
|
||||
@@ -20,6 +23,7 @@ import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -35,7 +39,10 @@ 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.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
|
||||
@@ -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.TinySwitchSettingItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
@@ -87,10 +95,21 @@ fun HighlightRuleEditSheet(
|
||||
var bgImageScale by remember { mutableFloatStateOf(initial.bgImageScale) }
|
||||
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
|
||||
var showTextColorPicker by remember { mutableStateOf(false) }
|
||||
var showBgColorPicker by remember { mutableStateOf(false) }
|
||||
var showUnderlineColorPicker by remember { mutableStateOf(false) }
|
||||
var showFontSelect by remember { mutableStateOf(false) }
|
||||
|
||||
// Validation
|
||||
var patternError by remember { mutableStateOf<String?>(null) }
|
||||
@@ -147,6 +166,8 @@ fun HighlightRuleEditSheet(
|
||||
bgImage = if (hasBgImage) bgImage.ifBlank { null } else null,
|
||||
bgImageFit = bgImageFit,
|
||||
bgImageScale = bgImageScale,
|
||||
configName = if (configNames.isEmpty()) null else configNames.toList().toJsonArray(),
|
||||
fontPath = if (hasFont) fontPath.ifBlank { null } else null,
|
||||
)
|
||||
)
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user