[新增] 由 Compose 重写的字典规则功能
This commit is contained in:
@@ -1,10 +1,14 @@
|
|||||||
package io.legado.app.data.dao
|
package io.legado.app.data.dao
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface DictRuleDao {
|
interface DictRuleDao {
|
||||||
|
|
||||||
@@ -17,9 +21,15 @@ interface DictRuleDao {
|
|||||||
@Query("select * from dictRules order by sortNumber")
|
@Query("select * from dictRules order by sortNumber")
|
||||||
fun flowAll(): Flow<List<DictRule>>
|
fun flowAll(): Flow<List<DictRule>>
|
||||||
|
|
||||||
|
@Query("select * from dictRules where name LIKE '%' || :key || '%' order by sortNumber")
|
||||||
|
fun flowSearch(key: String): Flow<List<DictRule>>
|
||||||
|
|
||||||
@Query("select * from dictRules where name = :name")
|
@Query("select * from dictRules where name = :name")
|
||||||
fun getByName(name: String): DictRule?
|
fun getByName(name: String): DictRule?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM dictRules WHERE name IN (:names)")
|
||||||
|
fun getByNames(names: Set<String>): List<DictRule>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(vararg dictRule: DictRule)
|
fun insert(vararg dictRule: DictRule)
|
||||||
|
|
||||||
@@ -29,4 +39,10 @@ interface DictRuleDao {
|
|||||||
@Delete
|
@Delete
|
||||||
fun delete(vararg dictRule: DictRule)
|
fun delete(vararg dictRule: DictRule)
|
||||||
|
|
||||||
}
|
@Query("UPDATE dictRules SET enabled = :enabled WHERE name IN (:names)")
|
||||||
|
suspend fun updateEnabled(names: Set<String>, enabled: Boolean)
|
||||||
|
|
||||||
|
@Query("DELETE FROM dictRules WHERE name IN (:names)")
|
||||||
|
suspend fun deleteByIds(names: Set<String>)
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.DictRule
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class DictRuleRepository {
|
||||||
|
|
||||||
|
private val dao = appDb.dictRuleDao
|
||||||
|
|
||||||
|
fun flowAll(): Flow<List<DictRule>> {
|
||||||
|
return dao.flowAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun flowSearch(key: String): Flow<List<DictRule>> {
|
||||||
|
return dao.flowSearch(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAll(): List<DictRule> {
|
||||||
|
return dao.all
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insert(vararg rule: DictRule) {
|
||||||
|
dao.insert(*rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(vararg rule: DictRule) {
|
||||||
|
dao.delete(*rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(vararg rule: DictRule) {
|
||||||
|
dao.update(*rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun enableByIds(names: Set<String>) = withContext(Dispatchers.IO) {
|
||||||
|
if (names.isEmpty()) return@withContext
|
||||||
|
val rules = dao.getByNames(names)
|
||||||
|
val updated = rules.map { it.copy(enabled = true) }
|
||||||
|
dao.update(*updated.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun disableByIds(names: Set<String>) = withContext(Dispatchers.IO) {
|
||||||
|
if (names.isEmpty()) return@withContext
|
||||||
|
val rules = dao.getByNames(names)
|
||||||
|
val updated = rules.map { it.copy(enabled = false) }
|
||||||
|
dao.update(*updated.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun deleteByIds(names: Set<String>) = withContext(Dispatchers.IO) {
|
||||||
|
if (names.isEmpty()) return@withContext
|
||||||
|
val rules = dao.getByNames(names)
|
||||||
|
dao.delete(*rules.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun moveOrder(rules: List<DictRule>) = withContext(Dispatchers.IO) {
|
||||||
|
val updatedRules = rules.mapIndexed { index, rule ->
|
||||||
|
rule.copy(sortNumber = index + 1)
|
||||||
|
}
|
||||||
|
dao.update(*updatedRules.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,62 +1,29 @@
|
|||||||
package io.legado.app.ui.dict.rule
|
package io.legado.app.ui.dict.rule
|
||||||
|
|
||||||
|
//import io.legado.app.lib.theme.primaryColor
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.VMBaseActivity
|
import io.legado.app.base.BaseComposeActivity
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.DictRule
|
|
||||||
import io.legado.app.databinding.ActivityDictRuleBinding
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
import io.legado.app.databinding.DialogEditTextBinding
|
||||||
import io.legado.app.help.DirectLinkUpload
|
import io.legado.app.help.DirectLinkUpload
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.ui.association.ImportDictRuleDialog
|
import io.legado.app.ui.association.ImportDictRuleDialog
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
import io.legado.app.ui.file.HandleFileContract
|
||||||
import io.legado.app.ui.qrcode.QrCodeResult
|
import io.legado.app.ui.theme.AppTheme
|
||||||
import io.legado.app.ui.widget.SelectActionBar
|
|
||||||
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
|
|
||||||
import io.legado.app.ui.widget.recycler.ItemTouchCallback
|
|
||||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
|
||||||
import io.legado.app.utils.ACache
|
import io.legado.app.utils.ACache
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
import io.legado.app.utils.launch
|
|
||||||
import io.legado.app.utils.readText
|
import io.legado.app.utils.readText
|
||||||
import io.legado.app.utils.sendToClip
|
import io.legado.app.utils.sendToClip
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
import io.legado.app.utils.showHelp
|
|
||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewModel>(),
|
class DictRuleActivity : BaseComposeActivity() {
|
||||||
PopupMenu.OnMenuItemClickListener,
|
|
||||||
SelectActionBar.CallBack,
|
|
||||||
DictRuleAdapter.CallBack {
|
|
||||||
|
|
||||||
override val viewModel by viewModels<DictRuleViewModel>()
|
|
||||||
override val binding by viewBinding(ActivityDictRuleBinding::inflate)
|
|
||||||
private val importRecordKey = "dictRuleUrls"
|
private val importRecordKey = "dictRuleUrls"
|
||||||
private val adapter by lazy { DictRuleAdapter(this, this) }
|
|
||||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
|
||||||
it ?: return@registerForActivityResult
|
|
||||||
showDialogFragment(
|
|
||||||
ImportDictRuleDialog(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
private val importDoc = registerForActivityResult(HandleFileContract()) {
|
private val importDoc = registerForActivityResult(HandleFileContract()) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
it.uri?.readText(this)?.let {
|
it.uri?.readText(this)?.let {
|
||||||
@@ -88,134 +55,17 @@ class DictRuleActivity : VMBaseActivity<ActivityDictRuleBinding, DictRuleViewMod
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
initRecyclerView()
|
|
||||||
initSelectActionView()
|
//observeDictRuleData()
|
||||||
observeDictRuleData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
@Composable
|
||||||
menuInflater.inflate(R.menu.dict_rule, menu)
|
override fun Content() {
|
||||||
return super.onCompatCreateOptionsMenu(menu)
|
AppTheme {
|
||||||
}
|
DictRuleScreen(onBackClick = { finish() })
|
||||||
|
|
||||||
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 initSelectActionView() {
|
|
||||||
binding.selectActionBar.setMainActionText(R.string.delete)
|
|
||||||
binding.selectActionBar.inflateMenu(R.menu.dict_rule_sel)
|
|
||||||
binding.selectActionBar.setOnMenuItemClickListener(this)
|
|
||||||
binding.selectActionBar.setCallBack(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeDictRuleData() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
appDb.dictRuleDao.flowAll().catch {
|
|
||||||
AppLog.put("字典规则获取数据失败\n${it.localizedMessage}", it)
|
|
||||||
}.flowOn(IO).collect {
|
|
||||||
adapter.setItems(it, adapter.diffItemCallBack)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_add -> showDialogFragment<DictRuleEditDialog>()
|
|
||||||
R.id.menu_import_local -> importDoc.launch {
|
|
||||||
mode = HandleFileContract.FILE
|
|
||||||
allowExtensions = arrayOf("txt", "json")
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_import_onLine -> showImportDialog()
|
|
||||||
R.id.menu_import_qr -> qrCodeResult.launch()
|
|
||||||
R.id.menu_import_default -> viewModel.importDefault()
|
|
||||||
R.id.menu_help -> showHelp("dictRuleHelp")
|
|
||||||
}
|
|
||||||
return super.onCompatOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_enable_selection -> {
|
|
||||||
viewModel.enableSelection(*adapter.selection.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_disable_selection -> {
|
|
||||||
viewModel.disableSelection(*adapter.selection.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_export_selection -> exportResult.launch {
|
|
||||||
mode = HandleFileContract.EXPORT
|
|
||||||
fileData = HandleFileContract.FileData(
|
|
||||||
"exportDictRule.json",
|
|
||||||
GSON.toJson(adapter.selection).toByteArray(),
|
|
||||||
"application/json"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClickSelectBarMainAction() {
|
|
||||||
viewModel.delete(*adapter.selection.toTypedArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun selectAll(selectAll: Boolean) {
|
|
||||||
if (selectAll) {
|
|
||||||
adapter.selectAll()
|
|
||||||
} else {
|
|
||||||
adapter.revertSelection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun revertSelection() {
|
|
||||||
adapter.revertSelection()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun update(vararg rule: DictRule) {
|
|
||||||
viewModel.update(*rule)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun delete(rule: DictRule) {
|
|
||||||
alert(R.string.draw) {
|
|
||||||
setMessage(getString(R.string.sure_del) + "\n" + rule.name)
|
|
||||||
noButton()
|
|
||||||
yesButton {
|
|
||||||
viewModel.delete(rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun edit(rule: DictRule) {
|
|
||||||
showDialogFragment(DictRuleEditDialog(rule.name))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upOrder() {
|
|
||||||
viewModel.upSortNumber()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upCountView() {
|
|
||||||
binding.selectActionBar.upCountView(
|
|
||||||
adapter.selection.size,
|
|
||||||
adapter.itemCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun showImportDialog() {
|
private fun showImportDialog() {
|
||||||
val aCache = ACache.get(cacheDir = false)
|
val aCache = ACache.get(cacheDir = false)
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
package io.legado.app.ui.dict.rule
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.DictRule
|
|
||||||
import io.legado.app.databinding.ItemDictRuleBinding
|
|
||||||
//import io.legado.app.lib.theme.backgroundColor
|
|
||||||
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
|
|
||||||
import io.legado.app.ui.widget.recycler.ItemTouchCallback
|
|
||||||
|
|
||||||
|
|
||||||
class DictRuleAdapter(context: Context, var callBack: CallBack) :
|
|
||||||
RecyclerAdapter<DictRule, ItemDictRuleBinding>(context),
|
|
||||||
ItemTouchCallback.Callback {
|
|
||||||
|
|
||||||
private val selected = linkedSetOf<DictRule>()
|
|
||||||
|
|
||||||
val selection: List<DictRule>
|
|
||||||
get() {
|
|
||||||
return getItems().filter {
|
|
||||||
selected.contains(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val diffItemCallBack = object : DiffUtil.ItemCallback<DictRule>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: DictRule, newItem: DictRule): Boolean {
|
|
||||||
return oldItem.name == newItem.name
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: DictRule, newItem: DictRule): Boolean {
|
|
||||||
if (oldItem.name != newItem.name) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if (oldItem.enabled != newItem.enabled) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: DictRule, newItem: DictRule): Any? {
|
|
||||||
val payload = Bundle()
|
|
||||||
if (oldItem.name != newItem.name) {
|
|
||||||
payload.putBoolean("upName", true)
|
|
||||||
}
|
|
||||||
if (oldItem.enabled != newItem.enabled) {
|
|
||||||
payload.putBoolean("enabled", newItem.enabled)
|
|
||||||
}
|
|
||||||
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): ItemDictRuleBinding {
|
|
||||||
return ItemDictRuleBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCurrentListChanged() {
|
|
||||||
callBack.upCountView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemDictRuleBinding,
|
|
||||||
item: DictRule,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
//root.setBackgroundColor(ColorUtils.withAlpha(context.backgroundColor, 0.5f))
|
|
||||||
cbName.text = item.name
|
|
||||||
swtEnabled.isChecked = item.enabled
|
|
||||||
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.name
|
|
||||||
"enabled" -> swtEnabled.isChecked = item.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemDictRuleBinding) {
|
|
||||||
binding.apply {
|
|
||||||
swtEnabled.setOnCheckedChangeListener { buttonView, isChecked ->
|
|
||||||
if (buttonView.isPressed) {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
it.enabled = isChecked
|
|
||||||
callBack.update(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cbName.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
if (cbName.isChecked) {
|
|
||||||
selected.add(it)
|
|
||||||
} else {
|
|
||||||
selected.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callBack.upCountView()
|
|
||||||
}
|
|
||||||
ivEdit.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.edit(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ivDelete.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.delete(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun swap(srcPosition: Int, targetPosition: Int): Boolean {
|
|
||||||
val srcItem = getItem(srcPosition)
|
|
||||||
val targetItem = getItem(targetPosition)
|
|
||||||
if (srcItem != null && targetItem != null) {
|
|
||||||
if (srcItem.sortNumber == targetItem.sortNumber) {
|
|
||||||
callBack.upOrder()
|
|
||||||
} else {
|
|
||||||
val srcOrder = srcItem.sortNumber
|
|
||||||
srcItem.sortNumber = targetItem.sortNumber
|
|
||||||
targetItem.sortNumber = srcOrder
|
|
||||||
movedItems.add(srcItem)
|
|
||||||
movedItems.add(targetItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
swapItem(srcPosition, targetPosition)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val movedItems = linkedSetOf<DictRule>()
|
|
||||||
|
|
||||||
override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
|
||||||
if (movedItems.isNotEmpty()) {
|
|
||||||
callBack.update(*movedItems.toTypedArray())
|
|
||||||
movedItems.clear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dragSelectCallback: DragSelectTouchHelper.Callback =
|
|
||||||
object : DragSelectTouchHelper.AdvanceCallback<DictRule>(Mode.ToggleAndReverse) {
|
|
||||||
override fun currentSelectedId(): MutableSet<DictRule> {
|
|
||||||
return selected
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): DictRule {
|
|
||||||
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: DictRule)
|
|
||||||
fun delete(rule: DictRule)
|
|
||||||
fun edit(rule: DictRule)
|
|
||||||
fun upOrder()
|
|
||||||
fun upCountView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
package io.legado.app.ui.dict.rule
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
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.viewModels
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.base.BaseViewModel
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.DictRule
|
|
||||||
import io.legado.app.databinding.DialogDictRuleEditBinding
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.utils.*
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class DictRuleEditDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict_rule_edit),
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
val viewModel by viewModels<DictRuleEditViewModel>()
|
|
||||||
val binding by viewBinding(DialogDictRuleEditBinding::bind)
|
|
||||||
|
|
||||||
constructor(name: String) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString("name", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
|
||||||
binding.toolBar.inflateMenu(R.menu.dict_rule_edit)
|
|
||||||
//binding.toolBar.menu.applyTint(requireContext())
|
|
||||||
binding.toolBar.setOnMenuItemClickListener(this)
|
|
||||||
viewModel.initData(arguments?.getString("name")) {
|
|
||||||
upRuleView(viewModel.dictRule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_save -> viewModel.save(getDictRule()) {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
R.id.menu_copy_rule -> viewModel.copyRule(getDictRule())
|
|
||||||
R.id.menu_paste_rule -> viewModel.pasteRule {
|
|
||||||
upRuleView(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRuleView(dictRule: DictRule?) {
|
|
||||||
binding.tvRuleName.setText(dictRule?.name)
|
|
||||||
binding.tvUrlRule.setText(dictRule?.urlRule)
|
|
||||||
binding.tvShowRule.setText(dictRule?.showRule)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDictRule(): DictRule {
|
|
||||||
val dictRule = viewModel.dictRule?.copy() ?: DictRule()
|
|
||||||
dictRule.name = binding.tvRuleName.text.toString()
|
|
||||||
dictRule.urlRule = binding.tvUrlRule.text.toString()
|
|
||||||
dictRule.showRule = binding.tvShowRule.text.toString()
|
|
||||||
return dictRule
|
|
||||||
}
|
|
||||||
|
|
||||||
class DictRuleEditViewModel(application: Application) : BaseViewModel(application) {
|
|
||||||
|
|
||||||
var dictRule: DictRule? = null
|
|
||||||
|
|
||||||
fun initData(name: String?, onFinally: () -> Unit) {
|
|
||||||
execute {
|
|
||||||
if (dictRule == null && name != null) {
|
|
||||||
dictRule = appDb.dictRuleDao.getByName(name)
|
|
||||||
}
|
|
||||||
}.onFinally {
|
|
||||||
onFinally.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(newDictRule: DictRule, onFinally: () -> Unit) {
|
|
||||||
execute {
|
|
||||||
dictRule?.let {
|
|
||||||
appDb.dictRuleDao.delete(it)
|
|
||||||
}
|
|
||||||
appDb.dictRuleDao.insert(newDictRule)
|
|
||||||
dictRule = newDictRule
|
|
||||||
}.onFinally {
|
|
||||||
onFinally.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun copyRule(dictRule: DictRule) {
|
|
||||||
context.sendToClip(GSON.toJson(dictRule))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pasteRule(success: (DictRule) -> Unit) {
|
|
||||||
val text = context.getClipText()
|
|
||||||
if (text.isNullOrBlank()) {
|
|
||||||
context.toastOnUi("剪贴板没有内容")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
execute {
|
|
||||||
GSON.fromJsonObject<DictRule>(text).getOrThrow()
|
|
||||||
}.onSuccess {
|
|
||||||
success.invoke(it)
|
|
||||||
}.onError {
|
|
||||||
context.toastOnUi("格式不对")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
package io.legado.app.ui.dict.rule
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.NoteAdd
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.ContentPaste
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.DictRule
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DictRuleEditSheet(
|
||||||
|
rule: DictRule?,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onSave: (DictRule) -> Unit,
|
||||||
|
onCopy: (DictRule) -> Unit,
|
||||||
|
onPaste: () -> DictRule?
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
|
||||||
|
var name by remember(rule) { mutableStateOf(rule?.name ?: "") }
|
||||||
|
var urlRule by remember(rule) { mutableStateOf(rule?.urlRule ?: "") }
|
||||||
|
var showRule by remember(rule) { mutableStateOf(rule?.showRule ?: "") }
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.dict_rule)) },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onDismissRequest) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.cancel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { showMenu = true }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false }
|
||||||
|
) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.copy_rule)) },
|
||||||
|
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
|
||||||
|
onClick = {
|
||||||
|
onCopy(
|
||||||
|
DictRule(
|
||||||
|
name,
|
||||||
|
urlRule,
|
||||||
|
showRule,
|
||||||
|
enabled = rule?.enabled ?: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.paste_rule)) },
|
||||||
|
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
onPaste()?.let {
|
||||||
|
name = it.name
|
||||||
|
urlRule = it.urlRule
|
||||||
|
showRule = it.showRule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showMenu = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 96.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = name,
|
||||||
|
onValueChange = { name = it },
|
||||||
|
label = { Text(stringResource(R.string.name)) },
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = urlRule,
|
||||||
|
onValueChange = { urlRule = it },
|
||||||
|
label = { Text(stringResource(R.string.url_rule)) }
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = showRule,
|
||||||
|
onValueChange = { showRule = it },
|
||||||
|
label = { Text(stringResource(R.string.show_rule)) },
|
||||||
|
minLines = 3
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
onSave(
|
||||||
|
rule?.copy(name = name, urlRule = urlRule, showRule = showRule)
|
||||||
|
?: DictRule(name = name, urlRule = urlRule, showRule = showRule)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(16.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Save, contentDescription = "Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
package io.legado.app.ui.dict.rule
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingToolbarDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MediumFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.PlainTooltip
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
|
import androidx.compose.material3.rememberTooltipState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.DictRule
|
||||||
|
import io.legado.app.ui.widget.components.ActionItem
|
||||||
|
import io.legado.app.ui.widget.components.AnimatedText
|
||||||
|
import io.legado.app.ui.widget.components.DraggableSelectionHandler
|
||||||
|
import io.legado.app.ui.widget.components.ReorderableSelectionItem
|
||||||
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
|
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun DictRuleScreen(
|
||||||
|
viewModel: DictRuleViewModel = viewModel(),
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
var isSearch by remember { mutableStateOf(false) }
|
||||||
|
val selectedIds by viewModel.selectedIds.collectAsState()
|
||||||
|
val inSelectionMode = selectedIds.isNotEmpty()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
var showDeleteSelectedDialog by remember { mutableStateOf(false) }
|
||||||
|
var showEditSheet by remember { mutableStateOf(false) }
|
||||||
|
var editingRule by remember { mutableStateOf<DictRule?>(null) }
|
||||||
|
var showDeleteRuleDialog by remember { mutableStateOf<DictRule?>(null) }
|
||||||
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
|
viewModel.moveItemInList(from.index, to.index)
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(reorderableState.isAnyItemDragging) {
|
||||||
|
if (!reorderableState.isAnyItemDragging) {
|
||||||
|
viewModel.saveSortOrder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDeleteRuleDialog != null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteRuleDialog = null },
|
||||||
|
title = { Text(stringResource(R.string.delete)) },
|
||||||
|
text = { Text(stringResource(R.string.sure_del) + showDeleteRuleDialog!!.name) },
|
||||||
|
confirmButton = {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.delete(showDeleteRuleDialog!!)
|
||||||
|
showDeleteRuleDialog = null
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteRuleDialog = null }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDeleteSelectedDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDeleteSelectedDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.delete)) },
|
||||||
|
text = { Text(stringResource(R.string.del_msg)) },
|
||||||
|
confirmButton = {
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.delSelectionByIds(selectedIds)
|
||||||
|
viewModel.setSelection(emptySet())
|
||||||
|
showDeleteSelectedDialog = false
|
||||||
|
},
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.error,
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDeleteSelectedDialog = false }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showEditSheet) {
|
||||||
|
DictRuleEditSheet(
|
||||||
|
rule = editingRule,
|
||||||
|
onDismissRequest = { showEditSheet = false },
|
||||||
|
onSave = {
|
||||||
|
if (editingRule == null) {
|
||||||
|
viewModel.insert(it)
|
||||||
|
} else {
|
||||||
|
viewModel.update(it)
|
||||||
|
}
|
||||||
|
showEditSheet = false
|
||||||
|
},
|
||||||
|
onCopy = { viewModel.copyRule(it) },
|
||||||
|
onPaste = { viewModel.pasteRule() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
Column {
|
||||||
|
MediumFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
val titleText = remember(inSelectionMode, selectedIds, uiState.items) {
|
||||||
|
when {
|
||||||
|
inSelectionMode -> "已选择 ${selectedIds.size}/${uiState.items.size}"
|
||||||
|
else -> "字典规则"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AnimatedText(
|
||||||
|
text = titleText
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (inSelectionMode) {
|
||||||
|
viewModel.setSelection(emptySet())
|
||||||
|
} else {
|
||||||
|
onBackClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (inSelectionMode) Icons.Default.Close else Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = if (inSelectionMode) "Cancel" else "Back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
if (!inSelectionMode) {
|
||||||
|
IconButton(onClick = { isSearch = !isSearch }) {
|
||||||
|
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = isSearch && !inSelectionMode) {
|
||||||
|
SearchBarSection(
|
||||||
|
query = uiState.searchKey ?: "",
|
||||||
|
onQueryChange = {
|
||||||
|
viewModel.setSearchKey(it)
|
||||||
|
},
|
||||||
|
placeholder = stringResource(id = R.string.search)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider =
|
||||||
|
TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||||
|
tooltip = { PlainTooltip { Text("Localized description") } },
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
|
visible = !inSelectionMode,
|
||||||
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
editingRule = null
|
||||||
|
showEditSheet = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Add Rule")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
) {
|
||||||
|
FastScrollLazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 120.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(uiState.items, key = { it.name }) { item ->
|
||||||
|
ReorderableSelectionItem(
|
||||||
|
state = reorderableState,
|
||||||
|
key = item.name,
|
||||||
|
title = item.name,
|
||||||
|
isEnabled = item.isEnabled,
|
||||||
|
isSelected = selectedIds.contains(item.name),
|
||||||
|
inSelectionMode = inSelectionMode,
|
||||||
|
onToggleSelection = {
|
||||||
|
viewModel.toggleSelection(item.name)
|
||||||
|
},
|
||||||
|
onEnabledChange = { enabled ->
|
||||||
|
viewModel.update(item.rule.copy(enabled = enabled))
|
||||||
|
},
|
||||||
|
onClickEdit = {
|
||||||
|
editingRule = item.rule
|
||||||
|
showEditSheet = true
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
|
trailingAction = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
showDeleteRuleDialog = item.rule
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Delete, contentDescription = "Delete")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inSelectionMode) {
|
||||||
|
DraggableSelectionHandler(
|
||||||
|
listState = listState,
|
||||||
|
items = uiState.items,
|
||||||
|
selectedIds = selectedIds,
|
||||||
|
onSelectionChange = viewModel::setSelection,
|
||||||
|
idProvider = { it.name },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(60.dp)
|
||||||
|
.align(Alignment.TopStart)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = inSelectionMode,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.offset(y = -FloatingToolbarDefaults.ScreenOffset)
|
||||||
|
.zIndex(1f),
|
||||||
|
enter = slideInVertically { it } + fadeIn(),
|
||||||
|
exit = slideOutVertically { it } + fadeOut()
|
||||||
|
) {
|
||||||
|
SelectionBottomBar(
|
||||||
|
onSelectAll = {
|
||||||
|
viewModel.setSelection(uiState.items.map { it.name }.toSet())
|
||||||
|
},
|
||||||
|
onSelectInvert = {
|
||||||
|
val allIds = uiState.items.map { it.name }.toSet()
|
||||||
|
viewModel.setSelection(allIds - selectedIds)
|
||||||
|
},
|
||||||
|
primaryAction = ActionItem(
|
||||||
|
text = stringResource(R.string.delete),
|
||||||
|
icon = { Icon(Icons.Default.Delete, null) },
|
||||||
|
onClick = { showDeleteSelectedDialog = true }
|
||||||
|
),
|
||||||
|
secondaryActions = listOf(
|
||||||
|
ActionItem(
|
||||||
|
text = stringResource(R.string.enable),
|
||||||
|
onClick = {
|
||||||
|
viewModel.enableSelectionByIds(selectedIds)
|
||||||
|
viewModel.setSelection(emptySet())
|
||||||
|
}
|
||||||
|
),
|
||||||
|
ActionItem(
|
||||||
|
text = stringResource(R.string.disable_selection),
|
||||||
|
onClick = {
|
||||||
|
viewModel.disableSelectionByIds(selectedIds)
|
||||||
|
viewModel.setSelection(emptySet())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,57 +1,184 @@
|
|||||||
package io.legado.app.ui.dict.rule
|
package io.legado.app.ui.dict.rule
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
|
import io.legado.app.data.repository.DictRuleRepository
|
||||||
import io.legado.app.help.DefaultData
|
import io.legado.app.help.DefaultData
|
||||||
|
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 io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class DictRuleItemUi(
|
||||||
|
val name: String,
|
||||||
|
val urlRule: String,
|
||||||
|
val showRule: String,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val rule: DictRule
|
||||||
|
)
|
||||||
|
|
||||||
|
data class DictRuleUiState(
|
||||||
|
val searchKey: String? = null,
|
||||||
|
val items: List<DictRuleItemUi> = emptyList(),
|
||||||
|
val dictRule: List<DictRule> = emptyList(),
|
||||||
|
val selectedIds: Set<String> = emptySet(),
|
||||||
|
val isLoading: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
class DictRuleViewModel(application: Application) : BaseViewModel(application) {
|
class DictRuleViewModel(application: Application) : BaseViewModel(application) {
|
||||||
|
|
||||||
|
private val repository = DictRuleRepository()
|
||||||
|
private val _searchKey = MutableStateFlow<String?>(null)
|
||||||
|
private val _uiRules = MutableStateFlow<List<DictRuleItemUi>>(emptyList())
|
||||||
|
|
||||||
fun update(vararg dictRule: DictRule) {
|
private val _selectedIds = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
val selectedIds: StateFlow<Set<String>> = _selectedIds.asStateFlow()
|
||||||
|
|
||||||
|
fun toggleSelection(id: String) {
|
||||||
|
_selectedIds.update {
|
||||||
|
if (it.contains(id)) it - id else it + id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val rulesFlow = _searchKey.flatMapLatest { searchKey ->
|
||||||
|
val baseFlow = if (searchKey.isNullOrEmpty()) {
|
||||||
|
repository.flowAll()
|
||||||
|
} else {
|
||||||
|
repository.flowSearch("%$searchKey%")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseFlow.map { rules ->
|
||||||
|
rules.sortedBy { it.sortNumber }
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
private val ruleUiFlow: Flow<List<DictRuleItemUi>> =
|
||||||
|
rulesFlow.map { rules ->
|
||||||
|
rules.map { rule ->
|
||||||
|
DictRuleItemUi(
|
||||||
|
name = rule.name,
|
||||||
|
urlRule = rule.urlRule,
|
||||||
|
showRule = rule.showRule,
|
||||||
|
isEnabled = rule.enabled,
|
||||||
|
rule = rule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uiState: StateFlow<DictRuleUiState> = combine(
|
||||||
|
_searchKey,
|
||||||
|
_uiRules,
|
||||||
|
_selectedIds
|
||||||
|
) { searchKey, rules, selectedIds ->
|
||||||
|
DictRuleUiState(
|
||||||
|
searchKey = searchKey,
|
||||||
|
items = rules,
|
||||||
|
selectedIds = selectedIds,
|
||||||
|
isLoading = false
|
||||||
|
)
|
||||||
|
}.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = DictRuleUiState(isLoading = true)
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
ruleUiFlow.collect { rules ->
|
||||||
|
_uiRules.value = rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchKey(key: String?) {
|
||||||
|
_searchKey.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSelection(ids: Set<String>) {
|
||||||
|
_selectedIds.value = ids
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun enableSelectionByIds(ids: Set<String>) {
|
||||||
execute {
|
execute {
|
||||||
appDb.dictRuleDao.update(*dictRule)
|
repository.enableByIds(ids)
|
||||||
}.onError {
|
}
|
||||||
val msg = "更新字典规则出错\n${it.localizedMessage}"
|
}
|
||||||
AppLog.put(msg, it)
|
|
||||||
context.toastOnUi(msg)
|
fun disableSelectionByIds(ids: Set<String>) {
|
||||||
|
execute {
|
||||||
|
repository.disableByIds(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delSelectionByIds(ids: Set<String>) {
|
||||||
|
execute {
|
||||||
|
repository.deleteByIds(ids)
|
||||||
|
_selectedIds.update { it - ids }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(vararg rule: DictRule) {
|
||||||
|
execute {
|
||||||
|
repository.update(*rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun insert(vararg rule: DictRule) {
|
||||||
|
execute {
|
||||||
|
repository.insert(*rule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveItemInList(fromIndex: Int, toIndex: Int) {
|
||||||
|
_uiRules.update { currentList ->
|
||||||
|
val list = currentList.toMutableList()
|
||||||
|
val item = list.removeAt(fromIndex)
|
||||||
|
list.add(toIndex, item)
|
||||||
|
list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveSortOrder() {
|
||||||
|
val currentRules = _uiRules.value
|
||||||
|
execute {
|
||||||
|
repository.moveOrder(currentRules.map { it.rule })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(vararg dictRule: DictRule) {
|
fun delete(vararg dictRule: DictRule) {
|
||||||
execute {
|
execute {
|
||||||
appDb.dictRuleDao.delete(*dictRule)
|
repository.delete(*dictRule)
|
||||||
}.onError {
|
|
||||||
val msg = "删除字典规则出错\n${it.localizedMessage}"
|
|
||||||
AppLog.put(msg, it)
|
|
||||||
context.toastOnUi(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upSortNumber() {
|
fun upSortNumber() {
|
||||||
execute {
|
execute {
|
||||||
val rules = appDb.dictRuleDao.all
|
val rules = repository.getAll()
|
||||||
for ((index, rule) in rules.withIndex()) {
|
for ((index, rule) in rules.withIndex()) {
|
||||||
rule.sortNumber = index + 1
|
rule.sortNumber = index + 1
|
||||||
}
|
}
|
||||||
appDb.dictRuleDao.insert(*rules.toTypedArray())
|
repository.insert(*rules.toTypedArray())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableSelection(vararg dictRule: DictRule) {
|
|
||||||
execute {
|
|
||||||
val array = dictRule.map { it.copy(enabled = true) }.toTypedArray()
|
|
||||||
appDb.dictRuleDao.insert(*array)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableSelection(vararg dictRule: DictRule) {
|
|
||||||
execute {
|
|
||||||
val array = dictRule.map { it.copy(enabled = false) }.toTypedArray()
|
|
||||||
appDb.dictRuleDao.insert(*array)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +188,22 @@ class DictRuleViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
fun copyRule(dictRule: DictRule) {
|
||||||
|
context.sendToClip(GSON.toJson(dictRule))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pasteRule(): DictRule? {
|
||||||
|
val text = context.getClipText()
|
||||||
|
if (text.isNullOrBlank()) {
|
||||||
|
context.toastOnUi("剪贴板没有内容")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
GSON.fromJsonObject<DictRule>(text).getOrThrow()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
context.toastOnUi("格式不对")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package io.legado.app.ui.replace
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlobalModalBottomSheet
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun GroupManageBottomSheet(
|
||||||
|
groups: List<String>,
|
||||||
|
sheetState: SheetState,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: ReplaceRuleViewModel
|
||||||
|
) {
|
||||||
|
var editingGroup by remember { mutableStateOf<String?>(null) }
|
||||||
|
var updatedGroupName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
GlobalModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
|
text = stringResource(R.string.group_manage),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
items(groups) { group ->
|
||||||
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
if (editingGroup == group) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = updatedGroupName,
|
||||||
|
onValueChange = { updatedGroupName = it },
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
viewModel.upGroup(group, updatedGroupName)
|
||||||
|
editingGroup = null
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(id = R.string.ok)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(group) },
|
||||||
|
trailingContent = {
|
||||||
|
Row {
|
||||||
|
IconButton(onClick = {
|
||||||
|
editingGroup = group
|
||||||
|
updatedGroupName = group
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Edit,
|
||||||
|
contentDescription = stringResource(id = R.string.edit)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = { viewModel.delGroup(group) }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(id = R.string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package io.legado.app.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.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
|
||||||
import io.legado.app.databinding.ItemReplaceRuleBinding
|
|
||||||
//import io.legado.app.lib.theme.backgroundColor
|
|
||||||
import io.legado.app.ui.widget.recycler.DragSelectTouchHelper
|
|
||||||
import io.legado.app.ui.widget.recycler.ItemTouchCallback
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.backgroundColor
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
if (oldItem.order != newItem.order) return false // 添加 order 检查
|
|
||||||
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 (oldItem.order != newItem.order) {
|
|
||||||
payload.putBoolean("orderChanged", true)
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
var needUpdatePin = payloads.isEmpty()
|
|
||||||
|
|
||||||
if (payloads.isNotEmpty()) {
|
|
||||||
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
|
|
||||||
"orderChanged" -> needUpdatePin = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payloads.isEmpty() || needUpdatePin) {
|
|
||||||
when (item.order) {
|
|
||||||
-1 -> {
|
|
||||||
ivPin.visible()
|
|
||||||
ivPin.setImageResource(R.drawable.ic_praise_filled)
|
|
||||||
ivPin.rotation = 0f
|
|
||||||
ivPin.setColorFilter(context.themeColor(com.google.android.material.R.attr.colorSecondary))
|
|
||||||
}
|
|
||||||
-2 -> {
|
|
||||||
ivPin.visible()
|
|
||||||
ivPin.setImageResource(R.drawable.ic_praise_filled)
|
|
||||||
ivPin.rotation = 180f
|
|
||||||
ivPin.setColorFilter(context.themeColor(com.google.android.material.R.attr.colorSecondary))
|
|
||||||
}
|
|
||||||
else -> ivPin.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cbName.text = item.getDisplayNameGroup()
|
|
||||||
swtEnabled.isChecked = item.isEnabled
|
|
||||||
cbName.isChecked = selected.contains(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,52 +3,33 @@ package io.legado.app.ui.replace
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.slideInVertically
|
import androidx.compose.animation.slideInVertically
|
||||||
import androidx.compose.animation.slideOutVertically
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Edit
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -58,21 +39,16 @@ import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
|
|||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.ListItemDefaults
|
|
||||||
import androidx.compose.material3.LoadingIndicator
|
import androidx.compose.material3.LoadingIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MediumFlexibleTopAppBar
|
import androidx.compose.material3.MediumFlexibleTopAppBar
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.PlainTooltip
|
import androidx.compose.material3.PlainTooltip
|
||||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SheetState
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -91,16 +67,12 @@ import androidx.compose.runtime.mutableIntStateOf
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.shadow
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.ClipEntry
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -118,7 +90,9 @@ import io.legado.app.data.repository.UploadRepository
|
|||||||
import io.legado.app.ui.replace.edit.ReplaceEditActivity
|
import io.legado.app.ui.replace.edit.ReplaceEditActivity
|
||||||
import io.legado.app.ui.widget.components.ActionItem
|
import io.legado.app.ui.widget.components.ActionItem
|
||||||
import io.legado.app.ui.widget.components.AnimatedText
|
import io.legado.app.ui.widget.components.AnimatedText
|
||||||
|
import io.legado.app.ui.widget.components.DraggableSelectionHandler
|
||||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||||
|
import io.legado.app.ui.widget.components.ReorderableSelectionItem
|
||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
|
import io.legado.app.ui.widget.components.exportComponents.FilePickerSheet
|
||||||
@@ -127,12 +101,9 @@ import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
|||||||
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
import io.legado.app.ui.widget.components.importComponents.BatchImportDialog
|
||||||
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||||
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.GlobalModalBottomSheet
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||||
@@ -148,7 +119,6 @@ fun ReplaceRuleScreen(
|
|||||||
|
|
||||||
//TODO: 期望换为Navigation
|
//TODO: 期望换为Navigation
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val haptic = LocalHapticFeedback.current
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
@@ -585,71 +555,53 @@ fun ReplaceRuleScreen(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
items(rules, key = { it.id }) { ui ->
|
items(rules, key = { it.id }) { ui ->
|
||||||
val isSelected = selectedRuleIds.contains(ui.id)
|
ReorderableSelectionItem(
|
||||||
ReorderableItem(
|
|
||||||
state = reorderableState,
|
state = reorderableState,
|
||||||
key = ui.id
|
key = ui.id,
|
||||||
) { isDragging ->
|
title = ui.name,
|
||||||
|
isEnabled = ui.isEnabled,
|
||||||
val elevation by animateDpAsState(if (isDragging) 8.dp else 0.dp)
|
isSelected = selectedRuleIds.contains(ui.id),
|
||||||
ReplaceRuleItem(
|
inSelectionMode = inSelectionMode,
|
||||||
modifier = Modifier
|
canReorder = canReorder,
|
||||||
.padding(horizontal = 12.dp)
|
onToggleSelection = {
|
||||||
.zIndex(if (isDragging) 1f else 0f)
|
viewModel.toggleSelection(ui.id)
|
||||||
.shadow(
|
},
|
||||||
elevation = elevation,
|
onEnabledChange = { enabled ->
|
||||||
shape = MaterialTheme.shapes.medium,
|
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
||||||
clip = false
|
},
|
||||||
|
onClickEdit = {
|
||||||
|
context.startActivity(
|
||||||
|
ReplaceEditActivity.startIntent(
|
||||||
|
context,
|
||||||
|
ui.id
|
||||||
)
|
)
|
||||||
.then(
|
)
|
||||||
if (canReorder) {
|
},
|
||||||
Modifier.longPressDraggableHandle(
|
modifier = Modifier.padding(horizontal = 12.dp),
|
||||||
onDragStarted = {
|
dropdownContent = { dismiss ->
|
||||||
hapticFeedback.performHapticFeedback(
|
DropdownMenuItem(
|
||||||
HapticFeedbackType.GestureThresholdActivate
|
text = { Text("移至顶部") },
|
||||||
)
|
onClick = { viewModel.toTop(ui.rule); dismiss() }
|
||||||
},
|
)
|
||||||
onDragStopped = {
|
DropdownMenuItem(
|
||||||
hapticFeedback.performHapticFeedback(
|
text = { Text("移至底部") },
|
||||||
HapticFeedbackType.GestureEnd
|
onClick = { viewModel.toBottom(ui.rule); dismiss() }
|
||||||
)
|
)
|
||||||
},
|
DropdownMenuItem(
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
text = { Text("删除") },
|
||||||
)
|
onClick = { showDeleteRuleDialog = ui.rule; dismiss() }
|
||||||
} else {
|
)
|
||||||
Modifier
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
.animateItem(),
|
|
||||||
name = ui.name,
|
|
||||||
isEnabled = ui.isEnabled,
|
|
||||||
isSelected = isSelected,
|
|
||||||
inSelectionMode = inSelectionMode,
|
|
||||||
onEnabledChange = { enabled ->
|
|
||||||
viewModel.update(ui.rule.copy(isEnabled = enabled))
|
|
||||||
},
|
|
||||||
onDelete = { showDeleteRuleDialog = ui.rule },
|
|
||||||
onToTop = { viewModel.toTop(ui.rule) },
|
|
||||||
onToBottom = { viewModel.toBottom(ui.rule) },
|
|
||||||
onToggleSelection = {
|
|
||||||
viewModel.toggleSelection(ui.id)
|
|
||||||
},
|
|
||||||
onClickEdit = {
|
|
||||||
context.startActivity(
|
|
||||||
ReplaceEditActivity.startIntent(context, ui.id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (inSelectionMode) {
|
if (inSelectionMode) {
|
||||||
DraggableSelectionHandler(
|
DraggableSelectionHandler(
|
||||||
listState = listState,
|
listState = listState,
|
||||||
rules = rules,
|
items = rules,
|
||||||
selectedRuleIds = selectedRuleIds,
|
selectedIds = selectedRuleIds,
|
||||||
onSelectionChange = viewModel::setSelection,
|
onSelectionChange = viewModel::setSelection,
|
||||||
haptic = haptic,
|
idProvider = { it.id },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.width(60.dp)
|
.width(60.dp)
|
||||||
@@ -719,303 +671,3 @@ fun ReplaceRuleScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DraggableSelectionHandler(
|
|
||||||
listState: LazyListState,
|
|
||||||
rules: List<ReplaceRuleItemUi>,
|
|
||||||
selectedRuleIds: Set<Long>,
|
|
||||||
onSelectionChange: (Set<Long>) -> Unit,
|
|
||||||
haptic: HapticFeedback,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val latestSelectedRuleIds by rememberUpdatedState(selectedRuleIds)
|
|
||||||
var isAddingMode by remember { mutableStateOf(true) }
|
|
||||||
var lastProcessedIndex by remember { mutableIntStateOf(-1) }
|
|
||||||
|
|
||||||
fun findRuleAtOffset(offsetY: Float): Pair<Int, ReplaceRuleItemUi>? {
|
|
||||||
val itemInfo = listState.layoutInfo.visibleItemsInfo
|
|
||||||
.firstOrNull { item ->
|
|
||||||
offsetY >= item.offset && offsetY <= item.offset + item.size
|
|
||||||
}
|
|
||||||
|
|
||||||
return itemInfo?.let { info ->
|
|
||||||
rules.getOrNull(info.index)?.let { rule ->
|
|
||||||
info.index to rule
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applySelection(id: Long, add: Boolean) {
|
|
||||||
val current = latestSelectedRuleIds
|
|
||||||
onSelectionChange(
|
|
||||||
if (add) current + id else current - id
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = modifier.pointerInput(Unit) {
|
|
||||||
coroutineScope {
|
|
||||||
launch {
|
|
||||||
detectTapGestures(
|
|
||||||
onTap = { offset ->
|
|
||||||
val result = findRuleAtOffset(offset.y)
|
|
||||||
if (result != null) {
|
|
||||||
val (_, rule) = result
|
|
||||||
val id = rule.id
|
|
||||||
val current = latestSelectedRuleIds
|
|
||||||
onSelectionChange(
|
|
||||||
if (current.contains(id)) current - id
|
|
||||||
else current + id
|
|
||||||
)
|
|
||||||
haptic.performHapticFeedback(
|
|
||||||
HapticFeedbackType.TextHandleMove
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 拖拽多选
|
|
||||||
launch {
|
|
||||||
detectDragGestures(
|
|
||||||
onDragStart = { offset ->
|
|
||||||
val result = findRuleAtOffset(offset.y)
|
|
||||||
if (result != null) {
|
|
||||||
val (index, rule) = result
|
|
||||||
lastProcessedIndex = index
|
|
||||||
|
|
||||||
val id = rule.id
|
|
||||||
val current = latestSelectedRuleIds
|
|
||||||
isAddingMode = !current.contains(id)
|
|
||||||
applySelection(id, isAddingMode)
|
|
||||||
|
|
||||||
haptic.performHapticFeedback(
|
|
||||||
HapticFeedbackType.LongPress
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDrag = { change, _ ->
|
|
||||||
val result = findRuleAtOffset(change.position.y)
|
|
||||||
if (result != null) {
|
|
||||||
val (index, rule) = result
|
|
||||||
if (index != lastProcessedIndex) {
|
|
||||||
lastProcessedIndex = index
|
|
||||||
applySelection(rule.id, isAddingMode)
|
|
||||||
haptic.performHapticFeedback(
|
|
||||||
HapticFeedbackType.TextHandleMove
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
lastProcessedIndex = -1
|
|
||||||
},
|
|
||||||
onDragCancel = {
|
|
||||||
lastProcessedIndex = -1
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun GroupManageBottomSheet(
|
|
||||||
groups: List<String>,
|
|
||||||
sheetState: SheetState,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
viewModel: ReplaceRuleViewModel
|
|
||||||
) {
|
|
||||||
var editingGroup by remember { mutableStateOf<String?>(null) }
|
|
||||||
var updatedGroupName by remember { mutableStateOf("") }
|
|
||||||
|
|
||||||
GlobalModalBottomSheet(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(bottom = 16.dp),
|
|
||||||
text = stringResource(R.string.group_manage),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
items(groups) { group ->
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
if (editingGroup == group) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = updatedGroupName,
|
|
||||||
onValueChange = { updatedGroupName = it },
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
IconButton(onClick = {
|
|
||||||
viewModel.upGroup(group, updatedGroupName)
|
|
||||||
editingGroup = null
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Check,
|
|
||||||
contentDescription = stringResource(id = R.string.ok)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(group) },
|
|
||||||
trailingContent = {
|
|
||||||
Row {
|
|
||||||
IconButton(onClick = {
|
|
||||||
editingGroup = group
|
|
||||||
updatedGroupName = group
|
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Edit,
|
|
||||||
contentDescription = stringResource(id = R.string.edit)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { viewModel.delGroup(group) }) {
|
|
||||||
Icon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
contentDescription = stringResource(id = R.string.delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun ReplaceRuleItem(
|
|
||||||
name: String,
|
|
||||||
isEnabled: Boolean,
|
|
||||||
isSelected: Boolean,
|
|
||||||
inSelectionMode: Boolean,
|
|
||||||
onToggleSelection: () -> Unit,
|
|
||||||
onEnabledChange: (Boolean) -> Unit,
|
|
||||||
onDelete: () -> Unit,
|
|
||||||
onToTop: () -> Unit,
|
|
||||||
onToBottom: () -> Unit,
|
|
||||||
onClickEdit: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showRuleMenu by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val containerColor by animateColorAsState(
|
|
||||||
targetValue = if (isSelected)
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 200,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
),
|
|
||||||
label = "CardColor"
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
|
||||||
onClick = { onToggleSelection() },
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = containerColor
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.animateContentSize(),
|
|
||||||
headlineContent = {
|
|
||||||
AnimatedContent(targetState = name, label = "RuleNameAnimation") { targetName ->
|
|
||||||
Text(
|
|
||||||
text = targetName,
|
|
||||||
style = MaterialTheme.typography.titleMedium,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingContent = {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = inSelectionMode,
|
|
||||||
label = "LeadingCheckbox"
|
|
||||||
) { visible ->
|
|
||||||
if (visible) {
|
|
||||||
Checkbox(
|
|
||||||
checked = isSelected,
|
|
||||||
onCheckedChange = null
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Spacer(modifier = Modifier.width(0.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Switch(
|
|
||||||
checked = isEnabled,
|
|
||||||
onCheckedChange = onEnabledChange
|
|
||||||
)
|
|
||||||
IconButton(onClick = onClickEdit) {
|
|
||||||
Icon(Icons.Default.Edit, contentDescription = "Edit")
|
|
||||||
}
|
|
||||||
Box {
|
|
||||||
IconButton(onClick = { showRuleMenu = true }) {
|
|
||||||
Icon(Icons.Default.MoreVert, contentDescription = "More Actions")
|
|
||||||
}
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = showRuleMenu,
|
|
||||||
onDismissRequest = { showRuleMenu = false }
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("移至顶部") },
|
|
||||||
onClick = {
|
|
||||||
onToTop()
|
|
||||||
showRuleMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("移至底部") },
|
|
||||||
onClick = {
|
|
||||||
onToBottom()
|
|
||||||
showRuleMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text("删除") },
|
|
||||||
onClick = {
|
|
||||||
onDelete()
|
|
||||||
showRuleMenu = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
colors = ListItemDefaults.colors(
|
|
||||||
containerColor = Color.Transparent
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedback
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T, ID> DraggableSelectionHandler(
|
||||||
|
listState: LazyListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
items: List<T>,
|
||||||
|
selectedIds: Set<ID>,
|
||||||
|
onSelectionChange: (Set<ID>) -> Unit,
|
||||||
|
idProvider: (T) -> ID,
|
||||||
|
haptic: HapticFeedback = LocalHapticFeedback.current,
|
||||||
|
) {
|
||||||
|
val latestSelectedIds by rememberUpdatedState(selectedIds)
|
||||||
|
var isAddingMode by remember { mutableStateOf(true) }
|
||||||
|
var lastProcessedIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
|
||||||
|
fun findItemAtOffset(offsetY: Float): Pair<Int, T>? {
|
||||||
|
val itemInfo = listState.layoutInfo.visibleItemsInfo
|
||||||
|
.firstOrNull { item ->
|
||||||
|
offsetY >= item.offset && offsetY <= item.offset + item.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemInfo?.let { info ->
|
||||||
|
items.getOrNull(info.index)?.let { item ->
|
||||||
|
info.index to item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applySelection(id: ID, add: Boolean) {
|
||||||
|
val current = latestSelectedIds
|
||||||
|
onSelectionChange(
|
||||||
|
if (add) current + id else current - id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.pointerInput(Unit) {
|
||||||
|
coroutineScope {
|
||||||
|
launch {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { offset ->
|
||||||
|
findItemAtOffset(offset.y)?.let { (_, item) ->
|
||||||
|
val id = idProvider(item)
|
||||||
|
applySelection(id, !latestSelectedIds.contains(id))
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
launch {
|
||||||
|
detectDragGestures(
|
||||||
|
onDragStart = { offset ->
|
||||||
|
findItemAtOffset(offset.y)?.let { (index, item) ->
|
||||||
|
lastProcessedIndex = index
|
||||||
|
val id = idProvider(item)
|
||||||
|
isAddingMode = !latestSelectedIds.contains(id)
|
||||||
|
applySelection(id, isAddingMode)
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrag = { change, _ ->
|
||||||
|
findItemAtOffset(change.position.y)?.let { (index, item) ->
|
||||||
|
if (index != lastProcessedIndex) {
|
||||||
|
lastProcessedIndex = index
|
||||||
|
applySelection(idProvider(item), isAddingMode)
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = { lastProcessedIndex = -1 },
|
||||||
|
onDragCancel = { lastProcessedIndex = -1 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.ReorderableLazyListState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectionItemCard(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
subtitle: String? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
inSelectionMode: Boolean = false,
|
||||||
|
onToggleSelection: () -> Unit = {},
|
||||||
|
onEnabledChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onClickEdit: (() -> Unit)? = null,
|
||||||
|
trailingAction: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val containerColor by animateColorAsState(
|
||||||
|
targetValue = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
|
||||||
|
label = "CardColor"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
onClick = onToggleSelection,
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = CardDefaults.cardColors(containerColor = containerColor)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.animateContentSize(),
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = subtitle?.let {
|
||||||
|
{
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = inSelectionMode,
|
||||||
|
enter = fadeIn() + expandHorizontally(),
|
||||||
|
exit = fadeOut() + shrinkHorizontally()
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
onEnabledChange?.let {
|
||||||
|
Switch(
|
||||||
|
checked = isEnabled,
|
||||||
|
onCheckedChange = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onClickEdit != null) {
|
||||||
|
IconButton(onClick = onClickEdit) {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = "Edit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trailingAction != null) {
|
||||||
|
trailingAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropdownContent != null) {
|
||||||
|
Box {
|
||||||
|
IconButton(onClick = { showMenu = true }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false }
|
||||||
|
) {
|
||||||
|
dropdownContent { showMenu = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.ReorderableSelectionItem(
|
||||||
|
state: ReorderableLazyListState,
|
||||||
|
key: Any,
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
subtitle: String? = null,
|
||||||
|
isEnabled: Boolean = true,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
inSelectionMode: Boolean = false,
|
||||||
|
canReorder: Boolean = true,
|
||||||
|
onToggleSelection: () -> Unit = {},
|
||||||
|
onEnabledChange: ((Boolean) -> Unit)? = null,
|
||||||
|
onClickEdit: (() -> Unit)? = null,
|
||||||
|
trailingAction: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
|
||||||
|
ReorderableItem(state, key = key) { isDragging ->
|
||||||
|
val elevation by animateDpAsState(
|
||||||
|
targetValue = if (isDragging) 8.dp else 0.dp,
|
||||||
|
label = "DragElevation"
|
||||||
|
)
|
||||||
|
|
||||||
|
SelectionItemCard(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
isEnabled = isEnabled,
|
||||||
|
isSelected = isSelected,
|
||||||
|
inSelectionMode = inSelectionMode,
|
||||||
|
onToggleSelection = onToggleSelection,
|
||||||
|
onEnabledChange = onEnabledChange,
|
||||||
|
onClickEdit = onClickEdit,
|
||||||
|
trailingAction = trailingAction,
|
||||||
|
dropdownContent = dropdownContent,
|
||||||
|
modifier = modifier
|
||||||
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
|
.shadow(elevation, MaterialTheme.shapes.medium)
|
||||||
|
.then(
|
||||||
|
if (canReorder && !inSelectionMode) {
|
||||||
|
Modifier.longPressDraggableHandle(
|
||||||
|
onDragStarted = {
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.animateItem()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user