Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 55 additions & 70 deletions AudioPriorityBar/Views/DeviceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
118 changes: 59 additions & 59 deletions AudioPriorityBar/Views/MenuBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down