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.themeManage.ThemeManageScreen
|
||||
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.RssSortRouteScreen
|
||||
import io.legado.app.ui.rss.favorites.RssFavoritesScreen
|
||||
@@ -158,6 +159,9 @@ fun MainActivity.mainEntryProvider(
|
||||
onNavigateToReadRecord = {
|
||||
onNavigateToRoute(MainRouteReadRecord)
|
||||
},
|
||||
onNavigateToHighlightTagRule = {
|
||||
onNavigateToRoute(MainRouteHighlightTagRule)
|
||||
},
|
||||
onNavigateToAbout = {
|
||||
onNavigateToRoute(MainRouteAbout)
|
||||
},
|
||||
@@ -618,6 +622,12 @@ fun MainActivity.mainEntryProvider(
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteHighlightTagRule> {
|
||||
HighlightTagRuleScreen(
|
||||
onBackClick = { onNavigateBack() }
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteAbout> {
|
||||
val viewModel = koinViewModel<AboutViewModel>()
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -104,6 +104,9 @@ data class MainRouteSearchContent(
|
||||
val searchResultIndex: Int = 0,
|
||||
) : MainRoute
|
||||
|
||||
@Serializable
|
||||
data object MainRouteHighlightTagRule : MainRoute
|
||||
|
||||
@Serializable
|
||||
data object MainRouteAbout : MainRoute
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ object MainNavigator {
|
||||
}
|
||||
}
|
||||
|
||||
MainRouteHighlightTagRule,
|
||||
MainRouteReadRecord -> {
|
||||
if (currentRoute == MainRouteHome) {
|
||||
backStack.add(route)
|
||||
|
||||
@@ -117,6 +117,7 @@ fun MainScreen(
|
||||
onNavigateToRssFavorites: () -> Unit,
|
||||
onNavigateToRuleSub: () -> Unit,
|
||||
onNavigateToReadRecord: () -> Unit,
|
||||
onNavigateToHighlightTagRule: () -> Unit,
|
||||
onNavigateToAbout: () -> Unit,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
@@ -156,6 +157,7 @@ fun MainScreen(
|
||||
|
||||
MainEffect.ExitApp -> (context as? ComponentActivity)?.finish()
|
||||
MainEffect.NavigateToReadRecord -> onNavigateToReadRecord()
|
||||
MainEffect.NavigateToHighlightTagRule -> onNavigateToHighlightTagRule()
|
||||
MainEffect.NavigateToAbout -> onNavigateToAbout()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,8 @@ class MainViewModel(
|
||||
|
||||
PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
|
||||
|
||||
PrefClickEvent.OpenHighlightTagRule -> _effects.tryEmit(MainEffect.NavigateToHighlightTagRule)
|
||||
|
||||
PrefClickEvent.OpenAbout -> _effects.tryEmit(MainEffect.NavigateToAbout)
|
||||
|
||||
else -> Unit
|
||||
@@ -118,6 +120,7 @@ sealed interface MainEffect {
|
||||
|
||||
data object ExitApp : MainEffect
|
||||
data object NavigateToReadRecord : MainEffect
|
||||
data object NavigateToHighlightTagRule : 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.Info
|
||||
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.Source
|
||||
import androidx.compose.material.icons.filled.Web
|
||||
@@ -71,7 +72,6 @@ fun MyScreen(
|
||||
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
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(
|
||||
|
||||
@@ -26,6 +26,7 @@ sealed class PrefClickEvent {
|
||||
data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
|
||||
object OpenReadRecord : PrefClickEvent()
|
||||
object OpenBookCacheManage : PrefClickEvent()
|
||||
object OpenHighlightTagRule : PrefClickEvent()
|
||||
object OpenAbout : PrefClickEvent()
|
||||
object ToggleWebService : 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