Skip to content

Commit

Permalink
feat(ChordDictionary): added basic module
Browse files Browse the repository at this point in the history
  • Loading branch information
ArTiSTiX committed Dec 19, 2023
1 parent 437f159 commit 8b04d90
Show file tree
Hide file tree
Showing 18 changed files with 773 additions and 9 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.base {
display: flex;
align-items: center;
justify-content: center;
height: 6em;
}

Expand Down Expand Up @@ -35,7 +36,7 @@
}

.interval--active {
background-color: $color-success-normal;
background-color: $color-primary-normal;
}

.interval--played {
Expand Down
29 changes: 22 additions & 7 deletions src/renderer/components/ChordName/ChordName.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ enum ALIAS_NOTATION {
symbol = 2,
}

function getChordSymbol(chord: ChordNameProps['chord'], notation: ChordNameProps['notation']) {
if (!chord) {
return '';
}

if (typeof notation === 'string') {
if (chord.aliases[ALIAS_NOTATION[notation]] !== undefined) {
return chord.tonic + chord.aliases[ALIAS_NOTATION[notation]];
}
} else if (typeof notation === 'number' && chord.aliases[notation] !== undefined) {
return chord.tonic + chord.aliases[notation];
}

if (chord.aliases[ALIAS_NOTATION.short] !== undefined) {
return chord.tonic + chord.aliases[ALIAS_NOTATION.short];
}
return chord.symbol;
}

export const ChordName: React.FC<ChordNameProps> = ({
className,
chord,
Expand All @@ -26,13 +45,9 @@ export const ChordName: React.FC<ChordNameProps> = ({
}) => {
if (!chord) return null;

const symbol =
// eslint-disable-next-line no-nested-ternary
chord.aliases[ALIAS_NOTATION[notation]] !== undefined
? chord.tonic + chord.aliases[ALIAS_NOTATION[notation]]
: chord.aliases[ALIAS_NOTATION.short] !== undefined
? chord.tonic + chord.aliases[ALIAS_NOTATION.short]
: chord.symbol;
const symbol = getChordSymbol(chord, notation);

if (!symbol) return null;

const [tonic, type] = tokenizeChord(symbol);
const tokens = tokenizeChordType(type);
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/components/ChordName/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Chord } from '@tonaljs/chord';
export type ChordNameProps = {
className?: string;
chord?: Chord | null;
notation?: 'long' | 'short' | 'symbol';
notation?: 'long' | 'short' | 'symbol' | number;
hideRoot?: boolean;
highlightAlterations?: boolean;
latinSharpsFlats?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/components/Icon/icons/dictionary.react.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/renderer/components/Icon/icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const ICON_NAMES = [
'clock',
'controller',
'cross',
'dictionary',
'exclamation',
'github',
'heart',
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/helpers/chords.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,8 @@ export function overrideDictionary() {
);
}

export function getChordTypes() {
return ChordType.all();
}

overrideDictionary();
27 changes: 27 additions & 0 deletions src/renderer/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import Licenses from './views/Settings/Licenses';
import packageJSON from '../../package.json';
import icon from '../../assets/icon.svg';
import ChordDisplayNamespaceSettings from './views/Settings/ChordDisplaySettings/ChordDisplayModuleSettings';
import ChordDictionary from './views/ChordDictionary';
import ChordDictionaryDetail from './views/ChordDictionary/Detail';

const router = createHashRouter(
createRoutesFromElements(
Expand Down Expand Up @@ -108,6 +110,31 @@ const router = createHashRouter(
}
/>
</Route>
<Route
path="dictionary"
handle={{
title: 'Chord Dictionary',
icon: <Icon name="dictionary" />,
hasSettings: false,
}}
element={
<MidiMessageManagerProvider namespace="chord-dictionary" source="internal">
<ChordDictionary />
</MidiMessageManagerProvider>
}
>
<Route index element={<ChordDictionaryDetail />} />
<Route
path=":chordName"
handle={{
title: (params: Params) => (
<span style={{ textTransform: 'none' }}>{params.chordName}</span>
),
icon: <Icon name="music" />,
}}
element={<ChordDictionaryDetail />}
/>
</Route>
<Route
path="settings"
handle={{ title: 'Settings', icon: <Icon name="settings" /> }}
Expand Down
26 changes: 26 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionary.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import 'tokens';

.container {
position: relative;
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: flex-end;
align-items: center;
overflow: hidden;
}

.pitchbar {
--Sidebar_minSize: 48px;
--Button_textTransform: none;
}

.content {
height: 100%;
}

.chordbar {
--Sidebar_minSize: 200px;
--Button_textTransform: none;
}
87 changes: 87 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import classnames from 'classnames/bind';
import { Chord, Note } from 'tonal';
import { SidebarContainer } from '@la-jarre-a-son/ui';

import { useSettings } from 'renderer/contexts/Settings';
import { NOTE_NAMES, getKeySignature, getNoteInKeySignature } from 'renderer/helpers';

import ChordDictionaryChromaMenu from './ChordDictionaryChromaMenu';
import ChordDictionaryChordMenu from './ChordDictionaryChordMenu';

import styles from './ChordDictionary.module.scss';

const cx = classnames.bind(styles);

const ChordDictionary: React.FC = () => {
const { settings } = useSettings();
const { chordName } = useParams();

const navigate = useNavigate();
const { key, accidentals } = settings.notation;
const keySignature = useMemo(
() => getKeySignature(key, accidentals === 'sharp'),
[key, accidentals]
);

const [chroma, setChroma] = useState<number | null>(null);
const [chordType, setChordType] = useState<string | null>(null);

useEffect(() => {
if (chroma !== null && chordType !== null) {
const name = encodeURIComponent(
`${getNoteInKeySignature(NOTE_NAMES[chroma], keySignature.notes)}${chordType}`
);

navigate(`./${name}`);
}
return () => {};
}, [navigate, chroma, chordType, keySignature]);

useEffect(() => {
const chord = chordName ? Chord.get(chordName) : null;

if (chord && chord.tonic) {
setChroma(Note.chroma(chord.tonic) ?? null);
}

if (chord && chord.aliases[0]) {
setChordType(chord.aliases[0]);
}
}, [chordName]);

return (
<SidebarContainer
className={cx('container')}
sidebar={
<ChordDictionaryChromaMenu
keySignature={keySignature}
selected={chroma}
onSelect={setChroma}
/>
}
sidebarProps={{ className: cx('pitchbar') }}
contentProps={{ className: cx('content') }}
size="xs"
open
inset
>
<SidebarContainer
className={cx('container')}
sidebar={<ChordDictionaryChordMenu selected={chordType} onSelect={setChordType} />}
sidebarProps={{ className: cx('chordbar') }}
contentProps={{ className: cx('content') }}
size="sm"
open
inset
>
<Outlet />
</SidebarContainer>
</SidebarContainer>
);
};

ChordDictionary.defaultProps = {};

export default ChordDictionary;
61 changes: 61 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionaryChordMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useEffect, useMemo, useRef } from 'react';
import classnames from 'classnames/bind';
import { Tab, TabList } from '@la-jarre-a-son/ui';

import { getChordTypes } from 'renderer/helpers';

import styles from './ChordDictionary.module.scss';

const cx = classnames.bind(styles);

type Props = {
selected: string | null;
onSelect: (note: string) => void;
};

const ChordDictionaryChordMenu: React.FC<Props> = ({ selected, onSelect }) => {
const ref = useRef<HTMLElement>();
const chordTypes = useMemo(() => getChordTypes(), []);

useEffect(() => {
if (ref.current) {
const currentEl: HTMLElement | null = ref.current.querySelector('[aria-selected=true]');

if (currentEl) {
if (ref.current.scrollTop > currentEl.offsetTop) {
currentEl.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
}
if (
ref.current.scrollTop + ref.current.offsetHeight <
currentEl.offsetTop + currentEl.offsetHeight
) {
currentEl.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
}
}
}
}, [selected]);

return (
<TabList
ref={ref}
className={cx('chordnav')}
aria-label="Chord Types Navigation"
direction="vertical"
variant="ghost"
block
>
{chordTypes.map((chordType) => (
<Tab
key={chordType.aliases[0]}
className={cx('tab')}
selected={selected === chordType.aliases[0]}
onClick={() => onSelect(chordType.aliases[0])}
>
<span className={cx('label')}>{chordType.aliases[0] || 'maj'}</span>
</Tab>
))}
</TabList>
);
};

export default ChordDictionaryChordMenu;
68 changes: 68 additions & 0 deletions src/renderer/views/ChordDictionary/ChordDictionaryChromaMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { useEffect, useRef } from 'react';
import classnames from 'classnames/bind';
import { Tab, TabList } from '@la-jarre-a-son/ui';

import {
KeySignatureConfig,
NOTE_NAMES,
formatSharpsFlats,
getNoteInKeySignature,
} from 'renderer/helpers';

import styles from './ChordDictionary.module.scss';

const cx = classnames.bind(styles);

type Props = {
keySignature: KeySignatureConfig;
selected: number | null;
onSelect: (chroma: number) => void;
};

const ChordDictionaryChromaMenu: React.FC<Props> = ({ keySignature, selected, onSelect }) => {
const ref = useRef<HTMLElement>();

useEffect(() => {
if (ref.current) {
const currentEl: HTMLElement | null = ref.current.querySelector('[aria-selected=true]');

if (currentEl) {
if (ref.current.scrollTop > currentEl.offsetTop) {
currentEl.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'start' });
}
if (
ref.current.scrollTop + ref.current.offsetHeight <
currentEl.offsetTop + currentEl.offsetHeight
) {
currentEl.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'start' });
}
}
}
}, [selected]);

return (
<TabList
ref={ref}
className={cx('chromanav')}
aria-label="Chroma Navigation"
direction="vertical"
variant="ghost"
block
>
{NOTE_NAMES.map((note, index) => (
<Tab
key={note}
className={cx('tab')}
onClick={() => onSelect(index)}
selected={selected === index}
>
<span className={cx('label')}>
{formatSharpsFlats(getNoteInKeySignature(note, keySignature.notes))}
</span>
</Tab>
))}
</TabList>
);
};

export default ChordDictionaryChromaMenu;
Loading

0 comments on commit 8b04d90

Please sign in to comment.