bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
339 lines
11 KiB
Swift
339 lines
11 KiB
Swift
import SwiftUI
|
|
|
|
/**
|
|
* QUIXZOOM Capture App — Huvudvy
|
|
*
|
|
* 4 lägen:
|
|
* 1. Recording Mode — Filma hela gatan
|
|
* 2. Guided Mode — AI ger realtidsinstruktioner
|
|
* 3. Mission Mode — Specifika uppgifter
|
|
* 4. Adaptive Mode — AI planerar nästa observation
|
|
*/
|
|
struct ContentView: View {
|
|
@StateObject private var captureManager = CaptureManager()
|
|
@State private var selectedMode: CaptureSession.CaptureMode = .recording
|
|
@State private var showingSettings = false
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
VStack(spacing: 0) {
|
|
// Status bar
|
|
StatusBarView(captureManager: captureManager)
|
|
|
|
// Kamera-vy
|
|
CameraPreviewView(captureManager: captureManager)
|
|
.frame(maxHeight: .infinity)
|
|
|
|
// AI Instruktion (om aktiv)
|
|
if let instruction = captureManager.currentInstruction {
|
|
AIInstructionView(instruction: instruction)
|
|
}
|
|
|
|
// Kontroller
|
|
ControlPanelView(
|
|
captureManager: captureManager,
|
|
selectedMode: $selectedMode
|
|
)
|
|
}
|
|
.navigationTitle("QUIXZOOM Capture")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button(action: { showingSettings = true }) {
|
|
Image(systemName: "gear")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingSettings) {
|
|
SettingsView()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Status Bar
|
|
|
|
struct StatusBarView: View {
|
|
@ObservedObject var captureManager: CaptureManager
|
|
|
|
var body: some View {
|
|
HStack(spacing: 16) {
|
|
// GPS-status
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "location.fill")
|
|
.foregroundColor(gpsColor)
|
|
Text("\(Int(captureManager.gpsSignal * 100))%")
|
|
.font(.caption)
|
|
}
|
|
|
|
// Kvalitet
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "checkmark.shield")
|
|
.foregroundColor(qualityColor)
|
|
Text("\(Int(captureManager.qualityScore * 100))%")
|
|
.font(.caption)
|
|
}
|
|
|
|
// Uppladdning
|
|
if captureManager.uploadProgress > 0 && captureManager.uploadProgress < 1 {
|
|
HStack(spacing: 4) {
|
|
ProgressView(value: captureManager.uploadProgress)
|
|
.frame(width: 40)
|
|
Text("\(Int(captureManager.uploadProgress * 100))%")
|
|
.font(.caption)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Session-status
|
|
if let session = captureManager.currentSession {
|
|
Text(session.mode.rawValue.uppercased())
|
|
.font(.caption.bold())
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(session.status == .recording ? Color.red.opacity(0.2) : Color.gray.opacity(0.2))
|
|
.cornerRadius(4)
|
|
}
|
|
}
|
|
.padding(.horizontal)
|
|
.padding(.vertical, 8)
|
|
.background(Color(.systemBackground))
|
|
}
|
|
|
|
var gpsColor: Color {
|
|
if captureManager.gpsSignal > 0.8 { return .green }
|
|
if captureManager.gpsSignal > 0.5 { return .yellow }
|
|
return .red
|
|
}
|
|
|
|
var qualityColor: Color {
|
|
if captureManager.qualityScore > 0.8 { return .green }
|
|
if captureManager.qualityScore > 0.5 { return .yellow }
|
|
return .red
|
|
}
|
|
}
|
|
|
|
// MARK: - Camera Preview
|
|
|
|
struct CameraPreviewView: View {
|
|
@ObservedObject var captureManager: CaptureManager
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
// Kamera-preview (placeholder)
|
|
Rectangle()
|
|
.fill(Color.black)
|
|
|
|
// Inspelnings-indikator
|
|
if captureManager.isRecording {
|
|
VStack {
|
|
HStack {
|
|
Circle()
|
|
.fill(Color.red)
|
|
.frame(width: 12, height: 12)
|
|
Text("REC")
|
|
.font(.caption.bold())
|
|
.foregroundColor(.red)
|
|
Spacer()
|
|
}
|
|
.padding()
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
// Grid overlay för komposition
|
|
GeometryReader { geometry in
|
|
Path { path in
|
|
let w = geometry.size.width
|
|
let h = geometry.size.height
|
|
|
|
// Vertikala linjer
|
|
path.move(to: CGPoint(x: w/3, y: 0))
|
|
path.addLine(to: CGPoint(x: w/3, y: h))
|
|
path.move(to: CGPoint(x: 2*w/3, y: 0))
|
|
path.addLine(to: CGPoint(x: 2*w/3, y: h))
|
|
|
|
// Horisontella linjer
|
|
path.move(to: CGPoint(x: 0, y: h/3))
|
|
path.addLine(to: CGPoint(x: w, y: h/3))
|
|
path.move(to: CGPoint(x: 0, y: 2*h/3))
|
|
path.addLine(to: CGPoint(x: w, y: 2*h/3))
|
|
}
|
|
.stroke(Color.white.opacity(0.3), lineWidth: 0.5)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - AI Instruction View
|
|
|
|
struct AIInstructionView: View {
|
|
let instruction: AIInstruction
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Image(systemName: "brain.head.profile")
|
|
.foregroundColor(.blue)
|
|
Text("AI Instruction")
|
|
.font(.caption.bold())
|
|
.foregroundColor(.blue)
|
|
Spacer()
|
|
Text(instruction.priority.uppercased())
|
|
.font(.caption2.bold())
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(priorityColor.opacity(0.2))
|
|
.foregroundColor(priorityColor)
|
|
.cornerRadius(4)
|
|
}
|
|
|
|
Text(instruction.text)
|
|
.font(.body)
|
|
|
|
Text(instruction.reason)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding()
|
|
.background(Color(.systemBackground))
|
|
.cornerRadius(12)
|
|
.shadow(radius: 4)
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
var priorityColor: Color {
|
|
switch instruction.priority {
|
|
case "high": return .red
|
|
case "medium": return .orange
|
|
case "low": return .green
|
|
default: return .gray
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Control Panel
|
|
|
|
struct ControlPanelView: View {
|
|
@ObservedObject var captureManager: CaptureManager
|
|
@Binding var selectedMode: CaptureSession.CaptureMode
|
|
|
|
var body: some View {
|
|
VStack(spacing: 16) {
|
|
// Lägesväljare
|
|
Picker("Mode", selection: $selectedMode) {
|
|
Text("Recording").tag(CaptureSession.CaptureMode.recording)
|
|
Text("Guided").tag(CaptureSession.CaptureMode.guided)
|
|
Text("Mission").tag(CaptureSession.CaptureMode.mission)
|
|
Text("Adaptive").tag(CaptureSession.CaptureMode.adaptive)
|
|
}
|
|
.pickerStyle(SegmentedPickerStyle())
|
|
.padding(.horizontal)
|
|
.disabled(captureManager.isRecording)
|
|
|
|
// Inspelningsknapp
|
|
Button(action: {
|
|
if captureManager.isRecording {
|
|
captureManager.stopSession()
|
|
} else {
|
|
let config = SessionConfig(
|
|
videoQuality: .p1080,
|
|
telemetryRate: 10,
|
|
uploadMode: .buffered,
|
|
missionType: selectedMode == .mission ? "crosswalks" : nil,
|
|
area: nil
|
|
)
|
|
captureManager.startSession(mode: selectedMode, config: config)
|
|
}
|
|
}) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(captureManager.isRecording ? Color.red : Color.green)
|
|
.frame(width: 72, height: 72)
|
|
|
|
if captureManager.isRecording {
|
|
RoundedRectangle(cornerRadius: 4)
|
|
.fill(Color.white)
|
|
.frame(width: 24, height: 24)
|
|
} else {
|
|
Circle()
|
|
.fill(Color.white)
|
|
.frame(width: 60, height: 60)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Status-text
|
|
if let session = captureManager.currentSession {
|
|
Text("Mission: \(session.missionId)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical)
|
|
.background(Color(.systemBackground))
|
|
}
|
|
}
|
|
|
|
// MARK: - Settings View
|
|
|
|
struct SettingsView: View {
|
|
@Environment(\.presentationMode) var presentationMode
|
|
|
|
var body: some View {
|
|
NavigationView {
|
|
List {
|
|
Section(header: Text("Video")) {
|
|
Picker("Quality", selection: .constant(1)) {
|
|
Text("720p").tag(0)
|
|
Text("1080p").tag(1)
|
|
Text("4K").tag(2)
|
|
}
|
|
|
|
Toggle("Stabilization", isOn: .constant(true))
|
|
}
|
|
|
|
Section(header: Text("Telemetry")) {
|
|
Picker("Rate", selection: .constant(10)) {
|
|
Text("5 Hz").tag(5)
|
|
Text("10 Hz").tag(10)
|
|
Text("20 Hz").tag(20)
|
|
}
|
|
|
|
Toggle("IMU", isOn: .constant(true))
|
|
Toggle("Compass", isOn: .constant(true))
|
|
Toggle("Altimeter", isOn: .constant(true))
|
|
}
|
|
|
|
Section(header: Text("Upload")) {
|
|
Picker("Mode", selection: .constant(1)) {
|
|
Text("Realtime").tag(0)
|
|
Text("Buffered").tag(1)
|
|
Text("Manual").tag(2)
|
|
}
|
|
|
|
Toggle("WiFi Only", isOn: .constant(true))
|
|
}
|
|
}
|
|
.navigationTitle("Settings")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
Button("Done") {
|
|
presentationMode.wrappedValue.dismiss()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Preview
|
|
|
|
struct ContentView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
ContentView()
|
|
}
|
|
}
|