[新增] 由Compose重写的替换净化编辑界面,没啥变化,可能有BUG
This commit is contained in:
@@ -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)
|
|
||||||
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)
|
setResult(RESULT_OK)
|
||||||
finish()
|
finish()
|
||||||
}
|
},
|
||||||
|
onShowHelp = { key -> showHelp(key) }
|
||||||
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 ->
|
||||||
|
_uiState.update { it.copy(allGroups = listOf("默认") + groups) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
if (text.isNullOrBlank()) {
|
||||||
throw NoStackTraceException("剪贴板为空")
|
throw NoStackTraceException("剪贴板为空")
|
||||||
}
|
}
|
||||||
GSON.fromJsonObject<ReplaceRule>(text).getOrNull()
|
|
||||||
|
val pastedRule = GSON.fromJsonObject<ReplaceRule>(text).getOrNull()
|
||||||
?: throw NoStackTraceException("格式不对")
|
?: throw NoStackTraceException("格式不对")
|
||||||
}.onSuccess {
|
|
||||||
success.invoke(it)
|
launch(Dispatchers.Main) {
|
||||||
}.onError {
|
updateStateFromRule(pastedRule)
|
||||||
context.toastOnUi(it.localizedMessage ?: "Error")
|
onSuccess()
|
||||||
it.printOnDebug()
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
app.toastOnUi(e.localizedMessage ?: "格式不对")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun save(replaceRule: ReplaceRule, success: () -> Unit) {
|
fun onNameChange(v: TextFieldValue) {
|
||||||
execute {
|
_uiState.update { it.copy(name = v) }
|
||||||
replaceRule.checkValid()
|
activeField = ActiveField.Name
|
||||||
if (replaceRule.order == Int.MIN_VALUE) {
|
|
||||||
replaceRule.order = appDb.replaceRuleDao.maxOrder + 1
|
|
||||||
}
|
}
|
||||||
appDb.replaceRuleDao.insert(replaceRule)
|
fun onScopeChange(v: TextFieldValue) {
|
||||||
}.onSuccess {
|
_uiState.update { it.copy(scope = v) }
|
||||||
success()
|
activeField = ActiveField.Scope
|
||||||
}.onError {
|
}
|
||||||
context.toastOnUi("save error, ${it.localizedMessage}")
|
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,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
|
||||||
Reference in New Issue
Block a user