Initial commit: Jarvis iOS app
SwiftUI client for a self-hosted RSS news-correlation platform: signal feed, story detail, article reader, feed manager, and LAN⇄Tailscale connectivity. Project generated from project.yml via XcodeGen. Includes CI build matrix (macOS 14/15 × Debug/Release), issue templates, backlog, and API/backend handoff docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
128
Jarvis/Views/Onboarding/OnboardingView.swift
Normal file
128
Jarvis/Views/Onboarding/OnboardingView.swift
Normal file
@@ -0,0 +1,128 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user