261 lines
9.0 KiB
Kotlin
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 }
|
|
}
|
|
}
|
|
}
|