704859a369
iOS app (ios/, SwiftUI, iOS 17+) in the web UI's styling with the Kamon logo. It covers everything the web interface does except password, API tokens and backups: dashboard, peers with search and filter, peer detail with traffic charts, add/edit peers, one-time config with QR code and share sheet, server settings with apply bar, app settings, data retention and log viewer. - Pairing by QR code or pasted pairing code; token kept in the keychain; self-signed certificates are pinned by SHA-256 fingerprint. - Colour providers and logo drawing are nonisolated: SwiftUI's background renderer calls them, and main-actor closures crashed there when the camera scanner was open. - App Store: version 1.0, export compliance, privacy manifest, app icon, release.sh (archive and upload), listing text, review notes and 6.9" screenshots in ios/AppStore. Server: - Full-access API tokens may use settings, logs and restart; password, tokens, backup/restore and the admin username stay admin-only. - Web pairing dialog gains "Copy pairing code". - The development simulator reports health checks in Linux wording.
66 lines
1.8 KiB
Swift
66 lines
1.8 KiB
Swift
import SwiftUI
|
|
|
|
@main
|
|
struct GhostwireApp: App {
|
|
@State private var session = AppSession()
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.environment(session)
|
|
.tint(Color.gwAccent)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct RootView: View {
|
|
@Environment(AppSession.self) private var session
|
|
|
|
var body: some View {
|
|
@Bindable var session = session
|
|
Group {
|
|
if session.api == nil {
|
|
PairingView()
|
|
} else {
|
|
MainTabView()
|
|
}
|
|
}
|
|
.alert("GHOSTWIRE", isPresented: Binding(get: { session.alert != nil }, set: { if !$0 { session.alert = nil } })) {
|
|
Button("OK", role: .cancel) {}
|
|
} message: {
|
|
Text(session.alert ?? "")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct MainTabView: View {
|
|
enum Tab: String { case dashboard, peers, server, settings }
|
|
|
|
@Environment(AppSession.self) private var session
|
|
@State private var tab: Tab = {
|
|
#if DEBUG
|
|
// Development: `-tab peers` opens a tab directly.
|
|
if let t = UserDefaults.standard.string(forKey: "tab"), let tab = Tab(rawValue: t) { return tab }
|
|
#endif
|
|
return .dashboard
|
|
}()
|
|
|
|
var body: some View {
|
|
TabView(selection: $tab) {
|
|
DashboardView()
|
|
.tabItem { Label("Dashboard", systemImage: "square.grid.2x2") }
|
|
.tag(Tab.dashboard)
|
|
PeersView()
|
|
.tabItem { Label("Peers", systemImage: "person.2") }
|
|
.tag(Tab.peers)
|
|
ServerView()
|
|
.tabItem { Label("Server", systemImage: "server.rack") }
|
|
.tag(Tab.server)
|
|
SettingsView()
|
|
.tabItem { Label("Settings", systemImage: "slider.horizontal.3") }
|
|
.tag(Tab.settings)
|
|
}
|
|
.task { await session.loadMe() }
|
|
}
|
|
}
|