feat: ability to navigate around while still recording (#43)
This commit is contained in:
@@ -10,6 +10,8 @@ import Combine
|
|||||||
/// Manages audio capture from microphone and system audio and handles real-time transcription via OpenAI
|
/// Manages audio capture from microphone and system audio and handles real-time transcription via OpenAI
|
||||||
@MainActor
|
@MainActor
|
||||||
class AudioManager: NSObject, ObservableObject {
|
class AudioManager: NSObject, ObservableObject {
|
||||||
|
static let shared = AudioManager()
|
||||||
|
|
||||||
@Published var transcriptChunks: [TranscriptChunk] = []
|
@Published var transcriptChunks: [TranscriptChunk] = []
|
||||||
@Published var isRecording = false
|
@Published var isRecording = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
@@ -43,7 +45,7 @@ class AudioManager: NSObject, ObservableObject {
|
|||||||
private var pingTimers: [AudioSource: Timer] = [:]
|
private var pingTimers: [AudioSource: Timer] = [:]
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
NotificationCenter.default.addObserver(forName: .AVAudioEngineConfigurationChange,
|
NotificationCenter.default.addObserver(forName: .AVAudioEngineConfigurationChange,
|
||||||
object: audioEngine,
|
object: audioEngine,
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
/// Manages recording sessions at the app level to persist across navigation
|
||||||
|
@MainActor
|
||||||
|
class RecordingSessionManager: ObservableObject {
|
||||||
|
static let shared = RecordingSessionManager()
|
||||||
|
|
||||||
|
@Published var isRecording = false
|
||||||
|
@Published var activeMeetingId: UUID?
|
||||||
|
@Published var errorMessage: String?
|
||||||
|
@Published var activeRecordingTranscriptChunksUpdated: [TranscriptChunk] = []
|
||||||
|
|
||||||
|
private let audioManager = AudioManager.shared
|
||||||
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let transcriptUpdateSubject = PassthroughSubject<[TranscriptChunk], Never>()
|
||||||
|
|
||||||
|
// Store transcript chunks for the active recording session
|
||||||
|
private var activeRecordingTranscriptChunks: [TranscriptChunk] = []
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
setupAudioManagerBindings()
|
||||||
|
setupDebouncedSaving()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupAudioManagerBindings() {
|
||||||
|
// Bind to audio manager state
|
||||||
|
audioManager.$isRecording
|
||||||
|
.sink { [weak self] isRecording in
|
||||||
|
self?.isRecording = isRecording
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
audioManager.$errorMessage
|
||||||
|
.sink { [weak self] errorMessage in
|
||||||
|
self?.errorMessage = errorMessage
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// When transcript chunks change, store them for the active recording and send to debouncer
|
||||||
|
audioManager.$transcriptChunks
|
||||||
|
.sink { [weak self] newChunks in
|
||||||
|
guard let self = self, self.isRecording, self.activeMeetingId != nil else { return }
|
||||||
|
self.activeRecordingTranscriptChunks = newChunks
|
||||||
|
self.activeRecordingTranscriptChunksUpdated = newChunks
|
||||||
|
|
||||||
|
self.transcriptUpdateSubject.send(newChunks)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupDebouncedSaving() {
|
||||||
|
transcriptUpdateSubject
|
||||||
|
.debounce(for: .seconds(2), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] chunks in
|
||||||
|
guard let self = self, let activeMeetingId = self.activeMeetingId else { return }
|
||||||
|
print("💾 Debounced save triggered for meeting: \(activeMeetingId.uuidString)")
|
||||||
|
self.updateActiveMeetingTranscript(meetingId: activeMeetingId, chunks: chunks)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRecording(for meetingId: UUID) {
|
||||||
|
print("🎙️ Starting recording for meeting: \(meetingId)")
|
||||||
|
|
||||||
|
// Load the meeting to get existing transcript chunks
|
||||||
|
if let existingMeeting = LocalStorageManager.shared.loadMeetings().first(where: { $0.id == meetingId }) {
|
||||||
|
activeRecordingTranscriptChunks = existingMeeting.transcriptChunks
|
||||||
|
// Seed the audio manager with existing chunks
|
||||||
|
audioManager.transcriptChunks = existingMeeting.transcriptChunks
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMeetingId = meetingId
|
||||||
|
audioManager.startRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording() {
|
||||||
|
print("🛑 Stopping recording for meeting: \(activeMeetingId?.uuidString ?? "unknown")")
|
||||||
|
|
||||||
|
audioManager.stopRecording()
|
||||||
|
|
||||||
|
// Perform a final, immediate save of transcript chunks to the meeting
|
||||||
|
if let activeMeetingId = activeMeetingId {
|
||||||
|
updateActiveMeetingTranscript(meetingId: activeMeetingId, chunks: activeRecordingTranscriptChunks)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeMeetingId = nil
|
||||||
|
activeRecordingTranscriptChunks = []
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRecordingMeeting(_ meetingId: UUID) -> Bool {
|
||||||
|
return isRecording && activeMeetingId == meetingId
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateActiveMeetingTranscript(meetingId: UUID, chunks: [TranscriptChunk]) {
|
||||||
|
// Load all meetings
|
||||||
|
var meetings = LocalStorageManager.shared.loadMeetings()
|
||||||
|
|
||||||
|
// Find and update the active meeting
|
||||||
|
if let index = meetings.firstIndex(where: { $0.id == meetingId }) {
|
||||||
|
meetings[index].transcriptChunks = chunks
|
||||||
|
|
||||||
|
// Save the updated meeting
|
||||||
|
let success = LocalStorageManager.shared.saveMeeting(meetings[index])
|
||||||
|
if success {
|
||||||
|
print("✅ Saved meeting transcript: \(meetingId.uuidString)")
|
||||||
|
NotificationCenter.default.post(name: .meetingSaved, object: meetings[index])
|
||||||
|
} else {
|
||||||
|
print("❌ Failed to save meeting transcript: \(meetingId.uuidString)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getActiveRecordingTranscriptChunks() -> [TranscriptChunk] {
|
||||||
|
return activeRecordingTranscriptChunks
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get transcript chunks for a specific meeting, ensuring proper data separation
|
||||||
|
func getTranscriptChunks(for meetingId: UUID) -> [TranscriptChunk] {
|
||||||
|
if isRecording && activeMeetingId == meetingId {
|
||||||
|
// Return live transcript chunks for the active recording
|
||||||
|
return activeRecordingTranscriptChunks
|
||||||
|
} else {
|
||||||
|
// Load saved transcript chunks from storage for non-active meetings
|
||||||
|
if let savedMeeting = LocalStorageManager.shared.loadMeetings().first(where: { $0.id == meetingId }) {
|
||||||
|
return savedMeeting.transcriptChunks
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ class MeetingListViewModel: ObservableObject {
|
|||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
private let recordingSessionManager = RecordingSessionManager.shared
|
||||||
|
|
||||||
// Computed property to filter meetings based on search text
|
// Computed property to filter meetings based on search text
|
||||||
var filteredMeetings: [Meeting] {
|
var filteredMeetings: [Meeting] {
|
||||||
|
|||||||
@@ -15,24 +15,26 @@ enum MeetingViewTab: String, CaseIterable {
|
|||||||
case enhancedNotes = "Enhanced Notes"
|
case enhancedNotes = "Enhanced Notes"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RecordingState {
|
|
||||||
case idle // Not recording, shows "Transcribe" or "Resume" based on transcript content
|
|
||||||
case recording // Recording, shows "Stop"
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MeetingViewModel: ObservableObject {
|
class MeetingViewModel: ObservableObject {
|
||||||
@Published var meeting: Meeting
|
@Published var meeting: Meeting
|
||||||
@Published var isGeneratingNotes = false
|
@Published var isGeneratingNotes = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
@Published var isRecording = false
|
@Published private var recordingStateChanged = false // Trigger SwiftUI updates
|
||||||
|
|
||||||
|
// Computed property that always uses the direct RecordingSessionManager check
|
||||||
|
var isRecording: Bool {
|
||||||
|
return recordingSessionManager.isRecordingMeeting(meeting.id)
|
||||||
|
}
|
||||||
@Published var selectedTab: MeetingViewTab = .transcript // Default to transcript tab
|
@Published var selectedTab: MeetingViewTab = .transcript // Default to transcript tab
|
||||||
@Published var recordingState: RecordingState = .idle
|
|
||||||
@Published var isDeleted = false
|
@Published var isDeleted = false
|
||||||
@Published var templates: [NoteTemplate] = []
|
@Published var templates: [NoteTemplate] = []
|
||||||
@Published var selectedTemplateId: UUID?
|
@Published var selectedTemplateId: UUID?
|
||||||
|
|
||||||
private let audioManager = AudioManager()
|
private let recordingSessionManager = RecordingSessionManager.shared
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private var isNewMeeting = false
|
private var isNewMeeting = false
|
||||||
|
|
||||||
@@ -54,6 +56,8 @@ class MeetingViewModel: ObservableObject {
|
|||||||
self.meeting = meeting
|
self.meeting = meeting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Detect if this is a new meeting based on content, not storage existence
|
// Detect if this is a new meeting based on content, not storage existence
|
||||||
isNewMeeting = isEmpty
|
isNewMeeting = isEmpty
|
||||||
|
|
||||||
@@ -78,31 +82,38 @@ class MeetingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// NEW: Seed the audio manager with any existing transcript chunks so the initial
|
// Trigger SwiftUI updates when recording state changes
|
||||||
// published value doesn't overwrite the saved transcript with an empty array.
|
Publishers.CombineLatest(recordingSessionManager.$isRecording, recordingSessionManager.$activeMeetingId)
|
||||||
audioManager.transcriptChunks = self.meeting.transcriptChunks
|
.sink { [weak self] (isRecording, activeMeetingId) in
|
||||||
|
guard let self = self else { return }
|
||||||
// Update meeting transcript chunks when audio manager transcript chunks change
|
// Toggle the dummy property to trigger SwiftUI re-render
|
||||||
audioManager.$transcriptChunks
|
self.recordingStateChanged.toggle()
|
||||||
.sink { [weak self] newChunks in
|
|
||||||
self?.meeting.transcriptChunks = newChunks
|
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// Update isRecording when audio manager recording state changes
|
// Update error message when recording session manager encounters errors
|
||||||
audioManager.$isRecording
|
recordingSessionManager.$errorMessage
|
||||||
.sink { [weak self] isRecording in
|
|
||||||
self?.isRecording = isRecording
|
|
||||||
self?.updateRecordingState()
|
|
||||||
}
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
// Update error message when audio manager encounters errors
|
|
||||||
audioManager.$errorMessage
|
|
||||||
.compactMap { $0 }
|
.compactMap { $0 }
|
||||||
.sink { [weak self] errorMessage in
|
.sink { [weak self] errorMessage in
|
||||||
self?.errorMessage = errorMessage
|
self?.errorMessage = errorMessage
|
||||||
print("🚨 Audio Manager Error: \(errorMessage)")
|
print("🚨 Recording Session Manager Error: \(errorMessage)")
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// If currently recording this meeting, load live transcript chunks
|
||||||
|
if recordingSessionManager.isRecordingMeeting(meeting.id) {
|
||||||
|
self.meeting.transcriptChunks = recordingSessionManager.getTranscriptChunks(for: meeting.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen to real-time transcript updates for this meeting if it's being recorded
|
||||||
|
recordingSessionManager.$activeRecordingTranscriptChunksUpdated
|
||||||
|
.dropFirst()
|
||||||
|
.sink { [weak self] updatedChunks in
|
||||||
|
guard let self = self else { return }
|
||||||
|
// Only update if this meeting is the active recording
|
||||||
|
if recordingSessionManager.isRecordingMeeting(self.meeting.id) {
|
||||||
|
self.meeting.transcriptChunks = updatedChunks
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
@@ -117,37 +128,26 @@ class MeetingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
// Auto-start recording for empty meetings
|
|
||||||
if isNewMeeting {
|
|
||||||
print("🚀 Auto-starting recording for empty meeting")
|
|
||||||
startRecording()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateRecordingState() {
|
|
||||||
if isRecording {
|
|
||||||
recordingState = .recording
|
|
||||||
} else {
|
|
||||||
recordingState = .idle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var recordingButtonText: String {
|
var recordingButtonText: String {
|
||||||
switch recordingState {
|
// Use the same computed isRecording property for perfect consistency
|
||||||
case .idle:
|
if isRecording {
|
||||||
|
return "Stop"
|
||||||
|
} else {
|
||||||
// Check if there's existing transcript content
|
// Check if there's existing transcript content
|
||||||
return meeting.transcriptChunks.isEmpty ? "Transcribe" : "Resume"
|
return meeting.transcriptChunks.isEmpty ? "Transcribe" : "Resume"
|
||||||
case .recording:
|
|
||||||
return "Stop"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleRecording() {
|
func toggleRecording() {
|
||||||
switch recordingState {
|
// Use the same computed isRecording property for perfect consistency
|
||||||
case .idle:
|
if isRecording {
|
||||||
startRecording()
|
|
||||||
case .recording:
|
|
||||||
stopRecording()
|
stopRecording()
|
||||||
|
} else {
|
||||||
|
startRecording()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ class MeetingViewModel: ObservableObject {
|
|||||||
switch validationResult {
|
switch validationResult {
|
||||||
case .success():
|
case .success():
|
||||||
// Key is valid, proceed with recording
|
// Key is valid, proceed with recording
|
||||||
audioManager.startRecording()
|
recordingSessionManager.startRecording(for: meeting.id)
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// Show error message
|
// Show error message
|
||||||
errorMessage = error.localizedDescription
|
errorMessage = error.localizedDescription
|
||||||
@@ -169,7 +169,7 @@ class MeetingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func stopRecording() {
|
func stopRecording() {
|
||||||
audioManager.stopRecording()
|
recordingSessionManager.stopRecording()
|
||||||
saveMeeting()
|
saveMeeting()
|
||||||
|
|
||||||
// Auto-generate notes if there's a transcript and no existing notes
|
// Auto-generate notes if there's a transcript and no existing notes
|
||||||
@@ -198,9 +198,6 @@ class MeetingViewModel: ObservableObject {
|
|||||||
isGeneratingNotes = true
|
isGeneratingNotes = true
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
|
|
||||||
// Clear any audio manager errors as well
|
|
||||||
audioManager.errorMessage = nil
|
|
||||||
|
|
||||||
// Clear existing notes for streaming
|
// Clear existing notes for streaming
|
||||||
meeting.generatedNotes = ""
|
meeting.generatedNotes = ""
|
||||||
|
|
||||||
@@ -287,7 +284,7 @@ class MeetingViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func deleteIfEmpty() {
|
func deleteIfEmpty() {
|
||||||
if isEmpty {
|
if isEmpty && !isRecording {
|
||||||
print("🗑️ Auto-deleting empty meeting")
|
print("🗑️ Auto-deleting empty meeting")
|
||||||
deleteMeeting()
|
deleteMeeting()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ struct DancingAudioBars: View {
|
|||||||
|
|
||||||
private func getDancingBarHeight(index: Int, level: Float) -> CGFloat {
|
private func getDancingBarHeight(index: Int, level: Float) -> CGFloat {
|
||||||
let baseHeight: CGFloat = 4 // Flat when no audio
|
let baseHeight: CGFloat = 4 // Flat when no audio
|
||||||
let maxHeight: CGFloat = 32 // Max dancing height
|
let maxHeight: CGFloat = 24 // Max dancing height
|
||||||
|
|
||||||
if level > 0.03 { // Only dance when there's actual audio
|
if level > 0.03 { // Only dance when there's actual audio
|
||||||
// Each bar responds to the same audio level but with different scaling
|
// Each bar responds to the same audio level but with different scaling
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
struct MeetingListView: View {
|
struct MeetingListView: View {
|
||||||
@StateObject private var viewModel = MeetingListViewModel()
|
@StateObject private var viewModel = MeetingListViewModel()
|
||||||
@ObservedObject var settingsViewModel: SettingsViewModel
|
@ObservedObject var settingsViewModel: SettingsViewModel
|
||||||
|
@StateObject private var recordingSessionManager = RecordingSessionManager.shared
|
||||||
@State private var selectedMeeting: Meeting?
|
@State private var selectedMeeting: Meeting?
|
||||||
@State private var navigationPath = NavigationPath()
|
@State private var navigationPath = NavigationPath()
|
||||||
|
|
||||||
@@ -113,7 +114,8 @@ struct MeetingListView: View {
|
|||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus")
|
Image(systemName: "plus")
|
||||||
}
|
}
|
||||||
.help("New Meeting")
|
.disabled(recordingSessionManager.isRecording)
|
||||||
|
.help(recordingSessionManager.isRecording ? "Cannot create new meeting while recording is active" : "New Meeting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationDestination(for: String.self) { path in
|
.navigationDestination(for: String.self) { path in
|
||||||
@@ -160,14 +162,21 @@ struct DayGroup {
|
|||||||
|
|
||||||
struct MeetingRowView: View {
|
struct MeetingRowView: View {
|
||||||
let meeting: Meeting
|
let meeting: Meeting
|
||||||
|
@StateObject private var recordingSessionManager = RecordingSessionManager.shared
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
// Title or default
|
// Title or default
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
if recordingSessionManager.isRecordingMeeting(meeting.id) {
|
||||||
|
Image(systemName: "record.circle")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.headline)
|
||||||
|
}
|
||||||
Text(meeting.title.isEmpty ? "Untitled meeting" : meeting.title)
|
Text(meeting.title.isEmpty ? "Untitled meeting" : meeting.title)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
}
|
||||||
// Date
|
// Date
|
||||||
HStack {
|
HStack {
|
||||||
Text(meeting.date, style: .time)
|
Text(meeting.date, style: .time)
|
||||||
@@ -213,6 +222,7 @@ struct CollapsedTranscriptChunkView: View {
|
|||||||
|
|
||||||
struct MeetingDetailContentView: View {
|
struct MeetingDetailContentView: View {
|
||||||
@StateObject private var viewModel: MeetingViewModel
|
@StateObject private var viewModel: MeetingViewModel
|
||||||
|
@StateObject private var recordingSessionManager = RecordingSessionManager.shared
|
||||||
@State private var showDeleteAlert = false
|
@State private var showDeleteAlert = false
|
||||||
@State private var isEditing = false
|
@State private var isEditing = false
|
||||||
@State private var showCopyConfirmation = false
|
@State private var showCopyConfirmation = false
|
||||||
@@ -223,6 +233,12 @@ struct MeetingDetailContentView: View {
|
|||||||
self.onDelete = onDelete
|
self.onDelete = onDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Computed property to determine if recording button should be disabled
|
||||||
|
private var cannotStartRecording: Bool {
|
||||||
|
// Disable if another meeting is recording (not this one)
|
||||||
|
return recordingSessionManager.isRecording && !recordingSessionManager.isRecordingMeeting(viewModel.meeting.id)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 20) {
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
// Meeting Title with Menu
|
// Meeting Title with Menu
|
||||||
@@ -272,15 +288,17 @@ struct MeetingDetailContentView: View {
|
|||||||
viewModel.toggleRecording()
|
viewModel.toggleRecording()
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Image(systemName: viewModel.recordingState == .recording ? "stop.circle.fill" : "record.circle")
|
Image(systemName: viewModel.isRecording ? "stop.circle.fill" : "record.circle")
|
||||||
.foregroundColor(viewModel.recordingState == .recording ? .red : .accentColor)
|
.foregroundColor(viewModel.isRecording ? .red : .accentColor)
|
||||||
Text(viewModel.recordingButtonText)
|
Text(viewModel.recordingButtonText)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 110, minHeight: 36)
|
.frame(minWidth: 110, minHeight: 36)
|
||||||
.background(viewModel.recordingState == .recording ? Color.red.opacity(0.1) : Color.accentColor.opacity(0.1))
|
.background(viewModel.isRecording ? Color.red.opacity(0.1) : Color.accentColor.opacity(0.1))
|
||||||
.cornerRadius(8)
|
.cornerRadius(8)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.disabled(cannotStartRecording)
|
||||||
|
.help(cannotStartRecording ? "Another meeting is currently being recorded" : "Start or stop recording for this meeting")
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.copyCurrentTabContent()
|
viewModel.copyCurrentTabContent()
|
||||||
@@ -336,12 +354,6 @@ struct MeetingDetailContentView: View {
|
|||||||
Text("Are you sure you want to delete this meeting? This action cannot be undone.")
|
Text("Are you sure you want to delete this meeting? This action cannot be undone.")
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// Stop recording if it's in progress when leaving the page
|
|
||||||
if viewModel.isRecording {
|
|
||||||
print("🛑 Stopping recording because user is leaving the page")
|
|
||||||
viewModel.stopRecording()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-delete empty meetings when leaving, otherwise save
|
// Auto-delete empty meetings when leaving, otherwise save
|
||||||
viewModel.deleteIfEmpty()
|
viewModel.deleteIfEmpty()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user