Refactor server tabs in HomeActivity
- Switched from Fragments to simple Views for pager - Moved the whole implementation to ServerTabs.kt, instead of splitting across multiple files - Uniformly name adapters & layout files
This commit is contained in:
parent
69dec7ed78
commit
1ebb97a78f
16 changed files with 279 additions and 384 deletions
|
|
@ -11,7 +11,10 @@ package com.gaurav.avnc
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
import com.gaurav.avnc.model.ServerProfile
|
||||||
import com.gaurav.avnc.model.db.MainDb
|
import com.gaurav.avnc.model.db.MainDb
|
||||||
import com.gaurav.avnc.ui.about.AboutActivity
|
import com.gaurav.avnc.ui.about.AboutActivity
|
||||||
import com.gaurav.avnc.ui.home.*
|
import com.gaurav.avnc.ui.home.HomeActivity
|
||||||
|
import com.gaurav.avnc.ui.home.ProfileEditorFragment
|
||||||
|
import com.gaurav.avnc.ui.home.ServerTabs
|
||||||
|
import com.gaurav.avnc.ui.home.UrlBarActivity
|
||||||
import com.gaurav.avnc.ui.prefs.PrefsActivity
|
import com.gaurav.avnc.ui.prefs.PrefsActivity
|
||||||
import com.gaurav.avnc.ui.vnc.*
|
import com.gaurav.avnc.ui.vnc.*
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
import com.gaurav.avnc.viewmodel.HomeViewModel
|
||||||
|
|
@ -68,8 +71,8 @@ import com.gaurav.avnc.vnc.VncClient
|
||||||
* - A urlbar, which launches [UrlBarActivity], allowing user to quickly connect
|
* - A urlbar, which launches [UrlBarActivity], allowing user to quickly connect
|
||||||
* to a server without creating a profile for it.
|
* to a server without creating a profile for it.
|
||||||
*
|
*
|
||||||
* - List of saved servers, shown by [ServersFragment].
|
* - Lists of saved & discovered servers, in [ServerTabs].
|
||||||
* - List of servers found by [Discovery], shown by [DiscoveryFragment].
|
*
|
||||||
* - [ProfileEditorFragment], used for creating/editing [ServerProfile].
|
* - [ProfileEditorFragment], used for creating/editing [ServerProfile].
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.gaurav.avnc.databinding.FragmentDiscoveryBinding
|
|
||||||
import com.gaurav.avnc.ui.home.adapter.DiscoveryAdapter
|
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment for displaying VNC servers found by Discovery.
|
|
||||||
*/
|
|
||||||
class DiscoveryFragment : Fragment() {
|
|
||||||
|
|
||||||
val viewModel by activityViewModels<HomeViewModel>()
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
val binding = FragmentDiscoveryBinding.inflate(inflater, container, false)
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
val adapter = DiscoveryAdapter(viewModel)
|
|
||||||
binding.discoveredRv.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
binding.discoveredRv.adapter = adapter
|
|
||||||
binding.discoveredRv.setHasFixedSize(true)
|
|
||||||
|
|
||||||
viewModel.discovery.servers.observe(viewLifecycleOwner) { adapter.submitList(it) }
|
|
||||||
|
|
||||||
launchRestarter()
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchRestarter() = lifecycleScope.launch(Dispatchers.Main) {
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
delay(viewModel.pref.server.discoveryTimeout + viewModel.pref.server.discoveryRestartDelay)
|
|
||||||
|
|
||||||
if (activity?.lifecycle?.currentState == Lifecycle.State.RESUMED &&
|
|
||||||
viewModel.pref.server.discoveryAutoStart &&
|
|
||||||
viewModel.pref.server.discoveryRestart) {
|
|
||||||
viewModel.startDiscovery()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -34,7 +34,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
class HomeActivity : AppCompatActivity() {
|
class HomeActivity : AppCompatActivity() {
|
||||||
val viewModel by viewModels<HomeViewModel>()
|
val viewModel by viewModels<HomeViewModel>()
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var tabController: TabController
|
private val tabs = ServerTabs(this)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
@ -44,7 +44,8 @@ class HomeActivity : AppCompatActivity() {
|
||||||
binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
|
binding = DataBindingUtil.setContentView(this, R.layout.activity_home)
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
|
|
||||||
tabController = TabController(this, binding.pager, binding.tabLayout)
|
tabs.create(binding.tabLayout, binding.pager)
|
||||||
|
|
||||||
binding.drawerNav.setNavigationItemSelectedListener { onMenuItemSelected(it.itemId) }
|
binding.drawerNav.setNavigationItemSelectedListener { onMenuItemSelected(it.itemId) }
|
||||||
binding.navigationBtn.setOnClickListener { binding.drawerLayout.open() }
|
binding.navigationBtn.setOnClickListener { binding.drawerLayout.open() }
|
||||||
binding.settingsBtn.setOnClickListener { showSettings() }
|
binding.settingsBtn.setOnClickListener { showSettings() }
|
||||||
|
|
@ -107,7 +108,7 @@ class HomeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onProfileInserted(profile: ServerProfile) {
|
private fun onProfileInserted(profile: ServerProfile) {
|
||||||
tabController.showSavedServers()
|
tabs.showSavedServers()
|
||||||
|
|
||||||
// Show snackbar for new servers
|
// Show snackbar for new servers
|
||||||
if (profile.ID == 0L)
|
if (profile.ID == 0L)
|
||||||
|
|
@ -124,6 +125,6 @@ class HomeActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDiscoveryBadge(list: List<ServerProfile>) {
|
private fun updateDiscoveryBadge(list: List<ServerProfile>) {
|
||||||
tabController.updateDiscoveryBadge(list.size)
|
tabs.updateDiscoveryBadge(list.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
255
app/src/main/java/com/gaurav/avnc/ui/home/ServerTabs.kt
Normal file
255
app/src/main/java/com/gaurav/avnc/ui/home/ServerTabs.kt
Normal file
|
|
@ -0,0 +1,255 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2021 Gaurav Ujjwal.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* See COPYING.txt for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.gaurav.avnc.ui.home
|
||||||
|
|
||||||
|
import android.view.*
|
||||||
|
import androidx.core.view.forEach
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewpager2.widget.ViewPager2
|
||||||
|
import com.gaurav.avnc.R
|
||||||
|
import com.gaurav.avnc.databinding.ServerDiscoveryBinding
|
||||||
|
import com.gaurav.avnc.databinding.ServerDiscoveryItemBinding
|
||||||
|
import com.gaurav.avnc.databinding.ServerSavedBinding
|
||||||
|
import com.gaurav.avnc.databinding.ServerSavedItemBinding
|
||||||
|
import com.gaurav.avnc.model.ServerProfile
|
||||||
|
import com.gaurav.avnc.ui.home.ServerTabs.PagerAdapter.ViewHolder
|
||||||
|
import com.gaurav.avnc.util.Experimental
|
||||||
|
import com.gaurav.avnc.viewmodel.HomeViewModel
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class creates and manages tabs in [HomeActivity].
|
||||||
|
* Tabs:
|
||||||
|
* 1. Saved servers
|
||||||
|
* 2. Discovered servers
|
||||||
|
*/
|
||||||
|
class ServerTabs(val activity: HomeActivity) {
|
||||||
|
|
||||||
|
lateinit var savedServersTab: TabLayout.Tab
|
||||||
|
lateinit var discoveredServersTab: TabLayout.Tab
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and initializes tabs
|
||||||
|
*
|
||||||
|
* [tabLayout] Hosts tabs
|
||||||
|
* [pager] Hosts actual content views
|
||||||
|
*/
|
||||||
|
fun create(tabLayout: TabLayout, pager: ViewPager2) {
|
||||||
|
pager.adapter = PagerAdapter()
|
||||||
|
pager.offscreenPageLimit = 1 // Tell pager to initialize & keep both tabs in memory
|
||||||
|
|
||||||
|
TabLayoutMediator(tabLayout, pager) { _, _ -> }
|
||||||
|
.attach()
|
||||||
|
|
||||||
|
savedServersTab = tabLayout.getTabAt(0)!!
|
||||||
|
savedServersTab.setIcon(R.drawable.ic_computer)
|
||||||
|
savedServersTab.setContentDescription(R.string.desc_saved_servers_tab)
|
||||||
|
|
||||||
|
discoveredServersTab = tabLayout.getTabAt(1)!!
|
||||||
|
discoveredServersTab.setIcon(R.drawable.ic_search)
|
||||||
|
discoveredServersTab.setContentDescription(R.string.desc_discovered_servers_tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSavedServers() {
|
||||||
|
savedServersTab.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDiscoveryBadge(count: Int) {
|
||||||
|
//Currently, we are not showing the actual count in the badge.
|
||||||
|
//But maybe we could implement a preference???
|
||||||
|
discoveredServersTab.getOrCreateBadge().isVisible = (count != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for our pager.
|
||||||
|
*
|
||||||
|
* We have fixed number of static views so our implementation is really simple.
|
||||||
|
* We override [getItemViewType] to use the given position itself as view type.
|
||||||
|
* Then, in [onCreateViewHolder], that view type (i.e. position) is used to
|
||||||
|
* generate corresponding view.
|
||||||
|
*
|
||||||
|
* As a result, nothing else needs to be done by [ViewHolder] & [bindViewHolder].
|
||||||
|
*/
|
||||||
|
private inner class PagerAdapter : RecyclerView.Adapter<PagerAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v)
|
||||||
|
|
||||||
|
override fun getItemCount() = 2
|
||||||
|
override fun getItemViewType(position: Int) = position
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
return when (viewType) {
|
||||||
|
0 -> ViewHolder(createSavedServersView(parent))
|
||||||
|
1 -> ViewHolder(createDiscoveredServersView(parent))
|
||||||
|
else -> throw IllegalStateException("Unexpected view type: [$viewType]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************************************************************************
|
||||||
|
* Saved servers
|
||||||
|
**********************************************************************************************/
|
||||||
|
|
||||||
|
private fun createSavedServersView(parent: ViewGroup): View {
|
||||||
|
val binding = ServerSavedBinding.inflate(activity.layoutInflater, parent, false)
|
||||||
|
binding.lifecycleOwner = activity
|
||||||
|
binding.viewModel = activity.viewModel
|
||||||
|
|
||||||
|
val adapter = SavedServerAdapter(activity.viewModel)
|
||||||
|
binding.serversRv.layoutManager = LinearLayoutManager(activity)
|
||||||
|
binding.serversRv.adapter = adapter
|
||||||
|
binding.serversRv.setHasFixedSize(true)
|
||||||
|
|
||||||
|
activity.viewModel.serverProfiles.observe(activity) { adapter.submitList(it) }
|
||||||
|
|
||||||
|
if (activity.viewModel.pref.experimental.indicator) {
|
||||||
|
Experimental.Indicator().setup(binding, adapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter for saved servers
|
||||||
|
*/
|
||||||
|
class SavedServerAdapter(val viewModel: HomeViewModel)
|
||||||
|
: ListAdapter<ServerProfile, SavedServerAdapter.ViewHolder>(Differ) {
|
||||||
|
|
||||||
|
//Experimental feature
|
||||||
|
var bindListener: ((ViewHolder, ServerProfile) -> Unit)? = null
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = ServerSavedItemBinding.inflate(inflater, parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val profile = getItem(position)
|
||||||
|
holder.profile = profile
|
||||||
|
holder.binding.viewModel = profile
|
||||||
|
bindListener?.invoke(holder, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ServerSavedItemBinding)
|
||||||
|
: ProfileViewHolder(viewModel, binding.root, R.menu.saved_server)
|
||||||
|
|
||||||
|
object Differ : DiffUtil.ItemCallback<ServerProfile>() {
|
||||||
|
override fun areItemsTheSame(old: ServerProfile, new: ServerProfile) = (old.ID == new.ID)
|
||||||
|
override fun areContentsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**********************************************************************************************
|
||||||
|
* Discovered servers
|
||||||
|
**********************************************************************************************/
|
||||||
|
|
||||||
|
private fun createDiscoveredServersView(parent: ViewGroup): View {
|
||||||
|
val binding = ServerDiscoveryBinding.inflate(activity.layoutInflater, parent, false)
|
||||||
|
binding.lifecycleOwner = activity
|
||||||
|
binding.viewModel = activity.viewModel
|
||||||
|
|
||||||
|
val adapter = DiscoveredServerAdapter(activity.viewModel)
|
||||||
|
binding.discoveredRv.layoutManager = LinearLayoutManager(activity)
|
||||||
|
binding.discoveredRv.adapter = adapter
|
||||||
|
binding.discoveredRv.setHasFixedSize(true)
|
||||||
|
|
||||||
|
activity.viewModel.discovery.servers.observe(activity) { adapter.submitList(it) }
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DiscoveredServerAdapter(val viewModel: HomeViewModel)
|
||||||
|
: ListAdapter<ServerProfile, DiscoveredServerAdapter.ViewHolder>(Differ) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = ServerDiscoveryItemBinding.inflate(inflater, parent, false)
|
||||||
|
return ViewHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val profile = getItem(position)
|
||||||
|
holder.profile = profile
|
||||||
|
holder.binding.viewModel = profile
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(val binding: ServerDiscoveryItemBinding) :
|
||||||
|
ProfileViewHolder(viewModel, binding.root, R.menu.discovered_server) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.saveBtn.setOnClickListener { viewModel.onNewProfile(profile) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Profiles generated by discovery don't have unique IDs, so we compare the whole profile.
|
||||||
|
*/
|
||||||
|
object Differ : DiffUtil.ItemCallback<ServerProfile>() {
|
||||||
|
override fun areItemsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
||||||
|
override fun areContentsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base ViewHolder for [ServerProfile], used by both adapters.
|
||||||
|
*/
|
||||||
|
open class ProfileViewHolder(
|
||||||
|
private val homeViewModel: HomeViewModel,
|
||||||
|
private val rootView: View,
|
||||||
|
private val contextMenuId: Int)
|
||||||
|
: RecyclerView.ViewHolder(rootView) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Points to the profile being rendered by this view holder.
|
||||||
|
* Updated by adapters during onBindViewHolder().
|
||||||
|
*/
|
||||||
|
var profile = ServerProfile()
|
||||||
|
|
||||||
|
init {
|
||||||
|
rootView.setOnClickListener { homeViewModel.startConnection(profile) }
|
||||||
|
|
||||||
|
rootView.setOnCreateContextMenuListener { contextMenu, view, _ ->
|
||||||
|
MenuInflater(view.context).inflate(contextMenuId, contextMenu)
|
||||||
|
contextMenu.forEach { item ->
|
||||||
|
item.setOnMenuItemClickListener { onContextMenuItemClick(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onContextMenuItemClick(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.edit -> homeViewModel.onEditProfile(profile)
|
||||||
|
R.id.duplicate -> homeViewModel.onDuplicateProfile(profile)
|
||||||
|
R.id.delete -> homeViewModel.deleteProfile(profile)
|
||||||
|
R.id.copy_host -> copyToClipboard(profile.host)
|
||||||
|
R.id.copy_name -> copyToClipboard(profile.name)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyToClipboard(text: String) {
|
||||||
|
homeViewModel.setClipboardText(text)
|
||||||
|
Snackbar.make(rootView, R.string.msg_copied_to_clipboard, Snackbar.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.gaurav.avnc.databinding.FragmentServersBinding
|
|
||||||
import com.gaurav.avnc.ui.home.adapter.ServersAdapter
|
|
||||||
import com.gaurav.avnc.util.Experimental
|
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment for displaying list of known servers.
|
|
||||||
*/
|
|
||||||
class ServersFragment : Fragment() {
|
|
||||||
|
|
||||||
val viewModel by activityViewModels<HomeViewModel>()
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
val binding = FragmentServersBinding.inflate(inflater, container, false)
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
val adapter = ServersAdapter(viewModel)
|
|
||||||
binding.serversRv.layoutManager = LinearLayoutManager(context)
|
|
||||||
binding.serversRv.adapter = adapter
|
|
||||||
binding.serversRv.setHasFixedSize(true)
|
|
||||||
|
|
||||||
viewModel.serverProfiles.observe(viewLifecycleOwner) { adapter.submitList(it) }
|
|
||||||
|
|
||||||
if (viewModel.pref.experimental.indicator) {
|
|
||||||
Experimental.Indicator().setup(binding, adapter)
|
|
||||||
}
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2021 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.gaurav.avnc.R
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controls tab layout and associated pager in home activity.
|
|
||||||
*/
|
|
||||||
class TabController(val activity: HomeActivity, private val pager: ViewPager2, tabLayout: TabLayout) {
|
|
||||||
|
|
||||||
private data class PageInfo(
|
|
||||||
val fragment: Fragment,
|
|
||||||
@DrawableRes val iconId: Int,
|
|
||||||
@StringRes val descriptionId: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
private val pageList = listOf(
|
|
||||||
PageInfo(ServersFragment(), R.drawable.ic_computer, R.string.desc_saved_servers_tab),
|
|
||||||
PageInfo(DiscoveryFragment(), R.drawable.ic_search, R.string.desc_discovered_servers_tab),
|
|
||||||
)
|
|
||||||
|
|
||||||
private inner class PagerAdapter : FragmentStateAdapter(activity) {
|
|
||||||
override fun getItemCount() = pageList.size
|
|
||||||
override fun createFragment(position: Int) = pageList[position].fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val discoveryTab: TabLayout.Tab
|
|
||||||
|
|
||||||
init {
|
|
||||||
pager.adapter = PagerAdapter()
|
|
||||||
|
|
||||||
// Ask the the pager to keep both fragments in memory
|
|
||||||
// This allows smoother transition between fragments
|
|
||||||
pager.offscreenPageLimit = 1
|
|
||||||
|
|
||||||
val mediator = TabLayoutMediator(tabLayout, pager) { tab, position ->
|
|
||||||
tab.setIcon(pageList[position].iconId)
|
|
||||||
tab.setContentDescription(pageList[position].descriptionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
mediator.attach()
|
|
||||||
discoveryTab = tabLayout.getTabAt(1)!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSavedServers() {
|
|
||||||
pager.setCurrentItem(0, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDiscoveryBadge(count: Int) {
|
|
||||||
//Currently, we are not showing the actual count in the badge.
|
|
||||||
//But maybe we could implement a preference???
|
|
||||||
discoveryTab.getOrCreateBadge().isVisible = (count != 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import com.gaurav.avnc.R
|
|
||||||
import com.gaurav.avnc.databinding.ItemDiscoveryBinding
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
|
||||||
|
|
||||||
class DiscoveryAdapter(val viewModel: HomeViewModel) : ListAdapter<ServerProfile, DiscoveryAdapter.ViewHolder>(Differ) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = ItemDiscoveryBinding.inflate(inflater, parent, false)
|
|
||||||
return ViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
holder.bind(getItem(position))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View holder for discovered servers.
|
|
||||||
*/
|
|
||||||
inner class ViewHolder(binding: ItemDiscoveryBinding) :
|
|
||||||
ProfileViewHolder(viewModel, binding, R.menu.discovered_server) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
binding.saveBtn.setOnClickListener { viewModel.onNewProfile(profile) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is slightly different from [ServersAdapter.Differ] because all profiles generated
|
|
||||||
* by service discovery have same ID (0) & all discovered servers are unique. So we
|
|
||||||
* compare the whole profile instead of a particular field.
|
|
||||||
*/
|
|
||||||
object Differ : DiffUtil.ItemCallback<ServerProfile>() {
|
|
||||||
override fun areItemsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
|
||||||
override fun areContentsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home.adapter
|
|
||||||
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.core.view.forEach
|
|
||||||
import androidx.databinding.ViewDataBinding
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import com.gaurav.avnc.BR
|
|
||||||
import com.gaurav.avnc.R
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ViewHolder for [ServerProfile].
|
|
||||||
*/
|
|
||||||
open class ProfileViewHolder(
|
|
||||||
private val homeViewModel: HomeViewModel,
|
|
||||||
val binding: ViewDataBinding,
|
|
||||||
private val menuId: Int)
|
|
||||||
: RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Points to the profile being rendered by this view holder.
|
|
||||||
* See [bind].
|
|
||||||
*/
|
|
||||||
var profile = ServerProfile()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val root = binding.root
|
|
||||||
|
|
||||||
root.setOnClickListener {
|
|
||||||
homeViewModel.startConnection(profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
root.setOnCreateContextMenuListener { contextMenu, view, _ ->
|
|
||||||
MenuInflater(view.context).inflate(menuId, contextMenu)
|
|
||||||
contextMenu.forEach { item ->
|
|
||||||
item.setOnMenuItemClickListener { onContextOptionClick(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle context menu events
|
|
||||||
*/
|
|
||||||
private fun onContextOptionClick(item: MenuItem): Boolean {
|
|
||||||
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.edit -> homeViewModel.onEditProfile(profile)
|
|
||||||
R.id.duplicate -> homeViewModel.onDuplicateProfile(profile)
|
|
||||||
R.id.delete -> homeViewModel.deleteProfile(profile)
|
|
||||||
|
|
||||||
R.id.copy_host -> {
|
|
||||||
homeViewModel.setClipboardText(profile.host)
|
|
||||||
showCopiedMsg()
|
|
||||||
}
|
|
||||||
R.id.copy_name -> {
|
|
||||||
homeViewModel.setClipboardText(profile.name)
|
|
||||||
showCopiedMsg()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCopiedMsg() {
|
|
||||||
Snackbar.make(binding.root, R.string.msg_copied_to_clipboard, Snackbar.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binds given profile to this view holder.
|
|
||||||
*/
|
|
||||||
fun bind(profile: ServerProfile) {
|
|
||||||
this.profile = profile
|
|
||||||
binding.setVariable(BR.viewModel, profile)
|
|
||||||
binding.executePendingBindings()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2020 Gaurav Ujjwal.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* See COPYING.txt for more details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.gaurav.avnc.ui.home.adapter
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import com.gaurav.avnc.R
|
|
||||||
import com.gaurav.avnc.databinding.ItemServerBinding
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
|
||||||
import com.gaurav.avnc.viewmodel.HomeViewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for known servers
|
|
||||||
*/
|
|
||||||
class ServersAdapter(val viewModel: HomeViewModel)
|
|
||||||
: ListAdapter<ServerProfile, ProfileViewHolder>(Differ) {
|
|
||||||
|
|
||||||
//Experimental feature
|
|
||||||
var bindListener: ((ProfileViewHolder, ServerProfile) -> Unit)? = null
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
|
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = ItemServerBinding.inflate(inflater, parent, false)
|
|
||||||
return ProfileViewHolder(viewModel, binding, R.menu.server_profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
|
|
||||||
val profile = getItem(position)
|
|
||||||
holder.bind(profile)
|
|
||||||
bindListener?.invoke(holder, profile)
|
|
||||||
}
|
|
||||||
|
|
||||||
object Differ : DiffUtil.ItemCallback<ServerProfile>() {
|
|
||||||
override fun areItemsTheSame(old: ServerProfile, new: ServerProfile) = (old.ID == new.ID)
|
|
||||||
override fun areContentsTheSame(old: ServerProfile, new: ServerProfile) = (old == new)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -21,10 +21,9 @@ import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleObserver
|
import androidx.lifecycle.LifecycleObserver
|
||||||
import androidx.lifecycle.OnLifecycleEvent
|
import androidx.lifecycle.OnLifecycleEvent
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.gaurav.avnc.databinding.FragmentServersBinding
|
import com.gaurav.avnc.databinding.ServerSavedBinding
|
||||||
import com.gaurav.avnc.databinding.ItemServerBinding
|
|
||||||
import com.gaurav.avnc.model.ServerProfile
|
import com.gaurav.avnc.model.ServerProfile
|
||||||
import com.gaurav.avnc.ui.home.adapter.ServersAdapter
|
import com.gaurav.avnc.ui.home.ServerTabs.SavedServerAdapter
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All the experimental stuff is dumped here.
|
* All the experimental stuff is dumped here.
|
||||||
|
|
@ -83,7 +82,7 @@ object Experimental {
|
||||||
*
|
*
|
||||||
* Note: Viewmodel & lifecycle must have been assigned to [binding].
|
* Note: Viewmodel & lifecycle must have been assigned to [binding].
|
||||||
*/
|
*/
|
||||||
fun setup(binding: FragmentServersBinding, adapter: ServersAdapter) {
|
fun setup(binding: ServerSavedBinding, adapter: SavedServerAdapter) {
|
||||||
Log.i(javaClass.simpleName, "Setting up Experimental Indicator")
|
Log.i(javaClass.simpleName, "Setting up Experimental Indicator")
|
||||||
|
|
||||||
// There are 4 event sources which we are interested in:
|
// There are 4 event sources which we are interested in:
|
||||||
|
|
@ -100,11 +99,9 @@ object Experimental {
|
||||||
|
|
||||||
// 2 - RV Binding: We need to know which RV Item View is rendering which profile.
|
// 2 - RV Binding: We need to know which RV Item View is rendering which profile.
|
||||||
adapter.bindListener = { holder, profile ->
|
adapter.bindListener = { holder, profile ->
|
||||||
if (holder.binding is ItemServerBinding) {
|
val view = holder.binding.indicator
|
||||||
val view = holder.binding.indicator
|
profileMap[view] = profile
|
||||||
profileMap[view] = profile
|
animatorMap[view] = isServerDiscovered(profile)
|
||||||
animatorMap[view] = isServerDiscovered(profile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3 - RV Views: We need to clear Views which are no longer used by RecyclerView.
|
// 3 - RV Views: We need to clear Views which are no longer used by RecyclerView.
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/desc_discovered_list"
|
android:contentDescription="@string/desc_discovered_list"
|
||||||
tools:listitem="@layout/item_discovery" />
|
tools:listitem="@layout/server_discovery_item" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/TipView"
|
style="@style/TipView"
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
layout="@layout/server_profile"
|
layout="@layout/server_item"
|
||||||
app:profile="@{viewModel}" />
|
app:profile="@{viewModel}" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
~ See COPYING.txt for more details.
|
~ See COPYING.txt for more details.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
- This is common server view, used by both saved and discovered server items.
|
||||||
|
-->
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:contentDescription="@string/desc_saved_servers_list"
|
android:contentDescription="@string/desc_saved_servers_list"
|
||||||
tools:listitem="@layout/item_server" />
|
tools:listitem="@layout/server_saved_item" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/TipView"
|
style="@style/TipView"
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
layout="@layout/server_profile"
|
layout="@layout/server_item"
|
||||||
app:profile="@{viewModel}" />
|
app:profile="@{viewModel}" />
|
||||||
|
|
||||||
<!--Experimental Feature-->
|
<!--Experimental Feature-->
|
||||||
Loading…
Add table
Reference in a new issue