Add worldwide genre style selector
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import MusicAssistantCore
|
||||
import SwiftUI
|
||||
|
||||
struct GenreStyleBrowserView: View {
|
||||
@Binding private var project: SongProject
|
||||
private let saveProject: (SongProject) async -> Bool
|
||||
private let catalog: LocalGenreStyleCatalog
|
||||
|
||||
@State private var searchText = ""
|
||||
@State private var selectedFamilyCategory: String?
|
||||
@State private var selectedRegionOrigin: 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: LocalGenreStyleCatalog = LocalGenreStyleCatalog()
|
||||
) {
|
||||
_project = project
|
||||
self.saveProject = saveProject
|
||||
self.catalog = catalog
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Picker("Category", selection: $selectedFamilyCategory) {
|
||||
Text("All categories").tag(String?.none)
|
||||
|
||||
ForEach(catalog.familyCategories, id: \.self) { familyCategory in
|
||||
Text(familyCategory).tag(Optional(familyCategory))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
Spacer()
|
||||
|
||||
Picker("Region", selection: $selectedRegionOrigin) {
|
||||
Text("All regions").tag(String?.none)
|
||||
|
||||
ForEach(catalog.regionsOfOrigin, id: \.self) { regionOrigin in
|
||||
Text(regionOrigin).tag(Optional(regionOrigin))
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 8)
|
||||
|
||||
Divider()
|
||||
|
||||
List(
|
||||
catalog.styles(
|
||||
matching: searchText,
|
||||
familyCategory: selectedFamilyCategory,
|
||||
regionOrigin: selectedRegionOrigin
|
||||
),
|
||||
id: \.id
|
||||
) { style in
|
||||
Toggle(isOn: selectionBinding(for: style)) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(style.name)
|
||||
.font(.body.weight(.medium))
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(style.familyCategory)
|
||||
|
||||
if let regionOrigin = style.regionOrigin {
|
||||
Text(regionOrigin)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
.toggleStyle(.checkbox)
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Genre & Style Catalog")
|
||||
.searchable(text: $searchText, prompt: "Search genres and styles")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
Task {
|
||||
await saveSelectionChangesIfNeeded()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.disabled(isSaving)
|
||||
}
|
||||
|
||||
if isSaving {
|
||||
ToolbarItem(placement: .status) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 520, minHeight: 560)
|
||||
.onDisappear {
|
||||
guard hasUnsavedSelectionChanges, !isSaving else { return }
|
||||
|
||||
let projectToSave = project
|
||||
hasUnsavedSelectionChanges = false
|
||||
|
||||
Task {
|
||||
_ = await saveProject(projectToSave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func selectionBinding(for item: GenreStyleCatalogItem) -> Binding<Bool> {
|
||||
Binding {
|
||||
project.isGenreStyleSelected(item.genreStyle)
|
||||
} set: { isSelected in
|
||||
if isSelected {
|
||||
project.selectGenreStyle(item.genreStyle)
|
||||
} else {
|
||||
project.deselectGenreStyle(id: item.id)
|
||||
}
|
||||
hasUnsavedSelectionChanges = true
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSelectionChangesIfNeeded() async {
|
||||
guard hasUnsavedSelectionChanges else { return }
|
||||
|
||||
isSaving = true
|
||||
let didSave = await saveProject(project)
|
||||
hasUnsavedSelectionChanges = !didSave
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
GenreStyleBrowserPreview()
|
||||
}
|
||||
|
||||
private struct GenreStyleBrowserPreview: View {
|
||||
@State private var project = SongProject(
|
||||
title: "Genre Catalog Preview",
|
||||
idea: "Browse music styles."
|
||||
)
|
||||
|
||||
var body: some View {
|
||||
GenreStyleBrowserView(project: $project)
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ struct ProjectInspectorView: View {
|
||||
@Binding var project: SongProject
|
||||
let saveProject: (SongProject) async -> Bool
|
||||
|
||||
@State private var newGenreName = ""
|
||||
@State private var newInstrumentName = ""
|
||||
@State private var newVocalistLabel = ""
|
||||
@State private var newLanguageName = ""
|
||||
@State private var isInstrumentBrowserPresented = false
|
||||
@State private var isGenreStyleBrowserPresented = false
|
||||
@State private var isSaving = false
|
||||
@State private var saveStatus: SaveStatus?
|
||||
|
||||
@@ -50,17 +50,18 @@ struct ProjectInspectorView: View {
|
||||
}
|
||||
|
||||
Section("Genres") {
|
||||
Button {
|
||||
isGenreStyleBrowserPresented = true
|
||||
} label: {
|
||||
Label("Browse genres and styles", systemImage: "music.note.list")
|
||||
}
|
||||
|
||||
EditableTokenList(
|
||||
values: project.genres.map(\.name),
|
||||
removeAction: { index in
|
||||
project.deselectGenreStyle(id: project.genres[index].id)
|
||||
}
|
||||
)
|
||||
AddTextRow(
|
||||
placeholder: "Add genre",
|
||||
text: $newGenreName,
|
||||
addAction: addGenre
|
||||
)
|
||||
}
|
||||
|
||||
Section("Instruments") {
|
||||
@@ -149,6 +150,9 @@ struct ProjectInspectorView: View {
|
||||
.sheet(isPresented: $isInstrumentBrowserPresented) {
|
||||
InstrumentBrowserView(project: $project, saveProject: saveProject)
|
||||
}
|
||||
.sheet(isPresented: $isGenreStyleBrowserPresented) {
|
||||
GenreStyleBrowserView(project: $project, saveProject: saveProject)
|
||||
}
|
||||
}
|
||||
|
||||
private var durationSecondsBinding: Binding<String> {
|
||||
@@ -172,13 +176,6 @@ struct ProjectInspectorView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func addGenre() {
|
||||
let name = trimmed(newGenreName)
|
||||
guard !name.isEmpty else { return }
|
||||
project.selectGenreStyle(GenreStyle(id: stableID(from: name), name: name))
|
||||
newGenreName = ""
|
||||
}
|
||||
|
||||
private func addInstrument() {
|
||||
let name = trimmed(newInstrumentName)
|
||||
guard !name.isEmpty else { return }
|
||||
|
||||
Reference in New Issue
Block a user