Files
BeamScribe/BeamScribe/Views/GuestBrowserView.swift
jared ce40831933 Initial commit
Add BeamScribe iOS app for real-time transcription with multipeer connectivity.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 22:03:21 -05:00

235 lines
8.2 KiB
Swift

//
// GuestBrowserView.swift
// BeamScribe
//
// Displays list of nearby hosts for guests to join.
//
import SwiftUI
import MultipeerConnectivity
struct GuestBrowserView: View {
@EnvironmentObject var sessionState: SessionState
@ObservedObject var multipeerManager: MultipeerManager
@ObservedObject var fileManager: FileStorageManager
@State private var isConnecting: Bool = false
@State private var selectedHost: MCPeerID?
@State private var connectionFailed: Bool = false
var body: some View {
ZStack {
// Background
LinearGradient(
colors: [Color.purple.opacity(0.05), Color.blue.opacity(0.05)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.ignoresSafeArea()
VStack(spacing: 24) {
// Header
VStack(spacing: 8) {
Image(systemName: "antenna.radiowaves.left.and.right")
.font(.system(size: 50))
.foregroundColor(.purple)
.symbolEffect(.variableColor.iterative.reversing, options: .repeating)
Text("Finding Sessions...")
.font(.title)
.fontWeight(.bold)
Text("Looking for nearby hosts")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top, 40)
// Connection Failed Alert
if connectionFailed {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text("Connection failed. Tap to try again.")
.font(.callout)
.foregroundColor(.primary)
}
.padding()
.background(Color.orange.opacity(0.15))
.cornerRadius(12)
.padding(.horizontal, 20)
}
// Host List
if multipeerManager.availableHosts.isEmpty {
Spacer()
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.5)
Text("Searching for active sessions")
.font(.callout)
.foregroundColor(.secondary)
Text("Make sure the host has started transcribing")
.font(.caption)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
Spacer()
} else {
ScrollView {
LazyVStack(spacing: 12) {
ForEach(Array(multipeerManager.availableHosts.keys), id: \.self) { peerID in
HostRow(
eventName: multipeerManager.availableHosts[peerID] ?? "Unknown",
deviceName: peerID.displayName,
isConnecting: isConnecting && selectedHost == peerID,
statusMessage: selectedHost == peerID ? multipeerManager.connectionStatus : ""
) {
joinHost(peerID)
}
}
}
.padding(.horizontal, 20)
}
}
// Cancel Button
Button(action: {
// Cancel any pending retries
if let host = selectedHost {
multipeerManager.cancelRetry(for: host.displayName)
}
multipeerManager.stopBrowsing()
withAnimation {
sessionState.appPhase = .roleSelection
}
}) {
Text("Cancel")
.foregroundColor(.secondary)
.padding(.vertical, 16)
}
.padding(.bottom, 20)
}
}
.onAppear {
multipeerManager.startBrowsing()
setupConnectionFailedHandler()
}
.onDisappear {
if sessionState.appPhase != .activeSession {
multipeerManager.stopBrowsing()
}
}
.onChange(of: multipeerManager.isConnectedToHost) { oldValue, newValue in
if newValue, let host = selectedHost,
let eventName = multipeerManager.availableHosts[host] {
// Connected successfully
// Note: We do NOT call stopBrowsing() here anymore because it kills the session.
// The MultipeerManager handles the radio shutdown safely after stabilization.
// Create a transcript file for the guest immediately
_ = try? fileManager.createTranscriptFile(eventName: eventName)
sessionState.joinGuestSession(eventName: eventName)
withAnimation {
sessionState.appPhase = .activeSession
}
}
}
}
private func joinHost(_ peerID: MCPeerID) {
selectedHost = peerID
isConnecting = true
connectionFailed = false
multipeerManager.joinHost(peerID)
}
private func setupConnectionFailedHandler() {
multipeerManager.onConnectionFailed = { [self] failedPeer in
Task { @MainActor in
if failedPeer == selectedHost {
isConnecting = false
connectionFailed = true
}
}
}
}
}
// MARK: - Host Row
struct HostRow: View {
let eventName: String
let deviceName: String
let isConnecting: Bool
let statusMessage: String
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 16) {
// Icon
ZStack {
Circle()
.fill(Color.purple.opacity(0.15))
.frame(width: 50, height: 50)
Image(systemName: "waveform")
.font(.title2)
.foregroundColor(.purple)
}
// Event Info
VStack(alignment: .leading, spacing: 4) {
Text(eventName)
.font(.headline)
.foregroundColor(.primary)
Text(deviceName)
.font(.caption)
.foregroundColor(.secondary)
// Show connection status when connecting
if isConnecting && !statusMessage.isEmpty {
Text(statusMessage)
.font(.caption2)
.foregroundColor(.orange)
.transition(.opacity)
}
}
Spacer()
// Join Indicator
if isConnecting {
ProgressView()
} else {
Image(systemName: "arrow.right.circle.fill")
.font(.title2)
.foregroundColor(.purple)
}
}
.padding(16)
.background(Color(.systemBackground))
.cornerRadius(16)
.shadow(color: .black.opacity(0.05), radius: 5, x: 0, y: 2)
}
.disabled(isConnecting)
}
}
#Preview {
GuestBrowserView(
multipeerManager: MultipeerManager(),
fileManager: FileStorageManager()
)
.environmentObject(SessionState())
}