[新增] 由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.SearchRule
import io.legado.app.di.appDatabaseModule
import io.legado.app.di.bookmarkModule
import io.legado.app.di.exploreModule
import io.legado.app.di.readRecordModule
import io.legado.app.di.appModule
import io.legado.app.help.AppFreezeMonitor
import io.legado.app.help.AppWebDav
import io.legado.app.help.CrashHandler
@@ -80,7 +78,7 @@ class App : Application() {
override fun onCreate() {
startKoin {
androidContext(this@App)
modules(appDatabaseModule, readRecordModule, exploreModule, bookmarkModule)
modules(appDatabaseModule, appModule)
}
if (getPrefString("app_theme", "0") == "12") {
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.AnimatedTextLine
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 kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -2,37 +2,15 @@ package io.legado.app.ui.replace.edit
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.ArrayAdapter
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
import androidx.activity.compose.setContent
import androidx.compose.runtime.Composable
import io.legado.app.base.AppTheme
import io.legado.app.base.BaseComposeActivity
/**
* 编辑替换规则
*/
class ReplaceEditActivity :
VMBaseActivity<ActivityReplaceEditBinding, ReplaceEditViewModel>(),
KeyboardToolPop.CallBack {
class ReplaceEditActivity : BaseComposeActivity() {
companion object {
fun startIntent(
@@ -53,161 +31,24 @@ class ReplaceEditActivity :
}
}
override val binding by viewBinding(ActivityReplaceEditBinding::inflate)
override val viewModel by viewModels<ReplaceEditViewModel>()
private val softKeyboardTool by lazy {
KeyboardToolPop(this, lifecycleScope, binding.root, this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
softKeyboardTool.attachToWindow(window)
initView()
viewModel.initData(intent, ::upReplaceView)
}
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
@Composable
override fun Content() {
setContent {
AppTheme {
ReplaceEditScreen(
onBack = { finish() },
onSaveSuccess = {
setResult(RESULT_OK)
finish()
},
onShowHelp = { key -> showHelp(key) }
)
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 {
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 showHelp(key: String) {
}
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
import android.app.Application
import android.content.Intent
import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
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.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.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) {
execute {
val id = intent.getLongExtra("id", -1)
replaceRule = if (id > 0) {
appDb.replaceRuleDao.findById(id)
class ReplaceEditViewModel(
private val app: Application,
private val replaceRuleDao: ReplaceRuleDao,
private val savedStateHandle: SavedStateHandle
) : 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 {
val pattern = intent.getStringExtra("pattern") ?: ""
val isRegex = intent.getBooleanExtra("isRegex", false)
val scope = intent.getStringExtra("scope")
val isScopeTitle = intent.getBooleanExtra("isScopeTitle", false)
val isScopeContent = intent.getBooleanExtra("isScopeContent", false)
ReplaceRule(
name = pattern,
pattern = pattern,
isRegex = isRegex,
scope = scope,
scopeTitle = isScopeTitle,
scopeContent = isScopeContent
)
}
}.onFinally {
replaceRule?.let {
finally(it)
_uiState.update {
it.copy(
id = -1,
name = TextFieldValue(""),
pattern = TextFieldValue(savedStateHandle["pattern"] ?: ""),
isRegex = savedStateHandle["isRegex"] ?: false,
scope = TextFieldValue(savedStateHandle["scope"] ?: ""),
scopeTitle = savedStateHandle["isScopeTitle"] ?: false,
scopeContent = savedStateHandle["isScopeContent"] ?: false,
excludeScope = TextFieldValue(""),
)
}
}
}
}
fun pasteRule(success: (ReplaceRule) -> Unit) {
execute(context = Dispatchers.Main) {
val text = context.getClipText()
if (text.isNullOrBlank()) {
throw NoStackTraceException("剪贴板为空")
private fun observeGroups() {
viewModelScope.launch {
replaceRuleDao.flowGroups().collectLatest { groups ->
_uiState.update { it.copy(allGroups = listOf("默认") + groups) }
}
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) {
execute {
replaceRule.checkValid()
if (replaceRule.order == Int.MIN_VALUE) {
replaceRule.order = appDb.replaceRuleDao.maxOrder + 1
}
appDb.replaceRuleDao.insert(replaceRule)
}.onSuccess {
success()
}.onError {
context.toastOnUi("save error, ${it.localizedMessage}")
private fun updateStateFromRule(rule: ReplaceRule) {
_uiState.update {
it.copy(
id = rule.id,
name = TextFieldValue(rule.name),
group = rule.group ?: "默认",
pattern = TextFieldValue(rule.pattern),
replacement = TextFieldValue(rule.replacement),
isRegex = rule.isRegex,
scopeTitle = rule.scopeTitle,
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.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable