removed unwanted bar now with waveforms
This commit is contained in:
@@ -18,10 +18,11 @@ struct DJLibraryDragTitle: NSViewRepresentable {
|
||||
let reference: SongDragReference
|
||||
let fontSize: Double
|
||||
let select: () -> Void
|
||||
var doubleClick: (() -> Void)? = nil
|
||||
func makeNSView(context: Context) -> DJLibraryDragView { DJLibraryDragView() }
|
||||
func updateNSView(_ view: DJLibraryDragView,context: Context) {
|
||||
view.title = title;view.reference = reference;view.fontSize = fontSize;view.select = select
|
||||
view.toolTip = "Drag \(title) to either deck";view.setAccessibilityLabel(title);view.needsDisplay = true
|
||||
view.title = title;view.reference = reference;view.fontSize = fontSize;view.select = select;view.doubleClick = doubleClick
|
||||
view.toolTip = "Double-click to load alternating decks, or drag \(title) to either deck";view.setAccessibilityLabel(title);view.needsDisplay = true
|
||||
}
|
||||
func sizeThatFits(_ proposal: ProposedViewSize,nsView: DJLibraryDragView,context: Context) -> CGSize? { .init(width:proposal.width ?? 240,height:24) }
|
||||
}
|
||||
@@ -31,6 +32,7 @@ final class DJLibraryDragView: NSView {
|
||||
var reference: SongDragReference?
|
||||
var fontSize: Double = 11
|
||||
var select: (() -> Void)?
|
||||
var doubleClick: (() -> Void)?
|
||||
private var armed = false
|
||||
private var dragging = false
|
||||
override init(frame: NSRect) { super.init(frame:frame);setAccessibilityElement(true);setAccessibilityRole(.staticText) }
|
||||
@@ -58,7 +60,7 @@ final class DJLibraryDragView: NSView {
|
||||
armed = false;dragging = false;NSCursor.arrow.set()
|
||||
for target in DJNativeDropView.targets.allObjects { target.layer?.borderWidth = 0 }
|
||||
}
|
||||
guard dragging,let reference,let window else { select?();return }
|
||||
guard dragging,let reference,let window else { select?();if armed,event.clickCount == 2 { doubleClick?() };return }
|
||||
for target in DJNativeDropView.targets.allObjects where target.window === window && !target.isHiddenOrHasHiddenAncestor {
|
||||
if target.bounds.contains(target.convert(event.locationInWindow,from:nil)),target.validSong?(reference) == true {
|
||||
let receive = target.acceptSong
|
||||
|
||||
@@ -291,6 +291,15 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func perform(_ action: () throws -> Void) { do { try action() } catch { self.error = error.localizedDescription } }
|
||||
func applyMix() { let weights = DJMix.weights(crossfader,curve:settings.crossfaderCurve);let output = master*pow(10,-settings.headroomDB/20);a.master = output;b.master = output;a.crossWeight = routing.mode == "External" ? 1 : weights.0;b.crossWeight = routing.mode == "External" ? 1 : weights.1 }
|
||||
func channel(_ letter: String) -> DJChannel { letter == "A" ? a : b }
|
||||
private(set) var nextLibraryDeck = "A"
|
||||
func loadFromLibraryDoubleClick(_ song: Song) {
|
||||
guard pendingReplacement == nil else { return }
|
||||
let target = nextLibraryDeck
|
||||
nextLibraryDeck = target == "A" ? "B" : "A"
|
||||
if channel(target).player.playing {
|
||||
pendingReplacement = .init(deck:target,song:song,urls:[])
|
||||
} else { load(song,into:target) }
|
||||
}
|
||||
func load(_ song: Song,into letter: String,overridePlaying: Bool = false,automated: Bool = false) {
|
||||
perform {
|
||||
let deck = channel(letter)
|
||||
@@ -818,9 +827,6 @@ struct DJConsole: View {
|
||||
header
|
||||
DJContinuousConsole(system:system,loadA:{loadSelected("A")},loadB:{loadSelected("B")}).fixedSize(horizontal:false,vertical:true)
|
||||
browser.frame(maxWidth:.infinity,maxHeight:.infinity)
|
||||
if let progress = system.waveformJob {
|
||||
WaveformProgressCard(progress:progress,running:system.generatingWaveforms,studio:"DJ",cancel:{system.cancelWaveforms()},details:{waveformDetails = true},dismiss:{system.dismissWaveforms()})
|
||||
}
|
||||
if let progress = system.importProgress {
|
||||
HStack {
|
||||
Text(progress.current).lineLimit(1);Text("\(progress.completed)/\(progress.total)").monospacedDigit()
|
||||
@@ -844,12 +850,24 @@ struct DJConsole: View {
|
||||
}
|
||||
.sheet(isPresented:$system.cueEditing) { DJCueEditor(system:system) }
|
||||
.sheet(isPresented:Binding(get:{system.loopEditor != nil},set:{if !$0 { system.loopEditor = nil }})) { let deck = system.channel(system.loopEditor ?? "A");DJLoopEditor(player:deck.player,letter:deck.letter) }
|
||||
.sheet(isPresented:$waveformDetails) {
|
||||
VStack(alignment:.leading,spacing:16) {
|
||||
Text("DJ Waveform Details").font(.title2.bold())
|
||||
ScrollView { Text(system.waveformJob?.messages.joined(separator:"\n\n") ?? "").textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading) }
|
||||
HStack { Spacer();Button("Done") { waveformDetails = false }.keyboardShortcut(.defaultAction) }
|
||||
}.padding(24).frame(width:620,height:400)
|
||||
.sheet(isPresented:Binding(get:{system.waveformJob != nil},set:{if !$0 { system.dismissWaveforms() }})) {
|
||||
if let progress = system.waveformJob {
|
||||
VStack(alignment:.leading,spacing:20) {
|
||||
Label("DJ Studio Waveforms",systemImage:"waveform").font(.title2.bold()).foregroundStyle(DJStyle.a)
|
||||
Text(system.generatingWaveforms ? "Preparing saved waveforms in the background." : "Completed waveforms are saved and ready to use.").font(.callout).foregroundStyle(.secondary)
|
||||
WaveformProgressCard(progress:progress,running:system.generatingWaveforms,studio:"DJ",cancel:{system.cancelWaveforms()},details:{waveformDetails.toggle()},dismiss:{system.dismissWaveforms()})
|
||||
if waveformDetails || !progress.messages.isEmpty {
|
||||
Text("Analysis Log").font(.headline)
|
||||
ScrollView { Text(progress.messages.isEmpty ? "No issues reported." : progress.messages.joined(separator:"\n\n")).font(.system(size:11,design:.monospaced)).textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading) }.frame(height:180)
|
||||
}
|
||||
HStack {
|
||||
Text("Cancelling keeps completed waveforms.").font(.caption).foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
if !system.generatingWaveforms { Button("Done") { system.dismissWaveforms() }.buttonStyle(.borderedProminent).keyboardShortcut(.defaultAction) }
|
||||
}
|
||||
}.padding(26).frame(width:660).background(DJStyle.background).preferredColorScheme(.dark)
|
||||
.interactiveDismissDisabled(system.generatingWaveforms)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented:$queueVisible) {
|
||||
VStack(spacing:16) {
|
||||
@@ -903,7 +921,7 @@ struct DJConsole: View {
|
||||
ForEach(visible) { song in
|
||||
HStack(spacing:8) {
|
||||
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)")
|
||||
DJLibraryDragTitle(title:song.title,reference:system.dragReference(song.id),fontSize:system.settings.libraryFont,select:{selected = song.id}).frame(maxWidth:.infinity,alignment:.leading)
|
||||
DJLibraryDragTitle(title:song.title,reference:system.dragReference(song.id),fontSize:system.settings.libraryFont,select:{selected = song.id},doubleClick:{system.loadFromLibraryDoubleClick(song)}).frame(maxWidth:.infinity,alignment:.leading)
|
||||
Text(song.artist).font(.system(size:10)).foregroundStyle(.secondary).lineLimit(1).frame(width:130,alignment:.leading)
|
||||
Text(DJStyle.stamp(song.duration)).font(.system(size:10).monospacedDigit()).foregroundStyle(.secondary).frame(width:35)
|
||||
Button { system.automix.enqueue([song]) } label: { Image(systemName:"plus") }.accessibilityLabel("Add \(song.title) to DJ queue")
|
||||
|
||||
@@ -2700,4 +2700,26 @@ extension KJamsCloneTests {
|
||||
XCTAssertEqual(system.waveformJob?.cached,1);XCTAssertEqual(system.waveformJob?.generated,0)
|
||||
}
|
||||
|
||||
@MainActor func testDJLibraryDoubleClickAlternatesDecksAndAlwaysProtectsPlayingTrack() throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at:root) }
|
||||
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
|
||||
let system = DJSystem(storageURL:root)
|
||||
defer { system.a.unload();system.b.unload() }
|
||||
let song = Song(id:1,title:"Demo",audio:fixture.path,cdg:nil,rating:0,plays:0)
|
||||
system.loadFromLibraryDoubleClick(song)
|
||||
XCTAssertEqual(system.a.song?.id,1);XCTAssertNil(system.b.song)
|
||||
system.loadFromLibraryDoubleClick(song)
|
||||
XCTAssertEqual(system.b.song?.id,1);XCTAssertEqual(system.nextLibraryDeck,"A")
|
||||
system.settings.protectPlayingDeck = false
|
||||
system.a.player.toggle()
|
||||
system.loadFromLibraryDoubleClick(song)
|
||||
XCTAssertEqual(system.pendingReplacement?.deck,"A")
|
||||
XCTAssertTrue(system.a.player.playing)
|
||||
system.cancelReplacement()
|
||||
system.loadFromLibraryDoubleClick(song)
|
||||
XCTAssertNil(system.pendingReplacement);XCTAssertEqual(system.nextLibraryDeck,"A")
|
||||
XCTAssertTrue(system.a.player.playing)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,3 +89,5 @@ DJ library rows include the same gold favourite star as Karaoke Studio. Favourit
|
||||
Dragging a DJ waveform previews short audio grains through that deck’s EQ, level and output routing while paused; releasing stops the preview and retains the selected position. Scrubbing a playing deck continues playback. This is audio preview seeking, rather than reverse vinyl scratching.
|
||||
|
||||
The DJ library advances to the next page when a user scroll reaches the bottom. Page changes return the list to the top and select the first track. Right-click a DJ track and choose Waveforms to generate its cache, prepare loaded decks or the entire DJ library, or force regeneration. Batch analysis runs on a utility worker with saved-cache reuse, progress and cancellation.
|
||||
|
||||
DJ waveform generation opens a modal with a coloured progress bar, percentage ring, counts and Cancel. The results stay visible until Done; any analysis issues are listed in the modal.
|
||||
|
||||
Reference in New Issue
Block a user