fix: 书签问题与高亮规则代码优化

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