feat: add tray tunnel/lockdown status indicator

This commit is contained in:
zaneschepke
2026-03-13 02:12:40 -04:00
parent 810a1f1e7d
commit ad0e2c1a03
17 changed files with 214 additions and 528 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" width="100%" viewBox="0 0 500 500" enable-background="new 0 0 500 500" xml:space="preserve" style="background: transparent;">
<path fill="#FDFDFD" opacity="1.000000" stroke="none" d=" M343.600830,328.661926 C336.364685,343.733398 329.901733,358.782043 322.205139,373.170807 C315.794189,385.156036 300.452118,392.714020 286.033417,386.615112 C278.383942,383.379486 272.853790,377.422882 269.419830,369.957458 C258.360992,345.915649 247.455307,321.801636 236.702835,297.621094 C227.138412,276.112305 217.782761,254.509644 208.474655,232.888275 C201.677689,217.099991 195.120636,201.208496 188.431564,185.373611 C184.111832,175.147583 179.699860,164.960342 175.412231,154.720978 C167.168060,135.032944 183.615067,116.238884 199.803223,116.928459 C211.526840,117.427849 219.589691,122.657906 224.396683,132.725189 C232.842834,150.413971 240.886108,168.300461 248.816269,186.228821 C256.720245,204.097977 264.289948,222.115158 271.984283,240.076828 C279.991425,258.768646 287.971436,277.472137 295.974182,296.165863 C296.095032,296.448151 296.356415,296.670319 296.872162,297.329437 C315.180756,254.966919 333.386810,212.841614 351.915741,169.969193 C336.988495,169.969193 323.198395,170.198090 309.420380,169.888458 C300.284088,169.683136 291.076721,171.237061 282.013672,168.092346 C270.647339,164.148422 262.275635,152.328369 263.931549,140.821854 C265.858398,127.432617 277.109894,118.064262 290.438477,116.993347 C322.543274,114.413818 354.683655,115.404877 386.813324,115.130096 C394.489471,115.064453 401.705994,116.824165 408.265320,120.840240 C420.633514,128.412872 422.526917,142.616318 418.216461,153.712448 C411.739807,170.384842 404.531006,186.774277 397.558136,203.252121 C386.530487,229.312042 375.498291,255.370331 364.355072,281.380920 C357.626984,297.085663 350.685486,312.698975 343.600830,328.661926 z"/>
<path fill="#FCFDFD" opacity="1.000000" stroke="none" d=" M217.117798,326.634277 C221.556976,336.960388 227.093643,346.597900 229.804153,356.973724 C234.438812,374.715240 218.140427,393.432953 198.211578,388.023956 C189.952576,385.782349 183.895462,380.786255 180.372223,373.821899 C172.057144,357.385559 164.707733,340.457397 157.088318,323.673462 C151.241760,310.794708 145.483231,297.874054 139.856674,284.897827 C130.721710,263.830353 121.621635,242.746506 112.701210,221.587616 C102.994286,198.563171 93.242340,175.551285 84.013725,152.333710 C76.931572,134.516266 92.602669,116.441895 110.310730,117.172417 C120.190109,117.579979 128.268326,122.562485 132.329987,131.279694 C145.059433,158.599945 157.107895,186.239868 169.245682,213.832703 C180.019928,238.325790 190.660950,262.878815 201.164383,287.489166 C206.642761,300.325470 211.697678,313.342468 217.117798,326.634277 z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -1,372 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="500dp"
android:height="500dp"
android:viewportWidth="500"
android:viewportHeight="500">
<path
android:pathData="M316,114L393,114L406,118L408,119L408,121L412,122L417,129L419,135L419,149L413,165L405,185L387,226L375,254L367,274L356,299L343,329L336,344L327,364L319,377L312,383L304,387L299,388L291,388L282,385L272,376L266,366L258,350L250,331L234,295L222,268L207,232L194,202L178,165L172,149L172,136L177,126L185,119L192,116L206,116L216,121L223,129L231,145L243,171L256,203L270,234L283,264L291,284L295,294L298,290L306,270L317,246L328,220L333,209L341,189L349,171L349,170L299,170L286,169L276,166L266,157L262,147L262,138L266,128L275,120L283,116L293,115Z"
android:fillColor="#FCFCFC"/>
<path
android:pathData="M103,116L113,116L121,119L131,128L140,145L158,186L166,205L180,237L190,260L200,283L219,331L227,347L230,356L230,366L226,376L219,384L210,388L198,388L189,384L181,377L173,363L163,341L150,312L139,287L128,261L122,246L108,214L89,170L82,151L81,140L84,130L89,123L97,118Z"
android:fillColor="#FCFCFC"/>
<path
android:pathData="M130,258L132,261L137,272L136,277L138,277L138,279L140,279L143,286L145,295L143,296L129,264L128,259Z"
android:fillColor="#C8CACB"/>
<path
android:pathData="M94,179L97,179L96,183L99,184L98,188L100,188L100,190L102,190L107,203L106,207L103,203L94,182Z"
android:fillColor="#C1C3CA"/>
<path
android:pathData="M339,194L341,195L339,204L332,219L330,223L327,224L329,219Z"
android:fillColor="#DFE1E4"/>
<path
android:pathData="M400,187L403,187L402,192L394,210L393,210L393,204L398,190Z"
android:fillColor="#B9BABE"/>
<path
android:pathData="M175,129L177,133L175,149L172,149L172,136Z"
android:fillColor="#CBCBCE"/>
<path
android:pathData="M157,325L160,326L163,332L165,336L167,341L166,345L163,341L157,327Z"
android:fillColor="#E7E7E9"/>
<path
android:pathData="M135,276L138,277L138,279L140,279L143,286L145,295L143,296L135,278Z"
android:fillColor="#C0C3C4"/>
<path
android:pathData="M161,200L164,200L172,219L171,221L168,218L164,210Z"
android:fillColor="#DADBDD"/>
<path
android:pathData="M330,349L333,349L330,358L325,368L324,364L329,351Z"
android:fillColor="#A9AAAC"/>
<path
android:pathData="M265,130L267,131L266,135L265,139L263,139L264,145L266,146L266,152L264,153L262,147L262,138Z"
android:fillColor="#8D9091"/>
<path
android:pathData="M339,330L341,330L340,335L335,346L333,343L338,331Z"
android:fillColor="#E3E5E7"/>
<path
android:pathData="M120,240L122,240L123,243L125,244L129,254L130,259L127,258L120,241ZM127,254Z"
android:fillColor="#E5E6E8"/>
<path
android:pathData="M169,353L173,354L173,356L175,357L177,363L175,367L169,355Z"
android:fillColor="#C1C2C4"/>
<path
android:pathData="M190,190L195,194L197,200L197,206L194,202L190,193Z"
android:fillColor="#C2C4C5"/>
<path
android:pathData="M260,353L263,356L266,362L268,362L270,367L268,370L260,355Z"
android:fillColor="#929698"/>
<path
android:pathData="M400,187L403,187L402,192L401,194L399,194L399,196L397,196L396,198L397,192Z"
android:fillColor="#CDCECF"/>
<path
android:pathData="M349,167L351,167L350,170L321,169L321,168Z"
android:fillColor="#DFE2E3"/>
<path
android:pathData="M257,208L260,212L264,221L263,223L259,219L256,210Z"
android:fillColor="#CED0D2"/>
<path
android:pathData="M339,194L341,195L339,204L335,208L334,206Z"
android:fillColor="#B1B3B4"/>
<path
android:pathData="M89,168L94,171L96,177L94,177L93,179L89,170Z"
android:fillColor="#B5B6B8"/>
<path
android:pathData="M270,237L273,241L275,248L271,247L269,241Z"
android:fillColor="#BFC1C1"/>
<path
android:pathData="M326,224L327,228L326,235L321,239L324,229Z"
android:fillColor="#C9CBCC"/>
<path
android:pathData="M94,179L97,179L96,183L99,184L98,188L100,188L101,192L99,193L94,182Z"
android:fillColor="#A7A8A8"/>
<path
android:pathData="M272,122L274,123L271,129L267,132L266,128Z"
android:fillColor="#CDCECF"/>
<path
android:pathData="M224,270L227,274L230,276L231,286L228,282L224,273Z"
android:fillColor="#949D9F"/>
<path
android:pathData="M229,356L230,356L230,366L227,374L225,371L227,365L229,365Z"
android:fillColor="#B7B9BA"/>
<path
android:pathData="M197,206L200,208L200,210L202,210L203,217L201,218L197,209Z"
android:fillColor="#CCCDCF"/>
<path
android:pathData="M135,137L138,141L140,145L140,148L136,146L135,144Z"
android:fillColor="#C4C4C5"/>
<path
android:pathData="M130,258L132,261L134,266L132,266L131,269L128,259Z"
android:fillColor="#B7B8B9"/>
<path
android:pathData="M371,254L374,254L373,260L371,265L369,261Z"
android:fillColor="#C1C2C2"/>
<path
android:pathData="M264,223L267,227L270,234L270,237L267,236L268,233L266,233Z"
android:fillColor="#B2B3B5"/>
<path
android:pathData="M135,276L138,277L138,279L140,279L141,285L138,285Z"
android:fillColor="#B1B2B3"/>
<path
android:pathData="M241,169L243,171L245,178L242,178L240,175ZM241,178Z"
android:fillColor="#B4B6B7"/>
<path
android:pathData="M144,296L147,297L146,299L149,300L148,302L150,303L148,307L144,298Z"
android:fillColor="#C4C6CB"/>
<path
android:pathData="M222,266L226,267L228,271L227,275L225,270L223,270Z"
android:fillColor="#D3D5D5"/>
<path
android:pathData="M364,276L365,279L362,285L360,284L361,278Z"
android:fillColor="#BABCBD"/>
<path
android:pathData="M172,221L175,225L177,232L174,231L172,227Z"
android:fillColor="#BBBBBB"/>
<path
android:pathData="M207,308L210,308L213,318L210,316Z"
android:fillColor="#D6DCDD"/>
<path
android:pathData="M302,280L303,280L303,288L299,289Z"
android:fillColor="#BABBBC"/>
<path
android:pathData="M190,262L192,264L195,273L193,272L193,270L190,269L189,266Z"
android:fillColor="#A3A4A8"/>
<path
android:pathData="M377,244L378,247L375,254L372,253L376,245Z"
android:fillColor="#EFF5F8"/>
<path
android:pathData="M167,211L170,214L172,221L169,220L167,216Z"
android:fillColor="#C5C7C7"/>
<path
android:pathData="M231,147L234,151L236,158L234,159L234,154L231,153Z"
android:fillColor="#A7ABAD"/>
<path
android:pathData="M369,265L370,267L366,276L364,273L366,269L368,269L367,266Z"
android:fillColor="#9E9EA2"/>
<path
android:pathData="M330,349L333,349L331,356L328,355Z"
android:fillColor="#CACBCE"/>
<path
android:pathData="M256,343L260,344L261,348L259,348L258,350L256,346Z"
android:fillColor="#C0C0C2"/>
<path
android:pathData="M202,218L204,218L207,224L206,227L204,225Z"
android:fillColor="#CFD2D3"/>
<path
android:pathData="M402,179L406,180L405,185L403,187L403,185L401,184L403,183Z"
android:fillColor="#B2B2B2"/>
<path
android:pathData="M204,300L207,300L209,307L206,307L206,303L204,303Z"
android:fillColor="#B0B1B5"/>
<path
android:pathData="M195,274L198,278L199,283L196,282Z"
android:fillColor="#999A9E"/>
<path
android:pathData="M206,227L209,228L211,233L208,234Z"
android:fillColor="#D3D4E2"/>
<path
android:pathData="M392,205L393,205L393,213L391,215L390,209Z"
android:fillColor="#DBDCDC"/>
<path
android:pathData="M259,350L263,351L264,356L262,356L259,352Z"
android:fillColor="#DEDEE0"/>
<path
android:pathData="M285,166L289,167L295,167L291,169L284,168Z"
android:fillColor="#F3F4F5"/>
<path
android:pathData="M173,150L176,151L178,156L176,156L175,158L173,153Z"
android:fillColor="#A8A8A8"/>
<path
android:pathData="M264,145L266,146L266,152L264,153L263,146Z"
android:fillColor="#BBBDBE"/>
<path
android:pathData="M175,129L177,133L175,137L173,136L174,131Z"
android:fillColor="#B4B8BB"/>
<path
android:pathData="M34,498Z"
android:fillColor="#000000"/>
<path
android:pathData="M323,493Z"
android:fillColor="#000000"/>
<path
android:pathData="M495,485Z"
android:fillColor="#000000"/>
<path
android:pathData="M43,480Z"
android:fillColor="#000000"/>
<path
android:pathData="M388,476Z"
android:fillColor="#000000"/>
<path
android:pathData="M103,471Z"
android:fillColor="#000000"/>
<path
android:pathData="M36,456Z"
android:fillColor="#000000"/>
<path
android:pathData="M276,453Z"
android:fillColor="#000000"/>
<path
android:pathData="M27,445Z"
android:fillColor="#000000"/>
<path
android:pathData="M206,442Z"
android:fillColor="#000000"/>
<path
android:pathData="M221,441Z"
android:fillColor="#000000"/>
<path
android:pathData="M386,431Z"
android:fillColor="#000000"/>
<path
android:pathData="M37,422Z"
android:fillColor="#000000"/>
<path
android:pathData="M175,420Z"
android:fillColor="#000000"/>
<path
android:pathData="M199,417Z"
android:fillColor="#000000"/>
<path
android:pathData="M72,412Z"
android:fillColor="#000000"/>
<path
android:pathData="M307,392Z"
android:fillColor="#000000"/>
<path
android:pathData="M409,387Z"
android:fillColor="#000000"/>
<path
android:pathData="M170,386Z"
android:fillColor="#000000"/>
<path
android:pathData="M90,382Z"
android:fillColor="#000000"/>
<path
android:pathData="M164,374Z"
android:fillColor="#000000"/>
<path
android:pathData="M131,361Z"
android:fillColor="#000000"/>
<path
android:pathData="M478,351Z"
android:fillColor="#000000"/>
<path
android:pathData="M441,346Z"
android:fillColor="#000000"/>
<path
android:pathData="M448,343Z"
android:fillColor="#000000"/>
<path
android:pathData="M27,339Z"
android:fillColor="#000000"/>
<path
android:pathData="M412,334Z"
android:fillColor="#000000"/>
<path
android:pathData="M63,324Z"
android:fillColor="#000000"/>
<path
android:pathData="M222,322Z"
android:fillColor="#000000"/>
<path
android:pathData="M129,322Z"
android:fillColor="#000000"/>
<path
android:pathData="M87,309Z"
android:fillColor="#000000"/>
<path
android:pathData="M461,247Z"
android:fillColor="#000000"/>
<path
android:pathData="M466,239Z"
android:fillColor="#000000"/>
<path
android:pathData="M23,229Z"
android:fillColor="#000000"/>
<path
android:pathData="M462,224Z"
android:fillColor="#000000"/>
<path
android:pathData="M15,206Z"
android:fillColor="#000001"/>
<path
android:pathData="M185,203Z"
android:fillColor="#000000"/>
<path
android:pathData="M34,181Z"
android:fillColor="#000000"/>
<path
android:pathData="M6,180Z"
android:fillColor="#000000"/>
<path
android:pathData="M462,140Z"
android:fillColor="#000000"/>
<path
android:pathData="M163,131Z"
android:fillColor="#000000"/>
<path
android:pathData="M483,119Z"
android:fillColor="#000000"/>
<path
android:pathData="M174,118Z"
android:fillColor="#000000"/>
<path
android:pathData="M405,110Z"
android:fillColor="#000000"/>
<path
android:pathData="M267,90Z"
android:fillColor="#000000"/>
<path
android:pathData="M242,83Z"
android:fillColor="#000000"/>
<path
android:pathData="M55,76Z"
android:fillColor="#000000"/>
<path
android:pathData="M309,71Z"
android:fillColor="#000000"/>
<path
android:pathData="M318,66Z"
android:fillColor="#000000"/>
<path
android:pathData="M457,61Z"
android:fillColor="#000000"/>
<path
android:pathData="M373,47Z"
android:fillColor="#000000"/>
<path
android:pathData="M350,44Z"
android:fillColor="#000000"/>
<path
android:pathData="M371,41Z"
android:fillColor="#000000"/>
<path
android:pathData="M329,41Z"
android:fillColor="#000000"/>
<path
android:pathData="M85,37Z"
android:fillColor="#000000"/>
<path
android:pathData="M428,36Z"
android:fillColor="#000000"/>
<path
android:pathData="M238,32Z"
android:fillColor="#000000"/>
<path
android:pathData="M21,30Z"
android:fillColor="#000000"/>
<path
android:pathData="M358,29Z"
android:fillColor="#000000"/>
<path
android:pathData="M146,26Z"
android:fillColor="#000000"/>
<path
android:pathData="M175,22Z"
android:fillColor="#000000"/>
<path
android:pathData="M389,19Z"
android:fillColor="#000000"/>
<path
android:pathData="M392,9Z"
android:fillColor="#000000"/>
</vector>
@@ -45,7 +45,7 @@ import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.support.license.Li
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.TunnelsScreen
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.tunnel.TunnelScreen
import com.zaneschepke.wireguardautotunnel.desktop.ui.state.AppUiState
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.ErrorRed
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.TunnelViewModel
import io.github.sudarshanmhasrup.localina.api.LocalinaApp
@@ -158,7 +158,7 @@ fun App(uiState: AppUiState, viewModel: AppViewModel, toaster: ToasterState) {
railState.targetValue == WideNavigationRailValue.Expanded,
icon = {
CustomTooltip(text = "Lockdown active") {
Icon(Icons.Filled.Lock, "Lockdown active", tint = AlertRed)
Icon(Icons.Filled.Lock, "Lockdown active", tint = ErrorRed)
}
},
enabled = false,
@@ -230,7 +230,7 @@ fun App(uiState: AppUiState, viewModel: AppViewModel, toaster: ToasterState) {
entryProvider =
entryProvider {
currentTab.startRoute
entry<Route.Tunnels> { TunnelsScreen() }
entry<Route.Tunnels> { TunnelsScreen(viewModel) }
entry<Route.Tunnel> {
val viewModel: TunnelViewModel =
koinViewModel(parameters = { parametersOf(it.id) })
@@ -1,22 +1,18 @@
package com.zaneschepke.wireguardautotunnel.desktop
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.Surface
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
@@ -33,132 +29,181 @@ import com.dokar.sonner.rememberToasterState
import com.kdroid.composetray.tray.api.ExperimentalTrayAppApi
import com.kdroid.composetray.tray.api.Tray
import com.kdroid.composetray.utils.SingleInstanceManager
import com.kdroid.composetray.utils.isMenuBarInDarkMode
import com.zaneschepke.wireguardautotunnel.client.di.databaseModule
import com.zaneschepke.wireguardautotunnel.client.di.serviceModule
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.Res
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.app_name
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.wgtunnel
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.appicon
import com.zaneschepke.wireguardautotunnel.core.helper.FilePathsHelper
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState
import com.zaneschepke.wireguardautotunnel.desktop.di.viewModelModule
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.bar.TitleBar
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.asColor
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.asTooltipMessage
import com.zaneschepke.wireguardautotunnel.desktop.ui.state.TrayBadgeState
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.ErrorRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.WGTunnelTheme
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.AppViewModel
import java.nio.file.Paths
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.koin.compose.KoinApplication
import org.koin.compose.viewmodel.koinViewModel
import org.koin.dsl.koinConfiguration
import org.orbitmvi.orbit.compose.collectAsState
@OptIn(ExperimentalTrayAppApi::class)
fun main() = application {
var isWindowVisible by remember { mutableStateOf(true) }
SingleInstanceManager.configuration =
SingleInstanceManager.Configuration(
lockFilesDir = Paths.get(FilePathsHelper.getDatabaseDir().path),
lockIdentifier = "wg_tunnel",
)
val isSingleInstance =
SingleInstanceManager.isSingleInstance(onRestoreRequest = { isWindowVisible = true })
if (!isSingleInstance) {
exitApplication()
return@application
}
KoinApplication(
configuration =
koinConfiguration(
declaration = { modules(databaseModule, serviceModule, viewModelModule) }
fun main() {
// TODO support GPU acceleration later, software for now for reliability
System.setProperty("skiko.renderApi", "SOFTWARE_FASTEST")
application {
var isWindowVisible by remember { mutableStateOf(true) }
SingleInstanceManager.configuration =
SingleInstanceManager.Configuration(
lockFilesDir = Paths.get(FilePathsHelper.getDatabaseDir().path),
lockIdentifier = "wg_tunnel",
)
) {
val appIcon = painterResource(Res.drawable.wgtunnel)
val appName = stringResource(Res.string.app_name)
val isSingleInstance =
SingleInstanceManager.isSingleInstance(onRestoreRequest = { isWindowVisible = true })
val isMenuBarDark = isMenuBarInDarkMode()
val windowState = rememberWindowState(size = DpSize(800.dp, 650.dp))
val toaster = rememberToasterState()
Tray(
iconContent = {
Icon(
imageVector = vectorResource(Res.drawable.wgtunnel),
contentDescription = appName,
tint = if (isMenuBarDark) Color.White else Color.Black,
)
},
tooltip = appName,
) {
if (!isWindowVisible) {
Item(label = "Open") {
Logger.i { "Open menu item clicked" }
isWindowVisible = true
}
}
Item(label = "Exit") { exitApplication() }
if (!isSingleInstance) {
exitApplication()
return@application
}
Window(
visible = isWindowVisible,
onCloseRequest = {
isWindowVisible = false
Logger.i { "OS close requested - hiding to tray" }
},
title = stringResource(Res.string.app_name),
icon = appIcon,
state = windowState,
undecorated = true,
transparent = true,
var trayBadgeState: TrayBadgeState? by remember { mutableStateOf(null) }
KoinApplication(
configuration =
koinConfiguration(
declaration = { modules(databaseModule, serviceModule, viewModelModule) }
)
) {
window.minimumSize = java.awt.Dimension(800, 650)
val appIcon = painterResource(Res.drawable.appicon)
val appName = stringResource(Res.string.app_name)
val viewModel: AppViewModel = koinViewModel()
val uiState by viewModel.collectAsState()
val windowState = rememberWindowState(size = DpSize(800.dp, 650.dp))
val toaster = rememberToasterState()
WGTunnelTheme(uiState.theme) {
Surface(
modifier =
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
color = MaterialTheme.colorScheme.background,
) {
Column {
TitleBar(onClose = { isWindowVisible = false })
App(uiState, viewModel, toaster)
Toaster(
state = toaster,
elevation = 0.dp,
border = { BorderStroke(0.dp, Color.Transparent) },
background = { SolidColor(MaterialTheme.colorScheme.inverseOnSurface) },
iconSlot = {
Icon(
when (it.type) {
ToastType.Normal,
ToastType.Info -> Icons.Default.Info
ToastType.Success -> Icons.Default.Check
ToastType.Warning -> Icons.Default.Warning
ToastType.Error -> Icons.Default.Error
},
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.inverseSurface,
)
},
messageSlot = {
val message = it.message as? String ?: return@Toaster
Text(
message,
color = MaterialTheme.colorScheme.inverseSurface,
fontSize = 16.sp,
modifier = Modifier.padding(start = 12.dp),
)
},
contentColor = { MaterialTheme.colorScheme.inverseSurface },
shape = { RoundedCornerShape(8.dp) },
containerPadding = PaddingValues(48.dp),
Tray(
iconContent = {
Box(modifier = Modifier.fillMaxSize()) {
Image(
painter = appIcon,
contentDescription = appName,
modifier = Modifier.fillMaxSize(),
)
trayBadgeState?.let {
Icon(
imageVector = Icons.Filled.Circle,
contentDescription = it.description,
modifier = Modifier.size(40.dp).align(Alignment.TopEnd),
tint = it.iconColor,
)
}
}
},
tooltip = appName,
primaryAction = { isWindowVisible = true },
) {
if (!isWindowVisible) {
Item(label = "Open") {
Logger.i { "Open menu item clicked" }
isWindowVisible = true
}
}
Item(label = "Exit") { exitApplication() }
}
Window(
visible = isWindowVisible,
onCloseRequest = {
isWindowVisible = false
Logger.i { "OS close requested - hiding to tray" }
},
title = stringResource(Res.string.app_name),
icon = appIcon,
state = windowState,
undecorated = true,
transparent = true,
) {
window.minimumSize = java.awt.Dimension(800, 650)
val viewModel: AppViewModel = koinViewModel()
val uiState by viewModel.collectAsState()
LaunchedEffect(uiState.tunnelStatuses, uiState.lockdownActive) {
if (uiState.tunnelStatuses.isEmpty() && !uiState.lockdownActive) {
trayBadgeState = null
return@LaunchedEffect
}
val state: TunnelState? =
(uiState.tunnelStatuses.firstOrNull {
it.state == TunnelState.HANDSHAKE_FAILURE
}
?: uiState.tunnelStatuses.firstOrNull {
it.state == TunnelState.RESOLVING_DNS ||
it.state == TunnelState.STOPPING ||
it.state == TunnelState.STARTING
}
?: uiState.tunnelStatuses.firstOrNull {
it.state == TunnelState.HEALTHY
})
?.state
val (color, description) =
when {
uiState.lockdownActive && state == null -> ErrorRed to "Lockdown active"
else -> state!!.asColor() to state.asTooltipMessage()
}
trayBadgeState = TrayBadgeState(color, description)
}
WGTunnelTheme(uiState.theme) {
Surface(
modifier =
Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background),
color = MaterialTheme.colorScheme.background,
) {
Column {
TitleBar(onClose = { isWindowVisible = false })
App(uiState, viewModel, toaster)
Toaster(
state = toaster,
elevation = 0.dp,
border = { BorderStroke(0.dp, Color.Transparent) },
background = {
SolidColor(MaterialTheme.colorScheme.inverseOnSurface)
},
iconSlot = {
Icon(
when (it.type) {
ToastType.Normal,
ToastType.Info -> Icons.Default.Info
ToastType.Success -> Icons.Default.Check
ToastType.Warning -> Icons.Default.Warning
ToastType.Error -> Icons.Default.Error
},
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.inverseSurface,
)
},
messageSlot = {
val message = it.message as? String ?: return@Toaster
Text(
message,
color = MaterialTheme.colorScheme.inverseSurface,
fontSize = 16.sp,
modifier = Modifier.padding(start = 12.dp),
)
},
contentColor = { MaterialTheme.colorScheme.inverseSurface },
shape = { RoundedCornerShape(8.dp) },
containerPadding = PaddingValues(48.dp),
)
}
}
}
}
@@ -18,12 +18,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.ErrorRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.HealthyGreen
@Composable
fun PulsingStatusLed(isHealthy: Boolean, modifier: Modifier = Modifier) {
val color = if (isHealthy) SilverTree else AlertRed
val color = if (isHealthy) HealthyGreen else ErrorRed
val infiniteTransition = rememberInfiniteTransition(label = "PulseTransition")
@@ -1,5 +1,6 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.common.bar
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.window.WindowDraggableArea
@@ -7,16 +8,16 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.CropSquare
import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowScope
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.Res
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.select_window_2
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.wgtunnel
import com.zaneschepke.wireguardautotunnel.composeapp.generated.resources.titleicon
import java.awt.Frame
import java.awt.event.WindowStateListener
import org.jetbrains.compose.resources.painterResource
@@ -41,16 +42,16 @@ fun WindowScope.TitleBar(onClose: () -> Unit) {
Row(
modifier =
Modifier.fillMaxWidth()
.height(40.dp)
.height(24.dp)
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(Res.drawable.wgtunnel),
Image(
painterResource(Res.drawable.titleicon),
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(24.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
Spacer(Modifier.weight(1f))
@@ -30,6 +30,7 @@ import com.zaneschepke.wireguardautotunnel.desktop.ui.common.tooltip.CustomToolt
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components.TunnelList
import com.zaneschepke.wireguardautotunnel.desktop.ui.sideeffects.AppSideEffect
import com.zaneschepke.wireguardautotunnel.desktop.util.FileUtils
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.AppViewModel
import com.zaneschepke.wireguardautotunnel.desktop.viewmodel.TunnelsViewModel
import io.github.vinceglb.filekit.PlatformFile
import io.github.vinceglb.filekit.dialogs.FileKitMode
@@ -45,9 +46,10 @@ import org.orbitmvi.orbit.compose.collectSideEffect
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TunnelsScreen(viewModel: TunnelsViewModel = koinViewModel()) {
fun TunnelsScreen(appViewModel: AppViewModel, viewModel: TunnelsViewModel = koinViewModel()) {
val uiState by viewModel.collectAsState()
val appUiState by appViewModel.collectAsState()
var pendingDeleteIntent by remember { mutableStateOf<DeleteIntent?>(null) }
@@ -171,6 +173,7 @@ fun TunnelsScreen(viewModel: TunnelsViewModel = koinViewModel()) {
) {
TunnelList(
uiState = uiState,
tunnelStatuses = appUiState.tunnelStatuses,
startTunnel = viewModel::onStartTunnel,
stopTunnel = viewModel::onStopTunnel,
viewModel::onItemsReordered,
@@ -2,26 +2,26 @@ package com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.component
import androidx.compose.ui.graphics.Color
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.AlertRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.SilverTree
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.Straw
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.ErrorRed
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.HealthyGreen
import com.zaneschepke.wireguardautotunnel.desktop.ui.theme.WarningAmber
fun TunnelState.asColor(): Color {
return when (this) {
TunnelState.DOWN,
TunnelState.UNKNOWN -> Color.Gray
TunnelState.HEALTHY -> SilverTree
TunnelState.HANDSHAKE_FAILURE -> AlertRed
TunnelState.HEALTHY -> HealthyGreen
TunnelState.HANDSHAKE_FAILURE -> ErrorRed
TunnelState.RESOLVING_DNS,
TunnelState.STARTING,
TunnelState.STOPPING -> Straw
TunnelState.STOPPING -> WarningAmber
}
}
fun TunnelState.asTooltipMessage(): String? {
fun TunnelState.asTooltipMessage(): String {
return when (this) {
TunnelState.DOWN,
TunnelState.UNKNOWN -> null
TunnelState.UNKNOWN -> "Unknown"
TunnelState.STARTING -> "Starting"
TunnelState.STOPPING -> "Stopping"
TunnelState.HEALTHY -> "Healthy"
@@ -1,11 +1,7 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.components
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ContextMenuArea
import androidx.compose.foundation.ContextMenuItem
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -32,6 +28,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelState
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.LocalNavController
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.button.SurfaceRow
import com.zaneschepke.wireguardautotunnel.desktop.ui.common.button.SwitchWithDivider
@@ -51,6 +48,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun TunnelList(
uiState: TunnelsUiState,
tunnelStatuses: List<TunnelStatus>,
startTunnel: (id: Long) -> Unit,
stopTunnel: (id: Long) -> Unit,
onReorder: (Int, Int) -> Unit,
@@ -71,11 +69,11 @@ fun TunnelList(
}
val tunnelIndicators by
remember(uiState.tunnelStates, uiState.tunnels) {
remember(tunnelStatuses, uiState.tunnels) {
derivedStateOf {
uiState.tunnels.associate { tunnel ->
val state =
uiState.tunnelStates.firstOrNull { it.id == tunnel.id }?.state
tunnelStatuses.firstOrNull { it.id == tunnel.id }?.state
?: TunnelState.UNKNOWN
tunnel.id to (state.asColor() to state.asTooltipMessage())
}
@@ -1,6 +1,7 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.state
import com.zaneschepke.wireguardautotunnel.client.data.model.Theme
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus
data class AppUiState(
val isLoaded: Boolean = false,
@@ -9,6 +10,7 @@ data class AppUiState(
val locale: String = DEFAULT_LOCALE,
val alreadyDonated: Boolean = false,
val lockdownActive: Boolean = false,
val tunnelStatuses: List<TunnelStatus> = emptyList(),
) {
companion object {
const val DEFAULT_LOCALE = "en-US"
@@ -0,0 +1,5 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.state
import androidx.compose.ui.graphics.Color
data class TrayBadgeState(val iconColor: Color, val description: String)
@@ -1,11 +1,9 @@
package com.zaneschepke.wireguardautotunnel.desktop.ui.state
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.core.ipc.dto.TunnelStatus
data class TunnelsUiState(
val tunnels: List<TunnelConfig> = emptyList(),
val tunnelStates: List<TunnelStatus> = emptyList(),
val selectedTunnels: List<TunnelConfig> = emptyList(),
val isSelectionMode: Boolean = false,
val isLoaded: Boolean = false,
@@ -14,7 +14,7 @@ val BalticSea = Color(0xFF1C1B1F)
val ElectricTeal = Color(0xFF4DD0E1)
// Status colors
val SilverTree = Color(0xFF6DB58B)
val AlertRed = Color(0xFFCF6679)
val Straw = Color(0xFFD4C483)
val HealthyGreen = Color(0xFF26A69A)
val ErrorRed = Color(0xFFE53935)
val WarningAmber = Color(0xFFFFB300)
val Disabled = CoolGray.copy(alpha = 0.4f)
@@ -8,6 +8,8 @@ import com.zaneschepke.wireguardautotunnel.client.service.DaemonService
import com.zaneschepke.wireguardautotunnel.desktop.ui.sideeffects.AppSideEffect
import com.zaneschepke.wireguardautotunnel.desktop.ui.state.AppUiState
import io.github.sudarshanmhasrup.localina.api.LocaleUpdater
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
@@ -39,9 +41,15 @@ class AppViewModel(
}
intent { daemonService.alive.collect { reduce { state.copy(daemonConnected = it) } } }
intent {
backendService.statusFlow().collect {
reduce { state.copy(lockdownActive = it.killSwitchEnabled) }
}
backendService
.statusFlow()
.map { it.killSwitchEnabled to it.activeTunnels }
.distinctUntilChanged()
.collect { (lockdown, tunnelStates) ->
reduce {
state.copy(tunnelStatuses = tunnelStates, lockdownActive = lockdown)
}
}
}
}
@@ -5,7 +5,6 @@ import com.dokar.sonner.ToastType
import com.zaneschepke.wireguardautotunnel.client.domain.error.ClientException
import com.zaneschepke.wireguardautotunnel.client.domain.model.TunnelConfig
import com.zaneschepke.wireguardautotunnel.client.domain.repository.TunnelRepository
import com.zaneschepke.wireguardautotunnel.client.service.BackendService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelImportService
import com.zaneschepke.wireguardautotunnel.client.service.TunnelService
import com.zaneschepke.wireguardautotunnel.desktop.ui.screens.tunnels.DeleteIntent
@@ -19,14 +18,12 @@ import io.github.vinceglb.filekit.dialogs.openFileSaver
import io.github.vinceglb.filekit.name
import io.github.vinceglb.filekit.write
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.viewmodel.container
class TunnelsViewModel(
private val tunnelRepository: TunnelRepository,
private val backendService: BackendService,
private val tunnelService: TunnelService,
private val tunnelImportService: TunnelImportService,
) : ContainerHost<TunnelsUiState, AppSideEffect>, ViewModel() {
@@ -41,13 +38,6 @@ class TunnelsViewModel(
reduce { state.copy(tunnels = tunnels, isLoaded = true) }
}
}
intent {
backendService
.statusFlow()
.map { it.activeTunnels }
.distinctUntilChanged()
.collect { reduce { state.copy(tunnelStates = it) } }
}
}
fun onItemsReordered(fromIndex: Int, toIndex: Int) = intent {
+3 -1
View File
@@ -1,9 +1,11 @@
include required("conveyor.conf")
# Replace IP with machine IP for testing on private network
app {
site {
copy-to = "./local-site"
base-url = "http://localhost"
base-url = "localhost:5500"
consistency-checks = "warn"
}
ignore-connection-issues-for-hosts += "localhost:5500"
}