Add instrument family filtering

This commit is contained in:
diyaa
2026-09-14 01:04:44 +02:00
parent ecbf07d1ea
commit 9ddac746e5
4 changed files with 63 additions and 17 deletions
@@ -5,6 +5,7 @@ struct InstrumentBrowserView: View {
private let catalog: LocalInstrumentCatalog private let catalog: LocalInstrumentCatalog
@State private var searchText = "" @State private var searchText = ""
@State private var selectedFamilyCategory: String?
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
init(catalog: LocalInstrumentCatalog = LocalInstrumentCatalog()) { init(catalog: LocalInstrumentCatalog = LocalInstrumentCatalog()) {
@@ -13,23 +14,44 @@ struct InstrumentBrowserView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
List(catalog.instruments(matching: searchText), id: \.id) { instrument in VStack(spacing: 0) {
VStack(alignment: .leading, spacing: 4) { Picker("Family", selection: $selectedFamilyCategory) {
Text(instrument.name) Text("All families").tag(String?.none)
.font(.body.weight(.medium))
HStack(spacing: 6) { ForEach(catalog.familyCategories, id: \.self) { familyCategory in
Text(instrument.familyCategory) Text(familyCategory).tag(Optional(familyCategory))
if let regionOrigin = instrument.regionOrigin {
Text(regionOrigin)
}
} }
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
} }
.padding(.vertical, 2) .pickerStyle(.menu)
.padding(.horizontal)
.padding(.vertical, 8)
Divider()
List(
catalog.instruments(
matching: searchText,
familyCategory: selectedFamilyCategory
),
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") .navigationTitle("Instrument Catalog")
.searchable(text: $searchText, prompt: "Search instruments") .searchable(text: $searchText, prompt: "Search instruments")
@@ -7,14 +7,25 @@ public struct LocalInstrumentCatalog: Sendable {
self.items = items self.items = items
} }
public func instruments(matching query: String) -> [InstrumentCatalogItem] { public var familyCategories: [String] {
Array(Set(items.map(\.familyCategory)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public func instruments(
matching query: String,
familyCategory: String? = nil
) -> [InstrumentCatalogItem] {
let searchTokens = Self.searchTokens(for: query) let searchTokens = Self.searchTokens(for: query)
let selectedFamilyCategory = familyCategory?.trimmingCharacters(in: .whitespacesAndNewlines)
return items return items
.filter { item in .filter { item in
searchTokens.isEmpty || searchTokens.allSatisfy { token in let matchesSearch = searchTokens.isEmpty || searchTokens.allSatisfy { token in
Self.searchableTerms(for: item).contains { $0.contains(token) } Self.searchableTerms(for: item).contains { $0.contains(token) }
} }
let matchesFamily = selectedFamilyCategory?.isEmpty != false || item.familyCategory == selectedFamilyCategory
return matchesSearch && matchesFamily
} }
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } .sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
} }
@@ -34,4 +34,17 @@ final class LocalInstrumentCatalogTests: XCTestCase {
XCTAssertEqual(catalog.instruments(matching: "nylon string").map(\.id), ["classical-guitar"]) XCTAssertEqual(catalog.instruments(matching: "nylon string").map(\.id), ["classical-guitar"])
XCTAssertEqual(catalog.instruments(matching: "عود").map(\.id), ["oud"]) XCTAssertEqual(catalog.instruments(matching: "عود").map(\.id), ["oud"])
} }
func testFamilyCategoriesAndFilteringComeFromCatalogMetadata() {
let catalog = LocalInstrumentCatalog()
let pluckedStrings = catalog.instruments(matching: "", familyCategory: "Plucked strings")
XCTAssertTrue(catalog.familyCategories.contains("Plucked strings"))
XCTAssertTrue(pluckedStrings.contains { $0.id == "oud" })
XCTAssertTrue(pluckedStrings.allSatisfy { $0.familyCategory == "Plucked strings" })
XCTAssertTrue(
catalog.instruments(matching: "guitar", familyCategory: "Plucked strings")
.allSatisfy { $0.familyCategory == "Plucked strings" }
)
}
} }
+1 -1
View File
@@ -83,7 +83,7 @@ requirement is missing and blocks implementation, record it in
instruments. instruments.
- [x] Keep catalog data out of SwiftUI views. - [x] Keep catalog data out of SwiftUI views.
- [x] Build searchable instrument browser. - [x] Build searchable instrument browser.
- [ ] Add browsing/filtering by family/category. - [x] Add browsing/filtering by family/category.
- [ ] Add browsing/filtering by region/origin where useful. - [ ] Add browsing/filtering by region/origin where useful.
- [ ] Add checkbox-based multi-select and deselect behavior. - [ ] Add checkbox-based multi-select and deselect behavior.
- [ ] Persist selected instruments on the current Song Project. - [ ] Persist selected instruments on the current Song Project.