You've already forked ios-client
mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-05-22 17:10:12 -07:00
8bf2de390e
Move the Lottie button into an overlay on the background image so both share the same coordinate space, matching Android's approach of placing btn_connect as a sibling of bg_mask inside bg_mask_container.
203 lines
9.7 KiB
Swift
203 lines
9.7 KiB
Swift
//
|
||
// iOSConnectionView.swift
|
||
// NetBird
|
||
//
|
||
// Connection tab: VPN button, FQDN/IP display, status indicator.
|
||
//
|
||
|
||
import SwiftUI
|
||
import Lottie
|
||
import NetworkExtension
|
||
|
||
#if os(iOS)
|
||
|
||
struct iOSConnectionView: View {
|
||
@EnvironmentObject var viewModel: ViewModel
|
||
@State private var animationKey: UUID = UUID()
|
||
@State private var fqdnCopied = false
|
||
@State private var ipCopied = false
|
||
|
||
var body: some View {
|
||
GeometryReader { geometry in
|
||
let isLandscape = geometry.size.width > geometry.size.height
|
||
let imageName = isLandscape ? "bg-bottom-landscape" : "bg-bottom"
|
||
|
||
ZStack {
|
||
|
||
if viewModel.statusDetailsValid {
|
||
// Background layers
|
||
VStack {
|
||
Color("BgSecondary")
|
||
.frame(height: UIScreen.main.bounds.height * 4/5)
|
||
.ignoresSafeArea(.all)
|
||
Color("BgPrimary")
|
||
.frame(height: UIScreen.main.bounds.height * 1/5)
|
||
.ignoresSafeArea(.all)
|
||
}
|
||
|
||
VStack {
|
||
Image(imageName)
|
||
.resizable(resizingMode: .stretch)
|
||
.aspectRatio(contentMode: DeviceType.isPad ? .fill : .fit)
|
||
// Button overlaid directly on the image so both share the same
|
||
// coordinate space — mirrors Android where btn_connect is a sibling
|
||
// of bg_mask inside bg_mask_container.
|
||
// vertical_bias=0.07: button top = 7% of (imageHeight - buttonHeight)
|
||
// Portrait image ratio h/w = 488/360 = 1.357
|
||
// offset = (Screen.width*1.357 - Screen.width*0.79) * 0.07 ≈ Screen.width * 0.04
|
||
.overlay(alignment: .top) {
|
||
let btnSize = Screen.width * (isLandscape ? 0.40 : 0.79)
|
||
let imgH = isLandscape
|
||
? Screen.height * 0.81 // landscape: height-constrained
|
||
: Screen.width * 1.357 // portrait: width-constrained
|
||
let biasOffset = max(0, (imgH - btnSize) * 0.07)
|
||
|
||
VStack(spacing: 0) {
|
||
Color.clear.frame(height: biasOffset)
|
||
|
||
Button(action: {
|
||
if !viewModel.buttonLock {
|
||
switch viewModel.vpnDisplayState {
|
||
case .disconnected:
|
||
viewModel.connect()
|
||
case .connecting, .connected:
|
||
viewModel.close()
|
||
case .disconnecting:
|
||
break
|
||
}
|
||
}
|
||
}) {
|
||
CustomLottieView(vpnState: $viewModel.vpnDisplayState)
|
||
.id(animationKey)
|
||
.frame(width: btnSize, height: btnSize)
|
||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||
self.animationKey = UUID()
|
||
}
|
||
}
|
||
|
||
Text(viewModel.extensionStateText)
|
||
.foregroundColor(Color("TextSecondary"))
|
||
.font(.system(size: 24, weight: .regular))
|
||
.padding(.top, 24)
|
||
|
||
Spacer()
|
||
}
|
||
}
|
||
.padding(.top, Screen.height * (DeviceType.isPad ? (isLandscape ? -0.15 : 0.36) : 0.19))
|
||
.padding(.leading, UIScreen.main.bounds.height * (isLandscape ? 0.04 : 0))
|
||
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
|
||
.edgesIgnoringSafeArea(.bottom)
|
||
}
|
||
|
||
// FQDN + IP
|
||
VStack {
|
||
ProfileBadge(profileName: viewModel.activeProfileName) {
|
||
viewModel.navigateToProfilesView = true
|
||
}
|
||
.padding(.top, 8)
|
||
|
||
Text(fqdnCopied ? "Copied" : viewModel.fqdn)
|
||
.foregroundColor(Color("TextPrimary"))
|
||
.font(.system(size: 20, weight: .regular))
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.5)
|
||
.opacity(fqdnCopied ? 0.7 : 1.0)
|
||
.animation(.easeInOut(duration: 0.2), value: fqdnCopied)
|
||
.padding(.horizontal, 16)
|
||
.padding(.top, Screen.height * (DeviceType.isPad ? 0.09 : 0.13))
|
||
.padding(.bottom, 5)
|
||
.onTapGesture {
|
||
guard !viewModel.fqdn.isEmpty else { return }
|
||
UIPasteboard.general.string = viewModel.fqdn
|
||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||
withAnimation(.smooth) {
|
||
fqdnCopied = true
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||
withAnimation(.smooth) {
|
||
fqdnCopied = false
|
||
}
|
||
}
|
||
}
|
||
|
||
Text(ipCopied ? "Copied" : viewModel.ip)
|
||
.foregroundColor(Color("TextPrimary"))
|
||
.font(.system(size: 20, weight: .regular))
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.5)
|
||
.opacity(ipCopied ? 0.7 : 1.0)
|
||
.animation(.easeInOut(duration: 0.2), value: ipCopied)
|
||
.onTapGesture {
|
||
guard !viewModel.ip.isEmpty else { return }
|
||
UIPasteboard.general.string = viewModel.ip
|
||
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
||
withAnimation(.smooth) {
|
||
ipCopied = true
|
||
}
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||
withAnimation(.smooth) {
|
||
ipCopied = false
|
||
}
|
||
}
|
||
}
|
||
|
||
Spacer()
|
||
}
|
||
|
||
// Network warning banner – above tab bar
|
||
if viewModel.vpnDisplayState == .connected && !viewModel.isInternetConnected {
|
||
VStack {
|
||
Spacer()
|
||
NetworkWarningBanner()
|
||
.padding(.bottom, geometry.safeAreaInsets.bottom + 80)
|
||
}
|
||
.transition(.opacity.combined(with: .move(edge: .bottom)))
|
||
.animation(.easeInOut(duration: 0.3), value: viewModel.isInternetConnected)
|
||
}
|
||
|
||
// Hidden NavigationLink for ProfilesView
|
||
NavigationLink("", destination: ProfilesListView(), isActive: $viewModel.navigateToProfilesView)
|
||
.hidden()
|
||
|
||
// Hidden NavigationLink for ServerView
|
||
NavigationLink("", destination: ServerView(), isActive: $viewModel.navigateToServerView)
|
||
.hidden()
|
||
.onChange(of: viewModel.navigateToServerView) { newValue in
|
||
if !newValue {
|
||
viewModel.startPollingDetails()
|
||
}
|
||
}
|
||
|
||
} else {
|
||
// Loading placeholder
|
||
ZStack {
|
||
Color("BgPrimary")
|
||
.ignoresSafeArea()
|
||
Image("netbird-logo-menu")
|
||
.resizable()
|
||
.scaledToFit()
|
||
.frame(width: 200)
|
||
}
|
||
}
|
||
|
||
// Safari login view — shown regardless of statusDetailsValid
|
||
if viewModel.networkExtensionAdapter.showBrowser,
|
||
let loginURLString = viewModel.networkExtensionAdapter.loginURL,
|
||
let loginURL = URL(string: loginURLString)
|
||
{
|
||
SafariView(isPresented: $viewModel.networkExtensionAdapter.showBrowser,
|
||
url: loginURL,
|
||
didFinish: {
|
||
print("Finish login")
|
||
viewModel.networkExtensionAdapter.startVPNConnection()
|
||
})
|
||
}
|
||
}
|
||
}
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.navigationBarHidden(true)
|
||
}
|
||
}
|
||
|
||
#endif
|