feat: improve stats ui
This commit is contained in:
parent
3f1ff22488
commit
960af02beb
|
@ -62,7 +62,6 @@ import com.zaneschepke.wireguardautotunnel.ui.theme.WireguardAutoTunnelTheme
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
import com.zaneschepke.wireguardautotunnel.util.extensions.requestTunnelTileServiceStateUpdate
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import xyz.teamgravity.pin_lock_compose.PinManager
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ExpandingRowListItem(
|
||||||
|
leading: @Composable () -> Unit,
|
||||||
|
text: String,
|
||||||
|
onHold: () -> Unit = {},
|
||||||
|
onClick: () -> Unit,
|
||||||
|
trailing: @Composable () -> Unit,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
expanded: @Composable () -> Unit = {},
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.animateContentSize()
|
||||||
|
.clip(RoundedCornerShape(30.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick() },
|
||||||
|
onLongClick = { onHold() },
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 15.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth(13 / 20f),
|
||||||
|
) {
|
||||||
|
leading()
|
||||||
|
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge)
|
||||||
|
}
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
if (isExpanded) expanded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,108 +0,0 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.common
|
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun RowListItem(
|
|
||||||
icon: @Composable () -> Unit,
|
|
||||||
text: String,
|
|
||||||
onHold: () -> Unit,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
rowButton: @Composable () -> Unit,
|
|
||||||
expanded: Boolean,
|
|
||||||
statistics: TunnelStatistics?,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.animateContentSize()
|
|
||||||
.clip(RoundedCornerShape(30.dp))
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = { onClick() },
|
|
||||||
onLongClick = { onHold() },
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 15.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(15.dp),
|
|
||||||
modifier = Modifier.fillMaxWidth(13 / 20f),
|
|
||||||
) {
|
|
||||||
icon()
|
|
||||||
Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge)
|
|
||||||
}
|
|
||||||
rowButton()
|
|
||||||
}
|
|
||||||
if (expanded) {
|
|
||||||
statistics?.getPeers()?.forEach {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(30.dp, Alignment.Start),
|
|
||||||
) {
|
|
||||||
val handshakeEpoch = statistics.peerStats(it)!!.latestHandshakeEpochMillis
|
|
||||||
val peerId = it.toBase64().subSequence(0, 3).toString() + "***"
|
|
||||||
val handshakeSec =
|
|
||||||
NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch)
|
|
||||||
val handshake =
|
|
||||||
if (handshakeSec == null) stringResource(R.string.never) else "$handshakeSec " + stringResource(R.string.sec)
|
|
||||||
val peerTx = statistics.peerStats(it)!!.txBytes
|
|
||||||
val peerRx = statistics.peerStats(it)!!.rxBytes
|
|
||||||
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
|
||||||
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
|
|
||||||
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
|
|
||||||
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +1,26 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.net.VpnService
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.focusable
|
|
||||||
import androidx.compose.foundation.gestures.ScrollableDefaults
|
import androidx.compose.foundation.gestures.ScrollableDefaults
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.overscroll
|
import androidx.compose.foundation.overscroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.rounded.Bolt
|
|
||||||
import androidx.compose.material.icons.rounded.Circle
|
|
||||||
import androidx.compose.material.icons.rounded.CopyAll
|
|
||||||
import androidx.compose.material.icons.rounded.Delete
|
|
||||||
import androidx.compose.material.icons.rounded.Info
|
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
|
||||||
import androidx.compose.material.icons.rounded.Smartphone
|
|
||||||
import androidx.compose.material.icons.rounded.Star
|
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
@ -45,41 +32,30 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.journeyapps.barcodescanner.ScanContract
|
import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import com.wireguard.android.backend.GoBackend
|
|
||||||
import com.zaneschepke.wireguardautotunnel.R
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.TunnelState
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
import com.zaneschepke.wireguardautotunnel.ui.AppUiState
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.Route
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
|
import com.zaneschepke.wireguardautotunnel.ui.common.NestedScrollListener
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.RowListItem
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.AutoTunnelRowItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.GettingStartedLabel
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ScrollDismissFab
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelImportSheet
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.TunnelRowItem
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.VpnDeniedDialog
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.Corn
|
|
||||||
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.handshakeStatus
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.mapPeerStats
|
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
|
||||||
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
import com.zaneschepke.wireguardautotunnel.util.extensions.startTunnelBackground
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
@ -88,7 +64,6 @@ import kotlinx.coroutines.delay
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) {
|
fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState, focusRequester: FocusRequester) {
|
||||||
val haptic = LocalHapticFeedback.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
val snackbar = SnackbarController.current
|
val snackbar = SnackbarController.current
|
||||||
|
@ -114,7 +89,7 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (context.isRunningOnTv()) {
|
if (context.isRunningOnTv()) {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
kotlin.runCatching {
|
runCatching {
|
||||||
focusRequester.requestFocus()
|
focusRequester.requestFocus()
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
delay(Constants.FOCUS_REQUEST_DELAY)
|
delay(Constants.FOCUS_REQUEST_DELAY)
|
||||||
|
@ -158,6 +133,8 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
fun onTunnelToggle(checked: Boolean, tunnel: TunnelConfig) {
|
||||||
|
val intent = if (uiState.settings.isKernelEnabled) null else VpnService.prepare(context)
|
||||||
|
if (intent != null) return vpnActivityResultState.launch(intent)
|
||||||
if (!checked) viewModel.onTunnelStop(tunnel).also { return }
|
if (!checked) viewModel.onTunnelStop(tunnel).also { return }
|
||||||
if (uiState.settings.isKernelEnabled) {
|
if (uiState.settings.isKernelEnabled) {
|
||||||
context.startTunnelBackground(tunnel.id)
|
context.startTunnelBackground(tunnel.id)
|
||||||
|
@ -180,13 +157,12 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.pointerInput(Unit) {
|
Modifier.pointerInput(Unit) {
|
||||||
if (uiState.tunnels.isNotEmpty()) {
|
if (uiState.tunnels.isEmpty()) return@pointerInput
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onTap = {
|
onTap = {
|
||||||
selectedTunnel = null
|
selectedTunnel = null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
floatingActionButtonPosition = FabPosition.End,
|
floatingActionButtonPosition = FabPosition.End,
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
@ -231,68 +207,9 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
GettingStartedLabel(onClick = { context.openWebUrl(it) })
|
GettingStartedLabel(onClick = { context.openWebUrl(it) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
|
||||||
if (uiState.settings.isAutoTunnelEnabled) {
|
if (uiState.settings.isAutoTunnelEnabled) {
|
||||||
val itemFocusRequester = remember { FocusRequester() }
|
item {
|
||||||
val autoTunnelingLabel =
|
AutoTunnelRowItem(uiState.settings, { viewModel.onToggleAutoTunnelingPause() }, focusRequester)
|
||||||
buildAnnotatedString {
|
|
||||||
append(stringResource(id = R.string.auto_tunneling))
|
|
||||||
append(": ")
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) {
|
|
||||||
append(
|
|
||||||
stringResource(id = R.string.paused),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
append(
|
|
||||||
stringResource(id = R.string.active),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RowListItem(
|
|
||||||
icon = {
|
|
||||||
val icon = Icons.Rounded.Bolt
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.size(iconSize),
|
|
||||||
tint =
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) {
|
|
||||||
Color.Gray
|
|
||||||
} else {
|
|
||||||
SilverTree
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = autoTunnelingLabel.text,
|
|
||||||
rowButton = {
|
|
||||||
if (uiState.settings.isAutoTunnelPaused) {
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
|
||||||
onClick = { viewModel.resumeAutoTunneling() },
|
|
||||||
) {
|
|
||||||
Text(stringResource(id = R.string.resume))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TextButton(
|
|
||||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
|
||||||
onClick = { viewModel.pauseAutoTunneling() },
|
|
||||||
) {
|
|
||||||
Text(stringResource(id = R.string.pause))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (context.isRunningOnTv()) {
|
|
||||||
itemFocusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onHold = {},
|
|
||||||
expanded = false,
|
|
||||||
statistics = null,
|
|
||||||
focusRequester = focusRequester,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items(
|
items(
|
||||||
|
@ -304,186 +221,18 @@ fun MainScreen(viewModel: MainViewModel = hiltViewModel(), uiState: AppUiState,
|
||||||
it.isActive
|
it.isActive
|
||||||
}
|
}
|
||||||
val expanded = uiState.generalState.isTunnelStatsExpanded
|
val expanded = uiState.generalState.isTunnelStatsExpanded
|
||||||
val leadingIconColor =
|
TunnelRowItem(
|
||||||
(
|
isActive,
|
||||||
if (
|
expanded,
|
||||||
isActive && uiState.vpnState.statistics != null
|
selectedTunnel?.id == tunnel.id,
|
||||||
) {
|
tunnel,
|
||||||
uiState.vpnState.statistics.mapPeerStats()
|
vpnState = uiState.vpnState,
|
||||||
.map { it.value?.handshakeStatus() }
|
{ selectedTunnel = tunnel },
|
||||||
.let { statuses ->
|
{ viewModel.onExpandedChanged(!expanded) },
|
||||||
when {
|
onDelete = { showDeleteTunnelAlertDialog = true },
|
||||||
statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree
|
onCopy = { viewModel.onCopyTunnel(tunnel) },
|
||||||
statuses.any { it == HandshakeStatus.STALE } -> Corn
|
onSwitchClick = { onTunnelToggle(it, tunnel) },
|
||||||
statuses.all { it == HandshakeStatus.NOT_STARTED } ->
|
|
||||||
Color.Gray
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Color.Gray
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Color.Gray
|
|
||||||
}
|
|
||||||
)
|
|
||||||
val itemFocusRequester = remember { FocusRequester() }
|
|
||||||
RowListItem(
|
|
||||||
icon = {
|
|
||||||
val circleIcon = Icons.Rounded.Circle
|
|
||||||
val icon =
|
|
||||||
if (tunnel.isPrimaryTunnel) {
|
|
||||||
Icons.Rounded.Star
|
|
||||||
} else if (tunnel.isMobileDataTunnel) {
|
|
||||||
Icons.Rounded.Smartphone
|
|
||||||
} else {
|
|
||||||
circleIcon
|
|
||||||
}
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
tint = leadingIconColor,
|
|
||||||
modifier = Modifier.size(iconSize),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = tunnel.name,
|
|
||||||
onHold = {
|
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (!context.isRunningOnTv()) {
|
|
||||||
if (
|
|
||||||
isActive
|
|
||||||
) {
|
|
||||||
viewModel.onExpandedChanged(!expanded)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
itemFocusRequester.requestFocus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
statistics = uiState.vpnState.statistics,
|
|
||||||
expanded = expanded && isActive,
|
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
rowButton = {
|
|
||||||
if (
|
|
||||||
tunnel.id == selectedTunnel?.id &&
|
|
||||||
!context.isRunningOnTv()
|
|
||||||
) {
|
|
||||||
Row {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
selectedTunnel?.let {
|
|
||||||
navController.navigate(
|
|
||||||
Route.Option(it.id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Settings
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusable(),
|
|
||||||
onClick = { viewModel.onCopyTunnel(selectedTunnel) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.CopyAll
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
enabled = !isActive,
|
|
||||||
modifier = Modifier.focusable(),
|
|
||||||
onClick = { showDeleteTunnelAlertDialog = true },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Delete
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
@Composable
|
|
||||||
fun TunnelSwitch() = Switch(
|
|
||||||
modifier = Modifier.focusRequester(itemFocusRequester),
|
|
||||||
checked = isActive,
|
|
||||||
onCheckedChange = { checked ->
|
|
||||||
val intent = if (uiState.settings.isKernelEnabled) null else GoBackend.VpnService.prepare(context)
|
|
||||||
if (intent != null) return@Switch vpnActivityResultState.launch(intent)
|
|
||||||
onTunnelToggle(checked, tunnel)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if (context.isRunningOnTv()) {
|
|
||||||
Row {
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
selectedTunnel?.let {
|
|
||||||
navController.navigate(
|
|
||||||
Route.Option(it.id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Settings
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
modifier = Modifier.focusRequester(focusRequester),
|
|
||||||
onClick = {
|
|
||||||
if (
|
|
||||||
uiState.vpnState.status == TunnelState.UP &&
|
|
||||||
(uiState.vpnState.tunnelConfig?.name == tunnel.name)
|
|
||||||
) {
|
|
||||||
viewModel.onExpandedChanged(!expanded)
|
|
||||||
} else {
|
|
||||||
snackbar.showMessage(
|
|
||||||
context.getString(R.string.turn_on_tunnel),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Info
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = { viewModel.onCopyTunnel(tunnel) },
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.CopyAll
|
|
||||||
Icon(icon, icon.name)
|
|
||||||
}
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
if (
|
|
||||||
uiState.vpnState.status == TunnelState.UP &&
|
|
||||||
tunnel.name == uiState.vpnState.tunnelConfig?.name
|
|
||||||
) {
|
|
||||||
snackbar.showMessage(
|
|
||||||
context.getString(R.string.turn_off_tunnel),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
selectedTunnel = tunnel
|
|
||||||
showDeleteTunnelAlertDialog = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
val icon = Icons.Rounded.Delete
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
icon.name,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
TunnelSwitch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,17 +194,10 @@ constructor(
|
||||||
saveTunnelConfigFromStream(stream, name)
|
saveTunnelConfigFromStream(stream, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pauseAutoTunneling() = viewModelScope.launch {
|
fun onToggleAutoTunnelingPause() = viewModelScope.launch {
|
||||||
val settings = appDataRepository.settings.getSettings()
|
val settings = appDataRepository.settings.getSettings()
|
||||||
appDataRepository.settings.save(
|
appDataRepository.settings.save(
|
||||||
settings.copy(isAutoTunnelPaused = true),
|
settings.copy(isAutoTunnelPaused = !settings.isAutoTunnelPaused),
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resumeAutoTunneling() = viewModelScope.launch {
|
|
||||||
val settings = appDataRepository.settings.getSettings()
|
|
||||||
appDataRepository.settings.save(
|
|
||||||
settings.copy(isAutoTunnelPaused = false),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Bolt
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.Settings
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoTunnelRowItem(settings: Settings, onToggle: () -> Unit, focusRequester: FocusRequester) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val itemFocusRequester = remember { FocusRequester() }
|
||||||
|
val autoTunnelingLabel =
|
||||||
|
buildAnnotatedString {
|
||||||
|
append(stringResource(id = R.string.auto_tunneling))
|
||||||
|
append(": ")
|
||||||
|
if (settings.isAutoTunnelPaused) {
|
||||||
|
append(
|
||||||
|
stringResource(id = R.string.paused),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
append(
|
||||||
|
stringResource(id = R.string.active),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ExpandingRowListItem(
|
||||||
|
leading = {
|
||||||
|
val icon = Icons.Rounded.Bolt
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(iconSize).scale(1.5f),
|
||||||
|
tint =
|
||||||
|
if (settings.isAutoTunnelPaused) {
|
||||||
|
Color.Gray
|
||||||
|
} else {
|
||||||
|
SilverTree
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = autoTunnelingLabel.text,
|
||||||
|
trailing = {
|
||||||
|
TextButton(
|
||||||
|
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||||
|
onClick = { onToggle() },
|
||||||
|
) {
|
||||||
|
Text(stringResource(id = if (settings.isAutoTunnelPaused) R.string.resume else R.string.pause))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (context.isRunningOnTv()) {
|
||||||
|
itemFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded = false,
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Circle
|
||||||
|
import androidx.compose.material.icons.rounded.CopyAll
|
||||||
|
import androidx.compose.material.icons.rounded.Delete
|
||||||
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material.icons.rounded.Smartphone
|
||||||
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.VpnState
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.Route
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.navigation.LocalNavController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.common.snackbar.SnackbarController
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.isRunningOnTv
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelRowItem(
|
||||||
|
isActive: Boolean,
|
||||||
|
expanded: Boolean,
|
||||||
|
isSelected: Boolean,
|
||||||
|
tunnel: TunnelConfig,
|
||||||
|
vpnState: VpnState,
|
||||||
|
onHold: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onCopy: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
onSwitchClick: (checked: Boolean) -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
) {
|
||||||
|
val leadingIconColor = if (!isActive) Color.Gray else vpnState.statistics.asColor()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackbar = SnackbarController.current
|
||||||
|
val navController = LocalNavController.current
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val itemFocusRequester = remember { FocusRequester() }
|
||||||
|
ExpandingRowListItem(
|
||||||
|
leading = {
|
||||||
|
val circleIcon = Icons.Rounded.Circle
|
||||||
|
val icon =
|
||||||
|
if (tunnel.isPrimaryTunnel) {
|
||||||
|
Icons.Rounded.Star
|
||||||
|
} else if (tunnel.isMobileDataTunnel) {
|
||||||
|
Icons.Rounded.Smartphone
|
||||||
|
} else {
|
||||||
|
circleIcon
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
tint = leadingIconColor,
|
||||||
|
modifier = Modifier.size(iconSize),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = tunnel.name,
|
||||||
|
onHold = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onHold()
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (!context.isRunningOnTv()) {
|
||||||
|
if (isActive) {
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onHold()
|
||||||
|
itemFocusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isExpanded = expanded && isActive,
|
||||||
|
expanded = { if (isActive && expanded) TunnelStatisticsBox(vpnState.statistics, tunnel) },
|
||||||
|
focusRequester = focusRequester,
|
||||||
|
trailing = {
|
||||||
|
if (
|
||||||
|
isSelected &&
|
||||||
|
!context.isRunningOnTv()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
navController.navigate(
|
||||||
|
Route.Option(tunnel.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Settings
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
|
onClick = { onCopy() },
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.CopyAll
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
enabled = !isActive,
|
||||||
|
modifier = Modifier.focusable(),
|
||||||
|
onClick = { onDelete() },
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Delete
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (context.isRunningOnTv()) {
|
||||||
|
Row {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onHold()
|
||||||
|
navController.navigate(
|
||||||
|
Route.Option(tunnel.id),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Settings
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
modifier = Modifier.focusRequester(focusRequester),
|
||||||
|
onClick = {
|
||||||
|
if (isActive) {
|
||||||
|
onClick()
|
||||||
|
} else {
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.turn_on_tunnel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Info
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { onCopy() },
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.CopyAll
|
||||||
|
Icon(icon, icon.name)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (isActive) {
|
||||||
|
snackbar.showMessage(
|
||||||
|
context.getString(R.string.turn_off_tunnel),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
onHold()
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
val icon = Icons.Rounded.Delete
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
icon.name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||||
|
checked = isActive,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
onSwitchClick(checked)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Switch(
|
||||||
|
modifier = Modifier.focusRequester(itemFocusRequester),
|
||||||
|
checked = isActive,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
onSwitchClick(checked)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package com.zaneschepke.wireguardautotunnel.ui.screens.main.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.zaneschepke.wireguardautotunnel.R
|
||||||
|
import com.zaneschepke.wireguardautotunnel.data.domain.TunnelConfig
|
||||||
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
|
import com.zaneschepke.wireguardautotunnel.util.extensions.toThreeDecimalPlaceString
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TunnelStatisticsBox(statistics: TunnelStatistics?, tunnelConfig: TunnelConfig) {
|
||||||
|
val config = TunnelConfig.configFromAmQuick(tunnelConfig.wgQuick)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(end = 10.dp, bottom = 10.dp, start = 45.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(30.dp, Alignment.Start),
|
||||||
|
) {
|
||||||
|
config.peers.forEach {
|
||||||
|
val peerId = it.publicKey.toBase64().subSequence(0, 3).toString() + "***"
|
||||||
|
val peerRx = statistics?.peerStats(it.publicKey)?.rxBytes ?: 0
|
||||||
|
val peerTx = statistics?.peerStats(it.publicKey)?.txBytes ?: 0
|
||||||
|
val peerTxMB = NumberUtils.bytesToMB(peerTx).toThreeDecimalPlaceString()
|
||||||
|
val peerRxMB = NumberUtils.bytesToMB(peerRx).toThreeDecimalPlaceString()
|
||||||
|
val handshake = statistics?.peerStats(it.publicKey)?.latestHandshakeEpochMillis?.let {
|
||||||
|
"${NumberUtils.getSecondsBetweenTimestampAndNow(it)} ${stringResource(R.string.sec)}"
|
||||||
|
} ?: stringResource(R.string.never)
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.peer).lowercase() + ": $peerId", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Text("tx: $peerTxMB MB", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.handshake) + ": $handshake", style = MaterialTheme.typography.bodySmall)
|
||||||
|
Text("rx: $peerRxMB MB", style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,11 @@
|
||||||
package com.zaneschepke.wireguardautotunnel.util.extensions
|
package com.zaneschepke.wireguardautotunnel.util.extensions
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import com.wireguard.config.Peer
|
import com.wireguard.config.Peer
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.HandshakeStatus
|
||||||
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
import com.zaneschepke.wireguardautotunnel.service.tunnel.statistics.TunnelStatistics
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.Corn
|
||||||
|
import com.zaneschepke.wireguardautotunnel.ui.theme.SilverTree
|
||||||
import com.zaneschepke.wireguardautotunnel.util.Constants
|
import com.zaneschepke.wireguardautotunnel.util.Constants
|
||||||
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
import com.zaneschepke.wireguardautotunnel.util.NumberUtils
|
||||||
import org.amnezia.awg.config.Config
|
import org.amnezia.awg.config.Config
|
||||||
|
@ -48,6 +51,19 @@ fun Peer.isReachable(): Boolean {
|
||||||
return reachable
|
return reachable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TunnelStatistics?.asColor(): Color {
|
||||||
|
return this?.mapPeerStats()
|
||||||
|
?.map { it.value?.handshakeStatus() }
|
||||||
|
?.let { statuses ->
|
||||||
|
when {
|
||||||
|
statuses.all { it == HandshakeStatus.HEALTHY } -> SilverTree
|
||||||
|
statuses.any { it == HandshakeStatus.STALE } -> Corn
|
||||||
|
statuses.all { it == HandshakeStatus.NOT_STARTED } -> Color.Gray
|
||||||
|
else -> Color.Gray
|
||||||
|
}
|
||||||
|
} ?: Color.Gray
|
||||||
|
}
|
||||||
|
|
||||||
fun Config.toWgQuickString(): String {
|
fun Config.toWgQuickString(): String {
|
||||||
val amQuick = toAwgQuickString(true)
|
val amQuick = toAwgQuickString(true)
|
||||||
val lines = amQuick.lines().toMutableList()
|
val lines = amQuick.lines().toMutableList()
|
||||||
|
|
Loading…
Reference in New Issue