removed unwanted bar now with waveforms, major updates
This commit is contained in:
@@ -172,6 +172,51 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
@MainActor final class DJSystem: ObservableObject {
|
||||
@Published private(set) var mediaDirectory: URL
|
||||
@Published private(set) var waveformDirectory: URL
|
||||
@Published private(set) var movingStorage = false
|
||||
@Published var storageProgress: ImportProgress?
|
||||
private var storageTask: Task<Void,Never>?
|
||||
private var storageAccess: URL?
|
||||
private func loadStorage() throws {
|
||||
let location = try library.flatMap{try DJStorageMove.location(in:$0)}
|
||||
var folder = location.map{URL(fileURLWithPath:$0.path)} ?? root
|
||||
if let bookmark = location?.bookmark { var stale = false;if let resolved = try? URL(resolvingBookmarkData:bookmark,options:[.withSecurityScope,.withoutUI],relativeTo:nil,bookmarkDataIsStale:&stale) { folder = resolved } }
|
||||
if storageAccess != folder {
|
||||
storageAccess?.stopAccessingSecurityScopedResource();storageAccess = folder.startAccessingSecurityScopedResource() ? folder : nil
|
||||
}
|
||||
mediaDirectory = folder.appendingPathComponent("Media");waveformDirectory = folder.appendingPathComponent("Waveforms")
|
||||
for player in [a.player,b.player,preview] { player.useWaveformCache(at:waveformDirectory) }
|
||||
}
|
||||
func chooseStorage() {
|
||||
guard !movingStorage,!importing,!generatingWaveforms,!namesBusy else { error = "Finish the current library job first.";return }
|
||||
guard !a.player.playing,!b.player.playing,!preview.playing else { error = "Stop both decks and headphone preview before moving DJ files.";return }
|
||||
let panel = NSOpenPanel();panel.title = "Move DJ Media and Waveforms";panel.prompt = "Move Here"
|
||||
panel.canChooseFiles = false;panel.canChooseDirectories = true;panel.canCreateDirectories = true
|
||||
panel.message = "Choose a folder on your external drive. DJ media and saved waveforms move into Media and Waveforms folders here. The database and playlists stay on this Mac."
|
||||
panel.begin { [weak self] response in guard response == .OK,let url = panel.url else { return };Task { @MainActor in self?.moveStorage(to:url) } }
|
||||
}
|
||||
func moveStorage(to destination: URL) {
|
||||
guard !movingStorage,!importing,!generatingWaveforms,!namesBusy,!a.player.playing,!b.player.playing,!preview.playing else { error = "Stop playback and finish the current library job before moving DJ files.";return }
|
||||
let loadedA = a.song?.id,loadedB = b.song?.id,database = root.appendingPathComponent("Library.sqlite"),localRoot = root
|
||||
a.unload();b.unload();stopPreview();movingStorage = true;storageProgress = .init()
|
||||
storageTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = destination.startAccessingSecurityScopedResource();defer { if access { destination.stopAccessingSecurityScopedResource() } }
|
||||
var latest = ImportProgress()
|
||||
do { latest = try DJStorageMove.run(database:database,defaultRoot:localRoot,destination:destination,cancelled:{Task.isCancelled}) { value in
|
||||
latest = value;Task { @MainActor [weak self] in self?.storageProgress = value }
|
||||
} } catch is CancellationError { latest.cancelled = true;latest.current = "Move cancelled; verified files kept" }
|
||||
catch { latest.failure = error.localizedDescription;latest.current = "Move needs attention" }
|
||||
latest.finished = true;let result = latest
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self else { return };self.movingStorage = false;self.storageTask = nil
|
||||
self.perform { try self.loadStorage();try self.refresh() }
|
||||
for (id,deck) in [(loadedA,self.a),(loadedB,self.b)] { if let id,let song = self.songs.first(where:{$0.id == id}) { self.perform { try deck.load(song) } } }
|
||||
self.storageProgress = result
|
||||
}
|
||||
}
|
||||
}
|
||||
func cancelStorageMove() { storageTask?.cancel() }
|
||||
@Published var namesBusy = false
|
||||
private var graphicSaveTask: Task<Void,Never>?
|
||||
@Published var graphicEQ = DJGraphicEQSettings() { didSet {
|
||||
@@ -193,10 +238,10 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
@Published private(set) var generatingWaveforms = false
|
||||
private var waveformTask: Task<Void,Never>?
|
||||
func generateWaveforms(_ ids: Set<Int64>? = nil,force: Bool = false) {
|
||||
guard !generatingWaveforms,!importing else { error = "Wait for the current import or waveform job to finish first.";return }
|
||||
guard !generatingWaveforms,!importing,!movingStorage else { error = "Wait for the current import or waveform job to finish first.";return }
|
||||
let tracks = songs.filter { ids == nil || ids!.contains($0.id) }
|
||||
guard !tracks.isEmpty else { error = "Choose a DJ library track first.";return }
|
||||
let cache = root.appendingPathComponent("Waveforms")
|
||||
let cache = waveformDirectory
|
||||
generatingWaveforms = true;waveformJob = .init(total:tracks.count)
|
||||
waveformTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let result = await WaveformBatch.runConcurrent(songs:tracks,locations:[],cache:cache,force:force) { update in
|
||||
@@ -253,6 +298,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
private var queuedFileLoads = [(urls:[URL],deck:String,override:Bool,song:Int64?)]()
|
||||
init(storageURL: URL? = nil) {
|
||||
root = storageURL ?? FileManager.default.urls(for:.applicationSupportDirectory,in:.userDomainMask)[0].appendingPathComponent("KJamsClone/DJ")
|
||||
mediaDirectory = root.appendingPathComponent("Media");waveformDirectory = root.appendingPathComponent("Waveforms")
|
||||
a = DJChannel("A",waveformCache:root.appendingPathComponent("Waveforms"));b = DJChannel("B",waveformCache:root.appendingPathComponent("Waveforms"))
|
||||
preview.autoFade = false;preview.volume = cueLevel;refreshDevices();applyMix()
|
||||
Publishers.CombineLatest4(a.$bpm,b.$bpm,a.player.$tempo,b.player.$tempo).dropFirst().sink { [weak self] _ in
|
||||
@@ -266,7 +312,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
}.store(in:&syncObservers)
|
||||
perform {
|
||||
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
|
||||
library = try Library(url:root.appendingPathComponent("Library.sqlite"));try refresh()
|
||||
library = try Library(url:root.appendingPathComponent("Library.sqlite"));try loadStorage();try refresh()
|
||||
if let data = try? Data(contentsOf:root.appendingPathComponent("DJGraphicEQ.json")),let saved = try? JSONDecoder().decode(DJGraphicEQSettings.self,from:data) { graphicEQ = saved.sanitized }
|
||||
applyGraphicEQ()
|
||||
favourites = Set((try library?.setting("DJ.Favourites.v1") ?? "").split(separator:",").compactMap{Int64($0)})
|
||||
@@ -292,9 +338,9 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func addToPlaylist(_ playlist: Int64,song: Int64) { perform { try library?.addToPlaylist(playlist,song:song);try refresh() } }
|
||||
func dragReference(_ song: Int64) -> SongDragReference { .init(song:song,library:dragSession,singer:nil,venue:nil,fromTonight:false) }
|
||||
func importFiles(_ urls: [URL],loadInto: String? = nil,overridePlaying: Bool = false) {
|
||||
guard !importing,!generatingWaveforms,!urls.isEmpty else { return }
|
||||
guard !importing,!generatingWaveforms,!movingStorage,!urls.isEmpty else { return }
|
||||
importing = true;importProgress = .init();importLogVisible = true
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = root.appendingPathComponent("Media"),cache = root.appendingPathComponent("Waveforms"),replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = mediaDirectory,cache = waveformDirectory,replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
importTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = urls.filter{$0.startAccessingSecurityScopedResource()}
|
||||
defer { for url in access { url.stopAccessingSecurityScopedResource() } }
|
||||
@@ -326,7 +372,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
requestFileLoad(next.urls,into:next.deck,override:next.override && channel(next.deck).song?.id == next.song)
|
||||
}
|
||||
func cancelReplacement() { pendingReplacement = nil;continueFileLoads() }
|
||||
deinit { importTask?.cancel();waveformTask?.cancel() }
|
||||
deinit { importTask?.cancel();waveformTask?.cancel();storageTask?.cancel();storageAccess?.stopAccessingSecurityScopedResource() }
|
||||
|
||||
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 }
|
||||
@@ -341,6 +387,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
} else { load(song,into:target) }
|
||||
}
|
||||
func load(_ song: Song,into letter: String,overridePlaying: Bool = false,automated: Bool = false) {
|
||||
guard !movingStorage else { error = "Wait for the DJ storage move to finish.";return }
|
||||
perform {
|
||||
let deck = channel(letter)
|
||||
if settings.protectPlayingDeck,deck.player.playing,!overridePlaying { pendingReplacement = .init(deck:letter,song:song,urls:[]);return }
|
||||
@@ -1609,6 +1656,14 @@ struct DJPreferencesPane: View {
|
||||
}
|
||||
private var library: some View {
|
||||
VStack(alignment:.leading,spacing:26) {
|
||||
PreferencesSection("Media and Waveform Storage") {
|
||||
VStack(alignment:.leading,spacing:14) {
|
||||
LabeledContent("Media") { Text(system.mediaDirectory.path).textSelection(.enabled) }
|
||||
LabeledContent("Waveforms") { Text(system.waveformDirectory.path).textSelection(.enabled) }
|
||||
Button("Move Media and Waveforms…") { system.chooseStorage() }.disabled(system.movingStorage || system.importing || system.generatingWaveforms)
|
||||
Text("Choose a folder on an external drive. Files are copied and verified before originals are removed. The DJ database and playlists stay on this Mac. Stop playback before moving files.").font(.caption).foregroundStyle(.secondary)
|
||||
}.padding(12)
|
||||
}
|
||||
PreferencesSection("Independent DJ Collection") {
|
||||
VStack(alignment:.leading,spacing:14) {
|
||||
LabeledContent("Tracks",value:"\(system.songs.count)");LabeledContent("Playlists",value:"\(system.playlists.count)")
|
||||
|
||||
Reference in New Issue
Block a user