Improve dashboard

This commit is contained in:
世界 2023-08-04 21:04:45 +08:00
parent 3ccf4d6def
commit dd9db2599b
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
13 changed files with 1283 additions and 624 deletions

View file

@ -1,6 +1,7 @@
package io.nekohasekai.sfa.ktx
import android.content.Context
import android.graphics.Color
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
@ -15,3 +16,16 @@ fun Context.getAttrColor(
theme.resolveAttribute(attrColor, typedValue, resolveRefs)
return typedValue.data
}
@ColorInt
fun colorForURLTestDelay(urlTestDelay: Int): Int {
return if (urlTestDelay <= 0) {
Color.GRAY
} else if (urlTestDelay <= 800) {
Color.GREEN
} else if (urlTestDelay <= 1500) {
Color.YELLOW
} else {
Color.RED
}
}

View file

@ -0,0 +1,297 @@
package io.nekohasekai.sfa.ui.dashboard
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import go.Seq
import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.CommandClientHandler
import io.nekohasekai.libbox.CommandClientOptions
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroup
import io.nekohasekai.libbox.OutboundGroupItem
import io.nekohasekai.libbox.OutboundGroupIterator
import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.databinding.FragmentDashboardGroupsBinding
import io.nekohasekai.sfa.databinding.ViewDashboardGroupBinding
import io.nekohasekai.sfa.databinding.ViewDashboardGroupItemBinding
import io.nekohasekai.sfa.ktx.colorForURLTestDelay
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ui.MainActivity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class GroupsFragment : Fragment(), CommandClientHandler {
private val activity: MainActivity? get() = super.getActivity() as MainActivity?
private var _binding: FragmentDashboardGroupsBinding? = null
private val binding get() = _binding!!
private var commandClient: CommandClient? = null
private var _adapter: Adapter? = null
private val adapter get() = _adapter!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentDashboardGroupsBinding.inflate(inflater, container, false)
onCreate()
return binding.root
}
private fun onCreate() {
val activity = activity ?: return
_adapter = Adapter()
binding.container.adapter = adapter
binding.container.layoutManager = LinearLayoutManager(requireContext())
activity.serviceStatus.observe(viewLifecycleOwner) {
if (it == Status.Started) {
reconnect()
}
}
}
private fun reconnect() {
disconnect()
val options = CommandClientOptions()
options.command = Libbox.CommandGroup
options.statusInterval = 2 * 1000 * 1000 * 1000
val commandClient = CommandClient(requireContext().filesDir.absolutePath, this, options)
this.commandClient = commandClient
lifecycleScope.launch(Dispatchers.IO) {
for (i in 1..3) {
delay(100)
try {
commandClient.connect()
break
} catch (e: Exception) {
break
}
}
}
}
private fun disconnect() {
commandClient?.apply {
runCatching {
disconnect()
}
Seq.destroyRef(refnum)
}
commandClient = null
}
override fun connected() {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.statusText.isVisible = false
binding.container.isVisible = true
}
}
override fun disconnected(message: String?) {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.statusText.isVisible = true
binding.container.isVisible = false
}
}
@SuppressLint("NotifyDataSetChanged")
override fun writeGroups(message: OutboundGroupIterator) {
val groups = mutableListOf<OutboundGroup>()
while (message.hasNext()) {
groups.add(message.next())
}
activity?.runOnUiThread {
adapter.groups = groups
adapter.notifyDataSetChanged()
}
}
override fun writeLog(message: String?) {
}
override fun writeStatus(message: StatusMessage?) {
}
private class Adapter : RecyclerView.Adapter<GroupView>() {
lateinit var groups: List<OutboundGroup>
private val expandStatus: MutableMap<String, Boolean> = mutableMapOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GroupView {
return GroupView(
ViewDashboardGroupBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
expandStatus
)
}
override fun getItemCount(): Int {
if (!::groups.isInitialized) {
return 0
}
return groups.size
}
override fun onBindViewHolder(holder: GroupView, position: Int) {
holder.bind(groups[position])
}
}
private class GroupView(
val binding: ViewDashboardGroupBinding,
val expandStatus: MutableMap<String, Boolean>
) :
RecyclerView.ViewHolder(binding.root) {
lateinit var group: OutboundGroup
lateinit var items: MutableList<OutboundGroupItem>
lateinit var adapter: ItemAdapter
fun bind(group: OutboundGroup) {
this.group = group
binding.groupName.text = group.tag
binding.groupType.text = Libbox.proxyDisplayType(group.type)
binding.urlTestButton.setOnClickListener {
GlobalScope.launch {
runCatching {
Libbox.newStandaloneCommandClient().urlTest(group.tag)
}.onFailure {
withContext(Dispatchers.Main) {
binding.root.context.errorDialogBuilder(it).show()
}
}
}
}
items = mutableListOf()
val itemIterator = group.items
while (itemIterator.hasNext()) {
items.add(itemIterator.next())
}
adapter = ItemAdapter(this, group, items)
binding.itemList.adapter = adapter
(binding.itemList.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.itemList.layoutManager = GridLayoutManager(binding.root.context, 2)
updateExpand()
}
private fun updateExpand(isExpand: Boolean? = null) {
val newExpandStatus: Boolean
if (isExpand == null) {
newExpandStatus = expandStatus[group.tag] ?: group.selectable
} else {
expandStatus[group.tag] = isExpand
newExpandStatus = isExpand
}
binding.itemList.isVisible = newExpandStatus
binding.itemText.isVisible = !newExpandStatus
if (!newExpandStatus) {
val builder = SpannableStringBuilder()
items.forEach {
builder.append("")
builder.setSpan(
ForegroundColorSpan(colorForURLTestDelay(it.urlTestDelay)),
builder.length - 1,
builder.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
builder.append(" ")
}
binding.itemText.text = builder
}
if (newExpandStatus) {
binding.expandButton.setImageResource(R.drawable.ic_expand_less_24)
} else {
binding.expandButton.setImageResource(R.drawable.ic_expand_more_24)
}
binding.expandButton.setOnClickListener {
updateExpand(!binding.itemList.isVisible)
}
}
fun updateSelected(group: OutboundGroup, item: OutboundGroupItem) {
val oldSelected = items.indexOfFirst { it.tag == group.selected }
group.selected = item.tag
if (oldSelected != -1) {
adapter.notifyItemChanged(oldSelected)
}
}
}
private class ItemAdapter(
val groupView: GroupView,
val group: OutboundGroup,
val items: List<OutboundGroupItem>
) :
RecyclerView.Adapter<ItemGroupView>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemGroupView {
return ItemGroupView(
ViewDashboardGroupItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ItemGroupView, position: Int) {
holder.bind(groupView, group, items[position])
}
}
private class ItemGroupView(val binding: ViewDashboardGroupItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(groupView: GroupView, group: OutboundGroup, item: OutboundGroupItem) {
binding.itemCard.setOnClickListener {
binding.selectedView.isVisible = true
groupView.updateSelected(group, item)
GlobalScope.launch {
runCatching {
Libbox.newStandaloneCommandClient().selectOutbound(group.tag, item.tag)
}.onFailure {
withContext(Dispatchers.Main) {
binding.root.context.errorDialogBuilder(it).show()
}
}
}
}
binding.selectedView.isInvisible = group.selected != item.tag
binding.itemName.text = item.tag
binding.itemType.text = Libbox.proxyDisplayType(item.type)
binding.itemStatus.isVisible = item.urlTestTime > 0
if (item.urlTestTime > 0) {
binding.itemStatus.text = "${item.urlTestDelay}ms"
binding.itemStatus.setTextColor(colorForURLTestDelay(item.urlTestDelay))
}
}
}
}

View file

@ -0,0 +1,262 @@
package io.nekohasekai.sfa.ui.dashboard
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDividerItemDecoration
import go.Seq
import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.CommandClientHandler
import io.nekohasekai.libbox.CommandClientOptions
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroupIterator
import io.nekohasekai.libbox.StatusMessage
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.bg.BoxService
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.FragmentDashboardOverviewBinding
import io.nekohasekai.sfa.databinding.ViewProfileItemBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class OverviewFragment : Fragment(), CommandClientHandler {
private val activity: MainActivity? get() = super.getActivity() as MainActivity?
private var _binding: FragmentDashboardOverviewBinding? = null
private val binding get() = _binding!!
private var commandClient: CommandClient? = null
private var _adapter: Adapter? = null
private val adapter get() = _adapter!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View {
_binding = FragmentDashboardOverviewBinding.inflate(inflater, container, false)
onCreate()
return binding.root
}
private fun onCreate() {
val activity = activity ?: return
binding.profileList.adapter = Adapter(lifecycleScope, binding).apply {
_adapter = this
reload()
}
binding.profileList.layoutManager = LinearLayoutManager(requireContext())
val divider = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
binding.profileList.addItemDecoration(divider)
activity.serviceStatus.observe(viewLifecycleOwner) {
binding.statusContainer.isVisible = it == Status.Starting || it == Status.Started
if (it == Status.Started) {
reconnect()
}
}
ProfileManager.registerCallback(this::updateProfiles)
}
private fun reconnect() {
disconnect()
val options = CommandClientOptions()
options.command = Libbox.CommandStatus
options.statusInterval = 2 * 1000 * 1000 * 1000
val commandClient = CommandClient(requireContext().filesDir.absolutePath, this, options)
this.commandClient = commandClient
lifecycleScope.launch(Dispatchers.IO) {
for (i in 1..3) {
delay(100)
try {
commandClient.connect()
break
} catch (e: Exception) {
break
}
}
}
}
private fun disconnect() {
commandClient?.apply {
runCatching {
disconnect()
}
Seq.destroyRef(refnum)
}
commandClient = null
}
override fun onDestroyView() {
super.onDestroyView()
_adapter = null
_binding = null
disconnect()
ProfileManager.unregisterCallback(this::updateProfiles)
}
private fun updateProfiles() {
_adapter?.reload()
}
override fun connected() {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = getString(R.string.loading)
binding.goroutinesText.text = getString(R.string.loading)
}
}
override fun disconnected(message: String?) {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = getString(R.string.loading)
binding.goroutinesText.text = getString(R.string.loading)
}
}
override fun writeLog(message: String) {
}
override fun writeStatus(message: StatusMessage) {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = Libbox.formatBytes(message.memory)
binding.goroutinesText.text = message.goroutines.toString()
val trafficAvailable = message.trafficAvailable
binding.trafficContainer.isVisible = trafficAvailable
if (trafficAvailable) {
binding.inboundConnectionsText.text = message.connectionsIn.toString()
binding.outboundConnectionsText.text = message.connectionsOut.toString()
binding.uplinkText.text = Libbox.formatBytes(message.uplink) + "/s"
binding.downlinkText.text = Libbox.formatBytes(message.downlink) + "/s"
binding.uplinkTotalText.text = Libbox.formatBytes(message.uplinkTotal)
binding.downlinkTotalText.text = Libbox.formatBytes(message.downlinkTotal)
}
}
}
override fun writeGroups(message: OutboundGroupIterator?) {
}
class Adapter(
internal val scope: CoroutineScope,
private val parent: FragmentDashboardOverviewBinding
) :
RecyclerView.Adapter<Holder>() {
internal var items: MutableList<Profile> = mutableListOf()
internal var selectedProfileID = -1L
internal var lastSelectedIndex: Int? = null
internal fun reload() {
scope.launch(Dispatchers.IO) {
items = ProfileManager.list().toMutableList()
if (items.isNotEmpty()) {
selectedProfileID = Settings.selectedProfile
for ((index, profile) in items.withIndex()) {
if (profile.id == selectedProfileID) {
lastSelectedIndex = index
break
}
}
if (lastSelectedIndex == null) {
lastSelectedIndex = 0
selectedProfileID = items[0].id
Settings.selectedProfile = selectedProfileID
}
}
withContext(Dispatchers.Main) {
parent.statusText.isVisible = items.isEmpty()
parent.container.isVisible = items.isNotEmpty()
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder(
this,
ViewProfileItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(items[position])
}
override fun getItemCount(): Int {
return items.size
}
}
class Holder(
private val adapter: Adapter,
private val binding: ViewProfileItemBinding
) :
RecyclerView.ViewHolder(binding.root) {
internal fun bind(profile: Profile) {
binding.profileName.text = profile.name
binding.profileSelected.setOnCheckedChangeListener(null)
binding.profileSelected.isChecked = profile.id == adapter.selectedProfileID
binding.profileSelected.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
adapter.selectedProfileID = profile.id
adapter.lastSelectedIndex?.let { index ->
adapter.notifyItemChanged(index)
}
adapter.lastSelectedIndex = adapterPosition
adapter.scope.launch(Dispatchers.IO) {
switchProfile(profile)
}
}
}
binding.root.setOnClickListener {
binding.profileSelected.toggle()
}
}
private suspend fun switchProfile(profile: Profile) {
Settings.selectedProfile = profile.id
val mainActivity = (binding.root.context as? MainActivity) ?: return
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) {
return
}
val restart = Settings.rebuildServiceMode()
if (restart) {
mainActivity.reconnect()
BoxService.stop()
delay(200)
mainActivity.startService()
return
}
runCatching {
Libbox.newStandaloneCommandClient().serviceReload()
}.onFailure {
withContext(Dispatchers.Main) {
mainActivity.errorDialogBuilder(it).show()
}
}
}
}
}

