import MusicAssistantCore import SwiftUI struct ArrangementEditorView: View { @Binding var project: SongProject var body: some View { VStack(alignment: .leading, spacing: 12) { let selectedInstrumentIndices = project.instruments.indices.filter { project.instruments[$0].selected } if selectedInstrumentIndices.isEmpty { Text("Select instruments before arranging entries.") .foregroundStyle(.secondary) } else { ForEach(selectedInstrumentIndices, id: \.self) { index in InstrumentArrangementCard( instrument: $project.instruments[index], sections: project.orderedSections ) } } } } } private struct InstrumentArrangementCard: View { @Binding var instrument: InstrumentTrack let sections: [SongSection] var body: some View { VStack(alignment: .leading, spacing: 10) { HStack { Text(instrumentDisplayName) .font(.headline) .lineLimit(1) Spacer() Button(action: addPlacement) { Label("Add placement", systemImage: "plus.circle.fill") .labelStyle(.iconOnly) } .buttonStyle(.borderless) .help("Add placement") } Toggle("Auto arrangement", isOn: $instrument.autoArrangementEnabled) TextField("Role", text: optionalStringBinding(\.role)) TextField("Playing style", text: optionalStringBinding(\.playingStyle)) if instrument.placements.isEmpty { Text("No placements yet") .foregroundStyle(.secondary) } else { ForEach(instrument.placements.indices, id: \.self) { placementIndex in InstrumentPlacementEditor( placement: $instrument.placements[placementIndex], sections: sections, removeAction: { instrument.placements.remove(at: placementIndex) } ) } } } .padding(10) .background(Color(nsColor: .controlBackgroundColor), in: RoundedRectangle(cornerRadius: 8)) } private var instrumentDisplayName: String { if let variant = instrument.variant, !variant.isEmpty { return variant } return instrument.instrumentId } private func addPlacement() { instrument.placements.append( InstrumentPlacement(sectionId: sections.first?.id) ) } private func optionalStringBinding(_ keyPath: WritableKeyPath) -> Binding { Binding { instrument[keyPath: keyPath] ?? "" } set: { newValue in let trimmedValue = newValue.trimmingCharacters(in: .whitespacesAndNewlines) instrument[keyPath: keyPath] = trimmedValue.isEmpty ? nil : trimmedValue } } } private struct InstrumentPlacementEditor: View { @Binding var placement: InstrumentPlacement let sections: [SongSection] let removeAction: () -> Void var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Picker("Section", selection: sectionIDBinding) { Text("No section").tag("") ForEach(sections) { section in Text(sectionLabel(for: section)).tag(section.id) } } Spacer() Button(action: removeAction) { Label("Remove placement", systemImage: "trash") .labelStyle(.iconOnly) } .buttonStyle(.borderless) .foregroundStyle(.red) .help("Remove placement") } TextField("Start seconds", text: optionalTimeBinding(\.startTime)) TextField("End seconds", text: optionalTimeBinding(\.endTime)) TextField("Direction", text: optionalStringBinding(\.direction), axis: .vertical) .lineLimit(1...3) } .padding(8) .background(Color(nsColor: .windowBackgroundColor), in: RoundedRectangle(cornerRadius: 8)) } private var sectionIDBinding: Binding { Binding { placement.sectionId ?? "" } set: { newValue in placement.sectionId = newValue.isEmpty ? nil : newValue } } private func optionalTimeBinding(_ keyPath: WritableKeyPath) -> Binding { Binding { guard let value = placement[keyPath: keyPath] else { return "" } return String(format: "%.0f", value) } set: { newValue in let trimmedValue = newValue.trimmingCharacters(in: .whitespacesAndNewlines) placement[keyPath: keyPath] = trimmedValue.isEmpty ? nil : TimeInterval(trimmedValue) } } private func optionalStringBinding(_ keyPath: WritableKeyPath) -> Binding { Binding { placement[keyPath: keyPath] ?? "" } set: { newValue in let trimmedValue = newValue.trimmingCharacters(in: .whitespacesAndNewlines) placement[keyPath: keyPath] = trimmedValue.isEmpty ? nil : trimmedValue } } private func sectionLabel(for section: SongSection) -> String { "\(section.order + 1). \(section.title) (\(section.type.displayName))" } } #Preview { ArrangementEditorPreview() } private struct ArrangementEditorPreview: View { @State private var project = SongProject( title: "Arrangement Preview", idea: "Place instruments", sections: [ SongSection(type: .intro, title: "Intro"), SongSection(type: .verse, title: "Verse", order: 1) ], instruments: [ InstrumentTrack( instrumentId: "piano", selected: true, variant: "Piano", role: "Harmony bed", placements: [ InstrumentPlacement(sectionId: nil, startTime: 0, endTime: 12, direction: "Soft opening") ] ) ] ) var body: some View { Form { ArrangementEditorView(project: $project) } .formStyle(.grouped) .frame(width: 380) } }