Button(automix.enabled?"Stop Automix":"Start Automix"){ifautomix.enabled{automix.stop()}else{automix.start()}}.tint(automix.enabled?.green:.accentColor).help("Start or stop automatic transitions through the queued tracks")
Button("Mix Now"){automix.mixNow()}.disabled(!automix.enabled||automix.entries.isEmpty||automix.progress!=nil).help("Transition immediately to the next queued track using the selected fade duration")
Text("Fade");Slider(value:$automix.seconds,in:0...30,step:1).help("Duration in seconds for each automatic crossfade");Text("\(Int(automix.seconds)) s").monospacedDigit().frame(width:34)
}.font(.caption)
Toggle("Match BPM when available",isOn:$automix.matchTempo).font(.caption)
Toggle("Match BPM when available",isOn:$automix.matchTempo).font(.caption).help("Match confirmed track tempos before the automatic transition")
Button{automix.remove(entry.id)}label:{Image(systemName:"xmark")}.accessibilityLabel("Remove from queue")
Button{automix.move(entry.id,by:-1)}label:{Image(systemName:"chevron.up")}.accessibilityLabel("Move earlier").help("Move this queued track one place earlier")
Button{automix.move(entry.id,by:1)}label:{Image(systemName:"chevron.down")}.accessibilityLabel("Move later").help("Move this queued track one place later")
Button{automix.remove(entry.id)}label:{Image(systemName:"xmark")}.accessibilityLabel("Remove from queue").help("Remove this track from the queue without deleting it from the library")
}.font(.caption).buttonStyle(.borderless)
}
ifautomix.entries.isEmpty{Text("Add songs with + or drag them here from the DJ library.").font(.caption).foregroundStyle(.secondary)}
HStack{Button("Shuffle"){automix.shuffle()}.help("Randomise the order of the remaining queued tracks");Button("Clear"){automix.clear()}.help("Remove all queued entries; library tracks are kept");Spacer()}.controlSize(.mini).disabled(automix.entries.isEmpty)
HStack{Text("Start (seconds)").frame(width:120,alignment:.leading);TextField("Start",value:$start,format:.number.precision(.fractionLength(3))).textFieldStyle(.roundedBorder);Button("Use Playhead"){start=player.time}.help("Use the current playback position as the loop start")}
HStack{Text("End (seconds)").frame(width:120,alignment:.leading);TextField("End",value:$end,format:.number.precision(.fractionLength(3))).textFieldStyle(.roundedBorder);Button("Use Playhead"){end=player.time}.help("Use the current playback position as the loop end")}
Text(valid?String(format:"Loop length: %.3f seconds",end-start):"End must be at least 0.1 seconds after start, within the track.").font(.caption).foregroundStyle(valid?Color.secondary:Color.orange)
Text("On either waveform, drag the green edges to resize or the loop region to move. Shift-drag defines a new loop. Playback continues while editing.").font(.callout).foregroundStyle(.secondary)
HStack{Button("Cancel"){dismiss()}.help("Close without changing the loop");Spacer();Button("Activate Loop"){player.defineLoop(start:start,end:end);dismiss()}.disabled(!valid||!player.canJog).keyboardShortcut(.defaultAction).help("Apply these start/end points and enable looping")}
HStack(spacing:8){Text("MASTER").font(.system(size:9,weight:.semibold)).foregroundStyle(.secondary);DJHardwareFader(value:$system.master,label:"DJ master level").frame(width:95,height:28).help("Adjust the overall DJ output level for both decks")}
Button{sidebarVisible.toggle()}label:{Image(systemName:"sidebar.left")}.help("Show or hide collections").buttonStyle(LibraryActionButtonStyle(tint:DJStyle.a))
Button{graphicEQVisible=true}label:{Label("Equaliser",systemImage:"slider.vertical.3")}.buttonStyle(LibraryActionButtonStyle(tint:system.graphicEQ.enabled?DJStyle.a:.gray)).help("Open the ten-band graphic equaliser and sound presets for both decks")
StudioSearchField(placeholder:"Search title, artist or album",text:$query).frame(minWidth:160,idealWidth:250,maxWidth:340).help("Filter this collection or playlist by title, artist, album or other track metadata")
Button{importMedia()}label:{Label("Import",systemImage:"square.and.arrow.down")}.buttonStyle(LibraryActionButtonStyle(tint:DJStyle.a,prominent:true)).help("Import audio files or folders, including subfolders; already imported tracks are skipped")
Picker("Sort tracks",selection:$librarySort){ForEach(DJLibrarySort.allCases){Text($0.rawValue).tag($0)}}.labelsHidden().frame(width:155).help("Sort the visible tracks; playlist/import order preserves the original sequence")
Button{system.automix.enqueue(selectedTracks)}label:{Label("Queue Selected",systemImage:"text.badge.plus")}.buttonStyle(LibraryActionButtonStyle(tint:.green)).disabled(selectedTracks.isEmpty).help("Add every selected track to the DJ queue")
Button{system.toggleFavourite(song.id)}label:{GoldStar(filled:system.favourites.contains(song.id),size:15)}.buttonStyle(.plain).accessibilityLabel(system.favourites.contains(song.id)?"Remove \(song.title) from favourites":"Favourite \(song.title)")
Button{system.toggleFavourite(song.id)}label:{GoldStar(filled:system.favourites.contains(song.id),size:15)}.buttonStyle(.plain).accessibilityLabel(system.favourites.contains(song.id)?"Remove \(song.title) from favourites":"Favourite \(song.title)").help(system.favourites.contains(song.id)?"Remove this track from Favourites":"Add this track to Favourites")
VStack(alignment:.leading,spacing:2){Text(song.title).font(.system(size:11,weight:.semibold)).lineLimit(1);Text(song.artist.isEmpty?"Artist not set":song.artist).font(.caption).foregroundStyle(.secondary).lineLimit(1)}
Button("Info"){editingTrack=song}.buttonStyle(LibraryActionButtonStyle(tint:.gray)).help("Edit the selected track’s title, artist, album and metadata")
Button("Load A"){system.load(song,into:"A")}.buttonStyle(LibraryActionButtonStyle(tint:DJStyle.a)).help("Load the selected track into deck A; a playing deck is protected")
Button("Load B"){system.load(song,into:"B")}.buttonStyle(LibraryActionButtonStyle(tint:DJStyle.b)).help("Load the selected track into deck B; a playing deck is protected")
Button("SYNC"){system.sync(deck)}.foregroundStyle(system.syncActive?color:Color.secondary).font(.system(size:11,weight:.bold)).buttonStyle(DJConsoleButtonStyle(system.syncActive?color:nil,active:system.syncActive)).accessibilityValue(system.syncActive?"On":"Off").help("Match the other deck to this deck’s BPM; click again to restore both original tempos").accessibilityIdentifier("dj.\(deck.letter).sync")
HStack(spacing:16){Button("−"){player.tempo=max(1-system.settings.tempoRange/100,player.tempo-0.001)}.help("Decrease tempo by 0.1 percentage points");Button("+"){player.tempo=min(1+system.settings.tempoRange/100,player.tempo+0.001)}.help("Increase tempo by 0.1 percentage points")}.buttonStyle(.plain).font(.system(size:15,weight:.medium))
funcupdateNSView(_slider:NSSlider,context:Context){context.coordinator.value=$value;slider.isEnabled=context.environment.isEnabled;slider.isVertical=vertical;slider.doubleValue=min(1,max(0,value));slider.setAccessibilityLabel(label);slider.toolTip=label+". Drag to adjust."}
TextField("BPM",value:Binding(get:{deck.bpm},set:{deck.bpm=min(240,max(40,$0));deck.bpmConfirmed=true;deck.bpmEstimated=false}),format:.number.precision(.fractionLength(1))).frame(width:44).textFieldStyle(.roundedBorder).controlSize(.mini).accessibilityLabel("Deck \(deck.letter) source BPM").help("Set the track’s original BPM to enable beat sync and beat loops")
DJWaveform(player:player,color:color,bpm:deck.bpmConfirmed?deck.bpm:nil).frame(height:22).help("Track overview: click or drag to seek to a position in the song")
Button{player.toggle()}label:{Image(systemName:player.playing?"pause.fill":"play.fill").font(.system(size:17)).frame(width:36,height:22)}.foregroundStyle(player.playing?Color.green:Color.white).accessibilityLabel("Deck \(deck.letter) play pause").accessibilityIdentifier("dj.\(deck.letter).play")
.foregroundStyle(player.playing?Color.green:Color.white).help("Play or pause deck \(deck.letter)").accessibilityLabel("Deck \(deck.letter) play pause").accessibilityIdentifier("dj.\(deck.letter).play")
ForEach(0..<4){indexinButton("\(index+1)"){system.hotCue(index,deck:deck)}.foregroundStyle(player.hotCues[index]==nil?Color.secondary:DJStyle.color(deck.letter)).contextMenu{Button("Clear hot cue"){player.hotCue(index,clear:true)}}}
.help(player.hotCues[index].map{"Hot cue \(index+1) at \(DJStyle.stamp($0)). Click to jump; Option-click or right-click to clear."}??"Click to set hot cue \(index+1) at the current position. Option-click or right-click clears it.")
.contextMenu{Button("Clear Hot Cue \(index+1)"){player.hotCue(index,clear:true)}.disabled(player.hotCues[index]==nil)}
}label:{Image(systemName:"flag")}.help("Set, edit or clear the main cue; open the loop start/end editor")
Button("IN"){player.markLoopIn()}.foregroundStyle(player.loopIn==nil?.primary:DJStyle.color(deck.letter)).help("Set the loop start at the current position. Right-click to clear the loop markers.")
Button("OUT"){player.markLoopOut()}.foregroundStyle(player.loopOut==nil?.primary:DJStyle.color(deck.letter)).help("Set the loop end and enable the loop. Set IN first; right-click to clear both markers.")
HStack{Text("Position in seconds");TextField("Seconds",value:$system.cueDraft,format:.number.precision(.fractionLength(2))).textFieldStyle(.roundedBorder).frame(width:100);Text("/ "+DJStyle.stamp(deck.player.duration)).foregroundStyle(.secondary)}
Slider(value:$system.cueDraft,in:0...max(0.001,deck.player.duration)).accessibilityLabel("Cue point position")
Button("Use Current Playhead"){system.cueDraft=deck.player.time}
Button("Use Current Playhead"){system.cueDraft=deck.player.time}.help("Use the current playback position as the main cue")
Text("Saving changes the cue point without moving or pausing playback. The CUE button pauses and returns to this position.").font(.caption).foregroundStyle(.secondary)
HStack{Button("Cancel"){system.cueEditing=false}.help("Close without changing the cue");Spacer();Button("Save Cue Point"){system.saveCue()}.keyboardShortcut(.defaultAction).help("Save the main cue without moving or pausing playback")}
@@ -136,3 +136,7 @@ Loop activation and cancellation prepare an idle transport node while the audibl
DJ Studio’s performance workspace uses a teal/violet palette, separate track headers, full-width stacked scrolling waveforms, two deck performance panels and a central two-channel mixer. Each performance panel groups pitch/sync, platter, transport, hot cues and loop controls. Cue and loop editors are in the flag menu. The mixer groups both channels’ EQ/filter/level controls above the crossfader; master level is in the studio header. The performance/library divider and collection/table divider resize independently.
The library sidebar provides All Music, Favourites, Top Rated (four or five stars), Unrated and Needs Metadata (missing artist), plus existing playlists. Sorting supports title, artist, album, duration, rating and original playlist/import order; use the arrow to reverse it. Playlist selection retains playlist order by default. Search, multiple selection, select-all, native column resizing, ratings, context actions, waveform jobs and paged scrolling remain available. Selecting a track reveals Info and Load A/B controls below the table. The collection sidebar scrolls and can be hidden from the header. All views use the existing independent DJ database and playlists; no external resources or third-party artwork are included.
EQ and filter knobs use native mouse-event click counting, so double-clicking either the knob or its label restores its defined default (0 dB for EQ and centred filter). Dragging remains vertical; arrow keys adjust the focused control and Space resets it. Resize handle positions and collection-sidebar visibility are remembered between launches.
The eraser menu beside IN/OUT clears individual hot cues, all hot cues, the main cue, loop markers, or every cue/loop marker on that deck. Hot cues also support Option-click and right-click to clear. Clearing the main cue resets it to track start without seeking; clearing markers retains the loaded track and playback. Hover descriptions cover the performance controls, mixer, library actions, queue controls and cue/loop editors.
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.