View file

@ -4,44 +4,24 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.divider.MaterialDividerItemDecoration
import go.Seq
import io.nekohasekai.libbox.CommandClient
import io.nekohasekai.libbox.CommandClientHandler
import io.nekohasekai.libbox.CommandClientOptions
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.OutboundGroupIterator
import io.nekohasekai.libbox.StatusMessage
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import io.nekohasekai.sfa.R
import io.nekohasekai.sfa.bg.BoxService
import io.nekohasekai.sfa.constant.Status
import io.nekohasekai.sfa.database.Profile
import io.nekohasekai.sfa.database.ProfileManager
import io.nekohasekai.sfa.database.Settings
import io.nekohasekai.sfa.databinding.FragmentDashboardBinding
import io.nekohasekai.sfa.databinding.ViewProfileItemBinding
import io.nekohasekai.sfa.ktx.errorDialogBuilder
import io.nekohasekai.sfa.ui.MainActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import io.nekohasekai.sfa.ui.dashboard.GroupsFragment
import io.nekohasekai.sfa.ui.dashboard.OverviewFragment
class DashboardFragment : Fragment(), CommandClientHandler {
class DashboardFragment : Fragment(R.layout.fragment_dashboard) {
private val activity: MainActivity? get() = super.getActivity() as MainActivity?
private var _binding: FragmentDashboardBinding? = null
private val binding get() = _binding!!
private var commandClient: CommandClient? = null
private var _adapter: Adapter? = null
private val adapter get() = _adapter!!
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
@ -53,22 +33,17 @@ class DashboardFragment : Fragment(), CommandClientHandler {
private fun onCreate() {
val activity = activity ?: return
binding.profileList.adapter = Adapter(lifecycleScope, binding).apply {
_adapter = this
reload()
}
binding.profileList.layoutManager = LinearLayoutManager(requireContext())
val divider = MaterialDividerItemDecoration(requireContext(), LinearLayoutManager.VERTICAL)
divider.isLastItemDecorated = false
binding.profileList.addItemDecoration(divider)
binding.dashboardPager.adapter = Adapter(this)
TabLayoutMediator(binding.dashboardTabLayout, binding.dashboardPager) { tab, position ->
tab.setText(Page.values()[position].titleRes)
}.attach()
activity.serviceStatus.observe(viewLifecycleOwner) {
binding.statusContainer.isVisible = it == Status.Starting || it == Status.Started
when (it) {
Status.Stopped -> {
binding.fab.setImageResource(R.drawable.ic_play_arrow_24)
binding.fab.show()
binding.dashboardTabLayout.isVisible = false
binding.dashboardPager.isUserInputEnabled = false
}
Status.Starting -> {
@ -78,7 +53,8 @@ class DashboardFragment : Fragment(), CommandClientHandler {
Status.Started -> {
binding.fab.setImageResource(R.drawable.ic_stop_24)
binding.fab.show()
reconnect()
binding.dashboardTabLayout.isVisible = true
binding.dashboardPager.isUserInputEnabled = true
}
Status.Stopping -> {
@ -101,196 +77,20 @@ class DashboardFragment : Fragment(), CommandClientHandler {
else -> {}
}
}
ProfileManager.registerCallback(this::updateProfiles)
}
private fun reconnect() {
disconnect()
val options = CommandClientOptions()
options.command = Libbox.CommandStatus
options.statusInterval = 2 * 1000 * 1000 * 1000
val commandClient = CommandClient(requireContext().filesDir.absolutePath, this, options)
this.commandClient = commandClient
lifecycleScope.launch(Dispatchers.IO) {
for (i in 1..3) {
delay(100)
try {
commandClient.connect()
break
} catch (e: Exception) {
break
}
}
}
enum class Page(@StringRes val titleRes: Int, val fragmentClass: Class<out Fragment>) {
Overview(R.string.title_overview, OverviewFragment::class.java),
Groups(R.string.title_groups, GroupsFragment::class.java);
}
private fun disconnect() {
commandClient?.apply {
runCatching {
disconnect()
}
Seq.destroyRef(refnum)
}
commandClient = null
}
override fun onDestroyView() {
super.onDestroyView()
_adapter = null
_binding = null
disconnect()
ProfileManager.unregisterCallback(this::updateProfiles)
}
private fun updateProfiles() {
_adapter?.reload()
}
override fun connected() {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = getString(R.string.loading)
binding.goroutinesText.text = getString(R.string.loading)
}
}
override fun disconnected(message: String?) {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = getString(R.string.loading)
binding.goroutinesText.text = getString(R.string.loading)
}
}
override fun writeLog(message: String) {
}
override fun writeStatus(message: StatusMessage) {
val binding = _binding ?: return
lifecycleScope.launch(Dispatchers.Main) {
binding.memoryText.text = Libbox.formatBytes(message.memory)
binding.goroutinesText.text = message.goroutines.toString()
val trafficAvailable = message.trafficAvailable
binding.trafficContainer.isVisible = trafficAvailable
if (trafficAvailable) {
binding.inboundConnectionsText.text = message.connectionsIn.toString()
binding.outboundConnectionsText.text = message.connectionsOut.toString()
binding.uplinkText.text = Libbox.formatBytes(message.uplink) + "/s"
binding.downlinkText.text = Libbox.formatBytes(message.downlink) + "/s"
binding.uplinkTotalText.text = Libbox.formatBytes(message.uplinkTotal)
binding.downlinkTotalText.text = Libbox.formatBytes(message.downlinkTotal)
}
}
}
override fun writeGroups(message: OutboundGroupIterator?) {
}
class Adapter(
internal val scope: CoroutineScope,
private val parent: FragmentDashboardBinding
) :
RecyclerView.Adapter<Holder>() {
internal var items: MutableList<Profile> = mutableListOf()
internal var selectedProfileID = -1L
internal var lastSelectedIndex: Int? = null
internal fun reload() {
scope.launch(Dispatchers.IO) {
items = ProfileManager.list().toMutableList()
if (items.isNotEmpty()) {
selectedProfileID = Settings.selectedProfile
for ((index, profile) in items.withIndex()) {
if (profile.id == selectedProfileID) {
lastSelectedIndex = index
break
}
}
if (lastSelectedIndex == null) {
lastSelectedIndex = 0
selectedProfileID = items[0].id
Settings.selectedProfile = selectedProfileID
}
}
withContext(Dispatchers.Main) {
parent.statusText.isVisible = items.isEmpty()
parent.container.isVisible = items.isNotEmpty()
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return Holder(
this,
ViewProfileItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: Holder, position: Int) {
holder.bind(items[position])
}
class Adapter(parent: Fragment) : FragmentStateAdapter(parent) {
override fun getItemCount(): Int {
return items.size
return Page.values().size
}
}
class Holder(
private val adapter: Adapter,
private val binding: ViewProfileItemBinding
) :
RecyclerView.ViewHolder(binding.root) {
internal fun bind(profile: Profile) {
binding.profileName.text = profile.name
binding.profileSelected.setOnCheckedChangeListener(null)
binding.profileSelected.isChecked = profile.id == adapter.selectedProfileID
binding.profileSelected.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
adapter.selectedProfileID = profile.id
adapter.lastSelectedIndex?.let { index ->
adapter.notifyItemChanged(index)
}
adapter.lastSelectedIndex = adapterPosition
adapter.scope.launch(Dispatchers.IO) {
switchProfile(profile)
}
}
}
binding.root.setOnClickListener {
binding.profileSelected.toggle()
}
}
private suspend fun switchProfile(profile: Profile) {
Settings.selectedProfile = profile.id
val mainActivity = (binding.root.context as? MainActivity) ?: return
val started = mainActivity.serviceStatus.value == Status.Started
if (!started) {
return
}
val restart = Settings.rebuildServiceMode()
if (restart) {
mainActivity.reconnect()
BoxService.stop()
delay(200)
mainActivity.startService()
return
}
runCatching {
Libbox.newStandaloneCommandClient().serviceReload()
}.onFailure {
withContext(Dispatchers.Main) {
mainActivity.errorDialogBuilder(it).show()
}
}
override fun createFragment(position: Int): Fragment {
return Page.values()[position].fragmentClass.newInstance()
}
}

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M14.69,2.21L4.33,11.49c-0.64,0.58 -0.28,1.65 0.58,1.73L13,14l-4.85,6.76c-0.22,0.31 -0.19,0.74 0.08,1.01h0c0.3,0.3 0.77,0.31 1.08,0.02l10.36,-9.28c0.64,-0.58 0.28,-1.65 -0.58,-1.73L11,10l4.85,-6.76c0.22,-0.31 0.19,-0.74 -0.08,-1.01l0,0C15.47,1.93 15,1.92 14.69,2.21z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View file

@ -6,418 +6,39 @@
android:layout_height="match_parent"
tools:context=".ui.main.DashboardFragment">
<TextView
android:id="@+id/statusText"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/profile_empty"
android:textSize="20sp"
android:visibility="gone" />
android:gravity="center|top"
android:orientation="vertical">
<ScrollView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
<com.google.android.material.tabs.TabLayout
android:id="@+id/dashboardTabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="16dp">
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:tabIndicatorFullWidth="true">
<LinearLayout
android:id="@+id/statusContainer"
android:layout_width="match_parent"
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:clipChildren="false"
android:visibility="gone"
tools:visibility="visible">
android:text="@string/title_overview" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_memory"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/memoryText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_goroutines"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/goroutinesText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:id="@+id/trafficContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_inbound_connections"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="bottom|start"
android:orientation="vertical">
<TextView
android:id="@+id/inboundConnectionsText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_outbound_connections"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/outboundConnectionsText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_uplink"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/uplinkText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_downlink"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/downlinkText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_uplink_total"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/uplinkTotalText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_downlink_total"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<TextView
android:id="@+id/downlinkTotalText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/loading" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/profileCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile"
android:textAppearance="?attr/textAppearanceTitleLarge">
</TextView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profileList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
tools:itemCount="3"
tools:listitem="@layout/view_profile_item" />
android:text="@string/title_groups" />
</LinearLayout>
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/dashboardPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</ScrollView>
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/status_default"
android:textSize="20sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,472 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/profile_empty"
android:textSize="20sp"
android:visibility="gone" />
<ScrollView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:id="@+id/statusContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_status"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_memory" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/memoryText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_goroutines" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/goroutinesText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_connections"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_connections_inbound" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/inboundConnectionsText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_connections_outbound" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/outboundConnectionsText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<LinearLayout
android:id="@+id/trafficContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_traffic"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_uplink" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/uplinkText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_downlink" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/downlinkText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<View
android:layout_width="16dp"
android:layout_height="match_parent" />
<com.google.android.material.card.MaterialCardView
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_traffic_total"
android:textAppearance="?attr/textAppearanceTitleSmall">
</TextView>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_uplink" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/uplinkTotalText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/status_downlink" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/downlinkTotalText"
style="?attr/textAppearanceBodySmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/loading"
android:textColor="?android:colorForeground" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/profileCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
tools:visibility="visible">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/profile"
android:textAppearance="?attr/textAppearanceTitleLarge">
</TextView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/profileList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
tools:itemCount="3"
tools:listitem="@layout/view_profile_item" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</ScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/groupName"
style="?attr/textAppearanceTitleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Group Name" />
<TextView
android:id="@+id/groupType"
style="?attr/textAppearanceBodyLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
tools:text="Group Name" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<ImageButton
android:id="@+id/expandButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:backgroundTint="?colorSurface"
android:contentDescription="@string/expand"
android:src="@drawable/ic_expand_less_24"
app:tint="?colorControlNormal" />
<ImageButton
android:id="@+id/urlTestButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:backgroundTint="?colorSurface"
android:contentDescription="@string/urltest"
android:src="@drawable/ic_electric_bolt_24"
app:tint="?colorControlNormal" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/itemText"
android:layout_width="match_parent"
android:paddingEnd="56dp"
android:layout_height="wrap_content" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemList"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>

View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_card"
style="?materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/selected_view"
android:layout_width="4dp"
android:layout_height="match_parent"
android:background="?colorPrimary"
android:orientation="horizontal"
android:visibility="invisible"
tools:visibility="visible" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/item_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
tools:text="Name" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/item_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary"
tools:text="Type" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end">
<TextView
android:id="@+id/item_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="Status" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -10,6 +10,8 @@
<string name="title_new_profile">New Profile</string>
<string name="title_edit_profile">Edit Profile</string>
<string name="title_edit_configuration">Edit Configuration</string>
<string name="title_overview">Overview</string>
<string name="title_groups">Groups</string>
<string name="quick_toggle">Toggle</string>
<string name="profile_name">Name</string>
@ -61,14 +63,16 @@
<string name="service_error_title_create_service">Create service</string>
<string name="service_error_title_start_service">Start service</string>
<string name="status_status">Status</string>
<string name="status_memory">Memory</string>
<string name="status_goroutines">Goroutines</string>
<string name="status_inbound_connections">Inbound Connections</string>
<string name="status_outbound_connections">Outbound Connections</string>
<string name="status_connections">Connections</string>
<string name="status_connections_inbound">Inbound</string>
<string name="status_connections_outbound">Outbound</string>
<string name="status_uplink">Uplink</string>
<string name="status_downlink">Downlink</string>
<string name="status_uplink_total">Uplink Total</string>
<string name="status_downlink_total">Downlink Total</string>
<string name="status_traffic">Traffic</string>
<string name="status_traffic_total">Traffic Total</string>
<string name="profile">Profile</string>
<string name="core_version">Version</string>
@ -125,4 +129,6 @@
<string name="import_profile_message">Are you sure to import profile %s?</string>
<string name="icloud_profile_unsupported">iCloud profile is not support on current platform</string>
<string name="search">Search</string>
<string name="urltest">URLTest</string>
<string name="expand">Expand</string>
</resources>