feat: 添加高亮标签规则配置入口和导航
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
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.HighlightTagRule
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface HighlightTagRuleDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlight_tag_rules ORDER BY `order` ASC")
|
||||||
|
fun flowAll(): Flow<List<HighlightTagRule>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlight_tag_rules WHERE id = :id")
|
||||||
|
suspend fun getById(id: Long): HighlightTagRule?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlight_tag_rules WHERE id IN (:ids)")
|
||||||
|
suspend fun getByIds(ids: Set<Long>): List<HighlightTagRule>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlight_tag_rules WHERE enabled = 1 ORDER BY `order` ASC")
|
||||||
|
suspend fun getEnabled(): List<HighlightTagRule>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insert(vararg rules: HighlightTagRule)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun update(vararg rules: HighlightTagRule)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun delete(vararg rules: HighlightTagRule)
|
||||||
|
|
||||||
|
@Query("DELETE FROM highlight_tag_rules")
|
||||||
|
suspend fun deleteAll()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
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 = "highlight_tag_rules")
|
||||||
|
data class HighlightTagRule(
|
||||||
|
@PrimaryKey
|
||||||
|
var id: Long = System.currentTimeMillis(),
|
||||||
|
var title: String = "",
|
||||||
|
var pattern: String = "",
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
var order: Int = 0,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun hashCode(): Int = id.hashCode()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (other is HighlightTagRule) return id == other.id
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.HighlightTagRule
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class HighlightTagRuleRepository {
|
||||||
|
|
||||||
|
private val dao = appDb.highlightTagRuleDao
|
||||||
|
|
||||||
|
fun flowAll(): Flow<List<HighlightTagRule>> {
|
||||||
|
return dao.flowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun insert(vararg rule: HighlightTagRule) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
dao.insert(*rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun delete(vararg rule: HighlightTagRule) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
dao.delete(*rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun update(vararg rule: HighlightTagRule) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
dao.update(*rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findById(id: Long): HighlightTagRule? = withContext(Dispatchers.IO) {
|
||||||
|
dao.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getByIds(ids: Set<Long>): List<HighlightTagRule> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (ids.isEmpty()) emptyList() else dao.getByIds(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun enableByIds(ids: Set<Long>) = withContext(Dispatchers.IO) {
|
||||||
|
if (ids.isEmpty()) return@withContext
|
||||||
|
val rules = dao.getByIds(ids)
|
||||||
|
val updated = rules.map { it.copy(enabled = true) }
|
||||||
|
dao.update(*updated.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun disableByIds(ids: Set<Long>) = withContext(Dispatchers.IO) {
|
||||||
|
if (ids.isEmpty()) return@withContext
|
||||||
|
val rules = dao.getByIds(ids)
|
||||||
|
val updated = rules.map { it.copy(enabled = false) }
|
||||||
|
dao.update(*updated.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
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<HighlightTagRule>) = withContext(Dispatchers.IO) {
|
||||||
|
val updatedRules = rules.mapIndexed { index, rule ->
|
||||||
|
rule.copy(order = index)
|
||||||
|
}
|
||||||
|
dao.update(*updatedRules.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package io.legado.app.ui.highlightTagRule
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import io.legado.app.data.entities.HighlightTagRule
|
||||||
|
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 HighlightTagRuleItemUi(
|
||||||
|
override val id: Long,
|
||||||
|
val displayName: String,
|
||||||
|
val pattern: String,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val rule: HighlightTagRule
|
||||||
|
) : SelectableItem<Long>
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class HighlightTagRuleUiState(
|
||||||
|
override val items: ImmutableList<HighlightTagRuleItemUi> = persistentListOf(),
|
||||||
|
override val selectedIds: ImmutableSet<Long> = persistentSetOf(),
|
||||||
|
override val searchKey: String = "",
|
||||||
|
val interaction: InteractionState = InteractionState()
|
||||||
|
) : ListUiState<HighlightTagRuleItemUi> {
|
||||||
|
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||||
|
override val isLoading: Boolean get() = interaction.isUploading
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HighlightTagRuleIntent {
|
||||||
|
data class SetSearchMode(val active: Boolean) : HighlightTagRuleIntent
|
||||||
|
data class UpdateSearchQuery(val query: String) : HighlightTagRuleIntent
|
||||||
|
data object ClearSelection : HighlightTagRuleIntent
|
||||||
|
data object SelectAll : HighlightTagRuleIntent
|
||||||
|
data object InvertSelection : HighlightTagRuleIntent
|
||||||
|
data class SetSelection(val ids: Set<Long>) : HighlightTagRuleIntent
|
||||||
|
data class ToggleSelection(val id: Long) : HighlightTagRuleIntent
|
||||||
|
data object EnableSelection : HighlightTagRuleIntent
|
||||||
|
data object DisableSelection : HighlightTagRuleIntent
|
||||||
|
data object DeleteSelection : HighlightTagRuleIntent
|
||||||
|
data object UploadSelection : HighlightTagRuleIntent
|
||||||
|
data class ExportSelection(val uri: android.net.Uri) : HighlightTagRuleIntent
|
||||||
|
data class MoveItem(val from: Int, val to: Int) : HighlightTagRuleIntent
|
||||||
|
data object SaveSortOrder : HighlightTagRuleIntent
|
||||||
|
data class SaveRule(val rule: HighlightTagRule, val isNew: Boolean) : HighlightTagRuleIntent
|
||||||
|
data class DeleteRule(val rule: HighlightTagRule) : HighlightTagRuleIntent
|
||||||
|
data class SetRuleEnabled(val rule: HighlightTagRule, val enabled: Boolean) : HighlightTagRuleIntent
|
||||||
|
data class CopyRule(val rule: HighlightTagRule) : HighlightTagRuleIntent
|
||||||
|
data class ImportSource(val text: String) : HighlightTagRuleIntent
|
||||||
|
data object CancelImport : HighlightTagRuleIntent
|
||||||
|
data class ToggleImportSelection(val index: Int) : HighlightTagRuleIntent
|
||||||
|
data class ToggleImportAll(val isSelected: Boolean) : HighlightTagRuleIntent
|
||||||
|
data class UpdateImportItem(val index: Int, val rule: HighlightTagRule) : HighlightTagRuleIntent
|
||||||
|
data object SaveImportedRules : HighlightTagRuleIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface HighlightTagRuleEffect
|
||||||
|
|
||||||
|
data class HighlightTagRuleRenderState(
|
||||||
|
val uiState: HighlightTagRuleUiState,
|
||||||
|
val importState: BaseImportUiState<HighlightTagRule>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package io.legado.app.ui.highlightTagRule
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.height
|
||||||
|
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.HighlightTagRule
|
||||||
|
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.AdaptiveSwitch
|
||||||
|
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 io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun HighlightTagRuleEditSheet(
|
||||||
|
show: Boolean,
|
||||||
|
rule: HighlightTagRule?,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSave: (HighlightTagRule) -> Unit,
|
||||||
|
onCopy: (HighlightTagRule) -> Unit,
|
||||||
|
onPaste: () -> HighlightTagRule?,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val isNew = rule == null || rule.id == 0L
|
||||||
|
val initial = remember(rule) {
|
||||||
|
rule ?: HighlightTagRule()
|
||||||
|
}
|
||||||
|
|
||||||
|
var title by remember(initial) { mutableStateOf(initial.title) }
|
||||||
|
var pattern by remember(initial) { mutableStateOf(initial.pattern) }
|
||||||
|
var enabled by remember(initial) { mutableStateOf(initial.enabled) }
|
||||||
|
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun getCurrentRule(): HighlightTagRule {
|
||||||
|
return initial.copy(
|
||||||
|
title = title,
|
||||||
|
pattern = pattern,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
title = if (isNew) {
|
||||||
|
stringResource(R.string.highlight_tag_add_rule)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.highlight_tag_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 ->
|
||||||
|
title = pasted.title
|
||||||
|
pattern = pasted.pattern
|
||||||
|
enabled = pasted.enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 = title,
|
||||||
|
onValueChange = { title = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||||
|
label = stringResource(R.string.highlight_tag_title),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
AppTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = pattern,
|
||||||
|
onValueChange = { pattern = it },
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||||
|
label = stringResource(R.string.highlight_tag_pattern),
|
||||||
|
minLines = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.enabled),
|
||||||
|
style = LegadoTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
AdaptiveSwitch(
|
||||||
|
checked = enabled,
|
||||||
|
onCheckedChange = { enabled = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppFloatingActionButton(
|
||||||
|
onClick = { onSave(getCurrentRule()) },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp),
|
||||||
|
icon = Icons.Default.Save
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
package io.legado.app.ui.highlightTagRule
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
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.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseRuleEvent
|
||||||
|
import io.legado.app.data.entities.HighlightTagRule
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.ActionItem
|
||||||
|
import io.legado.app.ui.widget.components.DraggableSelectionHandler
|
||||||
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
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.filePicker.FilePickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||||
|
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||||
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
|
import io.legado.app.ui.widget.components.rules.RuleListScaffold
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HighlightTagRuleScreen(
|
||||||
|
viewModel: HighlightTagRuleViewModel = koinViewModel(),
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
HighlightTagRuleContent(
|
||||||
|
state = uiState,
|
||||||
|
importState = importState,
|
||||||
|
events = viewModel.events,
|
||||||
|
onIntent = viewModel::onIntent,
|
||||||
|
onPasteRule = viewModel::pasteRule,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun HighlightTagRuleContent(
|
||||||
|
state: HighlightTagRuleUiState,
|
||||||
|
importState: BaseImportUiState<HighlightTagRule>,
|
||||||
|
events: Flow<BaseRuleEvent>,
|
||||||
|
onIntent: (HighlightTagRuleIntent) -> Unit,
|
||||||
|
onPasteRule: () -> HighlightTagRule?,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val rules = state.items
|
||||||
|
val selectedIds = state.selectedIds
|
||||||
|
val inSelectionMode = selectedIds.isNotEmpty()
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
var showEditSheet by remember { mutableStateOf(false) }
|
||||||
|
var editingRule by remember { mutableStateOf<HighlightTagRule?>(null) }
|
||||||
|
var showDeleteRuleDialog by remember { mutableStateOf<HighlightTagRule?>(null) }
|
||||||
|
var showUrlInput by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showImportSheet by remember { mutableStateOf(false) }
|
||||||
|
var showExportSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|
||||||
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
|
onIntent(HighlightTagRuleIntent.MoveItem(from.index, to.index))
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||||
|
}
|
||||||
|
|
||||||
|
val clipboardManager = LocalClipboard.current
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is BaseRuleEvent.ShowSnackbar -> {
|
||||||
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
message = event.message,
|
||||||
|
actionLabel = event.actionLabel,
|
||||||
|
withDismissAction = true
|
||||||
|
)
|
||||||
|
if (result == SnackbarResult.ActionPerformed && event.url != null) {
|
||||||
|
clipboardManager.setClipEntry(
|
||||||
|
ClipEntry(
|
||||||
|
ClipData.newPlainText(
|
||||||
|
"url",
|
||||||
|
event.url
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importDoc = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
onResult = { uri ->
|
||||||
|
uri?.let {
|
||||||
|
context.contentResolver.openInputStream(it)?.use { stream ->
|
||||||
|
val text = stream.reader().readText()
|
||||||
|
onIntent(HighlightTagRuleIntent.ImportSource(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val exportDoc = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.CreateDocument("application/json"),
|
||||||
|
onResult = { uri ->
|
||||||
|
uri?.let { onIntent(HighlightTagRuleIntent.ExportSelection(it)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SourceInputDialog(
|
||||||
|
show = showUrlInput,
|
||||||
|
title = stringResource(R.string.import_on_line),
|
||||||
|
onDismissRequest = { showUrlInput = false },
|
||||||
|
onConfirm = {
|
||||||
|
showUrlInput = false
|
||||||
|
onIntent(HighlightTagRuleIntent.ImportSource(it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
FilePickerSheet(
|
||||||
|
show = showExportSheet,
|
||||||
|
onDismissRequest = { showExportSheet = false },
|
||||||
|
title = stringResource(R.string.export),
|
||||||
|
onSelectSysDir = {
|
||||||
|
showExportSheet = false
|
||||||
|
exportDoc.launch("exportHighlightTagRule.json")
|
||||||
|
},
|
||||||
|
onUpload = {
|
||||||
|
showExportSheet = false
|
||||||
|
onIntent(HighlightTagRuleIntent.UploadSelection)
|
||||||
|
},
|
||||||
|
allowExtensions = arrayOf("json")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FilePickerSheet(
|
||||||
|
show = showImportSheet,
|
||||||
|
onDismissRequest = { showImportSheet = false },
|
||||||
|
title = stringResource(R.string.import_str),
|
||||||
|
onSelectSysFile = { types ->
|
||||||
|
importDoc.launch(types)
|
||||||
|
showImportSheet = false
|
||||||
|
},
|
||||||
|
onManualInput = {
|
||||||
|
showUrlInput = true
|
||||||
|
showImportSheet = false
|
||||||
|
},
|
||||||
|
allowExtensions = arrayOf("json", "txt")
|
||||||
|
)
|
||||||
|
|
||||||
|
BatchImportDialog(
|
||||||
|
title = stringResource(R.string.import_highlight_tag_rule),
|
||||||
|
importState = importState,
|
||||||
|
onDismissRequest = { onIntent(HighlightTagRuleIntent.CancelImport) },
|
||||||
|
onToggleItem = { onIntent(HighlightTagRuleIntent.ToggleImportSelection(it)) },
|
||||||
|
onToggleAll = { onIntent(HighlightTagRuleIntent.ToggleImportAll(it)) },
|
||||||
|
onUpdateItem = { index, rule -> onIntent(HighlightTagRuleIntent.UpdateImportItem(index, rule)) },
|
||||||
|
onConfirm = { onIntent(HighlightTagRuleIntent.SaveImportedRules) },
|
||||||
|
itemTitle = { rule -> rule.title.ifBlank { rule.pattern } },
|
||||||
|
itemSubtitle = { rule -> rule.pattern.takeIf { it.isNotBlank() } }
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
onIntent(HighlightTagRuleIntent.SaveSortOrder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
data = showDeleteRuleDialog,
|
||||||
|
onDismissRequest = { showDeleteRuleDialog = null },
|
||||||
|
title = stringResource(R.string.delete),
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
onConfirm = { rule ->
|
||||||
|
onIntent(HighlightTagRuleIntent.DeleteRule(rule))
|
||||||
|
showDeleteRuleDialog = null
|
||||||
|
},
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
onDismiss = { showDeleteRuleDialog = null }
|
||||||
|
)
|
||||||
|
|
||||||
|
HighlightTagRuleEditSheet(
|
||||||
|
show = showEditSheet,
|
||||||
|
rule = editingRule,
|
||||||
|
onDismissRequest = {
|
||||||
|
showEditSheet = false
|
||||||
|
editingRule = null
|
||||||
|
},
|
||||||
|
onSave = { updatedRule ->
|
||||||
|
onIntent(HighlightTagRuleIntent.SaveRule(updatedRule, isNew = editingRule == null))
|
||||||
|
showEditSheet = false
|
||||||
|
editingRule = null
|
||||||
|
},
|
||||||
|
onCopy = { onIntent(HighlightTagRuleIntent.CopyRule(it)) },
|
||||||
|
onPaste = onPasteRule
|
||||||
|
)
|
||||||
|
|
||||||
|
RuleListScaffold(
|
||||||
|
title = stringResource(R.string.highlight_tag_config),
|
||||||
|
state = state,
|
||||||
|
onBackClick = { onBackClick() },
|
||||||
|
onSearchToggle = { active ->
|
||||||
|
onIntent(HighlightTagRuleIntent.SetSearchMode(active))
|
||||||
|
},
|
||||||
|
onSearchQueryChange = { onIntent(HighlightTagRuleIntent.UpdateSearchQuery(it)) },
|
||||||
|
searchPlaceholder = stringResource(R.string.replace_purify_search),
|
||||||
|
onClearSelection = { onIntent(HighlightTagRuleIntent.ClearSelection) },
|
||||||
|
onSelectAll = { onIntent(HighlightTagRuleIntent.SelectAll) },
|
||||||
|
onSelectInvert = {
|
||||||
|
onIntent(HighlightTagRuleIntent.InvertSelection)
|
||||||
|
},
|
||||||
|
selectionSecondaryActions = listOf(
|
||||||
|
ActionItem(text = stringResource(R.string.enable), onClick = {
|
||||||
|
onIntent(HighlightTagRuleIntent.EnableSelection)
|
||||||
|
}),
|
||||||
|
ActionItem(text = stringResource(R.string.disable_selection), onClick = {
|
||||||
|
onIntent(HighlightTagRuleIntent.DisableSelection)
|
||||||
|
}),
|
||||||
|
ActionItem(
|
||||||
|
text = stringResource(R.string.export),
|
||||||
|
onClick = { showExportSheet = true })
|
||||||
|
),
|
||||||
|
onDeleteSelected = { ids ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
onIntent(HighlightTagRuleIntent.SetSelection(ids as Set<Long>))
|
||||||
|
onIntent(HighlightTagRuleIntent.DeleteSelection)
|
||||||
|
},
|
||||||
|
onAddClick = {
|
||||||
|
editingRule = null
|
||||||
|
showEditSheet = true
|
||||||
|
},
|
||||||
|
snackbarHostState = snackbarHostState,
|
||||||
|
dropDownMenuContent = { dismiss ->
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = stringResource(R.string.import_str),
|
||||||
|
onClick = { showImportSheet = true; dismiss() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
FastScrollLazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 120.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(rules, key = { it.id }) { item ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = item.id,
|
||||||
|
title = item.displayName,
|
||||||
|
subtitle = item.pattern,
|
||||||
|
isEnabled = item.isEnabled,
|
||||||
|
isSelected = selectedIds.contains(item.id),
|
||||||
|
inSelectionMode = inSelectionMode,
|
||||||
|
onToggleSelection = { onIntent(HighlightTagRuleIntent.ToggleSelection(item.id)) },
|
||||||
|
onEnabledChange = { enabled ->
|
||||||
|
onIntent(HighlightTagRuleIntent.SetRuleEnabled(item.rule, enabled))
|
||||||
|
},
|
||||||
|
onClickEdit = { editingRule = item.rule; showEditSheet = true },
|
||||||
|
trailingAction = {
|
||||||
|
SmallPlainButton(
|
||||||
|
onClick = { showDeleteRuleDialog = item.rule },
|
||||||
|
icon = AppIcons.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inSelectionMode) {
|
||||||
|
DraggableSelectionHandler(
|
||||||
|
listState = listState,
|
||||||
|
items = rules,
|
||||||
|
selectedIds = selectedIds,
|
||||||
|
onSelectionChange = { onIntent(HighlightTagRuleIntent.SetSelection(it)) },
|
||||||
|
idProvider = { it.id },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(60.dp)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package io.legado.app.ui.highlightTagRule
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.legado.app.base.BaseRuleViewModel
|
||||||
|
import io.legado.app.data.entities.HighlightTagRule
|
||||||
|
import io.legado.app.data.repository.HighlightTagRuleRepository
|
||||||
|
import io.legado.app.data.repository.UploadRepository
|
||||||
|
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 HighlightTagRuleViewModel(
|
||||||
|
application: Application,
|
||||||
|
uploadRepository: UploadRepository
|
||||||
|
) : BaseRuleViewModel<HighlightTagRuleItemUi, HighlightTagRule, Long, HighlightTagRuleUiState>(
|
||||||
|
application,
|
||||||
|
HighlightTagRuleUiState(interaction = InteractionState(isLoading = true)),
|
||||||
|
uploadRepository
|
||||||
|
) {
|
||||||
|
private val repository = HighlightTagRuleRepository()
|
||||||
|
|
||||||
|
override val rawDataFlow: Flow<List<HighlightTagRule>> = repository.flowAll()
|
||||||
|
|
||||||
|
fun onIntent(intent: HighlightTagRuleIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is HighlightTagRuleIntent.SetSearchMode -> setSearchMode(intent.active)
|
||||||
|
is HighlightTagRuleIntent.UpdateSearchQuery -> setSearchKey(intent.query)
|
||||||
|
HighlightTagRuleIntent.ClearSelection -> setSelection(emptySet())
|
||||||
|
HighlightTagRuleIntent.SelectAll -> selectAll()
|
||||||
|
HighlightTagRuleIntent.InvertSelection -> invertSelection()
|
||||||
|
is HighlightTagRuleIntent.SetSelection -> setSelection(intent.ids)
|
||||||
|
is HighlightTagRuleIntent.ToggleSelection -> toggleSelection(intent.id)
|
||||||
|
HighlightTagRuleIntent.EnableSelection -> {
|
||||||
|
enableSelectionByIds(uiState.value.selectedIds)
|
||||||
|
setSelection(emptySet())
|
||||||
|
}
|
||||||
|
HighlightTagRuleIntent.DisableSelection -> {
|
||||||
|
disableSelectionByIds(uiState.value.selectedIds)
|
||||||
|
setSelection(emptySet())
|
||||||
|
}
|
||||||
|
HighlightTagRuleIntent.DeleteSelection -> {
|
||||||
|
delSelectionByIds(uiState.value.selectedIds)
|
||||||
|
setSelection(emptySet())
|
||||||
|
}
|
||||||
|
HighlightTagRuleIntent.UploadSelection -> {
|
||||||
|
val state = uiState.value
|
||||||
|
uploadSelectedRules(state.selectedIds, state.items)
|
||||||
|
}
|
||||||
|
is HighlightTagRuleIntent.ExportSelection -> {
|
||||||
|
val state = uiState.value
|
||||||
|
exportToUri(intent.uri, state.items, state.selectedIds)
|
||||||
|
}
|
||||||
|
is HighlightTagRuleIntent.MoveItem -> moveItemInList(intent.from, intent.to)
|
||||||
|
HighlightTagRuleIntent.SaveSortOrder -> saveSortOrder()
|
||||||
|
is HighlightTagRuleIntent.SaveRule -> {
|
||||||
|
if (intent.isNew) {
|
||||||
|
insert(intent.rule)
|
||||||
|
} else {
|
||||||
|
update(intent.rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is HighlightTagRuleIntent.DeleteRule -> delete(intent.rule)
|
||||||
|
is HighlightTagRuleIntent.SetRuleEnabled -> update(intent.rule.copy(enabled = intent.enabled))
|
||||||
|
is HighlightTagRuleIntent.CopyRule -> copyRule(intent.rule)
|
||||||
|
is HighlightTagRuleIntent.ImportSource -> importSource(intent.text)
|
||||||
|
HighlightTagRuleIntent.CancelImport -> cancelImport()
|
||||||
|
is HighlightTagRuleIntent.ToggleImportSelection -> toggleImportSelection(intent.index)
|
||||||
|
is HighlightTagRuleIntent.ToggleImportAll -> toggleImportAll(intent.isSelected)
|
||||||
|
is HighlightTagRuleIntent.UpdateImportItem -> updateImportItem(intent.index, intent.rule)
|
||||||
|
HighlightTagRuleIntent.SaveImportedRules -> saveImportedRules()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun filterData(
|
||||||
|
data: List<HighlightTagRule>,
|
||||||
|
searchKey: String,
|
||||||
|
groupFilter: String
|
||||||
|
): List<HighlightTagRule> {
|
||||||
|
val key = groupFilter.ifEmpty { searchKey }
|
||||||
|
val filtered = if (key.isEmpty()) data else {
|
||||||
|
data.filter {
|
||||||
|
it.title.contains(key, ignoreCase = true) ||
|
||||||
|
it.pattern.contains(key, ignoreCase = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered.sortedBy { it.order }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun composeUiState(
|
||||||
|
items: List<HighlightTagRuleItemUi>,
|
||||||
|
selectedIds: Set<Long>,
|
||||||
|
isSearch: Boolean,
|
||||||
|
isUploading: Boolean,
|
||||||
|
importState: BaseImportUiState<HighlightTagRule>
|
||||||
|
): HighlightTagRuleUiState {
|
||||||
|
return HighlightTagRuleUiState(
|
||||||
|
items = items.toImmutableList(),
|
||||||
|
selectedIds = selectedIds.toImmutableSet(),
|
||||||
|
searchKey = _searchKey.value,
|
||||||
|
interaction = InteractionState(
|
||||||
|
isSearchMode = isSearch,
|
||||||
|
isUploading = isUploading || (importState is BaseImportUiState.Loading),
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun HighlightTagRule.toUiItem() = HighlightTagRuleItemUi(
|
||||||
|
id = id,
|
||||||
|
displayName = title.ifBlank { pattern },
|
||||||
|
pattern = pattern,
|
||||||
|
isEnabled = enabled,
|
||||||
|
rule = this
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun ruleItemToEntity(item: HighlightTagRuleItemUi): HighlightTagRule = item.rule
|
||||||
|
|
||||||
|
override suspend fun generateJson(entities: List<HighlightTagRule>): String = GSON.toJson(entities)
|
||||||
|
|
||||||
|
override fun parseImportRules(text: String): List<HighlightTagRule> {
|
||||||
|
return when {
|
||||||
|
text.isJsonArray() -> GSON.fromJsonArray<HighlightTagRule>(text).getOrThrow()
|
||||||
|
text.isJsonObject() -> listOf(GSON.fromJsonObject<HighlightTagRule>(text).getOrThrow())
|
||||||
|
else -> throw Exception("格式不正确")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasChanged(newRule: HighlightTagRule, oldRule: HighlightTagRule): Boolean {
|
||||||
|
return newRule.title != oldRule.title
|
||||||
|
|| newRule.pattern != oldRule.pattern
|
||||||
|
|| newRule.enabled != oldRule.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun findOldRule(newRule: HighlightTagRule): HighlightTagRule? {
|
||||||
|
return repository.findById(newRule.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveImportedRules() {
|
||||||
|
val state = _importState.value as? BaseImportUiState.Success<HighlightTagRule> ?: 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 enableSelectionByIds(ids: Set<Long>) {
|
||||||
|
viewModelScope.launch { repository.enableByIds(ids) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableSelectionByIds(ids: Set<Long>) {
|
||||||
|
viewModelScope.launch { repository.disableByIds(ids) }
|
||||||
|
}
|
||||||
|
|
||||||
|
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: HighlightTagRule) = viewModelScope.launch { repository.update(*rule) }
|
||||||
|
fun insert(vararg rule: HighlightTagRule) = viewModelScope.launch { repository.insert(*rule) }
|
||||||
|
fun delete(vararg rule: HighlightTagRule) = viewModelScope.launch { repository.delete(*rule) }
|
||||||
|
|
||||||
|
fun copyRule(rule: HighlightTagRule) {
|
||||||
|
context.sendToClip(GSON.toJson(rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pasteRule(): HighlightTagRule? {
|
||||||
|
val text = context.getClipText()
|
||||||
|
if (text.isNullOrBlank()) {
|
||||||
|
context.toastOnUi("剪贴板没有内容")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
GSON.fromJsonObject<HighlightTagRule>(text).getOrThrow()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
context.toastOnUi("格式不对")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,6 +56,7 @@ import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
|||||||
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
|
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
|
||||||
import io.legado.app.ui.config.themeManage.ThemeManageScreen
|
import io.legado.app.ui.config.themeManage.ThemeManageScreen
|
||||||
import io.legado.app.ui.config.translation.TranslationConfigScreen
|
import io.legado.app.ui.config.translation.TranslationConfigScreen
|
||||||
|
import io.legado.app.ui.highlightTagRule.HighlightTagRuleScreen
|
||||||
import io.legado.app.ui.rss.article.MainRouteRssSort
|
import io.legado.app.ui.rss.article.MainRouteRssSort
|
||||||
import io.legado.app.ui.rss.article.RssSortRouteScreen
|
import io.legado.app.ui.rss.article.RssSortRouteScreen
|
||||||
import io.legado.app.ui.rss.favorites.RssFavoritesScreen
|
import io.legado.app.ui.rss.favorites.RssFavoritesScreen
|
||||||
@@ -158,6 +159,9 @@ fun MainActivity.mainEntryProvider(
|
|||||||
onNavigateToReadRecord = {
|
onNavigateToReadRecord = {
|
||||||
onNavigateToRoute(MainRouteReadRecord)
|
onNavigateToRoute(MainRouteReadRecord)
|
||||||
},
|
},
|
||||||
|
onNavigateToHighlightTagRule = {
|
||||||
|
onNavigateToRoute(MainRouteHighlightTagRule)
|
||||||
|
},
|
||||||
onNavigateToAbout = {
|
onNavigateToAbout = {
|
||||||
onNavigateToRoute(MainRouteAbout)
|
onNavigateToRoute(MainRouteAbout)
|
||||||
},
|
},
|
||||||
@@ -618,6 +622,12 @@ fun MainActivity.mainEntryProvider(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<MainRouteHighlightTagRule> {
|
||||||
|
HighlightTagRuleScreen(
|
||||||
|
onBackClick = { onNavigateBack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
entry<MainRouteAbout> {
|
entry<MainRouteAbout> {
|
||||||
val viewModel = koinViewModel<AboutViewModel>()
|
val viewModel = koinViewModel<AboutViewModel>()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ data class MainRouteSearchContent(
|
|||||||
val searchResultIndex: Int = 0,
|
val searchResultIndex: Int = 0,
|
||||||
) : MainRoute
|
) : MainRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data object MainRouteHighlightTagRule : MainRoute
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data object MainRouteAbout : MainRoute
|
data object MainRouteAbout : MainRoute
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ object MainNavigator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MainRouteHighlightTagRule,
|
||||||
MainRouteReadRecord -> {
|
MainRouteReadRecord -> {
|
||||||
if (currentRoute == MainRouteHome) {
|
if (currentRoute == MainRouteHome) {
|
||||||
backStack.add(route)
|
backStack.add(route)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ fun MainScreen(
|
|||||||
onNavigateToRssFavorites: () -> Unit,
|
onNavigateToRssFavorites: () -> Unit,
|
||||||
onNavigateToRuleSub: () -> Unit,
|
onNavigateToRuleSub: () -> Unit,
|
||||||
onNavigateToReadRecord: () -> Unit,
|
onNavigateToReadRecord: () -> Unit,
|
||||||
|
onNavigateToHighlightTagRule: () -> Unit,
|
||||||
onNavigateToAbout: () -> Unit,
|
onNavigateToAbout: () -> Unit,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
@@ -156,6 +157,7 @@ fun MainScreen(
|
|||||||
|
|
||||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||||
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
||||||
|
MainEffect.NavigateToHighlightTagRule -> onNavigateToHighlightTagRule()
|
||||||
MainEffect.NavigateToAbout -> onNavigateToAbout()
|
MainEffect.NavigateToAbout -> onNavigateToAbout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ class MainViewModel(
|
|||||||
|
|
||||||
PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
|
PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
|
||||||
|
|
||||||
|
PrefClickEvent.OpenHighlightTagRule -> _effects.tryEmit(MainEffect.NavigateToHighlightTagRule)
|
||||||
|
|
||||||
PrefClickEvent.OpenAbout -> _effects.tryEmit(MainEffect.NavigateToAbout)
|
PrefClickEvent.OpenAbout -> _effects.tryEmit(MainEffect.NavigateToAbout)
|
||||||
|
|
||||||
else -> Unit
|
else -> Unit
|
||||||
@@ -118,6 +120,7 @@ sealed interface MainEffect {
|
|||||||
|
|
||||||
data object ExitApp : MainEffect
|
data object ExitApp : MainEffect
|
||||||
data object NavigateToReadRecord : MainEffect
|
data object NavigateToReadRecord : MainEffect
|
||||||
|
data object NavigateToHighlightTagRule : MainEffect
|
||||||
data object NavigateToAbout : MainEffect
|
data object NavigateToAbout : MainEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.Folder
|
|||||||
import androidx.compose.material.icons.filled.History
|
import androidx.compose.material.icons.filled.History
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.OpenInBrowser
|
import androidx.compose.material.icons.filled.OpenInBrowser
|
||||||
|
import androidx.compose.material.icons.filled.Sell
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Source
|
import androidx.compose.material.icons.filled.Source
|
||||||
import androidx.compose.material.icons.filled.Web
|
import androidx.compose.material.icons.filled.Web
|
||||||
@@ -71,7 +72,6 @@ fun MyScreen(
|
|||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
contentWindowInsets = WindowInsets.systemBars
|
contentWindowInsets = WindowInsets.systemBars
|
||||||
@@ -160,6 +160,11 @@ fun MyScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.highlight_tag_config),
|
||||||
|
imageVector = Icons.Default.Sell,
|
||||||
|
onClick = { onNavigate(PrefClickEvent.OpenHighlightTagRule) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup(
|
SplicedColumnGroup(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ sealed class PrefClickEvent {
|
|||||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||||
object OpenReadRecord : PrefClickEvent()
|
object OpenReadRecord : PrefClickEvent()
|
||||||
object OpenBookCacheManage : PrefClickEvent()
|
object OpenBookCacheManage : PrefClickEvent()
|
||||||
|
object OpenHighlightTagRule : PrefClickEvent()
|
||||||
object OpenAbout : PrefClickEvent()
|
object OpenAbout : PrefClickEvent()
|
||||||
object ToggleWebService : PrefClickEvent()
|
object ToggleWebService : PrefClickEvent()
|
||||||
object ExitApp : PrefClickEvent()
|
object ExitApp : PrefClickEvent()
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package io.legado.app.ui.widget.components.card
|
||||||
|
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.VerticalDivider
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||||
|
import io.legado.app.ui.book.info.HighlightedTag
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HighlightTagRow(
|
||||||
|
tags: List<HighlightedTag>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (tags.isEmpty()) return
|
||||||
|
|
||||||
|
if (tags.size == 1) {
|
||||||
|
HighlightTagItem(tag = tags[0])
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(IntrinsicSize.Max),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
tags.forEachIndexed { index, tag ->
|
||||||
|
if (index > 0) {
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 12.dp)
|
||||||
|
.fillMaxHeight(0.5f),
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = LegadoTheme.colorScheme.outlineVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HighlightTagItem(
|
||||||
|
tag = tag,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun HighlightTagItem(
|
||||||
|
tag: HighlightedTag,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.padding(vertical = 8.dp),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
) {
|
||||||
|
tag.title?.let {
|
||||||
|
AnimatedTextLine(
|
||||||
|
text = it,
|
||||||
|
style = LegadoTheme.typography.labelSmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val label = tag.matchedLabels.joinToString(" · ")
|
||||||
|
AnimatedTextLine(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.basicMarquee(),
|
||||||
|
style = LegadoTheme.typography.titleMediumEmphasized,
|
||||||
|
color = LegadoTheme.colorScheme.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user