Compare commits

..

13 Commits

Author SHA1 Message Date
dependabot[bot] c487f0beb4 chore(deps): bump accompanist from 0.37.2 to 0.37.3
Bumps `accompanist` from 0.37.2 to 0.37.3.

Updates `com.google.accompanist:accompanist-drawablepainter` from 0.37.2 to 0.37.3
- [Release notes](https://github.com/google/accompanist/releases)
- [Commits](https://github.com/google/accompanist/compare/v0.37.2...v0.37.3)

Updates `com.google.accompanist:accompanist-permissions` from 0.37.2 to 0.37.3
- [Release notes](https://github.com/google/accompanist/releases)
- [Commits](https://github.com/google/accompanist/compare/v0.37.2...v0.37.3)

---
updated-dependencies:
- dependency-name: com.google.accompanist:accompanist-drawablepainter
  dependency-version: 0.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.accompanist:accompanist-permissions
  dependency-version: 0.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-28 14:24:00 +00:00
Zane Schepke 0c90b33813 feat: display Wi-Fi security type for Android 12 and greater
refactor: deprecated clipboard manager
2025-04-25 19:25:06 -04:00
Zane Schepke e6671fd3b4 fix: switching APs or Wi-Fi bands with same SSID bug
#741
closes #154
2025-04-25 16:11:37 -04:00
Zane Schepke 735e38e989 feat: add darker theme options
closes #706
2025-04-25 01:59:57 -04:00
Zane Schepke 90698c2b17 fix: select split tunnel apps should appear at top of list
#662
closes #640
2025-04-25 01:17:14 -04:00
Zane Schepke 245b8ee3e7 ci: sort primary to always be first 2025-04-25 00:19:35 -04:00
dependabot[bot] 343554407a chore(deps): bump androidx.datastore:datastore-preferences from 1.1.4 to 1.1.5 (#748)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:12:22 -04:00
dependabot[bot] b493d83730 chore(deps): bump androidx.compose:compose-bom from 2025.04.00 to 2025.04.01 (#747)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:12:14 -04:00
dependabot[bot] 53cd717340 chore(deps): bump ClementTsang/delete-tag-and-release from 0.3.1 to 0.4.0 (#738)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:52 -04:00
dependabot[bot] 76574e3dd2 chore(deps): bump androidx.work:work-runtime-ktx from 2.10.0 to 2.10.1 (#746)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:16 -04:00
dependabot[bot] 282a752389 chore(deps): bump roomVersion from 2.7.0 to 2.7.1 (#745)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-25 00:11:06 -04:00
Zane Schepke 5aa9145361 fix: single click in select mode
closes #739
2025-04-25 00:04:30 -04:00
Zane Schepke 586726c848 ci: fix multiple artifacts 2025-04-23 06:32:04 -04:00
25 changed files with 280 additions and 225 deletions
+5 -4
View File
@@ -119,9 +119,10 @@ jobs:
- name: Get release apk path
id: apk-path
run: echo "path=$(find . -regex '^.*/build/outputs/apk/${{ inputs.flavor }}/${{ inputs.build_type }}/.*\.apk$' -type f | head -1 | tail -c+2)" >> $GITHUB_OUTPUT
- name: Upload release apk
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
path: ${{ github.workspace }}/${{ steps.apk-path.outputs.path }}
retention-days: 1
name: android_artifacts_${{ inputs.flavor }}
path: app/build/outputs/apk/${{ inputs.flavor }}/release/wgtunnel-${{ inputs.flavor }}-release-*.apk
retention-days: 1
if-no-files-found: warn
+3 -2
View File
@@ -139,8 +139,9 @@ jobs:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: ${{ env.UPLOAD_DIR_ANDROID }}
pattern: android_artifacts_*
path: ${{ github.workspace }}/temp
merge-multiple: true
- name: Set version release notes
if: ${{ inputs.release_type == 'release' }}
run: |
@@ -162,7 +163,7 @@ jobs:
- name: Delete previous release
if: ${{ contains(env.TAG_NAME, 'nightly') || inputs.release_type == 'prerelease' }}
uses: ClementTsang/delete-tag-and-release@v0.3.1
uses: ClementTsang/delete-tag-and-release@v0.4.0
with:
tag_name: ${{ env.TAG_NAME }}
delete_release: true
@@ -2,22 +2,21 @@ package com.zaneschepke.wireguardautotunnel.ui.common
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.indication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.style.TextOverflow
@@ -44,32 +43,25 @@ fun ExpandingRowListItem(
modifier =
Modifier.animateContentSize()
.clip(RoundedCornerShape(8.dp))
.background(
if (isSelected) MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
else Color.Transparent
)
.then(
if (!isTv) {
Modifier.combinedClickable(
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
.indication(
interactionSource = interactionSource,
indication = ripple(),
)
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick,
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onHold()
},
onDoubleClick = onDoubleClick,
)
} else Modifier
)
) {
LaunchedEffect(isSelected) {
if (isSelected) {
interactionSource.emit(PressInteraction.Press(Offset.Zero))
} else {
interactionSource.emit(
PressInteraction.Release(PressInteraction.Press(Offset.Zero))
)
}
}
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp),
@@ -0,0 +1,42 @@
package com.zaneschepke.wireguardautotunnel.ui.common.functions
import android.content.ClipData
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.Clipboard
import androidx.compose.ui.platform.LocalClipboard
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ClipboardHelper(
private val clipboard: Clipboard,
private val coroutineScope: CoroutineScope,
private val dispatcher: CoroutineDispatcher = Dispatchers.Main,
) {
fun copy(text: String, label: String = "") {
coroutineScope.launch(dispatcher) {
val clipData = ClipData.newPlainText(label, text)
clipboard.setClipEntry(ClipEntry(clipData))
}
}
fun paste(onResult: (String?) -> Unit) {
coroutineScope.launch(dispatcher) {
val entry = clipboard.getClipEntry()
val text = entry?.clipData?.getItemAt(0)?.text?.toString()
onResult(text)
}
}
}
@Composable
fun rememberClipboardHelper(
coroutineScope: CoroutineScope = rememberCoroutineScope()
): ClipboardHelper {
val clipboard = LocalClipboard.current
return remember(clipboard, coroutineScope) { ClipboardHelper(clipboard, coroutineScope) }
}
@@ -1,7 +1,7 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.autotunnel.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.AirplanemodeActive
import androidx.compose.material.icons.outlined.PublicOff
import androidx.compose.material.icons.outlined.SettingsEthernet
import androidx.compose.material.icons.outlined.SignalCellular4Bar
import androidx.compose.material3.MaterialTheme
@@ -95,7 +95,7 @@ fun NetworkTunnelingItems(uiState: AppUiState, viewModel: AppViewModel): List<Se
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnEthernet) },
),
SelectionItem(
leadingIcon = Icons.Outlined.AirplanemodeActive,
leadingIcon = Icons.Outlined.PublicOff,
title = {
Text(
stringResource(R.string.stop_on_no_internet),
@@ -22,16 +22,15 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.zaneschepke.networkmonitor.NetworkStatus
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.screens.settings.components.LearnMoreLinkLabel
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@@ -48,7 +47,7 @@ fun WifiTunnelingItems(
isWifiNameReadable: () -> Boolean,
): List<SelectionItem> {
val context = LocalContext.current
val clipboard = LocalClipboardManager.current
val clipboardHelper = rememberClipboardHelper()
val baseItems =
listOf(
@@ -71,29 +70,41 @@ fun WifiTunnelingItems(
)
},
description = {
val wifiName by
val wifiInfo by
remember(uiState.networkStatus) {
derivedStateOf {
(uiState.networkStatus as? NetworkStatus.Connected)
?.takeIf { it.wifiConnected }
?.wifiSsid
.let { Pair(it?.wifiSsid, it?.securityType) }
}
}
Text(
text =
wifiName?.let { stringResource(R.string.wifi_name_template, it) }
?: stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable {
wifiName?.let { clipboard.setText(AnnotatedString(it)) }
},
)
val (wifiName, securityType) = wifiInfo
Column {
Text(
text =
wifiName?.let { stringResource(R.string.wifi_name_template, it) }
?: stringResource(R.string.inactive),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable { wifiName?.let { clipboardHelper.copy(it) } },
)
securityType?.let {
Text(
text = stringResource(R.string.security_template, it.name),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.outline
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
},
onClick = { viewModel.handleEvent(AppEvent.ToggleAutoTunnelOnWifi) },
),
@@ -7,12 +7,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.common.dialog.InfoDialog
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberFileImportLauncherForResult
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.screens.main.components.ExportTunnelsBottomSheet
@@ -29,7 +29,7 @@ import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: AppViewModel) {
val navController = LocalNavController.current
val clipboard = LocalClipboardManager.current
val clipboard = rememberClipboardHelper()
var showUrlImportDialog by remember { mutableStateOf(false) }
@@ -90,8 +90,9 @@ fun MainScreen(appUiState: AppUiState, appViewState: AppViewState, viewModel: Ap
requestPermissionLauncher.launch(android.Manifest.permission.CAMERA)
},
onClipboardClick = {
clipboard.getText()?.text?.let {
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(it))
clipboard.paste { result ->
if (result != null)
viewModel.handleEvent(AppEvent.ImportTunnelFromClipboard(result))
}
},
onManualImportClick = {
@@ -7,6 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -18,6 +19,7 @@ import com.zaneschepke.wireguardautotunnel.core.tunnel.getValueById
import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.Route
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalNavController
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.util.extensions.openWebUrl
@@ -35,12 +37,19 @@ fun TunnelList(
onToggleTunnel: (TunnelConf, Boolean) -> Unit,
viewModel: AppViewModel,
) {
val isTv = LocalIsAndroidTV.current
val context = LocalContext.current
val navController = LocalNavController.current
val collator = Collator.getInstance(Locale.getDefault())
val sortedTunnels =
remember(appUiState.tunnels) {
appUiState.tunnels.sortedWith(compareBy(collator) { it.tunName })
appUiState.tunnels.sortedWith(
compareBy(
// primary tunnel first
{ !it.isPrimaryTunnel },
{ collator.compare(it.tunName, "") },
)
)
}
LazyColumn(
@@ -49,7 +58,7 @@ fun TunnelList(
modifier =
modifier
.pointerInput(Unit) { if (appUiState.tunnels.isEmpty()) return@pointerInput }
.overscroll(ScrollableDefaults.overscrollEffect()),
.overscroll(rememberOverscrollEffect()),
state = rememberLazyListState(0, appUiState.tunnels.count()),
userScrollEnabled = true,
reverseLayout = false,
@@ -71,8 +80,12 @@ fun TunnelList(
tunnel = tunnel,
tunnelState = tunnelState,
onClick = {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
if (selectedTunnels.isNotEmpty() && !isTv) {
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(tunnel))
} else {
navController.navigate(Route.TunnelOptions(tunnel.id))
viewModel.handleEvent(AppEvent.ClearSelectedTunnels)
}
},
onDoubleClick = {
viewModel.handleEvent(AppEvent.ToggleTunnelStatsExpanded(tunnel.id))
@@ -81,6 +94,7 @@ fun TunnelList(
viewModel.handleEvent(AppEvent.ToggleSelectedTunnel(it))
},
onSwitchClick = { checked -> onToggleTunnel(tunnel, checked) },
isTv = isTv,
)
}
}
@@ -26,7 +26,6 @@ import com.zaneschepke.wireguardautotunnel.domain.entity.TunnelConf
import com.zaneschepke.wireguardautotunnel.domain.state.TunnelState
import com.zaneschepke.wireguardautotunnel.ui.common.ExpandingRowListItem
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.navigation.LocalIsAndroidTV
import com.zaneschepke.wireguardautotunnel.util.extensions.asColor
@Composable
@@ -40,9 +39,8 @@ fun TunnelRowItem(
onDoubleClick: () -> Unit,
onToggleSelectedTunnel: (TunnelConf) -> Unit,
onSwitchClick: (Boolean) -> Unit,
isTv: Boolean,
) {
val isTv = LocalIsAndroidTV.current
val leadingIconColor =
remember(state) {
if (state.status.isUp()) tunnelState.statistics.asColor() else Color.Gray
@@ -16,16 +16,15 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.config.ConfigurationTextBox
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.InterfaceProxy
@Composable
@@ -38,7 +37,7 @@ fun InterfaceFields(
onInterfaceChange: (InterfaceProxy) -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
val clipboardManager = LocalClipboardManager.current
val clipboardManager = rememberClipboardHelper()
val keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() })
val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done)
@@ -88,9 +87,7 @@ fun InterfaceFields(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
IconButton(
onClick = { clipboardManager.setText(AnnotatedString(interfaceState.publicKey)) }
) {
IconButton(onClick = { clipboardManager.copy(interfaceState.publicKey) }) {
Icon(Icons.Rounded.ContentCopy, stringResource(R.string.copy_public_key))
}
},
@@ -1,12 +1,15 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.zaneschepke.wireguardautotunnel.R
@@ -34,21 +37,16 @@ fun SplitTunnelScreen(
appViewModel.handleEvent(AppEvent.PopBackStack(true))
}
}
Crossfade(
targetState = uiState.loading,
animationSpec = tween(200),
modifier = Modifier.fillMaxSize(),
) { isLoading ->
if (isLoading) {
SplitTunnelSkeleton()
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
if (uiState.loading) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.size(30.dp), strokeWidth = 5.dp)
}
} else {
SplitTunnelContent(
uiState = uiState,
onSplitOptionChange = viewModel::updateSplitOption,
onAppSelectionToggle = viewModel::toggleAppSelection,
onQueryChange = viewModel::onSearchQuery,
)
}
}
@@ -1,92 +0,0 @@
package com.zaneschepke.wireguardautotunnel.ui.screens.main.splittunnel
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
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.unit.dp
import com.zaneschepke.wireguardautotunnel.ui.common.animation.ShimmerEffect
import com.zaneschepke.wireguardautotunnel.ui.theme.iconSize
@Composable
fun SplitTunnelSkeleton() {
val shimmerBrush = ShimmerEffect()
Column(
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().padding(top = 24.dp),
) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
repeat(3) {
Box(
modifier =
Modifier.weight(1f)
.height(45.dp)
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
}
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp).height(45.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier =
Modifier.height(45.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(shimmerBrush)
)
}
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
contentPadding = PaddingValues(top = 10.dp),
modifier = Modifier.fillMaxWidth(),
) {
items(20) {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(
modifier =
Modifier.size(iconSize).clip(CircleShape).background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier =
Modifier.height(20.dp)
.weight(1f)
.clip(RoundedCornerShape(4.dp))
.background(shimmerBrush)
)
Spacer(modifier = Modifier.width(16.dp))
Box(modifier = Modifier.size(24.dp).clip(CircleShape).background(shimmerBrush))
}
}
}
}
}
@@ -18,7 +18,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import java.text.Collator
import java.util.*
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -50,7 +49,6 @@ constructor(
tunnelId?.let { loadInitialState(it) }
}
// TODO improve this loading experience
private fun loadInitialState(tunnelId: Int) =
viewModelScope.launch {
val tunnel = tunnelRepository.getById(tunnelId) ?: return@launch
@@ -66,7 +64,7 @@ constructor(
val installedPackages = packages.map { it.packageName }.toSet()
// remove uninstalled apps
// Remove uninstalled apps
proxyInterface.includedApplications.retainAll { it in installedPackages }
proxyInterface.excludedApplications.retainAll { it in installedPackages }
@@ -98,12 +96,13 @@ constructor(
selected,
)
}
.sortedWith(compareBy(collator) { it.first.name })
.sortedWith(
compareByDescending<Pair<TunnelApp, Boolean>> { it.second }
.thenBy(collator) { it.first.name }
)
allTunneledApps = tunneledApps
delay(500)
_uiState.update {
SplitTunnelUiState(
loading = false,
@@ -8,20 +8,19 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextOverflow
import com.zaneschepke.wireguardautotunnel.R
import com.zaneschepke.wireguardautotunnel.ui.common.button.ScaledSwitch
import com.zaneschepke.wireguardautotunnel.ui.common.button.surface.SelectionItem
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.viewmodel.event.AppEvent
@Composable
fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionItem {
val clipboardManager = LocalClipboardManager.current
val clipboardManager = rememberClipboardHelper()
return SelectionItem(
leadingIcon = Icons.Filled.SmartToy,
@@ -42,8 +41,7 @@ fun RemoteControlItem(uiState: AppUiState, viewModel: AppViewModel): SelectionIt
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier.clickable { clipboardManager.setText(AnnotatedString(key)) },
modifier = Modifier.clickable { clipboardManager.copy(key) },
)
}
}
@@ -23,25 +23,21 @@ fun DisplayScreen(appUiState: AppUiState, viewModel: AppViewModel) {
verticalArrangement = Arrangement.spacedBy(24.dp, Alignment.Top),
modifier = Modifier.fillMaxSize().padding(top = 24.dp).padding(horizontal = 24.dp),
) {
IconSurfaceButton(
title = stringResource(R.string.automatic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.AUTOMATIC)) },
selected = appUiState.appState.theme == Theme.AUTOMATIC,
)
IconSurfaceButton(
title = stringResource(R.string.light),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.LIGHT)) },
selected = appUiState.appState.theme == Theme.LIGHT,
)
IconSurfaceButton(
title = stringResource(R.string.dark),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DARK)) },
selected = appUiState.appState.theme == Theme.DARK,
)
IconSurfaceButton(
title = stringResource(R.string.dynamic),
onClick = { viewModel.handleEvent(AppEvent.SetTheme(Theme.DYNAMIC)) },
selected = appUiState.appState.theme == Theme.DYNAMIC,
)
enumValues<Theme>().forEach {
val title =
when (it) {
Theme.DARK -> stringResource(R.string.dark)
Theme.LIGHT -> stringResource(R.string.light)
Theme.AUTOMATIC -> stringResource(R.string.automatic)
Theme.DYNAMIC -> stringResource(R.string.dynamic)
Theme.DARKER -> stringResource(R.string.darker)
Theme.AMOLED -> stringResource(R.string.amoled)
}
IconSurfaceButton(
title = title,
onClick = { viewModel.handleEvent(AppEvent.SetTheme(it)) },
selected = appUiState.appState.theme == it,
)
}
}
}
@@ -11,17 +11,16 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.zaneschepke.logcatter.model.LogMessage
import com.zaneschepke.wireguardautotunnel.ui.common.functions.rememberClipboardHelper
import com.zaneschepke.wireguardautotunnel.ui.common.text.LogTypeLabel
@Composable
fun LogItem(log: LogMessage) {
val clipboardManager = LocalClipboardManager.current
val clipboardManager = rememberClipboardHelper()
val fontSize = 10.sp
Row(
@@ -32,7 +31,7 @@ fun LogItem(log: LogMessage) {
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { clipboardManager.setText(AnnotatedString(log.toString())) },
onClick = { clipboardManager.copy(log.toString()) },
),
) {
Text(text = log.tag, modifier = Modifier.fillMaxSize(0.3f), fontSize = fontSize)
@@ -121,7 +121,11 @@ fun SupportScreen(viewModel: SupportViewModel = hiltViewModel(), appViewModel: A
)
UpdateSection(
onUpdateCheck = {
if (BuildConfig.DEBUG || BuildConfig.VERSION_NAME.contains("beta") || BuildConfig.FLAVOR == "google")
if (
BuildConfig.DEBUG ||
BuildConfig.VERSION_NAME.contains("beta") ||
BuildConfig.FLAVOR == "google"
)
return@UpdateSection context.showToast(R.string.update_check_unsupported)
context.showToast(R.string.checking_for_update)
viewModel.handleUpdateCheck()
@@ -10,6 +10,9 @@ val Plantation = Color(0xFF264A49)
val Shark = Color(0xFF21272A)
val BalticSea = Color(0xFF1C1B1F)
// amoled
val ElectricTeal = Color(0xFF4DD0E1)
// Status colors
val SilverTree = Color(0xFF6DB58B)
val Brick = Color(0xFFCE4257)
@@ -44,6 +44,8 @@ enum class Theme {
AUTOMATIC,
LIGHT,
DARK,
DARKER,
AMOLED,
DYNAMIC,
}
@@ -59,6 +61,18 @@ fun WireguardAutoTunnelTheme(theme: Theme = Theme.AUTOMATIC, content: @Composabl
isDark = true
DarkColorScheme
}
Theme.DARKER -> {
isDark = true
DarkColorScheme.copy(surface = BalticSea, background = BalticSea)
}
Theme.AMOLED -> {
isDark = true
DarkColorScheme.copy(
surface = Color.Black,
background = Color.Black,
primary = ElectricTeal,
)
}
Theme.LIGHT -> {
isDark = false
LightColorScheme
+3
View File
@@ -221,6 +221,7 @@
<string name="wifi_name_template">Active: %1$s</string>
<string name="remote_key_template">Key: %1$s</string>
<string name="version_template">Version: %1$s</string>
<string name="security_template">Security: %1$s</string>
<string name="flavor_template">Flavor: %1$s</string>
<string name="config_error">config error</string>
<string name="dns_resolve_error">dns resolution error</string>
@@ -255,4 +256,6 @@
<string name="allow">Allow</string>
<string name="licenses">Licenses</string>
<string name="update_check_unsupported">Update check not supported this build type.</string>
<string name="darker">Darker</string>
<string name="amoled">AMOLED</string>
</resources>
+4 -4
View File
@@ -1,5 +1,5 @@
[versions]
accompanist = "0.37.2"
accompanist = "0.37.3"
activityCompose = "1.10.1"
amneziawgAndroid = "1.3.8"
androidx-junit = "1.2.1"
@@ -19,7 +19,7 @@ material3 = "1.3.2"
navigationCompose = "2.8.9"
pinLockCompose = "1.0.4"
qrcodeKotlin = "4.4.1"
roomVersion = "2.7.0"
roomVersion = "2.7.1"
semver4j = "3.1.0"
slf4jAndroid = "1.7.36"
timber = "5.0.1"
@@ -27,9 +27,9 @@ tunnel = "1.2.14"
androidGradlePlugin = "8.9.2"
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
composeBom = "2025.04.00"
composeBom = "2025.04.01"
compose = "1.7.8"
workRuntimeKtxVersion = "2.10.0"
workRuntimeKtxVersion = "2.10.1"
zxingAndroidEmbedded = "4.3.0"
coreSplashscreen = "1.0.1"
gradlePlugins-grgit = "5.3.0"
@@ -12,6 +12,7 @@ import android.net.NetworkRequest
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
import java.util.Collections
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.awaitClose
@@ -45,10 +46,18 @@ class AndroidNetworkMonitor(
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
@get:Synchronized @set:Synchronized var currentSsid: String? = null
@get:Synchronized @set:Synchronized var securityType: WifiSecurityType? = null
@get:Synchronized @set:Synchronized var wifiConnected = false
data class WifiState(val connected: Boolean = false, val ssid: String? = null)
// Track active Wi-Fi networks and last active network ID
private val activeNetworks = Collections.synchronizedSet(mutableSetOf<Network>())
data class WifiState(
val connected: Boolean = false,
val ssid: String? = null,
val securityType: WifiSecurityType? = null,
)
data class TransportState(val connected: Boolean = false)
@@ -72,15 +81,15 @@ class AndroidNetworkMonitor(
suspend fun handleUnknownWifi() {
val newSsid = getWifiSsid()
val securityType = wifiManager?.getCurrentSecurityType()
// Only update if new SSID is valid; preserve existing valid SSID otherwise
if (newSsid != null && newSsid != WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
trySend(WifiState(wifiConnected, currentSsid, securityType))
} else if (currentSsid == null || currentSsid == WifiManager.UNKNOWN_SSID) {
currentSsid = newSsid
trySend(WifiState(connected = wifiConnected, ssid = currentSsid))
trySend(WifiState(wifiConnected, currentSsid, securityType))
}
Timber.d("handleUnknownWifi: currentSsid=$currentSsid")
}
val locationPermissionReceiver =
@@ -139,18 +148,34 @@ class AndroidNetworkMonitor(
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Timber.d("Wi-Fi onAvailable: network=$network")
activeNetworks.add(network)
launch {
currentSsid = getWifiSsid()
securityType = wifiManager?.getCurrentSecurityType()
wifiConnected = true
trySend(WifiState(connected = true, ssid = currentSsid))
trySend(
WifiState(
connected = true,
ssid = currentSsid,
securityType = securityType,
)
)
}
}
override fun onLost(network: Network) {
Timber.d("Wi-Fi onLost: network=$network")
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null))
activeNetworks.remove(network)
if (activeNetworks.isEmpty()) {
Timber.d(
"All Wi-Fi networks disconnected, clearing currentSsid and wifiConnected"
)
currentSsid = null
wifiConnected = false
trySend(WifiState(connected = false, ssid = null, securityType = null))
} else {
Timber.d("Wi-Fi onLost, but still connected to other networks, ignoring")
}
}
}
@@ -228,6 +253,7 @@ class AndroidNetworkMonitor(
if (hasAnyConnection) {
NetworkStatus.Connected(
wifiSsid = wifi.ssid,
securityType = wifi.securityType,
wifiConnected = wifi.connected,
cellularConnected = cellular.connected,
ethernetConnected = ethernet.connected,
@@ -1,5 +1,7 @@
package com.zaneschepke.networkmonitor
import android.net.wifi.WifiManager
import android.os.Build
import com.wireguard.android.util.RootShell
fun RootShell.getCurrentWifiName(): String? {
@@ -10,3 +12,12 @@ fun RootShell.getCurrentWifiName(): String? {
)
return response.firstOrNull()
}
@Suppress("DEPRECATION")
fun WifiManager.getCurrentSecurityType(): WifiSecurityType? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
WifiSecurityType.from(connectionInfo.currentSecurityType)
} else {
null
}
}
@@ -9,6 +9,7 @@ sealed class NetworkStatus {
data class Connected(
val wifiSsid: String? = null,
val securityType: WifiSecurityType? = null,
override val wifiConnected: Boolean = false,
override val ethernetConnected: Boolean = false,
override val cellularConnected: Boolean = false,
@@ -0,0 +1,38 @@
package com.zaneschepke.networkmonitor
import android.net.wifi.WifiInfo
enum class WifiSecurityType {
UNKNOWN,
OPEN,
WEP,
WPA2, // WPA and WPA2
WPA3, // WPA3-Personal (SAE)
OWE,
WAPI, // All WAPI_PSK and WAPI_CERT
EAP, // All EAP (covers both WPA3 and others)
PASSPOINT, // All Passpoint versions
DPP;
companion object {
fun from(securityType: Int): WifiSecurityType {
return when (securityType) {
WifiInfo.SECURITY_TYPE_OPEN -> OPEN
WifiInfo.SECURITY_TYPE_WEP -> WEP
WifiInfo.SECURITY_TYPE_PSK -> WPA2
WifiInfo.SECURITY_TYPE_EAP -> EAP
WifiInfo.SECURITY_TYPE_SAE -> WPA3
WifiInfo.SECURITY_TYPE_OWE -> OWE
WifiInfo.SECURITY_TYPE_WAPI_PSK,
WifiInfo.SECURITY_TYPE_WAPI_CERT -> WAPI
WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE -> EAP
WifiInfo.SECURITY_TYPE_EAP_WPA3_ENTERPRISE_192_BIT -> EAP
WifiInfo.SECURITY_TYPE_PASSPOINT_R1_R2,
WifiInfo.SECURITY_TYPE_PASSPOINT_R3 -> PASSPOINT
WifiInfo.SECURITY_TYPE_DPP -> DPP
WifiInfo.SECURITY_TYPE_UNKNOWN -> UNKNOWN
else -> UNKNOWN
}
}
}
}