| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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
- )
- }
- 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")
- }
|