diff --git a/Sources/MusicAssistantApp/Presentation/FinalReviewView.swift b/Sources/MusicAssistantApp/Presentation/FinalReviewView.swift index 8a7e80c..4ec22b6 100644 --- a/Sources/MusicAssistantApp/Presentation/FinalReviewView.swift +++ b/Sources/MusicAssistantApp/Presentation/FinalReviewView.swift @@ -3,9 +3,11 @@ import SwiftUI struct FinalReviewView: View { @Binding var project: SongProject + @Environment(\.openURL) private var openURL @State private var newFieldKey = "" @State private var newFieldValue = "" + @State private var handoffStatus: HandoffStatus? var body: some View { VStack(alignment: .leading, spacing: 12) { @@ -42,6 +44,16 @@ struct FinalReviewView: View { Label("Prepare fields", systemImage: "wand.and.stars") } .disabled(project.sunoOutput != nil) + + Button(action: sendToSuno) { + Label("Send to Suno", systemImage: "arrow.up.forward.app") + } + } + + if let handoffStatus { + Label(handoffStatus.title, systemImage: handoffStatus.systemImage) + .font(.caption) + .foregroundStyle(handoffStatus.foregroundStyle) } TextField("Suno lyrics", text: sunoLyricsBinding, axis: .vertical) @@ -165,6 +177,62 @@ struct FinalReviewView: View { generatedAt: Date() ) } + + private func sendToSuno() { + ensureSunoOutput() + + guard let output = project.sunoOutput else { return } + + do { + try CompiledSunoOutputValidator().validate( + CompiledSunoOutput(lyricsText: output.lyricsText, stylePrompt: output.stylePrompt) + ) + } catch let validationError as LocalizedError { + handoffStatus = .failed(validationError.errorDescription ?? "Unable to prepare the Suno handoff.") + return + } catch { + handoffStatus = .failed("Unable to prepare the Suno handoff.") + return + } + + openURL(SunoHandoffDestination.createPageURL) { accepted in + handoffStatus = accepted + ? .opened + : .failed("Unable to open the Suno Create page.") + } + } +} + +private enum HandoffStatus: Equatable { + case opened + case failed(String) + + var title: String { + switch self { + case .opened: + return "Suno Create opened in your browser." + case let .failed(message): + return message + } + } + + var systemImage: String { + switch self { + case .opened: + return "checkmark.circle.fill" + case .failed: + return "exclamationmark.triangle.fill" + } + } + + var foregroundStyle: Color { + switch self { + case .opened: + return .green + case .failed: + return .red + } + } } #Preview { diff --git a/Sources/MusicAssistantCore/Integrations/Suno/SunoHandoffService.swift b/Sources/MusicAssistantCore/Integrations/Suno/SunoHandoffService.swift index ce52025..a02eefa 100644 --- a/Sources/MusicAssistantCore/Integrations/Suno/SunoHandoffService.swift +++ b/Sources/MusicAssistantCore/Integrations/Suno/SunoHandoffService.swift @@ -1,3 +1,9 @@ +import Foundation + public protocol SunoHandoffService: Sendable { func handOff(_ output: CompiledSunoOutput) async throws } + +public enum SunoHandoffDestination { + public static let createPageURL = URL(string: "https://suno.com/create")! +} diff --git a/Tests/MusicAssistantCoreTests/SunoHandoffDestinationTests.swift b/Tests/MusicAssistantCoreTests/SunoHandoffDestinationTests.swift new file mode 100644 index 0000000..31b6977 --- /dev/null +++ b/Tests/MusicAssistantCoreTests/SunoHandoffDestinationTests.swift @@ -0,0 +1,12 @@ +import MusicAssistantCore +import XCTest + +final class SunoHandoffDestinationTests: XCTestCase { + func testCreatePageUsesTheOfficialSunoCreateURL() { + let url = SunoHandoffDestination.createPageURL + + XCTAssertEqual(url.scheme, "https") + XCTAssertEqual(url.host, "suno.com") + XCTAssertEqual(url.path, "/create") + } +} diff --git a/docs/TASKS.md b/docs/TASKS.md index 764311a..21d43dc 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -107,8 +107,8 @@ requirement is missing and blocks implementation, record it in ## Phase 8 --- Suno Handoff -- [ ] Implement explicit `Send to Suno` action. -- [ ] Open the appropriate Suno creation surface. +- [x] Implement explicit `Send to Suno` action. +- [x] Open the appropriate Suno creation surface. - [ ] Fill supported fields with approved project output. - [ ] Never trigger Generate automatically. - [ ] Detect/report when fields cannot be filled rather than silently