feat: 添加标签分组规则功能,支持按书籍标签自动匹配分组
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.legado.app.help.book.applyTagGroupRulesForBook
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
@@ -387,6 +388,7 @@ data class Book(
|
||||
}
|
||||
|
||||
fun save() {
|
||||
applyTagGroupRulesForBook(this)
|
||||
if (appDb.bookDao.has(bookUrl)) {
|
||||
appDb.bookDao.update(this)
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import io.legado.app.ui.widget.components.list.SelectableItem
|
||||
|
||||
@Entity(tableName = "tag_group_rules")
|
||||
data class TagGroupRule(
|
||||
@PrimaryKey
|
||||
var id: Long = System.currentTimeMillis(),
|
||||
var pattern: String = "",
|
||||
var groupName: String = "",
|
||||
var order: Int = 0,
|
||||
) {
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is TagGroupRule) return id == other.id
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -39,4 +39,6 @@ val appDatabaseModule = module {
|
||||
factory<HomepageModuleDao> { get<AppDatabase>().homepageModuleDao }
|
||||
factory<HomepageCustomSetDao> { get<AppDatabase>().homepageCustomSetDao }
|
||||
factory<HighlightRuleDao> { get<AppDatabase>().highlightRuleDao }
|
||||
factory<HighlightTagRuleDao> { get<AppDatabase>().highlightTagRuleDao }
|
||||
factory<TagGroupRuleDao> { get<AppDatabase>().tagGroupRuleDao }
|
||||
}
|
||||
@@ -118,6 +118,8 @@ import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
|
||||
import io.legado.app.ui.dict.DictViewModel
|
||||
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
||||
import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
|
||||
import io.legado.app.ui.tagGroupRule.TagGroupRuleViewModel
|
||||
import io.legado.app.ui.main.MainRouteSearchContent
|
||||
import io.legado.app.ui.main.MainViewModel
|
||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||
@@ -231,6 +233,8 @@ val appModule = module {
|
||||
}
|
||||
|
||||
viewModelOf(::DictRuleViewModel)
|
||||
viewModelOf(::HighlightTagRuleViewModel)
|
||||
viewModelOf(::TagGroupRuleViewModel)
|
||||
viewModelOf(::DictViewModel)
|
||||
viewModelOf(::RssSourceViewModel)
|
||||
viewModelOf(::RssSortViewModel)
|
||||
|
||||
@@ -14,6 +14,8 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BaseBook
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.HighlightTagRule
|
||||
import io.legado.app.ui.book.info.HighlightedTag
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.RuleBigDataHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
@@ -345,6 +347,168 @@ fun Book.upKind() {
|
||||
kind = kinds.distinct().joinToString(",")
|
||||
}
|
||||
|
||||
fun parseHighlightedTags(
|
||||
kindLabels: List<String>,
|
||||
rules: List<HighlightTagRule>,
|
||||
): Pair<List<HighlightedTag>, List<String>> {
|
||||
if (rules.isEmpty()) {
|
||||
return emptyList<HighlightedTag>() to kindLabels
|
||||
}
|
||||
|
||||
val compiledRules = rules.mapNotNull { rule ->
|
||||
val regex = try {
|
||||
Regex(rule.pattern)
|
||||
} catch (_: Exception) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
rule to regex
|
||||
}
|
||||
if (compiledRules.isEmpty()) {
|
||||
return emptyList<HighlightedTag>() to kindLabels
|
||||
}
|
||||
|
||||
// Group matched labels by rule
|
||||
val ruleToLabels = linkedMapOf<HighlightTagRule, MutableList<String>>()
|
||||
val regular = mutableListOf<String>()
|
||||
|
||||
for (tag in kindLabels) {
|
||||
var matched = false
|
||||
for ((rule, regex) in compiledRules) {
|
||||
if (regex.containsMatchIn(tag)) {
|
||||
matched = true
|
||||
ruleToLabels.getOrPut(rule) { mutableListOf() }.add(tag)
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!matched) {
|
||||
regular.add(tag)
|
||||
}
|
||||
}
|
||||
|
||||
val highlighted = ruleToLabels.map { (rule, labels) ->
|
||||
HighlightedTag(
|
||||
matchedLabels = labels,
|
||||
title = rule.title.takeIf { it.isNotBlank() },
|
||||
)
|
||||
}
|
||||
|
||||
return highlighted to regular
|
||||
}
|
||||
|
||||
fun applyTagGroupRules(
|
||||
books: List<Book>,
|
||||
rules: List<io.legado.app.data.entities.TagGroupRule>,
|
||||
) {
|
||||
if (rules.isEmpty()) return
|
||||
|
||||
val compiledRules = rules.mapNotNull { rule ->
|
||||
val regex = try {
|
||||
Regex(rule.pattern)
|
||||
} catch (_: Exception) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
rule to regex
|
||||
}
|
||||
if (compiledRules.isEmpty()) return
|
||||
|
||||
val groupDao = appDb.bookGroupDao
|
||||
val bookDao = appDb.bookDao
|
||||
|
||||
// Resolve groupName -> groupId (find or create BookGroup)
|
||||
val groupCache = mutableMapOf<String, Long>()
|
||||
for ((rule, _) in compiledRules) {
|
||||
if (rule.groupName !in groupCache) {
|
||||
val existing = groupDao.getByName(rule.groupName)
|
||||
val groupId = existing?.groupId ?: run {
|
||||
val newId = groupDao.getUnusedId()
|
||||
groupDao.insert(
|
||||
io.legado.app.data.entities.BookGroup(
|
||||
groupId = newId,
|
||||
groupName = rule.groupName,
|
||||
)
|
||||
)
|
||||
newId
|
||||
}
|
||||
groupCache[rule.groupName] = groupId
|
||||
}
|
||||
}
|
||||
|
||||
// Mask of all group IDs managed by tag group rules
|
||||
val allRuleGroupMask = groupCache.values.fold(0L) { acc, id -> acc or id }
|
||||
|
||||
val updatedBooks = mutableListOf<Book>()
|
||||
for (book in books) {
|
||||
val kinds = book.kind?.splitNotBlank(",", "\n").orEmpty()
|
||||
var newGroupMask = 0L
|
||||
for ((rule, regex) in compiledRules) {
|
||||
if (kinds.any { regex.containsMatchIn(it) }) {
|
||||
newGroupMask = newGroupMask or (groupCache[rule.groupName] ?: 0L)
|
||||
}
|
||||
}
|
||||
// Clear old rule-managed bits, then set new ones
|
||||
val clearedGroup = book.group and allRuleGroupMask.inv()
|
||||
val finalGroup = clearedGroup or newGroupMask
|
||||
if (book.group != finalGroup) {
|
||||
book.group = finalGroup
|
||||
updatedBooks.add(book)
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedBooks.isNotEmpty()) {
|
||||
appDb.runInTransaction {
|
||||
bookDao.update(*updatedBooks.toTypedArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply tag group rules to a single book. Called from Book.save().
|
||||
* Lightweight: only processes the given book, not all books.
|
||||
*/
|
||||
fun applyTagGroupRulesForBook(book: Book) {
|
||||
val rules = appDb.tagGroupRuleDao.getAll()
|
||||
if (rules.isEmpty()) return
|
||||
|
||||
val compiledRules = rules.mapNotNull { rule ->
|
||||
val regex = try { Regex(rule.pattern) } catch (_: Exception) { return@mapNotNull null }
|
||||
rule to regex
|
||||
}
|
||||
if (compiledRules.isEmpty()) return
|
||||
|
||||
val groupDao = appDb.bookGroupDao
|
||||
val groupCache = mutableMapOf<String, Long>()
|
||||
for ((rule, _) in compiledRules) {
|
||||
if (rule.groupName !in groupCache) {
|
||||
val existing = groupDao.getByName(rule.groupName)
|
||||
val groupId = existing?.groupId ?: run {
|
||||
val newId = groupDao.getUnusedId()
|
||||
groupDao.insert(
|
||||
io.legado.app.data.entities.BookGroup(
|
||||
groupId = newId,
|
||||
groupName = rule.groupName,
|
||||
)
|
||||
)
|
||||
newId
|
||||
}
|
||||
groupCache[rule.groupName] = groupId
|
||||
}
|
||||
}
|
||||
|
||||
val allRuleGroupMask = groupCache.values.fold(0L) { acc, id -> acc or id }
|
||||
val kinds = book.kind?.splitNotBlank(",", "\n").orEmpty()
|
||||
var newGroupMask = 0L
|
||||
for ((rule, regex) in compiledRules) {
|
||||
if (kinds.any { regex.containsMatchIn(it) }) {
|
||||
newGroupMask = newGroupMask or (groupCache[rule.groupName] ?: 0L)
|
||||
}
|
||||
}
|
||||
val clearedGroup = book.group and allRuleGroupMask.inv()
|
||||
val finalGroup = clearedGroup or newGroupMask
|
||||
if (book.group != finalGroup) {
|
||||
book.group = finalGroup
|
||||
}
|
||||
}
|
||||
|
||||
fun Book.sync(oldBook: Book) {
|
||||
val curBook = appDb.bookDao.getBook(oldBook.bookUrl)!!
|
||||
durChapterTime = curBook.durChapterTime
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package io.legado.app.ui.tagGroupRule
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.legado.app.data.entities.TagGroupRule
|
||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||
import io.legado.app.ui.widget.components.list.InteractionState
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import io.legado.app.ui.widget.components.list.SelectableItem
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
@Stable
|
||||
data class TagGroupRuleItemUi(
|
||||
override val id: Long,
|
||||
val displayName: String,
|
||||
val pattern: String,
|
||||
val groupName: String,
|
||||
val rule: TagGroupRule
|
||||
) : SelectableItem<Long>
|
||||
|
||||
@Stable
|
||||
data class TagGroupRuleUiState(
|
||||
override val items: ImmutableList<TagGroupRuleItemUi> = persistentListOf(),
|
||||
override val selectedIds: ImmutableSet<Long> = persistentSetOf(),
|
||||
override val searchKey: String = "",
|
||||
val interaction: InteractionState = InteractionState()
|
||||
) : ListUiState<TagGroupRuleItemUi> {
|
||||
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||
override val isLoading: Boolean get() = interaction.isUploading
|
||||
}
|
||||
|
||||
sealed interface TagGroupRuleIntent {
|
||||
data class SetSearchMode(val active: Boolean) : TagGroupRuleIntent
|
||||
data class UpdateSearchQuery(val query: String) : TagGroupRuleIntent
|
||||
data object ClearSelection : TagGroupRuleIntent
|
||||
data object SelectAll : TagGroupRuleIntent
|
||||
data object InvertSelection : TagGroupRuleIntent
|
||||
data class SetSelection(val ids: Set<Long>) : TagGroupRuleIntent
|
||||
data class ToggleSelection(val id: Long) : TagGroupRuleIntent
|
||||
data object DeleteSelection : TagGroupRuleIntent
|
||||
data object UploadSelection : TagGroupRuleIntent
|
||||
data class ExportSelection(val uri: android.net.Uri) : TagGroupRuleIntent
|
||||
data class MoveItem(val from: Int, val to: Int) : TagGroupRuleIntent
|
||||
data object SaveSortOrder : TagGroupRuleIntent
|
||||
data class SaveRule(val rule: TagGroupRule, val isNew: Boolean) : TagGroupRuleIntent
|
||||
data class DeleteRule(val rule: TagGroupRule) : TagGroupRuleIntent
|
||||
data class CopyRule(val rule: TagGroupRule) : TagGroupRuleIntent
|
||||
data class ImportSource(val text: String) : TagGroupRuleIntent
|
||||
data object CancelImport : TagGroupRuleIntent
|
||||
data class ToggleImportSelection(val index: Int) : TagGroupRuleIntent
|
||||
data class ToggleImportAll(val isSelected: Boolean) : TagGroupRuleIntent
|
||||
data class UpdateImportItem(val index: Int, val rule: TagGroupRule) : TagGroupRuleIntent
|
||||
data object SaveImportedRules : TagGroupRuleIntent
|
||||
data object SyncGroups : TagGroupRuleIntent
|
||||
}
|
||||
|
||||
sealed interface TagGroupRuleEffect
|
||||
|
||||
data class TagGroupRuleRenderState(
|
||||
val uiState: TagGroupRuleUiState,
|
||||
val importState: BaseImportUiState<TagGroupRule>,
|
||||
)
|
||||
@@ -0,0 +1,151 @@
|
||||
package io.legado.app.ui.tagGroupRule
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.NoteAdd
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.ContentPaste
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.TagGroupRule
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.button.series.MediumPlainButton
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TagGroupRuleEditSheet(
|
||||
show: Boolean,
|
||||
rule: TagGroupRule?,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSave: (TagGroupRule) -> Unit,
|
||||
onCopy: (TagGroupRule) -> Unit,
|
||||
onPaste: () -> TagGroupRule?,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val isNew = rule == null || rule.id == 0L
|
||||
val initial = remember(rule) {
|
||||
rule ?: TagGroupRule()
|
||||
}
|
||||
|
||||
var pattern by remember(initial) { mutableStateOf(initial.pattern) }
|
||||
var groupName by remember(initial) { mutableStateOf(initial.groupName) }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
fun getCurrentRule(): TagGroupRule {
|
||||
return initial.copy(
|
||||
pattern = pattern,
|
||||
groupName = groupName
|
||||
)
|
||||
}
|
||||
|
||||
AppModalBottomSheet(
|
||||
title = if (isNew) {
|
||||
stringResource(R.string.tag_group_add_rule)
|
||||
} else {
|
||||
stringResource(R.string.tag_group_edit_rule)
|
||||
},
|
||||
startAction = {
|
||||
MediumPlainButton(
|
||||
onClick = onDismissRequest,
|
||||
icon = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
)
|
||||
},
|
||||
endAction = {
|
||||
Box {
|
||||
MediumPlainButton(
|
||||
onClick = { showMenu = true },
|
||||
icon = Icons.Default.MoreVert,
|
||||
contentDescription = "More"
|
||||
)
|
||||
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.copy_rule),
|
||||
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
|
||||
onClick = {
|
||||
onCopy(getCurrentRule())
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.paste_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
onPaste()?.let { pasted ->
|
||||
pattern = pasted.pattern
|
||||
groupName = pasted.groupName
|
||||
}
|
||||
}
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
show = show,
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 120.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
AppTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = groupName,
|
||||
onValueChange = { groupName = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
label = stringResource(R.string.tag_group_name),
|
||||
singleLine = true
|
||||
)
|
||||
AppTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = pattern,
|
||||
onValueChange = { pattern = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
label = stringResource(R.string.tag_group_pattern),
|
||||
minLines = 3
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
AppFloatingActionButton(
|
||||
onClick = { onSave(getCurrentRule()) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
icon = Icons.Default.Save
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package io.legado.app.ui.tagGroupRule
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.TagGroupRule
|
||||
import io.legado.app.data.repository.TagGroupRuleRepository
|
||||
import io.legado.app.data.repository.UploadRepository
|
||||
import io.legado.app.help.book.applyTagGroupRules
|
||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||
import io.legado.app.ui.widget.components.list.InteractionState
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.getClipText
|
||||
import io.legado.app.utils.isJsonArray
|
||||
import io.legado.app.utils.isJsonObject
|
||||
import io.legado.app.utils.sendToClip
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TagGroupRuleViewModel(
|
||||
application: Application,
|
||||
uploadRepository: UploadRepository
|
||||
) : BaseRuleViewModel<TagGroupRuleItemUi, TagGroupRule, Long, TagGroupRuleUiState>(
|
||||
application,
|
||||
TagGroupRuleUiState(interaction = InteractionState(isLoading = true)),
|
||||
uploadRepository
|
||||
) {
|
||||
private val repository = TagGroupRuleRepository()
|
||||
|
||||
override val rawDataFlow: Flow<List<TagGroupRule>> = repository.flowAll()
|
||||
|
||||
fun onIntent(intent: TagGroupRuleIntent) {
|
||||
when (intent) {
|
||||
is TagGroupRuleIntent.SetSearchMode -> setSearchMode(intent.active)
|
||||
is TagGroupRuleIntent.UpdateSearchQuery -> setSearchKey(intent.query)
|
||||
TagGroupRuleIntent.ClearSelection -> setSelection(emptySet())
|
||||
TagGroupRuleIntent.SelectAll -> selectAll()
|
||||
TagGroupRuleIntent.InvertSelection -> invertSelection()
|
||||
is TagGroupRuleIntent.SetSelection -> setSelection(intent.ids)
|
||||
is TagGroupRuleIntent.ToggleSelection -> toggleSelection(intent.id)
|
||||
TagGroupRuleIntent.DeleteSelection -> {
|
||||
delSelectionByIds(uiState.value.selectedIds)
|
||||
setSelection(emptySet())
|
||||
}
|
||||
TagGroupRuleIntent.UploadSelection -> {
|
||||
val state = uiState.value
|
||||
uploadSelectedRules(state.selectedIds, state.items)
|
||||
}
|
||||
is TagGroupRuleIntent.ExportSelection -> {
|
||||
val state = uiState.value
|
||||
exportToUri(intent.uri, state.items, state.selectedIds)
|
||||
}
|
||||
is TagGroupRuleIntent.MoveItem -> moveItemInList(intent.from, intent.to)
|
||||
TagGroupRuleIntent.SaveSortOrder -> saveSortOrder()
|
||||
is TagGroupRuleIntent.SaveRule -> {
|
||||
if (intent.isNew) {
|
||||
insert(intent.rule)
|
||||
} else {
|
||||
update(intent.rule)
|
||||
}
|
||||
}
|
||||
is TagGroupRuleIntent.DeleteRule -> delete(intent.rule)
|
||||
is TagGroupRuleIntent.CopyRule -> copyRule(intent.rule)
|
||||
is TagGroupRuleIntent.ImportSource -> importSource(intent.text)
|
||||
TagGroupRuleIntent.CancelImport -> cancelImport()
|
||||
is TagGroupRuleIntent.ToggleImportSelection -> toggleImportSelection(intent.index)
|
||||
is TagGroupRuleIntent.ToggleImportAll -> toggleImportAll(intent.isSelected)
|
||||
is TagGroupRuleIntent.UpdateImportItem -> updateImportItem(intent.index, intent.rule)
|
||||
TagGroupRuleIntent.SaveImportedRules -> saveImportedRules()
|
||||
TagGroupRuleIntent.SyncGroups -> syncGroups()
|
||||
}
|
||||
}
|
||||
|
||||
override fun filterData(
|
||||
data: List<TagGroupRule>,
|
||||
searchKey: String,
|
||||
groupFilter: String
|
||||
): List<TagGroupRule> {
|
||||
val key = groupFilter.ifEmpty { searchKey }
|
||||
val filtered = if (key.isEmpty()) data else {
|
||||
data.filter {
|
||||
it.groupName.contains(key, ignoreCase = true) ||
|
||||
it.pattern.contains(key, ignoreCase = true)
|
||||
}
|
||||
}
|
||||
return filtered.sortedBy { it.order }
|
||||
}
|
||||
|
||||
override fun composeUiState(
|
||||
items: List<TagGroupRuleItemUi>,
|
||||
selectedIds: Set<Long>,
|
||||
isSearch: Boolean,
|
||||
isUploading: Boolean,
|
||||
importState: BaseImportUiState<TagGroupRule>
|
||||
): TagGroupRuleUiState {
|
||||
return TagGroupRuleUiState(
|
||||
items = items.toImmutableList(),
|
||||
selectedIds = selectedIds.toImmutableSet(),
|
||||
searchKey = _searchKey.value,
|
||||
interaction = InteractionState(
|
||||
isSearchMode = isSearch,
|
||||
isUploading = isUploading || (importState is BaseImportUiState.Loading),
|
||||
isLoading = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun TagGroupRule.toUiItem() = TagGroupRuleItemUi(
|
||||
id = id,
|
||||
displayName = groupName.ifBlank { pattern },
|
||||
pattern = pattern,
|
||||
groupName = groupName,
|
||||
rule = this
|
||||
)
|
||||
|
||||
override fun ruleItemToEntity(item: TagGroupRuleItemUi): TagGroupRule = item.rule
|
||||
|
||||
override suspend fun generateJson(entities: List<TagGroupRule>): String = GSON.toJson(entities)
|
||||
|
||||
override fun parseImportRules(text: String): List<TagGroupRule> {
|
||||
return when {
|
||||
text.isJsonArray() -> GSON.fromJsonArray<TagGroupRule>(text).getOrThrow()
|
||||
text.isJsonObject() -> listOf(GSON.fromJsonObject<TagGroupRule>(text).getOrThrow())
|
||||
else -> throw Exception("格式不正确")
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasChanged(newRule: TagGroupRule, oldRule: TagGroupRule): Boolean {
|
||||
return newRule.groupName != oldRule.groupName
|
||||
|| newRule.pattern != oldRule.pattern
|
||||
}
|
||||
|
||||
override suspend fun findOldRule(newRule: TagGroupRule): TagGroupRule? {
|
||||
return repository.findById(newRule.id)
|
||||
}
|
||||
|
||||
override fun saveImportedRules() {
|
||||
val state = _importState.value as? BaseImportUiState.Success<TagGroupRule> ?: return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val rulesToSave = state.items
|
||||
.filter { it.isSelected }
|
||||
.map { it.data }
|
||||
repository.insert(*rulesToSave.toTypedArray())
|
||||
withContext(Dispatchers.Main) {
|
||||
_importState.value = BaseImportUiState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSortOrder() {
|
||||
val currentLocal = _localItems.value ?: return
|
||||
viewModelScope.launch {
|
||||
repository.moveOrder(currentLocal.map { it.rule })
|
||||
_localItems.value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun delSelectionByIds(ids: Set<Long>) {
|
||||
viewModelScope.launch {
|
||||
repository.deleteByIds(ids)
|
||||
_selectedIds.update { it - ids }
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectAll() {
|
||||
setSelection(uiState.value.items.map { it.id }.toSet())
|
||||
}
|
||||
|
||||
private fun invertSelection() {
|
||||
val state = uiState.value
|
||||
setSelection(state.items.map { it.id }.toSet() - state.selectedIds)
|
||||
}
|
||||
|
||||
fun update(vararg rule: TagGroupRule) = viewModelScope.launch {
|
||||
repository.update(*rule)
|
||||
autoApplyRules()
|
||||
}
|
||||
|
||||
fun insert(vararg rule: TagGroupRule) = viewModelScope.launch {
|
||||
repository.insert(*rule)
|
||||
autoApplyRules()
|
||||
}
|
||||
|
||||
fun delete(vararg rule: TagGroupRule) = viewModelScope.launch {
|
||||
repository.delete(*rule)
|
||||
autoApplyRules()
|
||||
}
|
||||
|
||||
private suspend fun autoApplyRules() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val books = appDb.bookDao.getAll()
|
||||
val rules = appDb.tagGroupRuleDao.getAll()
|
||||
applyTagGroupRules(books, rules)
|
||||
}
|
||||
}
|
||||
|
||||
fun copyRule(rule: TagGroupRule) {
|
||||
context.sendToClip(GSON.toJson(rule))
|
||||
}
|
||||
|
||||
fun pasteRule(): TagGroupRule? {
|
||||
val text = context.getClipText()
|
||||
if (text.isNullOrBlank()) {
|
||||
context.toastOnUi("剪贴板没有内容")
|
||||
return null
|
||||
}
|
||||
return try {
|
||||
GSON.fromJsonObject<TagGroupRule>(text).getOrThrow()
|
||||
} catch (e: Exception) {
|
||||
context.toastOnUi("格式不对")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncGroups() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val books = appDb.bookDao.getAll()
|
||||
val rules = appDb.tagGroupRuleDao.getAll()
|
||||
applyTagGroupRules(books, rules)
|
||||
withContext(Dispatchers.Main) {
|
||||
context.toastOnUi(R.string.tag_group_sync_complete)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user