[新增] 由Compose重写的替换净化编辑界面,没啥变化,可能有BUG

This commit is contained in:
HapeLee
2025-12-09 01:13:04 +08:00
parent b7323b54ed
commit dcff01c8d8
11 changed files with 666 additions and 280 deletions
+2 -4
View File
@@ -33,9 +33,7 @@ import io.legado.app.data.entities.rule.ContentRule
import io.legado.app.data.entities.rule.ExploreRule import io.legado.app.data.entities.rule.ExploreRule
import io.legado.app.data.entities.rule.SearchRule import io.legado.app.data.entities.rule.SearchRule
import io.legado.app.di.appDatabaseModule import io.legado.app.di.appDatabaseModule
import io.legado.app.di.bookmarkModule import io.legado.app.di.appModule
import io.legado.app.di.exploreModule
import io.legado.app.di.readRecordModule
import io.legado.app.help.AppFreezeMonitor import io.legado.app.help.AppFreezeMonitor
import io.legado.app.help.AppWebDav import io.legado.app.help.AppWebDav
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
@@ -80,7 +78,7 @@ class App : Application() {
override fun onCreate() { override fun onCreate() {
startKoin { startKoin {
androidContext(this@App) androidContext(this@App)
modules(appDatabaseModule, readRecordModule, exploreModule, bookmarkModule) modules(appDatabaseModule, appModule)
} }
if (getPrefString("app_theme", "0") == "12") { if (getPrefString("app_theme", "0") == "12") {
if (AppConfig.customMode == "accent") if (AppConfig.customMode == "accent")
@@ -0,0 +1,37 @@
package io.legado.app.di
import io.legado.app.data.AppDatabase
import io.legado.app.data.repository.ExploreRepository
import io.legado.app.data.repository.ExploreRepositoryImpl
import io.legado.app.data.repository.ReadRecordRepository
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.replace.edit.ReplaceEditViewModel
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val appModule = module {
viewModel { ReplaceEditViewModel(get(), get(), get()) }
// ReadRecord
single { get<AppDatabase>().readRecordDao }
single { get<AppDatabase>().bookDao }
single { get<AppDatabase>().bookChapterDao }
single { ReadRecordRepository(get()) }
viewModel { ReadRecordViewModel(get(), get(), get()) }
// Explore
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
viewModel { ExploreShowViewModel(get()) }
// Bookmark
viewModel {
AllBookmarkViewModel(
androidApplication(),
get()
)
}
}
@@ -1,16 +0,0 @@
package io.legado.app.di
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
import org.koin.android.ext.koin.androidApplication
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val bookmarkModule = module {
viewModel {
AllBookmarkViewModel(
androidApplication(),
get()
)
}
}
@@ -1,12 +0,0 @@
package io.legado.app.di
import io.legado.app.data.repository.ExploreRepository
import io.legado.app.data.repository.ExploreRepositoryImpl
import io.legado.app.ui.book.explore.ExploreShowViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val exploreModule = module {
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
viewModel { ExploreShowViewModel(get()) }
}
@@ -1,16 +0,0 @@
package io.legado.app.di
import io.legado.app.data.AppDatabase
import io.legado.app.data.repository.ReadRecordRepository
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import org.koin.core.module.dsl.viewModel
import org.koin.dsl.module
val readRecordModule = module {
single { get<AppDatabase>().readRecordDao }
single { get<AppDatabase>().bookDao }
single { get<AppDatabase>().bookChapterDao }
single { ReadRecordRepository(get()) }
viewModel { ReadRecordViewModel(get(), get(), get()) }
}
@@ -92,7 +92,6 @@ import io.legado.app.model.BookShelfState
import io.legado.app.ui.widget.components.AnimatedTextButton import io.legado.app.ui.widget.components.AnimatedTextButton
import io.legado.app.ui.widget.components.AnimatedTextLine import io.legado.app.ui.widget.components.AnimatedTextLine
import io.legado.app.ui.widget.components.Cover import io.legado.app.ui.widget.components.Cover
import io.legado.app.ui.widget.components.EmptyMessageView
import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.SearchBarSection
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -2,37 +2,15 @@ package io.legado.app.ui.replace.edit
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import androidx.activity.compose.setContent
import android.view.Menu import androidx.compose.runtime.Composable
import android.view.MenuItem import io.legado.app.base.AppTheme
import android.widget.ArrayAdapter import io.legado.app.base.BaseComposeActivity
import android.widget.EditText
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.data.entities.ReplaceRule
import io.legado.app.databinding.ActivityReplaceEditBinding
import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.ui.widget.keyboard.KeyboardToolPop
import io.legado.app.utils.GSON
import io.legado.app.utils.imeHeight
import io.legado.app.utils.sendToClip
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
import io.legado.app.utils.showHelp
import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.data.appDb
import io.legado.app.lib.dialogs.alert
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
/** /**
* 编辑替换规则 * 编辑替换规则
*/ */
class ReplaceEditActivity : class ReplaceEditActivity : BaseComposeActivity() {
VMBaseActivity<ActivityReplaceEditBinding, ReplaceEditViewModel>(),
KeyboardToolPop.CallBack {
companion object { companion object {
fun startIntent( fun startIntent(
@@ -53,161 +31,24 @@ class ReplaceEditActivity :
} }
} }
override val binding by viewBinding(ActivityReplaceEditBinding::inflate) @Composable
override val viewModel by viewModels<ReplaceEditViewModel>() override fun Content() {
private val softKeyboardTool by lazy { setContent {
KeyboardToolPop(this, lifecycleScope, binding.root, this) AppTheme {
} ReplaceEditScreen(
onBack = { finish() },
override fun onCreate(savedInstanceState: Bundle?) { onSaveSuccess = {
super.onCreate(savedInstanceState) setResult(RESULT_OK)
softKeyboardTool.attachToWindow(window) finish()
initView() },
viewModel.initData(intent, ::upReplaceView) onShowHelp = { key -> showHelp(key) }
}
override fun onDestroy() {
super.onDestroy()
softKeyboardTool.dismiss()
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.replace_edit, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_save -> viewModel.save(getReplaceRule()) {
setResult(RESULT_OK)
finish()
}
R.id.menu_copy_rule -> sendToClip(GSON.toJson(getReplaceRule()))
R.id.menu_paste_rule -> viewModel.pasteRule(::upReplaceView)
}
return true
}
private fun initView() = binding.run {
ivHelp.setOnClickListener { showHelp("regexHelp") }
ivManageGroups.setOnClickListener { showGroupManageDialog() }
root.setOnApplyWindowInsetsListenerCompat { _, insets ->
softKeyboardTool.initialPadding = insets.imeHeight
insets
}
setupGroupAutoComplete()
}
private fun setupGroupAutoComplete() = binding.run {
lifecycleScope.launch {
appDb.replaceRuleDao.flowGroups().collectLatest { groups ->
val allGroups = listOf("默认") + groups
val adapter = ArrayAdapter(
this@ReplaceEditActivity,
android.R.layout.simple_dropdown_item_1line,
allGroups
) )
etGroup.setAdapter(adapter)
}
}
val defaultGroup = viewModel.replaceRule?.group ?: "默认"
etGroup.setText(defaultGroup, false)
}
private fun showGroupManageDialog() {
lifecycleScope.launch {
val allGroups = appDb.replaceRuleDao.allGroups()
if (allGroups.isEmpty()) {
toastOnUi("暂无分组")
return@launch
}
val checked = BooleanArray(allGroups.size)
alert(title = getString(R.string.group_manage)) {
multiChoiceItems(allGroups.toTypedArray(), checked) { _, _, _ -> }
negativeButton("关闭")
neutralButton("删除所选") {
val toDelete = allGroups.filterIndexed { i, _ -> checked[i] }
if (toDelete.isEmpty()) {
toastOnUi("未选择任何分组")
return@neutralButton
}
lifecycleScope.launch {
appDb.replaceRuleDao.clearGroups(toDelete)
toastOnUi("已清空 ${toDelete.size} 个分组")
}
}
} }
} }
} }
private fun upReplaceView(rule: ReplaceRule) = binding.run { private fun showHelp(key: String) {
etName.setText(rule.name)
val group = rule.group.takeUnless { it.isNullOrBlank() } ?: "默认"
etGroup.setText(group, false)
etReplaceRule.setText(rule.pattern)
cbUseRegex.isChecked = rule.isRegex
etReplaceTo.setText(rule.replacement)
cbScopeTitle.isChecked = rule.scopeTitle
cbScopeContent.isChecked = rule.scopeContent
etScope.setText(rule.scope)
etExcludeScope.setText(rule.excludeScope)
etTimeout.setText(rule.timeoutMillisecond.toString())
} }
private fun getReplaceRule(): ReplaceRule = binding.run {
val rule = viewModel.replaceRule ?: ReplaceRule()
rule.name = etName.text.toString()
val groupText = binding.etGroup.text.toString().trim()
rule.group = when {
groupText.isBlank() -> null
groupText == "默认" -> null
else -> groupText
}
rule.pattern = etReplaceRule.text.toString()
rule.isRegex = cbUseRegex.isChecked
rule.replacement = etReplaceTo.text.toString()
rule.scopeTitle = cbScopeTitle.isChecked
rule.scopeContent = cbScopeContent.isChecked
rule.scope = etScope.text.toString()
rule.excludeScope = etExcludeScope.text.toString()
rule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong()
rule
}
override fun helpActions(): List<SelectItem<String>> =
listOf(SelectItem("正则教程", "regexHelp"))
override fun onHelpActionSelect(action: String) {
if (action == "regexHelp") showHelp("regexHelp")
}
override fun sendText(text: String) {
if (text.isBlank()) return
val view = window?.decorView?.findFocus()
if (view is EditText) {
val start = view.selectionStart
val end = view.selectionEnd
val edit = view.editableText
if (start < 0 || start >= edit.length) {
edit.append(text)
} else if (start > end) {
edit.replace(end, start, text)
} else {
edit.replace(start, end, text)
}
}
}
} }
@@ -0,0 +1,370 @@
package io.legado.app.ui.replace.edit
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.HelpOutline
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import org.koin.androidx.compose.koinViewModel
@Composable
fun keyboardAsState(): State<Boolean> {
val isImeVisible = WindowInsets.ime.getBottom(LocalDensity.current) > 0
return rememberUpdatedState(isImeVisible)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReplaceEditScreen(
onBack: () -> Unit,
onSaveSuccess: () -> Unit,
onShowHelp: (String) -> Unit,
viewModel: ReplaceEditViewModel = koinViewModel()
) {
val state by viewModel.uiState.collectAsState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showMenu by remember { mutableStateOf(false) }
val isKeyboardVisible by keyboardAsState()
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
MediumTopAppBar(
title = { Text(if (state.id > 0) "编辑替换规则" else "新增替换规则") },
navigationIcon = {
IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") }
},
actions = {
IconButton(onClick = { onShowHelp("regexHelp") }) {
Icon(Icons.AutoMirrored.Filled.HelpOutline, contentDescription = "帮助")
}
IconButton(onClick = {
viewModel.save(onSaveSuccess)
}) {
Icon(Icons.Default.Save, contentDescription = "保存")
}
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Default.MoreVert, contentDescription = "更多操作")
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
text = { Text("复制规则") },
onClick = {
showMenu = false
viewModel.copyRule()
}
)
DropdownMenuItem(
text = { Text("粘贴规则") },
onClick = {
showMenu = false
viewModel.pasteRule(onSuccess = {})
}
)
}
},
scrollBehavior = scrollBehavior
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.imePadding()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = state.name,
onValueChange = viewModel::onNameChange,
label = { Text("规则名称") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (it.isFocused) viewModel.activeField = ReplaceEditViewModel.ActiveField.Name },
singleLine = true
)
GroupSelector(
currentGroup = state.group,
allGroups = state.allGroups,
onGroupChange = viewModel::onGroupChange,
onManageClick = { viewModel.toggleGroupDialog(true) }
)
OutlinedTextField(
value = state.pattern,
onValueChange = viewModel::onPatternChange,
label = { Text("匹配规则") },
placeholder = { Text("输入正则表达式或关键字") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (it.isFocused) viewModel.activeField = ReplaceEditViewModel.ActiveField.Pattern }
)
OutlinedTextField(
value = state.replacement,
onValueChange = viewModel::onReplacementChange,
label = { Text("替换为") },
placeholder = { Text("输入替换内容或捕获组") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (it.isFocused) viewModel.activeField = ReplaceEditViewModel.ActiveField.Replacement }
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start
) {
FilterChip(
selected = state.scopeTitle,
onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
label = { Text("标题") },
leadingIcon = if (state.scopeTitle) {
{ Icon(Icons.Default.Check, contentDescription = "已选", Modifier.size(FilterChipDefaults.IconSize)) }
} else null
)
Spacer(Modifier.width(8.dp))
FilterChip(
selected = state.scopeContent,
onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
label = { Text("内容") },
leadingIcon = if (state.scopeContent) {
{ Icon(Icons.Default.Check, contentDescription = "已选", Modifier.size(FilterChipDefaults.IconSize)) }
} else null
)
Spacer(Modifier.weight(1f))
FilterChip(
selected = state.isRegex,
onClick = { viewModel.onRegexChange(!state.isRegex) },
label = { Text("使用正则") },
leadingIcon = if (state.isRegex) {
{ Icon(Icons.Default.Check, contentDescription = "正则已启用", Modifier.size(FilterChipDefaults.IconSize)) }
} else null
)
}
OutlinedTextField(
value = state.scope,
onValueChange = viewModel::onScopeChange,
label = { Text("特定范围") },
placeholder = { Text("指定规则适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (it.isFocused) viewModel.activeField = ReplaceEditViewModel.ActiveField.Scope }
)
OutlinedTextField(
value = state.excludeScope,
onValueChange = viewModel::onExcludeScopeChange,
label = { Text("排除范围") },
placeholder = { Text("指定规则不适用的范围") },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged { if (it.isFocused) viewModel.activeField = ReplaceEditViewModel.ActiveField.Exclude }
)
OutlinedTextField(
value = state.timeout,
onValueChange = viewModel::onTimeoutChange,
label = { Text("超时 (ms)") },
placeholder = { Text("3000") },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.height(16.dp))
}
}
AnimatedVisibility(
visible = isKeyboardVisible,
enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight },
),
exit = slideOutVertically(
targetOffsetY = { fullHeight -> fullHeight },
),
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding()
) {
QuickInputBar(
onInsert = { text -> viewModel.insertTextAtCursor(text) }
)
}
if (state.showGroupDialog) {
ManageGroupDialog(
groups = state.allGroups.filter { it != "默认" },
onDismiss = { viewModel.toggleGroupDialog(false) },
onDelete = { viewModel.deleteGroups(it) }
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GroupSelector(
currentGroup: String,
allGroups: List<String>,
onGroupChange: (String) -> Unit,
onManageClick: () -> Unit
) {
var expanded by remember { mutableStateOf(false) }
Row(verticalAlignment = Alignment.CenterVertically) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = { expanded = !expanded },
modifier = Modifier.weight(1f)
) {
OutlinedTextField(
value = currentGroup,
onValueChange = onGroupChange,
label = { Text("分组") },
placeholder = {Text("默认")},
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
modifier = Modifier.fillMaxWidth().menuAnchor(
ExposedDropdownMenuAnchorType.PrimaryEditable,
true
)
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
allGroups.forEach { selectionOption ->
DropdownMenuItem(
text = { Text(selectionOption) },
onClick = {
onGroupChange(selectionOption)
expanded = false
}
)
}
}
}
IconButton(onClick = onManageClick) {
Icon(Icons.Default.Settings, "Manage")
}
}
}
@Composable
fun ManageGroupDialog(
groups: List<String>,
onDismiss: () -> Unit,
onDelete: (List<String>) -> Unit
) {
val selected = remember { mutableStateMapOf<String, Boolean>() }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("分组管理") },
text = {
if (groups.isEmpty()) Text("暂无其他分组")
else Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
groups.forEach { group ->
val isSelected = selected[group] ?: false
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.small)
.background(
color = if (isSelected) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent,
shape = MaterialTheme.shapes.small
)
.clickable { selected[group] = !isSelected }
.padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(checked = isSelected, onCheckedChange = null)
Text(group, Modifier.padding(start = 8.dp))
}
}
}
},
confirmButton = {
OutlinedButton(
onClick = {
val toDelete = selected.filter { it.value }.keys.toList()
onDelete(toDelete)
},
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error,
containerColor = Color.Transparent,
),
) {
Text("删除选中")
}
},
dismissButton = { TextButton(onClick = onDismiss) { Text("关闭") } }
)
}
@Composable
fun QuickInputBar(
onInsert: (String) -> Unit,
modifier: Modifier = Modifier
) {
val symbols = listOf(".*", "\\d+", "\\w+", "[]", "()", "^", "$", "|", "{}", "<>")
BottomAppBar(
modifier = modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
symbols.forEach { symbol ->
AssistChip(
onClick = { onInsert(symbol) },
label = { Text(symbol) },
modifier = Modifier.padding(end = 8.dp)
)
}
}
}
@@ -1,73 +1,259 @@
package io.legado.app.ui.replace.edit package io.legado.app.ui.replace.edit
import android.app.Application import android.app.Application
import android.content.Intent import androidx.compose.ui.text.TextRange
import io.legado.app.base.BaseViewModel import androidx.compose.ui.text.input.TextFieldValue
import io.legado.app.data.appDb import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import io.legado.app.data.dao.ReplaceRuleDao
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.utils.* import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getClipText
import io.legado.app.utils.sendToClip
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class ReplaceEditViewModel(application: Application) : BaseViewModel(application) {
var replaceRule: ReplaceRule? = null data class ReplaceEditUiState(
val id: Long = -1,
val name: TextFieldValue = TextFieldValue(""),
val group: String = "默认",
val pattern: TextFieldValue = TextFieldValue(""),
val replacement: TextFieldValue = TextFieldValue(""),
val isRegex: Boolean = false,
val scope: TextFieldValue = TextFieldValue(""),
val scopeTitle: Boolean = false,
val scopeContent: Boolean = false,
val excludeScope: TextFieldValue = TextFieldValue(""),
val timeout: String = "3000",
val allGroups: List<String> = emptyList(),
val showGroupDialog: Boolean = false
)
fun initData(intent: Intent, finally: (replaceRule: ReplaceRule) -> Unit) { class ReplaceEditViewModel(
execute { private val app: Application,
val id = intent.getLongExtra("id", -1) private val replaceRuleDao: ReplaceRuleDao,
replaceRule = if (id > 0) { private val savedStateHandle: SavedStateHandle
appDb.replaceRuleDao.findById(id) ) : ViewModel() {
private val _uiState = MutableStateFlow(ReplaceEditUiState())
val uiState = _uiState.asStateFlow()
var activeField: ActiveField = ActiveField.None
enum class ActiveField { Name, None, Pattern, Replacement, Scope, Exclude }
init {
initData()
observeGroups()
}
private fun initData() {
viewModelScope.launch {
val id = savedStateHandle.get<Long>("id") ?: -1L
if (id > 0) {
val rule = replaceRuleDao.findById(id)
rule?.let { updateStateFromRule(it) }
} else { } else {
val pattern = intent.getStringExtra("pattern") ?: "" _uiState.update {
val isRegex = intent.getBooleanExtra("isRegex", false) it.copy(
val scope = intent.getStringExtra("scope") id = -1,
val isScopeTitle = intent.getBooleanExtra("isScopeTitle", false) name = TextFieldValue(""),
val isScopeContent = intent.getBooleanExtra("isScopeContent", false) pattern = TextFieldValue(savedStateHandle["pattern"] ?: ""),
ReplaceRule( isRegex = savedStateHandle["isRegex"] ?: false,
name = pattern, scope = TextFieldValue(savedStateHandle["scope"] ?: ""),
pattern = pattern, scopeTitle = savedStateHandle["isScopeTitle"] ?: false,
isRegex = isRegex, scopeContent = savedStateHandle["isScopeContent"] ?: false,
scope = scope, excludeScope = TextFieldValue(""),
scopeTitle = isScopeTitle, )
scopeContent = isScopeContent }
)
}
}.onFinally {
replaceRule?.let {
finally(it)
} }
} }
} }
fun pasteRule(success: (ReplaceRule) -> Unit) { private fun observeGroups() {
execute(context = Dispatchers.Main) { viewModelScope.launch {
val text = context.getClipText() replaceRuleDao.flowGroups().collectLatest { groups ->
if (text.isNullOrBlank()) { _uiState.update { it.copy(allGroups = listOf("默认") + groups) }
throw NoStackTraceException("剪贴板为空")
} }
GSON.fromJsonObject<ReplaceRule>(text).getOrNull()
?: throw NoStackTraceException("格式不对")
}.onSuccess {
success.invoke(it)
}.onError {
context.toastOnUi(it.localizedMessage ?: "Error")
it.printOnDebug()
} }
} }
fun save(replaceRule: ReplaceRule, success: () -> Unit) { private fun updateStateFromRule(rule: ReplaceRule) {
execute { _uiState.update {
replaceRule.checkValid() it.copy(
if (replaceRule.order == Int.MIN_VALUE) { id = rule.id,
replaceRule.order = appDb.replaceRuleDao.maxOrder + 1 name = TextFieldValue(rule.name),
} group = rule.group ?: "默认",
appDb.replaceRuleDao.insert(replaceRule) pattern = TextFieldValue(rule.pattern),
}.onSuccess { replacement = TextFieldValue(rule.replacement),
success() isRegex = rule.isRegex,
}.onError { scopeTitle = rule.scopeTitle,
context.toastOnUi("save error, ${it.localizedMessage}") scopeContent = rule.scopeContent,
scope = TextFieldValue(rule.scope ?: ""),
excludeScope = TextFieldValue(rule.excludeScope ?: ""),
timeout = rule.timeoutMillisecond.toString()
)
} }
} }
private fun getReplaceRuleFromState(): ReplaceRule {
val state = _uiState.value
val rule = ReplaceRule().apply {
id = state.id
name = state.name.text
group = if (state.group == "默认" || state.group.isBlank()) null else state.group
pattern = state.pattern.text
replacement = state.replacement.text
isRegex = state.isRegex
scopeTitle = state.scopeTitle
scopeContent = state.scopeContent
scope = state.scope.text
excludeScope = state.excludeScope.text
timeoutMillisecond = state.timeout.toLongOrNull() ?: 3000L
}
return rule
}
fun copyRule() {
viewModelScope.launch(Dispatchers.Main) {
val ruleToCopy = getReplaceRuleFromState()
val json = GSON.toJson(ruleToCopy)
app.sendToClip(json)
app.toastOnUi("规则已复制到剪贴板")
}
}
fun pasteRule(onSuccess: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
try {
val text = app.getClipText()
if (text.isNullOrBlank()) {
throw NoStackTraceException("剪贴板为空")
}
val pastedRule = GSON.fromJsonObject<ReplaceRule>(text).getOrNull()
?: throw NoStackTraceException("格式不对")
launch(Dispatchers.Main) {
updateStateFromRule(pastedRule)
onSuccess()
}
} catch (e: Exception) {
launch(Dispatchers.Main) {
app.toastOnUi(e.localizedMessage ?: "格式不对")
}
}
}
}
fun onNameChange(v: TextFieldValue) {
_uiState.update { it.copy(name = v) }
activeField = ActiveField.Name
}
fun onScopeChange(v: TextFieldValue) {
_uiState.update { it.copy(scope = v) }
activeField = ActiveField.Scope
}
fun onPatternChange(v: TextFieldValue) {
_uiState.update { it.copy(pattern = v) }
activeField = ActiveField.Pattern
}
fun onReplacementChange(v: TextFieldValue) {
_uiState.update { it.copy(replacement = v) }
activeField = ActiveField.Replacement
}
fun onExcludeScopeChange(v: TextFieldValue) {
_uiState.update { it.copy(excludeScope = v) }
activeField = ActiveField.Exclude
}
fun onGroupChange(v: String) = _uiState.update { it.copy(group = v) }
fun onRegexChange(v: Boolean) = _uiState.update { it.copy(isRegex = v) }
fun onScopeTitleChange(v: Boolean) = _uiState.update { it.copy(scopeTitle = v) }
fun onScopeContentChange(v: Boolean) = _uiState.update { it.copy(scopeContent = v) }
fun onTimeoutChange(v: String) = _uiState.update { it.copy(timeout = v) }
fun toggleGroupDialog(show: Boolean) = _uiState.update { it.copy(showGroupDialog = show) }
fun insertTextAtCursor(text: String) {
val state = _uiState.value
when (activeField) {
ActiveField.Name -> {
val newTfv = insertIntoTextFieldValue(state.name, text)
_uiState.update { it.copy(name = newTfv) }
}
ActiveField.Pattern -> {
val newTfv = insertIntoTextFieldValue(state.pattern, text)
_uiState.update { it.copy(pattern = newTfv) }
}
ActiveField.Replacement -> {
val newTfv = insertIntoTextFieldValue(state.replacement, text)
_uiState.update { it.copy(replacement = newTfv) }
}
ActiveField.Scope -> {
val newTfv = insertIntoTextFieldValue(state.scope, text)
_uiState.update { it.copy(scope = newTfv) }
}
ActiveField.Exclude -> {
val newTfv = insertIntoTextFieldValue(state.excludeScope, text)
_uiState.update { it.copy(excludeScope = newTfv) }
}
else -> {}
}
}
private fun insertIntoTextFieldValue(current: TextFieldValue, insert: String): TextFieldValue {
val start = current.selection.start
val end = current.selection.end
val newText = current.text.replaceRange(start, end, insert)
val newCursor = start + insert.length
return TextFieldValue(
text = newText,
selection = TextRange(newCursor)
)
}
fun save(onSuccess: () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val state = _uiState.value
val rule = ReplaceRule().apply {
id = state.id
name = state.name.text
group = if (state.group == "默认" || state.group.isBlank()) null else state.group
pattern = state.pattern.text
replacement = state.replacement.text
isRegex = state.isRegex
scopeTitle = state.scopeTitle
scopeContent = state.scopeContent
scope = state.scope.text
excludeScope = state.excludeScope.text
timeoutMillisecond = state.timeout.toLongOrNull() ?: 3000L
}
if (rule.id <= 0) {
rule.order = replaceRuleDao.maxOrder + 1
}
if (rule.order == Int.MIN_VALUE) {
rule.order = replaceRuleDao.maxOrder + 1
}
replaceRuleDao.insert(rule)
launch(Dispatchers.Main) {
onSuccess()
}
}
}
fun deleteGroups(groups: List<String>) {
viewModelScope.launch {
replaceRuleDao.clearGroups(groups)
toggleGroupDialog(false)
}
}
} }
@@ -8,7 +8,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
+1 -1
View File
@@ -1,5 +1,5 @@
VERSION_MAJOR=3 VERSION_MAJOR=3
VERSION_MINOR=26 VERSION_MINOR=26
VERSION_PATCH=5 VERSION_PATCH=5
VERSION_SUFFIX=0 VERSION_SUFFIX=1
# 1 = Pre, 0 = Release # 1 = Pre, 0 = Release