migrating to ViewBinding part 4: Fragments (#2108)

* migrating to ViewBinding part 4: Fragment

* fix imports

* don't use viewBinding extension in ViewImage and ViewVideoFragment

* don't use viewBinding extension in ViewImage and ViewVideoFragment
This commit is contained in:
Konrad Pozniak 2021-03-13 21:27:20 +01:00 committed by GitHub
parent fbb0b11d83
commit bea5098cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 412 additions and 349 deletions

View File

@ -125,6 +125,7 @@ dependencies {
implementation "androidx.emoji:emoji-appcompat:1.1.0" implementation "androidx.emoji:emoji-appcompat:1.1.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.paging:paging-runtime-ktx:2.1.2" implementation "androidx.paging:paging-runtime-ktx:2.1.2"

View File

@ -23,11 +23,13 @@ import android.view.ViewGroup
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.viewModels
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
@ -38,9 +40,6 @@ import com.keylesspalace.tusky.viewmodel.State
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_accounts_in_list.*
import kotlinx.android.synthetic.main.item_follow_request.*
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -48,23 +47,11 @@ private typealias AccountInfo = Pair<Account, Boolean>
class AccountsInListFragment : DialogFragment(), Injectable { class AccountsInListFragment : DialogFragment(), Injectable {
companion object {
private const val LIST_ID_ARG = "listId"
private const val LIST_NAME_ARG = "listName"
@JvmStatic
fun newInstance(listId: String, listName: String): AccountsInListFragment {
val args = Bundle().apply {
putString(LIST_ID_ARG, listId)
putString(LIST_NAME_ARG, listName)
}
return AccountsInListFragment().apply { arguments = args }
}
}
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
lateinit var viewModel: AccountsInListViewModel
private val viewModel: AccountsInListViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentAccountsInListBinding::bind)
private lateinit var listId: String private lateinit var listId: String
private lateinit var listName: String private lateinit var listName: String
@ -79,7 +66,6 @@ class AccountsInListFragment : DialogFragment(), Injectable {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
viewModel = viewModelFactory.create(AccountsInListViewModel::class.java)
val args = requireArguments() val args = requireArguments()
listId = args.getString(LIST_ID_ARG)!! listId = args.getString(LIST_ID_ARG)!!
listName = args.getString(LIST_NAME_ARG)!! listName = args.getString(LIST_NAME_ARG)!!
@ -100,12 +86,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context)
accountsRecycler.layoutManager = LinearLayoutManager(view.context) binding.accountsRecycler.adapter = adapter
accountsRecycler.adapter = adapter
accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context)
accountsSearchRecycler.adapter = searchAdapter binding.accountsSearchRecycler.adapter = searchAdapter
viewModel.state viewModel.state
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -114,15 +99,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
adapter.submitList(state.accounts.asRightOrNull() ?: listOf()) adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
when (state.accounts) { when (state.accounts) {
is Either.Right -> messageView.hide() is Either.Right -> binding.messageView.hide()
is Either.Left -> handleError(state.accounts.value) is Either.Left -> handleError(state.accounts.value)
} }
setupSearchView(state) setupSearchView(state)
} }
searchView.isSubmitButtonEnabled = true binding.searchView.isSubmitButtonEnabled = true
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
viewModel.search(query ?: "") viewModel.search(query ?: "")
return true return true
@ -141,30 +126,30 @@ class AccountsInListFragment : DialogFragment(), Injectable {
private fun setupSearchView(state: State) { private fun setupSearchView(state: State) {
if (state.searchResult == null) { if (state.searchResult == null) {
searchAdapter.submitList(listOf()) searchAdapter.submitList(listOf())
accountsSearchRecycler.hide() binding.accountsSearchRecycler.hide()
accountsRecycler.show() binding.accountsRecycler.show()
} else { } else {
val listAccounts = state.accounts.asRightOrNull() ?: listOf() val listAccounts = state.accounts.asRightOrNull() ?: listOf()
val newList = state.searchResult.map { acc -> val newList = state.searchResult.map { acc ->
acc to listAccounts.contains(acc) acc to listAccounts.contains(acc)
} }
searchAdapter.submitList(newList) searchAdapter.submitList(newList)
accountsSearchRecycler.show() binding.accountsSearchRecycler.show()
accountsRecycler.hide() binding.accountsRecycler.hide()
} }
} }
private fun handleError(error: Throwable) { private fun handleError(error: Throwable) {
messageView.show() binding.messageView.show()
val retryAction = { _: View -> val retryAction = { _: View ->
messageView.hide() binding.messageView.hide()
viewModel.load(listId) viewModel.load(listId)
} }
if (error is IOException) { if (error is IOException) {
messageView.setup(R.drawable.elephant_offline, binding.messageView.setup(R.drawable.elephant_offline,
R.string.error_network, retryAction) R.string.error_network, retryAction)
} else { } else {
messageView.setup(R.drawable.elephant_error, binding.messageView.setup(R.drawable.elephant_error,
R.string.error_generic, retryAction) R.string.error_generic, retryAction)
} }
} }
@ -187,39 +172,28 @@ class AccountsInListFragment : DialogFragment(), Injectable {
} }
} }
inner class Adapter : ListAdapter<Account, Adapter.ViewHolder>(AccountDiffer) { inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_follow_request, parent, false) val holder = BindingHolder(binding)
return ViewHolder(view)
binding.notificationTextView.hide()
binding.acceptButton.hide()
binding.rejectButton.setOnClickListener {
onRemoveFromList(getItem(holder.adapterPosition).id)
}
binding.rejectButton.contentDescription =
binding.root.context.getString(R.string.action_remove_from_list)
return holder
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
holder.bind(getItem(position)) val account = getItem(position)
} holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
View.OnClickListener, LayoutContainer {
override val containerView = itemView
init {
acceptButton.hide()
rejectButton.setOnClickListener(this)
rejectButton.contentDescription =
itemView.context.getString(R.string.action_remove_from_list)
}
fun bind(account: Account) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar)
}
override fun onClick(v: View?) {
onRemoveFromList(getItem(adapterPosition).id)
}
} }
} }
@ -232,57 +206,58 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.second == newItem.second return oldItem.second == newItem.second
&& oldItem.first.deepEquals(newItem.first) && oldItem.first.deepEquals(newItem.first)
} }
} }
inner class SearchAdapter : ListAdapter<AccountInfo, SearchAdapter.ViewHolder>(SearchDiffer) { inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val view = LayoutInflater.from(parent.context) val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.inflate(R.layout.item_follow_request, parent, false) val holder = BindingHolder(binding)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) { binding.notificationTextView.hide()
val (account, inAList) = getItem(position) binding.acceptButton.hide()
holder.bind(account, inAList) binding.rejectButton.setOnClickListener {
val (account, inAList) = getItem(holder.adapterPosition)
}
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
View.OnClickListener, LayoutContainer {
override val containerView = itemView
fun bind(account: Account, inAList: Boolean) {
displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis)
usernameTextView.text = account.username
loadAvatar(account.avatar, avatar, radius, animateAvatar)
rejectButton.apply {
contentDescription = if (inAList) {
setImageResource(R.drawable.ic_reject_24dp)
getString(R.string.action_remove_from_list)
} else {
setImageResource(R.drawable.ic_plus_24dp)
getString(R.string.action_add_to_list)
}
}
}
init {
acceptButton.hide()
rejectButton.setOnClickListener(this)
}
override fun onClick(v: View?) {
val (account, inAList) = getItem(adapterPosition)
if (inAList) { if (inAList) {
onRemoveFromList(account.id) onRemoveFromList(account.id)
} else { } else {
onAddToList(account) onAddToList(account)
} }
} }
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
val (account, inAList) = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar)
holder.binding.rejectButton.apply {
contentDescription = if (inAList) {
setImageResource(R.drawable.ic_reject_24dp)
getString(R.string.action_remove_from_list)
} else {
setImageResource(R.drawable.ic_plus_24dp)
getString(R.string.action_add_to_list)
}
}
}
}
companion object {
private const val LIST_ID_ARG = "listId"
private const val LIST_NAME_ARG = "listName"
@JvmStatic
fun newInstance(listId: String, listName: String): AccountsInListFragment {
val args = Bundle().apply {
putString(LIST_ID_ARG, listId)
putString(LIST_NAME_ARG, listName)
}
return AccountsInListFragment().apply { arguments = args }
} }
} }
} }

