import MusicAssistantCore import SwiftUI struct VocalistBrowserView: View { @Binding private var project: SongProject private let saveProject: (SongProject) async -> Bool private let catalog: LocalVocalistPresetCatalog @State private var searchText = "" @State private var selectedFamilyCategory: String? @State private var hasUnsavedSelectionChanges = false @State private var isSaving = false @Environment(\.dismiss) private var dismiss init( project: Binding, saveProject: @escaping (SongProject) async -> Bool = { _ in true }, catalog: LocalVocalistPresetCatalog = LocalVocalistPresetCatalog() ) { _project = project self.saveProject = saveProject self.catalog = catalog } var body: some View { NavigationStack { List( catalog.vocalists( matching: searchText, familyCategory: selectedFamilyCategory ), id: \.id ) { preset in Toggle(isOn: selectionBinding(for: preset)) { VStack(alignment: .leading, spacing: 4) { Text(preset.label) .font(.body.weight(.medium)) Text(detailText(for: preset)) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } .toggleStyle(.checkbox) .padding(.vertical, 2) } .navigationTitle("Vocalist Catalog") .searchable(text: $searchText, prompt: "Search vocalists") .toolbar { ToolbarItem(placement: .automatic) { Picker("Category", selection: $selectedFamilyCategory) { Text("All categories").tag(String?.none) ForEach(catalog.familyCategories, id: \.self) { familyCategory in Text(familyCategory).tag(Optional(familyCategory)) } } .pickerStyle(.menu) } ToolbarItem(placement: .cancellationAction) { Button("Done") { Task { await saveSelectionChangesIfNeeded() dismiss() } } .disabled(isSaving) } if isSaving { ToolbarItem(placement: .status) { ProgressView() .controlSize(.small) } } } } .frame(minWidth: 480, minHeight: 520) .onDisappear { guard hasUnsavedSelectionChanges, !isSaving else { return } let projectToSave = project hasUnsavedSelectionChanges = false Task { _ = await saveProject(projectToSave) } } } private func selectionBinding(for preset: VocalistPreset) -> Binding { Binding { project.vocalist(for: preset.id) != nil } set: { isSelected in if isSelected { project.upsertVocalist(preset.vocalist) } else { project.removeVocalist(id: preset.id) } hasUnsavedSelectionChanges = true } } private func detailText(for preset: VocalistPreset) -> String { [preset.familyCategory, preset.voiceType, preset.performanceStyle] .compactMap { $0 } .joined(separator: " ยท ") } private func saveSelectionChangesIfNeeded() async { guard hasUnsavedSelectionChanges else { return } isSaving = true let didSave = await saveProject(project) hasUnsavedSelectionChanges = !didSave isSaving = false } } #Preview { VocalistBrowserPreview() } private struct VocalistBrowserPreview: View { @State private var project = SongProject( title: "Vocalist Catalog Preview", idea: "Choose vocalist presets." ) var body: some View { VocalistBrowserView(project: $project) } }