425 lines
15 KiB
Swift
425 lines
15 KiB
Swift
import SwiftUI
|
|
|
|
struct AudioInputSettingsView: View {
|
|
@ObservedObject var audioDeviceManager = AudioDeviceManager.shared
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 0) {
|
|
heroSection
|
|
mainContent
|
|
}
|
|
}
|
|
.background(Color(NSColor.controlBackgroundColor))
|
|
}
|
|
|
|
private var mainContent: some View {
|
|
VStack(spacing: 40) {
|
|
inputModeSection
|
|
|
|
if audioDeviceManager.inputMode == .custom {
|
|
customDeviceSection
|
|
} else if audioDeviceManager.inputMode == .prioritized {
|
|
prioritizedDevicesSection
|
|
}
|
|
}
|
|
.padding(.horizontal, 32)
|
|
.padding(.vertical, 40)
|
|
}
|
|
|
|
private var heroSection: some View {
|
|
VStack(spacing: 24) {
|
|
Image(systemName: "waveform")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(.blue)
|
|
.padding(20)
|
|
.background(Circle()
|
|
.fill(Color(.windowBackgroundColor).opacity(0.4))
|
|
.shadow(color: .black.opacity(0.1), radius: 10, y: 5))
|
|
|
|
VStack(spacing: 8) {
|
|
Text("Audio Input")
|
|
.font(.system(size: 28, weight: .bold))
|
|
Text("Configure your microphone preferences")
|
|
.font(.system(size: 15))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 40)
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
|
|
private var inputModeSection: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
Text("Input Mode")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
HStack(spacing: 20) {
|
|
ForEach(AudioInputMode.allCases, id: \.self) { mode in
|
|
InputModeCard(
|
|
mode: mode,
|
|
isSelected: audioDeviceManager.inputMode == mode,
|
|
action: { audioDeviceManager.selectInputMode(mode) }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var customDeviceSection: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
HStack {
|
|
Text("Available Devices")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
Spacer()
|
|
|
|
Button(action: { audioDeviceManager.loadAvailableDevices() }) {
|
|
Label("Refresh", systemImage: "arrow.clockwise")
|
|
}
|
|
.buttonStyle(.borderless)
|
|
}
|
|
|
|
Text("Note: Selecting a device here will override your Mac\'s system-wide default microphone.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.bottom, 8)
|
|
|
|
VStack(spacing: 12) {
|
|
ForEach(audioDeviceManager.availableDevices, id: \.id) { device in
|
|
DeviceSelectionCard(
|
|
name: device.name,
|
|
isSelected: audioDeviceManager.selectedDeviceID == device.id,
|
|
isActive: audioDeviceManager.getCurrentDevice() == device.id
|
|
) {
|
|
audioDeviceManager.selectDevice(id: device.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private var prioritizedDevicesSection: some View {
|
|
VStack(alignment: .leading, spacing: 20) {
|
|
if audioDeviceManager.availableDevices.isEmpty {
|
|
emptyDevicesState
|
|
} else {
|
|
prioritizedDevicesContent
|
|
Divider().padding(.vertical, 8)
|
|
availableDevicesContent
|
|
}
|
|
}
|
|
}
|
|
|
|
private var prioritizedDevicesContent: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Prioritized Devices")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
Text("Devices will be used in order of priority. If a device is unavailable, the next one will be tried. If no prioritized device is available, the system default microphone will be used.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
Text("Warning: Using a prioritized device will override your Mac\'s system-wide default microphone if it becomes active.")
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.padding(.top, 4)
|
|
}
|
|
|
|
if audioDeviceManager.prioritizedDevices.isEmpty {
|
|
Text("No prioritized devices")
|
|
.foregroundStyle(.secondary)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
prioritizedDevicesList
|
|
}
|
|
}
|
|
}
|
|
|
|
private var availableDevicesContent: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Text("Available Devices")
|
|
.font(.title2)
|
|
.fontWeight(.semibold)
|
|
|
|
availableDevicesList
|
|
}
|
|
}
|
|
|
|
private var emptyDevicesState: some View {
|
|
VStack(spacing: 16) {
|
|
Image(systemName: "mic.slash.circle.fill")
|
|
.font(.system(size: 48))
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(.secondary)
|
|
|
|
VStack(spacing: 8) {
|
|
Text("No Audio Devices")
|
|
.font(.headline)
|
|
Text("Connect an audio input device to get started")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(40)
|
|
.background(CardBackground(isSelected: false))
|
|
}
|
|
|
|
private var prioritizedDevicesList: some View {
|
|
VStack(spacing: 12) {
|
|
ForEach(audioDeviceManager.prioritizedDevices.sorted(by: { $0.priority < $1.priority })) { device in
|
|
devicePriorityCard(for: device)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func devicePriorityCard(for prioritizedDevice: PrioritizedDevice) -> some View {
|
|
let device = audioDeviceManager.availableDevices.first(where: { $0.uid == prioritizedDevice.id })
|
|
return DevicePriorityCard(
|
|
name: prioritizedDevice.name,
|
|
priority: prioritizedDevice.priority,
|
|
isActive: device.map { audioDeviceManager.getCurrentDevice() == $0.id } ?? false,
|
|
isPrioritized: true,
|
|
isAvailable: device != nil,
|
|
canMoveUp: prioritizedDevice.priority > 0,
|
|
canMoveDown: prioritizedDevice.priority < audioDeviceManager.prioritizedDevices.count - 1,
|
|
onTogglePriority: { audioDeviceManager.removePrioritizedDevice(id: prioritizedDevice.id) },
|
|
onMoveUp: { moveDeviceUp(prioritizedDevice) },
|
|
onMoveDown: { moveDeviceDown(prioritizedDevice) }
|
|
)
|
|
}
|
|
|
|
private var availableDevicesList: some View {
|
|
let unprioritizedDevices = audioDeviceManager.availableDevices.filter { device in
|
|
!audioDeviceManager.prioritizedDevices.contains { $0.id == device.uid }
|
|
}
|
|
|
|
return Group {
|
|
if unprioritizedDevices.isEmpty {
|
|
Text("No additional devices available")
|
|
.foregroundStyle(.secondary)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
ForEach(unprioritizedDevices, id: \.id) { device in
|
|
DevicePriorityCard(
|
|
name: device.name,
|
|
priority: nil,
|
|
isActive: audioDeviceManager.getCurrentDevice() == device.id,
|
|
isPrioritized: false,
|
|
isAvailable: true,
|
|
canMoveUp: false,
|
|
canMoveDown: false,
|
|
onTogglePriority: { audioDeviceManager.addPrioritizedDevice(uid: device.uid, name: device.name) },
|
|
onMoveUp: {},
|
|
onMoveDown: {}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func moveDeviceUp(_ device: PrioritizedDevice) {
|
|
guard device.priority > 0,
|
|
let currentIndex = audioDeviceManager.prioritizedDevices.firstIndex(where: { $0.id == device.id })
|
|
else { return }
|
|
|
|
var devices = audioDeviceManager.prioritizedDevices
|
|
devices.swapAt(currentIndex, currentIndex - 1)
|
|
updatePriorities(devices)
|
|
}
|
|
|
|
private func moveDeviceDown(_ device: PrioritizedDevice) {
|
|
guard device.priority < audioDeviceManager.prioritizedDevices.count - 1,
|
|
let currentIndex = audioDeviceManager.prioritizedDevices.firstIndex(where: { $0.id == device.id })
|
|
else { return }
|
|
|
|
var devices = audioDeviceManager.prioritizedDevices
|
|
devices.swapAt(currentIndex, currentIndex + 1)
|
|
updatePriorities(devices)
|
|
}
|
|
|
|
private func updatePriorities(_ devices: [PrioritizedDevice]) {
|
|
let updatedDevices = devices.enumerated().map { index, device in
|
|
PrioritizedDevice(id: device.id, name: device.name, priority: index)
|
|
}
|
|
audioDeviceManager.updatePriorities(devices: updatedDevices)
|
|
}
|
|
}
|
|
|
|
struct InputModeCard: View {
|
|
let mode: AudioInputMode
|
|
let isSelected: Bool
|
|
let action: () -> Void
|
|
|
|
private var icon: String {
|
|
switch mode {
|
|
case .custom: return "mic.circle.fill"
|
|
case .prioritized: return "list.number"
|
|
}
|
|
}
|
|
|
|
private var description: String {
|
|
switch mode {
|
|
case .custom: return "Select a specific input device"
|
|
case .prioritized: return "Set up device priority order"
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 28))
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(isSelected ? .blue : .secondary)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(mode.rawValue)
|
|
.font(.headline)
|
|
|
|
Text(description)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding()
|
|
.background(CardBackground(isSelected: isSelected))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct DeviceSelectionCard: View {
|
|
let name: String
|
|
let isSelected: Bool
|
|
let isActive: Bool
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack {
|
|
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(isSelected ? .blue : .secondary)
|
|
.font(.system(size: 18))
|
|
|
|
Text(name)
|
|
.foregroundStyle(.primary)
|
|
|
|
Spacer()
|
|
|
|
if isActive {
|
|
Label("Active", systemImage: "wave.3.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
Capsule()
|
|
.fill(.green.opacity(0.1))
|
|
)
|
|
}
|
|
}
|
|
.padding()
|
|
.background(CardBackground(isSelected: isSelected))
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
struct DevicePriorityCard: View {
|
|
let name: String
|
|
let priority: Int?
|
|
let isActive: Bool
|
|
let isPrioritized: Bool
|
|
let isAvailable: Bool
|
|
let canMoveUp: Bool
|
|
let canMoveDown: Bool
|
|
let onTogglePriority: () -> Void
|
|
let onMoveUp: () -> Void
|
|
let onMoveDown: () -> Void
|
|
|
|
var body: some View {
|
|
HStack {
|
|
// Priority number or dash
|
|
if let priority = priority {
|
|
Text("\(priority + 1)")
|
|
.font(.system(size: 18, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: 24)
|
|
} else {
|
|
Text("-")
|
|
.font(.system(size: 18, weight: .medium))
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: 24)
|
|
}
|
|
|
|
// Device name
|
|
Text(name)
|
|
.foregroundStyle(isAvailable ? .primary : .secondary)
|
|
|
|
Spacer()
|
|
|
|
// Status and Controls
|
|
HStack(spacing: 12) {
|
|
// Active status
|
|
if isActive {
|
|
Label("Active", systemImage: "wave.3.right")
|
|
.font(.caption)
|
|
.foregroundStyle(.green)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
Capsule()
|
|
.fill(.green.opacity(0.1))
|
|
)
|
|
} else if !isAvailable && isPrioritized {
|
|
Label("Unavailable", systemImage: "exclamationmark.triangle")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color(.windowBackgroundColor).opacity(0.4))
|
|
)
|
|
}
|
|
|
|
// Priority controls (only show if prioritized)
|
|
if isPrioritized {
|
|
HStack(spacing: 2) {
|
|
Button(action: onMoveUp) {
|
|
Image(systemName: "chevron.up")
|
|
.foregroundStyle(canMoveUp ? .blue : .secondary.opacity(0.5))
|
|
}
|
|
.disabled(!canMoveUp)
|
|
|
|
Button(action: onMoveDown) {
|
|
Image(systemName: "chevron.down")
|
|
.foregroundStyle(canMoveDown ? .blue : .secondary.opacity(0.5))
|
|
}
|
|
.disabled(!canMoveDown)
|
|
}
|
|
}
|
|
|
|
// Toggle priority button
|
|
Button(action: onTogglePriority) {
|
|
Image(systemName: isPrioritized ? "minus.circle.fill" : "plus.circle.fill")
|
|
.symbolRenderingMode(.hierarchical)
|
|
.foregroundStyle(isPrioritized ? .red : .blue)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
.padding()
|
|
.background(CardBackground(isSelected: false))
|
|
}
|
|
}
|