Add media key interception, preferences UI, dynamic menu bar icon
- Play key (F8) launches preferred player when no player is running - Blocks Apple Music auto-launch (hide + forceTerminate, like noTunes) - Preferences window: player picker with app icons, "Choose Other App..." - Known players: Spotify, Apple Music, Yandex Music, VLC, IINA, Vox - Start at login via LaunchAgent - Menu bar icon dynamically reflects current volume level and mute state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
de21639c24
commit
056c79d15e
4 changed files with 600 additions and 7 deletions
|
|
@ -4,35 +4,70 @@ import SwiftUI
|
|||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private var statusItem: NSStatusItem!
|
||||
private var volumeMonitor: VolumeMonitor!
|
||||
private var mediaKeyMonitor: MediaKeyMonitor!
|
||||
private var hudPanel: HUDPanel!
|
||||
private var prefsWindow: NSWindow?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
// Hide from Dock
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
|
||||
setupStatusBar()
|
||||
setupHUD()
|
||||
setupVolumeMonitor()
|
||||
setupMediaKeyMonitor()
|
||||
}
|
||||
|
||||
private func updateStatusIcon(volume: Float, isMuted: Bool) {
|
||||
guard let button = statusItem?.button else { return }
|
||||
let iconName: String
|
||||
if isMuted || volume == 0 {
|
||||
iconName = "speaker.slash.fill"
|
||||
} else if volume < 0.33 {
|
||||
iconName = "speaker.wave.1.fill"
|
||||
} else if volume < 0.66 {
|
||||
iconName = "speaker.wave.2.fill"
|
||||
} else {
|
||||
iconName = "speaker.wave.3.fill"
|
||||
}
|
||||
button.image = NSImage(systemSymbolName: iconName, accessibilityDescription: "HUDini")
|
||||
button.image?.size = NSSize(width: 18, height: 18)
|
||||
}
|
||||
|
||||
private func setupStatusBar() {
|
||||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
|
||||
|
||||
// Set initial icon based on current volume
|
||||
if let button = statusItem.button {
|
||||
button.image = NSImage(systemSymbolName: "speaker.wave.2.fill", accessibilityDescription: "HUDini")
|
||||
button.image?.size = NSSize(width: 18, height: 18)
|
||||
}
|
||||
|
||||
let menu = NSMenu()
|
||||
menu.addItem(NSMenuItem(title: "HUDini", action: nil, keyEquivalent: ""))
|
||||
|
||||
let titleItem = NSMenuItem(title: "HUDini", action: nil, keyEquivalent: "")
|
||||
menu.addItem(titleItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let enabledItem = NSMenuItem(title: "Enabled", action: #selector(toggleEnabled(_:)), keyEquivalent: "e")
|
||||
enabledItem.state = .on
|
||||
enabledItem.target = self
|
||||
menu.addItem(enabledItem)
|
||||
let hudItem = NSMenuItem(title: "Volume HUD", action: #selector(toggleHUD(_:)), keyEquivalent: "")
|
||||
hudItem.state = .on
|
||||
hudItem.target = self
|
||||
hudItem.tag = 100
|
||||
menu.addItem(hudItem)
|
||||
|
||||
let mediaItem = NSMenuItem(title: "Play Key Launch", action: #selector(toggleMediaKeys(_:)), keyEquivalent: "")
|
||||
mediaItem.state = .on
|
||||
mediaItem.target = self
|
||||
mediaItem.tag = 200
|
||||
menu.addItem(mediaItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let prefsItem = NSMenuItem(title: "Preferences...", action: #selector(openPreferences), keyEquivalent: ",")
|
||||
prefsItem.target = self
|
||||
menu.addItem(prefsItem)
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let quitItem = NSMenuItem(title: "Quit", action: #selector(quit), keyEquivalent: "q")
|
||||
quitItem.target = self
|
||||
menu.addItem(quitItem)
|
||||
|
|
@ -48,12 +83,30 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
volumeMonitor = VolumeMonitor { [weak self] volume, isMuted in
|
||||
DispatchQueue.main.async {
|
||||
self?.hudPanel.show(volume: volume, isMuted: isMuted)
|
||||
self?.updateStatusIcon(volume: volume, isMuted: isMuted)
|
||||
}
|
||||
}
|
||||
volumeMonitor.start()
|
||||
|
||||
// Set initial icon from current volume
|
||||
let vol = volumeMonitor.getVolume()
|
||||
let muted = volumeMonitor.isMuted()
|
||||
updateStatusIcon(volume: vol, isMuted: muted)
|
||||
}
|
||||
|
||||
@objc private func toggleEnabled(_ sender: NSMenuItem) {
|
||||
private func setupMediaKeyMonitor() {
|
||||
mediaKeyMonitor = MediaKeyMonitor()
|
||||
let ok = mediaKeyMonitor.start()
|
||||
if !ok {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
_ = self?.mediaKeyMonitor.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func toggleHUD(_ sender: NSMenuItem) {
|
||||
if sender.state == .on {
|
||||
sender.state = .off
|
||||
volumeMonitor.stop()
|
||||
|
|
@ -63,8 +116,65 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
@objc private func toggleMediaKeys(_ sender: NSMenuItem) {
|
||||
if sender.state == .on {
|
||||
sender.state = .off
|
||||
mediaKeyMonitor.isEnabled = false
|
||||
} else {
|
||||
sender.state = .on
|
||||
mediaKeyMonitor.isEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func openPreferences() {
|
||||
if let window = prefsWindow {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
return
|
||||
}
|
||||
|
||||
let prefsView = PreferencesView(
|
||||
currentBundleID: mediaKeyMonitor.preferredBundleID,
|
||||
startAtLogin: LoginItemManager.isEnabled,
|
||||
onSave: { [weak self] bundleID in
|
||||
self?.mediaKeyMonitor.preferredBundleID = bundleID
|
||||
},
|
||||
onStartAtLoginChanged: { enabled in
|
||||
// Use the .app bundle path from current process
|
||||
let appPath = Bundle.main.bundlePath
|
||||
// If running from .build, try to find the .app bundle
|
||||
let finalPath: String
|
||||
if appPath.contains(".build") {
|
||||
let projectDir = (appPath as NSString)
|
||||
.components(separatedBy: ".build").first ?? ""
|
||||
let bundlePath = projectDir + "HUDini.app"
|
||||
finalPath = FileManager.default.fileExists(atPath: bundlePath)
|
||||
? bundlePath : "/Applications/HUDini.app"
|
||||
} else {
|
||||
finalPath = appPath
|
||||
}
|
||||
LoginItemManager.setEnabled(enabled, appPath: finalPath)
|
||||
}
|
||||
)
|
||||
|
||||
let window = NSWindow(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 320, height: 420),
|
||||
styleMask: [.titled, .closable],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
window.title = "HUDini"
|
||||
window.contentView = NSHostingView(rootView: prefsView)
|
||||
window.center()
|
||||
window.isReleasedWhenClosed = false
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
prefsWindow = window
|
||||
}
|
||||
|
||||
@objc private func quit() {
|
||||
volumeMonitor.stop()
|
||||
mediaKeyMonitor.stop()
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue