AppListDialogFragment: support both inclusion and exclusion
Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
This commit is contained in:
parent
e424765a61
commit
7db0fa915e
@ -5,14 +5,17 @@
|
|||||||
package com.wireguard.android.fragment
|
package com.wireguard.android.fragment
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Button
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.databinding.Observable
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.wireguard.android.Application
|
import com.wireguard.android.Application
|
||||||
|
import com.wireguard.android.BR
|
||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
import com.wireguard.android.databinding.AppListDialogFragmentBinding
|
||||||
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
import com.wireguard.android.databinding.ObservableKeyedArrayList
|
||||||
@ -21,7 +24,10 @@ import com.wireguard.android.util.ErrorMessages
|
|||||||
|
|
||||||
class AppListDialogFragment : DialogFragment() {
|
class AppListDialogFragment : DialogFragment() {
|
||||||
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
|
private val appData: ObservableKeyedArrayList<String, ApplicationData> = ObservableKeyedArrayList()
|
||||||
private var currentlyExcludedApps = emptyList<String>()
|
private var currentlySelectedApps = emptyList<String>()
|
||||||
|
private var initiallyExcluded: Boolean = false
|
||||||
|
private var button: Button? = null
|
||||||
|
private var tabs: TabLayout? = null
|
||||||
|
|
||||||
private fun loadData() {
|
private fun loadData() {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
@ -33,7 +39,14 @@ class AppListDialogFragment : DialogFragment() {
|
|||||||
val applicationData: MutableList<ApplicationData> = ArrayList()
|
val applicationData: MutableList<ApplicationData> = ArrayList()
|
||||||
resolveInfos.forEach {
|
resolveInfos.forEach {
|
||||||
val packageName = it.activityInfo.packageName
|
val packageName = it.activityInfo.packageName
|
||||||
applicationData.add(ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlyExcludedApps.contains(packageName)))
|
val appData = ApplicationData(it.loadIcon(pm), it.loadLabel(pm).toString(), packageName, currentlySelectedApps.contains(packageName))
|
||||||
|
applicationData.add(appData)
|
||||||
|
appData.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
|
||||||
|
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
|
||||||
|
if (propertyId == BR.selected)
|
||||||
|
setButtonText()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
applicationData.sortWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||||
applicationData
|
applicationData
|
||||||
@ -52,17 +65,34 @@ class AppListDialogFragment : DialogFragment() {
|
|||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val excludedApps = requireArguments().getStringArrayList(KEY_EXCLUDED_APPS)
|
currentlySelectedApps = (arguments?.getStringArrayList(KEY_SELECTED_APPS) ?: emptyList())
|
||||||
currentlyExcludedApps = (excludedApps ?: emptyList())
|
initiallyExcluded = arguments?.getBoolean(KEY_IS_EXCLUDED) ?: true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setButtonText() {
|
||||||
|
val numSelected = appData.count { it.isSelected }
|
||||||
|
button?.text = if (numSelected == 0)
|
||||||
|
getString(R.string.use_all_applications)
|
||||||
|
else when (tabs?.selectedTabPosition) {
|
||||||
|
0 -> resources.getQuantityString(R.plurals.exclude_n_applications, numSelected, numSelected)
|
||||||
|
1 -> resources.getQuantityString(R.plurals.include_n_applications, numSelected, numSelected)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
|
val alertDialogBuilder = AlertDialog.Builder(requireActivity())
|
||||||
alertDialogBuilder.setTitle(R.string.excluded_applications)
|
|
||||||
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
|
val binding = AppListDialogFragmentBinding.inflate(requireActivity().layoutInflater, null, false)
|
||||||
binding.executePendingBindings()
|
binding.executePendingBindings()
|
||||||
alertDialogBuilder.setView(binding.root)
|
alertDialogBuilder.setView(binding.root)
|
||||||
alertDialogBuilder.setPositiveButton(R.string.set_exclusions) { _, _ -> setExclusionsAndDismiss() }
|
tabs = binding.tabs
|
||||||
|
tabs!!.selectTab(binding.tabs.getTabAt(if (initiallyExcluded) 0 else 1))
|
||||||
|
tabs!!.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
|
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
|
||||||
|
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
|
||||||
|
override fun onTabSelected(tab: TabLayout.Tab?) = setButtonText()
|
||||||
|
})
|
||||||
|
alertDialogBuilder.setPositiveButton(" ") { _, _ -> setSelectionAndDismiss() }
|
||||||
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
alertDialogBuilder.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
|
||||||
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
|
alertDialogBuilder.setNeutralButton(R.string.toggle_all) { _, _ -> }
|
||||||
binding.fragment = this
|
binding.fragment = this
|
||||||
@ -70,39 +100,40 @@ class AppListDialogFragment : DialogFragment() {
|
|||||||
loadData()
|
loadData()
|
||||||
val dialog = alertDialogBuilder.create()
|
val dialog = alertDialogBuilder.create()
|
||||||
dialog.setOnShowListener {
|
dialog.setOnShowListener {
|
||||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
|
button = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
val selectedItems = appData
|
setButtonText()
|
||||||
.filter { it.isExcludedFromTunnel }
|
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).setOnClickListener { _ ->
|
||||||
|
val selectAll = appData.none { it.isSelected }
|
||||||
val excludeAll = selectedItems.isEmpty()
|
|
||||||
appData.forEach {
|
appData.forEach {
|
||||||
it.isExcludedFromTunnel = excludeAll
|
it.isSelected = selectAll
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setExclusionsAndDismiss() {
|
private fun setSelectionAndDismiss() {
|
||||||
val excludedApps: MutableList<String> = ArrayList()
|
val selectedApps: MutableList<String> = ArrayList()
|
||||||
for (data in appData) {
|
for (data in appData) {
|
||||||
if (data.isExcludedFromTunnel) {
|
if (data.isSelected) {
|
||||||
excludedApps.add(data.packageName)
|
selectedApps.add(data.packageName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(targetFragment as AppExclusionListener?)!!.onExcludedAppsSelected(excludedApps)
|
(targetFragment as AppSelectionListener?)!!.onSelectedAppsSelected(selectedApps, tabs?.selectedTabPosition == 0)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppExclusionListener {
|
interface AppSelectionListener {
|
||||||
fun onExcludedAppsSelected(excludedApps: List<String>)
|
fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_EXCLUDED_APPS = "excludedApps"
|
private const val KEY_SELECTED_APPS = "selected_apps"
|
||||||
fun <T> newInstance(excludedApps: ArrayList<String?>?, target: T): AppListDialogFragment where T : Fragment?, T : AppExclusionListener? {
|
private const val KEY_IS_EXCLUDED = "is_excluded"
|
||||||
|
fun <T> newInstance(selectedApps: ArrayList<String?>?, isExcluded: Boolean, target: T): AppListDialogFragment where T : Fragment?, T : AppSelectionListener? {
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
extras.putStringArrayList(KEY_EXCLUDED_APPS, excludedApps)
|
extras.putStringArrayList(KEY_SELECTED_APPS, selectedApps)
|
||||||
|
extras.putBoolean(KEY_IS_EXCLUDED, isExcluded)
|
||||||
val fragment = AppListDialogFragment()
|
val fragment = AppListDialogFragment()
|
||||||
fragment.setTargetFragment(target, 0)
|
fragment.setTargetFragment(target, 0)
|
||||||
fragment.arguments = extras
|
fragment.arguments = extras
|
||||||
|
@ -23,7 +23,7 @@ import com.wireguard.android.Application
|
|||||||
import com.wireguard.android.R
|
import com.wireguard.android.R
|
||||||
import com.wireguard.android.backend.Tunnel
|
import com.wireguard.android.backend.Tunnel
|
||||||
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
import com.wireguard.android.databinding.TunnelEditorFragmentBinding
|
||||||
import com.wireguard.android.fragment.AppListDialogFragment.AppExclusionListener
|
import com.wireguard.android.fragment.AppListDialogFragment.AppSelectionListener
|
||||||
import com.wireguard.android.model.ObservableTunnel
|
import com.wireguard.android.model.ObservableTunnel
|
||||||
import com.wireguard.android.util.BiometricAuthenticator
|
import com.wireguard.android.util.BiometricAuthenticator
|
||||||
import com.wireguard.android.util.ErrorMessages
|
import com.wireguard.android.util.ErrorMessages
|
||||||
@ -35,7 +35,7 @@ import com.wireguard.config.Config
|
|||||||
/**
|
/**
|
||||||
* Fragment for editing a WireGuard configuration.
|
* Fragment for editing a WireGuard configuration.
|
||||||
*/
|
*/
|
||||||
class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
|
||||||
private var haveShownKeys = false
|
private var haveShownKeys = false
|
||||||
private var binding: TunnelEditorFragmentBinding? = null
|
private var binding: TunnelEditorFragmentBinding? = null
|
||||||
private var tunnel: ObservableTunnel? = null
|
private var tunnel: ObservableTunnel? = null
|
||||||
@ -88,11 +88,20 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExcludedAppsSelected(excludedApps: List<String>) {
|
override fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean) {
|
||||||
requireNotNull(binding) { "Tried to set excluded apps while no view was loaded" }
|
requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
|
||||||
binding!!.config!!.`interface`.excludedApplications.apply {
|
if (isExcluded) {
|
||||||
clear()
|
binding!!.config!!.`interface`.includedApplications.clear()
|
||||||
addAll(excludedApps)
|
binding!!.config!!.`interface`.excludedApplications.apply {
|
||||||
|
clear()
|
||||||
|
addAll(selectedApps)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
binding!!.config!!.`interface`.excludedApplications.clear()
|
||||||
|
binding!!.config!!.`interface`.includedApplications.apply {
|
||||||
|
clear()
|
||||||
|
addAll(selectedApps)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,10 +159,16 @@ class TunnelEditorFragment : BaseFragment(), AppExclusionListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
fun onRequestSetExcludedApplications(view: View?) {
|
fun onRequestSetExcludedIncludedApplications(view: View?) {
|
||||||
if (binding != null) {
|
if (binding != null) {
|
||||||
val excludedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
var isExcluded = true
|
||||||
val fragment = AppListDialogFragment.newInstance(excludedApps, this)
|
var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
|
||||||
|
if (selectedApps.isEmpty()) {
|
||||||
|
selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
|
||||||
|
if (selectedApps.isNotEmpty())
|
||||||
|
isExcluded = false
|
||||||
|
}
|
||||||
|
val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded, this)
|
||||||
fragment.show(parentFragmentManager, null)
|
fragment.show(parentFragmentManager, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import androidx.databinding.Bindable
|
|||||||
import com.wireguard.android.BR
|
import com.wireguard.android.BR
|
||||||
import com.wireguard.android.databinding.Keyed
|
import com.wireguard.android.databinding.Keyed
|
||||||
|
|
||||||
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isExcludedFromTunnel: Boolean) : BaseObservable(), Keyed<String> {
|
class ApplicationData(val icon: Drawable, val name: String, val packageName: String, isSelected: Boolean) : BaseObservable(), Keyed<String> {
|
||||||
override val key = name
|
override val key = name
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var isExcludedFromTunnel = isExcludedFromTunnel
|
var isSelected = isSelected
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
notifyPropertyChanged(BR.excludedFromTunnel)
|
notifyPropertyChanged(BR.selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,9 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
|||||||
@get:Bindable
|
@get:Bindable
|
||||||
val excludedApplications: ObservableList<String> = ObservableArrayList()
|
val excludedApplications: ObservableList<String> = ObservableArrayList()
|
||||||
|
|
||||||
|
@get:Bindable
|
||||||
|
val includedApplications: ObservableList<String> = ObservableArrayList()
|
||||||
|
|
||||||
@get:Bindable
|
@get:Bindable
|
||||||
var addresses: String = ""
|
var addresses: String = ""
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -70,6 +73,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
|||||||
addresses = parcel.readString() ?: ""
|
addresses = parcel.readString() ?: ""
|
||||||
dnsServers = parcel.readString() ?: ""
|
dnsServers = parcel.readString() ?: ""
|
||||||
parcel.readStringList(excludedApplications)
|
parcel.readStringList(excludedApplications)
|
||||||
|
parcel.readStringList(includedApplications)
|
||||||
listenPort = parcel.readString() ?: ""
|
listenPort = parcel.readString() ?: ""
|
||||||
mtu = parcel.readString() ?: ""
|
mtu = parcel.readString() ?: ""
|
||||||
privateKey = parcel.readString() ?: ""
|
privateKey = parcel.readString() ?: ""
|
||||||
@ -80,6 +84,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
|||||||
val dnsServerStrings = other.dnsServers.map { it.hostAddress }
|
val dnsServerStrings = other.dnsServers.map { it.hostAddress }
|
||||||
dnsServers = Attribute.join(dnsServerStrings)
|
dnsServers = Attribute.join(dnsServerStrings)
|
||||||
excludedApplications.addAll(other.excludedApplications)
|
excludedApplications.addAll(other.excludedApplications)
|
||||||
|
includedApplications.addAll(other.includedApplications)
|
||||||
listenPort = other.listenPort.map { it.toString() }.orElse("")
|
listenPort = other.listenPort.map { it.toString() }.orElse("")
|
||||||
mtu = other.mtu.map { it.toString() }.orElse("")
|
mtu = other.mtu.map { it.toString() }.orElse("")
|
||||||
val keyPair = other.keyPair
|
val keyPair = other.keyPair
|
||||||
@ -103,6 +108,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
|||||||
if (addresses.isNotEmpty()) builder.parseAddresses(addresses)
|
if (addresses.isNotEmpty()) builder.parseAddresses(addresses)
|
||||||
if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers)
|
if (dnsServers.isNotEmpty()) builder.parseDnsServers(dnsServers)
|
||||||
if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications)
|
if (excludedApplications.isNotEmpty()) builder.excludeApplications(excludedApplications)
|
||||||
|
if (includedApplications.isNotEmpty()) builder.includeApplications(includedApplications)
|
||||||
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
|
if (listenPort.isNotEmpty()) builder.parseListenPort(listenPort)
|
||||||
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
|
if (mtu.isNotEmpty()) builder.parseMtu(mtu)
|
||||||
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
|
if (privateKey.isNotEmpty()) builder.parsePrivateKey(privateKey)
|
||||||
@ -113,6 +119,7 @@ class InterfaceProxy : BaseObservable, Parcelable {
|
|||||||
dest.writeString(addresses)
|
dest.writeString(addresses)
|
||||||
dest.writeString(dnsServers)
|
dest.writeString(dnsServers)
|
||||||
dest.writeStringList(excludedApplications)
|
dest.writeStringList(excludedApplications)
|
||||||
|
dest.writeStringList(includedApplications)
|
||||||
dest.writeString(listenPort)
|
dest.writeString(listenPort)
|
||||||
dest.writeString(mtu)
|
dest.writeString(mtu)
|
||||||
dest.writeString(privateKey)
|
dest.writeString(privateKey)
|
||||||
|
@ -18,30 +18,50 @@
|
|||||||
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" />
|
type="com.wireguard.android.databinding.ObservableKeyedArrayList<String, ApplicationData>" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<FrameLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:minHeight="200dp">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/progress_bar"
|
android:id="@+id/tabs"
|
||||||
android:layout_width="wrap_content"
|
style="@style/Widget.MaterialComponents.TabLayout.Colored"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_gravity="center"
|
android:layout_height="wrap_content">
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
|
|
||||||
tools:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<com.google.android.material.tabs.TabItem
|
||||||
android:id="@+id/app_list"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/exclude_from_tunnel" />
|
||||||
|
|
||||||
|
<com.google.android.material.tabs.TabItem
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/include_in_tunnel" />
|
||||||
|
</com.google.android.material.tabs.TabLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:items="@{appData}"
|
android:minHeight="200dp">
|
||||||
app:layout="@{@layout/app_list_item}"
|
|
||||||
tools:itemCount="10"
|
|
||||||
tools:listitem="@layout/app_list_item" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:visibility="@{appData.isEmpty() ? View.VISIBLE : View.GONE}"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/app_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:items="@{appData}"
|
||||||
|
app:layout="@{@layout/app_list_item}"
|
||||||
|
tools:itemCount="10"
|
||||||
|
tools:listitem="@layout/app_list_item" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/list_item_background"
|
android:background="@drawable/list_item_background"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:onClick="@{(view) -> item.setExcludedFromTunnel(!item.excludedFromTunnel)}"
|
android:onClick="@{(view) -> item.setSelected(!item.selected)}"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingTop="8dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingBottom="8dp">
|
android:paddingBottom="8dp">
|
||||||
@ -51,10 +51,10 @@
|
|||||||
tools:text="@tools:sample/full_names" />
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/excluded_checkbox"
|
android:id="@+id/selected_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:checked="@={item.excludedFromTunnel}"
|
android:checked="@={item.selected}"
|
||||||
tools:checked="true" />
|
tools:checked="true" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -220,8 +220,8 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="4dp"
|
android:layout_margin="4dp"
|
||||||
android:onClick="@{fragment::onRequestSetExcludedApplications}"
|
android:onClick="@{fragment::onRequestSetExcludedIncludedApplications}"
|
||||||
android:text="@{@plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size)}"
|
android:text="@{config.interface.includedApplications.size > 0 ? @plurals/set_included_applications(config.interface.includedApplications.size, config.interface.includedApplications.size) : config.interface.excludedApplications.size > 0 ? @plurals/set_excluded_applications(config.interface.excludedApplications.size, config.interface.excludedApplications.size) : @string/all_applications}"
|
||||||
android:textColor="?attr/colorSecondary"
|
android:textColor="?attr/colorSecondary"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
@ -159,7 +159,7 @@
|
|||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/excluded_checkbox"
|
android:id="@+id/selected_checkbox"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
|
@ -76,7 +76,6 @@
|
|||||||
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
|
<string name="error_root">Bitte root-Zugriff anfordern und erneut versuchen</string>
|
||||||
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
|
<string name="error_up">Fehler beim Starten des Tunnels: %s</string>
|
||||||
<string name="exclude_private_ips">Private IPs ausschließen</string>
|
<string name="exclude_private_ips">Private IPs ausschließen</string>
|
||||||
<string name="excluded_applications">Ausgeschlossene Anwendungen</string>
|
|
||||||
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
|
<string name="generate_new_private_key">Neuen privaten Schlüssel generieren</string>
|
||||||
<string name="generic_error">Unbekannter „%s“ Fehler</string>
|
<string name="generic_error">Unbekannter „%s“ Fehler</string>
|
||||||
<string name="hint_automatic">(auto)</string>
|
<string name="hint_automatic">(auto)</string>
|
||||||
@ -142,7 +141,6 @@
|
|||||||
<string name="restore_on_boot_title">Beim Neustart wiederherstellen</string>
|
<string name="restore_on_boot_title">Beim Neustart wiederherstellen</string>
|
||||||
<string name="save">Speichern</string>
|
<string name="save">Speichern</string>
|
||||||
<string name="select_all">Alle auswählen</string>
|
<string name="select_all">Alle auswählen</string>
|
||||||
<string name="set_exclusions">Ausnahmen festlegen</string>
|
|
||||||
<string name="settings">Einstellungen</string>
|
<string name="settings">Einstellungen</string>
|
||||||
<string name="shell_exit_status_read_error">Shell kann den Exit-Status nicht lesen</string>
|
<string name="shell_exit_status_read_error">Shell kann den Exit-Status nicht lesen</string>
|
||||||
<string name="shell_marker_count_error">Die Shell erwartete 4 Marker, erhielt aber %d</string>
|
<string name="shell_marker_count_error">Die Shell erwartete 4 Marker, erhielt aber %d</string>
|
||||||
|
@ -72,7 +72,6 @@
|
|||||||
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
|
<string name="error_root">कृपया रूट एक्सेस प्राप्त करें और पुनः प्रयास करें</string>
|
||||||
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
|
<string name="error_up">टनल को लाने में त्रुटि: %s</string>
|
||||||
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
|
<string name="exclude_private_ips">निजी आईपी को छोड़ दें</string>
|
||||||
<string name="excluded_applications">निकाले गए ऐप्स</string>
|
|
||||||
<string name="generic_error">अज्ञात “%s” त्रुटि</string>
|
<string name="generic_error">अज्ञात “%s” त्रुटि</string>
|
||||||
<string name="hint_automatic">(ऑटो)</string>
|
<string name="hint_automatic">(ऑटो)</string>
|
||||||
<string name="hint_generated">(उत्पन्न)</string>
|
<string name="hint_generated">(उत्पन्न)</string>
|
||||||
@ -128,7 +127,6 @@
|
|||||||
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
|
<string name="restore_on_boot_title">बूट पर पुनर्स्थापित करें</string>
|
||||||
<string name="save">सहेजें</string>
|
<string name="save">सहेजें</string>
|
||||||
<string name="select_all">सभी का चयन करे</string>
|
<string name="select_all">सभी का चयन करे</string>
|
||||||
<string name="set_exclusions">बहिष्करण सेट करें</string>
|
|
||||||
<string name="settings">सेटिंग्स</string>
|
<string name="settings">सेटिंग्स</string>
|
||||||
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
|
<string name="shell_exit_status_read_error">शेल बाहर निकलने की स्थिति नहीं पढ़ सकता</string>
|
||||||
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
|
<string name="shell_marker_count_error">शेल ने 4 मार्करों की अपेक्षा की, %d प्राप्त किया</string>
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
<string name="error_root">Izinkan akses root dan coba lagi</string>
|
<string name="error_root">Izinkan akses root dan coba lagi</string>
|
||||||
<string name="error_up">Kesalahan menambahkan tunel: %s</string>
|
<string name="error_up">Kesalahan menambahkan tunel: %s</string>
|
||||||
<string name="exclude_private_ips">Kecualikan IP pribadi</string>
|
<string name="exclude_private_ips">Kecualikan IP pribadi</string>
|
||||||
<string name="excluded_applications">Kecualikan aplikasi</string>
|
|
||||||
<string name="generate_new_private_key">Buat kunci privat baru</string>
|
<string name="generate_new_private_key">Buat kunci privat baru</string>
|
||||||
<string name="generic_error">Eror “%s” Tidak diketahui</string>
|
<string name="generic_error">Eror “%s” Tidak diketahui</string>
|
||||||
<string name="hint_automatic">(otomatis)</string>
|
<string name="hint_automatic">(otomatis)</string>
|
||||||
@ -136,7 +135,6 @@
|
|||||||
<string name="restore_on_boot_title">Pulihkan saat boot</string>
|
<string name="restore_on_boot_title">Pulihkan saat boot</string>
|
||||||
<string name="save">Simpan</string>
|
<string name="save">Simpan</string>
|
||||||
<string name="select_all">Pilih semua</string>
|
<string name="select_all">Pilih semua</string>
|
||||||
<string name="set_exclusions">Tetapkan pengecualian</string>
|
|
||||||
<string name="settings">Pengaturan</string>
|
<string name="settings">Pengaturan</string>
|
||||||
<string name="shell_exit_status_read_error">Shell tidak dapat membaca status keluar</string>
|
<string name="shell_exit_status_read_error">Shell tidak dapat membaca status keluar</string>
|
||||||
<string name="shell_marker_count_error">Shell diharapkan 4 nilai, diterima %d</string>
|
<string name="shell_marker_count_error">Shell diharapkan 4 nilai, diterima %d</string>
|
||||||
|
@ -76,7 +76,6 @@
|
|||||||
<string name="error_root">Accedi come root e riprova</string>
|
<string name="error_root">Accedi come root e riprova</string>
|
||||||
<string name="error_up">Errore di attivazione del tunnel: %s</string>
|
<string name="error_up">Errore di attivazione del tunnel: %s</string>
|
||||||
<string name="exclude_private_ips">Escludi IP privati</string>
|
<string name="exclude_private_ips">Escludi IP privati</string>
|
||||||
<string name="excluded_applications">Applicazioni escluse</string>
|
|
||||||
<string name="generate_new_private_key">Genera nuova chiave privata</string>
|
<string name="generate_new_private_key">Genera nuova chiave privata</string>
|
||||||
<string name="generic_error">Errore “%s” sconosciuto</string>
|
<string name="generic_error">Errore “%s” sconosciuto</string>
|
||||||
<string name="hint_automatic">(auto)</string>
|
<string name="hint_automatic">(auto)</string>
|
||||||
@ -142,7 +141,6 @@
|
|||||||
<string name="restore_on_boot_title">Ripristina all\'avvio</string>
|
<string name="restore_on_boot_title">Ripristina all\'avvio</string>
|
||||||
<string name="save">Salva</string>
|
<string name="save">Salva</string>
|
||||||
<string name="select_all">Seleziona tutto</string>
|
<string name="select_all">Seleziona tutto</string>
|
||||||
<string name="set_exclusions">Imposta esclusioni</string>
|
|
||||||
<string name="settings">Impostazioni</string>
|
<string name="settings">Impostazioni</string>
|
||||||
<string name="shell_exit_status_read_error">La shell non riesce a leggere lo stato di uscita</string>
|
<string name="shell_exit_status_read_error">La shell non riesce a leggere lo stato di uscita</string>
|
||||||
<string name="shell_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string>
|
<string name="shell_marker_count_error">La shell si aspettava 4 marker, ne ha ricevuti %d</string>
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
<string name="error_root">root 権限を取得して再試行してください</string>
|
<string name="error_root">root 権限を取得して再試行してください</string>
|
||||||
<string name="error_up">トンネル起動時エラー: %s</string>
|
<string name="error_up">トンネル起動時エラー: %s</string>
|
||||||
<string name="exclude_private_ips">プライベート IP アドレスを除外</string>
|
<string name="exclude_private_ips">プライベート IP アドレスを除外</string>
|
||||||
<string name="excluded_applications">対象外とするアプリケーション</string>
|
|
||||||
<string name="generate_new_private_key">新しい秘密鍵を生成する</string>
|
<string name="generate_new_private_key">新しい秘密鍵を生成する</string>
|
||||||
<string name="generic_error">未知の “%s” エラー</string>
|
<string name="generic_error">未知の “%s” エラー</string>
|
||||||
<string name="hint_automatic">(自動)</string>
|
<string name="hint_automatic">(自動)</string>
|
||||||
@ -136,7 +135,6 @@
|
|||||||
<string name="restore_on_boot_title">起動時に復元</string>
|
<string name="restore_on_boot_title">起動時に復元</string>
|
||||||
<string name="save">保存</string>
|
<string name="save">保存</string>
|
||||||
<string name="select_all">すべて選択</string>
|
<string name="select_all">すべて選択</string>
|
||||||
<string name="set_exclusions">対象外アプリを設定</string>
|
|
||||||
<string name="settings">設定</string>
|
<string name="settings">設定</string>
|
||||||
<string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string>
|
<string name="shell_exit_status_read_error">シェルは終了ステータスを取得できません</string>
|
||||||
<string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string>
|
<string name="shell_marker_count_error">シェルは 4 マーカーを期待していますが、 %d マーカーを受け取りました</string>
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
|
<string name="error_root">Пожалуйста, получите root-доступ и попробуйте снова</string>
|
||||||
<string name="error_up">Ошибка при запуске туннеля: %s</string>
|
<string name="error_up">Ошибка при запуске туннеля: %s</string>
|
||||||
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
|
<string name="exclude_private_ips">Исключить частные IP-адреса</string>
|
||||||
<string name="excluded_applications">Исключенные приложения</string>
|
|
||||||
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string>
|
<string name="generate_new_private_key">Сгенерировать новый приватный ключ</string>
|
||||||
<string name="generic_error">Неизвестная “%s” ошибка</string>
|
<string name="generic_error">Неизвестная “%s” ошибка</string>
|
||||||
<string name="hint_automatic">(авто)</string>
|
<string name="hint_automatic">(авто)</string>
|
||||||
@ -154,7 +153,6 @@
|
|||||||
<string name="restore_on_boot_title">Восстанавливать при загрузке</string>
|
<string name="restore_on_boot_title">Восстанавливать при загрузке</string>
|
||||||
<string name="save">Сохранить</string>
|
<string name="save">Сохранить</string>
|
||||||
<string name="select_all">Выбрать все</string>
|
<string name="select_all">Выбрать все</string>
|
||||||
<string name="set_exclusions">ОК</string>
|
|
||||||
<string name="settings">Настройки</string>
|
<string name="settings">Настройки</string>
|
||||||
<string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string>
|
<string name="shell_exit_status_read_error">Shell не может прочитать статус выхода</string>
|
||||||
<string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string>
|
<string name="shell_marker_count_error">Shell ожидает 4 маркера, получено %d</string>
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
<string name="config_file_exists_error">Konfiguracijska datoteka za „%s“ že obstaja</string>
|
<string name="config_file_exists_error">Konfiguracijska datoteka za „%s“ že obstaja</string>
|
||||||
<string name="config_not_found_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče najti</string>
|
<string name="config_not_found_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče najti</string>
|
||||||
<string name="config_rename_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče preimenovati</string>
|
<string name="config_rename_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče preimenovati</string>
|
||||||
<string name="config_save_error">Konfiguracijske datoteke za „%s“ ni bilo mogoče shraniti: %2$s</string>
|
<string name="config_save_error">Konfiguracijske datoteke za „%1$s“ ni bilo mogoče shraniti: %2$s</string>
|
||||||
<string name="config_save_success">Konfiguracijska datoteka za „%s“ uspešno shranjena</string>
|
<string name="config_save_success">Konfiguracijska datoteka za „%s“ uspešno shranjena</string>
|
||||||
<string name="create_activity_title">Ustvarite WireGuard tunel</string>
|
<string name="create_activity_title">Ustvarite WireGuard tunel</string>
|
||||||
<string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče kreirati</string>
|
<string name="create_bin_dir_error">Lokalnega imenika za aplikacijo ni bilo mogoče kreirati</string>
|
||||||
@ -88,7 +88,6 @@
|
|||||||
<string name="error_root">Prosim omogočite root dostop in poskusite ponovno</string>
|
<string name="error_root">Prosim omogočite root dostop in poskusite ponovno</string>
|
||||||
<string name="error_up">Napaka pri vzpostavitvi tunela: %s</string>
|
<string name="error_up">Napaka pri vzpostavitvi tunela: %s</string>
|
||||||
<string name="exclude_private_ips">Izključitev privatnih IP naslovov</string>
|
<string name="exclude_private_ips">Izključitev privatnih IP naslovov</string>
|
||||||
<string name="excluded_applications">Izključitev aplikacije</string>
|
|
||||||
<string name="generate_new_private_key">Generiraj nov privatni ključ</string>
|
<string name="generate_new_private_key">Generiraj nov privatni ključ</string>
|
||||||
<string name="generic_error">Neznana napaka „%s“</string>
|
<string name="generic_error">Neznana napaka „%s“</string>
|
||||||
<string name="hint_automatic">(samodejno)</string>
|
<string name="hint_automatic">(samodejno)</string>
|
||||||
@ -154,7 +153,6 @@
|
|||||||
<string name="restore_on_boot_title">Ponovna vzpostavitev pri ponovnem zagonu</string>
|
<string name="restore_on_boot_title">Ponovna vzpostavitev pri ponovnem zagonu</string>
|
||||||
<string name="save">Shrani</string>
|
<string name="save">Shrani</string>
|
||||||
<string name="select_all">Izbor vseh</string>
|
<string name="select_all">Izbor vseh</string>
|
||||||
<string name="set_exclusions">Določite izključitve</string>
|
|
||||||
<string name="settings">Nastavitve</string>
|
<string name="settings">Nastavitve</string>
|
||||||
<string name="shell_exit_status_read_error">Lupina ne more prebrati izhodnega statusa</string>
|
<string name="shell_exit_status_read_error">Lupina ne more prebrati izhodnega statusa</string>
|
||||||
<string name="shell_marker_count_error">Lupina je pričakovala 4 markerje, dobila pa je %d</string>
|
<string name="shell_marker_count_error">Lupina je pričakovala 4 markerje, dobila pa je %d</string>
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
<string name="error_root">请获取 root 权限并重试</string>
|
<string name="error_root">请获取 root 权限并重试</string>
|
||||||
<string name="error_up">建立连接时出错:%s</string>
|
<string name="error_up">建立连接时出错:%s</string>
|
||||||
<string name="exclude_private_ips">排除局域网</string>
|
<string name="exclude_private_ips">排除局域网</string>
|
||||||
<string name="excluded_applications">排除的应用</string>
|
|
||||||
<string name="generate_new_private_key">生成新的私钥</string>
|
<string name="generate_new_private_key">生成新的私钥</string>
|
||||||
<string name="generic_error">未知的 “%s” 错误</string>
|
<string name="generic_error">未知的 “%s” 错误</string>
|
||||||
<string name="hint_automatic">(自动)</string>
|
<string name="hint_automatic">(自动)</string>
|
||||||
@ -136,7 +135,6 @@
|
|||||||
<string name="restore_on_boot_title">启动时恢复</string>
|
<string name="restore_on_boot_title">启动时恢复</string>
|
||||||
<string name="save">保存</string>
|
<string name="save">保存</string>
|
||||||
<string name="select_all">全选</string>
|
<string name="select_all">全选</string>
|
||||||
<string name="set_exclusions">确定</string>
|
|
||||||
<string name="settings">设置</string>
|
<string name="settings">设置</string>
|
||||||
<string name="shell_exit_status_read_error">Shell 无法读取退出状态</string>
|
<string name="shell_exit_status_read_error">Shell 无法读取退出状态</string>
|
||||||
<string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string>
|
<string name="shell_marker_count_error">Shell 应获取 4 个标记,获取到 %d 个</string>
|
||||||
|
@ -24,6 +24,22 @@
|
|||||||
<item quantity="one">%d Excluded Application</item>
|
<item quantity="one">%d Excluded Application</item>
|
||||||
<item quantity="other">%d Excluded Applications</item>
|
<item quantity="other">%d Excluded Applications</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="set_included_applications">
|
||||||
|
<item quantity="one">%d Included Application</item>
|
||||||
|
<item quantity="other">%d Included Applications</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="all_applications">All Applications</string>
|
||||||
|
<string name="exclude_from_tunnel">Exclude:</string>
|
||||||
|
<string name="include_in_tunnel">Include only:</string>
|
||||||
|
<plurals name="include_n_applications">
|
||||||
|
<item quantity="one">Include %d apps</item>
|
||||||
|
<item quantity="other">Include %d apps</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="exclude_n_applications">
|
||||||
|
<item quantity="one">Exclude %d apps</item>
|
||||||
|
<item quantity="other">Exclude %d apps</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="use_all_applications">Use all apps</string>
|
||||||
<string name="add_peer">Add peer</string>
|
<string name="add_peer">Add peer</string>
|
||||||
<string name="addresses">Addresses</string>
|
<string name="addresses">Addresses</string>
|
||||||
<string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string>
|
<string name="allow_remote_control_intents_summary_off">External apps may not toggle tunnels (recommended)</string>
|
||||||
@ -76,7 +92,6 @@
|
|||||||
<string name="error_root">Please obtain root access and try again</string>
|
<string name="error_root">Please obtain root access and try again</string>
|
||||||
<string name="error_up">Error bringing up tunnel: %s</string>
|
<string name="error_up">Error bringing up tunnel: %s</string>
|
||||||
<string name="exclude_private_ips">Exclude private IPs</string>
|
<string name="exclude_private_ips">Exclude private IPs</string>
|
||||||
<string name="excluded_applications">Excluded Applications</string>
|
|
||||||
<string name="generate_new_private_key">Generate new private key</string>
|
<string name="generate_new_private_key">Generate new private key</string>
|
||||||
<string name="generic_error">Unknown “%s” error</string>
|
<string name="generic_error">Unknown “%s” error</string>
|
||||||
<string name="hint_automatic">(auto)</string>
|
<string name="hint_automatic">(auto)</string>
|
||||||
@ -142,7 +157,6 @@
|
|||||||
<string name="restore_on_boot_title">Restore on boot</string>
|
<string name="restore_on_boot_title">Restore on boot</string>
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="select_all">Select all</string>
|
<string name="select_all">Select all</string>
|
||||||
<string name="set_exclusions">Set Exclusions</string>
|
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="shell_exit_status_read_error">Shell cannot read exit status</string>
|
<string name="shell_exit_status_read_error">Shell cannot read exit status</string>
|
||||||
<string name="shell_marker_count_error">Shell expected 4 markers, received %d</string>
|
<string name="shell_marker_count_error">Shell expected 4 markers, received %d</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user