2026-06-18 18:04:59 +03:00
// C o n n e c t i v i t y V i e w . s w i f t
// J a r v i s — s e r v e r a d d r e s s , a R e m o t e A c c e s s c a r d ( U s e < p r o v i d e r > w h e n r e m o t e +
// h o s t + l i v e s t a t u s ) , a n d a n A u t o - c o n n e c t a u t o m a t i o n c a r d . T h e a p p c a n ' t s t a r t
// t h e V P N t u n n e l ( i O S ) , s o w h e n t h e r e m o t e i s d o w n w e o f f e r a o n e - t a p j u m p .
import SwiftUI
struct ConnectivityView : View {
@ EnvironmentObject var connectivity : ConnectivitySettings
@ EnvironmentObject var manager : ConnectivityManager
@ Environment ( \ . dismiss ) private var dismiss
private var directReach : Reachability {
manager . status [ connectivity . directHost . trimmingCharacters ( in : . whitespaces ) ] ? ? . unknown
}
private var remoteReach : Reachability {
manager . status [ connectivity . remoteHost . trimmingCharacters ( in : . whitespaces ) ] ? ? . unknown
}
private var providerName : String { connectivity . provider . displayName }
var body : some View {
ZStack {
2026-06-22 02:33:33 +03:00
Palette . background . ignoresSafeArea ( )
2026-06-18 18:04:59 +03:00
ScrollView {
VStack ( alignment : . leading , spacing : 26 ) {
header
serverSection
remoteAccessSection
automationSection
providerSection
actions
}
. padding ( . horizontal , 20 )
. padding ( . vertical , 12 )
}
}
. presentationDragIndicator ( . visible )
. task { await recheck ( ) }
}
// MARK: - H e a d e r
private var header : some View {
HStack {
Text ( " Connectivity " )
. font ( . system ( size : 22 , weight : . heavy ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . primaryText )
2026-06-18 18:04:59 +03:00
Spacer ( )
Button { dismiss ( ) } label : {
Image ( systemName : " xmark " )
. font ( . system ( size : 15 , weight : . bold ) )
. foregroundStyle ( Color ( hex : " 888888 " ) )
. frame ( width : 44 , height : 44 )
}
}
. padding ( . top , 8 )
}
// MARK: - S e r v e r ( L A N )
private var serverSection : some View {
VStack ( alignment : . leading , spacing : 10 ) {
sectionLabel ( " SERVER " )
Card {
cardRow ( icon : " server.rack " ) {
fieldStack ( title : " Direct (LAN) host " ,
text : $ connectivity . directHost ,
placeholder : " 192.168.30.50:8080 " ,
reach : directReach )
}
}
}
}
// MARK: - R e m o t e a c c e s s
private var remoteAccessSection : some View {
VStack ( alignment : . leading , spacing : 10 ) {
sectionLabel ( " REMOTE ACCESS " )
Card {
VStack ( spacing : 0 ) {
cardRow ( icon : " globe.badge.chevron.backward " ) {
Toggle ( isOn : $ connectivity . remoteEnabled ) {
VStack ( alignment : . leading , spacing : 3 ) {
Text ( " Use \( providerName ) when remote " )
. font ( . system ( size : 16 , weight : . heavy ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . primaryText )
2026-06-18 18:04:59 +03:00
Text ( " Connects via VPN when not on home network " )
. font ( . system ( size : 12 ) )
. foregroundStyle ( Color ( hex : " 888888 " ) )
. fixedSize ( horizontal : false , vertical : true )
}
}
. tint ( Palette . orange )
}
if connectivity . remoteEnabled {
divider
cardRow ( icon : " externaldrive.connected.to.line.below " ) {
fieldStack ( title : " \( providerName ) host " ,
text : $ connectivity . remoteHost ,
placeholder : connectivity . provider . remoteHint ,
reach : remoteReach )
}
divider
statusRow
}
}
}
}
}
private var statusRow : some View {
HStack ( spacing : 10 ) {
dot ( for : connectivity . remoteEnabled ? remoteReach : . unknown )
Text ( statusText )
. font ( . system ( size : 14 , weight : . bold ) )
. foregroundStyle ( statusColor )
Spacer ( )
if remoteReach != . reachable && connectivity . provider . canOpenApp {
Button { connectivity . provider . openApp ( ) } label : {
Text ( " Open \( providerName ) " )
. font ( . system ( size : 13 , weight : . bold ) )
. foregroundStyle ( . black )
. padding ( . horizontal , 14 )
. frame ( height : 36 )
. background ( Palette . orange )
. clipShape ( Capsule ( ) )
}
}
}
. padding ( . vertical , 12 )
. padding ( . horizontal , 14 )
}
private var statusText : String {
switch remoteReach {
case . reachable : return " \( providerName ) connected "
case . checking : return " Checking \( providerName ) … "
default : return " \( providerName ) not active "
}
}
private var statusColor : Color {
switch remoteReach {
case . reachable : return Color ( hex : " 33C25E " )
case . checking : return Palette . healthFailing
default : return Color ( hex : " 888888 " )
}
}
// MARK: - A u t o m a t i o n
private var automationSection : some View {
VStack ( alignment : . leading , spacing : 10 ) {
sectionLabel ( " AUTOMATION " )
Card {
VStack ( spacing : 0 ) {
cardRow ( icon : " wifi " ) {
Toggle ( isOn : $ connectivity . autoConnect ) {
VStack ( alignment : . leading , spacing : 3 ) {
Text ( " Auto-connect on network change " )
. font ( . system ( size : 16 , weight : . heavy ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . primaryText )
2026-06-18 18:04:59 +03:00
Text ( " Prefers the LAN on a trusted network; switches to \( providerName ) when away " )
. font ( . system ( size : 12 ) )
. foregroundStyle ( Color ( hex : " 888888 " ) )
. fixedSize ( horizontal : false , vertical : true )
}
}
. tint ( Palette . orange )
}
if connectivity . autoConnect {
divider
cardRow ( icon : " timer " ) {
HStack {
Text ( " Wait before switching " )
. font ( . system ( size : 15 , weight : . bold ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . primaryText )
2026-06-18 18:04:59 +03:00
Spacer ( )
Stepper ( value : $ connectivity . switchDelaySeconds , in : 1. . . 60 , step : 1 ) {
Text ( " \( connectivity . switchDelaySeconds ) s " )
. font ( . system ( size : 14 , weight : . bold , design : . monospaced ) )
. foregroundStyle ( Palette . orange )
}
. labelsHidden ( )
. fixedSize ( )
Text ( " \( connectivity . switchDelaySeconds ) s " )
. font ( . system ( size : 14 , weight : . bold , design : . monospaced ) )
. foregroundStyle ( Palette . orange )
. frame ( width : 36 , alignment : . trailing )
}
}
}
}
}
Text ( " iOS can't start the \( providerName ) tunnel for Jarvis — keep the \( providerName ) app connected when away. Jarvis only picks the reachable address. " )
. font ( . system ( size : 11 ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . tertiaryText )
2026-06-18 18:04:59 +03:00
. lineSpacing ( 2 )
}
}
// MARK: - P r o v i d e r
private var providerSection : some View {
VStack ( alignment : . leading , spacing : 10 ) {
sectionLabel ( " PROVIDER " )
ScrollView ( . horizontal , showsIndicators : false ) {
HStack ( spacing : 8 ) {
ForEach ( VPNProvider . allCases ) { p in
let selected = connectivity . provider = = p
Button { connectivity . provider = p } label : {
Text ( p . displayName )
. font ( . system ( size : 13 , weight : . bold ) )
. foregroundStyle ( selected ? . black : Color ( hex : " AAAAAA " ) )
. padding ( . horizontal , 14 )
. frame ( height : 34 )
. background ( selected ? Palette . orange : Palette . surface )
. clipShape ( Capsule ( ) )
}
}
}
}
}
}
// MARK: - A c t i o n s
private var actions : some View {
HStack ( spacing : 12 ) {
Button { Task { await recheck ( ) } } label : {
Text ( " Re-check " )
. font ( . system ( size : 15 , weight : . bold ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . primaryText )
2026-06-18 18:04:59 +03:00
. frame ( maxWidth : . infinity ) . frame ( height : 50 )
. background ( Palette . surface2 )
. clipShape ( RoundedRectangle ( cornerRadius : 14 ) )
}
Button { Task { await manager . resolveAndActivate ( ) ; await recheck ( ) } } label : {
Text ( " Apply " )
. font ( . system ( size : 15 , weight : . bold ) )
. foregroundStyle ( . black )
. frame ( maxWidth : . infinity ) . frame ( height : 50 )
. background ( connectivity . isConfigured ? Palette . orange : Palette . surface2 )
. clipShape ( RoundedRectangle ( cornerRadius : 14 ) )
}
. disabled ( ! connectivity . isConfigured )
}
}
// MARK: - B u i l d i n g b l o c k s
private func sectionLabel ( _ t : String ) -> some View {
Text ( t )
. font ( . system ( size : 11 , weight : . bold , design : . monospaced ) )
. kerning ( 0.8 )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . tertiaryText )
2026-06-18 18:04:59 +03:00
}
private func Card < Content : View > ( @ ViewBuilder _ content : ( ) -> Content ) -> some View {
content ( )
. background ( Palette . surface )
. clipShape ( RoundedRectangle ( cornerRadius : 14 ) )
}
private func cardRow < Content : View > ( icon : String , @ ViewBuilder _ content : ( ) -> Content ) -> some View {
HStack ( alignment : . center , spacing : 14 ) {
Image ( systemName : icon )
. font ( . system ( size : 18 , weight : . regular ) )
2026-06-22 02:33:33 +03:00
. foregroundStyle ( Palette . secondaryText )
2026-06-18 18:04:59 +03:00
. frame ( width : 26 )
content ( )
}
. padding ( . horizontal , 14 )
. padding ( . vertical , 14 )
. frame ( minHeight : 44 )
}
private var divider : some View {
Rectangle ( ) . fill ( Palette . surface2 ) . frame ( height : 1 ) . padding ( . leading , 54 )
}
private func fieldStack ( title : String , text : Binding < String > , placeholder : String , reach : Reachability ) -> some View {
VStack ( alignment : . leading , spacing : 3 ) {
Text ( title )
. font ( . system ( size : 12 ) )
. foregroundStyle ( Color ( hex : " 888888 " ) )
HStack ( spacing : 8 ) {
TextField ( " " , text : text ,
2026-06-22 02:33:33 +03:00
prompt : Text ( placeholder ) . foregroundColor ( Palette . mutedText ) )
2026-06-18 18:04:59 +03:00
. font ( . system ( size : 16 , weight : . regular , design : . monospaced ) )
. foregroundStyle ( Palette . orange )
. autocorrectionDisabled ( )
. textInputAutocapitalization ( . never )
. keyboardType ( . URL )
if reach != . unknown { dot ( for : reach ) }
}
}
}
// MARK: - R e a c h a b i l i t y h e l p e r s
private func recheck ( ) async {
await manager . check ( connectivity . directHost )
if connectivity . remoteActiveInPolicy {
await manager . check ( connectivity . remoteHost )
}
}
private func color ( for r : Reachability ) -> Color {
switch r {
case . reachable : return Color ( hex : " 33C25E " )
case . unreachable : return Color ( hex : " C25555 " )
case . checking : return Palette . healthFailing
2026-06-22 02:33:33 +03:00
case . unknown : return Palette . tertiaryText
2026-06-18 18:04:59 +03:00
}
}
private func dot ( for r : Reachability ) -> some View {
Circle ( ) . fill ( color ( for : r ) ) . frame ( width : 9 , height : 9 )
}
}