Add searchable instrument browser
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
import MusicAssistantCore
|
||||
import SwiftUI
|
||||
|
||||
struct InstrumentBrowserView: View {
|
||||
private let catalog: LocalInstrumentCatalog
|
||||
|
||||
@State private var searchText = ""
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
init(catalog: LocalInstrumentCatalog = LocalInstrumentCatalog()) {
|
||||
self.catalog = catalog
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List(catalog.instruments(matching: searchText), id: \.id) { instrument in
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(instrument.name)
|
||||
.font(.body.weight(.medium))
|
||||
|
||||
HStack(spacing: 6) {
|
||||
Text(instrument.familyCategory)
|
||||
|
||||
if let regionOrigin = instrument.regionOrigin {
|
||||
Text(regionOrigin)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.navigationTitle("Instrument Catalog")
|
||||
.searchable(text: $searchText, prompt: "Search instruments")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 460, minHeight: 520)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
InstrumentBrowserView()
|
||||
}
|
||||
@@ -9,6 +9,7 @@ struct ProjectInspectorView: View {
|
||||
@State private var newInstrumentName = ""
|
||||
@State private var newVocalistLabel = ""
|
||||
@State private var newLanguageName = ""
|
||||
@State private var isInstrumentBrowserPresented = false
|
||||
@State private var isSaving = false
|
||||
@State private var saveStatus: SaveStatus?
|
||||
|
||||
@@ -62,6 +63,12 @@ struct ProjectInspectorView: View {
|
||||
}
|
||||
|
||||
Section("Instruments") {
|
||||
Button {
|
||||
isInstrumentBrowserPresented = true
|
||||
} label: {
|
||||
Label("Browse catalog", systemImage: "magnifyingglass")
|
||||
}
|
||||
|
||||
ForEach($project.instruments, id: \.instrumentId) { $instrument in
|
||||
Toggle(isOn: $instrument.selected) {
|
||||
Text(instrument.variant?.isEmpty == false ? instrument.variant ?? instrument.instrumentId : instrument.instrumentId)
|
||||
@@ -138,6 +145,9 @@ struct ProjectInspectorView: View {
|
||||
}
|
||||
}
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.sheet(isPresented: $isInstrumentBrowserPresented) {
|
||||
InstrumentBrowserView()
|
||||
}
|
||||
}
|
||||
|
||||
private var durationSecondsBinding: Binding<String> {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
public struct LocalInstrumentCatalog: Sendable {
|
||||
public let items: [InstrumentCatalogItem]
|
||||
|
||||
@@ -5,6 +7,18 @@ public struct LocalInstrumentCatalog: Sendable {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public func instruments(matching query: String) -> [InstrumentCatalogItem] {
|
||||
let searchTokens = Self.searchTokens(for: query)
|
||||
|
||||
return items
|
||||
.filter { item in
|
||||
searchTokens.isEmpty || searchTokens.allSatisfy { token in
|
||||
Self.searchableTerms(for: item).contains { $0.contains(token) }
|
||||
}
|
||||
}
|
||||
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
|
||||
}
|
||||
|
||||
public static let worldwideItems: [InstrumentCatalogItem] =
|
||||
western + middleEastern + african + southAsian + eastAsian + southeastAsian + latinAmerican + folkAndModern
|
||||
|
||||
@@ -281,4 +295,23 @@ public struct LocalInstrumentCatalog: Sendable {
|
||||
searchTerms: terms
|
||||
)
|
||||
}
|
||||
|
||||
private static func searchableTerms(for item: InstrumentCatalogItem) -> [String] {
|
||||
searchTokens(for: [item.name] + 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 == "-" })
|
||||
.map(String.init)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user