vibetunnel/ios/VibeTunnel/Views/Connection/ConnectionView.swift
Peter Steinberger 15eea702ab linting
2025-06-25 02:11:51 +02:00

206 lines
7.5 KiB
Swift

import Observation
import SwiftUI
/// View for establishing connection to a VibeTunnel server.
///
/// Displays the app branding and provides interface for entering
/// server connection details with saved server management.
struct ConnectionView: View {
@Environment(ConnectionManager.self)
var connectionManager
@State private var networkMonitor = NetworkMonitor.shared
@State private var viewModel = ConnectionViewModel()
@State private var logoScale: CGFloat = 0.8
@State private var contentOpacity: Double = 0
var body: some View {
NavigationStack {
ScrollView {
// Content
VStack(spacing: Theme.Spacing.extraExtraLarge) {
// Logo and Title
VStack(spacing: Theme.Spacing.large) {
ZStack {
// Glow effect
Image(systemName: "terminal.fill")
.font(.system(size: 80))
.foregroundColor(Theme.Colors.primaryAccent)
.blur(radius: 20)
.opacity(0.5)
// Main icon
Image(systemName: "terminal.fill")
.font(.system(size: 80))
.foregroundColor(Theme.Colors.primaryAccent)
.glowEffect()
}
.scaleEffect(logoScale)
.onAppear {
withAnimation(Theme.Animation.smooth.delay(0.1)) {
logoScale = 1.0
}
}
VStack(spacing: Theme.Spacing.small) {
Text("VibeTunnel")
.font(.system(size: 42, weight: .bold, design: .rounded))
.foregroundColor(Theme.Colors.terminalForeground)
Text("Terminal Multiplexer")
.font(Theme.Typography.terminalSystem(size: 16))
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
.tracking(2)
// Network status
ConnectionStatusView()
.padding(.top, Theme.Spacing.small)
}
}
.padding(.top, 60)
// Connection Form
ServerConfigForm(
host: $viewModel.host,
port: $viewModel.port,
name: $viewModel.name,
password: $viewModel.password,
isConnecting: viewModel.isConnecting,
errorMessage: viewModel.errorMessage,
onConnect: connectToServer
)
.opacity(contentOpacity)
.onAppear {
withAnimation(Theme.Animation.smooth.delay(0.3)) {
contentOpacity = 1.0
}
}
Spacer()
}
.padding()
}
.scrollBounceBehavior(.basedOnSize)
.toolbar(.hidden, for: .navigationBar)
.background {
// Background
Theme.Colors.terminalBackground
.ignoresSafeArea()
}
}
.navigationViewStyle(StackNavigationViewStyle())
.preferredColorScheme(.dark)
.onAppear {
viewModel.loadLastConnection()
}
.sheet(isPresented: $viewModel.showLoginView) {
if let config = viewModel.pendingServerConfig,
let authService = connectionManager.authenticationService {
LoginView(
isPresented: $viewModel.showLoginView,
serverConfig: config,
authenticationService: authService
) {
// Authentication successful, mark as connected
connectionManager.isConnected = true
}
}
}
}
private func connectToServer() {
guard networkMonitor.isConnected else {
viewModel.errorMessage = "No internet connection available"
return
}
Task {
await viewModel.testConnection { config in
connectionManager.saveConnection(config)
// Show login view to authenticate
viewModel.showLoginView = true
}
}
}
}
/// View model for managing connection form state and validation.
@Observable
class ConnectionViewModel {
var host: String = "127.0.0.1"
var port: String = "4020"
var name: String = ""
var password: String = ""
var isConnecting: Bool = false
var errorMessage: String?
var showLoginView: Bool = false
var pendingServerConfig: ServerConfig?
func loadLastConnection() {
if let config = UserDefaults.standard.data(forKey: "savedServerConfig"),
let serverConfig = try? JSONDecoder().decode(ServerConfig.self, from: config)
{
self.host = serverConfig.host
self.port = String(serverConfig.port)
self.name = serverConfig.name ?? ""
}
}
@MainActor
func testConnection(onSuccess: @escaping (ServerConfig) -> Void) async {
errorMessage = nil
guard !host.isEmpty else {
errorMessage = "Please enter a server address"
return
}
guard let portNumber = Int(port), portNumber > 0, portNumber <= 65_535 else {
errorMessage = "Please enter a valid port number"
return
}
isConnecting = true
let config = ServerConfig(
host: host,
port: portNumber,
name: name.isEmpty ? nil : name
)
do {
// Test basic connectivity by checking health endpoint
let url = config.baseURL.appendingPathComponent("api/health")
let request = URLRequest(url: url)
let (_, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200
{
// Connection successful, save config and trigger authentication
pendingServerConfig = config
onSuccess(config)
} else {
errorMessage = "Failed to connect to server"
}
} catch {
if let urlError = error as? URLError {
switch urlError.code {
case .notConnectedToInternet:
errorMessage = "No internet connection"
case .cannotFindHost:
errorMessage = "Cannot find server"
case .cannotConnectToHost:
errorMessage = "Cannot connect to server"
case .timedOut:
errorMessage = "Connection timed out"
default:
errorMessage = "Connection failed: \(error.localizedDescription)"
}
} else {
errorMessage = "Connection failed: \(error.localizedDescription)"
}
}
isConnecting = false
}
}