vibetunnel/ios/VibeTunnel/Views/Connection/ServerConfigForm.swift
Peter Steinberger baaaa5a033 fix: CI and linting issues across all platforms
- Fix code signing in Mac and iOS test workflows
- Fix all SwiftFormat and SwiftLint issues
- Fix ESLint issues in web code
- Remove force casts and unwrapping in Swift code
- Update build scripts to use correct file paths
2025-06-23 19:40:53 +02:00

226 lines
9.9 KiB
Swift

import SwiftUI
/// Form component for entering server connection details.
///
/// Provides input fields for host, port, name, and password
/// with validation and recent servers functionality.
struct ServerConfigForm: View {
@Binding var host: String
@Binding var port: String
@Binding var name: String
@Binding var password: String
let isConnecting: Bool
let errorMessage: String?
let onConnect: () -> Void
@State private var networkMonitor = NetworkMonitor.shared
@FocusState private var focusedField: Field?
@State private var recentServers: [ServerConfig] = []
enum Field {
case host
case port
case name
case password
}
var body: some View {
VStack(spacing: Theme.Spacing.extraLarge) {
// Input Fields
VStack(spacing: Theme.Spacing.large) {
// Host/IP Field
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Label("Server Address", systemImage: "network")
.font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.primaryAccent)
TextField("192.168.1.100 or localhost", text: $host)
.textFieldStyle(TerminalTextFieldStyle())
.autocapitalization(.none)
.disableAutocorrection(true)
.focused($focusedField, equals: .host)
.submitLabel(.next)
.onSubmit {
focusedField = .port
}
}
// Port Field
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Label("Port", systemImage: "number.circle")
.font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.primaryAccent)
TextField("3000", text: $port)
.textFieldStyle(TerminalTextFieldStyle())
.keyboardType(.numberPad)
.focused($focusedField, equals: .port)
.submitLabel(.next)
.onSubmit {
focusedField = .name
}
}
// Name Field (Optional)
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Label("Connection Name (Optional)", systemImage: "tag")
.font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.primaryAccent)
TextField("My Mac", text: $name)
.textFieldStyle(TerminalTextFieldStyle())
.focused($focusedField, equals: .name)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
}
// Password Field (optional)
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Label("Password (optional)", systemImage: "lock")
.font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.primaryAccent)
SecureField("Enter password", text: $password)
.textFieldStyle(TerminalTextFieldStyle())
.focused($focusedField, equals: .password)
.submitLabel(.done)
.onSubmit {
focusedField = nil
onConnect()
}
}
}
.padding(.horizontal)
// Error Message
if let errorMessage {
HStack(spacing: Theme.Spacing.small) {
Image(systemName: "exclamationmark.triangle")
.font(.caption)
Text(errorMessage)
.font(Theme.Typography.terminalSystem(size: 12))
}
.foregroundColor(Theme.Colors.errorAccent)
.padding(.horizontal)
.transition(.asymmetric(
insertion: .scale.combined(with: .opacity),
removal: .scale.combined(with: .opacity)
))
}
// Connect Button
Button(action: {
HapticFeedback.impact(.medium)
onConnect()
}, label: {
if isConnecting {
HStack(spacing: Theme.Spacing.small) {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Theme.Colors.terminalBackground))
.scaleEffect(0.8)
Text("Connecting...")
.font(Theme.Typography.terminalSystem(size: 16))
}
.frame(maxWidth: .infinity)
} else if !networkMonitor.isConnected {
HStack(spacing: Theme.Spacing.small) {
Image(systemName: "wifi.slash")
Text("No Internet Connection")
}
.font(Theme.Typography.terminalSystem(size: 16))
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
} else {
HStack(spacing: Theme.Spacing.small) {
Image(systemName: "bolt.fill")
Text("Connect")
}
.font(Theme.Typography.terminalSystem(size: 16))
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
}
})
.foregroundColor(isConnecting || !networkMonitor.isConnected ? Theme.Colors.terminalForeground : Theme
.Colors.primaryAccent
)
.padding(.vertical, Theme.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.fill(isConnecting || !networkMonitor.isConnected ? Theme.Colors.cardBackground : Theme.Colors
.terminalBackground
)
)
.overlay(
RoundedRectangle(cornerRadius: Theme.CornerRadius.medium)
.stroke(
networkMonitor.isConnected ? Theme.Colors.primaryAccent : Theme.Colors.cardBorder,
lineWidth: isConnecting || !networkMonitor.isConnected ? 1 : 2
)
.opacity(host.isEmpty ? 0.5 : 1.0)
)
.disabled(isConnecting || host.isEmpty || !networkMonitor.isConnected)
.padding(.horizontal)
.scaleEffect(isConnecting ? 0.98 : 1.0)
.animation(Theme.Animation.quick, value: isConnecting)
.animation(Theme.Animation.quick, value: networkMonitor.isConnected)
// Recent Servers (if any)
if !recentServers.isEmpty {
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Text("Recent Connections")
.font(Theme.Typography.terminalSystem(size: 12))
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Theme.Spacing.small) {
ForEach(recentServers.prefix(3), id: \.host) { server in
Button(action: {
host = server.host
port = String(server.port)
name = server.name ?? ""
password = server.password ?? ""
HapticFeedback.selection()
}, label: {
VStack(alignment: .leading, spacing: 4) {
Text(server.displayName)
.font(Theme.Typography.terminalSystem(size: 12))
.fontWeight(.medium)
Text("\(server.host):\(server.port)")
.font(Theme.Typography.terminalSystem(size: 10))
.opacity(0.7)
}
.foregroundColor(Theme.Colors.terminalForeground)
.padding(.horizontal, Theme.Spacing.medium)
.padding(.vertical, Theme.Spacing.small)
.background(
RoundedRectangle(cornerRadius: Theme.CornerRadius.small)
.stroke(Theme.Colors.cardBorder, lineWidth: 1)
)
})
.buttonStyle(PlainButtonStyle())
}
}
.padding(.horizontal)
}
}
.transition(.move(edge: .bottom).combined(with: .opacity))
}
}
.onAppear {
focusedField = .host
loadRecentServers()
}
}
private func loadRecentServers() {
// Load recent servers from UserDefaults
if let data = UserDefaults.standard.data(forKey: "recentServers"),
let servers = try? JSONDecoder().decode([ServerConfig].self, from: data)
{
recentServers = servers
}
}
}