From 04d4093a3cfa68e151f3d451abcc9f8f0556332b Mon Sep 17 00:00:00 2001 From: diyaa Date: Mon, 14 Sep 2026 06:20:07 +0200 Subject: [PATCH] Add instrument checkbox selection --- .../Presentation/InstrumentBrowserView.swift | 55 +++++++++++++++---- .../Presentation/ProjectInspectorView.swift | 2 +- .../Domain/SongProject.swift | 27 +++++++++ .../SongProjectModelTests.swift | 34 ++++++++++++ docs/TASKS.md | 2 +- 5 files changed, 106 insertions(+), 14 deletions(-) diff --git a/Sources/MusicAssistantApp/Presentation/InstrumentBrowserView.swift b/Sources/MusicAssistantApp/Presentation/InstrumentBrowserView.swift index 186908d..c23c4af 100644 --- a/Sources/MusicAssistantApp/Presentation/InstrumentBrowserView.swift +++ b/Sources/MusicAssistantApp/Presentation/InstrumentBrowserView.swift @@ -2,6 +2,7 @@ import MusicAssistantCore import SwiftUI struct InstrumentBrowserView: View { + @Binding private var project: SongProject private let catalog: LocalInstrumentCatalog @State private var searchText = "" @@ -9,7 +10,11 @@ struct InstrumentBrowserView: View { @State private var selectedRegionOrigin: String? @Environment(\.dismiss) private var dismiss - init(catalog: LocalInstrumentCatalog = LocalInstrumentCatalog()) { + init( + project: Binding, + catalog: LocalInstrumentCatalog = LocalInstrumentCatalog() + ) { + _project = project self.catalog = catalog } @@ -50,21 +55,24 @@ struct InstrumentBrowserView: View { ), id: \.id ) { instrument in - VStack(alignment: .leading, spacing: 4) { - Text(instrument.name) - .font(.body.weight(.medium)) + Toggle(isOn: selectionBinding(for: instrument)) { + VStack(alignment: .leading, spacing: 4) { + Text(instrument.name) + .font(.body.weight(.medium)) - HStack(spacing: 6) { - Text(instrument.familyCategory) + HStack(spacing: 6) { + Text(instrument.familyCategory) - if let regionOrigin = instrument.regionOrigin { - Text(regionOrigin) + if let regionOrigin = instrument.regionOrigin { + Text(regionOrigin) + } } + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) } - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) } + .toggleStyle(.checkbox) .padding(.vertical, 2) } } @@ -80,8 +88,31 @@ struct InstrumentBrowserView: View { } .frame(minWidth: 460, minHeight: 520) } + + private func selectionBinding(for instrument: InstrumentCatalogItem) -> Binding { + Binding { + project.isInstrumentSelected(id: instrument.id) + } set: { isSelected in + project.setInstrumentSelected( + id: instrument.id, + isSelected: isSelected, + variant: instrument.name + ) + } + } } #Preview { - InstrumentBrowserView() + InstrumentBrowserPreview() +} + +private struct InstrumentBrowserPreview: View { + @State private var project = SongProject( + title: "Instrument Browser Preview", + idea: "Browse the instrument catalog." + ) + + var body: some View { + InstrumentBrowserView(project: $project) + } } diff --git a/Sources/MusicAssistantApp/Presentation/ProjectInspectorView.swift b/Sources/MusicAssistantApp/Presentation/ProjectInspectorView.swift index 8ec9912..8e4374e 100644 --- a/Sources/MusicAssistantApp/Presentation/ProjectInspectorView.swift +++ b/Sources/MusicAssistantApp/Presentation/ProjectInspectorView.swift @@ -146,7 +146,7 @@ struct ProjectInspectorView: View { } .background(Color(nsColor: .windowBackgroundColor)) .sheet(isPresented: $isInstrumentBrowserPresented) { - InstrumentBrowserView() + InstrumentBrowserView(project: $project) } } diff --git a/Sources/MusicAssistantCore/Domain/SongProject.swift b/Sources/MusicAssistantCore/Domain/SongProject.swift index 2f6e3e5..6c3e579 100644 --- a/Sources/MusicAssistantCore/Domain/SongProject.swift +++ b/Sources/MusicAssistantCore/Domain/SongProject.swift @@ -312,6 +312,33 @@ public extension SongProject { instruments[index].selected = false } + mutating func setInstrumentSelected( + id instrumentId: String, + isSelected: Bool, + variant: String? = nil + ) { + guard isSelected else { + deselectInstrument(id: instrumentId) + return + } + + if let index = instruments.firstIndex(where: { $0.instrumentId == instrumentId }) { + instruments[index].selected = true + + if instruments[index].variant == nil { + instruments[index].variant = variant + } + } else { + instruments.append( + InstrumentTrack( + instrumentId: instrumentId, + selected: true, + variant: variant + ) + ) + } + } + mutating func configureInstrument( id instrumentId: String, variant: String?, diff --git a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift index c901991..031312e 100644 --- a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift +++ b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift @@ -281,6 +281,40 @@ final class SongProjectModelTests: XCTestCase { XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement]) } + func testCatalogSelectionCanBeToggledWithoutReplacingExistingArrangement() { + let oudPlacement = InstrumentPlacement(sectionId: "intro", direction: "Opening melody") + var project = SongProject( + id: "catalog-selection-project", + title: "Catalog Selection", + idea: "Select and deselect instruments", + instruments: [ + InstrumentTrack( + instrumentId: "oud", + selected: false, + variant: "Arabic oud", + playingStyle: "tremolo", + role: "main hook", + autoArrangementEnabled: false, + placements: [oudPlacement] + ) + ] + ) + + project.setInstrumentSelected(id: "oud", isSelected: true, variant: "Oud") + + XCTAssertTrue(project.isInstrumentSelected(id: "oud")) + XCTAssertEqual(project.instrumentTrack(for: "oud")?.variant, "Arabic oud") + XCTAssertEqual(project.instrumentTrack(for: "oud")?.role, "main hook") + XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [oudPlacement]) + + project.setInstrumentSelected(id: "oud", isSelected: false) + project.setInstrumentSelected(id: "violin", isSelected: true, variant: "Violin") + + XCTAssertFalse(project.isInstrumentSelected(id: "oud")) + XCTAssertTrue(project.isInstrumentSelected(id: "violin")) + XCTAssertEqual(project.instrumentTrack(for: "violin")?.variant, "Violin") + } + func testMultipleVocalistsCanBeAssignedToSections() { var project = SongProject( id: "project-6", diff --git a/docs/TASKS.md b/docs/TASKS.md index 58e3619..ea5f111 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -85,7 +85,7 @@ requirement is missing and blocks implementation, record it in - [x] Build searchable instrument browser. - [x] Add browsing/filtering by family/category. - [x] Add browsing/filtering by region/origin where useful. -- [ ] Add checkbox-based multi-select and deselect behavior. +- [x] Add checkbox-based multi-select and deselect behavior. - [ ] Persist selected instruments on the current Song Project. - [ ] Make selected instruments available to OpenAI/song-generation logic for arrangement, roles, entry/exit timing, relevant structure