diff --git a/AudioPriorityBar/Views/DeviceListView.swift b/AudioPriorityBar/Views/DeviceListView.swift index 4077372..3578ace 100644 --- a/AudioPriorityBar/Views/DeviceListView.swift +++ b/AudioPriorityBar/Views/DeviceListView.swift @@ -229,94 +229,79 @@ struct DraggableDeviceRow: View { } .animation(.spring(response: 0.25, dampingFraction: 0.7), value: isSelected) - // Actions menu - always reserve space to prevent layout shifts - ZStack { - // Invisible placeholder to reserve space - Image(systemName: "ellipsis.circle") - .font(.system(size: 14)) - .frame(width: 28, height: 28) - .opacity(0) - - // Actual menu (shown on hover) - if isHovering && !isDragging { - Group { - Menu { - if showCategoryPicker { - Button { - audioManager.setCategory(.speaker, for: device) - } label: { - Label("Move to Speakers", systemImage: "speaker.wave.2.fill") - } - Button { - audioManager.setCategory(.headphone, for: device) - } label: { - Label("Move to Headphones", systemImage: "headphones") - } - Divider() + Menu { + if showCategoryPicker { + Button { + audioManager.setCategory(.speaker, for: device) + } label: { + Label("Move to Speakers", systemImage: "speaker.wave.2.fill") } + Button { + audioManager.setCategory(.headphone, for: device) + } label: { + Label("Move to Headphones", systemImage: "headphones") + } + Divider() + } - if isHiddenSection || isIgnored { + if isHiddenSection || isIgnored { + Button { + audioManager.unhideDevice(device) + } label: { + Label("Stop Ignoring", systemImage: "eye") + } + } else { + if let onHide { Button { - audioManager.unhideDevice(device) + onHide(device) } label: { - Label("Stop Ignoring", systemImage: "eye") + let categoryLabel = device.type == .input ? "microphone" : + (category == .headphone ? "headphone" : "speaker") + Label("Ignore as \(categoryLabel)", systemImage: "eye.slash") } - } else { - if let onHide { + + if device.type == .output { Button { - onHide(device) + audioManager.hideDeviceEntirely(device) } label: { - let categoryLabel = device.type == .input ? "microphone" : - (category == .headphone ? "headphone" : "speaker") - Label("Ignore as \(categoryLabel)", systemImage: "eye.slash") - } - - if device.type == .output { - Button { - audioManager.hideDeviceEntirely(device) - } label: { - Label("Ignore entirely", systemImage: "eye.slash.fill") - } + Label("Ignore entirely", systemImage: "eye.slash.fill") } } } + } - if isDisconnected { - Divider() - Button(role: .destructive) { - audioManager.priorityManager.forgetDevice(device.uid) - audioManager.refreshDevices() - } label: { - Label("Forget Device", systemImage: "trash") - } + if isDisconnected { + Divider() + Button(role: .destructive) { + audioManager.priorityManager.forgetDevice(device.uid) + audioManager.refreshDevices() + } label: { + Label("Forget Device", systemImage: "trash") } + } - if device.isConnected { - Divider() - Button { - audioManager.setNeverUse(device, neverUse: !audioManager.isNeverUse(device)) - } label: { - if audioManager.isNeverUse(device) { - Label("Allow Use", systemImage: "checkmark.circle") - } else { - Label("Never Use", systemImage: "nosign") - } - } - } + if device.isConnected { + Divider() + Button { + audioManager.setNeverUse(device, neverUse: !audioManager.isNeverUse(device)) } label: { - Image(systemName: "ellipsis.circle") - .font(.system(size: 14)) - .foregroundColor(.secondary) - .frame(width: 28, height: 28) - .contentShape(Rectangle()) - } - .menuStyle(.borderlessButton) + if audioManager.isNeverUse(device) { + Label("Allow Use", systemImage: "checkmark.circle") + } else { + Label("Never Use", systemImage: "nosign") + } } - .transition(.opacity.combined(with: .scale(scale: 0.8))) } + } label: { + Image(systemName: "ellipsis.circle") + .font(.system(size: 14)) + .foregroundColor(.secondary) + .frame(width: 28, height: 28) + .contentShape(Rectangle()) } + .menuStyle(.borderlessButton) .frame(width: 32) - .animation(.easeInOut(duration: 0.12), value: isHovering) + .opacity(isDragging ? 0.3 : 1) } .padding(.leading, 8) .padding(.trailing, 10) diff --git a/AudioPriorityBar/Views/MenuBarView.swift b/AudioPriorityBar/Views/MenuBarView.swift index 3ad55c2..7f4d200 100644 --- a/AudioPriorityBar/Views/MenuBarView.swift +++ b/AudioPriorityBar/Views/MenuBarView.swift @@ -19,70 +19,70 @@ struct MenuBarView: View { Divider() .padding(.horizontal, 12) - ScrollView { - VStack(spacing: 20) { - // Speakers (show in speaker mode or custom mode) - if audioManager.currentMode == .speaker || audioManager.isCustomMode { - DeviceSectionView( - title: "Speakers", - icon: "speaker.wave.2.fill", - devices: audioManager.speakerDevices, - currentDeviceId: audioManager.currentOutputId, - onMove: audioManager.moveSpeakerDevice, - onSelect: { device in - if !audioManager.isCustomMode { - audioManager.setMode(.speaker) - } - audioManager.setOutputDevice(device) - }, - onHide: { audioManager.hideDevice($0, category: .speaker) }, - onUnhide: { audioManager.unhideDevice($0, category: .speaker) }, - category: .speaker, - showCategoryPicker: true, - isActiveCategory: audioManager.currentMode == .speaker || audioManager.isCustomMode - ) - } - - // Headphones (show in headphone mode or custom mode) - if audioManager.currentMode == .headphone || audioManager.isCustomMode { - DeviceSectionView( - title: "Headphones", - icon: "headphones", - devices: audioManager.headphoneDevices, - currentDeviceId: audioManager.currentOutputId, - onMove: audioManager.moveHeadphoneDevice, - onSelect: { device in - if !audioManager.isCustomMode { - audioManager.setMode(.headphone) - } - audioManager.setOutputDevice(device) - }, - onHide: { audioManager.hideDevice($0, category: .headphone) }, - onUnhide: { audioManager.unhideDevice($0, category: .headphone) }, - category: .headphone, - showCategoryPicker: true, - isActiveCategory: audioManager.currentMode == .headphone || audioManager.isCustomMode - ) - } + // Do not wrap this in a ScrollView: MenuBarExtra windows give + // ScrollView an ideal height of 0, so the device lists vanish. + VStack(spacing: 20) { + // Speakers (show in speaker mode or custom mode) + if audioManager.currentMode == .speaker || audioManager.isCustomMode { + DeviceSectionView( + title: "Speakers", + icon: "speaker.wave.2.fill", + devices: audioManager.speakerDevices, + currentDeviceId: audioManager.currentOutputId, + onMove: audioManager.moveSpeakerDevice, + onSelect: { device in + if !audioManager.isCustomMode { + audioManager.setMode(.speaker) + } + audioManager.setOutputDevice(device) + }, + onHide: { audioManager.hideDevice($0, category: .speaker) }, + onUnhide: { audioManager.unhideDevice($0, category: .speaker) }, + category: .speaker, + showCategoryPicker: true, + isActiveCategory: audioManager.currentMode == .speaker || audioManager.isCustomMode + ) + } - // Microphones (always shown, at the bottom) + // Headphones (show in headphone mode or custom mode) + if audioManager.currentMode == .headphone || audioManager.isCustomMode { DeviceSectionView( - title: "Microphones", - icon: "mic.fill", - devices: audioManager.inputDevices, - currentDeviceId: audioManager.currentInputId, - onMove: audioManager.moveInputDevice, - onSelect: audioManager.setInputDevice, - onHide: { audioManager.hideDevice($0, category: nil) }, - onUnhide: { audioManager.unhideDevice($0, category: nil) }, - category: nil, - showCategoryPicker: false + title: "Headphones", + icon: "headphones", + devices: audioManager.headphoneDevices, + currentDeviceId: audioManager.currentOutputId, + onMove: audioManager.moveHeadphoneDevice, + onSelect: { device in + if !audioManager.isCustomMode { + audioManager.setMode(.headphone) + } + audioManager.setOutputDevice(device) + }, + onHide: { audioManager.hideDevice($0, category: .headphone) }, + onUnhide: { audioManager.unhideDevice($0, category: .headphone) }, + category: .headphone, + showCategoryPicker: true, + isActiveCategory: audioManager.currentMode == .headphone || audioManager.isCustomMode ) } - .padding(.horizontal, 16) - .padding(.vertical, 14) + + // Microphones (always shown, at the bottom) + DeviceSectionView( + title: "Microphones", + icon: "mic.fill", + devices: audioManager.inputDevices, + currentDeviceId: audioManager.currentInputId, + onMove: audioManager.moveInputDevice, + onSelect: audioManager.setInputDevice, + onHide: { audioManager.hideDevice($0, category: nil) }, + onUnhide: { audioManager.unhideDevice($0, category: nil) }, + category: nil, + showCategoryPicker: false + ) } - .frame(maxHeight: 420) + .padding(.horizontal, 16) + .padding(.vertical, 14) + .frame(maxWidth: .infinity, alignment: .leading) Divider() .padding(.horizontal, 12)