194 lines
6.5 KiB
Swift
194 lines
6.5 KiB
Swift
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<InstrumentTrack, String?>) -> Binding<String> {
|
|
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<String> {
|
|
Binding {
|
|
placement.sectionId ?? ""
|
|
} set: { newValue in
|
|
placement.sectionId = newValue.isEmpty ? nil : newValue
|
|
}
|
|
}
|
|
|
|
private func optionalTimeBinding(_ keyPath: WritableKeyPath<InstrumentPlacement, TimeInterval?>) -> Binding<String> {
|
|
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<InstrumentPlacement, String?>) -> Binding<String> {
|
|
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)
|
|
}
|
|
}
|