Tusky-App-Android/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt

261 lines
9.0 KiB
Kotlin

/* Copyright Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>.
*/
package com.keylesspalace.tusky.components.account.list
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.ListsActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.FragmentListsListBinding
import com.keylesspalace.tusky.databinding.ItemListBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ListSelectionFragment : DialogFragment(), Injectable {
interface ListSelectionListener {
fun onListSelected(list: MastoList)
}
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
private var _binding: FragmentListsListBinding? = null
// This property is only valid between onCreateDialog and onDestroyView
private val binding get() = _binding!!
private val adapter = Adapter()
private var selectListener: ListSelectionListener? = null
private var accountId: String? = null
override fun onAttach(context: Context) {
super.onAttach(context)
selectListener = context as? ListSelectionListener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
accountId = requireArguments().getString(ARG_ACCOUNT_ID)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val context = requireContext()
_binding = FragmentListsListBinding.inflate(layoutInflater)
binding.listsView.adapter = adapter
val dialogBuilder = AlertDialog.Builder(context)
.setView(binding.root)
.setTitle(R.string.select_list_title)
.setNeutralButton(R.string.select_list_manage) { _, _ ->
val listIntent = Intent(context, ListsActivity::class.java)
startActivity(listIntent)
}
.setNegativeButton(if (accountId != null) R.string.button_done else android.R.string.cancel, null)
val dialog = dialogBuilder.create()
val showProgressBarJob = getProgressBarJob(binding.progressBar, 500)
showProgressBarJob.start()
// TODO change this to a (single) LoadState like elsewhere?
lifecycleScope.launch {
viewModel.states.collectLatest { states ->
binding.progressBar.hide()
showProgressBarJob.cancel()
if (states.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists)
} else {
binding.listsView.show()
adapter.submitList(states)
}
}
}
lifecycleScope.launch {
viewModel.loadError.collectLatest { error ->
Log.e(TAG, "failed to load lists", error)
binding.progressBar.hide()
showProgressBarJob.cancel()
binding.listsView.hide()
binding.messageView.apply {
show()
setup(error) { load() }
}
}
}
lifecycleScope.launch {
viewModel.actionError.collectLatest { error ->
when (error.type) {
ActionError.Type.ADD -> {
Snackbar.make(
binding.root,
R.string.failed_to_add_to_list,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
viewModel.addAccountToList(accountId!!, error.listId)
}
.show()
}
ActionError.Type.REMOVE -> {
Snackbar.make(
binding.root,
R.string.failed_to_remove_from_list,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
viewModel.removeAccountFromList(accountId!!, error.listId)
}
.show()
}
}
}
}
lifecycleScope.launch {
load()
}
return dialog
}
private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch(
start = CoroutineStart.LAZY
) {
try {
delay(delayMs)
progressView.show()
awaitCancellation()
} finally {
progressView.hide()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun load() {
binding.progressBar.show()
binding.listsView.hide()
binding.messageView.hide()
viewModel.load(accountId)
}
private object Differ : DiffUtil.ItemCallback<AccountListState>() {
override fun areItemsTheSame(
oldItem: AccountListState,
newItem: AccountListState
): Boolean {
return oldItem.list.id == newItem.list.id
}
override fun areContentsTheSame(
oldItem: AccountListState,
newItem: AccountListState
): Boolean {
return oldItem == newItem
}
}
inner class Adapter :
ListAdapter<AccountListState, BindingHolder<ItemListBinding>>(Differ) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemListBinding> {
return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) {
val item = getItem(position)
holder.binding.listName.text = item.list.title
accountId?.let { accountId ->
holder.binding.addButton.apply {
visible(!item.includesAccount)
setOnClickListener {
viewModel.addAccountToList(accountId, item.list.id)
}
}
holder.binding.removeButton.apply {
visible(item.includesAccount)
setOnClickListener {
viewModel.removeAccountFromList(accountId, item.list.id)
}
}
}
holder.itemView.setOnClickListener {
selectListener?.onListSelected(item.list)
accountId?.let { accountId ->
if (item.includesAccount) {
viewModel.removeAccountFromList(accountId, item.list.id)
} else {
viewModel.addAccountToList(accountId, item.list.id)
}
}
}
}
}
companion object {
private const val TAG = "ListsListFragment"
private const val ARG_ACCOUNT_ID = "accountId"
fun newInstance(accountId: String?): ListSelectionFragment {
val args = Bundle().apply {
putString(ARG_ACCOUNT_ID, accountId)
}
return ListSelectionFragment().apply { arguments = args }
}
}
}