import Foundation import MusicAssistantCore import SwiftUI struct ProjectBrowserView: View { @StateObject private var viewModel: ProjectBrowserViewModel @State private var selectedProjectID: String? init(store: any ProjectStore = LocalSongProjectStore(directoryURL: ProjectBrowserStorage.projectsDirectoryURL)) { _viewModel = StateObject(wrappedValue: ProjectBrowserViewModel(store: store)) } var body: some View { NavigationSplitView { List(selection: $selectedProjectID) { Section("Projects") { ForEach(viewModel.projects) { project in ProjectListRow(project: project) .tag(project.id) } } } .navigationTitle("AI Music Studio") .overlay { if viewModel.isLoading { ProgressView() } else if viewModel.projects.isEmpty { ContentUnavailableView( "No Projects", systemImage: "music.note.list", description: Text("Create a song project to begin.") ) } } .toolbar { ToolbarItem { Button { Task { if let projectID = await viewModel.createProject() { selectedProjectID = projectID } } } label: { Label("New Project", systemImage: "plus") } .keyboardShortcut("n", modifiers: .command) .disabled(viewModel.isCreatingProject) } } } detail: { ProjectBrowserDetailView( project: viewModel.selectedProject, errorMessage: viewModel.errorMessage, isLoading: viewModel.isOpeningProject, saveProject: viewModel.saveProject ) } .task { await viewModel.loadProjects() selectedProjectID = selectedProjectID ?? viewModel.projects.first?.id if let selectedProjectID { await viewModel.openProject(id: selectedProjectID) } } .onChange(of: selectedProjectID) { _, projectID in guard let projectID else { viewModel.clearSelection() return } Task { await viewModel.openProject(id: projectID) } } } } private struct ProjectListRow: View { let project: SongProjectListItem var body: some View { VStack(alignment: .leading, spacing: 4) { Text(project.title) .font(.headline) .lineLimit(1) Text(project.idea.isEmpty ? "No idea yet" : project.idea) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) Text(project.updatedAt.formatted(date: .abbreviated, time: .shortened)) .font(.caption2) .foregroundStyle(.tertiary) } .padding(.vertical, 4) } } private struct ProjectBrowserDetailView: View { let project: SongProject? let errorMessage: String? let isLoading: Bool let saveProject: (SongProject) async -> Bool var body: some View { Group { if isLoading { ProgressView() .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let project { ChatWorkspaceView(project: project, saveProject: saveProject) .id(project.id) } else { ContentUnavailableView( "Select a Project", systemImage: "music.note", description: Text("Choose a saved song project from the browser.") ) } } .overlay(alignment: .bottom) { if let errorMessage { Text(errorMessage) .font(.callout) .foregroundStyle(.white) .padding(.horizontal, 12) .padding(.vertical, 8) .background(.red, in: RoundedRectangle(cornerRadius: 8)) .padding() } } } } @MainActor final class ProjectBrowserViewModel: ObservableObject { @Published private(set) var projects: [SongProjectListItem] = [] @Published private(set) var selectedProject: SongProject? @Published private(set) var isLoading = false @Published private(set) var isOpeningProject = false @Published private(set) var isCreatingProject = false @Published private(set) var errorMessage: String? private let store: any ProjectStore init(store: any ProjectStore) { self.store = store } func loadProjects() async { isLoading = true errorMessage = nil do { projects = try await store.loadProjectList() } catch { errorMessage = "Unable to load projects." } isLoading = false } func openProject(id projectID: String) async { isOpeningProject = true errorMessage = nil do { selectedProject = try await store.open(id: projectID) } catch { selectedProject = nil errorMessage = "Unable to open this project." } isOpeningProject = false } func createProject() async -> String? { isCreatingProject = true errorMessage = nil let now = Date() let project = SongProject( title: "Untitled Song", idea: "", createdAt: now, updatedAt: now ) do { try await store.create(project) projects = try await store.loadProjectList() selectedProject = project isCreatingProject = false return project.id } catch { errorMessage = "Unable to create a new project." isCreatingProject = false return nil } } func saveProject(_ project: SongProject) async -> Bool { var projectToSave = project projectToSave.updatedAt = Date() errorMessage = nil do { try await store.save(projectToSave) projects = try await store.loadProjectList() selectedProject = projectToSave return true } catch { errorMessage = "Unable to save this project." return false } } func clearSelection() { selectedProject = nil } } enum ProjectBrowserStorage { static var projectsDirectoryURL: URL { let baseURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory return baseURL .appendingPathComponent("AI Music Studio", isDirectory: true) .appendingPathComponent("Projects", isDirectory: true) } } #Preview { ProjectBrowserView() }