200 lines
6.7 KiB
Swift
200 lines
6.7 KiB
Swift
import MusicAssistantCore
|
|
import SwiftUI
|
|
|
|
struct FinalReviewView: View {
|
|
@Binding var project: SongProject
|
|
|
|
@State private var newFieldKey = ""
|
|
@State private var newFieldValue = ""
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
ArabicLyricsReviewView(project: $project)
|
|
|
|
Divider()
|
|
|
|
TextField("Approved lyrics", text: $project.lyrics.text, axis: .vertical)
|
|
.lineLimit(4...10)
|
|
|
|
if !project.orderedSections.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Section lyrics")
|
|
.font(.headline)
|
|
|
|
ForEach(project.orderedSections) { section in
|
|
TextField(section.title, text: sectionLyricsBinding(for: section.id), axis: .vertical)
|
|
.lineLimit(2...5)
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
HStack {
|
|
Text("Final song output")
|
|
.font(.headline)
|
|
|
|
Spacer()
|
|
|
|
Button {
|
|
ensureSunoOutput()
|
|
} label: {
|
|
Label("Prepare fields", systemImage: "wand.and.stars")
|
|
}
|
|
.disabled(project.sunoOutput != nil)
|
|
|
|
}
|
|
|
|
TextField("Lyrics", text: sunoLyricsBinding, axis: .vertical)
|
|
.lineLimit(4...10)
|
|
TextField("Style prompt", text: sunoStylePromptBinding, axis: .vertical)
|
|
.lineLimit(3...8)
|
|
|
|
if !additionalFieldKeys.isEmpty {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Text("Additional fields")
|
|
.font(.headline)
|
|
|
|
ForEach(additionalFieldKeys, id: \.self) { key in
|
|
HStack(alignment: .top, spacing: 8) {
|
|
TextField("Field", text: additionalFieldKeyBinding(for: key))
|
|
TextField("Value", text: additionalFieldValueBinding(for: key), axis: .vertical)
|
|
.lineLimit(1...4)
|
|
|
|
Button {
|
|
project.sunoOutput?.additionalFields.removeValue(forKey: key)
|
|
} label: {
|
|
Label("Remove field", systemImage: "minus.circle")
|
|
.labelStyle(.iconOnly)
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.help("Remove field")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
TextField("Field name", text: $newFieldKey)
|
|
TextField("Value", text: $newFieldValue)
|
|
|
|
Button(action: addAdditionalField) {
|
|
Label("Add field", systemImage: "plus.circle.fill")
|
|
.labelStyle(.iconOnly)
|
|
}
|
|
.buttonStyle(.borderless)
|
|
.disabled(newFieldKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
|
.help("Add field")
|
|
}
|
|
}
|
|
}
|
|
|
|
private var additionalFieldKeys: [String] {
|
|
guard let additionalFields = project.sunoOutput?.additionalFields else { return [] }
|
|
return additionalFields.keys.sorted { $0.localizedStandardCompare($1) == .orderedAscending }
|
|
}
|
|
|
|
private var sunoLyricsBinding: Binding<String> {
|
|
Binding {
|
|
project.sunoOutput?.lyricsText ?? ""
|
|
} set: { newValue in
|
|
ensureSunoOutput()
|
|
project.sunoOutput?.lyricsText = newValue
|
|
}
|
|
}
|
|
|
|
private var sunoStylePromptBinding: Binding<String> {
|
|
Binding {
|
|
project.sunoOutput?.stylePrompt ?? ""
|
|
} set: { newValue in
|
|
ensureSunoOutput()
|
|
project.sunoOutput?.stylePrompt = newValue
|
|
}
|
|
}
|
|
|
|
private func sectionLyricsBinding(for sectionID: String) -> Binding<String> {
|
|
Binding {
|
|
project.lyrics.sectionTexts[sectionID] ?? ""
|
|
} set: { newValue in
|
|
if newValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
project.lyrics.sectionTexts.removeValue(forKey: sectionID)
|
|
} else {
|
|
project.lyrics.sectionTexts[sectionID] = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
private func additionalFieldKeyBinding(for key: String) -> Binding<String> {
|
|
Binding {
|
|
key
|
|
} set: { newKey in
|
|
let trimmedKey = newKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmedKey.isEmpty, trimmedKey != key else { return }
|
|
ensureSunoOutput()
|
|
let value = project.sunoOutput?.additionalFields.removeValue(forKey: key) ?? ""
|
|
project.sunoOutput?.additionalFields[trimmedKey] = value
|
|
}
|
|
}
|
|
|
|
private func additionalFieldValueBinding(for key: String) -> Binding<String> {
|
|
Binding {
|
|
project.sunoOutput?.additionalFields[key] ?? ""
|
|
} set: { newValue in
|
|
ensureSunoOutput()
|
|
project.sunoOutput?.additionalFields[key] = newValue
|
|
}
|
|
}
|
|
|
|
private func addAdditionalField() {
|
|
let key = newFieldKey.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !key.isEmpty else { return }
|
|
|
|
ensureSunoOutput()
|
|
project.sunoOutput?.additionalFields[key] = newFieldValue
|
|
newFieldKey = ""
|
|
newFieldValue = ""
|
|
}
|
|
|
|
private func ensureSunoOutput() {
|
|
guard project.sunoOutput == nil else { return }
|
|
let compiledOutput = (try? SongProjectPromptCompiler().compile(project: project))
|
|
?? CompiledSunoOutput(lyricsText: project.lyrics.text, stylePrompt: "")
|
|
|
|
project.sunoOutput = SunoOutput(
|
|
lyricsText: compiledOutput.lyricsText,
|
|
stylePrompt: compiledOutput.stylePrompt,
|
|
generatedAt: Date()
|
|
)
|
|
}
|
|
|
|
}
|
|
|
|
#Preview {
|
|
FinalReviewPreview()
|
|
}
|
|
|
|
private struct FinalReviewPreview: View {
|
|
@State private var project = SongProject(
|
|
title: "Review",
|
|
idea: "Review generated fields",
|
|
sections: [
|
|
SongSection(id: "verse", type: .verse, title: "Verse"),
|
|
SongSection(id: "chorus", type: .chorus, title: "Chorus", order: 1)
|
|
],
|
|
lyrics: Lyrics(text: "Draft lyrics", sectionTexts: ["verse": "Verse text"]),
|
|
sunoOutput: SunoOutput(
|
|
lyricsText: "Draft lyrics",
|
|
stylePrompt: "Cinematic pop with oud",
|
|
additionalFields: ["mood": "Hopeful"]
|
|
)
|
|
)
|
|
|
|
var body: some View {
|
|
Form {
|
|
FinalReviewView(project: $project)
|
|
}
|
|
.formStyle(.grouped)
|
|
.frame(width: 420)
|
|
}
|
|
}
|