View File

@ -69,6 +69,9 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
private val binding by viewBinding(ActivityViewMediaBinding::inflate) private val binding by viewBinding(ActivityViewMediaBinding::inflate)
val toolbar: View
get() = binding.toolbar
var isToolbarVisible = true var isToolbarVisible = true
private set private set

View File

@ -28,6 +28,7 @@ import androidx.recyclerview.widget.SimpleItemAnimator
import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
@ -38,7 +39,7 @@ import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import kotlinx.android.synthetic.main.fragment_timeline.* import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject import javax.inject.Inject
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
@ -48,6 +49,8 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private val viewModel: ConversationsViewModel by viewModels { viewModelFactory }
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var adapter: ConversationAdapter private lateinit var adapter: ConversationAdapter
private var layoutManager: LinearLayoutManager? = null private var layoutManager: LinearLayoutManager? = null
@ -73,14 +76,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
layoutManager = LinearLayoutManager(view.context) layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
progressBar.hide() binding.progressBar.hide()
statusView.hide() binding.statusView.hide()
initSwipeToRefresh() initSwipeToRefresh()
@ -97,16 +100,16 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun initSwipeToRefresh() { private fun initSwipeToRefresh() {
viewModel.refreshState.observe(viewLifecycleOwner) { viewModel.refreshState.observe(viewLifecycleOwner) {
swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
} }
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh() viewModel.refresh()
} }
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
} }
private fun onTopLoaded() { private fun onTopLoaded() {
recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
} }
override fun onReblog(reblog: Boolean, position: Int) { override fun onReblog(reblog: Boolean, position: Int) {
@ -183,7 +186,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
private fun jumpToTop() { private fun jumpToTop() {
if (isAdded) { if (isAdded) {
layoutManager?.scrollToPosition(0) layoutManager?.scrollToPosition(0)
recyclerView.stopScroll() binding.recyclerView.stopScroll()
} }
} }

View File

@ -2,9 +2,7 @@ package com.keylesspalace.tusky.components.instancemute.fragment
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
@ -14,16 +12,17 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener
import com.keylesspalace.tusky.databinding.FragmentInstanceListBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_instance_list.*
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -31,9 +30,12 @@ import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val binding by viewBinding(FragmentInstanceListBinding::bind)
private var fetching = false private var fetching = false
private var bottomId: String? = null private var bottomId: String? = null
private var adapter = DomainMutesAdapter(this) private var adapter = DomainMutesAdapter(this)
@ -42,12 +44,12 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
val layoutManager = LinearLayoutManager(view.context) val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
scrollListener = object : EndlessOnScrollListener(layoutManager) { scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
@ -57,7 +59,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
} }
} }
recyclerView.addOnScrollListener(scrollListener) binding.recyclerView.addOnScrollListener(scrollListener)
fetchInstances() fetchInstances()
} }
@ -85,7 +87,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun onResponse(call: Call<Any>, response: Response<Any>) { override fun onResponse(call: Call<Any>, response: Response<Any>) {
if (response.isSuccessful) { if (response.isSuccessful) {
adapter.removeItem(position) adapter.removeItem(position)
Snackbar.make(recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG) Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) { .setAction(R.string.action_undo) {
mute(true, instance, position) mute(true, instance, position)
} }
@ -103,10 +105,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
return return
} }
fetching = true fetching = true
instanceProgressBar.show() binding.instanceProgressBar.show()
if (id != null) { if (id != null) {
recyclerView.post { adapter.bottomLoading = true } binding.recyclerView.post { adapter.bottomLoading = true }
} }
api.domainBlocks(id, bottomId) api.domainBlocks(id, bottomId)
@ -116,7 +118,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
val instances = response.body() val instances = response.body()
if (response.isSuccessful && instances != null) { if (response.isSuccessful && instances != null) {
onFetchInstancesSuccess(instances, response.headers().get("Link")) onFetchInstancesSuccess(instances, response.headers()["Link"])
} else { } else {
onFetchInstancesFailure(Exception(response.message())) onFetchInstancesFailure(Exception(response.message()))
} }
@ -127,7 +129,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) { private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
adapter.bottomLoading = false adapter.bottomLoading = false
instanceProgressBar.hide() binding.instanceProgressBar.hide()
val links = HttpHeaderLink.parse(linkHeader) val links = HttpHeaderLink.parse(linkHeader)
val next = HttpHeaderLink.findByRelationType(links, "next") val next = HttpHeaderLink.findByRelationType(links, "next")
@ -137,32 +139,32 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
fetching = false fetching = false
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
messageView.setup( binding.messageView.setup(
R.drawable.elephant_friend_empty, R.drawable.elephant_friend_empty,
R.string.message_empty, R.string.message_empty,
null null
) )
} else { } else {
messageView.hide() binding.messageView.hide()
} }
} }
private fun onFetchInstancesFailure(throwable: Throwable) { private fun onFetchInstancesFailure(throwable: Throwable) {
fetching = false fetching = false
instanceProgressBar.hide() binding.instanceProgressBar.hide()
Log.e(TAG, "Fetch failure", throwable) Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
if (throwable is IOException) { if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide() binding.messageView.hide()
this.fetchInstances(null) this.fetchInstances(null)
} }
} else { } else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide() binding.messageView.hide()
this.fetchInstances(null) this.fetchInstances(null)
} }
} }

