2020-04-05 11:50:47 -07:00
|
|
|
package com.geeksville.mesh.ui
|
|
|
|
|
|
2024-02-13 14:32:52 -07:00
|
|
|
import android.animation.ValueAnimator
|
2023-04-13 17:54:52 -03:00
|
|
|
import android.content.res.ColorStateList
|
2024-02-13 14:32:52 -07:00
|
|
|
import android.graphics.Color
|
2020-04-08 15:25:57 -07:00
|
|
|
import android.os.Bundle
|
2023-08-25 19:14:24 -03:00
|
|
|
import android.text.SpannableString
|
|
|
|
|
import android.text.style.StrikethroughSpan
|
2020-04-08 15:25:57 -07:00
|
|
|
import android.view.LayoutInflater
|
2022-09-30 15:57:04 -03:00
|
|
|
import android.view.MenuItem
|
2020-04-08 15:25:57 -07:00
|
|
|
import android.view.View
|
|
|
|
|
import android.view.ViewGroup
|
2024-02-13 14:32:52 -07:00
|
|
|
import android.view.animation.LinearInterpolator
|
2022-09-30 15:57:04 -03:00
|
|
|
import androidx.appcompat.widget.PopupMenu
|
2024-02-13 14:32:52 -07:00
|
|
|
import androidx.core.animation.doOnEnd
|
2020-04-08 15:25:57 -07:00
|
|
|
import androidx.fragment.app.activityViewModels
|
2023-04-16 06:16:41 -03:00
|
|
|
import androidx.lifecycle.asLiveData
|
2024-02-13 14:32:52 -07:00
|
|
|
import androidx.lifecycle.lifecycleScope
|
2020-04-08 15:25:57 -07:00
|
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
2024-02-13 14:32:52 -07:00
|
|
|
import androidx.recyclerview.widget.LinearSmoothScroller
|
2020-04-08 15:25:57 -07:00
|
|
|
import androidx.recyclerview.widget.RecyclerView
|
|
|
|
|
import com.geeksville.mesh.NodeInfo
|
2024-02-27 14:43:47 -07:00
|
|
|
import com.geeksville.mesh.Position
|
2020-04-05 11:50:47 -07:00
|
|
|
import com.geeksville.mesh.R
|
2022-09-30 15:57:04 -03:00
|
|
|
import com.geeksville.mesh.android.Logging
|
2020-12-07 20:33:29 +08:00
|
|
|
import com.geeksville.mesh.databinding.AdapterNodeLayoutBinding
|
|
|
|
|
import com.geeksville.mesh.databinding.NodelistFragmentBinding
|
2020-04-08 15:25:57 -07:00
|
|
|
import com.geeksville.mesh.model.UIViewModel
|
2024-02-26 15:19:32 -07:00
|
|
|
import com.geeksville.mesh.ui.theme.AppTheme
|
2022-09-30 15:57:04 -03:00
|
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
2022-02-08 13:50:21 -08:00
|
|
|
import dagger.hilt.android.AndroidEntryPoint
|
2024-02-13 14:32:52 -07:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
|
import kotlinx.coroutines.withContext
|
2020-04-08 15:25:57 -07:00
|
|
|
|
2022-02-08 13:50:21 -08:00
|
|
|
@AndroidEntryPoint
|
2020-04-08 15:25:57 -07:00
|
|
|
class UsersFragment : ScreenFragment("Users"), Logging {
|
|
|
|
|
|
2020-12-07 20:33:29 +08:00
|
|
|
private var _binding: NodelistFragmentBinding? = null
|
2021-03-15 23:46:53 +05:00
|
|
|
|
2020-12-07 20:33:29 +08:00
|
|
|
// This property is only valid between onCreateView and onDestroyView.
|
|
|
|
|
private val binding get() = _binding!!
|
|
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
private val model: UIViewModel by activityViewModels()
|
|
|
|
|
|
2023-10-08 21:16:38 -03:00
|
|
|
private val ignoreIncomingList: MutableList<Int> = mutableListOf()
|
|
|
|
|
private var gpsFormat = 0
|
|
|
|
|
private var displayUnits = 0
|
|
|
|
|
private var displayFahrenheit = false
|
|
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
// Provide a direct reference to each of the views within a data item
|
|
|
|
|
// Used to cache the views within the item layout for fast access
|
2020-12-07 20:33:29 +08:00
|
|
|
class ViewHolder(itemView: AdapterNodeLayoutBinding) : RecyclerView.ViewHolder(itemView.root) {
|
2022-09-27 16:29:41 -03:00
|
|
|
val chipNode = itemView.chipNode
|
2020-04-08 15:25:57 -07:00
|
|
|
val nodeNameView = itemView.nodeNameView
|
2020-12-07 20:33:29 +08:00
|
|
|
val distanceView = itemView.distanceView
|
2022-09-12 18:23:59 -03:00
|
|
|
val envMetrics = itemView.envMetrics
|
2024-02-13 14:32:52 -07:00
|
|
|
val background = itemView.nodeCard
|
2024-02-27 14:43:47 -07:00
|
|
|
val nodePosition = itemView.nodePosition
|
2024-02-26 15:19:32 -07:00
|
|
|
val batteryInfo = itemView.batteryInfo
|
2024-02-28 07:29:13 -07:00
|
|
|
val lastHeard = itemView.lastHeardInfo
|
2024-02-28 08:43:29 -07:00
|
|
|
val signalInfo = itemView.signalInfo
|
2024-02-13 14:32:52 -07:00
|
|
|
|
|
|
|
|
fun blink() {
|
|
|
|
|
val bg = background.backgroundTintList
|
|
|
|
|
ValueAnimator.ofArgb(
|
|
|
|
|
Color.parseColor("#00FFFFFF"),
|
|
|
|
|
Color.parseColor("#33FFFFFF")
|
|
|
|
|
).apply {
|
|
|
|
|
interpolator = LinearInterpolator()
|
|
|
|
|
startDelay = 500
|
|
|
|
|
duration = 250
|
|
|
|
|
repeatCount = 3
|
|
|
|
|
repeatMode = ValueAnimator.REVERSE
|
|
|
|
|
addUpdateListener {
|
|
|
|
|
background.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int)
|
|
|
|
|
}
|
|
|
|
|
start()
|
|
|
|
|
doOnEnd {
|
|
|
|
|
background.backgroundTintList = bg
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-26 15:19:32 -07:00
|
|
|
|
2024-02-27 14:43:47 -07:00
|
|
|
fun bind(
|
2024-02-28 08:43:29 -07:00
|
|
|
nodeInfo: NodeInfo,
|
|
|
|
|
isThisNode: Boolean,
|
2024-02-27 14:43:47 -07:00
|
|
|
gpsFormat: Int,
|
|
|
|
|
) {
|
2024-02-26 15:19:32 -07:00
|
|
|
batteryInfo.setContent {
|
|
|
|
|
AppTheme {
|
2024-02-28 08:43:29 -07:00
|
|
|
BatteryInfo(nodeInfo.batteryLevel, nodeInfo.voltage)
|
2024-02-26 15:19:32 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-27 14:43:47 -07:00
|
|
|
nodePosition.setContent {
|
|
|
|
|
AppTheme {
|
2024-02-28 17:50:50 -03:00
|
|
|
LinkedCoordinates(nodeInfo.validPosition, gpsFormat, nodeInfo.user?.longName)
|
2024-02-27 14:43:47 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-28 07:29:13 -07:00
|
|
|
this.lastHeard.setContent {
|
|
|
|
|
AppTheme {
|
2024-02-28 08:43:29 -07:00
|
|
|
LastHeardInfo(nodeInfo.lastHeard)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.signalInfo.setContent {
|
|
|
|
|
AppTheme {
|
|
|
|
|
SignalInfo(nodeInfo, isThisNode)
|
2024-02-28 07:29:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-26 15:19:32 -07:00
|
|
|
}
|
2020-04-08 15:25:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private val nodesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
|
|
|
|
|
|
2024-02-13 14:32:52 -07:00
|
|
|
var nodes = arrayOf<NodeInfo>()
|
|
|
|
|
private set
|
2023-09-04 18:40:21 -03:00
|
|
|
|
2023-08-25 19:14:24 -03:00
|
|
|
private fun CharSequence.strike() = SpannableString(this).apply {
|
|
|
|
|
setSpan(StrikethroughSpan(), 0, this.length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun CharSequence.strikeIf(isIgnored: Boolean) = if (isIgnored) strike() else this
|
2022-09-30 15:57:04 -03:00
|
|
|
|
|
|
|
|
private fun popup(view: View, position: Int) {
|
2023-04-22 12:06:25 -03:00
|
|
|
if (!model.isConnected()) return
|
2022-09-30 15:57:04 -03:00
|
|
|
val node = nodes[position]
|
2023-08-25 17:02:12 -03:00
|
|
|
val user = node.user ?: return
|
2022-10-10 18:09:20 -03:00
|
|
|
val showAdmin = position == 0 || model.adminChannelIndex > 0
|
2023-08-25 19:14:24 -03:00
|
|
|
val isIgnored = ignoreIncomingList.contains(node.num)
|
2022-09-30 15:57:04 -03:00
|
|
|
val popup = PopupMenu(requireContext(), view)
|
|
|
|
|
popup.inflate(R.menu.menu_nodes)
|
2022-11-06 17:46:57 -03:00
|
|
|
popup.menu.setGroupVisible(R.id.group_remote, position > 0)
|
2022-09-30 15:57:04 -03:00
|
|
|
popup.menu.setGroupVisible(R.id.group_admin, showAdmin)
|
2023-05-13 18:14:47 -03:00
|
|
|
popup.menu.setGroupEnabled(R.id.group_admin, !model.isManaged)
|
2023-08-25 19:14:24 -03:00
|
|
|
popup.menu.findItem(R.id.ignore).apply {
|
2023-08-31 15:43:30 -03:00
|
|
|
isEnabled = isIgnored || ignoreIncomingList.size < 3
|
2023-08-25 19:14:24 -03:00
|
|
|
isChecked = isIgnored
|
|
|
|
|
}
|
2022-09-30 15:57:04 -03:00
|
|
|
popup.setOnMenuItemClickListener { item: MenuItem ->
|
|
|
|
|
when (item.itemId) {
|
|
|
|
|
R.id.direct_message -> {
|
2024-01-17 19:34:55 -03:00
|
|
|
debug("calling MessagesFragment filter: ${node.channel}${user.id}")
|
2024-02-25 08:30:55 -03:00
|
|
|
model.setContactKey("${node.channel}${user.id}")
|
2023-08-25 17:02:12 -03:00
|
|
|
parentFragmentManager.beginTransaction()
|
|
|
|
|
.replace(R.id.mainActivityLayout, MessagesFragment())
|
|
|
|
|
.addToBackStack(null)
|
|
|
|
|
.commit()
|
2022-09-30 15:57:04 -03:00
|
|
|
}
|
2022-11-06 17:46:57 -03:00
|
|
|
R.id.request_position -> {
|
2023-08-25 17:02:12 -03:00
|
|
|
debug("requesting position for '${user.longName}'")
|
|
|
|
|
model.requestPosition(node.num)
|
2022-11-06 17:46:57 -03:00
|
|
|
}
|
2023-04-16 06:16:41 -03:00
|
|
|
R.id.traceroute -> {
|
2023-08-25 17:02:12 -03:00
|
|
|
debug("requesting traceroute for '${user.longName}'")
|
|
|
|
|
model.requestTraceroute(node.num)
|
2023-04-16 06:16:41 -03:00
|
|
|
}
|
2023-08-25 19:14:24 -03:00
|
|
|
R.id.ignore -> {
|
|
|
|
|
val message = if (isIgnored) R.string.ignore_remove else R.string.ignore_add
|
|
|
|
|
MaterialAlertDialogBuilder(requireContext())
|
|
|
|
|
.setTitle(R.string.ignore)
|
|
|
|
|
.setMessage(getString(message, user.longName))
|
|
|
|
|
.setNeutralButton(R.string.cancel) { _, _ -> }
|
|
|
|
|
.setPositiveButton(R.string.send) { _, _ ->
|
|
|
|
|
model.ignoreIncomingList = ignoreIncomingList.apply {
|
|
|
|
|
if (isIgnored) {
|
|
|
|
|
debug("removed '${user.longName}' from ignore list")
|
|
|
|
|
remove(node.num)
|
|
|
|
|
} else {
|
|
|
|
|
debug("added '${user.longName}' to ignore list")
|
|
|
|
|
add(node.num)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
item.isChecked = !item.isChecked
|
|
|
|
|
notifyItemChanged(position)
|
|
|
|
|
}
|
|
|
|
|
.show()
|
|
|
|
|
}
|
2023-04-22 12:06:25 -03:00
|
|
|
R.id.remote_admin -> {
|
2023-06-02 17:29:20 -03:00
|
|
|
debug("calling remote admin --> destNum: ${node.num.toUInt()}")
|
2023-09-11 21:26:42 -03:00
|
|
|
model.setDestNode(node)
|
2023-04-22 12:06:25 -03:00
|
|
|
parentFragmentManager.beginTransaction()
|
2023-09-11 21:26:42 -03:00
|
|
|
.replace(R.id.mainActivityLayout, DeviceSettingsFragment())
|
2023-04-22 12:06:25 -03:00
|
|
|
.addToBackStack(null)
|
|
|
|
|
.commit()
|
|
|
|
|
}
|
2022-09-30 15:57:04 -03:00
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
popup.show()
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
/**
|
|
|
|
|
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
|
|
|
|
|
* an item.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* This new ViewHolder should be constructed with a new View that can represent the items
|
|
|
|
|
* of the given type. You can either create a new View manually or inflate it from an XML
|
|
|
|
|
* layout file.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* The new ViewHolder will be used to display items of the adapter using
|
|
|
|
|
* [.onBindViewHolder]. Since it will be re-used to display
|
|
|
|
|
* different items in the data set, it is a good idea to cache references to sub views of
|
|
|
|
|
* the View to avoid unnecessary [View.findViewById] calls.
|
|
|
|
|
*
|
|
|
|
|
* @param parent The ViewGroup into which the new View will be added after it is bound to
|
|
|
|
|
* an adapter position.
|
|
|
|
|
* @param viewType The view type of the new View.
|
|
|
|
|
*
|
|
|
|
|
* @return A new ViewHolder that holds a View of the given view type.
|
|
|
|
|
* @see .getItemViewType
|
|
|
|
|
* @see .onBindViewHolder
|
|
|
|
|
*/
|
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
|
|
|
val inflater = LayoutInflater.from(requireContext())
|
|
|
|
|
|
|
|
|
|
// Inflate the custom layout
|
2020-12-07 20:33:29 +08:00
|
|
|
val contactView = AdapterNodeLayoutBinding.inflate(inflater, parent, false)
|
2020-04-05 11:50:47 -07:00
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
// Return a new holder instance
|
|
|
|
|
return ViewHolder(contactView)
|
|
|
|
|
}
|
2020-04-05 11:50:47 -07:00
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
/**
|
|
|
|
|
* Returns the total number of items in the data set held by the adapter.
|
|
|
|
|
*
|
|
|
|
|
* @return The total number of items in this adapter.
|
|
|
|
|
*/
|
|
|
|
|
override fun getItemCount(): Int = nodes.size
|
2020-04-05 11:50:47 -07:00
|
|
|
|
2020-04-08 15:25:57 -07:00
|
|
|
/**
|
|
|
|
|
* Called by RecyclerView to display the data at the specified position. This method should
|
|
|
|
|
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
|
|
|
|
|
* position.
|
|
|
|
|
*
|
|
|
|
|
*
|
|
|
|
|
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
|
|
|
|
|
* again if the position of the item changes in the data set unless the item itself is
|
|
|
|
|
* invalidated or the new position cannot be determined. For this reason, you should only
|
|
|
|
|
* use the `position` parameter while acquiring the related data item inside
|
|
|
|
|
* this method and should not keep a copy of it. If you need the position of an item later
|
|
|
|
|
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
|
|
|
|
|
* have the updated adapter position.
|
|
|
|
|
*
|
|
|
|
|
* Override [.onBindViewHolder] instead if Adapter can
|
|
|
|
|
* handle efficient partial bind.
|
|
|
|
|
*
|
|
|
|
|
* @param holder The ViewHolder which should be updated to represent the contents of the
|
|
|
|
|
* item at the given position in the data set.
|
|
|
|
|
* @param position The position of the item within the adapter's data set.
|
|
|
|
|
*/
|
|
|
|
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
|
|
|
val n = nodes[position]
|
2023-04-13 17:54:52 -03:00
|
|
|
val (textColor, nodeColor) = n.colors
|
2023-08-25 19:14:24 -03:00
|
|
|
val isIgnored: Boolean = ignoreIncomingList.contains(n.num)
|
2024-02-28 08:43:29 -07:00
|
|
|
val isThisNode = n.num == nodes[0].num
|
2024-02-26 15:19:32 -07:00
|
|
|
|
2024-02-28 08:43:29 -07:00
|
|
|
holder.bind(n, isThisNode, gpsFormat)
|
2024-02-28 07:29:13 -07:00
|
|
|
|
2024-02-28 08:43:29 -07:00
|
|
|
holder.nodeNameView.text = n.user?.longName
|
2024-02-26 15:19:32 -07:00
|
|
|
|
2023-04-13 17:54:52 -03:00
|
|
|
with(holder.chipNode) {
|
2024-02-28 08:43:29 -07:00
|
|
|
text = (n.user?.shortName ?: "UNK").strikeIf(isIgnored)
|
2023-04-13 17:54:52 -03:00
|
|
|
chipBackgroundColor = ColorStateList.valueOf(nodeColor)
|
|
|
|
|
setTextColor(textColor)
|
|
|
|
|
}
|
2020-04-05 11:50:47 -07:00
|
|
|
|
2024-02-28 08:43:29 -07:00
|
|
|
val distance = nodes[0].distanceStr(n, displayUnits)
|
2020-04-08 16:49:27 -07:00
|
|
|
if (distance != null) {
|
2020-12-07 20:33:29 +08:00
|
|
|
holder.distanceView.text = distance
|
|
|
|
|
holder.distanceView.visibility = View.VISIBLE
|
2020-04-08 16:49:27 -07:00
|
|
|
} else {
|
2020-12-07 20:33:29 +08:00
|
|
|
holder.distanceView.visibility = View.INVISIBLE
|
2020-04-08 16:49:27 -07:00
|
|
|
}
|
2020-07-13 23:49:07 -04:00
|
|
|
|
2023-10-08 21:16:38 -03:00
|
|
|
val envMetrics = n.envMetricStr(displayFahrenheit)
|
2023-07-16 03:46:54 -05:00
|
|
|
if (envMetrics.isNotEmpty()) {
|
|
|
|
|
holder.envMetrics.text = envMetrics
|
2022-09-12 18:23:59 -03:00
|
|
|
holder.envMetrics.visibility = View.VISIBLE
|
|
|
|
|
} else {
|
|
|
|
|
holder.envMetrics.visibility = View.GONE
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-27 16:29:41 -03:00
|
|
|
holder.chipNode.setOnClickListener {
|
2022-09-30 15:57:04 -03:00
|
|
|
popup(it, position)
|
2022-09-27 16:29:41 -03:00
|
|
|
}
|
|
|
|
|
holder.itemView.setOnLongClickListener {
|
2022-09-30 15:57:04 -03:00
|
|
|
popup(it, position)
|
2022-04-22 16:56:27 -03:00
|
|
|
true
|
|
|
|
|
}
|
2020-04-08 15:25:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Called when our node DB changes
|
2022-04-22 16:56:27 -03:00
|
|
|
fun onNodesChanged(nodesIn: Array<NodeInfo>) {
|
|
|
|
|
if (nodesIn.size > 1)
|
|
|
|
|
nodesIn.sortWith(compareByDescending { it.lastHeard }, 1)
|
|
|
|
|
nodes = nodesIn
|
2020-04-08 15:25:57 -07:00
|
|
|
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onCreateView(
|
|
|
|
|
inflater: LayoutInflater, container: ViewGroup?,
|
|
|
|
|
savedInstanceState: Bundle?
|
2022-03-28 15:52:32 -03:00
|
|
|
): View {
|
2020-12-07 20:33:29 +08:00
|
|
|
_binding = NodelistFragmentBinding.inflate(inflater, container, false)
|
|
|
|
|
return binding.root
|
2020-04-08 15:25:57 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
|
2020-12-07 20:33:29 +08:00
|
|
|
binding.nodeListView.adapter = nodesAdapter
|
|
|
|
|
binding.nodeListView.layoutManager = LinearLayoutManager(requireContext())
|
2020-04-08 15:25:57 -07:00
|
|
|
|
2024-02-06 20:03:15 -03:00
|
|
|
model.nodeDB.nodeDBbyNum.asLiveData().observe(viewLifecycleOwner) {
|
|
|
|
|
nodesAdapter.onNodesChanged(it.values.toTypedArray())
|
2020-04-05 11:50:47 -07:00
|
|
|
}
|
2023-04-16 06:16:41 -03:00
|
|
|
|
2023-08-25 19:14:24 -03:00
|
|
|
model.localConfig.asLiveData().observe(viewLifecycleOwner) { config ->
|
2023-10-08 21:16:38 -03:00
|
|
|
ignoreIncomingList.apply {
|
2023-08-25 19:14:24 -03:00
|
|
|
clear()
|
|
|
|
|
addAll(config.lora.ignoreIncomingList)
|
|
|
|
|
}
|
2023-10-08 21:16:38 -03:00
|
|
|
gpsFormat = config.display.gpsFormat.number
|
|
|
|
|
displayUnits = config.display.units.number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
model.moduleConfig.asLiveData().observe(viewLifecycleOwner) { module ->
|
|
|
|
|
displayFahrenheit = module.telemetry.environmentDisplayFahrenheit
|
2023-08-25 19:14:24 -03:00
|
|
|
}
|
|
|
|
|
|
2023-09-16 09:51:16 -03:00
|
|
|
model.tracerouteResponse.observe(viewLifecycleOwner) { response ->
|
|
|
|
|
MaterialAlertDialogBuilder(requireContext())
|
2023-10-12 18:36:35 -03:00
|
|
|
.setCancelable(false)
|
2023-09-16 09:51:16 -03:00
|
|
|
.setTitle(R.string.traceroute)
|
|
|
|
|
.setMessage(response ?: return@observe)
|
|
|
|
|
.setPositiveButton(R.string.okay) { _, _ -> }
|
|
|
|
|
.show()
|
|
|
|
|
|
|
|
|
|
model.clearTracerouteResponse()
|
2023-04-16 06:16:41 -03:00
|
|
|
}
|
2024-02-13 14:32:52 -07:00
|
|
|
|
|
|
|
|
model.focusedNode.asLiveData().observe(viewLifecycleOwner) { node ->
|
|
|
|
|
val idx = nodesAdapter.nodes.indexOfFirst {
|
|
|
|
|
it.user?.id == node?.user?.id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (idx < 1) return@observe
|
|
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
|
binding.nodeListView.layoutManager?.smoothScrollToTop(idx)
|
|
|
|
|
val vh = binding.nodeListView.findViewHolderForLayoutPosition(idx)
|
|
|
|
|
(vh as? ViewHolder)?.blink()
|
|
|
|
|
model.focusUserNode(null)
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-05 11:50:47 -07:00
|
|
|
}
|
2023-04-03 18:03:55 -03:00
|
|
|
|
|
|
|
|
override fun onDestroyView() {
|
|
|
|
|
super.onDestroyView()
|
|
|
|
|
_binding = null
|
|
|
|
|
}
|
2024-02-13 14:32:52 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Scrolls the recycler view until the item at [position] is at the top of the view, then waits
|
|
|
|
|
* until the scrolling is finished.
|
|
|
|
|
*/
|
|
|
|
|
private suspend fun RecyclerView.LayoutManager.smoothScrollToTop(position: Int) {
|
|
|
|
|
this.startSmoothScroll(
|
|
|
|
|
object : LinearSmoothScroller(requireContext()) {
|
|
|
|
|
override fun getVerticalSnapPreference(): Int {
|
|
|
|
|
return SNAP_TO_START
|
|
|
|
|
}
|
|
|
|
|
}.apply {
|
|
|
|
|
targetPosition = position
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
withContext(Dispatchers.Default) {
|
|
|
|
|
while (this@smoothScrollToTop.isSmoothScrolling) {
|
|
|
|
|
// noop
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-05 11:50:47 -07:00
|
|
|
}
|