This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

@@ -0,0 +1,144 @@
package io.legato.kazusa.ui.replace
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import io.legato.kazusa.R
import io.legato.kazusa.base.BaseDialogFragment
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.data.appDb
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.databinding.DialogRecyclerViewBinding
import io.legato.kazusa.databinding.ItemGroupManageBinding
import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.backgroundColor
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.applyTint
import io.legato.kazusa.utils.requestInputMethod
import io.legato.kazusa.utils.setLayout
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.launch
class GroupManageDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
Toolbar.OnMenuItemClickListener {
private val viewModel: ReplaceRuleViewModel by activityViewModels()
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
private val adapter by lazy { GroupAdapter(requireContext()) }
override fun onStart() {
super.onStart()
setLayout(0.9f, 0.9f)
}
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
// view.setBackgroundColor(backgroundColor)
// binding.toolBar.setBackgroundColor(primaryColor)
initView()
initData()
}
private fun initView() = binding.run {
toolBar.title = getString(R.string.group_manage)
toolBar.inflateMenu(R.menu.group_manage)
toolBar.menu.applyTint(requireContext())
toolBar.setOnMenuItemClickListener(this@GroupManageDialog)
recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.addItemDecoration(VerticalDivider(requireContext()))
recyclerView.adapter = adapter
}
private fun initData() {
lifecycleScope.launch {
appDb.replaceRuleDao.flowGroups().conflate().collect {
adapter.setItems(it)
}
}
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_add -> addGroup()
}
return true
}
@SuppressLint("InflateParams")
private fun addGroup() {
alert(title = getString(R.string.add_group)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
}
customView { alertBinding.root }
okButton {
alertBinding.editView.text?.toString()?.let {
if (it.isNotBlank()) {
viewModel.addGroup(it)
}
}
}
cancelButton()
}.requestInputMethod()
}
@SuppressLint("InflateParams")
private fun editGroup(group: String) {
alert(title = getString(R.string.group_edit)) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.setHint(R.string.group_name)
editView.setText(group)
}
customView { alertBinding.root }
okButton {
viewModel.upGroup(group, alertBinding.editView.text?.toString())
}
cancelButton()
}.requestInputMethod()
}
private inner class GroupAdapter(context: Context) :
RecyclerAdapter<String, ItemGroupManageBinding>(context) {
override fun getViewBinding(parent: ViewGroup): ItemGroupManageBinding {
return ItemGroupManageBinding.inflate(inflater, parent, false)
}
override fun convert(
holder: ItemViewHolder,
binding: ItemGroupManageBinding,
item: String,
payloads: MutableList<Any>
) {
binding.run {
//root.setBackgroundColor(context.backgroundColor)
tvGroup.text = item
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemGroupManageBinding) {
binding.apply {
tvEdit.setOnClickListener {
getItem(holder.layoutPosition)?.let {
editGroup(it)
}
}
tvDel.setOnClickListener {
getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) }
}
}
}
}
}
@@ -0,0 +1,382 @@
package io.legato.kazusa.ui.replace
import android.annotation.SuppressLint
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.SubMenu
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.databinding.ActivityReplaceRuleBinding
import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.help.book.ContentProcessor
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
//import io.legado.app.lib.theme.primaryTextColor
import io.legato.kazusa.ui.association.ImportReplaceRuleDialog
import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.qrcode.QrCodeResult
import io.legato.kazusa.ui.replace.edit.ReplaceEditActivity
import io.legato.kazusa.ui.widget.SelectActionBar
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
import io.legato.kazusa.ui.widget.recycler.VerticalDivider
import io.legato.kazusa.utils.ACache
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.hideSoftInput
import io.legato.kazusa.utils.isAbsUrl
import io.legato.kazusa.utils.launch
import io.legato.kazusa.utils.readText
import io.legato.kazusa.utils.sendToClip
import io.legato.kazusa.utils.shouldHideSoftInput
import io.legato.kazusa.utils.showDialogFragment
import io.legato.kazusa.utils.showHelp
import io.legato.kazusa.utils.splitNotBlank
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.transaction
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
/**
* 替换规则管理
*/
class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRuleViewModel>(),
SearchView.OnQueryTextListener,
PopupMenu.OnMenuItemClickListener,
SelectActionBar.CallBack,
ReplaceRuleAdapter.CallBack {
override val binding by viewBinding(ActivityReplaceRuleBinding::inflate)
override val viewModel by viewModels<ReplaceRuleViewModel>()
private val importRecordKey = "replaceRuleRecordKey"
private val adapter by lazy { ReplaceRuleAdapter(this, this) }
private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view)
}
private var groups = hashSetOf<String>()
private var groupMenu: SubMenu? = null
private var replaceRuleFlowJob: Job? = null
private var dataInit = false
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult
showDialogFragment(
ImportReplaceRuleDialog(it)
)
}
private val editActivity =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
setResult(RESULT_OK)
}
}
private val importDoc = registerForActivityResult(HandleFileContract()) {
kotlin.runCatching {
it.uri?.readText(this)?.let {
showDialogFragment(
ImportReplaceRuleDialog(it)
)
}
}.onFailure {
toastOnUi("readTextError:${it.localizedMessage}")
}
}
private val exportResult = registerForActivityResult(HandleFileContract()) {
it.uri?.let { uri ->
alert(R.string.export_success) {
if (uri.toString().isAbsUrl()) {
setMessage(DirectLinkUpload.getSummary())
}
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = getString(R.string.path)
editView.setText(uri.toString())
}
customView { alertBinding.root }
okButton {
sendToClip(uri.toString())
}
}
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
initRecyclerView()
initSearchView()
initSelectActionView()
observeReplaceRuleData()
observeGroupData()
}
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
if (ev.action == MotionEvent.ACTION_DOWN) {
currentFocus?.let {
if (it.shouldHideSoftInput(ev)) {
it.post {
it.clearFocus()
it.hideSoftInput()
}
}
}
}
return super.dispatchTouchEvent(ev)
}
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.replace_rule, menu)
return super.onCompatCreateOptionsMenu(menu)
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
groupMenu = menu.findItem(R.id.menu_group)?.subMenu
upGroupMenu()
return super.onPrepareOptionsMenu(menu)
}
private fun initRecyclerView() {
//binding.recyclerView.setEdgeEffectColor(primaryColor)
binding.recyclerView.layoutManager = LinearLayoutManager(this)
binding.recyclerView.adapter = adapter
binding.recyclerView.addItemDecoration(VerticalDivider(this))
val itemTouchCallback = ItemTouchCallback(adapter)
itemTouchCallback.isCanDrag = true
val dragSelectTouchHelper: DragSelectTouchHelper =
DragSelectTouchHelper(adapter.dragSelectCallback).setSlideArea(16, 50)
dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView)
// When this page is opened, it is in selection mode
dragSelectTouchHelper.activeSlideSelect()
// Note: need judge selection first, so add ItemTouchHelper after it.
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView)
}
private fun initSearchView() {
//searchView.applyTint(primaryTextColor)
searchView.queryHint = getString(R.string.replace_purify_search)
searchView.setOnQueryTextListener(this)
}
override fun selectAll(selectAll: Boolean) {
if (selectAll) {
adapter.selectAll()
} else {
adapter.revertSelection()
}
}
override fun revertSelection() {
adapter.revertSelection()
}
override fun onClickSelectBarMainAction() {
alert(titleResource = R.string.draw, messageResource = R.string.sure_del) {
yesButton { viewModel.delSelection(adapter.selection) }
noButton()
}
}
private fun initSelectActionView() {
binding.selectActionBar.setMainActionText(R.string.delete)
binding.selectActionBar.inflateMenu(R.menu.replace_rule_sel)
binding.selectActionBar.setOnMenuItemClickListener(this)
binding.selectActionBar.setCallBack(this)
}
private fun observeReplaceRuleData(searchKey: String? = null) {
dataInit = false
replaceRuleFlowJob?.cancel()
replaceRuleFlowJob = lifecycleScope.launch {
when {
searchKey.isNullOrEmpty() -> {
appDb.replaceRuleDao.flowAll()
}
searchKey == getString(R.string.no_group) -> {
appDb.replaceRuleDao.flowNoGroup()
}
searchKey.startsWith("group:") -> {
val key = searchKey.substringAfter("group:")
appDb.replaceRuleDao.flowGroupSearch("%$key%")
}
else -> {
appDb.replaceRuleDao.flowSearch("%$searchKey%")
}
}.catch {
AppLog.put("替换规则管理界面更新数据出错", it)
}.flowOn(IO).conflate().collect {
if (dataInit) {
setResult(Activity.RESULT_OK)
}
adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true
delay(100)
}
}
}
private fun observeGroupData() {
lifecycleScope.launch {
appDb.replaceRuleDao.flowGroups().collect {
groups.clear()
groups.addAll(it)
upGroupMenu()
}
}
}
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add_replace_rule ->
editActivity.launch(ReplaceEditActivity.startIntent(this))
R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
R.id.menu_del_selection -> viewModel.delSelection(adapter.selection)
R.id.menu_import_onLine -> showImportDialog()
R.id.menu_import_local -> importDoc.launch {
mode = HandleFileContract.FILE
allowExtensions = arrayOf("txt", "json")
}
R.id.menu_import_qr -> qrCodeResult.launch()
R.id.menu_help -> showHelp("replaceRuleHelp")
R.id.menu_group_null -> {
searchView.setQuery(getString(R.string.no_group), true)
}
else -> if (item.groupId == R.id.replace_group) {
searchView.setQuery("group:${item.title}", true)
}
}
return super.onCompatOptionsItemSelected(item)
}
override fun onMenuItemClick(item: MenuItem?): Boolean {
when (item?.itemId) {
R.id.menu_enable_selection -> viewModel.enableSelection(adapter.selection)
R.id.menu_disable_selection -> viewModel.disableSelection(adapter.selection)
R.id.menu_top_sel -> viewModel.topSelect(adapter.selection)
R.id.menu_bottom_sel -> viewModel.bottomSelect(adapter.selection)
R.id.menu_export_selection -> exportResult.launch {
mode = HandleFileContract.EXPORT
fileData = HandleFileContract.FileData(
"exportReplaceRule.json",
GSON.toJson(adapter.selection).toByteArray(),
"application/json"
)
}
}
return false
}
private fun upGroupMenu() = groupMenu?.transaction { menu ->
menu.removeGroup(R.id.replace_group)
groups.forEach {
menu.add(R.id.replace_group, Menu.NONE, Menu.NONE, it)
}
}
@SuppressLint("InflateParams")
private fun showImportDialog() {
val aCache = ACache.get(cacheDir = false)
val cacheUrls: MutableList<String> = aCache
.getAsString(importRecordKey)
?.splitNotBlank(",")
?.toMutableList() ?: mutableListOf()
alert(titleResource = R.string.import_on_line) {
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
editView.hint = "url"
editView.setFilterValues(cacheUrls)
editView.delCallBack = {
cacheUrls.remove(it)
aCache.put(importRecordKey, cacheUrls.joinToString(","))
}
}
customView { alertBinding.root }
okButton {
val text = alertBinding.editView.text?.toString()
text?.let {
if (it.isAbsUrl() && !cacheUrls.contains(it)) {
cacheUrls.add(0, it)
aCache.put(importRecordKey, cacheUrls.joinToString(","))
}
showDialogFragment(
ImportReplaceRuleDialog(it)
)
}
}
cancelButton()
}
}
override fun onQueryTextChange(newText: String?): Boolean {
observeReplaceRuleData(newText)
return false
}
override fun onQueryTextSubmit(query: String?): Boolean {
return false
}
override fun onDestroy() {
super.onDestroy()
Coroutine.async { ContentProcessor.upReplaceRules() }
}
override fun upCountView() {
binding.selectActionBar.upCountView(
adapter.selection.size,
adapter.itemCount
)
}
override fun update(vararg rule: ReplaceRule) {
setResult(RESULT_OK)
viewModel.update(*rule)
}
override fun delete(rule: ReplaceRule) {
alert(R.string.draw) {
setMessage(getString(R.string.sure_del) + "\n" + rule.name)
noButton()
yesButton {
setResult(RESULT_OK)
viewModel.delete(rule)
}
}
}
override fun edit(rule: ReplaceRule) {
setResult(RESULT_OK)
editActivity.launch(ReplaceEditActivity.startIntent(this, rule.id))
}
override fun toTop(rule: ReplaceRule) {
setResult(RESULT_OK)
viewModel.toTop(rule)
}
override fun toBottom(rule: ReplaceRule) {
setResult(RESULT_OK)
viewModel.toBottom(rule)
}
override fun upOrder() {
setResult(RESULT_OK)
viewModel.upOrder()
}
}
@@ -0,0 +1,235 @@
package io.legato.kazusa.ui.replace
import android.content.Context
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import io.legato.kazusa.R
import io.legato.kazusa.base.adapter.ItemViewHolder
import io.legato.kazusa.base.adapter.RecyclerAdapter
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.databinding.ItemReplaceRuleBinding
//import io.legado.app.lib.theme.backgroundColor
import io.legato.kazusa.ui.widget.recycler.DragSelectTouchHelper
import io.legato.kazusa.ui.widget.recycler.ItemTouchCallback
class ReplaceRuleAdapter(context: Context, var callBack: CallBack) :
RecyclerAdapter<ReplaceRule, ItemReplaceRuleBinding>(context),
ItemTouchCallback.Callback {
private val selected = linkedSetOf<ReplaceRule>()
val selection: List<ReplaceRule>
get() {
return getItems().filter {
selected.contains(it)
}
}
val diffItemCallBack = object : DiffUtil.ItemCallback<ReplaceRule>() {
override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean {
if (oldItem.name != newItem.name) {
return false
}
if (oldItem.group != newItem.group) {
return false
}
if (oldItem.isEnabled != newItem.isEnabled) {
return false
}
return true
}
override fun getChangePayload(oldItem: ReplaceRule, newItem: ReplaceRule): Any? {
val payload = Bundle()
if (oldItem.name != newItem.name
|| oldItem.group != newItem.group
) {
payload.putBoolean("upName", true)
}
if (oldItem.isEnabled != newItem.isEnabled) {
payload.putBoolean("enabled", newItem.isEnabled)
}
if (payload.isEmpty) {
return null
}
return payload
}
}
fun selectAll() {
getItems().forEach {
selected.add(it)
}
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
callBack.upCountView()
}
fun revertSelection() {
getItems().forEach {
if (selected.contains(it)) {
selected.remove(it)
} else {
selected.add(it)
}
}
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null)))
callBack.upCountView()
}
override fun getViewBinding(parent: ViewGroup): ItemReplaceRuleBinding {
return ItemReplaceRuleBinding.inflate(inflater, parent, false)
}
override fun onCurrentListChanged() {
callBack.upCountView()
}
override fun convert(
holder: ItemViewHolder,
binding: ItemReplaceRuleBinding,
item: ReplaceRule,
payloads: MutableList<Any>
) {
binding.run {
if (payloads.isEmpty()) {
//root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f))
cbName.text = item.getDisplayNameGroup()
swtEnabled.isChecked = item.isEnabled
cbName.isChecked = selected.contains(item)
} else {
for (i in payloads.indices) {
val bundle = payloads[i] as Bundle
bundle.keySet().forEach {
when (it) {
"selected" -> cbName.isChecked = selected.contains(item)
"upName" -> cbName.text = item.getDisplayNameGroup()
"enabled" -> swtEnabled.isChecked = item.isEnabled
}
}
}
}
}
}
override fun registerListener(holder: ItemViewHolder, binding: ItemReplaceRuleBinding) {
binding.apply {
swtEnabled.setOnCheckedChangeListener { buttonView, isChecked ->
if (buttonView.isPressed) {
getItem(holder.layoutPosition)?.let {
it.isEnabled = isChecked
callBack.update(it)
}
}
}
ivEdit.setOnClickListener {
getItem(holder.layoutPosition)?.let {
callBack.edit(it)
}
}
cbName.setOnClickListener {
getItem(holder.layoutPosition)?.let {
if (cbName.isChecked) {
selected.add(it)
} else {
selected.remove(it)
}
}
callBack.upCountView()
}
ivMenuMore.setOnClickListener {
showMenu(ivMenuMore, holder.layoutPosition)
}
}
}
private fun showMenu(view: View, position: Int) {
val item = getItem(position) ?: return
val popupMenu = PopupMenu(context, view)
popupMenu.inflate(R.menu.replace_rule_item)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.menu_top -> callBack.toTop(item)
R.id.menu_bottom -> callBack.toBottom(item)
R.id.menu_del -> {
callBack.delete(item)
selected.remove(item)
}
}
true
}
popupMenu.show()
}
override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
val srcItem = getItem(srcPosition)
val targetItem = getItem(targetPosition)
if (srcItem != null && targetItem != null) {
if (srcItem.order == targetItem.order) {
callBack.upOrder()
} else {
val srcOrder = srcItem.order
srcItem.order = targetItem.order
targetItem.order = srcOrder
movedItems.add(srcItem)
movedItems.add(targetItem)
}
}
swapItem(srcPosition, targetPosition)
return true
}
private val movedItems = linkedSetOf<ReplaceRule>()
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
if (movedItems.isNotEmpty()) {
callBack.update(*movedItems.toTypedArray())
movedItems.clear()
}
}
val dragSelectCallback: DragSelectTouchHelper.Callback =
object : DragSelectTouchHelper.AdvanceCallback<ReplaceRule>(Mode.ToggleAndReverse) {
override fun currentSelectedId(): MutableSet<ReplaceRule> {
return selected
}
override fun getItemId(position: Int): ReplaceRule {
return getItem(position)!!
}
override fun updateSelectState(position: Int, isSelected: Boolean): Boolean {
getItem(position)?.let {
if (isSelected) {
selected.add(it)
} else {
selected.remove(it)
}
notifyItemChanged(position, bundleOf(Pair("selected", null)))
callBack.upCountView()
return true
}
return false
}
}
interface CallBack {
fun update(vararg rule: ReplaceRule)
fun delete(rule: ReplaceRule)
fun edit(rule: ReplaceRule)
fun toTop(rule: ReplaceRule)
fun toBottom(rule: ReplaceRule)
fun upOrder()
fun upCountView()
}
}
@@ -0,0 +1,135 @@
package io.legato.kazusa.ui.replace
import android.app.Application
import android.text.TextUtils
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.utils.splitNotBlank
/**
* 替换规则数据修改
* 修改数据要copy,直接修改会导致界面不刷新
*/
class ReplaceRuleViewModel(application: Application) : BaseViewModel(application) {
fun update(vararg rule: ReplaceRule) {
execute {
appDb.replaceRuleDao.update(*rule)
}
}
fun delete(rule: ReplaceRule) {
execute {
appDb.replaceRuleDao.delete(rule)
}
}
fun toTop(rule: ReplaceRule) {
execute {
rule.order = appDb.replaceRuleDao.minOrder - 1
appDb.replaceRuleDao.update(rule)
}
}
fun topSelect(rules: List<ReplaceRule>) {
execute {
var minOrder = appDb.replaceRuleDao.minOrder - rules.size
rules.forEach {
it.order = ++minOrder
}
appDb.replaceRuleDao.update(*rules.toTypedArray())
}
}
fun toBottom(rule: ReplaceRule) {
execute {
rule.order = appDb.replaceRuleDao.maxOrder + 1
appDb.replaceRuleDao.update(rule)
}
}
fun bottomSelect(rules: List<ReplaceRule>) {
execute {
var maxOrder = appDb.replaceRuleDao.maxOrder
rules.forEach {
it.order = maxOrder++
}
appDb.replaceRuleDao.update(*rules.toTypedArray())
}
}
fun upOrder() {
execute {
val rules = appDb.replaceRuleDao.all
for ((index, rule) in rules.withIndex()) {
rule.order = index + 1
}
appDb.replaceRuleDao.update(*rules.toTypedArray())
}
}
fun enableSelection(rules: List<ReplaceRule>) {
execute {
val array = Array(rules.size) {
rules[it].copy(isEnabled = true)
}
appDb.replaceRuleDao.update(*array)
}
}
fun disableSelection(rules: List<ReplaceRule>) {
execute {
val array = Array(rules.size) {
rules[it].copy(isEnabled = false)
}
appDb.replaceRuleDao.update(*array)
}
}
fun delSelection(rules: List<ReplaceRule>) {
execute {
appDb.replaceRuleDao.delete(*rules.toTypedArray())
}
}
fun addGroup(group: String) {
execute {
val sources = appDb.replaceRuleDao.noGroup
sources.forEach { source ->
source.group = group
}
appDb.replaceRuleDao.update(*sources.toTypedArray())
}
}
fun upGroup(oldGroup: String, newGroup: String?) {
execute {
val sources = appDb.replaceRuleDao.getByGroup(oldGroup)
sources.forEach { source ->
source.group?.splitNotBlank(",")?.toHashSet()?.let {
it.remove(oldGroup)
if (!newGroup.isNullOrEmpty())
it.add(newGroup)
source.group = TextUtils.join(",", it)
}
}
appDb.replaceRuleDao.update(*sources.toTypedArray())
}
}
fun delGroup(group: String) {
execute {
execute {
val sources = appDb.replaceRuleDao.getByGroup(group)
sources.forEach { source ->
source.group?.splitNotBlank(",")?.toHashSet()?.let {
it.remove(group)
source.group = TextUtils.join(",", it)
}
}
appDb.replaceRuleDao.update(*sources.toTypedArray())
}
}
}
}
@@ -0,0 +1,157 @@
package io.legato.kazusa.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.EditText
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.base.VMBaseActivity
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.databinding.ActivityReplaceEditBinding
import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.ui.widget.keyboard.KeyboardToolPop
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.imeHeight
import io.legato.kazusa.utils.sendToClip
import io.legato.kazusa.utils.setOnApplyWindowInsetsListenerCompat
import io.legato.kazusa.utils.showHelp
import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
/**
* 编辑替换规则
*/
class ReplaceEditActivity :
VMBaseActivity<ActivityReplaceEditBinding, ReplaceEditViewModel>(),
KeyboardToolPop.CallBack {
companion object {
fun startIntent(
context: Context,
id: Long = -1,
pattern: String? = null,
isRegex: Boolean = false,
scope: String? = null
): Intent {
val intent = Intent(context, ReplaceEditActivity::class.java)
intent.putExtra("id", id)
intent.putExtra("pattern", pattern)
intent.putExtra("isRegex", isRegex)
intent.putExtra("scope", scope)
return intent
}
}
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 onActivityCreated(savedInstanceState: Bundle?) {
softKeyboardTool.attachToWindow(window)
initView()
viewModel.initData(intent) {
upReplaceView(it)
}
}
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(it)
}
}
return true
}
override fun onDestroy() {
super.onDestroy()
softKeyboardTool.dismiss()
}
private fun initView() {
binding.ivHelp.setOnClickListener {
showHelp("regexHelp")
}
binding.root.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
softKeyboardTool.initialPadding = windowInsets.imeHeight
windowInsets
}
}
private fun upReplaceView(replaceRule: ReplaceRule) = binding.run {
etName.setText(replaceRule.name)
etGroup.setText(replaceRule.group)
etReplaceRule.setText(replaceRule.pattern)
cbUseRegex.isChecked = replaceRule.isRegex
etReplaceTo.setText(replaceRule.replacement)
cbScopeTitle.isChecked = replaceRule.scopeTitle
cbScopeContent.isChecked = replaceRule.scopeContent
etScope.setText(replaceRule.scope)
etExcludeScope.setText(replaceRule.excludeScope)
etTimeout.setText(replaceRule.timeoutMillisecond.toString())
}
private fun getReplaceRule(): ReplaceRule = binding.run {
val replaceRule: ReplaceRule = viewModel.replaceRule ?: ReplaceRule()
replaceRule.name = etName.text.toString()
replaceRule.group = etGroup.text.toString()
replaceRule.pattern = etReplaceRule.text.toString()
replaceRule.isRegex = cbUseRegex.isChecked
replaceRule.replacement = etReplaceTo.text.toString()
replaceRule.scopeTitle = cbScopeTitle.isChecked
replaceRule.scopeContent = cbScopeContent.isChecked
replaceRule.scope = etScope.text.toString()
replaceRule.excludeScope = etExcludeScope.text.toString()
replaceRule.timeoutMillisecond = etTimeout.text.toString().ifEmpty { "3000" }.toLong()
return replaceRule
}
override fun helpActions(): List<SelectItem<String>> {
return arrayListOf(
SelectItem("正则教程", "regexHelp")
)
}
override fun onHelpActionSelect(action: String) {
when (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
//获取EditText的文字
val edit = view.editableText
if (start < 0 || start >= edit.length) {
edit.append(text)
} else {
//光标所在位置插入文字
edit.replace(start, end, text)
}
}
}
}
@@ -0,0 +1,69 @@
package io.legato.kazusa.ui.replace.edit
import android.app.Application
import android.content.Intent
import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.exception.NoStackTraceException
import io.legato.kazusa.utils.*
import kotlinx.coroutines.Dispatchers
class ReplaceEditViewModel(application: Application) : BaseViewModel(application) {
var replaceRule: ReplaceRule? = null
fun initData(intent: Intent, finally: (replaceRule: ReplaceRule) -> Unit) {
execute {
val id = intent.getLongExtra("id", -1)
replaceRule = if (id > 0) {
appDb.replaceRuleDao.findById(id)
} else {
val pattern = intent.getStringExtra("pattern") ?: ""
val isRegex = intent.getBooleanExtra("isRegex", false)
val scope = intent.getStringExtra("scope")
ReplaceRule(
name = pattern,
pattern = pattern,
isRegex = isRegex,
scope = scope
)
}
}.onFinally {
replaceRule?.let {
finally(it)
}
}
}
fun pasteRule(success: (ReplaceRule) -> Unit) {
execute(context = Dispatchers.Main) {
val text = context.getClipText()
if (text.isNullOrBlank()) {
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) {
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}")
}
}
}