[新增] 字典的导入功能
This commit is contained in:
@@ -34,6 +34,15 @@ class DictRuleRepository {
|
|||||||
dao.update(*rule)
|
dao.update(*rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findById(id: String): DictRule? = withContext(Dispatchers.IO) {
|
||||||
|
dao.getByName(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getByNames(names: Collection<String>): List<DictRule> =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (names.isEmpty()) emptyList() else dao.getByNames(names.toSet())
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun enableByIds(names: Set<String>) = withContext(Dispatchers.IO) {
|
suspend fun enableByIds(names: Set<String>) = withContext(Dispatchers.IO) {
|
||||||
if (names.isEmpty()) return@withContext
|
if (names.isEmpty()) return@withContext
|
||||||
val rules = dao.getByNames(names)
|
val rules = dao.getByNames(names)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package io.legado.app.ui.dict.rule
|
package io.legado.app.ui.dict.rule
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
@@ -21,9 +24,12 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
@@ -34,6 +40,8 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TooltipAnchorPosition
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
@@ -41,6 +49,7 @@ import androidx.compose.material3.TooltipBox
|
|||||||
import androidx.compose.material3.TooltipDefaults
|
import androidx.compose.material3.TooltipDefaults
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.animateFloatingActionButton
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.material3.rememberTooltipState
|
import androidx.compose.material3.rememberTooltipState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -48,19 +57,25 @@ import androidx.compose.runtime.collectAsState
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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
|
||||||
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.platform.ClipEntry
|
||||||
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.google.gson.Gson
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
|
import io.legado.app.data.repository.UploadRepository
|
||||||
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.DraggableSelectionHandler
|
import io.legado.app.ui.widget.components.DraggableSelectionHandler
|
||||||
@@ -68,19 +83,34 @@ import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
|||||||
import io.legado.app.ui.widget.components.ReorderableSelectionItem
|
import io.legado.app.ui.widget.components.ReorderableSelectionItem
|
||||||
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 io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun DictRuleScreen(
|
fun DictRuleScreen(
|
||||||
viewModel: DictRuleViewModel = viewModel(),
|
viewModel: DictRuleViewModel = koinViewModel(),
|
||||||
onBackClick: () -> Unit
|
onBackClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
val uploadRepository: UploadRepository = koinInject()
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
val uiState by viewModel.uiState.collectAsState()
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val rules = uiState.items
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
var isSearch by remember { mutableStateOf(false) }
|
var isSearch by remember { mutableStateOf(false) }
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
val selectedIds by viewModel.selectedIds.collectAsState()
|
val selectedIds by viewModel.selectedIds.collectAsState()
|
||||||
val inSelectionMode = selectedIds.isNotEmpty()
|
val inSelectionMode = selectedIds.isNotEmpty()
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
@@ -89,11 +119,129 @@ fun DictRuleScreen(
|
|||||||
var showEditSheet by remember { mutableStateOf(false) }
|
var showEditSheet by remember { mutableStateOf(false) }
|
||||||
var editingRule by remember { mutableStateOf<DictRule?>(null) }
|
var editingRule by remember { mutableStateOf<DictRule?>(null) }
|
||||||
var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) }
|
var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) }
|
||||||
|
|
||||||
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
viewModel.moveItemInList(from.index, to.index)
|
viewModel.moveItemInList(from.index, to.index)
|
||||||
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val clipboardManager = LocalClipboard.current
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
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 importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||||
|
val sheetState = rememberModalBottomSheetState()
|
||||||
|
|
||||||
|
val importDoc = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument(),
|
||||||
|
onResult = { uri ->
|
||||||
|
uri?.let {
|
||||||
|
context.contentResolver.openInputStream(it)?.use { stream ->
|
||||||
|
val text = stream.reader().readText()
|
||||||
|
viewModel.importSource(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val exportDoc = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.CreateDocument("application/json"),
|
||||||
|
onResult = { uri ->
|
||||||
|
uri?.let { it ->
|
||||||
|
scope.launch {
|
||||||
|
val rulesToExport = rules
|
||||||
|
.filter { selectedIds.contains(it.name) }
|
||||||
|
.map { it.rule }
|
||||||
|
|
||||||
|
val json = Gson().toJson(rulesToExport)
|
||||||
|
context.contentResolver.openOutputStream(it)?.use { stream ->
|
||||||
|
stream.writer().write(json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
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("exportDictRule.json")
|
||||||
|
},
|
||||||
|
onSelectSysFile = {},
|
||||||
|
onUpload = {
|
||||||
|
showFilePickerSheet = false
|
||||||
|
scope.launch {
|
||||||
|
val selectedRules = viewModel.getSelectedRules()
|
||||||
|
val json = GSON.toJson(selectedRules)
|
||||||
|
|
||||||
|
isUploading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
runCatching {
|
||||||
|
uploadRepository.upload(
|
||||||
|
fileName = "exportDictRule.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<DictRule>)?.let { state ->
|
||||||
|
BatchImportDialog(
|
||||||
|
title = "导入词典规则",
|
||||||
|
importState = state,
|
||||||
|
onDismissRequest = { viewModel.cancelImport() },
|
||||||
|
onToggleItem = { viewModel.toggleImportSelection(it) },
|
||||||
|
onToggleAll = { viewModel.toggleImportAll(it) },
|
||||||
|
onConfirm = { viewModel.saveImportedRules() },
|
||||||
|
itemContent = { rule, _ ->
|
||||||
|
Column {
|
||||||
|
Text(rule.name, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Text(rule.urlRule, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
if (!reorderableState.isAnyItemDragging) {
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
viewModel.saveSortOrder()
|
viewModel.saveSortOrder()
|
||||||
@@ -178,9 +326,10 @@ fun DictRuleScreen(
|
|||||||
Column {
|
Column {
|
||||||
GlassMediumFlexibleTopAppBar(
|
GlassMediumFlexibleTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
val titleText = remember(inSelectionMode, selectedIds, uiState.items) {
|
val titleText = remember(isUploading, inSelectionMode, selectedIds, rules) {
|
||||||
when {
|
when {
|
||||||
inSelectionMode -> "已选择 ${selectedIds.size}/${uiState.items.size}"
|
isUploading -> "正在上传..."
|
||||||
|
inSelectionMode -> "已选择 ${selectedIds.size}/${rules.size}"
|
||||||
else -> "字典规则"
|
else -> "字典规则"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,6 +358,32 @@ fun DictRuleScreen(
|
|||||||
IconButton(onClick = { isSearch = !isSearch }) {
|
IconButton(onClick = { isSearch = !isSearch }) {
|
||||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = { showMenu = !showMenu }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("在线导入") },
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
showUrlInput = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("本地导入") },
|
||||||
|
onClick = {
|
||||||
|
importDoc.launch(
|
||||||
|
arrayOf(
|
||||||
|
"text/plain",
|
||||||
|
"application/json"
|
||||||
|
)
|
||||||
|
); showMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -260,7 +435,7 @@ fun DictRuleScreen(
|
|||||||
),
|
),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(uiState.items, key = { it.name }) { item ->
|
items(rules, key = { it.name }) { item ->
|
||||||
ReorderableSelectionItem(
|
ReorderableSelectionItem(
|
||||||
state = reorderableState,
|
state = reorderableState,
|
||||||
key = item.name,
|
key = item.name,
|
||||||
@@ -294,7 +469,7 @@ fun DictRuleScreen(
|
|||||||
if (inSelectionMode) {
|
if (inSelectionMode) {
|
||||||
DraggableSelectionHandler(
|
DraggableSelectionHandler(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
items = uiState.items,
|
items = rules,
|
||||||
selectedIds = selectedIds,
|
selectedIds = selectedIds,
|
||||||
onSelectionChange = viewModel::setSelection,
|
onSelectionChange = viewModel::setSelection,
|
||||||
idProvider = { it.name },
|
idProvider = { it.name },
|
||||||
@@ -316,10 +491,10 @@ fun DictRuleScreen(
|
|||||||
) {
|
) {
|
||||||
SelectionBottomBar(
|
SelectionBottomBar(
|
||||||
onSelectAll = {
|
onSelectAll = {
|
||||||
viewModel.setSelection(uiState.items.map { it.name }.toSet())
|
viewModel.setSelection(rules.map { it.name }.toSet())
|
||||||
},
|
},
|
||||||
onSelectInvert = {
|
onSelectInvert = {
|
||||||
val allIds = uiState.items.map { it.name }.toSet()
|
val allIds = rules.map { it.name }.toSet()
|
||||||
viewModel.setSelection(allIds - selectedIds)
|
viewModel.setSelection(allIds - selectedIds)
|
||||||
},
|
},
|
||||||
primaryAction = ActionItem(
|
primaryAction = ActionItem(
|
||||||
@@ -341,6 +516,10 @@ fun DictRuleScreen(
|
|||||||
viewModel.disableSelectionByIds(selectedIds)
|
viewModel.disableSelectionByIds(selectedIds)
|
||||||
viewModel.setSelection(emptySet())
|
viewModel.setSelection(emptySet())
|
||||||
}
|
}
|
||||||
|
),
|
||||||
|
ActionItem(
|
||||||
|
text = stringResource(R.string.export),
|
||||||
|
onClick = { showFilePickerSheet = true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,14 +2,28 @@ package io.legado.app.ui.dict.rule
|
|||||||
|
|
||||||
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.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
import io.legado.app.data.repository.DictRuleRepository
|
import io.legado.app.data.repository.DictRuleRepository
|
||||||
import io.legado.app.help.DefaultData
|
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.GSON
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.fromJsonArray
|
||||||
import io.legado.app.utils.fromJsonObject
|
import io.legado.app.utils.fromJsonObject
|
||||||
import io.legado.app.utils.getClipText
|
import io.legado.app.utils.getClipText
|
||||||
|
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.readText
|
||||||
import io.legado.app.utils.sendToClip
|
import io.legado.app.utils.sendToClip
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -26,6 +40,7 @@ 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.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class DictRuleItemUi(
|
data class DictRuleItemUi(
|
||||||
@@ -51,8 +66,9 @@ class DictRuleViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
private val _uiRules = MutableStateFlow<List<DictRuleItemUi>>(emptyList())
|
private val _uiRules = MutableStateFlow<List<DictRuleItemUi>>(emptyList())
|
||||||
|
|
||||||
private val _selectedIds = MutableStateFlow<Set<String>>(emptySet())
|
private val _selectedIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
val selectedIds: StateFlow<Set<String>> = _selectedIds.asStateFlow()
|
val selectedIds: StateFlow<Set<String>> = _selectedIds
|
||||||
|
private val _importState = MutableStateFlow<BaseImportUiState<DictRule>>(BaseImportUiState.Idle)
|
||||||
|
val importState = _importState.asStateFlow()
|
||||||
fun toggleSelection(id: String) {
|
fun toggleSelection(id: String) {
|
||||||
_selectedIds.update {
|
_selectedIds.update {
|
||||||
if (it.contains(id)) it - id else it + id
|
if (it.contains(id)) it - id else it + id
|
||||||
@@ -172,22 +188,110 @@ class DictRuleViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upSortNumber() {
|
fun importSource(text: String) {
|
||||||
execute {
|
_importState.value = BaseImportUiState.Loading
|
||||||
val rules = repository.getAll()
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
for ((index, rule) in rules.withIndex()) {
|
runCatching {
|
||||||
rule.sortNumber = index + 1
|
val jsonText = resolveSource(text.trim())
|
||||||
|
val rules = parseDictRules(jsonText)
|
||||||
|
val wrappers = rules.map { newRule ->
|
||||||
|
val oldRule = repository.findById(newRule.name)
|
||||||
|
|
||||||
|
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")
|
||||||
}
|
}
|
||||||
repository.insert(*rules.toTypedArray())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importDefault() {
|
private suspend fun resolveSource(text: String): String {
|
||||||
execute {
|
return when {
|
||||||
DefaultData.importDefaultDictRules()
|
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(context)
|
||||||
|
else -> text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun parseDictRules(text: String): List<DictRule> {
|
||||||
|
return when {
|
||||||
|
text.isJsonArray() -> GSON.fromJsonArray<DictRule>(text).getOrThrow()
|
||||||
|
text.isJsonObject() -> listOf(GSON.fromJsonObject<DictRule>(text).getOrThrow())
|
||||||
|
else -> throw Exception("格式不正确")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasChanged(newRule: DictRule, oldRule: DictRule): Boolean {
|
||||||
|
return newRule.name != oldRule.name
|
||||||
|
|| newRule.urlRule != oldRule.urlRule
|
||||||
|
|| newRule.showRule != oldRule.showRule
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleImportSelection(index: Int) {
|
||||||
|
val currentState = _importState.value as? BaseImportUiState.Success<DictRule> ?: 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<DictRule> ?: return
|
||||||
|
val newItems = currentState.items.map { it.copy(isSelected = isSelected) }
|
||||||
|
_importState.value = currentState.copy(items = newItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveImportedRules() {
|
||||||
|
val state = _importState.value as? BaseImportUiState.Success<DictRule> ?: 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 cancelImport() {
|
||||||
|
_importState.value = BaseImportUiState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSelectedRules(): List<DictRule> {
|
||||||
|
val selectedIds = _selectedIds.value
|
||||||
|
return repository.getByNames(selectedIds)
|
||||||
|
}
|
||||||
|
|
||||||
fun copyRule(dictRule: DictRule) {
|
fun copyRule(dictRule: DictRule) {
|
||||||
context.sendToClip(GSON.toJson(dictRule))
|
context.sendToClip(GSON.toJson(dictRule))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user