@@ -10,37 +10,43 @@ struct AudioPriorityBarApp: App {
1010 MenuBarView ( )
1111 . environmentObject ( audioManager)
1212 } label: {
13- MenuBarLabel ( )
14- . environmentObject ( audioManager)
13+ MenuBarLabel (
14+ volume: audioManager. volume,
15+ isOutputMuted: audioManager. isActiveOutputMuted,
16+ isInputMuted: audioManager. isActiveInputMuted,
17+ isCustomMode: audioManager. isCustomMode,
18+ mode: audioManager. currentMode,
19+ micFlash: audioManager. micFlashState
20+ )
1521 }
1622 . menuBarExtraStyle ( . window)
1723 }
1824}
1925
2026struct MenuBarLabel : View {
21- @EnvironmentObject var audioManager : AudioManager
22-
23- var iconName : String {
24- if audioManager. isActiveOutputMuted {
25- return " speaker.slash.fill "
26- } else if audioManager. isCustomMode {
27- return " hand.raised.fill "
28- } else if audioManager. currentMode == . headphone {
29- return " headphones "
30- } else {
31- return " speaker.fill "
32- }
33- }
27+ let volume : Float
28+ let isOutputMuted : Bool
29+ let isInputMuted : Bool
30+ let isCustomMode : Bool
31+ let mode : OutputCategory
32+ let micFlash : Bool
3433
3534 var body : some View {
36- HStack ( spacing: 3 ) {
37- Image ( systemName: iconName)
38- . font ( . system( size: 12 ) )
39- . foregroundColor ( audioManager. isActiveOutputMuted ? . red : nil )
40-
41- // Volume meter bars
42- VolumeMeterView ( volume: audioManager. volume, isMuted: audioManager. isActiveOutputMuted)
43- . frame ( width: 16 , height: 11 )
35+ HStack ( spacing: 2 ) {
36+ if isInputMuted {
37+ Image ( systemName: micFlash ? " mic.fill " : " mic.slash.fill " )
38+ }
39+ if isCustomMode {
40+ Image ( systemName: " hand.raised.fill " )
41+ } else if mode == . headphone {
42+ Image ( systemName: " headphones " )
43+ }
44+ // Speaker with volume always last
45+ if isOutputMuted {
46+ Image ( systemName: " speaker.slash.fill " )
47+ } else {
48+ Image ( systemName: " speaker.wave.3.fill " , variableValue: Double ( volume) )
49+ }
4450 }
4551 }
4652}
@@ -92,11 +98,12 @@ class AudioManager: ObservableObject {
9298 @Published var mutedDeviceIds : Set < AudioObjectID > = [ ]
9399 @Published var isActiveOutputMuted : Bool = false
94100 @Published var isActiveInputMuted : Bool = false
101+ @Published var micFlashState : Bool = false
95102
96103 private let deviceService = AudioDeviceService ( )
104+ private var micFlashTimer : Timer ?
97105 let priorityManager = PriorityManager ( )
98106 private var connectedDeviceUIDs : Set < String > = [ ]
99- private var muteCheckTimer : Timer ?
100107
101108 var menuBarIcon : String {
102109 currentMode. icon
@@ -141,6 +148,19 @@ class AudioManager: ObservableObject {
141148 } else {
142149 isActiveInputMuted = false
143150 }
151+
152+ // Start/stop mic flash timer based on mute state
153+ if isActiveInputMuted && micFlashTimer == nil {
154+ micFlashTimer = Timer . scheduledTimer ( withTimeInterval: 0.7 , repeats: true ) { [ weak self] _ in
155+ Task { @MainActor in
156+ self ? . micFlashState. toggle ( )
157+ }
158+ }
159+ } else if !isActiveInputMuted && micFlashTimer != nil {
160+ micFlashTimer? . invalidate ( )
161+ micFlashTimer = nil
162+ micFlashState = false
163+ }
144164 }
145165
146166 func isDeviceMuted( _ device: AudioDevice ) -> Bool {
@@ -166,23 +186,27 @@ class AudioManager: ObservableObject {
166186 refreshVolume ( )
167187 refreshMuteStatus ( )
168188 setupDeviceChangeListener ( )
169- startMuteCheckTimer ( )
189+ setupMuteVolumeListener ( )
170190 // Apply priority on startup (unless in custom mode)
171191 if !isCustomMode {
172192 applyHighestPriorityInput ( )
173193 applyHighestPriorityOutput ( )
174194 }
175195 }
176196
177- private func startMuteCheckTimer ( ) {
178- muteCheckTimer = Timer . scheduledTimer ( withTimeInterval : 0.5 , repeats : true ) { [ weak self] _ in
197+ private func setupMuteVolumeListener ( ) {
198+ deviceService . onMuteOrVolumeChanged = { [ weak self] in
179199 Task { @MainActor in
180- self ? . refreshMuteStatus ( )
181- self ? . refreshVolume ( )
200+ self ? . handleMuteOrVolumeChange ( )
182201 }
183202 }
184203 }
185204
205+ private func handleMuteOrVolumeChange( ) {
206+ refreshMuteStatus ( )
207+ refreshVolume ( )
208+ }
209+
186210 func refreshDevices( ) {
187211 let allConnectedDevices = deviceService. getDevices ( )
188212
@@ -366,33 +390,11 @@ class AudioManager: ObservableObject {
366390 }
367391
368392 func setInputDevice( _ device: AudioDevice ) {
369- if isCustomMode {
370- // Custom mode: just select the device directly
371- applyInputDevice ( device)
372- } else {
373- // Normal mode: move device to top of priority list
374- moveDeviceToTop ( device, in: & inputDevices)
375- priorityManager. savePriorities ( inputDevices, type: . input)
376- applyHighestPriorityInput ( )
377- }
393+ applyInputDevice ( device)
378394 }
379395
380396 func setOutputDevice( _ device: AudioDevice ) {
381- if isCustomMode {
382- // Custom mode: just select the device directly
383- applyOutputDevice ( device)
384- } else {
385- // Normal mode: move device to top of priority list for current category
386- let category = priorityManager. getCategory ( for: device)
387- if category == . speaker {
388- moveDeviceToTop ( device, in: & speakerDevices)
389- priorityManager. savePriorities ( speakerDevices, category: . speaker)
390- } else {
391- moveDeviceToTop ( device, in: & headphoneDevices)
392- priorityManager. savePriorities ( headphoneDevices, category: . headphone)
393- }
394- applyHighestPriorityOutput ( )
395- }
397+ applyOutputDevice ( device)
396398 }
397399
398400 private func applyInputDevice( _ device: AudioDevice ) {
@@ -405,38 +407,14 @@ class AudioManager: ObservableObject {
405407 currentOutputId = device. id
406408 }
407409
408- private func moveDeviceToTop( _ device: AudioDevice , in devices: inout [ AudioDevice ] ) {
409- if let index = devices. firstIndex ( where: { $0. uid == device. uid } ) {
410- let removed = devices. remove ( at: index)
411- devices. insert ( removed, at: 0 )
412- }
413- }
414-
415410 private func applyHighestPriorityInput( ) {
416- // Find highest priority non-muted input device
417- for device in inputDevices where device. isConnected {
418- if !deviceService. isDeviceMuted ( device. id, type: . input) {
419- applyInputDevice ( device)
420- return
421- }
422- }
423- // Fallback: if all are muted, just use first one
424411 if let first = inputDevices. first ( where: { $0. isConnected } ) {
425412 applyInputDevice ( first)
426413 }
427414 }
428415
429416 private func applyHighestPriorityOutput( ) {
430417 let devices = activeOutputDevices
431- // Find highest priority non-muted output device
432- for device in devices where device. isConnected {
433- if !deviceService. isDeviceMuted ( device. id, type: . output) {
434- applyOutputDevice ( device)
435- refreshMuteStatus ( )
436- return
437- }
438- }
439- // Fallback: if all are muted, just use first one
440418 if let first = devices. first ( where: { $0. isConnected } ) {
441419 applyOutputDevice ( first)
442420 }
0 commit comments