Quellcode durchsuchen

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

HapeLee vor 4 Monaten
Ursprung
Commit
389239ca67

+ 44 - 0
app/src/main/java/io/legado/app/data/dao/TagGroupRuleDao.kt

@@ -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()
+}

+ 63 - 0
app/src/main/java/io/legado/app/data/repository/TagGroupRuleRepository.kt

@@ -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
+
+}

+ 49 - 3
app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt

@@ -30,6 +30,7 @@ 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.BookGroup
 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.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.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -98,6 +99,7 @@ fun GroupEditContent(
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
     coverPath: String?,
     coverPath: String?,
     onCoverPathChange: (String?) -> Unit,
     onCoverPathChange: (String?) -> Unit,
+    tagGroupRule: TagGroupRule? = null,
     viewModel: GroupViewModel = koinViewModel()
     viewModel: GroupViewModel = koinViewModel()
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
@@ -106,6 +108,8 @@ fun GroupEditContent(
     var isPrivate by remember(group) { mutableStateOf(group?.isPrivate ?: false) }
     var isPrivate by remember(group) { mutableStateOf(group?.isPrivate ?: false) }
     var showDisablePrivateDialog by remember(group) { mutableStateOf(false) }
     var showDisablePrivateDialog by remember(group) { mutableStateOf(false) }
     var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
     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 sortOptions = stringArrayResource(R.array.book_sort)
     val sortEntryValues = remember(sortOptions) {
     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))
         Spacer(modifier = Modifier.height(16.dp))
 
 
         ConfirmDismissButtonsRow(
         ConfirmDismissButtonsRow(
@@ -228,16 +254,36 @@ fun GroupEditContent(
                                 enableRefresh = enableRefresh,
                                 enableRefresh = enableRefresh,
                                 isPrivate = isPrivate
                                 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 {
                     } else {
                         viewModel.addGroup(
                         viewModel.addGroup(
                             groupName,
                             groupName,
                             selectedSortIndex,
                             selectedSortIndex,
                             enableRefresh,
                             enableRefresh,
                             isPrivate,
                             isPrivate,
-                            coverPath
+                            coverPath,
+                            pattern = pattern.takeIf { showPattern && it.isNotBlank() }
                         ) {
                         ) {
                             onDismissRequest()
                             onDismissRequest()
                         }
                         }

+ 38 - 0
app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt

@@ -3,7 +3,9 @@ package io.legado.app.ui.book.group
 import android.app.Application
 import android.app.Application
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
 import io.legado.app.data.entities.BookGroup
 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.BookGroupRepository
+import io.legado.app.data.repository.TagGroupRuleRepository
 import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
 import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
 
 
 class GroupViewModel(
 class GroupViewModel(
@@ -12,6 +14,8 @@ class GroupViewModel(
     private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase
     private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase
 ) : BaseViewModel(application) {
 ) : BaseViewModel(application) {
 
 
+    private val tagGroupRuleRepository = TagGroupRuleRepository()
+
     fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
     fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
         execute {
         execute {
             bookGroupRepository.update(*bookGroup)
             bookGroupRepository.update(*bookGroup)
@@ -26,6 +30,7 @@ class GroupViewModel(
         enableRefresh: Boolean,
         enableRefresh: Boolean,
         isPrivate: Boolean,
         isPrivate: Boolean,
         cover: String?,
         cover: String?,
+        pattern: String? = null,
         finally: () -> Unit
         finally: () -> Unit
     ) {
     ) {
         execute {
         execute {
@@ -41,6 +46,14 @@ class GroupViewModel(
             )
             )
             bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId)
             bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId)
             bookGroupRepository.insert(bookGroup)
             bookGroupRepository.insert(bookGroup)
+            if (!pattern.isNullOrBlank()) {
+                val tagRule = TagGroupRule(
+                    groupName = groupName,
+                    pattern = pattern,
+                    order = tagGroupRuleRepository.getMaxOrder()
+                )
+                tagGroupRuleRepository.insert(tagRule)
+            }
         }.onFinally {
         }.onFinally {
             finally()
             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()
+        }
+    }
+
 }
 }

+ 81 - 10
app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt

@@ -6,13 +6,17 @@ import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.togetherWith
 import androidx.compose.animation.togetherWith
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 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.MoreVert
+import androidx.compose.material.icons.filled.Sync
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 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.GroupEditContent
 import io.legado.app.ui.book.group.GroupResetCoverAction
 import io.legado.app.ui.book.group.GroupResetCoverAction
 import io.legado.app.ui.book.group.GroupViewModel
 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.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.series.SmallPlainButton
 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.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.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.utils.move
 import io.legado.app.utils.move
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
@@ -44,14 +53,27 @@ fun GroupManageSheet(
     show: Boolean,
     show: Boolean,
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
     viewModel: GroupViewModel = koinViewModel(),
     viewModel: GroupViewModel = koinViewModel(),
-    bookshelfViewModel: BookshelfViewModel = koinViewModel()
+    bookshelfViewModel: BookshelfViewModel = koinViewModel(),
+    tagGroupRuleViewModel: TagGroupRuleViewModel = koinViewModel()
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
     val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
     val groups by bookshelfViewModel.allGroupsFlow.collectAsState()
+
     var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
     var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
     var isEditing by remember { mutableStateOf(false) }
     var isEditing by remember { mutableStateOf(false) }
     var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) }
     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) }
     var listData by remember { mutableStateOf(groups) }
     val listState = rememberLazyListState()
     val listState = rememberLazyListState()
     val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
     val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
@@ -95,14 +117,42 @@ fun GroupManageSheet(
         },
         },
         endAction = {
         endAction = {
             if (!isEditing) {
             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 {
             } else {
                 GroupResetCoverAction(
                 GroupResetCoverAction(
                     group = editingGroup,
                     group = editingGroup,
@@ -128,6 +178,7 @@ fun GroupManageSheet(
                     },
                     },
                     coverPath = coverPath,
                     coverPath = coverPath,
                     onCoverPathChange = { coverPath = it },
                     onCoverPathChange = { coverPath = it },
+                    tagGroupRule = editingGroupTagRule,
                     viewModel = viewModel
                     viewModel = viewModel
                 )
                 )
             } else {
             } 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() }
+        )
     }
     }
 }
 }
-