View File

@ -22,12 +22,13 @@ import androidx.fragment.app.activityViewModels
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import kotlinx.android.synthetic.main.fragment_report_done.* import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject import javax.inject.Inject
class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
@ -37,8 +38,10 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportDoneBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) binding.textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName)
handleClicks() handleClicks()
subscribeObservables() subscribeObservables()
} }
@ -46,14 +49,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
private fun subscribeObservables() { private fun subscribeObservables() {
viewModel.muteState.observe(viewLifecycleOwner) { viewModel.muteState.observe(viewLifecycleOwner) {
if (it !is Loading) { if (it !is Loading) {
buttonMute.show() binding.buttonMute.show()
progressMute.show() binding.progressMute.show()
} else { } else {
buttonMute.hide() binding.buttonMute.hide()
progressMute.hide() binding.progressMute.hide()
} }
buttonMute.setText(when (it.data) { binding.buttonMute.setText(when (it.data) {
true -> R.string.action_unmute true -> R.string.action_unmute
else -> R.string.action_mute else -> R.string.action_mute
}) })
@ -61,14 +64,14 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
viewModel.blockState.observe(viewLifecycleOwner) { viewModel.blockState.observe(viewLifecycleOwner) {
if (it !is Loading) { if (it !is Loading) {
buttonBlock.show() binding.buttonBlock.show()
progressBlock.show() binding.progressBlock.show()
} }
else{ else {
buttonBlock.hide() binding.buttonBlock.hide()
progressBlock.hide() binding.progressBlock.hide()
} }
buttonBlock.setText(when (it.data) { binding.buttonBlock.setText(when (it.data) {
true -> R.string.action_unblock true -> R.string.action_unblock
else -> R.string.action_block else -> R.string.action_block
}) })
@ -77,13 +80,13 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
} }
private fun handleClicks() { private fun handleClicks() {
buttonDone.setOnClickListener { binding.buttonDone.setOnClickListener {
viewModel.navigateTo(Screen.Finish) viewModel.navigateTo(Screen.Finish)
} }
buttonBlock.setOnClickListener { binding.buttonBlock.setOnClickListener {
viewModel.toggleBlock() viewModel.toggleBlock()
} }
buttonMute.setOnClickListener { binding.buttonMute.setOnClickListener {
viewModel.toggleMute() viewModel.toggleMute()
} }
} }
@ -91,5 +94,4 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
companion object { companion object {
fun newInstance() = ReportDoneFragment() fun newInstance() = ReportDoneFragment()
} }
} }

View File

@ -24,10 +24,10 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_report_note.*
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -38,6 +38,8 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportNoteBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
fillViews() fillViews()
handleChanges() handleChanges()
@ -46,29 +48,29 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun handleChanges() { private fun handleChanges() {
editNote.doAfterTextChanged { binding.editNote.doAfterTextChanged {
viewModel.reportNote = it?.toString() ?: "" viewModel.reportNote = it?.toString() ?: ""
} }
checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
viewModel.isRemoteNotify = isChecked viewModel.isRemoteNotify = isChecked
} }
} }
private fun fillViews() { private fun fillViews() {
editNote.setText(viewModel.reportNote) binding.editNote.setText(viewModel.reportNote)
if (viewModel.isRemoteAccount){ if (viewModel.isRemoteAccount){
checkIsNotifyRemote.show() binding.checkIsNotifyRemote.show()
reportDescriptionRemoteInstance.show() binding.reportDescriptionRemoteInstance.show()
} }
else{ else{
checkIsNotifyRemote.hide() binding.checkIsNotifyRemote.hide()
reportDescriptionRemoteInstance.hide() binding.reportDescriptionRemoteInstance.hide()
} }
if (viewModel.isRemoteAccount) if (viewModel.isRemoteAccount)
checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer)
checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -83,13 +85,13 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun showError(error: Throwable?) { private fun showError(error: Throwable?) {
editNote.isEnabled = true binding.editNote.isEnabled = true
checkIsNotifyRemote.isEnabled = true binding.checkIsNotifyRemote.isEnabled = true
buttonReport.isEnabled = true binding.buttonReport.isEnabled = true
buttonBack.isEnabled = true binding.buttonBack.isEnabled = true
progressBar.hide() binding.progressBar.hide()
Snackbar.make(buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG) Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
.apply { .apply {
setAction(R.string.action_retry) { setAction(R.string.action_retry) {
sendReport() sendReport()
@ -103,19 +105,19 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
} }
private fun showLoading() { private fun showLoading() {
buttonReport.isEnabled = false binding.buttonReport.isEnabled = false
buttonBack.isEnabled = false binding.buttonBack.isEnabled = false
editNote.isEnabled = false binding.editNote.isEnabled = false
checkIsNotifyRemote.isEnabled = false binding.checkIsNotifyRemote.isEnabled = false
progressBar.show() binding.progressBar.show()
} }
private fun handleClicks() { private fun handleClicks() {
buttonBack.setOnClickListener { binding.buttonBack.setOnClickListener {
viewModel.navigateTo(Screen.Back) viewModel.navigateTo(Screen.Back)
} }
buttonReport.setOnClickListener { binding.buttonReport.setOnClickListener {
sendReport() sendReport()
} }
} }
@ -123,5 +125,4 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
companion object { companion object {
fun newInstance() = ReportNoteFragment() fun newInstance() = ReportNoteFragment()
} }
} }

View File

@ -34,6 +34,7 @@ import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.report.Screen import com.keylesspalace.tusky.components.report.Screen
import com.keylesspalace.tusky.components.report.adapter.AdapterHandler import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter
import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
@ -44,8 +45,8 @@ import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.android.synthetic.main.fragment_report_statuses.*
import javax.inject.Inject import javax.inject.Inject
class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler {
@ -58,6 +59,8 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } private val viewModel: ReportViewModel by activityViewModels { viewModelFactory }
private val binding by viewBinding(FragmentReportStatusesBinding::bind)
private lateinit var adapter: StatusesAdapter private lateinit var adapter: StatusesAdapter
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
@ -93,9 +96,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
} }
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
snackbarErrorRetry?.dismiss() snackbarErrorRetry?.dismiss()
viewModel.refreshStatuses() viewModel.refreshStatuses()
} }
@ -118,10 +121,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
adapter = StatusesAdapter(statusDisplayOptions, adapter = StatusesAdapter(statusDisplayOptions,
viewModel.statusViewState, this) viewModel.statusViewState, this)
recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
viewModel.statuses.observe(viewLifecycleOwner) { viewModel.statuses.observe(viewLifecycleOwner) {
adapter.submitList(it) adapter.submitList(it)
@ -129,9 +132,9 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateAfter.observe(viewLifecycleOwner) { viewModel.networkStateAfter.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarBottom.show() binding.progressBarBottom.show()
else else
progressBarBottom.hide() binding.progressBarBottom.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
@ -139,22 +142,22 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
viewModel.networkStateBefore.observe(viewLifecycleOwner) { viewModel.networkStateBefore.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING)
progressBarTop.show() binding.progressBarTop.show()
else else
progressBarTop.hide() binding.progressBarTop.hide()
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
} }
viewModel.networkStateRefresh.observe(viewLifecycleOwner) { viewModel.networkStateRefresh.observe(viewLifecycleOwner) {
if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !swipeRefreshLayout.isRefreshing) if (it?.status == com.keylesspalace.tusky.util.Status.RUNNING && !binding.swipeRefreshLayout.isRefreshing)
progressBarLoading.show() binding.progressBarLoading.show()
else else
progressBarLoading.hide() binding.progressBarLoading.hide()
if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING) if (it?.status != com.keylesspalace.tusky.util.Status.RUNNING)
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
if (it?.status == com.keylesspalace.tusky.util.Status.FAILED) if (it?.status == com.keylesspalace.tusky.util.Status.FAILED)
showError(it.msg) showError(it.msg)
} }
@ -162,7 +165,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) { private fun showError(@Suppress("UNUSED_PARAMETER") msg: String?) {
if (snackbarErrorRetry?.isShown != true) { if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry?.setAction(R.string.action_retry) {
viewModel.retryStatusLoad() viewModel.retryStatusLoad()
} }
@ -172,11 +175,11 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun handleClicks() { private fun handleClicks() {
buttonCancel.setOnClickListener { binding.buttonCancel.setOnClickListener {
viewModel.navigateTo(Screen.Back) viewModel.navigateTo(Screen.Back)
} }
buttonContinue.setOnClickListener { binding.buttonContinue.setOnClickListener {
viewModel.navigateTo(Screen.Note) viewModel.navigateTo(Screen.Note)
} }
} }

View File

@ -23,11 +23,10 @@ import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.NetworkState
import kotlinx.android.synthetic.main.fragment_search.*
class SearchAccountsFragment : SearchFragment<Account>() { class SearchAccountsFragment : SearchFragment<Account>() {
override fun createAdapter(): PagedListAdapter<Account, *> { override fun createAdapter(): PagedListAdapter<Account, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
return SearchAccountsAdapter( return SearchAccountsAdapter(
this, this,
@ -46,5 +45,4 @@ class SearchAccountsFragment : SearchFragment<Account>() {
companion object { companion object {
fun newInstance() = SearchAccountsFragment() fun newInstance() = SearchAccountsFragment()
} }
} }

View File

@ -17,11 +17,11 @@ import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.*
import kotlinx.android.synthetic.main.fragment_search.*
import javax.inject.Inject import javax.inject.Inject
abstract class SearchFragment<T> : Fragment(R.layout.fragment_search), abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
@ -32,6 +32,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory }
protected val binding by viewBinding(FragmentSearchBinding::bind)
private var snackbarErrorRetry: Snackbar? = null private var snackbarErrorRetry: Snackbar? = null
abstract fun createAdapter(): PagedListAdapter<T, *> abstract fun createAdapter(): PagedListAdapter<T, *>
@ -48,8 +50,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
} }
private fun setupSwipeRefreshLayout() { private fun setupSwipeRefreshLayout() {
swipeRefreshLayout.setOnRefreshListener(this) binding.swipeRefreshLayout.setOnRefreshListener(this)
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
} }
private fun subscribeObservables() { private fun subscribeObservables() {
@ -59,7 +61,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkStateRefresh.observe(viewLifecycleOwner) { networkStateRefresh.observe(viewLifecycleOwner) {
searchProgressBar.visible(it == NetworkState.LOADING) binding.searchProgressBar.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) { if (it.status == Status.FAILED) {
showError() showError()
@ -69,7 +71,7 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
networkState.observe(viewLifecycleOwner) { networkState.observe(viewLifecycleOwner) {
progressBarBottom.visible(it == NetworkState.LOADING) binding.progressBarBottom.visible(it == NetworkState.LOADING)
if (it.status == Status.FAILED) { if (it.status == Status.FAILED) {
showError() showError()
@ -82,24 +84,25 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
} }
private fun initAdapter() { private fun initAdapter() {
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
adapter = createAdapter() adapter = createAdapter()
searchRecyclerView.adapter = adapter binding.searchRecyclerView.adapter = adapter
searchRecyclerView.setHasFixedSize(true) binding.searchRecyclerView.setHasFixedSize(true)
(searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
private fun showNoData(isEmpty: Boolean) { private fun showNoData(isEmpty: Boolean) {
if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) if (isEmpty && networkStateRefresh.value == NetworkState.LOADED) {
searchNoResultsText.show() binding.searchNoResultsText.show()
else } else {
searchNoResultsText.hide() binding.searchNoResultsText.hide()
}
} }
private fun showError() { private fun showError() {
if (snackbarErrorRetry?.isShown != true) { if (snackbarErrorRetry?.isShown != true) {
snackbarErrorRetry = Snackbar.make(layoutRoot, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) snackbarErrorRetry = Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE)
snackbarErrorRetry?.setAction(R.string.action_retry) { snackbarErrorRetry?.setAction(R.string.action_retry) {
snackbarErrorRetry = null snackbarErrorRetry = null
viewModel.retryAllSearches() viewModel.retryAllSearches()
@ -122,8 +125,8 @@ abstract class SearchFragment<T> : Fragment(R.layout.fragment_search),
override fun onRefresh() { override fun onRefresh() {
// Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins. // Dismissed here because the RecyclerView bottomProgressBar is shown as soon as the retry begins.
swipeRefreshLayout.post { binding.swipeRefreshLayout.post {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
} }
viewModel.retryAllSearches() viewModel.retryAllSearches()
} }

View File

@ -63,7 +63,6 @@ import com.keylesspalace.tusky.viewdata.StatusViewData
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_search.*
class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener { class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concrete>>(), StatusActionListener {
@ -78,7 +77,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
get() = super.adapter as SearchStatusesAdapter get() = super.adapter as SearchStatusesAdapter
override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagedListAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions( val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false), animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled, mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
@ -91,12 +90,11 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
searchRecyclerView.addItemDecoration(DividerItemDecoration(searchRecyclerView.context, DividerItemDecoration.VERTICAL)) binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
searchRecyclerView.layoutManager = LinearLayoutManager(searchRecyclerView.context) binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context)
return SearchStatusesAdapter(statusDisplayOptions, this) return SearchStatusesAdapter(statusDisplayOptions, this)
} }
override fun onContentHiddenChange(isShowing: Boolean, position: Int) { override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
searchAdapter.getItem(position)?.let { searchAdapter.getItem(position)?.let {
viewModel.contentHiddenChange(it, isShowing) viewModel.contentHiddenChange(it, isShowing)
@ -486,5 +484,4 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
.show() .show()
} }
} }
} }

View File

@ -31,6 +31,7 @@ import com.keylesspalace.tusky.AccountListActivity.Type
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.* import com.keylesspalace.tusky.adapter.*
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
@ -40,12 +41,12 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
import com.uber.autodispose.autoDispose import com.uber.autodispose.autoDispose
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_account_list.*
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -56,6 +57,8 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val binding by viewBinding(FragmentAccountListBinding::bind)
private lateinit var type: Type private lateinit var type: Type
private var id: String? = null private var id: String? = null
@ -73,12 +76,12 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(view.context) val layoutManager = LinearLayoutManager(view.context)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
(recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
val pm = PreferenceManager.getDefaultSharedPreferences(view.context) val pm = PreferenceManager.getDefaultSharedPreferences(view.context)
val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
@ -90,7 +93,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis) Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis)
else -> FollowAdapter(this, animateAvatar, animateEmojis) else -> FollowAdapter(this, animateAvatar, animateEmojis)
} }
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
scrollListener = object : EndlessOnScrollListener(layoutManager) { scrollListener = object : EndlessOnScrollListener(layoutManager) {
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) {
@ -101,7 +104,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
} }
} }
recyclerView.addOnScrollListener(scrollListener) binding.recyclerView.addOnScrollListener(scrollListener)
fetchAccounts() fetchAccounts()
} }
@ -136,7 +139,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unmutedUser = mutesAdapter.removeItem(position) val unmutedUser = mutesAdapter.removeItem(position)
if (unmutedUser != null) { if (unmutedUser != null) {
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) { .setAction(R.string.action_undo) {
mutesAdapter.addItem(unmutedUser, position) mutesAdapter.addItem(unmutedUser, position)
onMute(true, id, position, notifications) onMute(true, id, position, notifications)
@ -180,7 +183,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
val unblockedUser = blocksAdapter.removeItem(position) val unblockedUser = blocksAdapter.removeItem(position)
if (unblockedUser != null) { if (unblockedUser != null) {
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) { .setAction(R.string.action_undo) {
blocksAdapter.addItem(unblockedUser, position) blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position) onBlock(true, id, position)
@ -260,7 +263,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = true fetching = true
if (fromId != null) { if (fromId != null) {
recyclerView.post { adapter.setBottomLoading(true) } binding.recyclerView.post { adapter.setBottomLoading(true) }
} }
getFetchCallByListType(fromId) getFetchCallByListType(fromId)
@ -303,14 +306,14 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
fetching = false fetching = false
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
messageView.setup( binding.messageView.setup(
R.drawable.elephant_friend_empty, R.drawable.elephant_friend_empty,
R.string.message_empty, R.string.message_empty,
null null
) )
} else { } else {
messageView.hide() binding.messageView.hide()
} }
} }
@ -339,15 +342,15 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
Log.e(TAG, "Fetch failure", throwable) Log.e(TAG, "Fetch failure", throwable)
if (adapter.itemCount == 0) { if (adapter.itemCount == 0) {
messageView.show() binding.messageView.show()
if (throwable is IOException) { if (throwable is IOException) {
messageView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
messageView.hide() binding.messageView.hide()
this.fetchAccounts(null) this.fetchAccounts(null)
} }
} else { } else {
messageView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
messageView.hide() binding.messageView.hide()
this.fetchAccounts(null) this.fetchAccounts(null)
} }
} }
@ -368,5 +371,4 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
} }
} }
} }
} }

