Initial commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user