145 lines
4.7 KiB
Swift
145 lines
4.7 KiB
Swift
import SwiftUI
|
|
|
|
/// The main presence screen.
|
|
///
|
|
/// This is the emotional core of the app. A large, tactile "I am here"
|
|
/// toggle that makes being present feel like an intentional act — not a
|
|
/// passive background process. Below it, the live feed of silhouettes.
|
|
struct PresenceView: View {
|
|
@EnvironmentObject private var vm: PresenceViewModel
|
|
@EnvironmentObject private var revealFlow: RevealFlow
|
|
@State private var showReveal = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
presenceToggle
|
|
radiusPicker
|
|
encounterFeed
|
|
}
|
|
.padding()
|
|
}
|
|
.navigationTitle("Proximity")
|
|
.toolbar {
|
|
#if DEBUG
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
NavigationLink {
|
|
DebugMenu()
|
|
} label: {
|
|
Image(systemName: "hammer")
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
.onChange(of: revealFlow.phase) { _, phase in
|
|
if case .mutual = phase {
|
|
showReveal = true
|
|
}
|
|
}
|
|
.fullScreenCover(isPresented: $showReveal) {
|
|
RevealView()
|
|
.environmentObject(revealFlow)
|
|
}
|
|
}
|
|
|
|
// MARK: - The presence toggle
|
|
|
|
private var presenceToggle: some View {
|
|
Button(action: vm.togglePresence) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(vm.isPresent ? Color.green : Color.gray.opacity(0.2))
|
|
.frame(width: 200, height: 200)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(vm.isPresent ? Color.green : Color.gray,
|
|
lineWidth: 4)
|
|
)
|
|
|
|
VStack(spacing: 8) {
|
|
Image(systemName: vm.isPresent
|
|
? "dot.radiowaves.left.and.right"
|
|
: "person.crop.circle.dashed")
|
|
.font(.system(size: 56))
|
|
Text(vm.isPresent ? "I'm Here" : "Tap to be Present")
|
|
.font(.headline)
|
|
}
|
|
.foregroundColor(vm.isPresent ? .white : .secondary)
|
|
}
|
|
.scaleEffect(vm.isPresent ? 1.0 : 0.98)
|
|
.animation(.spring(response: 0.4, dampingFraction: 0.6),
|
|
value: vm.isPresent)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.accessibilityLabel(vm.isPresent ? "You are present. Tap to go offline."
|
|
: "Go present")
|
|
}
|
|
|
|
// MARK: - Radius picker
|
|
|
|
private var radiusPicker: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("How far are you open to?")
|
|
.font(.subheadline)
|
|
.foregroundColor(.secondary)
|
|
|
|
Picker("Radius", selection: $vm.radiusTier) {
|
|
ForEach(DistanceTier.allCases) { tier in
|
|
Text(tier.title).tag(tier)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
|
|
Text(vm.radiusTier.subtitle)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
// MARK: - Encounter feed
|
|
|
|
private var encounterFeed: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Text("People you've passed")
|
|
.font(.headline)
|
|
Spacer()
|
|
if let last = vm.lastScan {
|
|
Text("last scan \(last, style: .time)")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
if vm.encounters.isEmpty {
|
|
emptyState
|
|
} else {
|
|
ForEach(vm.encounters) { encounter in
|
|
SilhouetteRow(encounter: encounter) {
|
|
// Tap a silhouette → consider it → wave.
|
|
revealFlow.consider(encounter)
|
|
Task { await revealFlow.wave() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var emptyState: some View {
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "eye.slash")
|
|
.font(.system(size: 40))
|
|
.foregroundColor(.secondary)
|
|
Text(vm.isPresent
|
|
? "Looking for people near you…"
|
|
: "Go present to start seeing people.")
|
|
.multilineTextAlignment(.center)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 40)
|
|
}
|
|
}
|