diff --git a/.dev/project-map.md b/.dev/project-map.md new file mode 100644 index 0000000..6b491c9 --- /dev/null +++ b/.dev/project-map.md @@ -0,0 +1,23 @@ +# Project Map +_Generated: 2026-05-14 17:20 | Git-backed | Custom location requested by user_ + +## Directory Structure +src/app/modules/songs/ - song list, editing, display, key and transpose services. +src/app/modules/shows/ - show list and show-song workflow. +src/app/services/ - shared app services including filter state and search helpers. +src/app/widget-modules/ - shared UI components and pipes. + +## Key Files +src/app/modules/songs/song-list/filter/filter.component.* - sidebar filters for the song list. +src/app/modules/songs/song-list/song-list.component.* - song list view model and filter application. +src/app/modules/songs/services/key.helper.ts - canonical key lists, display mapping, and scale selection. +src/app/services/filter-store.service.ts - in-memory filter state for songs and shows. +src/app/services/filter.helper.ts - text search scoring and filtering helpers. + +## Critical Constraints +- Memory markdown files are kept in `.dev/` by user request, not project root. +- Song key data stores major keys uppercase and minor keys lowercase. +- German notation uses `H`; `B` represents B-flat in flat spellings. + +## Hot Files +src/app/modules/songs/song-list/filter/filter.component.ts, src/app/modules/songs/song-list/filter/filter.component.html, src/app/modules/songs/services/key.helper.ts diff --git a/.dev/session-log.md b/.dev/session-log.md new file mode 100644 index 0000000..0e32952 --- /dev/null +++ b/.dev/session-log.md @@ -0,0 +1,12 @@ +# Session Log + +## 2026-05-14 17:20 [saved] +Goal: Improve song key filter readability. +Decisions: +- Use root-key controls with refinement because grouped enharmonic dropdowns were still awkward to change. +- Keep memory markdown files in `.dev/` because the user requested that location. +Rejected: +- Keep all sharp and flat variants as separate primary options. +- Use a grouped enharmonic dropdown that forces users through reset-like interaction. +Open: +- User will review the visual result. diff --git a/.gitignore b/.gitignore index 302d43f..9723f40 100644 --- a/.gitignore +++ b/.gitignore @@ -315,3 +315,6 @@ testem.log # System Files Thumbs.db firebase.ts + +# AI assistant artifacts +context-snapshot.json diff --git a/src/app/modules/songs/services/key-filter.helper.ts b/src/app/modules/songs/services/key-filter.helper.ts new file mode 100644 index 0000000..fc5b5ec --- /dev/null +++ b/src/app/modules/songs/services/key-filter.helper.ts @@ -0,0 +1,130 @@ +export interface KeyFilterSelection { + root: string; + mode: string; + accidental: string; + includeRelativeMinor: boolean; +} + +export const KEY_FILTER_ROOTS = [ + {value: '', label: 'Alle'}, + {value: 'C', label: 'C'}, + {value: 'D', label: 'D'}, + {value: 'E', label: 'E'}, + {value: 'F', label: 'F'}, + {value: 'G', label: 'G'}, + {value: 'A', label: 'A'}, + {value: 'H', label: 'H'}, +]; + +export const KEY_FILTER_MODES = [ + {value: 'major', label: 'Dur'}, + {value: 'minor', label: 'Moll'}, +]; + +export const KEY_FILTER_ACCIDENTALS = [ + {value: '', label: 'keins'}, + {value: 'sharp', label: '#'}, + {value: 'flat', label: 'b'}, +]; + +const rootSemitones: {[root: string]: number} = { + C: 0, + D: 2, + E: 4, + F: 5, + G: 7, + A: 9, + H: 11, +}; + +const sharpKeysBySemitone = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'H']; +const flatKeysBySemitone = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']; + +const relativeMinorByMajorKey: {[key: string]: string} = { + C: 'a', + 'C#': 'a#', + Db: 'b', + D: 'h', + 'D#': 'c', + Eb: 'c', + E: 'c#', + F: 'd', + 'F#': 'd#', + Gb: 'eb', + G: 'e', + 'G#': 'f', + Ab: 'f', + A: 'f#', + 'A#': 'g', + B: 'g', + H: 'g#', +}; + +const relativeMajorByMinorKey: {[key: string]: string} = Object.fromEntries(Object.entries(relativeMinorByMajorKey).map(([majorKey, minorKey]) => [minorKey, majorKey])); + +export const matchesKeyFilter = (songKey: string, filter: KeyFilterSelection): boolean => { + if (!filter.root) { + return true; + } + + return getKeysForFilter(filter).includes(songKey); +}; + +export const getKeysForFilter = (filter: KeyFilterSelection): string[] => { + const keys = new Set(); + const majorKey = getKeyForRoot(filter.root, 'major', filter.accidental); + const minorKey = getKeyForRoot(filter.root, 'minor', filter.accidental); + + if (filter.mode !== 'minor' && majorKey) { + keys.add(majorKey); + } + + if (filter.mode !== 'major' && minorKey) { + keys.add(minorKey); + } + + if (filter.includeRelativeMinor && filter.mode !== 'minor' && majorKey && relativeMinorByMajorKey[majorKey]) { + keys.add(relativeMinorByMajorKey[majorKey]); + } + + if (filter.includeRelativeMinor && filter.mode !== 'major' && minorKey && relativeMajorByMinorKey[minorKey]) { + keys.add(relativeMajorByMinorKey[minorKey]); + } + + return [...keys]; +}; + +export const supportsKeyAccidental = (root: string, accidental: string): boolean => { + return !root || rootSemitones[root] !== undefined || !accidental; +}; + +export const hasAvailableKeyForRoot = (root: string, availableKeys: string[]): boolean => { + if (!root) { + return true; + } + + const available = new Set(availableKeys.filter(key => !!key)); + return KEY_FILTER_MODES.some(mode => + KEY_FILTER_ACCIDENTALS.some(accidental => + getKeysForFilter({ + root, + mode: mode.value, + accidental: accidental.value, + includeRelativeMinor: false, + }).some(key => available.has(key)), + ), + ); +}; + +const getKeyForRoot = (root: string, mode: 'major' | 'minor', accidental: string): string => { + const semitone = rootSemitones[root]; + if (semitone === undefined) { + return ''; + } + + const offset = accidental === 'sharp' ? 1 : accidental === 'flat' ? -1 : 0; + const normalizedSemitone = (semitone + offset + 12) % 12; + const key = accidental === 'flat' ? flatKeysBySemitone[normalizedSemitone] : sharpKeysBySemitone[normalizedSemitone]; + + return mode === 'minor' ? key.toLowerCase() : key; +}; diff --git a/src/app/modules/songs/services/key.helper.spec.ts b/src/app/modules/songs/services/key.helper.spec.ts index c0ff541..93f2e20 100644 --- a/src/app/modules/songs/services/key.helper.spec.ts +++ b/src/app/modules/songs/services/key.helper.spec.ts @@ -1,4 +1,4 @@ -import {getScale, scaleMapping} from './key.helper'; +import {getScale, hasAvailableKeyForRoot, matchesKeyFilter, scaleMapping, supportsKeyAccidental} from './key.helper'; describe('key.helper', () => { it('should render Gb correctly', () => { @@ -12,4 +12,58 @@ describe('key.helper', () => { it('should keep flat-based spelling for Db', () => { void expect(getScale('Db')).toEqual(['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'B', 'H']); }); + + it('should match songs by root, mode and accidental', () => { + void expect(matchesKeyFilter('Db', {root: 'D', mode: 'major', accidental: 'flat', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('d#', {root: 'D', mode: 'minor', accidental: 'sharp', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('D', {root: 'D', mode: 'minor', accidental: '', includeRelativeMinor: false})).toBe(false); + }); + + it('should include both major and minor when no mode is selected', () => { + void expect(matchesKeyFilter('G', {root: 'G', mode: '', accidental: '', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('g', {root: 'G', mode: '', accidental: '', includeRelativeMinor: false})).toBe(true); + }); + + it('should include the relative minor for major keys when requested', () => { + void expect(matchesKeyFilter('a', {root: 'C', mode: 'major', accidental: '', includeRelativeMinor: true})).toBe(true); + void expect(matchesKeyFilter('h', {root: 'D', mode: 'major', accidental: '', includeRelativeMinor: true})).toBe(true); + void expect(matchesKeyFilter('e', {root: 'G', mode: 'major', accidental: '', includeRelativeMinor: true})).toBe(true); + }); + + it('should include the relative minor when all modes are selected', () => { + void expect(matchesKeyFilter('a', {root: 'C', mode: '', accidental: '', includeRelativeMinor: true})).toBe(true); + }); + + it('should include the relative major for minor keys when requested', () => { + void expect(matchesKeyFilter('C', {root: 'A', mode: 'minor', accidental: '', includeRelativeMinor: true})).toBe(true); + void expect(matchesKeyFilter('G', {root: 'E', mode: 'minor', accidental: '', includeRelativeMinor: true})).toBe(true); + }); + + it('should normalize enharmonic edge spellings to stored keys', () => { + void expect(matchesKeyFilter('F', {root: 'E', mode: 'major', accidental: 'sharp', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('e', {root: 'F', mode: 'minor', accidental: 'flat', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('H', {root: 'C', mode: 'major', accidental: 'flat', includeRelativeMinor: false})).toBe(true); + void expect(matchesKeyFilter('C', {root: 'H', mode: 'major', accidental: 'sharp', includeRelativeMinor: false})).toBe(true); + }); + + it('should allow every accidental for every root', () => { + void expect(supportsKeyAccidental('E', 'sharp')).toBe(true); + void expect(supportsKeyAccidental('F', 'flat')).toBe(true); + }); + + it('should detect whether a root has any available stored key', () => { + void expect(hasAvailableKeyForRoot('C', ['a'])).toBe(true); + void expect(hasAvailableKeyForRoot('E', ['F'])).toBe(true); + void expect(hasAvailableKeyForRoot('H', ['C'])).toBe(true); + void expect(hasAvailableKeyForRoot('D', ['F', 'a'])).toBe(false); + }); + + it('should not enable a root only because its relative counterpart exists', () => { + void expect(hasAvailableKeyForRoot('A', ['C'])).toBe(false); + void expect(hasAvailableKeyForRoot('C', ['a'])).toBe(false); + }); + + it('should not apply a key filter before a root is selected', () => { + void expect(matchesKeyFilter('Db', {root: '', mode: 'major', accidental: 'flat', includeRelativeMinor: true})).toBe(true); + }); }); diff --git a/src/app/modules/songs/services/key.helper.ts b/src/app/modules/songs/services/key.helper.ts index 905ce40..94e4619 100644 --- a/src/app/modules/songs/services/key.helper.ts +++ b/src/app/modules/songs/services/key.helper.ts @@ -154,3 +154,6 @@ export const scaleMapping: {[key: string]: string} = { export const getScale = (key: string): string[] => scaleAssignment[key]; export const getScaleType = (key: string): string[][] => scaleTypeAssignment[key]; + +export {KEY_FILTER_ACCIDENTALS, KEY_FILTER_MODES, KEY_FILTER_ROOTS, getKeysForFilter, hasAvailableKeyForRoot, matchesKeyFilter, supportsKeyAccidental} from './key-filter.helper'; +export type {KeyFilterSelection} from './key-filter.helper'; diff --git a/src/app/modules/songs/song-list/filter/filter-values.ts b/src/app/modules/songs/song-list/filter/filter-values.ts index 05745bf..132db57 100644 --- a/src/app/modules/songs/song-list/filter/filter-values.ts +++ b/src/app/modules/songs/song-list/filter/filter-values.ts @@ -1,7 +1,10 @@ export interface FilterValues { q: string; type: string; - key: string; + keyRoot: string; + keyMode: string; + keyAccidental: string; + includeRelativeMinor: boolean; legalType: string; flag: string; } diff --git a/src/app/modules/songs/song-list/filter/filter.component.html b/src/app/modules/songs/song-list/filter/filter.component.html index 9fa66a2..34f4f60 100644 --- a/src/app/modules/songs/song-list/filter/filter.component.html +++ b/src/app/modules/songs/song-list/filter/filter.component.html @@ -15,15 +15,38 @@ - - Tonart - - - kein Filter - - @for (key of keys; track key) { - {{ key | key }} +
+
Grundtonart
+ + @for (root of keyRoots; track root.value) { + {{ root.label }} } - - + + + @if (filterFormGroup.controls.keyRoot.value) { +
+
+
Vorzeichen
+ + @for (accidental of keyAccidentals; track accidental.value) { + {{ accidental.label }} + } + +
+ +
+
Dur/Moll
+ + @for (mode of keyModes; track mode.value) { + {{ mode.label }} + } + +
+ + Parallele Tonart einschließen +
+ } +
Rechtlicher Status diff --git a/src/app/modules/songs/song-list/filter/filter.component.less b/src/app/modules/songs/song-list/filter/filter.component.less index 7e9b9b9..14b5df3 100644 --- a/src/app/modules/songs/song-list/filter/filter.component.less +++ b/src/app/modules/songs/song-list/filter/filter.component.less @@ -13,3 +13,85 @@ div[formGroup] { :host ::ng-deep .mat-mdc-form-field { width: 100%; } + +.key-filter { + display: flex; + flex-direction: column; + gap: 10px; + padding: 2px 0 14px; +} + +.key-filter-label { + color: var(--mat-sys-on-surface-variant); + font-size: 12px; + font-weight: 500; + line-height: 18px; +} + +.key-filter-details { + display: flex; + flex-direction: column; + gap: 12px; +} + +mat-button-toggle-group { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + width: 100%; +} + +.key-mode-toggle-group { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.key-accidental-toggle-group { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +mat-button-toggle { + min-width: 0; +} + +.key-filter mat-button-toggle { + --mat-standard-button-toggle-selected-state-background-color: transparent; + --mat-standard-button-toggle-selected-state-text-color: var(--mat-sys-primary); +} + +.key-root-toggle-group { + grid-template-columns: repeat(7, minmax(0, 1fr)); +} + +.key-root-all { + grid-column: 1 / -1; +} + +:host ::ng-deep .mat-button-toggle-label-content { + line-height: 36px; + padding: 0 8px; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checked .mat-button-toggle-label-content { + color: var(--mat-sys-primary); + font-weight: 700; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checked { + background-color: transparent; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checked .mat-button-toggle-button { + background-color: transparent; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checkbox-wrapper { + display: none; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checked .mat-button-toggle-button { + padding-left: 0; + padding-right: 0; +} + +:host ::ng-deep .key-filter .mat-button-toggle-checked .mat-pseudo-checkbox { + display: none; +} diff --git a/src/app/modules/songs/song-list/filter/filter.component.ts b/src/app/modules/songs/song-list/filter/filter.component.ts index 0a22a6d..ca2f1bd 100644 --- a/src/app/modules/songs/song-list/filter/filter.component.ts +++ b/src/app/modules/songs/song-list/filter/filter.component.ts @@ -6,7 +6,7 @@ import {faFilterCircleXmark} from '@fortawesome/free-solid-svg-icons'; import {SongService} from '../../services/song.service'; import {FilterValues} from './filter-values'; import {Song} from '../../services/song'; -import {KEYS} from '../../services/key.helper'; +import {hasAvailableKeyForRoot, KEY_FILTER_ACCIDENTALS, KEY_FILTER_MODES, KEY_FILTER_ROOTS, supportsKeyAccidental} from '../../services/key.helper'; import {FilterStoreService} from '../../../../services/filter-store.service'; import {MatFormField, MatLabel} from '@angular/material/form-field'; import {MatInput} from '@angular/material/input'; @@ -14,15 +14,16 @@ import {MatSelect} from '@angular/material/select'; import {MatOption} from '@angular/material/core'; import {LegalTypePipe} from '../../../../widget-modules/pipes/legal-type-translator/legal-type.pipe'; -import {KeyPipe} from '../../../../widget-modules/pipes/key-translator/key.pipe'; import {SongTypePipe} from '../../../../widget-modules/pipes/song-type-translater/song-type.pipe'; import {ButtonComponent} from '../../../../widget-modules/components/button/button.component'; +import {MatButtonToggle, MatButtonToggleGroup} from '@angular/material/button-toggle'; +import {MatCheckbox} from '@angular/material/checkbox'; @Component({ selector: 'app-filter', templateUrl: './filter.component.html', styleUrls: ['./filter.component.less'], - imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, LegalTypePipe, KeyPipe, SongTypePipe, ButtonComponent], + imports: [ReactiveFormsModule, MatFormField, MatLabel, MatInput, MatSelect, MatOption, LegalTypePipe, SongTypePipe, ButtonComponent, MatButtonToggleGroup, MatButtonToggle, MatCheckbox], }) export class FilterComponent { private filterStore = inject(FilterStoreService); @@ -32,14 +33,19 @@ export class FilterComponent { public filterFormGroup: FormGroup<{ q: FormControl; type: FormControl; - key: FormControl; + keyRoot: FormControl; + keyMode: FormControl; + keyAccidental: FormControl; + includeRelativeMinor: FormControl; legalType: FormControl; flag: FormControl; }>; @Input() public songs: Song[] = []; public types = SongService.TYPES; public legalType = SongService.LEGAL_TYPE; - public keys = KEYS; + public keyRoots = KEY_FILTER_ROOTS; + public keyModes = KEY_FILTER_MODES; + public keyAccidentals = KEY_FILTER_ACCIDENTALS; public constructor() { const fb = inject(FormBuilder); @@ -47,7 +53,10 @@ export class FilterComponent { this.filterFormGroup = fb.nonNullable.group({ q: '', type: '', - key: '', + keyRoot: '', + keyMode: 'major', + keyAccidental: '', + includeRelativeMinor: false as boolean, legalType: '', flag: '', }); @@ -59,7 +68,12 @@ export class FilterComponent { this.filterFormGroup.controls.q.valueChanges .pipe(debounceTime(100), distinctUntilChanged(), takeUntilDestroyed(this.destroyRef)) .subscribe(value => this.filterValueChanged('q', value)); - this.filterFormGroup.controls.key.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('key', value)); + this.filterFormGroup.controls.keyRoot.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.updateKeyRoot(value)); + this.filterFormGroup.controls.keyMode.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.updateKeyMode(value)); + this.filterFormGroup.controls.keyAccidental.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('keyAccidental', value)); + this.filterFormGroup.controls.includeRelativeMinor.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => this.filterValueChanged('includeRelativeMinor', value)); this.filterFormGroup.controls.type.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('type', value)); this.filterFormGroup.controls.legalType.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('legalType', value)); this.filterFormGroup.controls.flag.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(value => this.filterValueChanged('flag', value)); @@ -78,14 +92,31 @@ export class FilterComponent { public get filterActive(): boolean { const filter = this.filterFormGroup.getRawValue(); - return !!(filter.q || filter.type || filter.key || filter.legalType || filter.flag); + return !!(filter.q || filter.type || filter.keyRoot || filter.legalType || filter.flag); } public resetFilter(): void { this.filterStore.resetSongFilter(); } - private filterValueChanged(key: keyof FilterValues, value: string): void { + public isAccidentalDisabled(accidental: string): boolean { + const root = this.filterFormGroup.controls.keyRoot.value; + return !!root && !supportsKeyAccidental(root, accidental); + } + + public isKeyRootDisabled(root: string): boolean { + return !hasAvailableKeyForRoot(root, this.songs.map(song => song.key)); + } + + private updateKeyRoot(value: string): void { + this.filterValueChanged('keyRoot', value); + } + + private updateKeyMode(value: string): void { + this.filterValueChanged('keyMode', value); + } + + private filterValueChanged(key: keyof FilterValues, value: string | boolean): void { this.filterStore.updateSongFilter({[key]: value} as Partial); } } diff --git a/src/app/modules/songs/song-list/song-list.component.html b/src/app/modules/songs/song-list/song-list.component.html index 052b87a..d05738b 100644 --- a/src/app/modules/songs/song-list/song-list.component.html +++ b/src/app/modules/songs/song-list/song-list.component.html @@ -1,7 +1,7 @@ @if (viewModel$ | async; as viewModel) {
diff --git a/src/app/modules/songs/song-list/song-list.component.spec.ts b/src/app/modules/songs/song-list/song-list.component.spec.ts index 0332ab2..55528dc 100644 --- a/src/app/modules/songs/song-list/song-list.component.spec.ts +++ b/src/app/modules/songs/song-list/song-list.component.spec.ts @@ -1,16 +1,22 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {SongListComponent} from './song-list.component'; -import {of} from 'rxjs'; +import {firstValueFrom, of} from 'rxjs'; import {ActivatedRoute} from '@angular/router'; import {TextRenderingService} from '../services/text-rendering.service'; import {UserService} from '../../../services/user/user.service'; import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {FilterStoreService} from '../../../services/filter-store.service'; +import {Song} from '../services/song'; describe('SongListComponent', () => { let component: SongListComponent; let fixture: ComponentFixture; + let filterStore: FilterStoreService; - const songs = [{id: 'song-1', title: 'title1', number: 1, text: '', flags: ''}]; + const songs = [ + {id: 'song-1', title: 'title1', number: 1, text: '', flags: '', key: 'C'}, + {id: 'song-2', title: 'title2', number: 2, text: '', flags: '', key: 'D'}, + ] as Song[]; beforeEach(async () => { await TestBed.configureTestingModule({ @@ -27,10 +33,20 @@ describe('SongListComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(SongListComponent); component = fixture.componentInstance; + filterStore = TestBed.inject(FilterStoreService); fixture.detectChanges(); }); it('should create', () => { void expect(component).toBeTruthy(); }); + + it('should expose unfiltered songs for filter option availability', async () => { + filterStore.updateSongFilter({keyRoot: 'C'}); + + const viewModel = await firstValueFrom(component.viewModel$); + + void expect(viewModel.songs.map(song => song.key)).toEqual(['C']); + void expect(viewModel.availableSongs.map(song => song.key)).toEqual(['C', 'D']); + }); }); diff --git a/src/app/modules/songs/song-list/song-list.component.ts b/src/app/modules/songs/song-list/song-list.component.ts index cff2c03..87264c7 100644 --- a/src/app/modules/songs/song-list/song-list.component.ts +++ b/src/app/modules/songs/song-list/song-list.component.ts @@ -16,6 +16,7 @@ import {RoleDirective} from '../../../services/user/role.directive'; import {FaIconComponent} from '@fortawesome/angular-fontawesome'; import {PageFrameComponent} from '../../../widget-modules/components/sidebar/page-frame.component'; import {ButtonComponent} from '../../../widget-modules/components/button/button.component'; +import {matchesKeyFilter} from '../services/key.helper'; interface SongListItem extends Song { hasChordValidationIssues: boolean; @@ -23,6 +24,7 @@ interface SongListItem extends Song { interface SongListViewModel { songs: SongListItem[]; + availableSongs: Song[]; filterActive: boolean; } @@ -56,6 +58,7 @@ export class SongListComponent { return { songs: filteredSongs, + availableSongs: songs, filterActive: this.isFilterActive(filter), }; }), @@ -69,7 +72,14 @@ export class SongListComponent { private filter(song: Song, filter: FilterValues): boolean { let baseFilter = !filter.type || filter.type === song.type; - baseFilter = baseFilter && (!filter.key || filter.key === song.key); + baseFilter = + baseFilter && + matchesKeyFilter(song.key, { + root: filter.keyRoot, + mode: filter.keyMode, + accidental: filter.keyAccidental, + includeRelativeMinor: filter.includeRelativeMinor, + }); baseFilter = baseFilter && (!filter.legalType || filter.legalType === song.legalType); baseFilter = baseFilter && (!filter.flag || this.checkFlag(filter.flag, song.flags)); @@ -77,7 +87,7 @@ export class SongListComponent { } private isFilterActive(filter: FilterValues): boolean { - return !!(filter.q || filter.type || filter.key || filter.legalType || filter.flag); + return !!(filter.q || filter.type || filter.keyRoot || filter.legalType || filter.flag); } private checkFlag(flag: string, flags: string) { diff --git a/src/app/services/filter-store.service.ts b/src/app/services/filter-store.service.ts index 1895f48..452e683 100644 --- a/src/app/services/filter-store.service.ts +++ b/src/app/services/filter-store.service.ts @@ -6,7 +6,10 @@ import {FilterValues as ShowFilterValues} from '../modules/shows/list/filter/fil const DEFAULT_SONG_FILTER: SongFilterValues = { q: '', type: '', - key: '', + keyRoot: '', + keyMode: 'major', + keyAccidental: '', + includeRelativeMinor: false, legalType: '', flag: '', };