Add worldwide genre style selector

This commit is contained in:
diyaa
2026-09-15 18:49:34 +02:00
parent 03bfd3ef75
commit 297b791a3e
10 changed files with 607 additions and 15 deletions
@@ -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 }