136 lines
4.3 KiB
Swift
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)
|
|
}
|
|
}
|