Skip to content

Commit

Permalink
Add toggle button for split tunneling
Browse files Browse the repository at this point in the history
Co-Authored-By: Boban Sijuk <[email protected]>
  • Loading branch information
2 people authored and Pururun committed Jan 10, 2024
1 parent edbd1f5 commit e56928d
Show file tree
Hide file tree
Showing 8 changed files with 197 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,45 @@ fun ScaffoldWithMediumTopBar(
)
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
fun ScaffoldWithLargeTopBarAndToggleButton(
appBarTitle: String,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
switch: @Composable () -> Unit = {},
lazyListState: LazyListState = rememberLazyListState(),
scrollbarColor: Color = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaScrollbar),
content: @Composable (modifier: Modifier, lazyListState: LazyListState) -> Unit
) {
val appBarState = rememberTopAppBarState()
val canScroll = lazyListState.canScrollForward || lazyListState.canScrollBackward
val scrollBehavior =
TopAppBarDefaults.exitUntilCollapsedScrollBehavior(appBarState, canScroll = { canScroll })

Scaffold(
modifier = modifier.fillMaxSize().nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
MullvadLargeTopBarWithToggleButton(
title = appBarTitle,
navigationIcon = navigationIcon,
switch = switch,
actions = actions,
scrollBehavior = scrollBehavior
)
},
content = {
content(
Modifier.fillMaxSize()
.padding(it)
.drawVerticalScrollbar(state = lazyListState, color = scrollbarColor),
lazyListState
)
}
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ScaffoldWithMediumTopBar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Surface
Expand Down Expand Up @@ -236,6 +237,45 @@ fun MullvadMediumTopBar(
)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MullvadLargeTopBarWithToggleButton(
title: String,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
switch: @Composable () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
LargeTopAppBar(
title = {
Row(
modifier = Modifier.padding(end = Dimens.mediumPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.Top
) {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)

if (scrollBehavior?.state?.collapsedFraction == 0f) {
switch()
}
}
},
navigationIcon = navigationIcon,
scrollBehavior = scrollBehavior,
colors =
TopAppBarDefaults.mediumTopAppBarColors(
containerColor = MaterialTheme.colorScheme.background,
actionIconContentColor = MaterialTheme.colorScheme.onPrimary.copy(AlphaTopBar)
),
actions = actions
)
}

@Preview
@Composable
private fun PreviewMullvadTopBarWithLongDeviceName() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import net.mullvad.mullvadvpn.compose.cell.BaseCell
import net.mullvad.mullvadvpn.compose.cell.HeaderSwitchComposeCell
import net.mullvad.mullvadvpn.compose.cell.SplitTunnelingCell
import net.mullvad.mullvadvpn.compose.component.MullvadCircularProgressIndicatorLarge
import net.mullvad.mullvadvpn.compose.component.MullvadSwitch
import net.mullvad.mullvadvpn.compose.component.NavigateBackIconButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithMediumTopBar
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithLargeTopBarAndToggleButton
import net.mullvad.mullvadvpn.compose.constant.CommonContentKey
import net.mullvad.mullvadvpn.compose.constant.ContentType
import net.mullvad.mullvadvpn.compose.constant.SplitTunnelingContentKey
import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
import net.mullvad.mullvadvpn.compose.state.AppListState
import net.mullvad.mullvadvpn.compose.state.SplitTunnelingUiState
import net.mullvad.mullvadvpn.compose.transitions.SlideInFromRightTransition
import net.mullvad.mullvadvpn.lib.theme.AppTheme
Expand All @@ -51,29 +53,32 @@ private fun PreviewSplitTunnelingScreen() {
AppTheme {
SplitTunnelingScreen(
uiState =
SplitTunnelingUiState.ShowAppList(
excludedApps =
listOf(
AppData(
packageName = "my.package.a",
name = "TitleA",
iconRes = R.drawable.icon_alert,
),
AppData(
packageName = "my.package.b",
name = "TitleB",
iconRes = R.drawable.icon_chevron,
)
),
includedApps =
listOf(
AppData(
packageName = "my.package.c",
name = "TitleC",
iconRes = R.drawable.icon_alert
)
),
showSystemApps = true
SplitTunnelingUiState(
appListState =
AppListState.ShowAppList(
excludedApps =
listOf(
AppData(
packageName = "my.package.a",
name = "TitleA",
iconRes = R.drawable.icon_alert
),
AppData(
packageName = "my.package.b",
name = "TitleB",
iconRes = R.drawable.icon_chevron
)
),
includedApps =
listOf(
AppData(
packageName = "my.package.c",
name = "TitleC",
iconRes = R.drawable.icon_alert
)
),
showSystemApps = true
)
)
)
}
Expand All @@ -88,6 +93,7 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
val packageManager = remember(context) { context.packageManager }
SplitTunnelingScreen(
uiState = state,
onShowSplitTunneling = viewModel::enableSplitTunneling,
onShowSystemAppsClick = viewModel::onShowSystemAppsClick,
onExcludeAppClick = viewModel::onExcludeAppClick,
onIncludeAppClick = viewModel::onIncludeAppClick,
Expand All @@ -101,18 +107,25 @@ fun SplitTunneling(navigator: DestinationsNavigator) {
@Composable
@OptIn(ExperimentalFoundationApi::class)
fun SplitTunnelingScreen(
uiState: SplitTunnelingUiState = SplitTunnelingUiState.Loading,
uiState: SplitTunnelingUiState = SplitTunnelingUiState(),
onShowSplitTunneling: (Boolean) -> Unit = {},
onShowSystemAppsClick: (show: Boolean) -> Unit = {},
onExcludeAppClick: (packageName: String) -> Unit = {},
onIncludeAppClick: (packageName: String) -> Unit = {},
onBackClick: () -> Unit = {},
onResolveIcon: (String) -> Bitmap? = { null },
onResolveIcon: (String) -> Bitmap? = { null }
) {
val focusManager = LocalFocusManager.current

ScaffoldWithMediumTopBar(
ScaffoldWithLargeTopBarAndToggleButton(
modifier = Modifier.fillMaxSize(),
appBarTitle = stringResource(id = R.string.split_tunneling),
switch = {
MullvadSwitch(
checked = uiState.enabled,
onCheckedChange = { newValue -> onShowSplitTunneling(newValue) }
)
},
navigationIcon = { NavigateBackIconButton(onBackClick) }
) { modifier, lazyListState ->
LazyColumn(
Expand All @@ -134,14 +147,14 @@ fun SplitTunnelingScreen(
)
}
}
when (uiState) {
SplitTunnelingUiState.Loading -> {
when (val appList = uiState.appListState) {
AppListState.Loading -> {
item(key = CommonContentKey.PROGRESS, contentType = ContentType.PROGRESS) {
MullvadCircularProgressIndicatorLarge()
}
}
is SplitTunnelingUiState.ShowAppList -> {
if (uiState.excludedApps.isNotEmpty()) {
is AppListState.ShowAppList -> {
if (appList.excludedApps.isNotEmpty()) {
itemWithDivider(
key = SplitTunnelingContentKey.EXCLUDED_APPLICATIONS,
contentType = ContentType.HEADER
Expand All @@ -155,11 +168,11 @@ fun SplitTunnelingScreen(
)
},
bodyView = {},
background = MaterialTheme.colorScheme.primary,
background = MaterialTheme.colorScheme.primary
)
}
itemsIndexed(
items = uiState.excludedApps,
items = appList.excludedApps,
key = { _, listItem -> listItem.packageName },
contentType = { _, _ -> ContentType.ITEM }
) { index, listItem ->
Expand All @@ -172,7 +185,7 @@ fun SplitTunnelingScreen(
) {
// Move focus down unless the clicked item was the last in this
// section.
if (index < uiState.excludedApps.size - 1) {
if (index < appList.excludedApps.size - 1) {
focusManager.moveFocus(FocusDirection.Down)
} else {
focusManager.moveFocus(FocusDirection.Up)
Expand All @@ -195,7 +208,7 @@ fun SplitTunnelingScreen(
) {
HeaderSwitchComposeCell(
title = stringResource(id = R.string.show_system_apps),
isToggled = uiState.showSystemApps,
isToggled = appList.showSystemApps,
onCellClicked = { newValue -> onShowSystemAppsClick(newValue) },
modifier = Modifier.animateItemPlacement()
)
Expand All @@ -214,11 +227,11 @@ fun SplitTunnelingScreen(
)
},
bodyView = {},
background = MaterialTheme.colorScheme.primary,
background = MaterialTheme.colorScheme.primary
)
}
itemsIndexed(
items = uiState.includedApps,
items = appList.includedApps,
key = { _, listItem -> listItem.packageName },
contentType = { _, _ -> ContentType.ITEM }
) { index, listItem ->
Expand All @@ -231,7 +244,7 @@ fun SplitTunnelingScreen(
) {
// Move focus down unless the clicked item was the last in this
// section.
if (index < uiState.includedApps.size - 1) {
if (index < appList.includedApps.size - 1) {
focusManager.moveFocus(FocusDirection.Down)
} else {
focusManager.moveFocus(FocusDirection.Up)
Expand All @@ -241,6 +254,7 @@ fun SplitTunnelingScreen(
}
}
}
AppListState.Disabled -> {}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,19 @@ package net.mullvad.mullvadvpn.compose.state

import net.mullvad.mullvadvpn.applist.AppData

sealed interface SplitTunnelingUiState {
data object Loading : SplitTunnelingUiState
data class SplitTunnelingUiState(
val enabled: Boolean = false,
val appListState: AppListState = AppListState.Disabled
)

sealed interface AppListState {
data object Disabled : AppListState

data object Loading : AppListState

data class ShowAppList(
val excludedApps: List<AppData> = emptyList(),
val includedApps: List<AppData> = emptyList(),
val showSystemApps: Boolean = false
) : SplitTunnelingUiState
) : AppListState
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ class SplitTunneling(private val connection: Messenger, eventDispatcher: EventDi
private var _excludedApps by
observable(emptySet<String>()) { _, _, apps -> excludedAppsChange.invoke(apps) }

var enabled by
observable(false) { _, wasEnabled, isEnabled ->
if (wasEnabled != isEnabled) {
connection.send(Request.SetEnableSplitTunneling(isEnabled).message)
}
var enabled by observable(false) { _, _, isEnabled -> enabledChange.invoke(isEnabled) }

var enabledChange: (enabled: Boolean) -> Unit = {}
set(value) {
field = value
synchronized(this) { value.invoke(enabled) }
}

var excludedAppsChange: (apps: Set<String>) -> Unit = {}
Expand All @@ -41,4 +42,7 @@ class SplitTunneling(private val connection: Messenger, eventDispatcher: EventDi
connection.send(Request.IncludeApp(appPackageName).message)

fun persist() = connection.send(Request.PersistExcludedApps.message)

fun enableSplitTunneling(isEnabled: Boolean) =
connection.send(Request.SetEnableSplitTunneling(isEnabled).message)
}
Loading

0 comments on commit e56928d

Please sign in to comment.