// OnboardingView.swift import SwiftUI struct OnboardingView: View { @EnvironmentObject var settings: ServerSettings @EnvironmentObject var connectivity: ConnectivitySettings @State private var host = "" @State private var isConnecting = false @State private var errorMessage: String? var body: some View { ZStack { Color.black.ignoresSafeArea() VStack(alignment: .leading, spacing: 0) { Spacer().frame(height: 48) // Wordmark Group { Text("J").foregroundColor(Color("KisaniOrange")) + Text("arvis").foregroundColor(.white) } .font(.system(size: 48, weight: .black, design: .default)) .kerning(-2) Text("Point Jarvis at your platform\nto start receiving signals.") .font(.system(size: 16)) .foregroundColor(Color(.systemGray)) .lineSpacing(4) .padding(.top, 10) .padding(.bottom, 40) // Field label Text("Server address") .font(.system(size: 11, weight: .bold)) .foregroundColor(Color(.systemGray2)) .kerning(0.8) .textCase(.uppercase) .padding(.bottom, 8) // Input field HStack(spacing: 12) { Image(systemName: "server.rack") .font(.system(size: 18)) .foregroundColor(host.isEmpty ? Color(.systemGray3) : Color("KisaniOrange")) TextField("192.168.1.42:8080", text: $host) .font(.system(size: 15, design: .monospaced)) .foregroundColor(Color("KisaniOrange")) .autocorrectionDisabled() .textInputAutocapitalization(.never) .keyboardType(.URL) } .padding(14) .background(host.isEmpty ? Color(.systemGray6).opacity(0.1) : Color("KisaniOrange").opacity(0.07)) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(host.isEmpty ? Color(.systemGray5) : Color("KisaniOrange"), lineWidth: 0.5) ) .cornerRadius(12) Text("http:// or https:// · local network or VPN") .font(.system(size: 11, design: .monospaced)) .foregroundColor(Color(.systemGray4)) .padding(.top, 8) .padding(.bottom, 32) if let errorMessage { Text(errorMessage) .font(.system(size: 13)) .foregroundColor(.red.opacity(0.8)) .padding(.bottom, 12) } // Connect button Button { Task { await connect() } } label: { HStack { Spacer() if isConnecting { ProgressView().tint(.white) } else { Text("Connect to Jarvis") .font(.system(size: 16, weight: .bold)) .foregroundColor(.white) } Spacer() } .frame(height: 52) .background(host.isEmpty ? Color(.systemGray5) : Color("KisaniOrange")) .cornerRadius(14) } .disabled(host.isEmpty || isConnecting) Button("Scan QR code instead") {} .font(.system(size: 14)) .foregroundColor(Color(.systemGray3)) .frame(maxWidth: .infinity) .frame(height: 48) Spacer() Text("Self-hosted · no account needed") .font(.system(size: 11, design: .monospaced)) .foregroundColor(Color(.systemGray5)) .frame(maxWidth: .infinity, alignment: .center) .padding(.bottom, 24) } .padding(.horizontal, 24) } } private func connect() async { isConnecting = true errorMessage = nil do { try await settings.save(host: host) // Seed the Direct/LAN endpoint so connectivity can fail over to a VPN later. if connectivity.directHost.trimmingCharacters(in: .whitespaces).isEmpty { connectivity.directHost = host.trimmingCharacters(in: .whitespaces) } } catch { errorMessage = error.localizedDescription } isConnecting = false } }