View File

@ -30,6 +30,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
@ -39,13 +40,13 @@ import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.view.SquareImageView
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.uber.autodispose.android.lifecycle.autoDispose import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.SingleObserver import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_timeline.*
import retrofit2.Response import retrofit2.Response
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
@ -58,49 +59,36 @@ import javax.inject.Inject
*/ */
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
companion object {
@JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
val fragment = AccountMediaFragment()
val args = Bundle()
args.putString(ACCOUNT_ID_ARG, accountId)
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
fragment.arguments = args
return fragment
}
private const val ACCOUNT_ID_ARG = "account_id"
private const val TAG = "AccountMediaFragment"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
}
private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
@Inject @Inject
lateinit var api: MastodonApi lateinit var api: MastodonApi
private val binding by viewBinding(FragmentTimelineBinding::bind)
private lateinit var accountId: String
private val adapter = MediaGridAdapter() private val adapter = MediaGridAdapter()
private val statuses = mutableListOf<Status>() private val statuses = mutableListOf<Status>()
private var fetchingStatus = FetchingStatus.NOT_FETCHING private var fetchingStatus = FetchingStatus.NOT_FETCHING
private lateinit var accountId: String private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
private val callback = object : SingleObserver<Response<List<Status>>> { private val callback = object : SingleObserver<Response<List<Status>>> {
override fun onError(t: Throwable) { override fun onError(t: Throwable) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
topProgressBar?.hide() binding.topProgressBar.hide()
statusView.show() binding.statusView.show()
if (t is IOException) { if (t is IOException) {
statusView.setup(R.drawable.elephant_offline, R.string.error_network) { binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
doInitialLoadingIfNeeded() doInitialLoadingIfNeeded()
} }
} else { } else {
statusView.setup(R.drawable.elephant_error, R.string.error_generic) { binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
doInitialLoadingIfNeeded() doInitialLoadingIfNeeded()
} }
} }
@ -112,9 +100,9 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
override fun onSuccess(response: Response<List<Status>>) { override fun onSuccess(response: Response<List<Status>>) {
fetchingStatus = FetchingStatus.NOT_FETCHING fetchingStatus = FetchingStatus.NOT_FETCHING
if (isAdded) { if (isAdded) {
swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
progressBar.visibility = View.GONE binding.progressBar.visibility = View.GONE
topProgressBar?.hide() binding.topProgressBar.hide()
val body = response.body() val body = response.body()
body?.let { fetched -> body?.let { fetched ->
@ -126,11 +114,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
} }
adapter.addTop(result) adapter.addTop(result)
if (result.isNotEmpty()) if (result.isNotEmpty())
recyclerView.scrollToPosition(0) binding.recyclerView.scrollToPosition(0)
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
statusView.show() binding.statusView.show()
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
} }
} }
} }
@ -181,18 +169,18 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
if (isSwipeToRefreshEnabled) { if (isSwipeToRefreshEnabled) {
swipeRefreshLayout.setOnRefreshListener { binding.swipeRefreshLayout.setOnRefreshListener {
refresh() refresh()
} }
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
} }
statusView.visibility = View.GONE binding.statusView.visibility = View.GONE
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { if (dy > 0) {
@ -216,7 +204,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
} }
private fun refresh() { private fun refresh() {
statusView.hide() binding.statusView.hide()
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
if (statuses.isEmpty()) { if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -229,12 +217,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
.subscribe(callback) .subscribe(callback)
if (!isSwipeToRefreshEnabled) if (!isSwipeToRefreshEnabled)
topProgressBar?.show() binding.topProgressBar.show()
} }
private fun doInitialLoadingIfNeeded() { private fun doInitialLoadingIfNeeded() {
if (isAdded) { if (isAdded) {
statusView.hide() binding.statusView.hide()
} }
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING fetchingStatus = FetchingStatus.INITIAL_FETCHING
@ -344,4 +332,19 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
needToRefresh = true needToRefresh = true
} }
companion object {
@JvmStatic
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
val fragment = AccountMediaFragment()
val args = Bundle()
args.putString(ACCOUNT_ID_ARG, accountId)
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
fragment.arguments = args
return fragment
}
private const val ACCOUNT_ID_ARG = "account_id"
private const val TAG = "AccountMediaFragment"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
}
} }

