importer chnges

This commit is contained in:
Mark Gregory
2026-11-05 11:16:29 +00:00
parent 87cdb42af9
commit 97662bf4bf
8 changed files with 117 additions and 13 deletions
+2 -1
View File
@@ -184,7 +184,7 @@ struct PlaylistWorkspace: View {
Menu { Button("Rename") { if let selected { model.rename("playlists",id:selected,name:name) } }.disabled(name.trimmingCharacters(in:.whitespacesAndNewlines).isEmpty);Button("Delete…",role:.destructive) { delete = true } } label: { Image(systemName:"ellipsis.circle") }.disabled(selected == nil)
}
HStack {
Picker("Sort",selection:$sort) { ForEach(["Saved Order","Song ID","Title","Artist","Album","Track","Rendition","Duration","Rating"],id:\.self) { Text($0) } }.frame(width:200)
Picker("Sort",selection:$sort) { ForEach(["Saved Order","Song ID","Title","Artist","Album","Track","Rendition","Duration","Rating"],id:\.self) { Text($0 == "Saved Order" ? "Saved Order (PLI)" : $0).tag($0) } }.frame(width:200)
Button { ascending.toggle() } label: { Image(systemName:ascending ? "arrow.up" : "arrow.down") }.disabled(sort == "Saved Order").help("Reverse sort")
Text("\(rows.count) songs").font(.caption).foregroundStyle(.secondary);Spacer();Button(nameSelection.isEmpty ? "Reinterpret Visible Songs…" : "Reinterpret Selected…") { reinterpretIDs = nameSelection.isEmpty ? Set(rows.map(\.id)) : nameSelection.intersection(Set(rows.map(\.id)));reinterpreting = true }.disabled(rows.isEmpty);Button("Add Songs…") { adding = true }.disabled(selected == nil)
}
@@ -203,6 +203,7 @@ struct PlaylistWorkspace: View {
Text("Use toolbar search for this playlist. Quoted phrases, artist:, album:, title:, id: and exact = searches are supported. Saved order can be changed with the arrow buttons when search is empty.").font(.caption).foregroundStyle(.secondary)
}.padding(22).background(Color(nsColor:.windowBackgroundColor)).sheet(isPresented:$adding) { addSheet }
.sheet(isPresented:$reinterpreting) { ReinterpretNamesSheet(model:model,ids:reinterpretIDs) }
.focusedSceneValue(\.nameReinterpretation,NameReinterpretationCommands(perform:{ reinterpretIDs = nameSelection.isEmpty ? Set(rows.map(\.id)) : nameSelection.intersection(Set(rows.map(\.id)));reinterpreting = true },enabled:!rows.isEmpty))
.onChange(of:query) { _ in nameSelection = [] }
.confirmationDialog("Delete this playlist? Library songs are kept.",isPresented:$delete) { Button("Delete Playlist",role:.destructive) { if let selected { model.delete("playlists",id:selected);self.selected = nil } } }
.onChange(of:sort) { _ in saveSort() }.onChange(of:ascending) { _ in saveSort() }
+6 -6
View File
@@ -484,8 +484,6 @@ struct SongDragReference: Codable, Sendable {
}
private struct LibraryCommands {
var importMusic: () -> Void
var reinterpret: () -> Void
var canReinterpret: Bool
var songInfo: () -> Void
var canEdit: Bool
var play: () -> Void
@@ -504,6 +502,7 @@ private extension FocusedValues {
@StateObject private var model = AppModel(storageURL:ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil ? nil : FileManager.default.temporaryDirectory.appendingPathComponent("KJMix-TestHost-"+String(ProcessInfo.processInfo.processIdentifier)))
@AppStorage("studioAppearance") private var appearance = "system"
@FocusedValue(\.libraryCommands) private var actions
@FocusedValue(\.nameReinterpretation) private var nameActions
var body: some Scene {
Window("KJMix Pro", id: "main") {
LibraryView(model: model).frame(minWidth: 1060, minHeight: 760)
@@ -524,7 +523,7 @@ private extension FocusedValues {
Button("Add Drive or Folder…") { model.chooseMediaLocation() }
}
CommandMenu("Advanced") {
Button("Reinterpret Names…") { actions?.reinterpret() }.disabled(actions?.canReinterpret != true || model.namesBusy)
Button("Reinterpret Names…") { nameActions?.perform() }.disabled(nameActions?.enabled != true || model.namesBusy)
Button("Undo Last Name Reinterpretation") { Task { do { try await model.undoNameChanges() } catch { model.error = error.localizedDescription } } }.disabled(!model.canUndoNames || model.namesBusy)
}
CommandMenu("Playback") {
@@ -727,7 +726,8 @@ struct LibraryView: View {
}
.background(StudioTheme.frame)
.tint(.accentColor)
.focusedSceneValue(\.libraryCommands,LibraryCommands(importMusic:{importing = true},reinterpret:{beginReinterpretation()},canReinterpret:["Library","Singer Songs"].contains(source) && (selectedSong != nil || !tableSelection.isEmpty),songInfo:{editingSong = selectedSong},canEdit:selectedSong != nil,play:playSelected,next:playNext,video:{openWindow(id:"lyrics")},settings:{source = "Settings"}))
.focusedSceneValue(\.nameReinterpretation,NameReinterpretationCommands(perform:{beginReinterpretation()},enabled:["Library","Singer Songs"].contains(source) && (selectedSong != nil || !tableSelection.isEmpty)))
.focusedSceneValue(\.libraryCommands,LibraryCommands(importMusic:{importing = true},songInfo:{editingSong = selectedSong},canEdit:selectedSong != nil,play:playSelected,next:playNext,video:{openWindow(id:"lyrics")},settings:{source = "Settings"}))
.navigationTitle(source == "Singer Songs" ? (model.singers.first { $0.id == model.selectedSinger }?.name ?? "Singer") : source)
.navigationSubtitle(model.currentVenueName)
.searchable(text:$search,placement:.toolbar,prompt:"Search Music")
@@ -958,10 +958,10 @@ struct LibraryView: View {
Text(song.title).fontWeight(.medium).lineLimit(1)
}.padding(.vertical,5).onDrag { model.dragReference(song.id).provider() }
}.width(min:180,ideal:220,max:600)
TableColumn("Track",value:\Song.trackOrder) { song in Text(song.trackNumber.isEmpty ? "—" : song.trackNumber).monospacedDigit().foregroundStyle(.secondary) }.width(45)
TableColumn("Rendition",value:\Song.rendition) { song in Text(song.rendition.isEmpty ? "—" : song.rendition).foregroundStyle(.secondary) }.width(min:70,ideal:100,max:220)
TableColumn("Artist",value:\Song.artist) { song in Text(song.artist.isEmpty ? "—" : song.artist).foregroundStyle(.secondary) }.width(min:90,ideal:110,max:240)
TableColumn("Album",value:\Song.album) { song in Text(song.album.isEmpty ? "—" : song.album).foregroundStyle(.secondary) }.width(min:85,ideal:95,max:220)
TableColumn("Track",value:\Song.trackOrder) { song in Text(song.trackNumber.isEmpty ? "—" : song.trackNumber).monospacedDigit().foregroundStyle(.secondary) }.width(45)
TableColumn("Rendition",value:\Song.rendition) { song in Text(song.rendition.isEmpty ? "—" : song.rendition).foregroundStyle(.secondary) }.width(min:70,ideal:100,max:220)
TableColumn("Time",value:\Song.duration) { song in Text(durationLabel(song.duration)).monospacedDigit().foregroundStyle(.secondary) }.width(50)
TableColumn("Key") { song in
HStack(spacing:2) {
+13 -1
View File
@@ -438,7 +438,7 @@ struct ReinterpretNamesSheet: View {
private var previewTable: some View {
Table(Array(previews.prefix(200))) {
TableColumn("ID") { Text("\($0.id)").monospacedDigit().foregroundStyle(.secondary) }.width(40)
TableColumn("Source / Result") { row in VStack(alignment:.leading,spacing:3) { Text(row.input).foregroundStyle(.secondary).lineLimit(1);Text(row.after?.title ?? row.issue ?? "No match").foregroundStyle(row.after == nil ? Color.orange : Color.primary).lineLimit(2) } }.width(min:220,ideal:300)
TableColumn("Source / Result") { row in VStack(alignment:.leading,spacing:3) { Text(row.input).foregroundStyle(.secondary).lineLimit(1).help(row.input);Text(row.after?.title ?? row.issue ?? "No match").foregroundStyle(row.after == nil ? Color.orange : Color.primary).lineLimit(2) } }.width(min:220,ideal:300)
TableColumn("Artist") { Text($0.after?.artist ?? "—") }.width(min:110,ideal:170)
TableColumn("Album") { Text($0.after?.album ?? "—") }.width(min:75,ideal:110)
TableColumn("Track") { Text($0.after?.track ?? "—").monospacedDigit() }.width(45)
@@ -470,3 +470,15 @@ struct ReinterpretNamesSheet: View {
Task { do { try await model.applyNameChanges(plan);dismiss() } catch { issue = error.localizedDescription } }
}
}
struct NameReinterpretationCommands {
var perform: () -> Void
var enabled: Bool
}
private struct NameReinterpretationKey: FocusedValueKey { typealias Value = NameReinterpretationCommands }
extension FocusedValues {
var nameReinterpretation: NameReinterpretationCommands? {
get { self[NameReinterpretationKey.self] }
set { self[NameReinterpretationKey.self] = newValue }
}
}