MixBoardApp.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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. Divider()
  53. Button("Library") {
  54. NotificationCenter.default.post(name: .toggleBrowsePanel, object: nil)
  55. }
  56. .keyboardShortcut("b", modifiers: .command)
  57. }
  58. CommandMenu("Playback") {
  59. Button("Play / Pause") {
  60. playerVM.togglePlayPause()
  61. }
  62. .keyboardShortcut(
  63. shortcutConfig.binding(for: .playPause).keyEquivalent,
  64. modifiers: shortcutConfig.binding(for: .playPause).eventModifiers
  65. )
  66. Button("Stop") {
  67. playerVM.stop()
  68. }
  69. .keyboardShortcut(
  70. shortcutConfig.binding(for: .stop).keyEquivalent,
  71. modifiers: shortcutConfig.binding(for: .stop).eventModifiers
  72. )
  73. Divider()
  74. Button("Next Track") {
  75. playerVM.playNext()
  76. }
  77. .keyboardShortcut(
  78. shortcutConfig.binding(for: .nextTrack).keyEquivalent,
  79. modifiers: shortcutConfig.binding(for: .nextTrack).eventModifiers
  80. )
  81. Button("Previous Track") {
  82. playerVM.playPrevious()
  83. }
  84. .keyboardShortcut(
  85. shortcutConfig.binding(for: .previousTrack).keyEquivalent,
  86. modifiers: shortcutConfig.binding(for: .previousTrack).eventModifiers
  87. )
  88. Divider()
  89. Button("Skip Forward 10s") {
  90. playerVM.skipForward()
  91. }
  92. .keyboardShortcut(
  93. shortcutConfig.binding(for: .skipForward).keyEquivalent,
  94. modifiers: shortcutConfig.binding(for: .skipForward).eventModifiers
  95. )
  96. Button("Skip Backward 10s") {
  97. playerVM.skipBackward()
  98. }
  99. .keyboardShortcut(
  100. shortcutConfig.binding(for: .skipBackward).keyEquivalent,
  101. modifiers: shortcutConfig.binding(for: .skipBackward).eventModifiers
  102. )
  103. Divider()
  104. Button(playerVM.shuffleEnabled ? "Shuffle: On" : "Shuffle: Off") {
  105. playerVM.shuffleEnabled.toggle()
  106. }
  107. .keyboardShortcut(
  108. shortcutConfig.binding(for: .toggleShuffle).keyEquivalent,
  109. modifiers: shortcutConfig.binding(for: .toggleShuffle).eventModifiers
  110. )
  111. Button("Repeat: \(playerVM.repeatMode.rawValue)") {
  112. switch playerVM.repeatMode {
  113. case .off: playerVM.repeatMode = .all
  114. case .all: playerVM.repeatMode = .one
  115. case .one: playerVM.repeatMode = .off
  116. }
  117. }
  118. .keyboardShortcut(
  119. shortcutConfig.binding(for: .toggleRepeat).keyEquivalent,
  120. modifiers: shortcutConfig.binding(for: .toggleRepeat).eventModifiers
  121. )
  122. }
  123. CommandMenu("Mix") {
  124. // Per-slot quick-add
  125. let mixActions: [ShortcutAction] = [.addToMix1, .addToMix2, .addToMix3]
  126. ForEach(0..<3, id: \.self) { slot in
  127. Button("Add to \(playlistVM.mixTargetName(slot))") {
  128. if let track = playerVM.currentTrack {
  129. NotificationCenter.default.post(
  130. name: .quickAddToMix,
  131. object: nil,
  132. userInfo: ["slot": slot, "track": track]
  133. )
  134. }
  135. }
  136. .keyboardShortcut(
  137. shortcutConfig.binding(for: mixActions[slot]).keyEquivalent,
  138. modifiers: shortcutConfig.binding(for: mixActions[slot]).eventModifiers
  139. )
  140. }
  141. Divider()
  142. Button("Export to DAW...") {
  143. playlistVM.showExportSheet = true
  144. }
  145. .keyboardShortcut(
  146. shortcutConfig.binding(for: .exportToDAW).keyEquivalent,
  147. modifiers: shortcutConfig.binding(for: .exportToDAW).eventModifiers
  148. )
  149. Divider()
  150. Button("Import from iPhone...") {
  151. NotificationCenter.default.post(name: .importFromiPhone, object: nil)
  152. }
  153. .keyboardShortcut(
  154. shortcutConfig.binding(for: .importFromiPhone).keyEquivalent,
  155. modifiers: shortcutConfig.binding(for: .importFromiPhone).eventModifiers
  156. )
  157. Divider()
  158. Button("Search All Playlists...") {
  159. NotificationCenter.default.post(name: .globalSearch, object: nil)
  160. }
  161. .keyboardShortcut(
  162. shortcutConfig.binding(for: .search).keyEquivalent,
  163. modifiers: shortcutConfig.binding(for: .search).eventModifiers
  164. )
  165. }
  166. }
  167. // Settings (⌘,)
  168. Settings {
  169. SettingsView()
  170. .environment(playlistVM)
  171. .environmentObject(theme)
  172. .preferredColorScheme(theme.preferredScheme)
  173. }
  174. .modelContainer(for: [Track.self, CuePoint.self, Playlist.self, PlaylistEntry.self, PlaylistFolder.self])
  175. // Now Playing window (Tidal-style separate window)
  176. Window("Now Playing", id: "now-playing") {
  177. NowPlayingView(displayMode: .floating)
  178. .environment(playerVM)
  179. .environment(playlistVM)
  180. .environmentObject(libraryManager)
  181. .environmentObject(theme)
  182. .preferredColorScheme(theme.preferredScheme)
  183. }
  184. .defaultSize(width: 850, height: 600)
  185. .windowStyle(.titleBar)
  186. }
  187. }
  188. // MARK: - Notification Names
  189. extension Notification.Name {
  190. static let newPlaylist = Notification.Name("newPlaylist")
  191. static let quickAddToTarget = Notification.Name("quickAddToTarget")
  192. static let quickAddToMix = Notification.Name("quickAddToMix")
  193. static let globalSearch = Notification.Name("globalSearch")
  194. static let importFromiPhone = Notification.Name("importFromiPhone")
  195. static let toggleNowPlaying = Notification.Name("toggleNowPlaying")
  196. static let toggleBrowsePanel = Notification.Name("toggleBrowsePanel")
  197. static let popOutNowPlaying = Notification.Name("popOutNowPlaying")
  198. static let closeInlineNowPlaying = Notification.Name("closeInlineNowPlaying")
  199. static let doubleClickPlayTrack = Notification.Name("doubleClickPlayTrack")
  200. }