View File

@ -32,13 +32,12 @@ import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.github.chrisbanes.photoview.PhotoViewAttacher import com.github.chrisbanes.photoview.PhotoViewAttacher
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewImageBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_image.*
import kotlin.math.abs import kotlin.math.abs
class ViewImageFragment : ViewMediaFragment() { class ViewImageFragment : ViewMediaFragment() {
@ -48,6 +47,9 @@ class ViewImageFragment : ViewMediaFragment() {
fun onPhotoTap() fun onPhotoTap()
} }
private var _binding: FragmentViewImageBinding? = null
private val binding get() = _binding!!
private lateinit var attacher: PhotoViewAttacher private lateinit var attacher: PhotoViewAttacher
private lateinit var photoActionsListener: PhotoActionsListener private lateinit var photoActionsListener: PhotoActionsListener
private lateinit var toolbar: View private lateinit var toolbar: View
@ -71,18 +73,19 @@ class ViewImageFragment : ViewMediaFragment() {
description: String?, description: String?,
showingDescription: Boolean showingDescription: Boolean
) { ) {
photoView.transitionName = url binding.photoView.transitionName = url
mediaDescription.text = description binding.mediaDescription.text = description
captionSheet.visible(showingDescription) binding.captionSheet.visible(showingDescription)
startedTransition = false startedTransition = false
loadImageFromNetwork(url, previewUrl, photoView) loadImageFromNetwork(url, previewUrl, binding.photoView)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar toolbar = (requireActivity() as ViewMediaActivity).toolbar
this.transition = BehaviorSubject.create() this.transition = BehaviorSubject.create()
return inflater.inflate(R.layout.fragment_view_image, container, false) _binding = FragmentViewImageBinding.inflate(inflater, container, false)
return binding.root
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -105,7 +108,7 @@ class ViewImageFragment : ViewMediaFragment() {
} }
} }
attacher = PhotoViewAttacher(photoView).apply { attacher = PhotoViewAttacher(binding.photoView).apply {
// This prevents conflicts with ViewPager // This prevents conflicts with ViewPager
setAllowParentInterceptOnEdge(true) setAllowParentInterceptOnEdge(true)
@ -127,7 +130,7 @@ class ViewImageFragment : ViewMediaFragment() {
var lastY = 0f var lastY = 0f
photoView.setOnTouchListener { v, event -> binding.photoView.setOnTouchListener { v, event ->
// This part is for scaling/translating on vertical move. // This part is for scaling/translating on vertical move.
// We use raw coordinates to get the correct ones during scaling // We use raw coordinates to get the correct ones during scaling
@ -140,11 +143,11 @@ class ViewImageFragment : ViewMediaFragment() {
val diff = event.rawY - lastY val diff = event.rawY - lastY
// This code is to prevent transformations during page scrolling // This code is to prevent transformations during page scrolling
// If we are already translating or we reached the threshold, then transform. // If we are already translating or we reached the threshold, then transform.
if (photoView.translationY != 0f || abs(diff) > 40) { if (binding.photoView.translationY != 0f || abs(diff) > 40) {
photoView.translationY += (diff) binding.photoView.translationY += (diff)
val scale = (-abs(photoView.translationY) / 720 + 1).coerceAtLeast(0.5f) val scale = (-abs(binding.photoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
photoView.scaleY = scale binding.photoView.scaleY = scale
photoView.scaleX = scale binding.photoView.scaleX = scale
lastY = event.rawY lastY = event.rawY
return@setOnTouchListener true return@setOnTouchListener true
} }
@ -158,13 +161,13 @@ class ViewImageFragment : ViewMediaFragment() {
} }
private fun onGestureEnd() { private fun onGestureEnd() {
if (photoView == null) { if (_binding == null) {
return return
} }
if (abs(photoView.translationY) > 180) { if (abs(binding.photoView.translationY) > 180) {
photoActionsListener.onDismiss() photoActionsListener.onDismiss()
} else { } else {
photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start() binding.photoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
} }
} }
@ -173,15 +176,17 @@ class ViewImageFragment : ViewMediaFragment() {
} }
override fun onToolbarVisibilityChange(visible: Boolean) { override fun onToolbarVisibilityChange(visible: Boolean) {
if (photoView == null || !userVisibleHint || captionSheet == null) { if (_binding == null || !userVisibleHint ) {
return return
} }
isDescriptionVisible = showingDescription && visible isDescriptionVisible = showingDescription && visible
val alpha = if (isDescriptionVisible) 1.0f else 0.0f val alpha = if (isDescriptionVisible) 1.0f else 0.0f
captionSheet.animate().alpha(alpha) binding.captionSheet.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
captionSheet?.visible(isDescriptionVisible) if (_binding != null) {
binding.captionSheet.visible(isDescriptionVisible)
}
animation.removeListener(this) animation.removeListener(this)
} }
}) })
@ -189,8 +194,9 @@ class ViewImageFragment : ViewMediaFragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
Glide.with(this).clear(photoView) Glide.with(this).clear(binding.photoView)
transition.onComplete() transition.onComplete()
_binding = null
super.onDestroyView() super.onDestroyView()
} }
@ -253,7 +259,7 @@ class ViewImageFragment : ViewMediaFragment() {
photoActionsListener.onBringUp() photoActionsListener.onBringUp()
} }
// Hide progress bar only on fail request from internet // Hide progress bar only on fail request from internet
if (!isCacheRequest) progressBar?.hide() if (!isCacheRequest && _binding != null) binding.progressBar.hide()
// We don't want to overwrite preview with null when main image fails to load // We don't want to overwrite preview with null when main image fails to load
return !isCacheRequest return !isCacheRequest
} }
@ -261,14 +267,16 @@ class ViewImageFragment : ViewMediaFragment() {
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>, override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>,
dataSource: DataSource, isFirstResource: Boolean): Boolean { dataSource: DataSource, isFirstResource: Boolean): Boolean {
progressBar?.hide() // Always hide the progress bar on success if (_binding != null) {
binding.progressBar.hide() // Always hide the progress bar on success
}
if (!startedTransition || !shouldStartTransition) { if (!startedTransition || !shouldStartTransition) {
// Set this right away so that we don't have to concurrent post() requests // Set this right away so that we don't have to concurrent post() requests
startedTransition = true startedTransition = true
// post() because load() replaces image with null. Sometimes after we set // post() because load() replaces image with null. Sometimes after we set
// the thumbnail. // the thumbnail.
photoView.post { binding.photoView.post {
target.onResourceReady(resource, null) target.onResourceReady(resource, null)
if (shouldStartTransition) photoActionsListener.onBringUp() if (shouldStartTransition) photoActionsListener.onBringUp()
} }

View File

@ -26,16 +26,18 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.MediaController import android.widget.MediaController
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
import kotlinx.android.synthetic.main.activity_view_media.*
import kotlinx.android.synthetic.main.fragment_view_video.*
class ViewVideoFragment : ViewMediaFragment() { class ViewVideoFragment : ViewMediaFragment() {
private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!!
private lateinit var toolbar: View private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable { private val hideToolbar = Runnable {
@ -52,7 +54,7 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun setUserVisibleHint(isVisibleToUser: Boolean) { override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden // Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser) super.setUserVisibleHint(isVisibleToUser)
if (videoView == null) { if (_binding == null) {
return return
} }
@ -60,10 +62,10 @@ class ViewVideoFragment : ViewMediaFragment() {
if (mediaActivity.isToolbarVisible) { if (mediaActivity.isToolbarVisible) {
handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS) handler.postDelayed(hideToolbar, TOOLBAR_HIDE_DELAY_MS)
} }
videoView.start() binding.videoView.start()
} else { } else {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
videoView.pause() binding.videoView.pause()
mediaController.hide() mediaController.hide()
} }
} }
@ -75,11 +77,11 @@ class ViewVideoFragment : ViewMediaFragment() {
description: String?, description: String?,
showingDescription: Boolean showingDescription: Boolean
) { ) {
mediaDescription.text = description binding.mediaDescription.text = description
mediaDescription.visible(showingDescription) binding.mediaDescription.visible(showingDescription)
videoView.transitionName = url binding.videoView.transitionName = url
videoView.setVideoPath(url) binding.videoView.setVideoPath(url)
mediaController = object : MediaController(mediaActivity) { mediaController = object : MediaController(mediaActivity) {
override fun show(timeout: Int) { override fun show(timeout: Int) {
// We're doing manual auto-close management. // We're doing manual auto-close management.
@ -100,10 +102,10 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
} }
mediaController.setMediaPlayer(videoView) mediaController.setMediaPlayer(binding.videoView)
videoView.setMediaController(mediaController) binding.videoView.setMediaController(mediaController)
videoView.requestFocus() binding.videoView.requestFocus()
videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { binding.videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener {
override fun onPause() { override fun onPause() {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
} }
@ -117,31 +119,31 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
} }
}) })
videoView.setOnPreparedListener { mp -> binding.videoView.setOnPreparedListener { mp ->
val containerWidth = videoContainer.measuredWidth.toFloat() val containerWidth = binding.videoContainer.measuredWidth.toFloat()
val containerHeight = videoContainer.measuredHeight.toFloat() val containerHeight = binding.videoContainer.measuredHeight.toFloat()
val videoWidth = mp.videoWidth.toFloat() val videoWidth = mp.videoWidth.toFloat()
val videoHeight = mp.videoHeight.toFloat() val videoHeight = mp.videoHeight.toFloat()
if(containerWidth/containerHeight > videoWidth/videoHeight) { if(containerWidth/containerHeight > videoWidth/videoHeight) {
videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT binding.videoView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT binding.videoView.layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT
} else { } else {
videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT binding.videoView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT binding.videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
} }
// Wait until the media is loaded before accepting taps as we don't want toolbar to // Wait until the media is loaded before accepting taps as we don't want toolbar to
// be hidden until then. // be hidden until then.
videoView.setOnTouchListener { _, _ -> binding.videoView.setOnTouchListener { _, _ ->
mediaActivity.onPhotoTap() mediaActivity.onPhotoTap()
false false
} }
progressBar.hide() binding.progressBar.hide()
mp.isLooping = true mp.isLooping = true
if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) {
videoView.start() binding.videoView.start()
} }
} }
@ -155,9 +157,10 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
toolbar = requireActivity().toolbar
mediaActivity = activity as ViewMediaActivity mediaActivity = activity as ViewMediaActivity
return inflater.inflate(R.layout.fragment_view_video, container, false) toolbar = mediaActivity.toolbar
_binding = FragmentViewVideoBinding.inflate(inflater, container, false)
return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -174,7 +177,7 @@ class ViewVideoFragment : ViewMediaFragment() {
} }
override fun onToolbarVisibilityChange(visible: Boolean) { override fun onToolbarVisibilityChange(visible: Boolean) {
if (videoView == null || mediaDescription == null || !userVisibleHint) { if (_binding == null || !userVisibleHint) {
return return
} }
@ -182,20 +185,22 @@ class ViewVideoFragment : ViewMediaFragment() {
val alpha = if (isDescriptionVisible) 1.0f else 0.0f val alpha = if (isDescriptionVisible) 1.0f else 0.0f
if (isDescriptionVisible) { if (isDescriptionVisible) {
// If to be visible, need to make visible immediately and animate alpha // If to be visible, need to make visible immediately and animate alpha
mediaDescription.alpha = 0.0f binding.mediaDescription.alpha = 0.0f
mediaDescription.visible(isDescriptionVisible) binding.mediaDescription.visible(isDescriptionVisible)
} }
mediaDescription.animate().alpha(alpha) binding.mediaDescription.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() { .setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { override fun onAnimationEnd(animation: Animator) {
mediaDescription?.visible(isDescriptionVisible) if (_binding != null) {
binding.mediaDescription.visible(isDescriptionVisible)
}
animation.removeListener(this) animation.removeListener(this)
} }
}) })
.start() .start()
if (visible && videoView.isPlaying && !isAudio) { if (visible && binding.videoView.isPlaying && !isAudio) {
hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS)
} else { } else {
handler.removeCallbacks(hideToolbar) handler.removeCallbacks(hideToolbar)
@ -204,4 +209,9 @@ class ViewVideoFragment : ViewMediaFragment() {
override fun onTransitionEnd() { override fun onTransitionEnd() {
} }
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
} }

