diff --git a/app/src/main/java/io/legado/app/data/dao/TagGroupRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/TagGroupRuleDao.kt new file mode 100644 index 000000000..9c5d3dad2 --- /dev/null +++ b/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> + + @Query("SELECT * FROM tag_group_rules ORDER BY `order` ASC") + fun getAll(): List + + @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): List + + @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() +} diff --git a/app/src/main/java/io/legado/app/data/repository/TagGroupRuleRepository.kt b/app/src/main/java/io/legado/app/data/repository/TagGroupRuleRepository.kt new file mode 100644 index 000000000..9e5bd0ef1 --- /dev/null +++ b/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> { + 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): List = + withContext(Dispatchers.IO) { + if (ids.isEmpty()) emptyList() else dao.getByIds(ids) + } + + suspend fun deleteByIds(ids: Set) = withContext(Dispatchers.IO) { + if (ids.isEmpty()) return@withContext + val rules = dao.getByIds(ids) + dao.delete(*rules.toTypedArray()) + } + + suspend fun moveOrder(rules: List) = withContext(Dispatchers.IO) { + val updatedRules = rules.mapIndexed { index, rule -> + rule.copy(order = index) + } + dao.update(*updatedRules.toTypedArray()) + } + + fun getMaxOrder(): Int = dao.maxOrder + +} diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt index b4ac2613c..be4a20797 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt +++ b/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 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() } diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt index a3b9fd160..19c9ad115 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt +++ b/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 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() + } + } + } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt index 2888b82e3..7c61d3684 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt +++ b/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.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(null) } var isEditing by remember { mutableStateOf(false) } var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) } + var editingTagRule by remember { mutableStateOf(null) } + var showTagRuleEdit by remember { mutableStateOf(false) } + + var editingGroupTagRule by remember { mutableStateOf(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() } + ) } } -