Files
2026-09-15 19:07:55 +02:00

136 lines
4.3 KiB
Swift

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<SongProject>,
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<Bool> {
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)
}
}