[优化] 为替换净化界面实现了原View界面的导出导入等功能
This commit is contained in:
2 files changed
+317
-15
No files matched your search
@@ -1,14 +1,20 @@
|
|||||||
package io.legado.app.ui.replace
|
package io.legado.app.ui.replace
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -27,6 +33,7 @@ import androidx.compose.material.icons.filled.*
|
|||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -34,36 +41,54 @@ import androidx.compose.ui.hapticfeedback.HapticFeedback
|
|||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.ui.replace.edit.ReplaceEditActivity
|
import io.legado.app.ui.replace.edit.ReplaceEditActivity
|
||||||
import io.legado.app.ui.widget.components.ActionItem
|
import io.legado.app.ui.widget.components.ActionItem
|
||||||
import io.legado.app.ui.widget.components.AnimatedText
|
import io.legado.app.ui.widget.components.AnimatedText
|
||||||
import io.legado.app.ui.widget.components.FastScrollLazyColumn
|
import io.legado.app.ui.widget.components.FastScrollLazyColumn
|
||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
|
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheetMode
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||||
|
ExperimentalMaterial3ExpressiveApi::class
|
||||||
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun ReplaceRuleScreen(
|
fun ReplaceRuleScreen(
|
||||||
viewModel: ReplaceRuleViewModel = koinViewModel(),
|
viewModel: ReplaceRuleViewModel = koinViewModel(),
|
||||||
onBackClick: () -> Unit
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val uploadRepository: UploadRepository = koinInject()
|
||||||
|
|
||||||
//TODO: 期望换为Navigation
|
//TODO: 期望换为Navigation
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
val haptic = LocalHapticFeedback.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val rules = uiState.rules
|
val rules = uiState.rules
|
||||||
@@ -85,13 +110,21 @@ fun ReplaceRuleScreen(
|
|||||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||||
val tabItems = listOf(stringResource(R.string.all)) + groups
|
val tabItems = listOf(stringResource(R.string.all)) + groups
|
||||||
|
|
||||||
|
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||||
|
var showUrlInput by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showFilePickerSheet by remember { mutableStateOf(false) }
|
||||||
|
var filePickerMode by remember { mutableStateOf(FilePickerSheetMode.EXPORT) }
|
||||||
|
var isUploading by remember { mutableStateOf(false) }
|
||||||
|
val clipboardManager = LocalClipboard.current
|
||||||
|
|
||||||
val importDoc = rememberLauncherForActivityResult(
|
val importDoc = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument(),
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
onResult = { uri ->
|
onResult = { uri ->
|
||||||
uri?.let {
|
uri?.let {
|
||||||
context.contentResolver.openInputStream(it)?.use { stream ->
|
context.contentResolver.openInputStream(it)?.use { stream ->
|
||||||
val text = stream.reader().readText()
|
val text = stream.reader().readText()
|
||||||
// show import dialog
|
viewModel.importSource(text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +133,7 @@ fun ReplaceRuleScreen(
|
|||||||
val exportDoc = rememberLauncherForActivityResult(
|
val exportDoc = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.CreateDocument("application/json"),
|
contract = ActivityResultContracts.CreateDocument("application/json"),
|
||||||
onResult = { uri ->
|
onResult = { uri ->
|
||||||
uri?.let {
|
uri?.let { it ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val rulesToExport = rules
|
val rulesToExport = rules
|
||||||
.filter { selectedRuleIds.contains(it.id) }
|
.filter { selectedRuleIds.contains(it.id) }
|
||||||
@@ -115,6 +148,104 @@ fun ReplaceRuleScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showUrlInput) {
|
||||||
|
SourceInputDialog(
|
||||||
|
title = stringResource(R.string.import_on_line),
|
||||||
|
onDismissRequest = { showUrlInput = false },
|
||||||
|
onConfirm = {
|
||||||
|
showUrlInput = false
|
||||||
|
viewModel.importSource(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showFilePickerSheet) {
|
||||||
|
FilePickerSheet(
|
||||||
|
sheetState = sheetState,
|
||||||
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
|
mode = filePickerMode,
|
||||||
|
onSelectSysDir = {
|
||||||
|
showFilePickerSheet = false
|
||||||
|
exportDoc.launch("exportReplaceRule.json")
|
||||||
|
},
|
||||||
|
onSelectSysFile = {},
|
||||||
|
onUpload = {
|
||||||
|
showFilePickerSheet = false
|
||||||
|
scope.launch {
|
||||||
|
val rulesToExport = rules
|
||||||
|
.filter { selectedRuleIds.contains(it.id) }
|
||||||
|
.map { it.rule }
|
||||||
|
val json = Gson().toJson(rulesToExport)
|
||||||
|
|
||||||
|
// [新增] 设置加载状态
|
||||||
|
isUploading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 使用 runCatching 处理上传
|
||||||
|
runCatching {
|
||||||
|
uploadRepository.upload(
|
||||||
|
fileName = "exportReplaceRule.json",
|
||||||
|
file = json,
|
||||||
|
contentType = "application/json"
|
||||||
|
)
|
||||||
|
}.onSuccess { url ->
|
||||||
|
isUploading = false
|
||||||
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
message = "上传成功: $url",
|
||||||
|
actionLabel = "复制链接",
|
||||||
|
withDismissAction = true
|
||||||
|
)
|
||||||
|
if (result == SnackbarResult.ActionPerformed) {
|
||||||
|
clipboardManager.setClipEntry(
|
||||||
|
ClipEntry(ClipData.newPlainText("export url", url))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.onFailure { e ->
|
||||||
|
isUploading = false
|
||||||
|
snackbarHostState.showSnackbar("上传失败: ${e.localizedMessage}")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isUploading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allowExtensions = arrayOf("json")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
(importState as? BaseImportUiState.Success<ReplaceRule>)?.let { state ->
|
||||||
|
BatchImportDialog(
|
||||||
|
title = stringResource(R.string.import_replace_rule),
|
||||||
|
importState = state,
|
||||||
|
onDismissRequest = { viewModel.cancelImport() },
|
||||||
|
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||||
|
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||||
|
onConfirm = { viewModel.saveImportedRules() },
|
||||||
|
topBarActions = {},
|
||||||
|
itemContent = { rule, _ ->
|
||||||
|
Column {
|
||||||
|
Text(rule.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
if (!rule.group.isNullOrBlank()) {
|
||||||
|
Text(rule.group!!, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importState is BaseImportUiState.Loading) {
|
||||||
|
Dialog(onDismissRequest = { viewModel.cancelImport() }) { LoadingIndicator() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(importState) {
|
||||||
|
(importState as? BaseImportUiState.Error)?.let {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(it.msg)
|
||||||
|
}
|
||||||
|
viewModel.cancelImport()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (showGroupManageSheet) {
|
if (showGroupManageSheet) {
|
||||||
GroupManageBottomSheet(
|
GroupManageBottomSheet(
|
||||||
groups = groups,
|
groups = groups,
|
||||||
@@ -181,20 +312,20 @@ fun ReplaceRuleScreen(
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
Column {
|
Column {
|
||||||
|
val titleText = remember(isUploading, inSelectionMode, selectedRuleIds.size, rules.size) {
|
||||||
|
when {
|
||||||
|
isUploading -> "正在上传..."
|
||||||
|
inSelectionMode -> "已选择 ${selectedRuleIds.size}/${rules.size}"
|
||||||
|
else -> "替换规则"
|
||||||
|
}
|
||||||
|
}
|
||||||
MediumTopAppBar(
|
MediumTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
AnimatedText(
|
AnimatedText(
|
||||||
text = if (inSelectionMode) {
|
text = titleText
|
||||||
stringResource(
|
|
||||||
R.string.select_count,
|
|
||||||
selectedRuleIds.size,
|
|
||||||
rules.size
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.replace_rule)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
@@ -227,7 +358,10 @@ fun ReplaceRuleScreen(
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("在线导入") },
|
text = { Text("在线导入") },
|
||||||
onClick = { /*TODO*/ showMenu = false }
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
showUrlInput = true // 触发输入框
|
||||||
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("本地导入") },
|
text = { Text("本地导入") },
|
||||||
@@ -431,7 +565,7 @@ fun ReplaceRuleScreen(
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
text = stringResource(R.string.export),
|
text = stringResource(R.string.export),
|
||||||
onClick = { exportDoc.launch("exportReplaceRule.json") }
|
onClick = { showFilePickerSheet = true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -618,6 +752,19 @@ fun ReplaceRuleItem(
|
|||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
var showRuleMenu by remember { mutableStateOf(false) }
|
var showRuleMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val containerColor by animateColorAsState(
|
||||||
|
targetValue = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "CardColor"
|
||||||
|
)
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -628,7 +775,7 @@ fun ReplaceRuleItem(
|
|||||||
),
|
),
|
||||||
shape = MaterialTheme.shapes.medium,
|
shape = MaterialTheme.shapes.medium,
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerLow
|
containerColor = containerColor
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
|
|||||||
@@ -2,26 +2,48 @@ package io.legado.app.ui.replace
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
import io.legado.app.data.repository.ReplaceRuleRepository
|
import io.legado.app.data.repository.ReplaceRuleRepository
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.help.ReplaceAnalyzer
|
||||||
|
import io.legado.app.help.http.decompressed
|
||||||
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
|
import io.legado.app.help.http.okHttpClient
|
||||||
|
import io.legado.app.help.http.text
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.ImportItemWrapper
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.ImportStatus
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
|
import io.legado.app.utils.isAbsUrl
|
||||||
|
import io.legado.app.utils.isJsonArray
|
||||||
|
import io.legado.app.utils.isJsonObject
|
||||||
|
import io.legado.app.utils.isUri
|
||||||
import io.legado.app.utils.putPrefString
|
import io.legado.app.utils.putPrefString
|
||||||
|
import io.legado.app.utils.readText
|
||||||
|
import io.legado.app.utils.splitNotBlank
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -53,6 +75,8 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
|
|||||||
private val _uiRules = MutableStateFlow<List<ReplaceRuleItemUi>>(emptyList())
|
private val _uiRules = MutableStateFlow<List<ReplaceRuleItemUi>>(emptyList())
|
||||||
private val _selectedRuleIds = MutableStateFlow<Set<Long>>(emptySet())
|
private val _selectedRuleIds = MutableStateFlow<Set<Long>>(emptySet())
|
||||||
val selectedRuleIds: StateFlow<Set<Long>> = _selectedRuleIds
|
val selectedRuleIds: StateFlow<Set<Long>> = _selectedRuleIds
|
||||||
|
private val _importState = MutableStateFlow<BaseImportUiState<ReplaceRule>>(BaseImportUiState.Idle)
|
||||||
|
val importState: StateFlow<BaseImportUiState<ReplaceRule>> = _importState.asStateFlow()
|
||||||
|
|
||||||
fun toggleSelection(id: Long) {
|
fun toggleSelection(id: Long) {
|
||||||
_selectedRuleIds.update {
|
_selectedRuleIds.update {
|
||||||
@@ -265,4 +289,135 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importSource(text: String) {
|
||||||
|
_importState.value = BaseImportUiState.Loading
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
val jsonText = resolveSource(text.trim())
|
||||||
|
val rules = parseRules(jsonText)
|
||||||
|
val wrappers = rules.map { newRule ->
|
||||||
|
val oldRule = appDb.replaceRuleDao.findById(newRule.id)
|
||||||
|
|
||||||
|
val status = when {
|
||||||
|
oldRule == null -> ImportStatus.New
|
||||||
|
hasChanged(newRule, oldRule) -> ImportStatus.Update
|
||||||
|
else -> ImportStatus.Existing
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportItemWrapper(
|
||||||
|
data = newRule,
|
||||||
|
oldData = oldRule,
|
||||||
|
status = status,
|
||||||
|
isSelected = status != ImportStatus.Existing
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_importState.value = BaseImportUiState.Success(
|
||||||
|
source = text,
|
||||||
|
items = wrappers
|
||||||
|
)
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
_importState.value = BaseImportUiState.Error(it.localizedMessage ?: "Unknown Error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveSource(text: String): String {
|
||||||
|
return when {
|
||||||
|
text.isAbsUrl() -> {
|
||||||
|
okHttpClient.newCallResponseBody {
|
||||||
|
if (text.endsWith("#requestWithoutUA")) {
|
||||||
|
url(text.substringBeforeLast("#requestWithoutUA"))
|
||||||
|
header(AppConst.UA_NAME, "null")
|
||||||
|
} else {
|
||||||
|
url(text)
|
||||||
|
}
|
||||||
|
}.decompressed().text("utf-8")
|
||||||
|
}
|
||||||
|
text.isUri() -> text.toUri().readText(appCtx)
|
||||||
|
else -> text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseRules(text: String): List<ReplaceRule> {
|
||||||
|
return when {
|
||||||
|
text.isJsonArray() -> ReplaceAnalyzer.jsonToReplaceRules(text).getOrThrow()
|
||||||
|
text.isJsonObject() -> listOf(ReplaceAnalyzer.jsonToReplaceRule(text).getOrThrow())
|
||||||
|
else -> throw Exception("格式不正确")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasChanged(newRule: ReplaceRule, oldRule: ReplaceRule): Boolean {
|
||||||
|
return newRule.pattern != oldRule.pattern
|
||||||
|
|| newRule.replacement != oldRule.replacement
|
||||||
|
|| newRule.isRegex != oldRule.isRegex
|
||||||
|
|| newRule.scope != oldRule.scope
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelImport() {
|
||||||
|
_importState.value = BaseImportUiState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleImportSelection(index: Int) {
|
||||||
|
val currentState = _importState.value as? BaseImportUiState.Success ?: return
|
||||||
|
val newItems = currentState.items.toMutableList()
|
||||||
|
val item = newItems[index]
|
||||||
|
newItems[index] = item.copy(isSelected = !item.isSelected)
|
||||||
|
_importState.value = currentState.copy(items = newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleImportAll(isSelected: Boolean) {
|
||||||
|
val currentState = _importState.value as? BaseImportUiState.Success ?: return
|
||||||
|
val newItems = currentState.items.map { it.copy(isSelected = isSelected) }
|
||||||
|
_importState.value = currentState.copy(items = newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新分组配置
|
||||||
|
fun updateImportConfig(keepName: Boolean? = null, group: String? = null, isAdd: Boolean? = null) {
|
||||||
|
val currentState = _importState.value as? BaseImportUiState.Success ?: return
|
||||||
|
_importState.value = currentState.copy(
|
||||||
|
keepOriginalName = keepName ?: currentState.keepOriginalName,
|
||||||
|
customGroup = group ?: currentState.customGroup,
|
||||||
|
isAddGroup = isAdd ?: currentState.isAddGroup
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveImportedRules() {
|
||||||
|
val state = _importState.value as? BaseImportUiState.Success ?: return
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val rulesToSave = state.items
|
||||||
|
.filter { it.isSelected }
|
||||||
|
.map { wrapper ->
|
||||||
|
val rule = wrapper.data
|
||||||
|
val oldRule = wrapper.oldData
|
||||||
|
|
||||||
|
if (state.keepOriginalName && oldRule != null) {
|
||||||
|
rule.name = oldRule.name
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetGroup = state.customGroup?.trim()
|
||||||
|
if (!targetGroup.isNullOrEmpty()) {
|
||||||
|
if (state.isAddGroup) {
|
||||||
|
val groups = linkedSetOf<String>()
|
||||||
|
rule.group?.splitNotBlank(AppPattern.splitGroupRegex)?.let { groups.addAll(it) }
|
||||||
|
groups.add(targetGroup)
|
||||||
|
rule.group = groups.joinToString(",")
|
||||||
|
} else {
|
||||||
|
rule.group = targetGroup
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rule
|
||||||
|
}
|
||||||
|
|
||||||
|
appDb.replaceRuleDao.insert(*rulesToSave.toTypedArray())
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
_importState.value = BaseImportUiState.Idle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user