fix: 合并标签分组与传统分组编辑界面,支持在分组编辑中查看和修改匹配规则

This commit is contained in:
HapeLee
2026-06-27 02:27:42 +08:00
parent a490135163
commit 389239ca67
5 changed files with 275 additions and 13 deletions
@@ -0,0 +1,44 @@
package io.legado.app.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legado.app.data.entities.TagGroupRule
import kotlinx.coroutines.flow.Flow
@Dao
interface TagGroupRuleDao {
@Query("SELECT * FROM tag_group_rules ORDER BY `order`")
fun flowAll(): Flow<List<TagGroupRule>>
@Query("SELECT * FROM tag_group_rules ORDER BY `order` ASC")
fun getAll(): List<TagGroupRule>
@Query("SELECT * FROM tag_group_rules WHERE id = :id")
suspend fun getById(id: Long): TagGroupRule?
@Query("SELECT * FROM tag_group_rules WHERE id IN (:ids)")
suspend fun getByIds(ids: Set<Long>): List<TagGroupRule>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg rule: TagGroupRule)
@Update
suspend fun update(vararg rule: TagGroupRule)
@Delete
suspend fun delete(vararg rule: TagGroupRule)
@Query("SELECT * FROM tag_group_rules WHERE groupName = :groupName LIMIT 1")
suspend fun getByGroupName(groupName: String): TagGroupRule?
@get:Query("SELECT COALESCE(MAX(`order`), -1) FROM tag_group_rules")
val maxOrder: Int
@Query("DELETE FROM tag_group_rules")
suspend fun deleteAll()
}
@@ -0,0 +1,63 @@
package io.legado.app.data.repository
import io.legado.app.data.appDb
import io.legado.app.data.entities.TagGroupRule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class TagGroupRuleRepository {
private val dao = appDb.tagGroupRuleDao
fun flowAll(): Flow<List<TagGroupRule>> {
return dao.flowAll()
}
suspend fun insert(vararg rule: TagGroupRule) {
withContext(Dispatchers.IO) {
dao.insert(*rule)
}
}
suspend fun delete(vararg rule: TagGroupRule) {
withContext(Dispatchers.IO) {
dao.delete(*rule)
}
}
suspend fun getByGroupName(groupName: String): TagGroupRule? = withContext(Dispatchers.IO) {
dao.getByGroupName(groupName)
}
suspend fun update(vararg rule: TagGroupRule) {
withContext(Dispatchers.IO) {
dao.update(*rule)
}
}
suspend fun findById(id: Long): TagGroupRule? = withContext(Dispatchers.IO) {
dao.getById(id)
}
suspend fun getByIds(ids: Set<Long>): List<TagGroupRule> =
withContext(Dispatchers.IO) {
if (ids.isEmpty()) emptyList() else dao.getByIds(ids)
}
suspend fun deleteByIds(ids: Set<Long>) = withContext(Dispatchers.IO) {
if (ids.isEmpty()) return@withContext
val rules = dao.getByIds(ids)
dao.delete(*rules.toTypedArray())
}
suspend fun moveOrder(rules: List<TagGroupRule>) = withContext(Dispatchers.IO) {
val updatedRules = rules.mapIndexed { index, rule ->
rule.copy(order = index)
}
dao.update(*updatedRules.toTypedArray())
}
fun getMaxOrder(): Int = dao.maxOrder
}
@@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.TagGroupRule
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -98,6 +99,7 @@ fun GroupEditContent(
onDismissRequest: () -> Unit,
coverPath: String?,
onCoverPathChange: (String?) -> Unit,
tagGroupRule: TagGroupRule? = null,
viewModel: GroupViewModel = koinViewModel()
) {
val context = LocalContext.current
@@ -106,6 +108,8 @@ fun GroupEditContent(
var isPrivate by remember(group) { mutableStateOf(group?.isPrivate ?: false) }
var showDisablePrivateDialog by remember(group) { mutableStateOf(false) }
var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
var pattern by remember(tagGroupRule) { mutableStateOf(tagGroupRule?.pattern ?: "") }
var showPattern by remember(tagGroupRule) { mutableStateOf(tagGroupRule != null || pattern.isNotBlank()) }
val sortOptions = stringArrayResource(R.array.book_sort)
val sortEntryValues = remember(sortOptions) {
@@ -210,6 +214,28 @@ fun GroupEditContent(
)
}
Spacer(modifier = Modifier.height(8.dp))
CompactSwitchSettingItem(
title = "标签匹配规则",
description = if (showPattern) "启用后分组将根据书籍标签自动匹配" else "关闭后分组为手动管理",
checked = showPattern,
onCheckedChange = { showPattern = it }
)
if (showPattern) {
Spacer(modifier = Modifier.height(8.dp))
AppTextField(
value = pattern,
onValueChange = { pattern = it },
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
label = stringResource(R.string.tag_group_pattern),
modifier = Modifier.fillMaxWidth(),
minLines = 3
)
}
Spacer(modifier = Modifier.height(16.dp))
ConfirmDismissButtonsRow(
@@ -228,16 +254,36 @@ fun GroupEditContent(
enableRefresh = enableRefresh,
isPrivate = isPrivate
)
) {
onDismissRequest()
)
if (showPattern && pattern.isNotBlank()) {
val existingRule = tagGroupRule
if (existingRule != null) {
viewModel.saveTagGroupRule(
existingRule.copy(
groupName = groupName,
pattern = pattern
)
)
} else {
viewModel.saveTagGroupRule(
TagGroupRule(
groupName = groupName,
pattern = pattern
)
)
}
} else if (!showPattern && tagGroupRule != null) {
viewModel.deleteTagGroupRule(tagGroupRule)
}
onDismissRequest()
} else {
viewModel.addGroup(
groupName,
selectedSortIndex,
enableRefresh,
isPrivate,
coverPath
coverPath,
pattern = pattern.takeIf { showPattern && it.isNotBlank() }
) {
onDismissRequest()
}
@@ -3,7 +3,9 @@ package io.legado.app.ui.book.group
import android.app.Application
import io.legado.app.base.BaseViewModel
import io.legado.app.data.entities.BookGroup
import io.legado.app.data.entities.TagGroupRule
import io.legado.app.data.repository.BookGroupRepository
import io.legado.app.data.repository.TagGroupRuleRepository
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
class GroupViewModel(
@@ -12,6 +14,8 @@ class GroupViewModel(
private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase
) : BaseViewModel(application) {
private val tagGroupRuleRepository = TagGroupRuleRepository()
fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
execute {
bookGroupRepository.update(*bookGroup)
@@ -26,6 +30,7 @@ class GroupViewModel(
enableRefresh: Boolean,
isPrivate: Boolean,
cover: String?,
pattern: String? = null,
finally: () -> Unit
) {
execute {
@@ -41,6 +46,14 @@ class GroupViewModel(
)
bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId)
bookGroupRepository.insert(bookGroup)
if (!pattern.isNullOrBlank()) {
val tagRule = TagGroupRule(
groupName = groupName,
pattern = pattern,
order = tagGroupRuleRepository.getMaxOrder()
)
tagGroupRuleRepository.insert(tagRule)
}
}.onFinally {
finally()
}
@@ -63,4 +76,29 @@ class GroupViewModel(
}
}
suspend fun getTagGroupRule(groupName: String): TagGroupRule? {
return tagGroupRuleRepository.getByGroupName(groupName)
}
fun saveTagGroupRule(rule: TagGroupRule, finally: (() -> Unit)? = null) {
execute {
val existing = tagGroupRuleRepository.getByGroupName(rule.groupName)
if (existing != null) {
tagGroupRuleRepository.update(rule.copy(id = existing.id))
} else {
tagGroupRuleRepository.insert(rule)
}
}.onFinally {
finally?.invoke()
}
}
fun deleteTagGroupRule(rule: TagGroupRule, finally: (() -> Unit)? = null) {
execute {
tagGroupRuleRepository.delete(rule)
}.onFinally {
finally?.invoke()
}
}
}
@@ -6,13 +6,17 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Sync
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@@ -30,9 +34,14 @@ import io.legado.app.ui.book.group.GroupDeleteAction
import io.legado.app.ui.book.group.GroupEditContent
import io.legado.app.ui.book.group.GroupResetCoverAction
import io.legado.app.ui.book.group.GroupViewModel
import io.legado.app.ui.tagGroupRule.TagGroupRuleEditSheet
import io.legado.app.ui.tagGroupRule.TagGroupRuleIntent
import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.series.SmallPlainButton
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.utils.move
import org.koin.androidx.compose.koinViewModel
@@ -44,14 +53,27 @@ fun GroupManageSheet(
show: Boolean,
onDismissRequest: () -> Unit,
viewModel: GroupViewModel = koinViewModel(),
bookshelfViewModel: BookshelfViewModel = koinViewModel()
bookshelfViewModel: BookshelfViewModel = koinViewModel(),
tagGroupRuleViewModel: TagGroupRuleViewModel = koinViewModel()
) {
val context = LocalContext.current
val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
var isEditing by remember { mutableStateOf(false) }
var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) }
var editingTagRule by remember { mutableStateOf<io.legado.app.data.entities.TagGroupRule?>(null) }
var showTagRuleEdit by remember { mutableStateOf(false) }
var editingGroupTagRule by remember { mutableStateOf<io.legado.app.data.entities.TagGroupRule?>(null) }
LaunchedEffect(editingGroup) {
editingGroupTagRule = editingGroup?.let { group ->
viewModel.getTagGroupRule(group.groupName)
}
}
var listData by remember { mutableStateOf(groups) }
val listState = rememberLazyListState()
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
@@ -95,14 +117,42 @@ fun GroupManageSheet(
},
endAction = {
if (!isEditing) {
SmallPlainButton(
onClick = {
editingGroup = null
coverPath = null
isEditing = true
},
icon = Icons.Default.Add
)
Box {
var showMenu by remember { mutableStateOf(false) }
SmallPlainButton(
onClick = { showMenu = true },
icon = Icons.Default.Add
)
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
RoundDropdownMenuItem(
text = stringResource(R.string.group_add),
leadingIcon = { Icon(Icons.Default.Add, null) },
onClick = {
showMenu = false
editingGroup = null
coverPath = null
isEditing = true
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.tag_group_add_rule),
leadingIcon = { Icon(Icons.Default.Add, null) },
onClick = {
showMenu = false
editingTagRule = null
showTagRuleEdit = true
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.tag_group_sync),
leadingIcon = { Icon(Icons.Default.Sync, null) },
onClick = {
showMenu = false
tagGroupRuleViewModel.onIntent(TagGroupRuleIntent.SyncGroups)
}
)
}
}
} else {
GroupResetCoverAction(
group = editingGroup,
@@ -128,6 +178,7 @@ fun GroupManageSheet(
},
coverPath = coverPath,
onCoverPathChange = { coverPath = it },
tagGroupRule = editingGroupTagRule,
viewModel = viewModel
)
} else {
@@ -158,6 +209,26 @@ fun GroupManageSheet(
}
}
}
TagGroupRuleEditSheet(
show = showTagRuleEdit,
rule = editingTagRule,
onDismissRequest = {
showTagRuleEdit = false
editingTagRule = null
},
onSave = { rule ->
val isNew = editingTagRule == null
tagGroupRuleViewModel.onIntent(
TagGroupRuleIntent.SaveRule(rule, isNew)
)
showTagRuleEdit = false
editingTagRule = null
},
onCopy = { rule ->
tagGroupRuleViewModel.onIntent(TagGroupRuleIntent.CopyRule(rule))
},
onPaste = { tagGroupRuleViewModel.pasteRule() }
)
}
}