import SwiftData import SwiftUI /// MixBoard — A macOS music player and mix preparation tool with DAW export. @main struct MixBoardApp: App { @State private var playerVM = PlayerViewModel() @State private var playlistVM = PlaylistViewModel() @StateObject private var libraryManager = LibraryManager() @StateObject private var theme = AppTheme() @StateObject private var syncWatcher = SyncWatcher() @ObservedObject private var shortcutConfig = KeyboardShortcutConfig.shared var body: some Scene { WindowGroup { ContentView() .environment(playerVM) .environment(playlistVM) .environmentObject(libraryManager) .environmentObject(theme) .environmentObject(syncWatcher) .preferredColorScheme(theme.preferredScheme) .onAppear { // Faster tooltips (200ms instead of default ~1000ms) UserDefaults.standard.set(200, forKey: "NSInitialToolTipDelay") MediaKeyHandler.shared.register(playerVM: playerVM) syncWatcher.createSyncFolders() syncWatcher.startWatching() AppIconConfig.shared.applyIcon() } } .modelContainer(for: [Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self]) .windowStyle(.titleBar) .windowToolbarStyle(.unified(showsTitle: true)) .defaultSize(width: 1200, height: 800) .commands { CommandGroup(replacing: .newItem) { Button("New Playlist...") { NotificationCenter.default.post(name: .newPlaylist, object: nil) } .keyboardShortcut( shortcutConfig.binding(for: .newPlaylist).keyEquivalent, modifiers: shortcutConfig.binding(for: .newPlaylist).eventModifiers ) } CommandMenu("View") { Button("Now Playing") { NotificationCenter.default.post(name: .toggleNowPlaying, object: nil) } .keyboardShortcut( shortcutConfig.binding(for: .nowPlaying).keyEquivalent, modifiers: shortcutConfig.binding(for: .nowPlaying).eventModifiers ) Divider() Button("Library") { NotificationCenter.default.post(name: .toggleBrowsePanel, object: nil) } .keyboardShortcut("b", modifiers: .command) } CommandMenu("Playback") { Button("Play / Pause") { playerVM.togglePlayPause() } .keyboardShortcut( shortcutConfig.binding(for: .playPause).keyEquivalent, modifiers: shortcutConfig.binding(for: .playPause).eventModifiers ) Button("Stop") { playerVM.stop() } .keyboardShortcut( shortcutConfig.binding(for: .stop).keyEquivalent, modifiers: shortcutConfig.binding(for: .stop).eventModifiers ) Divider() Button("Next Track") { playerVM.playNext() } .keyboardShortcut( shortcutConfig.binding(for: .nextTrack).keyEquivalent, modifiers: shortcutConfig.binding(for: .nextTrack).eventModifiers ) Button("Previous Track") { playerVM.playPrevious() } .keyboardShortcut( shortcutConfig.binding(for: .previousTrack).keyEquivalent, modifiers: shortcutConfig.binding(for: .previousTrack).eventModifiers ) Divider() Button("Skip Forward 10s") { playerVM.skipForward() } .keyboardShortcut( shortcutConfig.binding(for: .skipForward).keyEquivalent, modifiers: shortcutConfig.binding(for: .skipForward).eventModifiers ) Button("Skip Backward 10s") { playerVM.skipBackward() } .keyboardShortcut( shortcutConfig.binding(for: .skipBackward).keyEquivalent, modifiers: shortcutConfig.binding(for: .skipBackward).eventModifiers ) Divider() Button(playerVM.shuffleEnabled ? "Shuffle: On" : "Shuffle: Off") { playerVM.shuffleEnabled.toggle() } .keyboardShortcut( shortcutConfig.binding(for: .toggleShuffle).keyEquivalent, modifiers: shortcutConfig.binding(for: .toggleShuffle).eventModifiers ) Button("Repeat: \(playerVM.repeatMode.rawValue)") { switch playerVM.repeatMode { case .off: playerVM.repeatMode = .all case .all: playerVM.repeatMode = .one case .one: playerVM.repeatMode = .off } } .keyboardShortcut( shortcutConfig.binding(for: .toggleRepeat).keyEquivalent, modifiers: shortcutConfig.binding(for: .toggleRepeat).eventModifiers ) } CommandMenu("Mix") { // Per-slot quick-add let mixActions: [ShortcutAction] = [.addToMix1, .addToMix2, .addToMix3] ForEach(0..<3, id: \.self) { slot in Button("Add to \(playlistVM.mixTargetName(slot))") { if let track = playerVM.currentTrack { NotificationCenter.default.post( name: .quickAddToMix, object: nil, userInfo: ["slot": slot, "track": track] ) } } .keyboardShortcut( shortcutConfig.binding(for: mixActions[slot]).keyEquivalent, modifiers: shortcutConfig.binding(for: mixActions[slot]).eventModifiers ) } Divider() Button("Export to DAW...") { playlistVM.showExportSheet = true } .keyboardShortcut( shortcutConfig.binding(for: .exportToDAW).keyEquivalent, modifiers: shortcutConfig.binding(for: .exportToDAW).eventModifiers ) Divider() Button("Import from iPhone...") { NotificationCenter.default.post(name: .importFromiPhone, object: nil) } .keyboardShortcut( shortcutConfig.binding(for: .importFromiPhone).keyEquivalent, modifiers: shortcutConfig.binding(for: .importFromiPhone).eventModifiers ) Divider() Button("Search All Playlists...") { NotificationCenter.default.post(name: .globalSearch, object: nil) } .keyboardShortcut( shortcutConfig.binding(for: .search).keyEquivalent, modifiers: shortcutConfig.binding(for: .search).eventModifiers ) } } // Settings (⌘,) Settings { SettingsView() .environment(playlistVM) .environmentObject(theme) .preferredColorScheme(theme.preferredScheme) } .modelContainer(for: [Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self]) // Now Playing window (Tidal-style separate window) Window("Now Playing", id: "now-playing") { NowPlayingView(displayMode: .floating) .environment(playerVM) .environment(playlistVM) .environmentObject(libraryManager) .environmentObject(theme) .preferredColorScheme(theme.preferredScheme) } .defaultSize(width: 850, height: 600) .windowStyle(.titleBar) } } // MARK: - Notification Names extension Notification.Name { static let newPlaylist = Notification.Name("newPlaylist") static let quickAddToTarget = Notification.Name("quickAddToTarget") static let quickAddToMix = Notification.Name("quickAddToMix") static let globalSearch = Notification.Name("globalSearch") static let importFromiPhone = Notification.Name("importFromiPhone") static let toggleNowPlaying = Notification.Name("toggleNowPlaying") static let toggleBrowsePanel = Notification.Name("toggleBrowsePanel") static let popOutNowPlaying = Notification.Name("popOutNowPlaying") static let closeInlineNowPlaying = Notification.Name("closeInlineNowPlaying") static let doubleClickPlayTrack = Notification.Name("doubleClickPlayTrack") }