MixBoardApp.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import SwiftData
  2. import SwiftUI
  3. /// MixBoard — A macOS music player and mix preparation tool with DAW export.
  4. @main
  5. struct MixBoardApp: App {
  6. @State private var playerVM = PlayerViewModel()
  7. @State private var playlistVM = PlaylistViewModel()
  8. @StateObject private var libraryManager = LibraryManager()
  9. @StateObject private var theme = AppTheme()
  10. @StateObject private var syncWatcher = SyncWatcher()
  11. @ObservedObject private var shortcutConfig = KeyboardShortcutConfig.shared
  12. var body: some Scene {
  13. WindowGroup {
  14. ContentView()
  15. .environment(playerVM)
  16. .environment(playlistVM)
  17. .environmentObject(libraryManager)
  18. .environmentObject(theme)
  19. .environmentObject(syncWatcher)
  20. .preferredColorScheme(theme.preferredScheme)
  21. .onAppear {
  22. // Faster tooltips (200ms instead of default ~1000ms)
  23. UserDefaults.standard.set(200, forKey: "NSInitialToolTipDelay")
  24. MediaKeyHandler.shared.register(playerVM: playerVM)
  25. syncWatcher.createSyncFolders()
  26. syncWatcher.startWatching()
  27. AppIconConfig.shared.applyIcon()
  28. }
  29. }
  30. .modelContainer(for: [Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self])
  31. .windowStyle(.titleBar)
  32. .windowToolbarStyle(.unified(showsTitle: true))
  33. .defaultSize(width: 1200, height: 800)
  34. .commands {
  35. CommandGroup(replacing: .newItem) {
  36. Button("New Playlist...") {
  37. NotificationCenter.default.post(name: .newPlaylist, object: nil)
  38. }
  39. .keyboardShortcut(
  40. shortcutConfig.binding(for: .newPlaylist).keyEquivalent,
  41. modifiers: shortcutConfig.binding(for: .newPlaylist).eventModifiers
  42. )
  43. }
  44. CommandMenu("View") {
  45. Button("Now Playing") {
  46. NotificationCenter.default.post(name: .toggleNowPlaying, object: nil)
  47. }
  48. .keyboardShortcut(
  49. shortcutConfig.binding(for: .nowPlaying).keyEquivalent,
  50. modifiers: shortcutConfig.binding(for: .nowPlaying).eventModifiers
  51. )
  52. }
  53. CommandMenu("Playback") {
  54. Button("Play / Pause") {
  55. playerVM.togglePlayPause()
  56. }
  57. .keyboardShortcut(
  58. shortcutConfig.binding(for: .playPause).keyEquivalent,
  59. modifiers: shortcutConfig.binding(for: .playPause).eventModifiers
  60. )
  61. Button("Stop") {
  62. playerVM.stop()
  63. }
  64. .keyboardShortcut(
  65. shortcutConfig.binding(for: .stop).keyEquivalent,
  66. modifiers: shortcutConfig.binding(for: .stop).eventModifiers
  67. )
  68. Divider()
  69. Button("Next Track") {
  70. playerVM.playNext()
  71. }
  72. .keyboardShortcut(
  73. shortcutConfig.binding(for: .nextTrack).keyEquivalent,
  74. modifiers: shortcutConfig.binding(for: .nextTrack).eventModifiers
  75. )
  76. Button("Previous Track") {
  77. playerVM.playPrevious()
  78. }
  79. .keyboardShortcut(
  80. shortcutConfig.binding(for: .previousTrack).keyEquivalent,
  81. modifiers: shortcutConfig.binding(for: .previousTrack).eventModifiers
  82. )
  83. Divider()
  84. Button("Skip Forward 10s") {
  85. playerVM.skipForward()
  86. }
  87. .keyboardShortcut(
  88. shortcutConfig.binding(for: .skipForward).keyEquivalent,
  89. modifiers: shortcutConfig.binding(for: .skipForward).eventModifiers
  90. )
  91. Button("Skip Backward 10s") {
  92. playerVM.skipBackward()
  93. }
  94. .keyboardShortcut(
  95. shortcutConfig.binding(for: .skipBackward).keyEquivalent,
  96. modifiers: shortcutConfig.binding(for: .skipBackward).eventModifiers
  97. )
  98. Divider()
  99. Button(playerVM.shuffleEnabled ? "Shuffle: On" : "Shuffle: Off") {
  100. playerVM.shuffleEnabled.toggle()
  101. }
  102. .keyboardShortcut(
  103. shortcutConfig.binding(for: .toggleShuffle).keyEquivalent,
  104. modifiers: shortcutConfig.binding(for: .toggleShuffle).eventModifiers
  105. )
  106. Button("Repeat: \(playerVM.repeatMode.rawValue)") {
  107. switch playerVM.repeatMode {
  108. case .off: playerVM.repeatMode = .all
  109. case .all: playerVM.repeatMode = .one
  110. case .one: playerVM.repeatMode = .off
  111. }
  112. }
  113. .keyboardShortcut(
  114. shortcutConfig.binding(for: .toggleRepeat).keyEquivalent,
  115. modifiers: shortcutConfig.binding(for: .toggleRepeat).eventModifiers
  116. )
  117. }
  118. CommandMenu("Mix") {
  119. // Per-slot quick-add
  120. let mixActions: [ShortcutAction] = [.addToMix1, .addToMix2, .addToMix3]
  121. ForEach(0..<3, id: \.self) { slot in
  122. Button("Add to \(playlistVM.mixTargetName(slot))") {
  123. if let track = playerVM.currentTrack {
  124. NotificationCenter.default.post(
  125. name: .quickAddToMix,
  126. object: nil,
  127. userInfo: ["slot": slot, "track": track]
  128. )
  129. }
  130. }
  131. .keyboardShortcut(
  132. shortcutConfig.binding(for: mixActions[slot]).keyEquivalent,
  133. modifiers: shortcutConfig.binding(for: mixActions[slot]).eventModifiers
  134. )
  135. }
  136. Divider()
  137. Button("Export to DAW...") {
  138. playlistVM.showExportSheet = true
  139. }
  140. .keyboardShortcut(
  141. shortcutConfig.binding(for: .exportToDAW).keyEquivalent,
  142. modifiers: shortcutConfig.binding(for: .exportToDAW).eventModifiers
  143. )
  144. Divider()
  145. Button("Import from iPhone...") {
  146. NotificationCenter.default.post(name: .importFromiPhone, object: nil)
  147. }
  148. .keyboardShortcut(
  149. shortcutConfig.binding(for: .importFromiPhone).keyEquivalent,
  150. modifiers: shortcutConfig.binding(for: .importFromiPhone).eventModifiers
  151. )
  152. Divider()
  153. Button("Search All Playlists...") {
  154. NotificationCenter.default.post(name: .globalSearch, object: nil)
  155. }
  156. .keyboardShortcut(
  157. shortcutConfig.binding(for: .search).keyEquivalent,
  158. modifiers: shortcutConfig.binding(for: .search).eventModifiers
  159. )
  160. }
  161. }
  162. // Settings (⌘,)
  163. Settings {
  164. SettingsView()
  165. .environment(playlistVM)
  166. .environmentObject(theme)
  167. .preferredColorScheme(theme.preferredScheme)
  168. }
  169. .modelContainer(for: [Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self])
  170. // Now Playing window (Tidal-style separate window)
  171. Window("Now Playing", id: "now-playing") {
  172. NowPlayingView(displayMode: .floating)
  173. .environment(playerVM)
  174. .environment(playlistVM)
  175. .environmentObject(libraryManager)
  176. .environmentObject(theme)
  177. .preferredColorScheme(theme.preferredScheme)
  178. }
  179. .defaultSize(width: 850, height: 600)
  180. .windowStyle(.titleBar)
  181. }
  182. }
  183. // MARK: - Notification Names
  184. extension Notification.Name {
  185. static let newPlaylist = Notification.Name("newPlaylist")
  186. static let quickAddToTarget = Notification.Name("quickAddToTarget")
  187. static let quickAddToMix = Notification.Name("quickAddToMix")
  188. static let globalSearch = Notification.Name("globalSearch")
  189. static let importFromiPhone = Notification.Name("importFromiPhone")
  190. static let toggleNowPlaying = Notification.Name("toggleNowPlaying")
  191. static let toggleBrowsePanel = Notification.Name("toggleBrowsePanel")
  192. static let popOutNowPlaying = Notification.Name("popOutNowPlaying")
  193. static let closeInlineNowPlaying = Notification.Name("closeInlineNowPlaying")
  194. static let doubleClickPlayTrack = Notification.Name("doubleClickPlayTrack")
  195. }