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