Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
105 kB · 2204 lines
Swift
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205// JukeController.swift — the JukeWizard window.//// left: the queue (every track across every lane), each row showing// its stars + a 💬 count; click to load.// right: the track title, the waveform player (click=seek,// ⌥click=comment), a transport row, a star-rating row, a notes// box, and the timestamped comments list (click a comment to// jump there). Everything persists to <track>.juke.json.//// --watch <dir> arms the auto-pop: when a fresh audio file lands in a// watched folder it's added to the queue and starts playing, window to// front — so a new render announces itself.import AppKit
// Juke keeps its teal / gold / coral identity, now as translucent washes over// the same liquid material used by Menu Band.enum Palette { static let teal = NSColor(srgbRed: 0.10, green: 0.52, blue: 0.55, alpha: 1) static let gold = NSColor(srgbRed: 0.95, green: 0.74, blue: 0.20, alpha: 1) static let coral = NSColor(srgbRed: 0.95, green: 0.45, blue: 0.38, alpha: 1) static let cream = NSColor(srgbRed: 0.99, green: 0.97, blue: 0.91, alpha: 1) static let inkDim = NSColor(srgbRed: 0.42, green: 0.40, blue: 0.36, alpha: 1) static func bg(_ dark: Bool) -> NSColor { dark ? NSColor(srgbRed: 0.04, green: 0.12, blue: 0.14, alpha: 0.10) : NSColor(srgbRed: 0.92, green: 0.98, blue: 1.00, alpha: 0.04) } static func deckSurface(_ accent: NSColor, dark: Bool, alpha: CGFloat = 1) -> NSColor { let base = dark ? NSColor(white: 0.025, alpha: 1) : cream let wash = dark ? 0.20 : 0.14 return base.blended(withFraction: wash, of: accent)?.withAlphaComponent(alpha) ?? base.withAlphaComponent(alpha) } static func deckInk(_ accent: NSColor, dark: Bool) -> NSColor { let target = dark ? NSColor.white : NSColor.black return accent.blended(withFraction: dark ? 0.38 : 0.48, of: target) ?? target }}
final class JukeController: NSWindowController, NSWindowDelegate, NSTableViewDataSource, NSTableViewDelegate, WaveformViewDelegate, NSTextViewDelegate { let library: Library let watchDirs: [String] let selectPath: String? var playlistName: String? let fullLibraryPath: String var current: Int = -1 var watchTimer: Timer? var activityTimer: Timer? var activityPollInFlight = false var watchMtimes: [String: Date] = [:] var keyMonitor: Any?
// sort enum SortMode: Int, CaseIterable { case defaultOrder, newest, oldest, stars, title, lane, bpm, duration var label: String { switch self { case .defaultOrder: return "status · recent" case .newest: return "newest rendered" case .oldest: return "oldest rendered" case .stars: return "★ rating" case .title: return "title A–Z" case .lane: return "lane" case .bpm: return "BPM" case .duration: return "duration" } } } var sortMode: SortMode = .defaultOrder var sortPopup: NSPopUpButton! var scopeButton: NSButton!
// release-link services (button per platform, shown when a URL exists) enum LinkService: Int, CaseIterable { case spotify, apple, youtube, distrokid var title: String { switch self { case .spotify: return "♫ Spotify" case .apple: return " Apple" case .youtube: return "▶ YouTube" case .distrokid: return "◆ DistroKid" } } var color: NSColor { switch self { case .spotify: return NSColor(srgbRed: 0.11, green: 0.73, blue: 0.33, alpha: 1) case .apple: return NSColor(srgbRed: 0.98, green: 0.24, blue: 0.36, alpha: 1) case .youtube: return NSColor(srgbRed: 0.90, green: 0.13, blue: 0.13, alpha: 1) case .distrokid: return Palette.inkDim } } func url(_ l: TrackLinks?) -> String? { switch self { case .spotify: return l?.spotify case .apple: return l?.apple case .youtube: return l?.youtube case .distrokid: return l?.distrokid } } }
// views — Winamp-style: now-playing header on top, track list underneath. var nowPlaying: NowPlayingMedia! var listTable: NSTableView! var titleLabel: NSTextField! var artistLabel: NSTextField! var laneLabel: NSTextField! var activityLabel: NSTextField! var linkButtons: [NSButton] = [] var wave: WaveformView! var spotifyProgress: SpotifyProgressView! var spotifySearchField: NSSearchField! var sourceTabs: NSSegmentedControl! var appearanceTabs: NSSegmentedControl! var playButton: NSButton! var speedSlider: NSSlider! var speedLabel: NSTextField! var speedResetButton: NSButton! var ledLabel: NSTextField! var notesToggle: NSButton! var sourceActionButton: NSButton! var cloudWindow: JukeCloudWindowController? var djMixer: DJMixerView! var providerDeck: JukeProviderDeckView! var djMode = false var djConfigured = false var roomButton: NSButton! var roomPopover: NSPopover? var roomMixer: RoomMixerView? var miniPopover: NSPopover? var miniPlayer: JukeMiniPlayerView? let roomAudio = JukeRoomAudio() let spotify = JukeSpotify() let cloud = JukeCloudClient() private lazy var appleMusic = JukeAppleMusic() var activeSource: JukeSource = .local var spotifyMode = false var appleMusicMode = false var spotifyResults: [SpotifyTrackResult] = [] var selectedSpotifyRow = -1 var spotifyState: SpotifyPlaybackState? var spotifyArtworkURL: URL? var spotifyArt: NSImage? var cloudTracks: [JukeCloudTrack] = [] var selectedCloudRow = -1 var appleMusicResults: [AppleMusicTrackResult] = [] var selectedAppleMusicRow = -1 var appleMusicPlaying = false var appleMusicArtworkURL: URL? var appleMusicArt: NSImage? var drawerPanel: NSView! var drawerOpen = false var currentArt: NSImage? var quickVolume: Float = 0.8 var commentNowButton: NSButton! var starButtons: [NSButton] = [] var notesView: NSTextView! var notesScroll: NSScrollView! var commentsTable: NSTableView! var listScroll: NSScrollView! var commentsScroll: NSScrollView!
let sidebarW: CGFloat = 290
enum AppearanceMode: Int { case automatic, light, dark } var appearanceMode: AppearanceMode = .automatic
init(library: Library, watch: [String], select selectArg: String? = nil, spotifySearch: String? = nil, startPrimpats: Bool = false, startBeats: Bool = false, playlistName: String? = nil, fullLibraryPath: String) { self.library = library self.watchDirs = watch self.selectPath = selectArg self.playlistName = playlistName self.fullLibraryPath = fullLibraryPath let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 820, height: 568), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false) window.title = "" window.setAccessibilityLabel("Menu Band Juke") // Juke is a compact Menu Band listening surface: keep it visible above // normal document windows and available across Spaces. Previously it // could fall behind a full-screen development stack while its process // remained healthy, which looked exactly like a crash. window.level = .floating window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] window.hidesOnDeactivate = false window.isMovableByWindowBackground = true window.isOpaque = false window.backgroundColor = .clear window.titlebarAppearsTransparent = true window.titleVisibility = .hidden window.titlebarSeparatorStyle = .none window.hasShadow = true if let frame = window.contentView?.bounds { window.contentView = JukeLiquidSurface(frame: frame) } window.isRestorable = false // don't let AppKit re-select a stale row over our pick window.isReleasedWhenClosed = false // keep it around so the menu-bar CD can reopen it window.minSize = NSSize(width: 720, height: 420) window.center() super.init(window: window) window.delegate = self setupUI() appearanceMode = AppearanceMode(rawValue: UserDefaults.standard.integer(forKey: "appearanceMode")) ?? .automatic appearanceTabs.selectedSegment = appearanceMode.rawValue applyAppearance() DistributedNotificationCenter.default().addObserver( forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"), object: nil, queue: .main ) { [weak self] _ in guard self?.appearanceMode == .automatic else { return } self?.applyAppearance() } if UserDefaults.standard.object(forKey: "playerVolume") != nil { quickVolume = UserDefaults.standard.float(forKey: "playerVolume") } wave.volume = quickVolume if UserDefaults.standard.object(forKey: "listPlaybackRate") != nil { speedSlider.doubleValue = max(0.5, min(1.5, UserDefaults.standard.double(forKey: "listPlaybackRate"))) } speedLabel.stringValue = String(format: "%.2f×", speedSlider.doubleValue) relayout() roomAudio.onState = { [weak self] state in DispatchQueue.main.async { self?.renderRoomState(state) } } spotify.onState = { [weak self] state in self?.renderSpotifyState(state) } spotify.onStatus = { [weak self] message, failed in guard let self, self.spotifyMode else { return } self.activityLabel.stringValue = message self.activityLabel.textColor = failed ? .systemRed : Palette.teal } // open on the requested track (and play it) if given; else the top. if let sp = selectPath { let want = URL(fileURLWithPath: (sp as NSString).expandingTildeInPath).standardizedFileURL.path if let idx = library.tracks.firstIndex(where: { $0.url.standardizedFileURL.path == want }) { select(idx, autoplay: true) } else if !library.tracks.isEmpty { select(0, autoplay: false) } } else if !library.tracks.isEmpty { select(0, autoplay: false) } let requestedSpotify = !(spotifySearch?.trimmingCharacters( in: .whitespacesAndNewlines).isEmpty ?? true) requestedSpotify ? activateSource(.spotify) : activateSource(.local) if startBeats { setDJMode(true, singleDeck: true) djMixer.loadBeats(solo: true) } else if startPrimpats { setDJMode(true) djMixer.loadPrimpats() } spotify.start() DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in guard let self, case .idle = self.roomAudio.state else { return } self.roomAudio.apply(self.savedRoomLayout(), pan: self.savedRoomPan()) } if let query = spotifySearch?.trimmingCharacters(in: .whitespacesAndNewlines), !query.isEmpty { spotifySearchField.stringValue = query searchSpotify(query, autoplayFirst: true) } armWatch() armActivityStatus() installKeyMonitor() } required init?(coder: NSCoder) { fatalError() } deinit { watchTimer?.invalidate() activityTimer?.invalidate() djMixer?.stopDisplay() if let m = keyMonitor { NSEvent.removeMonitor(m) } }
// Keep the single player window alive: the menu-bar CD and Dock icon can // restore it instantly, and playback/queue state cannot be lost on close. func windowShouldClose(_ sender: NSWindow) -> Bool { suspendPresentationActivity() sender.orderOut(nil) return false }
func windowDidMiniaturize(_ notification: Notification) { suspendPresentationActivity() }
func windowDidDeminiaturize(_ notification: Notification) { resumePresentationActivity() }
// ── keyboard control (yields to text editing) ──────────────────────── private func installKeyMonitor() { keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] e in guard let self, self.window?.isKeyWindow == true else { return e } if let r = self.window?.firstResponder, r is NSText { return e } // editing notes return self.handleKey(e) ? nil : e } } private func handleKey(_ e: NSEvent) -> Bool { switch e.keyCode { case 53: if playlistName != nil { escapePlaylist(); return true } return false // esc leaves playlist scope case 49: togglePlay(); return true // space case 123: if djMode { djMixer.dominantDeck.deck.seek(to: djMixer.dominantDeck.deck.currentTime - 5) } else if spotifyMode { spotify.seek(offsetMS: -5000) } else if !appleMusicMode { wave.seek(to: wave.currentTime - 5) } return true // ← back 5s case 124: if djMode { djMixer.dominantDeck.deck.seek(to: djMixer.dominantDeck.deck.currentTime + 5) } else if spotifyMode { spotify.seek(offsetMS: 5000) } else if !appleMusicMode { wave.seek(to: wave.currentTime + 5) } return true // → fwd 5s case 126: prevTrack(); return true // ↑ prev track case 125: nextTrack(); return true // ↓ next track case 18, 19, 20, 21, 23: // 1–5 stars let map: [UInt16: Int] = [18: 1, 19: 2, 20: 3, 21: 4, 23: 5] if activeSource == .local, let n = map[e.keyCode], let t = track { t.data.stars = n; renderStars(n); t.save() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) } return true case 8: if activeSource == .local { addCommentNow() }; return true // c comment @ now case 29: if activeSource == .local { clearStarsClicked() }; return true // 0 clear stars default: return false } }
// ── construction ───────────────────────────────────────────────────── private func setupUI() { guard let content = window?.contentView else { return } content.wantsLayer = true applyThemeBackground()
// ── now-playing header: big art / streaming video on the left ───────── nowPlaying = NowPlayingMedia(frame: .zero) content.addSubview(nowPlaying)
titleLabel = label("", size: 19, bold: true) titleLabel.lineBreakMode = .byTruncatingTail titleLabel.textColor = Palette.gold artistLabel = label("", size: 13, color: .secondaryLabelColor) laneLabel = label("", size: 11, color: .secondaryLabelColor) laneLabel.lineBreakMode = .byTruncatingTail content.addSubview(titleLabel); content.addSubview(artistLabel); content.addSubview(laneLabel) activityLabel = label("● watching agents + renders", size: 10, color: Palette.teal) activityLabel.lineBreakMode = .byTruncatingTail content.addSubview(activityLabel)
for svc in LinkService.allCases { let b = NSButton(title: svc.title, target: self, action: #selector(linkClicked(_:))) b.bezelStyle = .inline b.tag = svc.rawValue b.contentTintColor = svc.color b.isHidden = true linkButtons.append(b) content.addSubview(b) }
wave = WaveformView(frame: .zero) wave.delegate = self content.addSubview(wave)
spotifyProgress = SpotifyProgressView(frame: .zero) spotifyProgress.isHidden = true spotifyProgress.onSeek = { [weak self] target in guard let self, let state = self.spotifyState else { return } self.spotify.seek(offsetMS: Int((target - state.position) * 1000)) } content.addSubview(spotifyProgress)
playButton = NSButton(title: "▶", target: self, action: #selector(togglePlay)) playButton.bezelStyle = .rounded playButton.setButtonType(.momentaryPushIn) content.addSubview(playButton) speedSlider = NSSlider(value: 1, minValue: 0.5, maxValue: 1.5, target: self, action: #selector(speedChanged(_:))) speedSlider.controlSize = .small speedSlider.isContinuous = true speedSlider.toolTip = "Playback speed · drag the waveform to scratch" content.addSubview(speedSlider) speedLabel = label("1.00×", size: 11, color: Palette.teal) speedLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .semibold) speedLabel.alignment = .right content.addSubview(speedLabel) speedResetButton = NSButton(title: "↺", target: self, action: #selector(resetSpeed)) speedResetButton.bezelStyle = .inline speedResetButton.toolTip = "Reset playback speed" speedResetButton.contentTintColor = Palette.teal content.addSubview(speedResetButton) let prev = NSButton(title: "⏮", target: self, action: #selector(prevTrack)) prev.bezelStyle = .rounded; prev.tag = 1 let next = NSButton(title: "⏭", target: self, action: #selector(nextTrack)) next.bezelStyle = .rounded; next.tag = 2 content.addSubview(prev); content.addSubview(next) transportExtra = [prev, next]
// green LED time readout (Winamp signature) — monospace, glowing green. ledLabel = label("0:00 / 0:00", size: 15, color: NSColor(srgbRed: 0.30, green: 1.0, blue: 0.45, alpha: 1)) ledLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 15, weight: .bold) ledLabel.alignment = .right content.addSubview(ledLabel)
notesToggle = NSButton(title: "♪ notes", target: self, action: #selector(toggleDrawer)) notesToggle.bezelStyle = .rounded notesToggle.setButtonType(.pushOnPushOff) content.addSubview(notesToggle)
roomButton = NSButton(title: "▰ Neo L · ▰ Blueberry R", target: self, action: #selector(showRoomMixer)) roomButton.bezelStyle = .rounded roomButton.setButtonType(.momentaryPushIn) roomButton.toolTip = "Choose the MacBooks, channels, and pan for room playback" if #unavailable(macOS 14.2) { roomButton.isEnabled = false roomButton.toolTip = "Room audio requires macOS 14.2 or newer" } content.addSubview(roomButton)
// ── the track list (underneath) ─────────────────────────────────────── sortPopup = NSPopUpButton(frame: .zero, pullsDown: false) sortPopup.addItems(withTitles: SortMode.allCases.map { "sort: \($0.label)" }) sortPopup.selectItem(at: sortMode.rawValue) sortPopup.target = self sortPopup.action = #selector(sortChanged(_:)) sortPopup.bezelStyle = .rounded sortPopup.controlSize = .small sortPopup.font = NSFont.systemFont(ofSize: 11) content.addSubview(sortPopup)
scopeButton = NSButton(title: "", target: self, action: #selector(escapePlaylist)) scopeButton.bezelStyle = .inline scopeButton.controlSize = .small scopeButton.contentTintColor = Palette.coral scopeButton.toolTip = "Leave this playlist and show all local tracks (Esc)" content.addSubview(scopeButton) refreshPlaylistScopeButton()
sourceTabs = NSSegmentedControl(labels: JukeSource.allCases.map { $0.label() }, trackingMode: .selectOne, target: self, action: #selector(sourceTabChanged)) sourceTabs.selectedSegment = JukeSource.local.rawValue sourceTabs.controlSize = .small content.addSubview(sourceTabs)
appearanceTabs = NSSegmentedControl(labels: ["Auto", "Light", "Dark"], trackingMode: .selectOne, target: self, action: #selector(appearanceChanged)) appearanceTabs.selectedSegment = 0 appearanceTabs.controlSize = .small appearanceTabs.toolTip = "Follow macOS, or pin Menu Band Juke to light or dark" content.addSubview(appearanceTabs)
sourceActionButton = NSButton(title: "Add files…", target: self, action: #selector(sourceActionClicked)) sourceActionButton.bezelStyle = .rounded sourceActionButton.contentTintColor = Palette.teal content.addSubview(sourceActionButton)
spotifySearchField = NSSearchField(frame: .zero) spotifySearchField.placeholderString = "Search" spotifySearchField.sendsSearchStringImmediately = false spotifySearchField.sendsWholeSearchString = true spotifySearchField.target = self spotifySearchField.action = #selector(searchActiveSourceFromField) content.addSubview(spotifySearchField)
listTable = NSTableView() let col = NSTableColumn(identifier: .init("track")) listTable.addTableColumn(col) listTable.headerView = nil listTable.rowHeight = TrackRowView.height listTable.backgroundColor = .clear listTable.selectionHighlightStyle = .none listTable.dataSource = self listTable.delegate = self listTable.setDraggingSourceOperationMask([.copy], forLocal: false) listTable.target = self listTable.action = #selector(listClicked) listScroll = NSScrollView() listScroll.documentView = listTable listScroll.hasVerticalScroller = true listScroll.drawsBackground = false listScroll.borderType = .noBorder content.addSubview(listScroll)
buildDrawer(in: content)
djMixer = DJMixerView(frame: .zero) djMixer.isHidden = true djMixer.onStateChange = { [weak self] in self?.refreshPlaybackPresence() } djMixer.onDetach = { [weak self] in self?.refreshPlaybackPresence() } content.addSubview(djMixer)
providerDeck = JukeProviderDeckView(frame: .zero) providerDeck.isHidden = true providerDeck.onToggle = { [weak self] in self?.togglePlay() } providerDeck.onSeek = { [weak self] target in guard let self, self.spotifyMode, let state = self.spotifyState else { return } self.spotify.seek(offsetMS: Int((target - state.position) * 1000)) } content.addSubview(providerDeck)
playButton.contentTintColor = Palette.teal transportExtra.forEach { $0.contentTintColor = Palette.teal } }
// The collapsible notes + comments + rating drawer (hidden by default). private func buildDrawer(in content: NSView) { drawerPanel = NSView() drawerPanel.wantsLayer = true drawerPanel.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.52).cgColor drawerPanel.layer?.cornerRadius = 10 drawerPanel.layer?.borderWidth = 1 drawerPanel.layer?.borderColor = Palette.teal.withAlphaComponent(0.5).cgColor drawerPanel.isHidden = true content.addSubview(drawerPanel)
for i in 1...5 { let b = NSButton(title: "☆", target: self, action: #selector(starClicked(_:))) b.tag = i; b.isBordered = false b.font = NSFont.systemFont(ofSize: 22) b.contentTintColor = .systemYellow starButtons.append(b); drawerPanel.addSubview(b) } clearStars = NSButton(title: "clear", target: self, action: #selector(clearStarsClicked)) clearStars.bezelStyle = .inline; clearStars.contentTintColor = Palette.inkDim drawerPanel.addSubview(clearStars)
notesView = NSTextView() notesView.isRichText = false notesView.font = NSFont.systemFont(ofSize: 13) notesView.delegate = self notesView.isAutomaticQuoteSubstitutionEnabled = false notesScroll = NSScrollView() notesScroll.documentView = notesView notesScroll.hasVerticalScroller = true notesScroll.borderType = .bezelBorder drawerPanel.addSubview(notesScroll) notesPlaceholder = label("notes…", size: 12, color: .tertiaryLabelColor) drawerPanel.addSubview(notesPlaceholder)
commentNowButton = NSButton(title: "+ comment @ now", target: self, action: #selector(addCommentNow)) commentNowButton.bezelStyle = .rounded; commentNowButton.contentTintColor = Palette.coral drawerPanel.addSubview(commentNowButton)
commentsTable = NSTableView() let cc = NSTableColumn(identifier: .init("comment")) commentsTable.addTableColumn(cc) commentsTable.headerView = nil commentsTable.rowHeight = 24 commentsTable.backgroundColor = .clear commentsTable.dataSource = self commentsTable.delegate = self commentsTable.target = self commentsTable.doubleAction = #selector(commentDoubleClicked) commentsTable.action = #selector(commentClicked) commentsScroll = NSScrollView() commentsScroll.documentView = commentsTable commentsScroll.hasVerticalScroller = true commentsScroll.drawsBackground = false commentsScroll.borderType = .bezelBorder drawerPanel.addSubview(commentsScroll) commentsHeader = label("comments · click to jump", size: 11, color: .secondaryLabelColor) drawerPanel.addSubview(commentsHeader) delCommentButton = NSButton(title: "– delete", target: self, action: #selector(deleteComment)) delCommentButton.bezelStyle = .inline; delCommentButton.contentTintColor = Palette.coral drawerPanel.addSubview(delCommentButton)
commentsHeader.textColor = Palette.inkDim notesScroll.drawsBackground = false notesView.drawsBackground = false }
@objc private func toggleDrawer() { drawerOpen.toggle() drawerPanel.isHidden = !drawerOpen notesToggle.state = drawerOpen ? .on : .off relayout() }
private func setDJMode(_ enabled: Bool, singleDeck: Bool = false) { guard enabled != djMode else { return } if enabled { wave.pause() playButton.title = "▶" nowPlaying.setPaused(true) if drawerOpen { drawerOpen = false notesToggle.state = .off } if !djConfigured { if singleDeck { djMixer.configureSolo(tracks: library.tracks, primaryIndex: max(0, current)) } else { djMixer.configure(tracks: library.tracks, primaryIndex: max(0, current)) } djConfigured = true } djMixer.setMasterVolume(quickVolume) djMode = true djMixer.isHidden = false providerDeck.isHidden = true setPlayerChromeHidden(true) listScroll.isHidden = false activityLabel.isHidden = false if isPresentationActive { djMixer.startDisplay() } roomAudio.useSource(.aesthetic) window?.isMovableByWindowBackground = false } else { djMixer.stopDisplay() djMixer.isHidden = true djMode = false window?.isMovableByWindowBackground = true if spotifyMode || appleMusicMode { setPlayerChromeHidden(true) listScroll.isHidden = false activityLabel.isHidden = false spotifySearchField.isHidden = false } else { setPlayerChromeHidden(false) } } relayout() refreshPlaybackPresence() }
private func setPlayerChromeHidden(_ hidden: Bool) { [nowPlaying, titleLabel, artistLabel, laneLabel, activityLabel, playButton, ledLabel, notesToggle, roomButton, speedSlider, speedLabel, speedResetButton, sortPopup, spotifySearchField, listScroll, wave, spotifyProgress, drawerPanel].forEach { $0?.isHidden = hidden } transportExtra.forEach { $0.isHidden = hidden } linkButtons.forEach { $0.isHidden = hidden } guard !hidden else { return }
nowPlaying.isHidden = false titleLabel.isHidden = false artistLabel.isHidden = false laneLabel.isHidden = false activityLabel.isHidden = false playButton.isHidden = false ledLabel.isHidden = false roomButton.isHidden = false let externalSource = spotifyMode || appleMusicMode speedSlider.isHidden = externalSource speedLabel.isHidden = externalSource speedResetButton.isHidden = externalSource listScroll.isHidden = false transportExtra.forEach { $0.isHidden = false } wave.isHidden = externalSource spotifyProgress.isHidden = !externalSource sortPopup.isHidden = externalSource spotifySearchField.isHidden = !externalSource notesToggle.isHidden = false drawerPanel.isHidden = !drawerOpen if !spotifyMode, let t = track { loadLinks(t) } }
private func applyThemeBackground() { let dark = window?.effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) == .darkAqua let chassis = Palette.bg(dark) window?.backgroundColor = .clear (window?.contentView as? JukeLiquidSurface)?.setTint(chassis) window?.contentView?.needsDisplay = true listTable?.reloadData() }
private func applyAppearance() { switch appearanceMode { case .automatic: window?.appearance = nil case .light: window?.appearance = NSAppearance(named: .aqua) case .dark: window?.appearance = NSAppearance(named: .darkAqua) } djMixer?.setAppearance(window?.appearance) providerDeck?.needsDisplay = true applyThemeBackground() }
func showDetachedPrimpats() { if !djMode { setDJMode(true) } djMixer.loadPrimpats(openPopouts: true) }
func showDetachedBeats() { if !djMode { setDJMode(true, singleDeck: true) } djMixer.loadBeats(openPopouts: true, autoplay: true, solo: true) }
@objc private func appearanceChanged() { appearanceMode = AppearanceMode(rawValue: appearanceTabs.selectedSegment) ?? .automatic UserDefaults.standard.set(appearanceMode.rawValue, forKey: "appearanceMode") applyAppearance() }
var transportExtra: [NSButton] = [] var clearStars: NSButton! var notesPlaceholder: NSTextField! var commentsHeader: NSTextField! var delCommentButton: NSButton!
private func label(_ s: String, size: CGFloat, bold: Bool = false, color: NSColor = .labelColor) -> NSTextField { let l = NSTextField(labelWithString: s) l.font = bold ? NSFont.boldSystemFont(ofSize: size) : NSFont.systemFont(ofSize: size) l.textColor = color return l }
// ── layout ─────────────────────────────────────────────────────────── func windowDidResize(_ notification: Notification) { relayout() }
private func relayout() { guard let content = window?.contentView else { return } let W = content.bounds.width, H = content.bounds.height let pad: CGFloat = 8 let topBarH: CGFloat = 34 let titlebarInset = max(content.safeAreaInsets.top, 28) let usableTop = H - titlebarInset let contentTop = usableTop - topBarH let tabsWidth = min(470, max(360, W - 290)) sourceTabs.frame = NSRect(x: pad, y: usableTop - 27, width: tabsWidth, height: 22) appearanceTabs.frame = NSRect(x: W - pad - 172, y: usableTop - 27, width: 172, height: 22) sourceActionButton.frame = NSRect(x: max(pad, W - pad - 272), y: usableTop - 28, width: 92, height: 24) scopeButton.frame = .zero scopeButton.isHidden = true
let externalSource = spotifyMode || appleMusicMode if djMode || externalSource { let browserWidth = max(260, min(370, W * 0.42)) let controlsY = contentTop - 24 if activeSource == .local { sortPopup.isHidden = false spotifySearchField.isHidden = true sortPopup.frame = NSRect(x: pad, y: controlsY, width: browserWidth, height: 22) } else { sortPopup.isHidden = true spotifySearchField.isHidden = false spotifySearchField.frame = NSRect(x: pad, y: controlsY, width: browserWidth, height: 22) } activityLabel.frame = NSRect(x: pad, y: controlsY - 19, width: browserWidth, height: 15) listScroll.frame = NSRect(x: pad, y: pad, width: browserWidth, height: max(80, controlsY - pad - 22)) let deckX = pad + browserWidth + 10 let deckFrame = NSRect(x: deckX, y: pad, width: W - deckX - pad, height: contentTop - pad) djMixer.frame = deckFrame providerDeck.frame = deckFrame return } // ── header (now-playing) across the top ─────────────────────────────── let headerH = max(178, min(245, (usableTop - topBarH) * 0.39)) let headerBottom = contentTop - headerH let mediaSide = min(headerH - pad * 2, W * 0.34) nowPlaying.frame = NSRect(x: pad, y: headerBottom + pad, width: mediaSide, height: headerH - pad * 2) let rx = pad + mediaSide + 10 let rw = max(120, W - rx - pad)
var y = contentTop - pad - 25 titleLabel.frame = NSRect(x: rx, y: y, width: rw, height: 25) y -= 19 artistLabel.frame = NSRect(x: rx, y: y, width: rw, height: 17) y -= 15 laneLabel.frame = NSRect(x: rx, y: y, width: rw, height: 16) y -= 16 activityLabel.frame = NSRect(x: rx, y: y, width: rw, height: 14) y -= 21 var lx = rx // per-service link buttons row for b in linkButtons where !b.isHidden { let bw = b.attributedTitle.size().width + 16 b.frame = NSRect(x: lx, y: y, width: bw, height: 20) lx += bw + 6 } let linksBottom = y
// transport row pinned to the header's bottom edge let transY = headerBottom + pad transportExtra[0].frame = NSRect(x: rx, y: transY, width: 34, height: 25) playButton.frame = NSRect(x: rx + 37, y: transY, width: 44, height: 25) transportExtra[1].frame = NSRect(x: rx + 84, y: transY, width: 34, height: 25) notesToggle.frame = NSRect(x: rx + 124, y: transY, width: 75, height: 25) let roomWidth = max(70, min(210, rw - 203 - 145)) roomButton.frame = NSRect(x: rx + 203, y: transY, width: roomWidth, height: 25) ledLabel.frame = NSRect(x: rx + rw - 135, y: transY + 3, width: 135, height: 20)
// waveform fills the space between the links row and the transport let waveTop = linksBottom - 6 let speedY = transY + 30 speedLabel.frame = NSRect(x: rx, y: speedY + 1, width: 45, height: 18) speedSlider.frame = NSRect(x: rx + 48, y: speedY, width: max(70, rw - 80), height: 20) speedResetButton.frame = NSRect(x: rx + rw - 27, y: speedY, width: 27, height: 20) let waveBottom = speedY + 23 wave.frame = NSRect(x: rx, y: waveBottom, width: rw, height: max(32, waveTop - waveBottom)) spotifyProgress.frame = wave.frame
// ── track list underneath ───────────────────────────────────────────── let sortY = headerBottom - 2 - 20 sortPopup.frame = NSRect(x: pad, y: sortY, width: 180, height: 20) let searchX = externalSource ? pad : pad + 184 spotifySearchField.frame = NSRect(x: searchX, y: sortY, width: max(120, W - searchX - pad), height: 20) listScroll.frame = NSRect(x: pad, y: pad, width: W - pad * 2, height: sortY - pad - 3)
// ── drawer overlays the list when open ──────────────────────────────── if drawerOpen { drawerPanel.frame = listScroll.frame.insetBy(dx: 0, dy: 0) layoutDrawer() } }
private func layoutDrawer() { let dw = drawerPanel.bounds.width, dh = drawerPanel.bounds.height, dp: CGFloat = 12 let starsY = dh - dp - 30 for (i, b) in starButtons.enumerated() { b.frame = NSRect(x: dp + CGFloat(i) * 30, y: starsY, width: 30, height: 30) } clearStars.frame = NSRect(x: dp + 5 * 30 + 8, y: starsY + 4, width: 50, height: 22) commentNowButton.frame = NSRect(x: dw - 180 - dp, y: starsY, width: 180, height: 30) let commentsH = max(90, (dh - dp * 2) * 0.4) commentsHeader.frame = NSRect(x: dp, y: dp + commentsH + 2, width: dw - 90, height: 16) delCommentButton.frame = NSRect(x: dw - 70 - dp, y: dp + commentsH + 2, width: 70, height: 18) commentsScroll.frame = NSRect(x: dp, y: dp, width: dw - dp * 2, height: commentsH) let notesTop = starsY - 8 let notesBottom = dp + commentsH + 22 let notesH = max(50, notesTop - notesBottom) notesScroll.frame = NSRect(x: dp, y: notesBottom, width: dw - dp * 2, height: notesH) notesPlaceholder.frame = NSRect(x: dp + 6, y: notesBottom + notesH - 20, width: 100, height: 16) }
// Keep Menu Band Juke's in-window playback surfaces in sync. Application // identity and status items remain exclusively owned by Menu Band. /// Fires on every playback-presence change (track select, play/pause, DJ /// or provider state, art arrival). MenuBandJuke forwards this to the /// menu-bar now-playing disc. var onPresenceChange: (() -> Void)?
private func refreshPlaybackPresence() { miniPlayer?.refresh() onPresenceChange?() }
/// Whether anything is actually loaded — the menu-bar disc hides when the /// Juke is just an empty library window. var quickHasContent: Bool { djMode || spotifyMode || appleMusicMode || track != nil }
var quickArtist: String { if djMode { return "DJ mix" } if spotifyMode { return spotifyState?.artists ?? "Spotify" } if appleMusicMode, appleMusicResults.indices.contains(selectedAppleMusicRow) { return appleMusicResults[selectedAppleMusicRow].artist } return track?.meta?.artist ?? "Aesthetic Dot Computer" }
private func refreshPlaylistScopeButton() { guard let scopeButton else { return } scopeButton.isHidden = playlistName == nil scopeButton.title = playlistName.map { "\($0) · All tracks" } ?? "" }
@objc private func escapePlaylist() { guard playlistName != nil else { return } let currentPath = track?.url.standardizedFileURL.path library.add(path: fullLibraryPath) applySort() // fold-ins land in JSON order; honor whatever the popup says playlistName = nil refreshPlaylistScopeButton() if let currentPath, let index = library.tracks.firstIndex(where: { $0.url.standardizedFileURL.path == currentPath }) { current = index } listTable.reloadData() activityLabel.stringValue = "● full Aesthetic library · \(library.tracks.count) tracks" activityLabel.textColor = Palette.teal relayout() }
@objc private func showRoomMixer() { let mixer = RoomMixerView(frame: NSRect(x: 0, y: 0, width: 420, height: 220)) mixer.onLayout = { [weak self, weak mixer] layout in guard let self else { return } UserDefaults.standard.set(layout.rawValue, forKey: "roomLayout") self.roomAudio.apply(layout, pan: self.roomAudio.pan) mixer?.show(self.roomAudio.state, layout: self.roomAudio.layout, pan: self.roomAudio.pan) } mixer.onPan = { [weak self, weak mixer] pan in guard let self else { return } UserDefaults.standard.set(JukeRoomAudio.Layout.panMono.rawValue, forKey: "roomLayout") UserDefaults.standard.set(pan, forKey: "roomPan") self.roomAudio.apply(.panMono, pan: pan) mixer?.show(self.roomAudio.state, layout: self.roomAudio.layout, pan: self.roomAudio.pan) } mixer.show(roomAudio.state, layout: roomAudio.layout, pan: roomAudio.pan) let controller = NSViewController() controller.view = mixer let popover = NSPopover() popover.behavior = .transient popover.contentSize = NSSize(width: 420, height: 220) popover.contentViewController = controller popover.show(relativeTo: roomButton.bounds, of: roomButton, preferredEdge: .minY) roomMixer = mixer roomPopover = popover }
private func savedRoomLayout() -> JukeRoomAudio.Layout { guard UserDefaults.standard.object(forKey: "roomLayout") != nil else { return .splitLR } return JukeRoomAudio.Layout(rawValue: UserDefaults.standard.integer(forKey: "roomLayout")) ?? .splitLR }
private func savedRoomPan() -> Float { guard UserDefaults.standard.object(forKey: "roomPan") != nil else { return 0 } return UserDefaults.standard.float(forKey: "roomPan") }
private func renderRoomState(_ state: JukeRoomAudio.State) { // The room sender presents audio 700 ms in the future (RoomSender // latencyMilliseconds) and mutes the deck's direct output — so while // the room is live, every drawn playhead must lag by that much too. if case .live = state { DJDeckPlayer.roomDisplayLatency = 0.7 } else { DJDeckPlayer.roomDisplayLatency = 0 } switch state { case .idle: roomButton.state = .off roomButton.title = "▰ Neo · ▰ Blueberry" roomButton.contentTintColor = Palette.teal case .live(let snapshot): roomButton.state = .on switch snapshot.layout { case .neoStereo: roomButton.title = "▰ Neo · stereo" case .blueberryStereo: roomButton.title = "▰ Blueberry · stereo" case .mirrorStereo: roomButton.title = "▰ Neo + ▰ Blueberry" case .splitLR: roomButton.title = "▰ Neo L · ▰ Blueberry R" case .splitRL: roomButton.title = "▰ Neo R · ▰ Blueberry L" case .panMono: roomButton.title = "▰ Neo ↔ ▰ Blueberry" } roomButton.toolTip = "\(snapshot.source.rawValue): Neo \(snapshot.neo), Blueberry \(snapshot.blueberry)" roomButton.contentTintColor = Palette.coral case .failed(let message): roomButton.state = .off roomButton.title = "⚠ room" roomButton.toolTip = message roomButton.contentTintColor = .systemRed } roomMixer?.show(state, layout: roomAudio.layout, pan: roomAudio.pan) miniPlayer?.refresh() }
// ── music sources ───────────────────────────────────────────────────── private func activateSource(_ source: JukeSource) { let departingSource = activeSource if departingSource == .spotify, source != .spotify { spotify.pause() } if departingSource == .appleMusic, source != .appleMusic { appleMusicPlaying = false if #available(macOS 14.0, *) { appleMusic.pause() } } if source == .spotify || source == .appleMusic { djMixer.pauseAll() } activeSource = source spotifyMode = source == .spotify appleMusicMode = source == .appleMusic sourceTabs?.selectedSegment = source.rawValue djMixer?.setRecordDetachmentAllowed(source.canDetachRecords)
if drawerOpen { drawerOpen = false drawerPanel?.isHidden = true notesToggle?.state = .off }
switch source { case .local: providerDeck.isHidden = true sourceActionButton.title = "Add files…" sourceActionButton.isEnabled = true spotifySearchField.placeholderString = "Search \(source.label())" setDJMode(true, singleDeck: true) activityLabel.stringValue = "\(library.tracks.count) tracks on \(source.label())" activityLabel.textColor = Palette.gold listTable.reloadData() pollActivityStatus() case .aesthetic: providerDeck.isHidden = true sourceActionButton.title = ACSession.shared.token() == nil ? "Sign in" : "Publish…" sourceActionButton.isEnabled = true spotifySearchField.placeholderString = "Search Aesthetic releases" setDJMode(true, singleDeck: true) activityLabel.stringValue = "Loading Aesthetic releases…" activityLabel.textColor = Palette.coral loadAestheticCloud() case .spotify: sourceActionButton.title = "Connected" sourceActionButton.isEnabled = false spotifySearchField.placeholderString = "Search Spotify" if djMode { setDJMode(false) } configureExternalSource() providerDeck.configure(source: .spotify) providerDeck.update(title: spotifyState?.title ?? "", artist: spotifyState?.artists ?? "", album: spotifyState?.album ?? "", art: spotifyArt, duration: spotifyState?.duration ?? 0, position: spotifyState?.position ?? 0, playing: spotifyState?.isPlaying ?? false, canSeek: true) roomAudio.useSource(.spotify) currentArt = spotifyArt activityLabel.stringValue = "● juked headless · connecting" activityLabel.textColor = Palette.teal if let state = spotifyState { renderSpotifyState(state) } case .appleMusic: sourceActionButton.title = "Connect" sourceActionButton.isEnabled = true spotifySearchField.placeholderString = "Search Apple Music" if djMode { setDJMode(false) } configureExternalSource() providerDeck.configure(source: .appleMusic) let selected = appleMusicResults.indices.contains(selectedAppleMusicRow) ? appleMusicResults[selectedAppleMusicRow] : nil providerDeck.update(title: selected?.title ?? "", artist: selected?.artist ?? "", album: selected?.album ?? "", art: appleMusicArt, duration: selected?.duration ?? 0, position: 0, playing: appleMusicPlaying, canSeek: false) activityLabel.stringValue = if #available(macOS 14.0, *) { "● Apple Music · ready to connect" } else { "⚠ Apple Music requires macOS 14 or newer" } activityLabel.textColor = if #available(macOS 14.0, *) { .labelColor } else { .systemRed } } relayout() refreshPlaybackPresence() }
private func configureExternalSource() { setPlayerChromeHidden(true) providerDeck?.isHidden = false listScroll?.isHidden = false activityLabel?.isHidden = false sortPopup?.isHidden = true spotifySearchField?.isHidden = false notesToggle?.isEnabled = false listTable?.reloadData() }
private func refreshProviderDeck() { guard !providerDeck.isHidden else { return } if spotifyMode { providerDeck.update(title: spotifyState?.title ?? "", artist: spotifyState?.artists ?? "", album: spotifyState?.album ?? "", art: spotifyArt, duration: spotifyState?.duration ?? 0, position: spotifyState?.position ?? 0, playing: spotifyState?.isPlaying ?? false, canSeek: true) } else if appleMusicMode { let selected = appleMusicResults.indices.contains(selectedAppleMusicRow) ? appleMusicResults[selectedAppleMusicRow] : nil providerDeck.update(title: selected?.title ?? "", artist: selected?.artist ?? "", album: selected?.album ?? "", art: appleMusicArt, duration: selected?.duration ?? 0, position: 0, playing: appleMusicPlaying, canSeek: false) } }
private func activateSpotifyMode() { activateSource(.spotify) } private func activateLibraryMode() { activateSource(.local) }
@objc private func sourceTabChanged() { guard let source = JukeSource(rawValue: sourceTabs.selectedSegment) else { return } activateSource(source) }
@objc private func searchActiveSourceFromField() { switch activeSource { case .local: break case .aesthetic: selectedCloudRow = filteredCloudTracks.isEmpty ? -1 : 0 activityLabel.stringValue = "\(filteredCloudTracks.count) Aesthetic release\(filteredCloudTracks.count == 1 ? "" : "s")" listTable.reloadData() case .spotify: searchSpotify(spotifySearchField.stringValue) case .appleMusic: searchAppleMusic(spotifySearchField.stringValue) } }
private var filteredCloudTracks: [JukeCloudTrack] { let query = spotifySearchField?.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !query.isEmpty else { return cloudTracks } return cloudTracks.filter { $0.name.localizedCaseInsensitiveContains(query) } }
@objc private func sourceActionClicked() { switch activeSource { case .local: let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = true panel.beginSheetModal(for: window!) { [weak self] response in guard response == .OK, let self else { return } panel.urls.forEach { self.library.add(path: $0.path) } self.listTable.reloadData() self.activityLabel.stringValue = "\(self.library.tracks.count) tracks on \(JukeSource.local.label())" } case .aesthetic: if ACSession.shared.token() == nil { ACLogin.shared.signIn { [weak self] result in guard case .success = result else { return } self?.sourceActionButton.title = "Publish…" self?.loadAestheticCloud() } } else { publishToAesthetic() } case .spotify: break case .appleMusic: connectAppleMusic() } }
private func loadAestheticCloud() { guard ACSession.shared.token() != nil else { cloudTracks = [] selectedCloudRow = -1 activityLabel.stringValue = "Sign in to browse Aesthetic releases" listTable.reloadData() return } Task { [weak self] in guard let self else { return } do { let tracks = try await cloud.list() await MainActor.run { guard self.activeSource == .aesthetic else { return } self.cloudTracks = tracks self.selectedCloudRow = tracks.isEmpty ? -1 : 0 self.activityLabel.stringValue = tracks.isEmpty ? "No Aesthetic releases" : "\(tracks.count) Aesthetic release\(tracks.count == 1 ? "" : "s")" self.listTable.reloadData() } } catch { await MainActor.run { self.activityLabel.stringValue = error.localizedDescription self.activityLabel.textColor = .systemRed } } } }
private func publishToAesthetic() { let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = false panel.beginSheetModal(for: window!) { [weak self] response in guard response == .OK, let self else { return } self.activityLabel.stringValue = "Publishing…" Task { do { for url in panel.urls { _ = try await self.cloud.upload(file: url) } await MainActor.run { self.loadAestheticCloud() } } catch { await MainActor.run { self.activityLabel.stringValue = error.localizedDescription self.activityLabel.textColor = .systemRed } } } } }
private func playAestheticCloudResult(at row: Int) { let visibleTracks = filteredCloudTracks guard visibleTracks.indices.contains(row) else { return } selectedCloudRow = row listTable.reloadData() let cloudTrack = visibleTracks[row] activityLabel.stringValue = "Loading \(cloudTrack.name)…" Task { [weak self] in guard let self else { return } do { let url = try await cloud.download(cloudTrack) await MainActor.run { self.library.addFile(url, lane: "Aesthetic") guard let track = self.library.tracks.first(where: { $0.url.standardizedFileURL == url.standardizedFileURL }) else { return } self.djMixer.loadPrimary(track) self.activityLabel.stringValue = "Pull the record off the bed to float it" self.refreshPlaybackPresence() } } catch { await MainActor.run { self.activityLabel.stringValue = error.localizedDescription self.activityLabel.textColor = .systemRed } } } }
private func connectAppleMusic() { guard #available(macOS 14.0, *) else { return } activityLabel.stringValue = "Connecting Apple Music…" Task { [weak self] in guard let self else { return } do { try await appleMusic.authorize() await MainActor.run { self.sourceActionButton.title = "Connected" self.sourceActionButton.isEnabled = false self.activityLabel.stringValue = "● Apple Music · connected" self.spotifySearchField.becomeFirstResponder() } } catch { await MainActor.run { self.activityLabel.stringValue = error.localizedDescription self.activityLabel.textColor = .systemRed } } } }
private func searchAppleMusic(_ rawQuery: String) { guard #available(macOS 14.0, *) else { return } let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return } activityLabel.stringValue = "Searching Apple Music for “\(query)”…" Task { [weak self] in guard let self else { return } do { let results = try await appleMusic.search(query) await MainActor.run { guard self.activeSource == .appleMusic else { return } self.appleMusicResults = results self.selectedAppleMusicRow = results.isEmpty ? -1 : 0 self.sourceActionButton.title = "Connected" self.sourceActionButton.isEnabled = false self.activityLabel.stringValue = results.isEmpty ? "No Apple Music tracks for “\(query)”" : "\(results.count) Apple Music tracks" self.listTable.reloadData() } } catch { await MainActor.run { self.activityLabel.stringValue = error.localizedDescription self.activityLabel.textColor = .systemRed } } } }
private func playAppleMusicResult(at row: Int) { guard #available(macOS 14.0, *), appleMusicResults.indices.contains(row) else { return } selectedAppleMusicRow = row let result = appleMusicResults[row] titleLabel.stringValue = result.title titleLabel.textColor = .labelColor artistLabel.stringValue = result.artist laneLabel.stringValue = [result.album, "Apple Music · stays on this deck"] .filter { !$0.isEmpty }.joined(separator: " · ") spotifyProgress.duration = result.duration spotifyProgress.position = 0 playButton.title = "❚❚" appleMusicPlaying = true djMixer.pauseAll() spotify.pause() listTable.reloadData() refreshProviderDeck() presentAppleMusicArtwork(result.artworkURL) Task { [weak self] in do { try await self?.appleMusic.play(result) await MainActor.run { self?.refreshPlaybackPresence() } } catch { await MainActor.run { self?.appleMusicPlaying = false self?.activityLabel.stringValue = error.localizedDescription self?.activityLabel.textColor = .systemRed self?.refreshProviderDeck() self?.refreshPlaybackPresence() } } } }
private func presentAppleMusicArtwork(_ url: URL?) { appleMusicArtworkURL = url appleMusicArt = nil currentArt = nil refreshProviderDeck() guard let url else { nowPlaying.present(art: nil, videoURL: nil); return } URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in guard let data, let art = NSImage(data: data) else { return } DispatchQueue.main.async { guard self?.appleMusicArtworkURL == url else { return } self?.appleMusicArt = art self?.currentArt = art self?.nowPlaying.present(art: art, videoURL: nil) self?.nowPlaying.setPaused(false) self?.refreshProviderDeck() self?.refreshPlaybackPresence() } }.resume() }
private func searchSpotify(_ rawQuery: String, autoplayFirst: Bool = false) { let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) guard !query.isEmpty else { return } activateSpotifyMode() activityLabel.stringValue = "● searching Spotify for “\(query)”" spotify.search(query) { [weak self] result in guard let self else { return } switch result { case .success(let tracks): self.spotifyResults = tracks self.selectedSpotifyRow = tracks.isEmpty ? -1 : 0 self.activityLabel.stringValue = tracks.isEmpty ? "● no Spotify tracks for “\(query)”" : "● \(tracks.count) Spotify tracks · click one to play" self.activityLabel.textColor = tracks.isEmpty ? Palette.inkDim : Palette.teal self.listTable.reloadData() if !tracks.isEmpty { self.listTable.scrollRowToVisible(0) } if autoplayFirst, !tracks.isEmpty { self.playSpotifyResult(at: 0) } case .failure(let error): self.activityLabel.stringValue = "⚠ \(error.localizedDescription)" self.activityLabel.textColor = .systemRed } } }
private func playSpotifyResult(at row: Int) { guard row >= 0, row < spotifyResults.count else { return } let old = selectedSpotifyRow selectedSpotifyRow = row let result = spotifyResults[row] titleLabel.stringValue = result.title titleLabel.textColor = NSColor(srgbRed: 0.11, green: 0.73, blue: 0.33, alpha: 1) artistLabel.stringValue = result.artists laneLabel.stringValue = [result.album, "Spotify · stays on this deck"].filter { !$0.isEmpty }.joined(separator: " · ") spotifyProgress.duration = result.duration spotifyProgress.position = 0 playButton.title = "❚❚" djMixer.pauseAll() if #available(macOS 14.0, *) { appleMusic.pause() } appleMusicPlaying = false providerDeck.update(title: result.title, artist: result.artists, album: result.album, art: nil, duration: result.duration, position: 0, playing: true, canSeek: true) spotify.play(result) var rows = IndexSet(integer: row) if old >= 0, old < spotifyResults.count { rows.insert(old) } listTable.reloadData(forRowIndexes: rows, columnIndexes: IndexSet(integer: 0)) }
private func renderSpotifyState(_ state: SpotifyPlaybackState?) { spotifyState = state guard spotifyMode, let state else { return } titleLabel.stringValue = state.title titleLabel.textColor = NSColor(srgbRed: 0.11, green: 0.73, blue: 0.33, alpha: 1) artistLabel.stringValue = state.artists laneLabel.stringValue = [state.album, "Spotify · stays on this deck"].filter { !$0.isEmpty }.joined(separator: " · ") laneLabel.textColor = NSColor(white: 0.68, alpha: 1) spotifyProgress.duration = state.duration spotifyProgress.position = state.position playButton.title = state.isPlaying ? "❚❚" : "▶" nowPlaying.setPaused(!state.isPlaying) ledLabel.stringValue = "\(Self.mmss(state.position)) / \(Self.mmss(state.duration))" if let spotifyArt { currentArt = spotifyArt nowPlaying.present(art: spotifyArt, videoURL: nil) } refreshProviderDeck() if state.artworkURL != spotifyArtworkURL { spotifyArtworkURL = state.artworkURL spotifyArt = nil currentArt = nil refreshProviderDeck() guard let url = state.artworkURL else { nowPlaying.present(art: nil, videoURL: nil); return } URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in guard let data, let art = NSImage(data: data) else { return } DispatchQueue.main.async { guard self?.spotifyArtworkURL == url else { return } self?.spotifyArt = art self?.currentArt = art self?.nowPlaying.present(art: art, videoURL: nil) self?.nowPlaying.setPaused(!state.isPlaying) self?.refreshProviderDeck() self?.refreshPlaybackPresence() } }.resume() } refreshPlaybackPresence() } var quickTitle: String { if djMode { return djMixer.dominantTitle } if spotifyMode { return spotifyState?.title ?? "Spotify" } if appleMusicMode, appleMusicResults.indices.contains(selectedAppleMusicRow) { return appleMusicResults[selectedAppleMusicRow].title } return track?.title ?? activeSource.label() } var quickSubtitle: String { if djMode { return String(format: "DJ mix · %.1f BPM", djMixer.dominantBPM) } if spotifyMode { return [spotifyState?.artists ?? "", "Spotify"].filter { !$0.isEmpty }.joined(separator: " · ") } if appleMusicMode, appleMusicResults.indices.contains(selectedAppleMusicRow) { return [appleMusicResults[selectedAppleMusicRow].artist, "Apple Music"] .filter { !$0.isEmpty }.joined(separator: " · ") } return [track?.meta?.artist ?? "Aesthetic Dot Computer", "Aesthetic"].joined(separator: " · ") } var quickIsPlaying: Bool { djMode ? djMixer.isPlaying : (spotifyMode ? (spotifyState?.isPlaying ?? false) : (appleMusicMode ? appleMusicPlaying : wave.isPlaying)) } var quickRoomSummary: String { switch roomAudio.state { case .idle: return "room · off" case .failed(let message): return "room · ⚠ \(message)" case .live(let snapshot): return "\(snapshot.neo == "off" ? "" : "Neo \(snapshot.neo)")\(snapshot.neo != "off" && snapshot.blueberry != "off" ? " · " : "")\(snapshot.blueberry == "off" ? "" : "Blueberry \(snapshot.blueberry)")" } }
@objc func quickOpenFull() { miniPopover?.close() guard let w = window else { return } if w.isMiniaturized { w.deminiaturize(nil) } w.makeKeyAndOrderFront(nil) resumePresentationActivity() NSApp.activate(ignoringOtherApps: true) }
@objc func quickToggleFull() { guard let w = window else { return } if w.isVisible, !w.isMiniaturized { miniPopover?.close() suspendPresentationActivity() w.orderOut(nil) } else { quickOpenFull() } }
@objc func quickTogglePlay() { togglePlay(); miniPlayer?.refresh() } @objc func quickPrevious() { prevTrack(); miniPlayer?.refresh() } @objc func quickNext() { nextTrack(); miniPlayer?.refresh() } @objc func quickVolumeChanged(_ sender: NSSlider) { quickVolume = max(0, min(1, sender.floatValue)) wave.volume = quickVolume djMixer.setMasterVolume(quickVolume) spotify.volume(percent: Int((quickVolume * 100).rounded())) UserDefaults.standard.set(quickVolume, forKey: "playerVolume") miniPlayer?.refresh() }
@objc func quickVolumeUp() { setQuickVolume(quickVolume + 0.1) } @objc func quickVolumeDown() { setQuickVolume(quickVolume - 0.1) }
private func setQuickVolume(_ value: Float) { quickVolume = max(0, min(1, value)) wave.volume = quickVolume djMixer.setMasterVolume(quickVolume) spotify.volume(percent: Int((quickVolume * 100).rounded())) UserDefaults.standard.set(quickVolume, forKey: "playerVolume") miniPlayer?.refresh() }
func makeDockMenu() -> NSMenu { let menu = NSMenu(title: "Menu Band Juke") let heading = NSMenuItem(title: "\(quickTitle) — \(quickSubtitle)", action: nil, keyEquivalent: "") heading.isEnabled = false menu.addItem(heading) menu.addItem(.separator()) for (title, action) in [("Previous", #selector(quickPrevious)), (quickIsPlaying ? "Pause" : "Play", #selector(quickTogglePlay)), ("Next", #selector(quickNext))] { let item = NSMenuItem(title: title, action: action, keyEquivalent: "") item.target = self menu.addItem(item) } let volume = NSMenuItem(title: "Volume \(Int((quickVolume * 100).rounded()))%", action: nil, keyEquivalent: "") volume.isEnabled = false menu.addItem(volume) let volumeUp = NSMenuItem(title: "Volume Up", action: #selector(quickVolumeUp), keyEquivalent: "") volumeUp.target = self menu.addItem(volumeUp) let volumeDown = NSMenuItem(title: "Volume Down", action: #selector(quickVolumeDown), keyEquivalent: "") volumeDown.target = self menu.addItem(volumeDown) menu.addItem(.separator()) let room = NSMenuItem(title: quickRoomSummary, action: nil, keyEquivalent: "") room.isEnabled = false menu.addItem(room) let open = NSMenuItem(title: "Open Menu Band Juke", action: #selector(quickOpenFull), keyEquivalent: "") open.target = self menu.addItem(open) return menu }
@objc private func showCloud() { if cloudWindow == nil { cloudWindow = JukeCloudWindowController( currentFile: { [weak self] in self?.track?.url }, loadLocalFile: { [weak self] in self?.loadCloudFile($0) }) } cloudWindow?.prepareForDisplay() cloudWindow?.showWindow(nil) cloudWindow?.window?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) }
private func loadCloudFile(_ url: URL) { if let index = library.tracks.firstIndex(where: { $0.url.standardizedFileURL == url.standardizedFileURL }) { select(index, autoplay: true) return } library.addFile(url, lane: "cloud") listTable.reloadData() if let index = library.tracks.firstIndex(where: { $0.url.standardizedFileURL == url.standardizedFileURL }) { select(index, autoplay: true) } }
// ── selection / playback ────────────────────────────────────────────── private var track: Track? { (current >= 0 && current < library.tracks.count) ? library.tracks[current] : nil }
func select(_ i: Int, autoplay: Bool) { guard i >= 0, i < library.tracks.count else { return } if spotifyMode { spotify.pause(); activateLibraryMode() } commitNotes() let old = current current = i let t = library.tracks[i] titleLabel.stringValue = t.title titleLabel.textColor = Self.statusColor(t.meta?.status) artistLabel.stringValue = t.meta?.artist ?? "Aesthetic Dot Computer" laneLabel.stringValue = Self.metaLine(t) laneLabel.textColor = .secondaryLabelColor updateNowPlaying(t) loadLinks(t) relayout() // link count changes the header row width if djMode { djMixer.loadPrimary(t, autoplay: autoplay) var rows = IndexSet(integer: i) if old >= 0, old < library.tracks.count { rows.insert(old) } if activeSource == .local { listTable.reloadData(forRowIndexes: rows, columnIndexes: IndexSet(integer: 0)) listTable.scrollRowToVisible(i) } refreshPlaybackPresence() return } wave.load(track: t) wave.playbackRate = speedSlider.doubleValue wave.comments = t.data.comments notesView.string = t.data.notes notesPlaceholder.isHidden = !t.data.notes.isEmpty renderStars(t.data.stars) commentsTable.reloadData() // refresh only the two affected rows (custom .selected chip) var rows = IndexSet(integer: i) if old >= 0, old < library.tracks.count { rows.insert(old) } listTable.reloadData(forRowIndexes: rows, columnIndexes: IndexSet(integer: 0)) listTable.scrollRowToVisible(i) updateTime() if autoplay { wave.play(); playButton.title = "❚❚"; nowPlaying.setPaused(false) } else { playButton.title = "▶"; nowPlaying.setPaused(true) } refreshPlaybackPresence() }
// ── sorting ──────────────────────────────────────────────────────────── @objc private func sortChanged(_ sender: NSPopUpButton) { guard let m = SortMode(rawValue: sender.indexOfSelectedItem) else { return } sortMode = m applySort() } private static func statusRank(_ s: String?) -> Int { switch s { case "RELEASED": return 0 case "MASTERING", "SUBMITTED": return 1 case "RENDER": return 2 case "WIP", "IDEA": return 3 default: return 4 } } private func applySort() { // keep the current track selected across the reorder (match by URL) let currentURL = track?.url.standardizedFileURL.path let mode = sortMode // "newest/oldest rendered" go by the audio file's real modification // time, not the library JSON's snapshot: fresh renders folded into // the queue after launch carry no meta at all, so a snapshot-string // sort pinned the genuinely newest tracks to the bottom. Refreshing // the cached stamp here keeps the row timestamps in step. if mode == .newest || mode == .oldest { for t in library.tracks { t.refreshRenderedAt() } } library.reorder { a, b in switch mode { case .defaultOrder: let ra = Self.statusRank(a.meta?.status), rb = Self.statusRank(b.meta?.status) if ra != rb { return ra < rb } return (a.meta?.updated ?? "") > (b.meta?.updated ?? "") // recent first case .newest: return a.renderedAt > b.renderedAt case .oldest: return a.renderedAt < b.renderedAt case .stars: if a.data.stars != b.data.stars { return a.data.stars > b.data.stars } return (a.meta?.updated ?? "") > (b.meta?.updated ?? "") case .title: return a.title.localizedCaseInsensitiveCompare(b.title) == .orderedAscending case .lane: if a.lane != b.lane { return a.lane.localizedCaseInsensitiveCompare(b.lane) == .orderedAscending } return a.title.localizedCaseInsensitiveCompare(b.title) == .orderedAscending case .bpm: return (a.meta?.bpm ?? 0) > (b.meta?.bpm ?? 0) case .duration: return (a.meta?.durationSec ?? 0) > (b.meta?.durationSec ?? 0) } } listTable.reloadData() if let cu = currentURL, let idx = library.tracks.firstIndex(where: { $0.url.standardizedFileURL.path == cu }) { current = idx listTable.selectRowIndexes(IndexSet(integer: idx), byExtendingSelection: false) listTable.scrollRowToVisible(idx) } }
@objc private func listClicked() { let r = listTable.clickedRow switch activeSource { case .local: if r >= 0 { select(r, autoplay: true) } case .aesthetic: playAestheticCloudResult(at: r) case .spotify: playSpotifyResult(at: r) case .appleMusic: playAppleMusicResult(at: r) } } /// Stop audio in every mode at once — DJ decks, providers, and the main /// wave. Safe to call when a mode isn't active; each branch is a no-op /// then. Used by the `stop` control command. func stopEverything() { djMixer.pauseAll() if spotifyMode { spotify.pause() } if appleMusicMode, #available(macOS 14.0, *) { appleMusicPlaying = false appleMusic.pause() } wave.pause() playButton.title = "▶" nowPlaying.setPaused(true) refreshPlaybackPresence() }
@objc private func togglePlay() { if djMode { djMixer.toggleDominant() } else if spotifyMode { spotify.toggle() let playing = !(spotifyState?.isPlaying ?? false) playButton.title = playing ? "❚❚" : "▶" nowPlaying.setPaused(!playing) } else if appleMusicMode { guard #available(macOS 14.0, *) else { return } appleMusicPlaying.toggle() playButton.title = appleMusicPlaying ? "❚❚" : "▶" nowPlaying.setPaused(!appleMusicPlaying) Task { [weak self] in do { try await self?.appleMusic.toggle() } catch { await MainActor.run { self?.appleMusicPlaying.toggle() self?.refreshPlaybackPresence() } } } } else { wave.togglePlay() playButton.title = wave.isPlaying ? "❚❚" : "▶" nowPlaying.setPaused(!wave.isPlaying) } refreshProviderDeck() refreshPlaybackPresence() } @objc private func speedChanged(_ sender: NSSlider) { guard !spotifyMode, !djMode else { return } wave.playbackRate = sender.doubleValue speedLabel.stringValue = String(format: "%.2f×", sender.doubleValue) UserDefaults.standard.set(sender.doubleValue, forKey: "listPlaybackRate") } @objc private func resetSpeed() { speedSlider.doubleValue = 1 speedChanged(speedSlider) } @objc private func prevTrack() { if djMode { djMixer.stepDominant(by: -1) } else if spotifyMode { spotify.previous() } else if appleMusicMode { return } else if current > 0 { select(current - 1, autoplay: true) } } @objc private func nextTrack() { if djMode { djMixer.stepDominant(by: 1) } else if spotifyMode { spotify.next() } else if appleMusicMode { return } else if current < library.tracks.count - 1 { select(current + 1, autoplay: true) } }
/// Stable command surface used by the user-only Unix socket. This is /// always called on AppKit's main thread by JukeControlServer. func control(_ request: [String: Any]) -> [String: Any] { guard Thread.isMainThread else { return ["ok": false, "error": "control must run on main thread"] } guard let command = request["command"] as? String else { return ["ok": false, "error": "missing command"] } func state() -> [String: Any] { var out: [String: Any] = [ "ok": true, "mode": djMode ? "dj" : (spotifyMode ? "spotify" : (appleMusicMode ? "appleMusic" : "library")), "source": String(describing: activeSource), "playing": quickIsPlaying, "index": current, "queueCount": library.tracks.count ] if let t = track { out["title"] = t.title; out["path"] = t.url.path; out["lane"] = t.lane } if !spotifyMode && !appleMusicMode && !djMode { out["position"] = wave.currentTime; out["duration"] = wave.duration out["speed"] = wave.playbackRate } return out } switch command { case "status": return state() case "source": guard let rawSource = request["source"] as? String else { return ["ok": false, "error": "source requires local, aesthetic, spotify, or appleMusic"] } let source: JukeSource? switch rawSource.lowercased() { case "local": source = .local case "aesthetic": source = .aesthetic case "spotify": source = .spotify case "apple", "applemusic", "apple-music": source = .appleMusic default: source = nil } guard let source else { return ["ok": false, "error": "unknown source"] } activateSource(source) return state() case "detach": guard djMode, activeSource.canDetachRecords else { return ["ok": false, "error": "this source must stay on the main deck"] } guard djMixer.detachPrimary() else { return ["ok": false, "error": "the main bed has no record"] } return state() case "list": let limit = max(1, min(1000, request["limit"] as? Int ?? 500)) let rows: [[String: Any]] = library.tracks.prefix(limit).enumerated().map { i, t in ["index": i, "title": t.title, "path": t.url.path, "lane": t.lane] } return ["ok": true, "count": library.tracks.count, "tracks": rows, "truncated": rows.count < library.tracks.count] case "select", "play": // Asking to play IS asking to be heard: a room that failed // earlier (far Mac slept, receiver killed) re-engages the saved // layout instead of staying silently dark. if case .failed = roomAudio.state { roomAudio.apply(savedRoomLayout(), pan: savedRoomPan()) } if let path = request["path"] as? String { let url = URL(fileURLWithPath: (path as NSString).expandingTildeInPath) let wanted = url.standardizedFileURL.path var found = library.tracks.firstIndex(where: { $0.url.standardizedFileURL.path == wanted }) if found == nil { // A fresh render that is not in the library yet: fold it // into the queue the way `--select` always has, laned by // the directory above its out/ folder. let lane = url.deletingLastPathComponent().deletingLastPathComponent().lastPathComponent if library.addFile(url, lane: lane) != nil { listTable.reloadData() found = library.tracks.count - 1 } } guard let i = found else { return ["ok": false, "error": "path is not in the queue and is not a readable audio file"] } select(i, autoplay: command == "play") } else if let title = request["title"] as? String { let matches = library.tracks.indices.filter { library.tracks[$0].title == title } guard matches.count == 1 else { return ["ok": false, "error": matches.isEmpty ? "exact title not found" : "title is ambiguous; use path or index"] } select(matches[0], autoplay: command == "play") } else if let i = request["index"] as? Int { guard library.tracks.indices.contains(i) else { return ["ok": false, "error": "index out of range"] } select(i, autoplay: command == "play") } else if command == "play" { guard !spotifyMode, !djMode else { return ["ok": false, "error": "plain play is unavailable in Spotify or DJ mode"] } wave.play(); playButton.title = "❚❚"; nowPlaying.setPaused(false); refreshPlaybackPresence() } else { return ["ok": false, "error": "select requires exact path, title, or index"] } return state() case "pause": guard !spotifyMode, !djMode else { return ["ok": false, "error": "pause is unavailable in Spotify or DJ mode"] } wave.pause(); playButton.title = "▶"; nowPlaying.setPaused(true); refreshPlaybackPresence(); return state() case "toggle": guard !spotifyMode, !djMode else { return ["ok": false, "error": "toggle is unavailable in Spotify or DJ mode"] } togglePlay(); return state() // Silence, whatever is playing it. `pause` deliberately refuses to act // in DJ or provider mode, which leaves the shell with no way to stop // the room; `stop` is that way, and works in every mode. case "stop": stopEverything(); return state() case "seek": guard !spotifyMode, !djMode, let seconds = request["seconds"] as? Double, seconds.isFinite else { return ["ok": false, "error": "seek requires finite seconds in library mode"] } wave.seek(to: seconds); return state() case "speed": guard !spotifyMode, !djMode, let value = request["speed"] as? Double, value.isFinite, (0.5...1.5).contains(value) else { return ["ok": false, "error": "speed must be between 0.5 and 1.5 in library mode"] } speedSlider.doubleValue = value; speedChanged(speedSlider); return state() case "next": guard !spotifyMode, !djMode else { return ["ok": false, "error": "next is unavailable in Spotify or DJ mode"] } nextTrack(); return state() case "previous": guard !spotifyMode, !djMode else { return ["ok": false, "error": "previous is unavailable in Spotify or DJ mode"] } prevTrack(); return state() default: return ["ok": false, "error": "unknown command"] } }
// ── rating ───────────────────────────────────────────────────────────── private func renderStars(_ n: Int) { for (i, b) in starButtons.enumerated() { b.title = (i < n) ? "★" : "☆" } } @objc private func starClicked(_ sender: NSButton) { guard let t = track else { return } t.data.stars = (t.data.stars == sender.tag) ? sender.tag - 1 : sender.tag // click same top star to step down renderStars(t.data.stars) t.save() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) } @objc private func clearStarsClicked() { guard let t = track else { return } t.data.stars = 0; renderStars(0); t.save() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) }
// ── notes ──────────────────────────────────────────────────────────── func textDidChange(_ notification: Notification) { notesPlaceholder.isHidden = !notesView.string.isEmpty } func textDidEndEditing(_ notification: Notification) { commitNotes() } private func commitNotes() { guard let t = track else { return } if t.data.notes != notesView.string { t.data.notes = notesView.string; t.save() } }
// ── comments ─────────────────────────────────────────────────────────── @objc private func addCommentNow() { promptComment(at: wave.currentTime) } func waveformRequestComment(at t: Double) { promptComment(at: t) }
private func promptComment(at t: Double) { guard let tr = track else { return } let wasPlaying = wave.isPlaying wave.pause(); playButton.title = "▶" let a = NSAlert() a.messageText = "Comment @ \(JukeController.mmss(t))" a.addButton(withTitle: "Add"); a.addButton(withTitle: "Cancel") let field = NSTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24)) field.placeholderString = "what about this moment?" a.accessoryView = field a.window.initialFirstResponder = field if a.runModal() == .alertFirstButtonReturn { let text = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) if !text.isEmpty { tr.data.comments.append(Comment(t: t, text: text)) tr.data.comments.sort { $0.t < $1.t } tr.save() wave.comments = tr.data.comments commentsTable.reloadData() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) } } if wasPlaying { wave.play(); playButton.title = "❚❚"; nowPlaying.setPaused(false) } refreshPlaybackPresence() } @objc private func commentClicked() { guard let t = track else { return } let r = commentsTable.clickedRow if r >= 0 && r < t.data.comments.count { wave.seek(to: t.data.comments[r].t) } } @objc private func commentDoubleClicked() { commentClicked() } @objc private func deleteComment() { guard let t = track else { return } let r = commentsTable.selectedRow guard r >= 0 && r < t.data.comments.count else { return } t.data.comments.remove(at: r) t.save() wave.comments = t.data.comments commentsTable.reloadData() listTable.reloadData(forRowIndexes: IndexSet(integer: current), columnIndexes: IndexSet(integer: 0)) }
// ── waveform delegate ─────────────────────────────────────────────────── func waveformDidFinish() { playButton.title = "▶"; nextTrack(); refreshPlaybackPresence() } func waveformTick() { updateTime() } private func updateTime() { if let state = spotifyMode ? spotifyState : nil { ledLabel.stringValue = "\(Self.mmss(state.position)) / \(Self.mmss(state.duration))" } else { ledLabel.stringValue = "\(Self.mmss(wave.currentTime)) / \(Self.mmss(wave.duration))" } }
// ── live work awareness ──────────────────────────────────────────────── // Slab's ledger tells us which agents are active; local process inspection // catches the narrower render/bake window. Neither this poll nor the DJ // display clock should run while the retained Juke window is hidden. private var isPresentationActive: Bool { guard let window else { return false } return window.isVisible && !window.isMiniaturized }
private func resumePresentationActivity() { guard isPresentationActive else { return } if djMode { djMixer.startDisplay() } armActivityStatus() }
private func suspendPresentationActivity() { djMixer.stopDisplay() activityTimer?.invalidate() activityTimer = nil }
private func armActivityStatus() { guard isPresentationActive, activityTimer == nil else { return } pollActivityStatus() activityTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in guard let self else { return } guard self.isPresentationActive else { self.activityTimer?.invalidate() self.activityTimer = nil return } self.pollActivityStatus() } } private func pollActivityStatus() { guard isPresentationActive, activeSource == .local, !activityPollInFlight else { return } activityPollInFlight = true let tracks = library.tracks DispatchQueue.global(qos: .utility).async { [weak self] in let activities = WorkStatus.snapshot(tracks: tracks) DispatchQueue.main.async { guard let self else { return } self.activityPollInFlight = false guard self.isPresentationActive, self.activeSource == .local else { return } self.renderActivityStatus(activities) } } }
private func renderActivityStatus(_ activities: [WorkActivity]) { for t in library.tracks { let matches = activities.filter { a in (a.track != nil && a.track == t.title) || (a.track == nil && a.lane == t.lane) } t.liveStatus = matches.first.map { $0.track == nil ? "\($0.state) in \(t.lane)" : $0.state } } if activities.isEmpty { activityLabel.stringValue = "● agents + renders idle" activityLabel.textColor = Palette.inkDim } else { activityLabel.stringValue = activities.prefix(3).map { a in let target = a.track ?? a.lane ?? "pop" return "● \(target): \(a.state)" }.joined(separator: " ") activityLabel.textColor = activities.contains(where: { $0.state == "baking" }) ? Palette.gold : Palette.teal } if let t = track { laneLabel.stringValue = Self.metaLine(t) laneLabel.textColor = t.liveStatus == nil ? .secondaryLabelColor : Palette.gold } listTable.reloadData() }
// ── tables ─────────────────────────────────────────────────────────────── func numberOfRows(in tableView: NSTableView) -> Int { if tableView == listTable { switch activeSource { case .local: return library.tracks.count case .aesthetic: return filteredCloudTracks.count case .spotify: return spotifyResults.count case .appleMusic: return appleMusicResults.count } } return track?.data.comments.count ?? 0 } // list = dressed-up view rows; comments = plain cell strings. func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if tableView == listTable, activeSource == .spotify, row < spotifyResults.count { let cell = (listTable.makeView(withIdentifier: SpotifyTrackRowView.id, owner: self) as? SpotifyTrackRowView) ?? { let view = SpotifyTrackRowView(); view.identifier = SpotifyTrackRowView.id; return view }() cell.configure(spotifyResults[row]) cell.selected = row == selectedSpotifyRow return cell } if tableView == listTable, activeSource == .aesthetic, row < filteredCloudTracks.count { let cell = (listTable.makeView(withIdentifier: AestheticCloudRowView.id, owner: self) as? AestheticCloudRowView) ?? { let view = AestheticCloudRowView(); view.identifier = AestheticCloudRowView.id; return view }() cell.configure(filteredCloudTracks[row]) cell.selected = row == selectedCloudRow return cell } if tableView == listTable, activeSource == .appleMusic, row < appleMusicResults.count { let cell = (listTable.makeView(withIdentifier: AppleMusicTrackRowView.id, owner: self) as? AppleMusicTrackRowView) ?? { let view = AppleMusicTrackRowView(); view.identifier = AppleMusicTrackRowView.id; return view }() cell.configure(appleMusicResults[row]) cell.selected = row == selectedAppleMusicRow return cell } guard activeSource == .local else { return nil } guard tableView == listTable, row < library.tracks.count else { return nil } let cell = (listTable.makeView(withIdentifier: TrackRowView.id, owner: self) as? TrackRowView) ?? { let v = TrackRowView(); v.identifier = TrackRowView.id; return v }() cell.configure(library.tracks[row]) cell.selected = (row == current) return cell } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { guard tableView == commentsTable, let t = track, row < t.data.comments.count else { return "" } let c = t.data.comments[row] return "\(JukeController.mmss(c.t)) \(c.text)" } // Export the actual audio file to Finder, Messages, Mail, etc. AppKit's // file-URL pasteboard type lets each destination decide whether to copy or // attach it; JukeWizard never moves or mutates the source track. func tableView(_ tableView: NSTableView, pasteboardWriterForRow row: Int) -> NSPasteboardWriting? { guard tableView == listTable, activeSource == .local, row >= 0, row < library.tracks.count else { return nil } return library.tracks[row].url as NSURL }
// ── auto-pop watcher ───────────────────────────────────────────────────── private func armWatch() { guard !watchDirs.isEmpty else { return } // seed mtimes so only files that change AFTER launch trigger a pop. for d in watchDirs { for (p, m) in scan(d) { watchMtimes[p] = m } } watchTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in self?.pollWatch() } } private func scan(_ dir: String) -> [(String, Date)] { let url = URL(fileURLWithPath: (dir as NSString).expandingTildeInPath) let items = (try? FileManager.default.contentsOfDirectory( at: url, includingPropertiesForKeys: [.contentModificationDateKey])) ?? [] var out: [(String, Date)] = [] for f in items where Library.audioExts.contains(f.pathExtension.lowercased()) { let m = (try? f.resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? .distantPast out.append((f.standardizedFileURL.path, m)) } return out } private func pollWatch() { for d in watchDirs { let laneName = URL(fileURLWithPath: (d as NSString).expandingTildeInPath).lastPathComponent for (p, m) in scan(d) { let prev = watchMtimes[p] if prev == nil || m > prev! { watchMtimes[p] = m if prev == nil && watchTimer == nil { continue } // initial seed guard popPlay(path: p, lane: laneName) } } } } private func popPlay(path: String, lane: String) { // Quietly fold new renders into the queue — NEVER switch the // current track, start playback, or steal focus (that broke the // flow). The new row just appears in the list; you choose when. let url = URL(fileURLWithPath: path) let here = url.standardizedFileURL.path let sel = listTable.selectedRow if library.tracks.contains(where: { $0.url.standardizedFileURL.path == here }) { listTable.reloadData() // refresh its sidecar/stars } else if library.addFile(url, lane: lane) != nil { listTable.reloadData() } if sel >= 0 { listTable.selectRowIndexes(IndexSet(integer: sel), byExtendingSelection: false) } }
// ── util ─────────────────────────────────────────────────────────────── static func mmss(_ s: Double) -> String { guard s.isFinite && s >= 0 else { return "0:00" } let total = Int(s.rounded()) return String(format: "%d:%02d", total / 60, total % 60) }
// ── /pop library metadata display ──────────────────────────────────────── static func statusColor(_ s: String?) -> NSColor { switch s { case "RELEASED": return NSColor.systemGreen case "MASTERING", "SUBMITTED": return Palette.teal case "RENDER": return Palette.gold case "WIP", "IDEA": return Palette.coral default: return Palette.inkDim } } static func bestLink(_ t: Track) -> String? { t.meta?.links?.spotify ?? t.meta?.links?.youtube ?? t.meta?.links?.apple ?? t.meta?.links?.distrokid } private static func ago(_ iso: String?) -> String? { guard let iso, let d = ISO8601DateFormatter().date(from: iso) else { return nil } let s = Date().timeIntervalSince(d) if s < 3600 { return "\(Int(s / 60))m ago" } if s < 86400 { return "\(Int(s / 3600))h ago" } if s < 86400 * 30 { return "\(Int(s / 86400))d ago" } return "\(Int(s / (86400 * 30)))mo ago" } private static func size(_ b: Int?) -> String? { guard let b else { return nil } return b >= 1_000_000 ? String(format: "%.1f MB", Double(b) / 1e6) : "\(b / 1000) KB" } // one secondary line: lane · backend · STATUS · updated · revisions · dur · bpm · key · size static func metaLine(_ t: Track) -> String { guard let m = t.meta else { return t.lane } var parts: [String] = [t.lane] if let b = m.backend { parts.append(b) } if let s = m.status { parts.append(s) } if let u = ago(m.updated) { parts.append("updated \(u)") } if let r = m.revisions { parts.append("\(r) rev") } if let d = m.durationSec { parts.append(mmss(d)) } if let bpm = m.bpm { parts.append("\(bpm) BPM") } if let k = m.key { parts.append(k) } if let sz = size(m.bytes) { parts.append(sz) } return parts.joined(separator: " · ") } // ── now-playing art + video + links ────────────────────────────────────── // Big cover in the header; if the track has a video (a local reel/story, or // the CDN cut for a released single) it streams there instead — muted. private func updateNowPlaying(_ t: Track) { currentArt = t.meta?.art.flatMap { NSImage(contentsOf: URL(fileURLWithPath: ($0 as NSString).expandingTildeInPath)) } nowPlaying.present(art: currentArt, videoURL: Self.videoURL(for: t)) } // Prefer a local clip; else stream the released single's CDN cut. private static func videoURL(for t: Track) -> URL? { if let v = t.meta?.media?.first(where: { $0.kind == "video" }) { return URL(fileURLWithPath: (v.path as NSString).expandingTildeInPath) } if t.meta?.status == "RELEASED" { return URL(string: "https://assets.aesthetic.computer/pop/\(t.title).mp4") } return nil } private func loadLinks(_ t: Track) { for b in linkButtons { let svc = LinkService(rawValue: b.tag)! b.isHidden = (svc.url(t.meta?.links) == nil) } } @objc private func linkClicked(_ sender: NSButton) { guard let t = track, let svc = LinkService(rawValue: sender.tag), let s = svc.url(t.meta?.links) else { return } if svc == .spotify, let id = JukeSpotify.trackID(from: s) { activateSpotifyMode() spotify.play(trackID: id) return } guard let u = URL(string: s) else { return } NSWorkspace.shared.open(u) }}