Files
Project_Velocity/iOS/velocity-ipad/velocity/App/ContentView.swift
sayan eeb684b46c
All checks were successful
Production Readiness / backend-contracts (push) Successful in 1m47s
Production Readiness / webos-typecheck (push) Successful in 1m50s
Production Readiness / ipad-parse (push) Successful in 1m34s
feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: #44
2026-05-03 18:30:38 +05:30

570 lines
21 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import LocalAuthentication
import UIKit
enum AppSection: String, CaseIterable, Hashable, Identifiable {
var id: String { rawValue }
case dashboard = "Dashboard"
case clients = "Clients"
case communications = "Communications"
case calendar = "Calendar"
case inventory = "Inventory"
case settings = "Settings"
var displayTitle: String {
rawValue
}
var dockTitle: String {
switch self {
case .communications:
return "Comms"
default:
return rawValue
}
}
var systemImage: String {
switch self {
case .dashboard: return "square.grid.2x2"
case .clients: return "person.text.rectangle"
case .communications: return "phone.connection"
case .calendar: return "calendar.badge.clock"
case .inventory: return "shippingbox"
case .settings: return "gearshape"
}
}
var accentColor: Color {
switch self {
case .dashboard: return VelocityTheme.accent
case .clients: return Color(red: 0.22, green: 0.78, blue: 0.96)
case .communications: return Color(red: 0.19, green: 0.84, blue: 0.63)
case .calendar: return Color(red: 0.96, green: 0.67, blue: 0.16)
case .inventory: return VelocityTheme.warning
case .settings: return VelocityTheme.mutedFg
}
}
}
struct ContentView: View {
@State private var selectedSection: AppSection = .dashboard
@State private var session = SessionStore.shared
@State private var store = AppStore.shared
@State private var isOraclePresented = false
@State private var dockFocusedSection: AppSection?
@State private var isPrivacyLocked = true
@State private var isAuthenticating = false
@State private var privacyMessage = "Unlock Velocity"
@Environment(\.scenePhase) private var scenePhase
@Namespace private var dockSelectionNamespace
var body: some View {
Group {
if session.isConfigured {
ZStack(alignment: .bottom) {
detailContent
floatingNavigationPill
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
.overlay(alignment: .bottomTrailing) {
OracleFloatingOrb(alertCount: oracleAlertCount) {
isOraclePresented = true
}
.padding(.trailing, 28)
.padding(.bottom, selectedSection == .clients ? 154 : 112)
}
.overlay(alignment: .top) {
OfflineSyncGlow(isActive: hasPendingSync)
}
.overlay(alignment: .top) {
VaultShareToast(
message: store.vaultShareMessage,
error: store.vaultShareError
) {
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.86)) {
store.vaultShareMessage = nil
store.vaultShareError = nil
}
}
.padding(.top, 18)
}
.overlay {
if store.isShowroomModeEnabled, store.errorMessage != nil {
ShowroomAmbientFallback()
.transition(.opacity.animation(.interactiveSpring(response: 0.55, dampingFraction: 0.9)))
}
}
.overlay {
if isPrivacyLocked {
PrivacyLockOverlay(
message: privacyMessage,
isAuthenticating: isAuthenticating
) {
authenticateSession()
}
.transition(.opacity.animation(.interactiveSpring(response: 0.35, dampingFraction: 0.88)))
}
}
.sheet(isPresented: $isOraclePresented) {
OracleConciergeSheet()
.presentationDetents([.medium, .large])
.presentationDragIndicator(.visible)
}
} else {
ConfigurationGateView()
}
}
.onAppear {
authenticateSession()
}
.onChange(of: scenePhase) { _, phase in
switch phase {
case .background, .inactive:
withAnimation(.interactiveSpring(response: 0.35, dampingFraction: 0.9)) {
isPrivacyLocked = true
privacyMessage = "Velocity is locked"
}
case .active:
if isPrivacyLocked {
authenticateSession()
}
@unknown default:
break
}
}
}
private var floatingNavigationPill: some View {
VStack(spacing: 6) {
HStack(alignment: .bottom, spacing: 12) {
ForEach(AppSection.allCases) { section in
let isSelected = selectedSection == section
let isFocused = dockFocusedSection == section
Button {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
withAnimation(.interactiveSpring(response: 0.46, dampingFraction: 0.78)) {
selectedSection = section
dockFocusedSection = section
}
hideDockTooltipAfterTap(for: section)
} label: {
dockItem(for: section, isSelected: isSelected, isFocused: isFocused)
}
.buttonStyle(.plain)
.accessibilityLabel(section.dockTitle)
.accessibilityAddTraits(isSelected ? [.isSelected] : [])
.onHover { hovering in
withAnimation(.interactiveSpring(response: 0.34, dampingFraction: 0.76)) {
dockFocusedSection = hovering ? section : nil
}
}
}
}
.padding(.horizontal, 13)
.padding(.top, 9)
.padding(.bottom, 8)
}
.background(
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(.ultraThinMaterial)
.background(
RoundedRectangle(cornerRadius: 30, style: .continuous)
.fill(Color.black.opacity(0.34))
.blur(radius: 0.5)
)
.shadow(color: Color.black.opacity(0.42), radius: 28, y: 18)
)
.overlay(
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(Color.white.opacity(0.16), lineWidth: 1)
)
.frame(maxWidth: .infinity, alignment: .center)
}
@ViewBuilder
private func dockItem(for section: AppSection, isSelected: Bool, isFocused: Bool) -> some View {
VStack(spacing: 7) {
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(
LinearGradient(
colors: [
section.accentColor.opacity(isSelected ? 0.34 : 0.18),
Color.white.opacity(isSelected ? 0.12 : 0.05),
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(Color.white.opacity(isSelected ? 0.26 : 0.12), lineWidth: 1)
)
.shadow(
color: section.accentColor.opacity(isSelected || isFocused ? 0.32 : 0.10),
radius: isSelected || isFocused ? 16 : 7,
y: isSelected || isFocused ? 8 : 3
)
.if(isSelected) { view in
view.matchedGeometryEffect(id: "selectedDockTile", in: dockSelectionNamespace)
}
Image(systemName: section.systemImage)
.font(.system(size: isFocused ? 24 : (isSelected ? 22 : 19), weight: .semibold))
.foregroundStyle(isSelected ? VelocityTheme.foreground : section.accentColor)
}
.frame(width: 50, height: 50)
.scaleEffect(isFocused ? 1.34 : (isSelected ? 1.16 : 1.0), anchor: .bottom)
.offset(y: isFocused ? -13 : (isSelected ? -5 : 0))
Circle()
.fill(isSelected ? section.accentColor.opacity(0.92) : Color.clear)
.frame(width: 5, height: 5)
.shadow(color: section.accentColor.opacity(isSelected ? 0.45 : 0), radius: 5)
}
.frame(width: 58, height: 70, alignment: .bottom)
.overlay(alignment: .top) {
if isFocused {
dockTooltip(section.dockTitle)
.offset(y: -46)
.transition(
.asymmetric(
insertion: .scale(scale: 0.88, anchor: .bottom).combined(with: .opacity),
removal: .opacity
)
)
}
}
.contentShape(Rectangle())
.animation(.interactiveSpring(response: 0.44, dampingFraction: 0.76), value: isSelected)
.animation(.interactiveSpring(response: 0.32, dampingFraction: 0.70), value: isFocused)
}
private func dockTooltip(_ title: String) -> some View {
VStack(spacing: 0) {
Text(title)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
.lineLimit(1)
.padding(.horizontal, 11)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(Color(red: 0.13, green: 0.13, blue: 0.13).opacity(0.96))
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(Color.white.opacity(0.18), lineWidth: 1)
)
)
Triangle()
.fill(Color(red: 0.13, green: 0.13, blue: 0.13).opacity(0.96))
.frame(width: 12, height: 7)
}
.fixedSize()
.shadow(color: Color.black.opacity(0.42), radius: 10, y: 6)
}
private func hideDockTooltipAfterTap(for section: AppSection) {
Task {
try? await Task.sleep(nanoseconds: 1_200_000_000)
await MainActor.run {
guard dockFocusedSection == section else { return }
withAnimation(.interactiveSpring(response: 0.32, dampingFraction: 0.78)) {
dockFocusedSection = nil
}
}
}
}
// MARK: Detail
private var detailContent: some View {
ZStack {
VelocityTheme.background.ignoresSafeArea()
Group {
switch selectedSection {
case .dashboard:
DashboardView { section in
withAnimation(.interactiveSpring(response: 0.46, dampingFraction: 0.82)) {
selectedSection = section
}
}
case .clients: ClientsView()
case .communications: CommunicationsView()
case .calendar: CalendarView()
case .inventory: InventoryView()
case .settings: SettingsView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private var operatorName: String {
session.operatorIdentity
}
private var operatorInitials: String {
let source = session.operatorIdentity
let parts = source
.replacingOccurrences(of: "@", with: " ")
.split(separator: ".")
.flatMap { $0.split(separator: " ") }
let initials = parts.prefix(2).compactMap(\.first)
return initials.isEmpty ? "VO" : String(initials)
}
private var hasPendingSync: Bool {
!store.isShowroomModeEnabled && (!store.pendingSyncTaskIDs.isEmpty || !store.pendingSyncCalendarEventIDs.isEmpty)
}
private var oracleAlertCount: Int {
guard let alertSnapshot = store.alertSnapshot else { return 0 }
return alertSnapshot.pendingInsights + alertSnapshot.pendingTranscriptions + alertSnapshot.upcomingCalendarEvents24h
}
private func authenticateSession() {
guard session.isConfigured, !isAuthenticating else { return }
isAuthenticating = true
privacyMessage = "Authenticating..."
let context = LAContext()
context.localizedCancelTitle = "Lock"
var error: NSError?
let policy: LAPolicy = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
? .deviceOwnerAuthenticationWithBiometrics
: .deviceOwnerAuthentication
guard context.canEvaluatePolicy(policy, error: &error) else {
isAuthenticating = false
privacyMessage = error?.localizedDescription ?? "Device authentication is unavailable."
return
}
context.evaluatePolicy(policy, localizedReason: "Unlock Project Velocity") { success, authError in
Task { @MainActor in
isAuthenticating = false
withAnimation(.interactiveSpring(response: 0.38, dampingFraction: 0.86)) {
isPrivacyLocked = !success
}
privacyMessage = success
? "Unlocked"
: (authError?.localizedDescription ?? "Authentication failed.")
}
}
}
}
private extension View {
@ViewBuilder
func `if`<Content: View>(_ condition: Bool, transform: (Self) -> Content) -> some View {
if condition {
transform(self)
} else {
self
}
}
}
private struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.closeSubpath()
return path
}
}
private struct OracleFloatingOrb: View {
let alertCount: Int
let action: () -> Void
@State private var isPressed = false
var body: some View {
Button {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
action()
} label: {
ZStack {
Circle()
.fill(.ultraThinMaterial)
.frame(width: 64, height: 64)
.overlay(
Circle()
.stroke(Color(red: 0.13, green: 0.83, blue: 0.93).opacity(0.55), lineWidth: 1)
)
.shadow(color: Color(red: 0.13, green: 0.83, blue: 0.93).opacity(0.35), radius: isPressed ? 10 : 18)
Image(systemName: "sparkles")
.font(.system(size: 24, weight: .semibold))
.foregroundStyle(Color(red: 0.68, green: 0.95, blue: 1.0))
if alertCount > 0 {
Text(alertCount > 99 ? "99+" : "\(alertCount)")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 3)
.background(Capsule().fill(VelocityTheme.danger))
.offset(x: 22, y: -22)
.transition(.scale.combined(with: .opacity))
}
}
}
.buttonStyle(.plain)
.accessibilityLabel("Open Oracle")
.simultaneousGesture(
DragGesture(minimumDistance: 0)
.onChanged { _ in
withAnimation(.interactiveSpring(response: 0.24, dampingFraction: 0.8)) {
isPressed = true
}
}
.onEnded { _ in
withAnimation(.interactiveSpring(response: 0.28, dampingFraction: 0.82)) {
isPressed = false
}
}
)
}
}
private struct OfflineSyncGlow: View {
let isActive: Bool
var body: some View {
Rectangle()
.fill(VelocityTheme.warning.opacity(isActive ? 0.28 : 0))
.frame(height: isActive ? 5 : 0)
.shadow(color: VelocityTheme.warning.opacity(isActive ? 0.65 : 0), radius: 14, y: 5)
.animation(.interactiveSpring(response: 0.42, dampingFraction: 0.9), value: isActive)
.ignoresSafeArea(edges: .top)
}
}
private struct VaultShareToast: View {
let message: String?
let error: String?
let dismiss: () -> Void
var body: some View {
if let text = message ?? error {
HStack(spacing: 10) {
Image(systemName: error == nil ? "link.circle.fill" : "exclamationmark.triangle.fill")
.foregroundStyle(error == nil ? VelocityTheme.success : VelocityTheme.warning)
Text(text)
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(VelocityTheme.foreground)
Button {
dismiss()
} label: {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(VelocityTheme.mutedFg)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 14)
.padding(.vertical, 9)
.background(
Capsule()
.fill(.ultraThinMaterial)
.overlay(Capsule().stroke(Color.white.opacity(0.16), lineWidth: 1))
)
.transition(.move(edge: .top).combined(with: .opacity))
}
}
}
private struct ShowroomAmbientFallback: View {
var body: some View {
ZStack {
LinearGradient(
colors: [
Color.black.opacity(0.88),
Color(red: 0.045, green: 0.055, blue: 0.075).opacity(0.96),
Color.black.opacity(0.92),
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
VStack(spacing: 14) {
Image(systemName: "building.2.crop.circle")
.font(.system(size: 58, weight: .light))
.foregroundStyle(.white.opacity(0.72))
Text("Velocity")
.font(.system(size: 34, weight: .semibold))
.foregroundStyle(.white)
Text("Preparing the showroom view")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white.opacity(0.58))
}
}
}
}
private struct PrivacyLockOverlay: View {
let message: String
let isAuthenticating: Bool
let unlock: () -> Void
var body: some View {
ZStack {
Rectangle()
.fill(.ultraThinMaterial)
.ignoresSafeArea()
Color.black.opacity(0.62)
.ignoresSafeArea()
VStack(spacing: 16) {
Image(systemName: "faceid")
.font(.system(size: 50, weight: .light))
.foregroundStyle(VelocityTheme.foreground)
Text("Velocity")
.font(.system(size: 30, weight: .semibold, design: .default))
.foregroundStyle(VelocityTheme.foreground)
Text(message)
.font(.system(size: 13, weight: .medium))
.foregroundStyle(VelocityTheme.mutedFg)
Button {
unlock()
} label: {
HStack(spacing: 8) {
if isAuthenticating {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
} else {
Image(systemName: "lock.open")
}
Text(isAuthenticating ? "Unlocking" : "Unlock")
}
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, 20)
.padding(.vertical, 11)
.background(Capsule().fill(VelocityTheme.accent))
}
.buttonStyle(.plain)
.disabled(isAuthenticating)
}
.padding(28)
.background(
RoundedRectangle(cornerRadius: 26)
.fill(Color.black.opacity(0.32))
.overlay(
RoundedRectangle(cornerRadius: 26)
.stroke(Color.white.opacity(0.16), lineWidth: 1)
)
)
}
}
}
#Preview {
ContentView()
}