fix: 高亮规则增加更多配置
This commit is contained in:
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
|
||||||
|
|||||||
Reference in New Issue
Block a user