Initial commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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) {
|
||||
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("Suno fields")
|
||||
.font(.headline)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
ensureSunoOutput()
|
||||
} label: {
|
||||
Label("Prepare fields", systemImage: "wand.and.stars")
|
||||
}
|
||||
.disabled(project.sunoOutput != nil)
|
||||
}
|
||||
|
||||
TextField("Suno lyrics", text: sunoLyricsBinding, axis: .vertical)
|
||||
.lineLimit(4...10)
|
||||
TextField("Suno 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 }
|
||||
project.sunoOutput = SunoOutput(
|
||||
lyricsText: project.lyrics.text,
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user