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.
240 lines
8.2 KiB
Swift
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())
|
|
}
|