Files
BeamScribe/BeamScribe/Views/SettingsView.swift
jared 0c1e3d6fff Refactor networking to use Network.framework
Replace MultipeerConnectivity with a custom P2P implementation using Network.framework (NWListener/NWConnection) and CoreBluetooth for discovery.
- Add P2PConnectionManager, BLEDiscoveryManager, and NetworkFraming.
- Add ConnectedPeer and DiscoveredHost models.
- Update Info.plist with local network and bluetooth permissions.
- Refactor Views to use P2PConnectionManager.
- Add implementation plan and transition docs.
2026-01-20 23:48:14 -05:00

240 lines
8.2 KiB
Swift

//
// SettingsView.swift
// BeamScribe
//
// Settings sheet with Keep Awake toggle, export, and end session.
//
import SwiftUI
import UIKit
struct SettingsView: View {
@EnvironmentObject var sessionState: SessionState
@ObservedObject var fileManager: FileStorageManager
@ObservedObject var p2pManager: P2PConnectionManager
@ObservedObject var transcriptionManager: TranscriptionManager
@EnvironmentObject var settings: SettingsModel
@Environment(\.dismiss) private var dismiss
@State private var showingExportSheet = false
@State private var exportURL: URL?
@State private var showingEndConfirmation = false
@State private var exportError: String?
var body: some View {
NavigationView {
List {
// Session Info
Section {
HStack {
Label("Event", systemImage: "calendar")
Spacer()
Text(sessionState.eventName)
.foregroundColor(.secondary)
}
HStack {
Label("Role", systemImage: "person.fill")
Spacer()
if sessionState.isSoloMode {
Text("Solo (Local Only)")
.foregroundColor(.secondary)
} else {
Text(sessionState.userRole == .host ? "Host (Broadcasting)" : "Guest (Listening)")
.foregroundColor(.secondary)
}
}
if sessionState.userRole == .host && !sessionState.isSoloMode {
HStack {
Label("Listeners", systemImage: "person.2.fill")
Spacer()
Text("\(p2pManager.connectedPeers.count)")
.foregroundColor(.secondary)
}
}
} header: {
Text("Session")
}
// Settings
if sessionState.userRole == .host {
Section {
Toggle(isOn: $sessionState.keepAwakeEnabled) {
Label("Keep Screen Awake", systemImage: "sun.max.fill")
}
.onChange(of: sessionState.keepAwakeEnabled) { oldValue, newValue in
UIApplication.shared.isIdleTimerDisabled = newValue
}
} header: {
Text("Display")
} footer: {
Text("Prevents the screen from dimming while transcribing.")
}
}
// Appearance
Section {
Picker("Theme", selection: $settings.selectedTheme) {
ForEach(AppTheme.allCases) { theme in
Text(theme.rawValue).tag(theme)
}
}
Picker("Font", selection: $settings.selectedFont) {
ForEach(AppFont.allCases) { font in
Text(font.rawValue).tag(font)
}
}
VStack(alignment: .leading) {
HStack {
Text("Font Size")
Spacer()
Text("\(Int(settings.fontSize)) pts")
.foregroundColor(.secondary)
}
Slider(value: $settings.fontSize, in: 12...48, step: 1)
Text("Preview Text")
.font(settings.font())
.frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 4)
.id("fontPreview") // Force redraw if needed
}
} header: {
Text("Appearance")
}
// Export
Section {
Button(action: exportPDF) {
Label("Export as PDF", systemImage: "arrow.up.doc.fill")
}
if let error = exportError {
Text(error)
.font(.caption)
.foregroundColor(.red)
}
} header: {
Text("Export")
} footer: {
Text("Creates a PDF with the transcript so far.")
}
// End Session
Section {
Button(action: {
showingEndConfirmation = true
}) {
Label("End Session", systemImage: "stop.circle.fill")
.foregroundColor(.red)
}
} footer: {
Text("Returns to the home screen. Your transcript is saved locally.")
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
dismiss()
}
}
}
}
.sheet(isPresented: $showingExportSheet) {
if let url = exportURL {
ShareSheet(activityItems: [url])
}
}
.alert("End Session?", isPresented: $showingEndConfirmation) {
Button("Cancel", role: .cancel) { }
Button("End Session", role: .destructive) {
endSession()
}
} message: {
Text("Your transcript will be saved locally. You can start a new session anytime.")
}
.onAppear {
if sessionState.keepAwakeEnabled {
UIApplication.shared.isIdleTimerDisabled = true
}
}
}
private func exportPDF() {
do {
let url = try fileManager.exportToPDF(
eventName: sessionState.eventName,
content: sessionState.fullTranscriptText
)
exportURL = url
showingExportSheet = true
exportError = nil
} catch {
exportError = "Failed to export: \(error.localizedDescription)"
}
}
private func endSession() {
// Stop transcription (Host)
if sessionState.userRole == .host {
transcriptionManager.stopTranscribing()
}
// Save pending partial text (Guest)
if sessionState.userRole == .guest {
let pendingText = sessionState.transcriptSegments
.filter { !$0.isFinal }
.map { $0.text }
.joined(separator: " ")
if !pendingText.isEmpty {
fileManager.appendText(pendingText)
}
}
// Disconnect networking
p2pManager.disconnect()
// Clear session info
fileManager.clearSessionInfo()
// Reset state
sessionState.reset()
// Re-enable idle timer
UIApplication.shared.isIdleTimerDisabled = false
dismiss()
}
}
// MARK: - Share Sheet
struct ShareSheet: UIViewControllerRepresentable {
let activityItems: [Any]
func makeUIViewController(context: Context) -> UIActivityViewController {
UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {}
}
#Preview {
SettingsView(
fileManager: FileStorageManager(),
p2pManager: P2PConnectionManager(),
transcriptionManager: TranscriptionManager()
)
.environmentObject(SessionState())
}