chore(ui): move disable resource into resources details (#6463)

To homogenize the UI in mobile and non-mobile, we re-enable the sub-menu
for the internet resource, where it shows all resource

## New UI for android


![image](https://github.com/user-attachments/assets/3b9ce82f-1dbd-4ace-b7ba-873a34fd9dec)



![image](https://github.com/user-attachments/assets/16e9b128-532c-467e-b663-ae83195b2730)

## New UI for apple


![98579](https://github.com/user-attachments/assets/8bbda871-1d16-4d99-b873-a26d480821cf)


![99694](https://github.com/user-attachments/assets/f2924a08-8996-4882-867d-7446f7cfbafd)
This commit is contained in:
Gabi
2024-08-28 21:26:30 -03:00
committed by GitHub
parent 36fc4cb593
commit 14b7652e0c
11 changed files with 269 additions and 196 deletions

View File

@@ -23,10 +23,9 @@ import com.google.android.material.button.MaterialButton
import dev.firezone.android.R
import dev.firezone.android.tunnel.model.StatusEnum
class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomSheetDialogFragment() {
class ResourceDetailsBottomSheet(private val resource: ViewResource, private val activity: SessionActivity) : BottomSheetDialogFragment() {
private lateinit var view: View
private val viewModel: SessionViewModel by activityViewModels()
private var isFavorite: Boolean = false
override fun onCreateView(
inflater: LayoutInflater,
@@ -43,11 +42,6 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe
this.view = view
super.onViewCreated(view, savedInstanceState)
val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn)
val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn)
val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName)
val resourceAddressTextView: TextView = view.findViewById(R.id.tvResourceAddress)
val resourceAddressDescriptionTextView: TextView = view.findViewById(R.id.tvResourceAddressDescription)
val siteNameTextView: TextView = view.findViewById(R.id.tvSiteName)
val siteStatusTextView: TextView = view.findViewById(R.id.tvSiteStatus)
val statusIndicatorDot: ImageView = view.findViewById(R.id.statusIndicatorDot)
@@ -55,43 +49,9 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe
val siteNameLayout: LinearLayout = view.findViewById(R.id.siteNameLayout)
val siteStatusLayout: LinearLayout = view.findViewById(R.id.siteStatusLayout)
addToFavoritesBtn.setOnClickListener {
viewModel.addFavoriteResource(resource.id)
refreshButtons()
}
removeFromFavoritesBtn.setOnClickListener {
viewModel.removeFavoriteResource(resource.id)
refreshButtons()
}
refreshButtons()
resourceHeader()
resourceNameTextView.text = resource.name
val displayAddress = resource.addressDescription ?: resource.address
resourceAddressTextView.text = displayAddress
if (!resource.addressDescription.isNullOrEmpty()) {
resourceAddressDescriptionTextView.text = resource.addressDescription
resourceAddressDescriptionTextView.visibility = View.VISIBLE
}
val addressUri = resource.addressDescription?.let { Uri.parse(it) }
if (addressUri != null && addressUri.scheme != null) {
resourceAddressTextView.setTextColor(Color.BLUE)
resourceAddressTextView.setTypeface(null, Typeface.ITALIC)
resourceAddressTextView.setOnClickListener {
openUrl(resource.addressDescription!!)
}
} else {
resourceAddressTextView.setOnClickListener {
copyToClipboard(displayAddress!!)
Toast.makeText(requireContext(), "Address copied to clipboard", Toast.LENGTH_SHORT).show()
}
}
resourceNameTextView.setOnClickListener {
copyToClipboard(resource.name)
Toast.makeText(requireContext(), "Name copied to clipboard", Toast.LENGTH_SHORT).show()
}
refreshDisableToggleButton()
if (!resource.sites.isNullOrEmpty()) {
val site = resource.sites.first()
@@ -129,6 +89,81 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe
}
}
private fun resourceToggleText(): String {
if (resource.enabled) {
return "Disable this resource"
} else {
return "Enable this resource"
}
}
private fun resourceHeader() {
if (resource.isInternetResource()) {
internetResourceHeader()
} else {
nonInternetResourceHeader()
}
}
private fun internetResourceHeader() {
val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn)
val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn)
val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName)
val resourceAddress: LinearLayout = view.findViewById(R.id.addressSection)
val resourceAddressDescriptionTextView: TextView = view.findViewById(R.id.tvResourceAddressDescription)
val resourceDescriptionLayout: LinearLayout = view.findViewById(R.id.resourceDescriptionLayout)
addToFavoritesBtn.visibility = View.GONE
removeFromFavoritesBtn.visibility = View.GONE
resourceNameTextView.text = resource.name
resourceAddress.visibility = View.GONE
resourceDescriptionLayout.visibility = View.VISIBLE
resourceAddressDescriptionTextView.text = "All network traffic"
}
private fun nonInternetResourceHeader() {
val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn)
val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn)
val resourceNameTextView: TextView = view.findViewById(R.id.tvResourceName)
val resourceAddressTextView: TextView = view.findViewById(R.id.tvResourceAddress)
addToFavoritesBtn.setOnClickListener {
viewModel.addFavoriteResource(resource.id)
refreshButtons()
}
removeFromFavoritesBtn.setOnClickListener {
viewModel.removeFavoriteResource(resource.id)
refreshButtons()
}
refreshButtons()
resourceNameTextView.text = resource.name
val displayAddress = resource.addressDescription ?: resource.address
resourceAddressTextView.text = displayAddress
val addressUri = resource.addressDescription?.let { Uri.parse(it) }
if (addressUri != null && addressUri.scheme != null) {
resourceAddressTextView.setTextColor(Color.BLUE)
resourceAddressTextView.setTypeface(null, Typeface.ITALIC)
resourceAddressTextView.setOnClickListener {
openUrl(resource.addressDescription!!)
}
} else {
resourceAddressTextView.setOnClickListener {
copyToClipboard(displayAddress!!)
Toast.makeText(requireContext(), "Address copied to clipboard", Toast.LENGTH_SHORT).show()
}
}
resourceNameTextView.setOnClickListener {
copyToClipboard(resource.name)
Toast.makeText(requireContext(), "Name copied to clipboard", Toast.LENGTH_SHORT).show()
}
}
private fun refreshButtons() {
val addToFavoritesBtn: MaterialButton = view.findViewById(R.id.addToFavoritesBtn)
val removeFromFavoritesBtn: MaterialButton = view.findViewById(R.id.removeFromFavoritesBtn)
@@ -137,6 +172,21 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource) : BottomShe
removeFromFavoritesBtn.visibility = if (isFavorite) View.VISIBLE else View.GONE
}
private fun refreshDisableToggleButton() {
if (resource.canBeDisabled) {
val toggleResourceEnabled: MaterialButton = view.findViewById(R.id.toggleResourceEnabled)
toggleResourceEnabled.visibility = View.VISIBLE
toggleResourceEnabled.text = resourceToggleText()
toggleResourceEnabled.setOnClickListener {
resource.enabled = !resource.enabled
activity.onViewResourceToggled(resource)
refreshDisableToggleButton()
}
}
}
private fun copyToClipboard(text: String) {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Copied Text", text)

View File

@@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
@@ -14,8 +13,6 @@ import dev.firezone.android.databinding.ListItemResourceBinding
internal class ResourcesAdapter(private val activity: SessionActivity) : ListAdapter<ViewResource, ResourcesAdapter.ViewHolder>(
ResourceDiffCallback(),
) {
private var favoriteResources: HashSet<String> = HashSet()
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
@@ -29,45 +26,24 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda
position: Int,
) {
val resource = getItem(position)
holder.bind(resource) { newResource -> onSwitchToggled(newResource) }
if (!resource.isInternetResource()) {
holder.itemView.setOnClickListener {
// Show bottom sheet
val isFavorite = favoriteResources.contains(resource.id)
val fragmentManager =
(holder.itemView.context as AppCompatActivity).supportFragmentManager
val bottomSheet = ResourceDetailsBottomSheet(resource)
bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet")
}
holder.bind(resource)
holder.itemView.setOnClickListener {
// Show bottom sheet
val fragmentManager =
(holder.itemView.context as AppCompatActivity).supportFragmentManager
val bottomSheet = ResourceDetailsBottomSheet(resource, activity)
bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet")
}
}
private fun onSwitchToggled(resource: ViewResource) {
activity.onViewResourceToggled(resource)
}
class ViewHolder(private val binding: ListItemResourceBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(
resource: ViewResource,
onSwitchToggled: (ViewResource) -> Unit,
) {
fun bind(resource: ViewResource) {
binding.resourceNameText.text = resource.name
if (resource.isInternetResource()) {
binding.addressText.visibility = View.GONE
} else {
binding.addressText.text = resource.address
}
// Without this the item gets reset when out of view, isn't android wonderful?
binding.enableSwitch.setOnCheckedChangeListener(null)
binding.enableSwitch.isChecked = resource.enabled
binding.enableSwitch.isVisible = resource.canBeDisabled
binding.enableSwitch.setOnCheckedChangeListener {
_, isChecked ->
resource.enabled = isChecked
onSwitchToggled(resource)
}
}
}

View File

@@ -20,7 +20,7 @@ import dev.firezone.android.features.settings.ui.SettingsActivity
import dev.firezone.android.tunnel.TunnelService
@AndroidEntryPoint
internal class SessionActivity : AppCompatActivity() {
class SessionActivity : AppCompatActivity() {
private lateinit var binding: ActivitySessionBinding
private var tunnelService: TunnelService? = null
private var serviceBound = false

View File

@@ -14,7 +14,7 @@ data class ViewResource(
val sites: List<Site>?,
val name: String,
val status: StatusEnum,
var enabled: Boolean = true,
var enabled: Boolean,
var canBeDisabled: Boolean = true,
)

View File

@@ -16,7 +16,6 @@ data class Resource(
val sites: List<Site>?,
val name: String,
val status: StatusEnum,
var enabled: Boolean = true,
@Json(name = "can_be_disabled") val canBeDisabled: Boolean,
) : Parcelable

View File

@@ -33,6 +33,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/addressSection"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
@@ -68,6 +69,13 @@
android:layout_weight="1" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/toggleResourceEnabled"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addToFavoritesBtn"
style="?attr/materialButtonOutlinedStyle"

View File

@@ -28,14 +28,4 @@
app:layout_constraintTop_toBottomOf="@id/resourceNameText"
tools:text="Resource Address" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enable_switch"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:checked="true"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="@+id/resourceNameText"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -91,7 +91,8 @@ pub struct ResourceDescriptionInternet {
#[serde(rename = "gateway_groups")]
pub sites: Vec<Site>,
/// Whether or not resource can be disabled from UI
pub can_be_disabled: Option<bool>,
#[serde(default)]
pub can_be_disabled: bool,
}
impl ResourceDescriptionInternet {
@@ -100,7 +101,7 @@ impl ResourceDescriptionInternet {
name: self.name,
id: self.id,
sites: self.sites,
can_be_disabled: self.can_be_disabled.unwrap_or_default(),
can_be_disabled: self.can_be_disabled,
status,
}
}

View File

@@ -83,7 +83,7 @@ pub fn internet_resource(
name: "Internet Resource".to_string(),
id,
sites,
can_be_disabled: Some(can_be_disabled),
can_be_disabled,
}
})
}

View File

@@ -8,6 +8,11 @@
import SwiftUI
#if os(iOS)
private func copyToClipboard(_ value: String) {
let pasteboard = UIPasteboard.general
pasteboard.string = value
}
struct ResourceView: View {
@ObservedObject var model: SessionViewModel
var resource: Resource
@@ -15,82 +20,10 @@ struct ResourceView: View {
var body: some View {
List {
Section(header: Text("Resource")) {
HStack {
Text("NAME")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(width: 80, alignment: .leading)
Text(resource.name)
}
.contextMenu {
Button(action: {
copyToClipboard(resource.name)
}) {
Text("Copy name")
Image(systemName: "doc.on.doc")
}
}
HStack {
Text("ADDRESS")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(width: 80, alignment: .leading)
if let url = URL(string: resource.addressDescription ?? resource.address!),
let _ = url.host {
Button(action: {
openURL(url)
}) {
Text(resource.addressDescription ?? resource.address!)
.foregroundColor(.blue)
.underline()
.font(.system(size: 16))
.contextMenu {
Button(action: {
copyToClipboard(resource.addressDescription ?? resource.address!)
}) {
Text("Copy address")
Image(systemName: "doc.on.doc")
}
}
}
} else {
Text(resource.addressDescription ?? resource.address!)
.contextMenu {
Button(action: {
copyToClipboard(resource.addressDescription ?? resource.address!)
}) {
Text("Copy address")
Image(systemName: "doc.on.doc")
}
}
}
}
if(model.favorites.ids.contains(resource.id)) {
Button(action: {
model.favorites.remove(resource.id)
}) {
HStack {
Image(systemName: "star")
Text("Remove from favorites")
Spacer()
}
}
} else {
Button(action: {
model.favorites.add(resource.id)
}) {
HStack {
Image(systemName: "star.fill")
Text("Add to favorites")
Spacer()
}
}
}
if resource.isInternetResource() {
InternetResourceHeader(model: model, resource: resource)
} else {
NonInternetResourceHeader(model: model, resource: resource)
}
if let site = resource.sites.first {
@@ -156,10 +89,150 @@ struct ResourceView: View {
return .gray
}
}
}
private func copyToClipboard(_ value: String) {
let pasteboard = UIPasteboard.general
pasteboard.string = value
struct NonInternetResourceHeader: View {
@ObservedObject var model: SessionViewModel
var resource: Resource
@Environment(\.openURL) var openURL
var body: some View {
Section(header: Text("Resource")) {
HStack {
Text("NAME")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(width: 80, alignment: .leading)
Text(resource.name)
}
.contextMenu {
Button(action: {
copyToClipboard(resource.name)
}) {
Text("Copy name")
Image(systemName: "doc.on.doc")
}
}
HStack {
Text("ADDRESS")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(width: 80, alignment: .leading)
if let url = URL(string: resource.addressDescription ?? resource.address!),
let _ = url.host {
Button(action: {
openURL(url)
}) {
Text(resource.addressDescription ?? resource.address!)
.foregroundColor(.blue)
.underline()
.font(.system(size: 16))
.contextMenu {
Button(action: {
copyToClipboard(resource.addressDescription ?? resource.address!)
}) {
Text("Copy address")
Image(systemName: "doc.on.doc")
}
}
}
} else {
Text(resource.addressDescription ?? resource.address!)
.contextMenu {
Button(action: {
copyToClipboard(resource.addressDescription ?? resource.address!)
}) {
Text("Copy address")
Image(systemName: "doc.on.doc")
}
}
}
}
if(model.favorites.ids.contains(resource.id)) {
Button(action: {
model.favorites.remove(resource.id)
}) {
HStack {
Image(systemName: "star")
Text("Remove from favorites")
Spacer()
}
}
} else {
Button(action: {
model.favorites.add(resource.id)
}) {
HStack {
Image(systemName: "star.fill")
Text("Add to favorites")
Spacer()
}
}
}
ToggleResourceEnabledButton(resource: resource, model: model)
}
}
}
struct InternetResourceHeader: View {
@ObservedObject var model: SessionViewModel
var resource: Resource
var body: some View {
Section(header: Text("Resource")) {
HStack {
Text("NAME")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(width: 80, alignment: .leading)
Text(resource.name)
}
HStack {
Text("DESCRIPTION")
.bold()
.font(.system(size: 14))
.foregroundColor(.secondary)
.frame(alignment: .leading)
Text("All network traffic")
}
ToggleResourceEnabledButton(resource: resource, model: model)
}
}
}
struct ToggleResourceEnabledButton: View {
var resource: Resource
@ObservedObject var model: SessionViewModel
private func toggleResourceEnabledText() -> String {
if model.isResourceEnabled(resource.id) {
"Disable this resource"
} else {
"Enable this resource"
}
}
var body: some View {
if resource.canBeDisabled {
Button(action: {
model.store.toggleResourceDisabled(resource: resource.id, enabled: !model.isResourceEnabled(resource.id))
}) {
HStack {
Text(toggleResourceEnabledText())
Spacer()
}
}
}
}
}
#endif

View File

@@ -130,37 +130,13 @@ struct ResourceSection: View {
var body: some View {
ForEach(resources) { resource in
HStack {
if !resource.isInternetResource() {
NavigationLink { ResourceView(model: model, resource: resource) }
NavigationLink { ResourceView(model: model, resource: resource) }
label: {
ResourceLabel(resource: resource, model: model )
Text(resource.name)
}
} else {
ResourceLabel(resource: resource, model: model)
}
}
.navigationTitle("All Resources")
}
}
}
struct ResourceLabel: View {
let resource: Resource
@ObservedObject var model: SessionViewModel
var body: some View {
HStack {
Text(resource.name)
if resource.canBeDisabled {
Spacer()
Toggle("Enabled", isOn: Binding<Bool>(
get: { model.isResourceEnabled(resource.id) },
set: { newValue in
model.store.toggleResourceDisabled(resource: resource.id, enabled: newValue)
}
)).labelsHidden()
}
}
}
}
#endif