View File

@ -1,15 +1,67 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/** /**
* https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c * https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
*/ */
inline fun <T : ViewBinding> AppCompatActivity.viewBinding( inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T crossinline bindingInflater: (LayoutInflater) -> T
) = lazy(LazyThreadSafetyMode.NONE) { ) = lazy(LazyThreadSafetyMode.NONE) {
bindingInflater(layoutInflater) bindingInflater(layoutInflater)
} }
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
init {
fragment.lifecycle.addObserver(
object : DefaultLifecycleObserver {
override fun onCreate(owner: LifecycleOwner) {
fragment.viewLifecycleOwnerLiveData.observe(
fragment,
{ t ->
t?.lifecycle?.addObserver(
object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
binding = null
}
}
)
}
)
}
}
)
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val binding = binding
if (binding != null) {
return binding
}
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
}
return viewBindingFactory(thisRef.requireView()).also { this@FragmentViewBindingDelegate.binding = it }
}
}
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layoutRoot"
android:layout_width="@dimen/timeline_width" android:layout_width="@dimen/timeline_width"
android:layout_height="match_parent"> android:layout_height="match_parent">

View File

@ -32,12 +32,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" android:visibility="gone"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" tools:visibility="visible" />
app:layout_constrainedHeight="true" />
<androidx.core.widget.ContentLoadingProgressBar <androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/topProgressBar" android:id="@+id/topProgressBar"
@ -50,4 +50,5 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>