165 lines
6.2 KiB
Swift
165 lines
6.2 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Dancing Audio Bars
|
|
struct DancingAudioBars: View {
|
|
let micLevel: Float
|
|
let systemLevel: Float
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// App logo
|
|
Image("Icon32")
|
|
.resizable()
|
|
.frame(width: 32, height: 32)
|
|
|
|
// Interleaved bars - alternating blue (mic) and orange (system)
|
|
HStack(spacing: -2) {
|
|
// Bar 1: Microphone (blue)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.blue.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 0, level: micLevel))
|
|
.animation(.easeInOut(duration: 0.1), value: micLevel)
|
|
|
|
// Bar 2: System (orange)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.orange.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 1, level: systemLevel))
|
|
.animation(.easeInOut(duration: 0.1).delay(0.03), value: systemLevel)
|
|
|
|
// Bar 3: Microphone (blue)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.blue.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 2, level: micLevel))
|
|
.animation(.easeInOut(duration: 0.1).delay(0.06), value: micLevel)
|
|
|
|
// Bar 4: System (orange)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.orange.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 0, level: systemLevel))
|
|
.animation(.easeInOut(duration: 0.1).delay(0.09), value: systemLevel)
|
|
|
|
// Bar 5: Microphone (blue)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.blue.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 1, level: micLevel))
|
|
.animation(.easeInOut(duration: 0.1).delay(0.12), value: micLevel)
|
|
|
|
// Bar 6: System (orange)
|
|
RoundedRectangle(cornerRadius: 2)
|
|
.fill(Color.orange.opacity(0.6))
|
|
.frame(width: 6, height: getDancingBarHeight(index: 2, level: systemLevel))
|
|
.animation(.easeInOut(duration: 0.1).delay(0.15), value: systemLevel)
|
|
}
|
|
.frame(width: 32, height: 32)
|
|
}
|
|
.padding(0)
|
|
}
|
|
|
|
private func getDancingBarHeight(index: Int, level: Float) -> CGFloat {
|
|
let baseHeight: CGFloat = 4 // Flat when no audio
|
|
let maxHeight: CGFloat = 24 // Max dancing height
|
|
|
|
if level > 0.03 { // Only dance when there's actual audio
|
|
// Each bar responds to the same audio level but with different scaling
|
|
// This creates a natural dancing effect as audio levels fluctuate
|
|
let barVariation: [CGFloat] = [0.9, 1.0, 0.8] // Different responsiveness per bar
|
|
let audioResponse = CGFloat(level) * maxHeight * barVariation[index % 3]
|
|
return max(baseHeight, min(maxHeight, baseHeight + audioResponse))
|
|
}
|
|
|
|
return baseHeight
|
|
}
|
|
}
|
|
|
|
// MARK: - Audio Level Window View
|
|
struct AudioLevelWindowView: View {
|
|
@StateObject private var audioLevelManager = AudioLevelManager.shared
|
|
|
|
var body: some View {
|
|
let micLevel = audioLevelManager.isRecording ? audioLevelManager.micAudioLevel * 40 : 0
|
|
let systemLevel = audioLevelManager.isRecording ? audioLevelManager.systemAudioLevel * 5 : 0
|
|
return DancingAudioBars(
|
|
micLevel: micLevel,
|
|
systemLevel: systemLevel
|
|
)
|
|
.padding(2)
|
|
.background(.regularMaterial.opacity(0.9), in: RoundedRectangle(cornerRadius: 10))
|
|
.shadow(color: .black.opacity(0.2), radius: 3)
|
|
}
|
|
}
|
|
|
|
// MARK: - Audio Level Manager (Singleton to share data)
|
|
class AudioLevelManager: ObservableObject {
|
|
static let shared = AudioLevelManager()
|
|
|
|
@Published var micAudioLevel: Float = 0.0
|
|
@Published var systemAudioLevel: Float = 0.0
|
|
@Published var isRecording: Bool = false
|
|
|
|
private init() {}
|
|
|
|
func updateMicLevel(_ level: Float) {
|
|
DispatchQueue.main.async {
|
|
self.micAudioLevel = level
|
|
}
|
|
}
|
|
|
|
func updateSystemLevel(_ level: Float) {
|
|
DispatchQueue.main.async {
|
|
self.systemAudioLevel = level
|
|
}
|
|
}
|
|
|
|
func updateRecordingState(_ isRecording: Bool) {
|
|
DispatchQueue.main.async {
|
|
self.isRecording = isRecording
|
|
if isRecording {
|
|
// Auto-show the window when recording starts
|
|
AudioLevelWindowManager.shared.showWindow()
|
|
} else {
|
|
// Auto-hide the window when recording stops
|
|
AudioLevelWindowManager.shared.hideWindow()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Audio Level Window Manager
|
|
@MainActor
|
|
class AudioLevelWindowManager: ObservableObject {
|
|
static let shared = AudioLevelWindowManager()
|
|
|
|
private init() {}
|
|
|
|
func showWindow() {
|
|
// Find the audio levels window and bring it to front
|
|
for window in NSApplication.shared.windows {
|
|
if window.identifier?.rawValue == "audio-levels" {
|
|
window.orderFront(nil)
|
|
window.level = .floating
|
|
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle]
|
|
window.isMovableByWindowBackground = true
|
|
window.hasShadow = true
|
|
window.styleMask = [.borderless]
|
|
return
|
|
}
|
|
}
|
|
|
|
// If window not found, post a notification to trigger opening
|
|
NotificationCenter.default.post(name: .openAudioLevelWindow, object: nil)
|
|
}
|
|
|
|
func hideWindow() {
|
|
// Find the audio levels window and hide it
|
|
for window in NSApplication.shared.windows {
|
|
if window.identifier?.rawValue == "audio-levels" {
|
|
window.orderOut(nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Notification.Name {
|
|
static let openAudioLevelWindow = Notification.Name("openAudioLevelWindow")
|
|
} |