The bundle-ID and display-name renames weren't enough — the actual Xcode target/product name was still "Jarvis", which drives CFBundleName, Xcode's Organizer archive list, the .xcodeproj filename, and the scheme name. Renamed all the way through: - project.yml: top-level name, target key, PRODUCT_NAME, source/info paths - Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content diffs on the moved files) - .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj / scheme Jarvis -> Jervis.xcodeproj / scheme Jervis Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app, CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis". CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior decision — bundle ID isn't user-facing anywhere including Organizer). Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo name/URL are unchanged — pure internal source identifiers, not user or developer-facing product identity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
129 lines
4.9 KiB
Swift
129 lines
4.9 KiB
Swift
// 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 {
|
|
Palette.background.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
|
|
}
|
|
}
|