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 }
@@ -594,6 +594,35 @@ public struct GenreStyle: Codable, Equatable, Identifiable, Sendable {
}
}
public struct GenreStyleCatalogItem: Codable, Equatable, Identifiable, Sendable {
public let id: String
public var name: String
public var familyCategory: String
public var regionOrigin: String?
public var aliases: [String]
public var searchTerms: [String]
public init(
id: String,
name: String,
familyCategory: String,
regionOrigin: String? = nil,
aliases: [String] = [],
searchTerms: [String] = []
) {
self.id = id
self.name = name
self.familyCategory = familyCategory
self.regionOrigin = regionOrigin
self.aliases = aliases
self.searchTerms = searchTerms
}
public var genreStyle: GenreStyle {
GenreStyle(id: id, name: name, aliases: aliases)
}
}
public struct MoodTag: Codable, Equatable, Identifiable, Sendable {
public let id: String
public var name: String
@@ -0,0 +1,338 @@
import Foundation
public struct LocalGenreStyleCatalog: Sendable {
public let items: [GenreStyleCatalogItem]
public init(items: [GenreStyleCatalogItem] = LocalGenreStyleCatalog.worldwideItems) {
self.items = items
}
public var familyCategories: [String] {
Array(Set(items.map(\.familyCategory)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public var regionsOfOrigin: [String] {
Array(Set(items.compactMap(\.regionOrigin)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public func styles(
matching query: String,
familyCategory: String? = nil,
regionOrigin: String? = nil
) -> [GenreStyleCatalogItem] {
let searchTokens = Self.searchTokens(for: query)
let selectedFamilyCategory = familyCategory?.trimmingCharacters(in: .whitespacesAndNewlines)
let selectedRegionOrigin = regionOrigin?.trimmingCharacters(in: .whitespacesAndNewlines)
return items
.filter { item in
let matchesSearch = searchTokens.isEmpty || searchTokens.allSatisfy { token in
Self.searchableTerms(for: item).contains { $0.contains(token) }
}
let matchesFamily = selectedFamilyCategory?.isEmpty != false || item.familyCategory == selectedFamilyCategory
let matchesRegion = selectedRegionOrigin?.isEmpty != false || item.regionOrigin == selectedRegionOrigin
return matchesSearch && matchesFamily && matchesRegion
}
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
public static let worldwideItems: [GenreStyleCatalogItem] =
popAndContemporary + rockAndAlternative + electronicAndDance + urbanAndSoul + jazzAndBlues +
countryFolkAndRoots + latinAndCaribbean + middleEasternAndNorthAfrican + southAsian + eastAsian +
southeastAsianAndOceania + african + reggaeAndCaribbean + classicalAndCinematic
private static let popAndContemporary: [GenreStyleCatalogItem] = [
item("pop", "Pop", "Pop & Contemporary", "Global"),
item("adult-contemporary", "Adult Contemporary", "Pop & Contemporary", "Western"),
item("art-pop", "Art Pop", "Pop & Contemporary", "Western"),
item("baroque-pop", "Baroque Pop", "Pop & Contemporary", "Western"),
item("chamber-pop", "Chamber Pop", "Pop & Contemporary", "Western"),
item("dance-pop", "Dance Pop", "Pop & Contemporary", "Western"),
item("dream-pop", "Dream Pop", "Pop & Contemporary", "Western"),
item("electropop", "Electropop", "Pop & Contemporary", "Western"),
item("hyperpop", "Hyperpop", "Pop & Contemporary", "Western"),
item("indie-pop", "Indie Pop", "Pop & Contemporary", "Western"),
item("power-pop", "Power Pop", "Pop & Contemporary", "Western"),
item("synth-pop", "Synth Pop", "Pop & Contemporary", "Western"),
item("teen-pop", "Teen Pop", "Pop & Contemporary", "Western"),
item("soft-pop", "Soft Pop", "Pop & Contemporary", "Western"),
item("singer-songwriter", "Singer-Songwriter", "Pop & Contemporary", "Western")
]
private static let rockAndAlternative: [GenreStyleCatalogItem] = [
item("rock", "Rock", "Rock & Alternative", "Western"),
item("alternative-rock", "Alternative Rock", "Rock & Alternative", "Western", aliases: ["alt rock"]),
item("classic-rock", "Classic Rock", "Rock & Alternative", "Western"),
item("hard-rock", "Hard Rock", "Rock & Alternative", "Western"),
item("indie-rock", "Indie Rock", "Rock & Alternative", "Western"),
item("progressive-rock", "Progressive Rock", "Rock & Alternative", "Western", aliases: ["prog rock"]),
item("psychedelic-rock", "Psychedelic Rock", "Rock & Alternative", "Western", aliases: ["psych rock"]),
item("garage-rock", "Garage Rock", "Rock & Alternative", "Western"),
item("surf-rock", "Surf Rock", "Rock & Alternative", "Western"),
item("post-rock", "Post-Rock", "Rock & Alternative", "Western"),
item("math-rock", "Math Rock", "Rock & Alternative", "Western"),
item("grunge", "Grunge", "Rock & Alternative", "Western"),
item("shoegaze", "Shoegaze", "Rock & Alternative", "Western"),
item("emo", "Emo", "Rock & Alternative", "Western"),
item("punk", "Punk", "Rock & Alternative", "Western"),
item("pop-punk", "Pop Punk", "Rock & Alternative", "Western"),
item("post-punk", "Post-Punk", "Rock & Alternative", "Western"),
item("metal", "Metal", "Rock & Alternative", "Western"),
item("heavy-metal", "Heavy Metal", "Rock & Alternative", "Western"),
item("power-metal", "Power Metal", "Rock & Alternative", "Western"),
item("progressive-metal", "Progressive Metal", "Rock & Alternative", "Western"),
item("thrash-metal", "Thrash Metal", "Rock & Alternative", "Western"),
item("death-metal", "Death Metal", "Rock & Alternative", "Western"),
item("black-metal", "Black Metal", "Rock & Alternative", "Western"),
item("doom-metal", "Doom Metal", "Rock & Alternative", "Western"),
item("folk-rock", "Folk Rock", "Rock & Alternative", "Western"),
item("blues-rock", "Blues Rock", "Rock & Alternative", "Western")
]
private static let electronicAndDance: [GenreStyleCatalogItem] = [
item("electronic", "Electronic", "Electronic & Dance", "Global"),
item("edm", "EDM", "Electronic & Dance", "Global", aliases: ["electronic dance music"]),
item("ambient", "Ambient", "Electronic & Dance", "Western"),
item("downtempo", "Downtempo", "Electronic & Dance", "Western"),
item("chillout", "Chillout", "Electronic & Dance", "Western"),
item("lofi", "Lo-Fi", "Electronic & Dance", "Western", aliases: ["lofi"]),
item("synthwave", "Synthwave", "Electronic & Dance", "Western"),
item("vaporwave", "Vaporwave", "Electronic & Dance", "Western"),
item("electro", "Electro", "Electronic & Dance", "Western"),
item("industrial", "Industrial", "Electronic & Dance", "Western"),
item("house", "House", "Electronic & Dance", "Western"),
item("deep-house", "Deep House", "Electronic & Dance", "Western"),
item("tech-house", "Tech House", "Electronic & Dance", "Western"),
item("progressive-house", "Progressive House", "Electronic & Dance", "Western"),
item("future-house", "Future House", "Electronic & Dance", "Western"),
item("techno", "Techno", "Electronic & Dance", "Western"),
item("minimal-techno", "Minimal Techno", "Electronic & Dance", "Western"),
item("trance", "Trance", "Electronic & Dance", "Western"),
item("progressive-trance", "Progressive Trance", "Electronic & Dance", "Western"),
item("psytrance", "Psytrance", "Electronic & Dance", "Western"),
item("drum-and-bass", "Drum and Bass", "Electronic & Dance", "UK", aliases: ["dnb"]),
item("jungle", "Jungle", "Electronic & Dance", "UK"),
item("dubstep", "Dubstep", "Electronic & Dance", "UK"),
item("uk-garage", "UK Garage", "Electronic & Dance", "UK"),
item("breakbeat", "Breakbeat", "Electronic & Dance", "Western"),
item("hardstyle", "Hardstyle", "Electronic & Dance", "Europe"),
item("moombahton", "Moombahton", "Electronic & Dance", "Latin America")
]
private static let urbanAndSoul: [GenreStyleCatalogItem] = [
item("hip-hop", "Hip Hop", "Hip Hop, R&B & Soul", "Global", aliases: ["hiphop"]),
item("rap", "Rap", "Hip Hop, R&B & Soul", "Global"),
item("boom-bap", "Boom Bap", "Hip Hop, R&B & Soul", "US"),
item("trap", "Trap", "Hip Hop, R&B & Soul", "US"),
item("drill", "Drill", "Hip Hop, R&B & Soul", "US/UK"),
item("conscious-hip-hop", "Conscious Hip Hop", "Hip Hop, R&B & Soul", "US"),
item("cloud-rap", "Cloud Rap", "Hip Hop, R&B & Soul", "US"),
item("lofi-hip-hop", "Lo-Fi Hip Hop", "Hip Hop, R&B & Soul", "Global"),
item("r-and-b", "R&B", "Hip Hop, R&B & Soul", "US", aliases: ["rhythm and blues"]),
item("contemporary-r-and-b", "Contemporary R&B", "Hip Hop, R&B & Soul", "US"),
item("soul", "Soul", "Hip Hop, R&B & Soul", "US"),
item("neo-soul", "Neo Soul", "Hip Hop, R&B & Soul", "US"),
item("funk", "Funk", "Hip Hop, R&B & Soul", "US"),
item("disco", "Disco", "Hip Hop, R&B & Soul", "US"),
item("nu-disco", "Nu Disco", "Hip Hop, R&B & Soul", "Western")
]
private static let jazzAndBlues: [GenreStyleCatalogItem] = [
item("jazz", "Jazz", "Jazz & Blues", "US"),
item("swing", "Swing", "Jazz & Blues", "US"),
item("bebop", "Bebop", "Jazz & Blues", "US"),
item("cool-jazz", "Cool Jazz", "Jazz & Blues", "US"),
item("hard-bop", "Hard Bop", "Jazz & Blues", "US"),
item("modal-jazz", "Modal Jazz", "Jazz & Blues", "US"),
item("jazz-fusion", "Jazz Fusion", "Jazz & Blues", "US"),
item("smooth-jazz", "Smooth Jazz", "Jazz & Blues", "US"),
item("vocal-jazz", "Vocal Jazz", "Jazz & Blues", "US"),
item("big-band", "Big Band", "Jazz & Blues", "US"),
item("blues", "Blues", "Jazz & Blues", "US"),
item("delta-blues", "Delta Blues", "Jazz & Blues", "US"),
item("chicago-blues", "Chicago Blues", "Jazz & Blues", "US"),
item("gospel", "Gospel", "Jazz & Blues", "US")
]
private static let countryFolkAndRoots: [GenreStyleCatalogItem] = [
item("country", "Country", "Country, Folk & Roots", "US"),
item("modern-country", "Modern Country", "Country, Folk & Roots", "US"),
item("americana", "Americana", "Country, Folk & Roots", "US"),
item("bluegrass", "Bluegrass", "Country, Folk & Roots", "US"),
item("folk", "Folk", "Country, Folk & Roots", "Global"),
item("indie-folk", "Indie Folk", "Country, Folk & Roots", "Western"),
item("celtic", "Celtic", "Country, Folk & Roots", "Ireland/Scotland"),
item("roots", "Roots", "Country, Folk & Roots", "Global"),
item("traditional", "Traditional", "Country, Folk & Roots", "Global"),
item("world", "World", "Country, Folk & Roots", "Global", aliases: ["world music"]),
item("skiffle", "Skiffle", "Country, Folk & Roots", "UK")
]
private static let latinAndCaribbean: [GenreStyleCatalogItem] = [
item("latin-pop", "Latin Pop", "Latin & Caribbean", "Latin America"),
item("reggaeton", "Reggaeton", "Latin & Caribbean", "Caribbean"),
item("salsa", "Salsa", "Latin & Caribbean", "Caribbean"),
item("bachata", "Bachata", "Latin & Caribbean", "Caribbean"),
item("merengue", "Merengue", "Latin & Caribbean", "Caribbean"),
item("cumbia", "Cumbia", "Latin & Caribbean", "Latin America"),
item("bossa-nova", "Bossa Nova", "Latin & Caribbean", "Brazil"),
item("samba", "Samba", "Latin & Caribbean", "Brazil"),
item("tango", "Tango", "Latin & Caribbean", "Argentina/Uruguay"),
item("bolero", "Bolero", "Latin & Caribbean", "Latin America"),
item("mariachi", "Mariachi", "Latin & Caribbean", "Mexico"),
item("norteno", "Norteno", "Latin & Caribbean", "Mexico"),
item("banda", "Banda", "Latin & Caribbean", "Mexico"),
item("corrido", "Corrido", "Latin & Caribbean", "Mexico"),
item("baile-funk", "Baile Funk", "Latin & Caribbean", "Brazil"),
item("dembow", "Dembow", "Latin & Caribbean", "Caribbean")
]
private static let middleEasternAndNorthAfrican: [GenreStyleCatalogItem] = [
item("arabic-pop", "Arabic Pop", "Middle East & North Africa", "Arab World", aliases: ["arab pop"], terms: ["بوب عربي"]),
item("arabic-traditional", "Arabic Traditional", "Middle East & North Africa", "Arab World", terms: ["موسيقى عربية", "طرب"]),
item("khaleeji", "Khaleeji", "Middle East & North Africa", "Gulf", aliases: ["gulf pop"], terms: ["خليجي"]),
item("levantine-pop", "Levantine Pop", "Middle East & North Africa", "Levant", terms: ["شامي"]),
item("egyptian-pop", "Egyptian Pop", "Middle East & North Africa", "Egypt", terms: ["مصري"]),
item("iraqi-pop", "Iraqi Pop", "Middle East & North Africa", "Iraq", terms: ["عراقي"]),
item("maghrebi-pop", "Maghrebi Pop", "Middle East & North Africa", "North Africa", terms: ["مغاربي"]),
item("rai", "Rai", "Middle East & North Africa", "Algeria/Morocco"),
item("chaabi", "Chaabi", "Middle East & North Africa", "North Africa"),
item("gnawa", "Gnawa", "Middle East & North Africa", "Morocco"),
item("andalusian", "Andalusian", "Middle East & North Africa", "North Africa"),
item("turkish-pop", "Turkish Pop", "Middle East & North Africa", "Turkey"),
item("turkish-folk", "Turkish Folk", "Middle East & North Africa", "Turkey"),
item("persian-pop", "Persian Pop", "Middle East & North Africa", "Iran"),
item("persian-traditional", "Persian Traditional", "Middle East & North Africa", "Iran"),
item("kurdish", "Kurdish", "Middle East & North Africa", "Kurdistan"),
item("armenian", "Armenian", "Middle East & North Africa", "Armenia")
]
private static let southAsian: [GenreStyleCatalogItem] = [
item("bollywood", "Bollywood", "South Asian", "India", aliases: ["filmi"]),
item("hindustani-classical", "Hindustani Classical", "South Asian", "India"),
item("carnatic", "Carnatic", "South Asian", "South India"),
item("bhangra", "Bhangra", "South Asian", "Punjab"),
item("punjabi-pop", "Punjabi Pop", "South Asian", "Punjab"),
item("ghazal", "Ghazal", "South Asian", "South Asia"),
item("qawwali", "Qawwali", "South Asian", "South Asia"),
item("bhajan", "Bhajan", "South Asian", "India"),
item("sufi", "Sufi", "South Asian", "South Asia"),
item("pakistani-pop", "Pakistani Pop", "South Asian", "Pakistan"),
item("bangladeshi-folk", "Bangladeshi Folk", "South Asian", "Bangladesh")
]
private static let eastAsian: [GenreStyleCatalogItem] = [
item("k-pop", "K-Pop", "East Asian", "Korea", aliases: ["kpop"]),
item("korean-traditional", "Korean Traditional", "East Asian", "Korea"),
item("j-pop", "J-Pop", "East Asian", "Japan", aliases: ["jpop"]),
item("japanese-city-pop", "Japanese City Pop", "East Asian", "Japan", aliases: ["city pop"]),
item("enka", "Enka", "East Asian", "Japan"),
item("anime-music", "Anime Music", "East Asian", "Japan"),
item("c-pop", "C-Pop", "East Asian", "China", aliases: ["cpop"]),
item("mandopop", "Mandopop", "East Asian", "China/Taiwan"),
item("cantopop", "Cantopop", "East Asian", "Hong Kong"),
item("chinese-traditional", "Chinese Traditional", "East Asian", "China"),
item("taiwanese-pop", "Taiwanese Pop", "East Asian", "Taiwan")
]
private static let southeastAsianAndOceania: [GenreStyleCatalogItem] = [
item("indonesian-pop", "Indonesian Pop", "Southeast Asia & Oceania", "Indonesia"),
item("dangdut", "Dangdut", "Southeast Asia & Oceania", "Indonesia"),
item("gamelan", "Gamelan", "Southeast Asia & Oceania", "Indonesia"),
item("malay-pop", "Malay Pop", "Southeast Asia & Oceania", "Malaysia"),
item("thai-pop", "Thai Pop", "Southeast Asia & Oceania", "Thailand"),
item("luk-thung", "Luk Thung", "Southeast Asia & Oceania", "Thailand"),
item("vietnamese-pop", "Vietnamese Pop", "Southeast Asia & Oceania", "Vietnam"),
item("filipino-pop", "Filipino Pop", "Southeast Asia & Oceania", "Philippines"),
item("pinoy-rock", "Pinoy Rock", "Southeast Asia & Oceania", "Philippines"),
item("maori", "Maori", "Southeast Asia & Oceania", "New Zealand"),
item("pacific", "Pacific", "Southeast Asia & Oceania", "Oceania"),
item("australian-indigenous", "Australian Indigenous", "Southeast Asia & Oceania", "Australia")
]
private static let african: [GenreStyleCatalogItem] = [
item("afrobeats", "Afrobeats", "African", "West Africa"),
item("afropop", "Afropop", "African", "Africa"),
item("afro-house", "Afro House", "African", "Africa"),
item("amapiano", "Amapiano", "African", "South Africa"),
item("highlife", "Highlife", "African", "West Africa"),
item("juju", "Juju", "African", "Nigeria"),
item("fuji", "Fuji", "African", "Nigeria"),
item("benga", "Benga", "African", "East Africa"),
item("soukous", "Soukous", "African", "Central Africa"),
item("kwaito", "Kwaito", "African", "South Africa"),
item("mbalax", "Mbalax", "African", "Senegal"),
item("taarab", "Taarab", "African", "East Africa"),
item("ethiopian", "Ethiopian", "African", "Ethiopia"),
item("sudanese", "Sudanese", "African", "Sudan"),
item("desert-blues", "Desert Blues", "African", "Sahara")
]
private static let reggaeAndCaribbean: [GenreStyleCatalogItem] = [
item("reggae", "Reggae", "Reggae & Caribbean", "Jamaica"),
item("roots-reggae", "Roots Reggae", "Reggae & Caribbean", "Jamaica"),
item("dub", "Dub", "Reggae & Caribbean", "Jamaica"),
item("dancehall", "Dancehall", "Reggae & Caribbean", "Jamaica"),
item("ska", "Ska", "Reggae & Caribbean", "Jamaica"),
item("rocksteady", "Rocksteady", "Reggae & Caribbean", "Jamaica"),
item("lovers-rock", "Lovers Rock", "Reggae & Caribbean", "Jamaica"),
item("calypso", "Calypso", "Reggae & Caribbean", "Caribbean"),
item("soca", "Soca", "Reggae & Caribbean", "Caribbean")
]
private static let classicalAndCinematic: [GenreStyleCatalogItem] = [
item("classical", "Classical", "Classical & Cinematic", "Western"),
item("baroque", "Baroque", "Classical & Cinematic", "Europe"),
item("classical-period", "Classical Period", "Classical & Cinematic", "Europe"),
item("romantic", "Romantic", "Classical & Cinematic", "Europe"),
item("contemporary-classical", "Contemporary Classical", "Classical & Cinematic", "Global"),
item("orchestral", "Orchestral", "Classical & Cinematic", "Western"),
item("chamber-music", "Chamber Music", "Classical & Cinematic", "Western"),
item("opera", "Opera", "Classical & Cinematic", "Europe"),
item("choral", "Choral", "Classical & Cinematic", "Global"),
item("cinematic", "Cinematic", "Classical & Cinematic", "Global"),
item("soundtrack", "Soundtrack", "Classical & Cinematic", "Global"),
item("musical-theater", "Musical Theater", "Classical & Cinematic", "Western"),
item("new-age", "New Age", "Classical & Cinematic", "Western"),
item("minimalism", "Minimalism", "Classical & Cinematic", "Western")
]
private static func item(
_ id: String,
_ name: String,
_ familyCategory: String,
_ regionOrigin: String? = nil,
aliases: [String] = [],
terms: [String] = []
) -> GenreStyleCatalogItem {
GenreStyleCatalogItem(
id: id,
name: name,
familyCategory: familyCategory,
regionOrigin: regionOrigin,
aliases: aliases,
searchTerms: terms
)
}
private static func searchableTerms(for item: GenreStyleCatalogItem) -> [String] {
searchTokens(for: [item.name, item.familyCategory, item.regionOrigin ?? ""] + item.aliases + item.searchTerms)
}
private static func searchTokens(for query: String) -> [String] {
searchTokens(for: [query])
}
private static func searchTokens(for values: [String]) -> [String] {
values
.joined(separator: " ")
.folding(
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
locale: Locale(identifier: "en_US_POSIX")
)
.split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "/" })
.map(String.init)
}
}