From bbdbbe964054cb1b127e1a3481cda999632b3111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Jarasson?= Date: Tue, 19 Dec 2023 10:30:58 +0100 Subject: [PATCH] feat(Dictionary): added basic module --- package.json | 1 + src/main/types/Settings.schema.json | 1 + src/main/types/Settings.ts | 2 +- .../assets/thumbnails/chord-dictionary.jpg | Bin 0 -> 67901 bytes .../ChordIntervals/ChordIntervals.module.scss | 3 +- .../components/ChordName/ChordName.tsx | 29 ++- src/renderer/components/ChordName/types.ts | 2 +- .../Icon/icons/dictionary.react.svg | 4 + src/renderer/components/Icon/icons/index.tsx | 1 + .../components/PianoKeyboard/utils.ts | 12 + src/renderer/helpers/chords-data.ts | 4 +- src/renderer/helpers/chords.ts | 20 +- src/renderer/helpers/note.ts | 2 +- src/renderer/router.tsx | 27 ++ .../ChordDictionary.module.scss | 26 ++ .../views/ChordDictionary/ChordDictionary.tsx | 87 +++++++ .../ChordDictionaryChordMenu.tsx | 61 +++++ .../ChordDictionaryChromaMenu.tsx | 68 +++++ .../Detail/ChordDetail.module.scss | 127 ++++++++++ .../ChordDictionary/Detail/ChordDetail.tsx | 235 ++++++++++++++++++ .../views/ChordDictionary/Detail/index.tsx | 1 + .../views/ChordDictionary/Detail/utils.ts | 83 +++++++ src/renderer/views/ChordDictionary/index.tsx | 1 + src/renderer/views/Home/Home.module.scss | 2 + src/renderer/views/Home/Home.tsx | 25 +- .../views/Settings/About/About.module.scss | 1 - .../Settings/ChordDisplaySettings/utils.ts | 1 + 27 files changed, 810 insertions(+), 16 deletions(-) create mode 100644 src/renderer/assets/thumbnails/chord-dictionary.jpg create mode 100644 src/renderer/components/Icon/icons/dictionary.react.svg create mode 100644 src/renderer/views/ChordDictionary/ChordDictionary.module.scss create mode 100644 src/renderer/views/ChordDictionary/ChordDictionary.tsx create mode 100644 src/renderer/views/ChordDictionary/ChordDictionaryChordMenu.tsx create mode 100644 src/renderer/views/ChordDictionary/ChordDictionaryChromaMenu.tsx create mode 100644 src/renderer/views/ChordDictionary/Detail/ChordDetail.module.scss create mode 100644 src/renderer/views/ChordDictionary/Detail/ChordDetail.tsx create mode 100644 src/renderer/views/ChordDictionary/Detail/index.tsx create mode 100644 src/renderer/views/ChordDictionary/Detail/utils.ts create mode 100644 src/renderer/views/ChordDictionary/index.tsx diff --git a/package.json b/package.json index ad52e30..16f4dfe 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "start:preload": "cross-env NODE_ENV=development DEBUG=app:* DEBUG_COLORS=true TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts", "start:renderer": "cross-env NODE_ENV=development DEBUG=app:* DEBUG_COLORS=true TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts", "test": "jest", + "types": "npm exec tsc", "schema": "concurrently \"npm run schema:settings\" \"npm run schema:sessions\" \"npm run schema:windowstate\"", "schema:settings": "typescript-json-schema --strictNullChecks --refs false \"src/main/types/Settings.ts\" Settings -o \"src/main/types/Settings.schema.json\"", "schema:sessions": "typescript-json-schema --strictNullChecks --refs false \"src/main/types/Midi.ts\" Midi -o \"src/main/types/Midi.schema.json\"", diff --git a/src/main/types/Settings.schema.json b/src/main/types/Settings.schema.json index cffc5d8..7e30842 100644 --- a/src/main/types/Settings.schema.json +++ b/src/main/types/Settings.schema.json @@ -105,6 +105,7 @@ }, "label": { "enum": [ + "chordNote", "interval", "none", "note", diff --git a/src/main/types/Settings.ts b/src/main/types/Settings.ts index 1daa53c..52bf4bf 100644 --- a/src/main/types/Settings.ts +++ b/src/main/types/Settings.ts @@ -2,7 +2,7 @@ export type KeyboardSettings = { skin: 'classic' | 'flat'; from: string; to: string; - label: 'none' | 'pitchClass' | 'note' | 'interval'; + label: 'none' | 'pitchClass' | 'note' | 'chordNote' | 'interval'; keyName: 'none' | 'octave' | 'pitchClass' | 'note'; keyInfo: 'none' | 'tonic' | 'interval' | 'tonicAndInterval'; fadeOutDuration: number; diff --git a/src/renderer/assets/thumbnails/chord-dictionary.jpg b/src/renderer/assets/thumbnails/chord-dictionary.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a3a5d7528444db7a318adb31b5c3e8f31afd1343 GIT binary patch literal 67901 zcmeFZXINBA({Lk^OIM2UhRA~VB)d%nCFiqdQ1{;N_rBkEo^zgif81-&GrhXIs;jH3tEuV()y<-hqK%kcW?f-N4e$%--FZ1K{W57Z8W? zi$g^jpu*xpeBw|(kO}}`K7*WK2&oF+Kf#LrQ^H&&Bg2n^@90=x5K5_nD7YNf| zb%AjHy)GD#-!%9!e$ysL$pht-!9OUKv*+p9=^8+YuEkw>4FKbG9Uw->05g9S-svO| z2oU1o;o;*E;^PyN5u76+yFfxnNOFOig5m-N1vMEV+WmZ-MgG-?5D^d%5fhOT6O*1N zCMG_Q@`%r$Wg+|DA#nN@Aj1Q!foGTyCIEvBf=LEBodPR)?z9CW4RVPMK|7FPl*0rd z7+BaixOn*I2tWn+ClCT)VEzJ<0uU?+CI%J;HZBexHYT9}2qeSAVkC!R%WA_Ym>eJQ zix+4j>vTb3-{op{OY#Rg`Q1x6@27RZ4k69qQ^gP8Lu4w`iOzX@D{e{p&kE3bP2;Vk%_1qa0dE~`-U z0LVl(m6_sAVHf1r-z@xR4%q%;;dBBZ#6*`#21o;&p)evd@7=?+Yn5w|IzJJsL3`CU zM@QSN`kci>;n%=aZOP=i-eucqBog^}E7h#Lr(tbcrJKsne!oh*goJJFBISq#Wwqb_ zr}-;+g9mpowG(t_S4bSC>76eXjNiUBzm}%39I^ZL-Ok-;xDVfarRIUO?ymF~;e)V- z1+lj^ahR`F^{}3?MaKumSE8ON_Xl*IvEa|LFtUg=!p$P%d!HY^w8|Q3=(H-|5k923 zJr!k<9kwxGcwqFYa7O2-IPCk&)Rnu=0h}d)m@n!+rav&tk8OEhC&Bc_m~$#7#xm&Ut&cO9&W+DU1ns@2R!fA2ZY!2$9ZcOE z7A^1mL|{BhMB7$*OvRIV3K)w!l!q<9_jV{09y#z4Pg!xKj|zRfLqlM*b6ml7Y2`<` zT6}jqkjAo=kJc@0%q@8H61z6mHa$ zHA67I0B=vjx`NZ%pCOK6St>l*jQO*lM*dg7WxCjm(+``~27cqImeS97{n=zr0rz=7 zu0bcO3^iko;Z!3J$XuXx?`PU)KD-)bB-nE;lrOpAj4C{O7vNN_as$A+3%hhsC%aSw{^rSa6wYO4} zZk3O{vdJ^ruim#i;DmK`cgwYQ(k&Q$`MgW%9T3=chlFINdH?;w$@jy7kEZ~6UA2$i z;>;1EIy(Y-$hCTs`F_fL3S{NPd1$*a?Gy;Sukr0ZaF$fm**7|;ha8Ig zj;`9t_)hnCrB=Im`zRkxP1k{?cBCp_I95Y1^|a zIp7_fbf_{hHEDgvV8s4;8XLh1?(iG+tFrpS-p>zhVSS8WMh0)cd%9cOJ#{DY#P(`( zHy&1iW5m`Pw56b3P5VR4&NZHez4&VBmNJcl@olc<9g^YR?4$?&AjgDTw$^Ko6v(UY zuewb7p6#AkEr@Rn9}(UyxI_PZ2I1v%o#V6FEbg`1cjof1Y9pPW+i$x1!r>+c&PS@Z zr@tDMnjOm?Jn!bRvCu%+*m12{KT1;eDD%EkGU=N#<7Gemk?Ry_9o1T)LaZ;xPrrKl z@J?;s?4 z8?m`w8h&~j+TEKc#En_47AyI3OLwS>T+CfQI!1g)6_y%;h* zJ?1CWGb|wE9ANzUeJsQi%U{}yW?f$_-D3L77M{bW=@CS0)WhvMHIL3BYwpS#vNfXW zAsag1fTz<=@+X>gCsI)dPZJ2Za{ayB!c~SRjf`*tG(GI=oWgx$OWO}ewey#Qb(g81 z)b{0dr)S)AV<~MkB_3p~6TW@NlkQ+_!yT-?{?q<#?J2WuXCLWGOxbOz!Tf$-KF_x8 zhQ7uRyhTG>Ne>Th`Z2Fd@1L*dY>e5ZKMGUz?kkSpTF)HaB-(hsDv+wbF%ZAKRjo~dI+DHGGahry6Q578_ioxw+Dj>F?GH2QtoRx|W2 zk7^nVBhm{-aD1}YFyz(lXl_bXXCIAL?B_PQUNuP?@_k{rGdk#oU4Mg4(sbaK%_K4- z`(>hVhRVjV$fk90HO+;Lo{28k)#fSYCGYxawTaB7icp*TBqxf57mrJ!&1`LSmv4$X z>Q^<}?yd%Gn;$*3wrhVj9>EmSP6R-^Up{ue=zQDS+MD_m7|8E)A1*Blj~U$j0o{22 z=EWU@(vfbPs`TX&PlHQw3zIXS-LSkjGVsSnZEK0s-(_G?NuGuRq&)aIO z8kaZfPAfg%Q}r(6PEY$5cj2U@f%I0_2Z0wS`4$$Yb93WS=9(J8UnJHb4Hyfu9Lwck+PffAcq`GnNF2qVWLJ8hWVx8*6S(dfzI zZR>^+3D=CAPjC2iRUC+~5?#TNS9x$a=@w{T?y@(^5jUq6vVXbr!bYRC$g{C`5l6+& zy30bZ8;xEb2wboo=$Ul$rZkb(JctV(uv;AKdtOXX>^cH{!UIK`*U=!#>;3eDUk>S3 zY!#o_dOWV#>t--OaARGRATSNs@{@Mkm@V)+(?YEH!#sPngP7ET?_Nr}-`AG*Hi+AD z=46Q{E$T*)Yp;7gnmSvy-!UW(&m-lU%;*F7EBe5Y^Nf;bj=YG9;jKe;AMR{8Rk(pvB2(FC)Xn}f$T zlwUYtgQ%pbe}B?l#BTP|TUhD9cZ`vBQ>o9Fj7|Yvi*;XXgPRs3CUI{Xhn-Vj!-gA( z-$*>NPG|~dPpXZFKQDsP-kpKYJ)JRzeyvWc##t-ba^z3h=-Z}Y4mu`^uwSshx%zyW zWU0u)pk80sL-*tj~UnC2et6a^|UEJ(0*Z zd=&8%7R`e+t?lOKc6``oI(^URzGD5O z5B%kmmXtz4Nvqy&NhkLz+?J2)+!AzGMW|lJP7ge^<_8#GXY}s)jb3;|S92u3R#GWq z+ehMT?O5^}?{V~TXVOGkZRu8FV&6QG#W8EcmXo*CCn?N>K7+bVzXzLsAG5CA*ZI2d z^5wXs)9docmpa}NgwdcW_ro>oD-v4~$21LL*{;Mnm7-OXsWp@}qOvE25;qUT*W$lO zQ1p`?ABN*0BqvS*zW1v;#fRLksmNlx@hFW4ankkGOQ(S4)lYrIy!Q@j{BCdhiJed# zcV@OZeVsU_pE8YAb-C49V*5(xVkFT(jvgy`~aKl*R&!a-U5|<>| zE}sHwOIPwB8S*kqJJeL1J_Dwo(@VoRjiHhdw;ATFdUqxlG^cqe5 zDmPtvk6UfxjpI|GV=RMuHhj#{J-q3dK7T8JpOalR{<{i17 zL8PlK#ldykjOBYeI>w3H7*qT3_NktX`VIQ#A)FtPM||(8Xy3ReK6K;nY15oKUJ!%4 z$;nf>6052MdhxL;S8}h!s&J@|stFTkM_L!HQQ~&7bMul&r{2vQsUNXB?z_>qSN*&o zc~^pN$9UlLyw$)cr6_%m^HuxWNc)W{7KtfDfh*_h_bNpf>fFm$Qj8XaS7-+#KXz0j z`-(Wm3EeA@hLi^)t9tCqhC*AbM`~8(D-}=Md@}onOA*y$E0>HGj1K)u6MWoeToY%9 zc&I3GtMUiKtV@P&2I$$3haa_VG;Uj+0*M8;V{AtnRw7qy9epApCD^?Z>2A=>$Gg7E zs>nL7UE^%a$_L9BSWBD|r@*K}>j1e<`yTzEVC>rp7iFbW^Vji{LCFpduE~d-QfWz2(>=rp zquyz?kGsAH1fB}z%d|ZSA10n6rs{jeovYn9GOOd`JFZl!Da7Z*_S03nd>c8a`ecIJ zXq|jX{g}0{&TiU1{}gzblY`GOagX;H(`4j23B!}P!@5Qxd2}9ba-CQ;O(D)6*_k2Y zt4E*5;^SLc5PmnJ=F4!D9<2GZq}JITNIlo^(};2vp_oHl|73a!41xNk+T^E^#?f%) zdVR$!$xl+p)3S1dAx6`(i9+v|lA>i#@=#OK92MnQHH9!$ztiDO_~Xcxu^FULI1_}Y z_*Orb^yb98NYo~&YIP`GLflTe<|ai&ke|d!a|?{mFSO7Apr2ghL1^UoD{@VcCt+xK9wPO1C? zV_Ex>H3=Vup5;bX_e7ba$$F;j2*T-i70w8XU)Gq?KRXJG5BMu3nRh8t0vsM$Y)!Z{5}1?t5_xe9Wjg zJhrd)YvQQDz`FV=#S(An1Y$@umuvm$?yjad7M}QizFN8Jg=J9fb>hevBH3zg#H!j) z4nu7$CkGr>Jnz{TOzX7}WuBxKZ&mx{>>REhGZmR%FJKtd#f9k#TVav7)VKfaF6jQF zcm*ZU@zXUL_+J}X0z7EoT>9xV^m%8big-1&OqCwM79?VboLJ#t9WCLFd)A^gv&q0G;b{@*p?pJwP2Z1_m>GSGbJ*XB3BMciqYE zzog08{8v0VCo{d9b}mZHIyR_Q{_W&!%ozS%x{kAr3p!oP!$!vBH~Kj@B*MksNde{p zL)8Gyq?U!2^Dq5Cg;B`h6U8>5jx;|b^Do8ffm^Qsu2@}!%~`sZ6GFxiMFH(d5l;3F z2KSI)#fxrhS)ThQtM4Gb*{6lt(-KP83Q!2c`8hmJwc`|n)__y{ny> z^S|_dgbNgep_B+fb(!CK3C90LuK~H=`hUiT&TsWbR|v=1#u9;a*0)hdbx_DZ%E3m% zz+`gpkBg3|DEzG~Tni_ASBHNj;n+J_T3G(Jy^6Xh>YC^@kOmRV)y4ig(hlhab3vMc z34orb!>>W)+!-<|1qD&DwO{}r|4-K+)78o5A43sZ`2S4NaJKm8aD@Z2anXZW`~!;^ zfwZyFLwdNVI4kREsQ)s9c)ybWfsB98-pNbW#?s=Kq)5&xuly?+tU0b3(j4ZBHp}>K zNGF$ngRcK8`JdiTok_2;PT&r#Q(qpm+k zU4M?c{v37vIqLdz)b;16>(5cwpQEn-uOD@t?IbaRyF&nA0RF+;X7< zJj4KK0r$YL3;1_Nha5nR^Zx?|1HcdPfqwxM$+I$Wz#wY(7kv2`Rm$lXV%B>uE)L?n zymrn!Fw{%qJP3PRUJsZ9FO-Lm7q}+v;Q&KeBV8EaNN_(?l4+x^k%__5Op?h!NP|zq zK^}R}QrXK1sq1w^58-8v5Hn+vmSVW(A?{)8V2gBtF?iV8*g1=PNHU>`i-TcQGA|PY z8pXw0k_r7Iwr6TYbAuS% z&YpHIFb{4!XXalN6p+pcCrk7(83T$Ucm(9?BFO}@dR7Nphu>uXt+0L}njwDUIe@#e z=)%nqyht0QEz-`#85ArORev;5hhJ~#|IYFc(!asz*7%hPl&yxwzt8Zi;cRVx7vb!p z=mu8fziVb^)DA1J4$|4))d_)AbOYIBMiu5DF7Jedfd^uG;9=OWmip^V_Mevu{+0m= z9-tv5nNYV3xS{-Dlj(uC37P`Q5?N*43-SCCxB){LqlA}&e;WKhd`<*NHT$y z=CQOi6Gxhx!-a%JpxmN-d?MV!B7!2^FsPU)HyjETM#9Vm#e~e@zw#^CBV18Ui^_kd z=4SQ?kj7u73JHnw!;o-MZX^r}<2FMGBDmqga3O9pb0m@vCMt*&5itKn?d+5RX7hKd zD6Y&viXw1?u#ku-k{c?>591a<3JQP>35#;`!32a5d?GLrQ8ROtI-@m2T-nkYY&*}N z9Yh!D_%mc<$$(Z0aTo%1CM?N>K(#W`jOk~x<-d`~|5nLA^Sgs5ng4C;pOx-xZ|>p_ zb3)2mfU^4^wTt&ZGH-8(I#&3LeFvBmcrNdPbaIwtGIz4KWjNCW2RjP}7yF+Pv|9W} zlFl%A!4(~N{gx5E zP6Nzj!1W_|^OqD81M_S#iCR#C4t*mR3lrr)9}5S(I*NmfjfI7ci-&`Qi-UuA4i6u6 zc<0U$qTHE}4*t!9U}0n9;o^~;J4f<=cq6wNypanj!SKg~fLB=0ZsfK=sKJ*uQ1@-m zI7R!AU)OEHR18e;n(kjP5QL3`g^LGX!-e2Nz#tYj7KnjbLz6L{Wg~^kk~7Is2%Nuw z#~@?M&nHMprNw-OMMR#J4c|dXm|Bz!lo19dCJqh(J`pw^SO5kD6AK`Nl4CPc;E>8X zGV!0+hA|7o4Kh6Fmx~>!5RAWo%NO%fNB2slkZ>m4sazfrpyx6~spRJTPQfgv>C40H z7DZP5ESA8VNGfvhA~6J$FwRHX0$`H%ycs0wWHYX^R9I7FdZM1A!^B8A zFIoyP`h%~rnPN}}fdNzS^)G55h%p3=1ODLKXpA9nYT&Zq+(WW>aflIBU{~t!OoP;L zQ#Xuhgq5%c0uqP#2cpZw!Xz6Ex)&sf#TbGE5@4VX0{BRSzz3th|1ujJejTG%uIB(*$UvhJj}iS5sS_!}G$5ABEyrg&6$ zx7e12JVrSLPmWvN(il!1gaJYEi3bNC7k3yoLuZ)BbJX$uxl;SzKOUMQ$@q$bVn^dG{V$l4*|8yF0FnD-IA6SH$QZ2@f7-zD@vC5H09cxwtUpojqwLL9)w#&y)G zw_m{Sk&kp5aGs9msd`boYJL-G*TEw?KryaF^LwMe1ft z&M({y^9RXesU(37{ZqLZ!1g>-E;zM6sBQi~)d1q}Z=o&54z>wV*`pZpA1E+qD087q|-SP zMrN%5^A8<zUvCni$q%*N}VBwmPXz+I;3P zAOFophSc=^*U;eMfC#q~dkG)r^!dA@i=IgrY7d^8iO^|YK8}lCKcd`x^NnVOd~aV^ zvN5jqI4OFbUSzR0pQ`2{57w6){IU<0sJQe)?-NPh^$-$H`kRm2t3<~)<9B73nP)>k zXy)bzlU$`YAjIF)TE5OABKlaTOEd*7*c-+7@<(-LrQmcgC8O{6@C6SIDo%MEIq&dX zSKJ?!edb70mT!LDcJ8~yc+JUvv%Mt#=Gty|0JV>*Xs~78ru%@{8pXn8;(%5=-;5;h zD7Ns1j&}DG0?$P{Odr#`8JuC>er$$^yo{Omr?Won)wd1FlinYYyzjMI&Z`-px9r1m zO~=#WAkq#yvt2WttvX60gPmyaK+Yq66S6HhO7a9(Ta3f^w(7_QV2tsGftvZ(*F7b& z;Z#(i!x{{SIQQ~w^pf{{3kgnuWBuEu|7MH}=;28?wPzRQ9&$Qo z&&saJPvLgM+Ycg~dq$&L=s{hn?mZyB^KQMN?ukZOQm3EB{6QOU#TG9?-99H=^93$s zP4Lt0gX@w#Qm!AIiJ4uuSL-J|ucd9B*uVJUttsa)`NlCuN;JNj?#9f{4SCi5HhImk zkH-vr!5wiQEncZ%9PN$RFAVFx3r_FL!&x?@)!O&+!Iw0@|5A8x3I8*5JMLJ8p_G1_ z^6K3zU!T}bEtxus&Nyn72YW%Q4vTHD($yr$=_ z+>Pq?k{R*2Rk6o!kh47ZJ>}Z=gyoyutw=A?lIaGY#_ei~S|W?#aOnURuN$K6$@1;e z>ht>|u);_#ua(&;1FsKCUD}1SYRd>b%-lM^8ca zb_m7ur$+})mU*4#p~;xy$_tayA}Lb&-7ae+b%we1ESsePqj88rNCapr74V~} z!I^^G6$J!o3qOOpKR8yw$(WcyjI?XB^Sg$*xB8t>U%~;%SPekC4Guj&haYf22tgTX zQ!oMi91hB+gM%Ap!P8_cnF7Fe=ap4~qUT%|86!Ee$(I3~Pz3USSQe9v-ze_+x{a_j z(l6!0-nLh?U52A5DHig728XD(a3&jyIC#B%VNfr>39Lavw8zBrA!h^%gaq^BvO-8X z0V?uB3ar~GjsOU1+CtEpDOgAu%aKG{k${@HfNX*e4qz1b9I^Mn$TQu!42@*KB#=qe zVDjjtTU*h{FbWATjt2mzNvNO02;}1>SQZy{s_%mI+%1Q}pRW z9$4 z|HMcRGA$uJ;5l)!dTA$MI@jT_NTAe*Ev;<*ZCFAIRhjsHl3Mr=q@q)I19wk=-{-~4 zuTmbZ^fn*|z8*c%Hm(p~ILo_!mRGJDWF9OO^RMX$;L04E#&k=y7k4t?{*r5dazrNd zq7qD%Pg`_@*w!FATWAWp^!w zo~HW!{nVo26(4S8f0S0SDuC(&C*x;8O-w^$FbiqpJDZ)lT%<@;kCUQu?`3=BdSxwK z_%0RUvcgxq{AQ%>(b3(PHImTX1{%kzngx-y{u8xf>p{AN!)l7^Pp|DpyQ)@>MW5E} zi8R-F-=)vKs5)y`8Qu)7Z5W+cswdgv@zWjKuQ6zr@VYduPfR1Ih+l1UI7;+twv2K9 z)g}6lgCxo!R^TEzs?oqH6zIoX0euu>ShiD?jRW97y!eU3r1liReSX;IE%9~Gt-;E* zl0&>&o}E3gDI0fcjn2e$mI%()$K_FR$Aj<2K#(e!-rSR6=MldlzfHV6R?$ z4Z2!!pD1C)_~Rtwr?5ea@zmWD)E>O^`RS=h9_Rr<7WHzw`tc~ns69#v@}XHjR;Qs(U!?eZ>24*_r8m6_!Hf!VE<0gykYZ4g9{-DIJ#Qun5hMB_7!(fxQ3hccnPy0esFcZ;Axt5RK~-k4 zNL#1LOKoAHq5Pk*KvJM%^d|&l&>?Wm*Y(xkzn2V=7>w z%7WAz?SFJM+FNIl<7zj=Q;0DJ5NJ8!MaX~_8eIQ@mR*Sfln>h8V=$9~O{fTvVLSr? z5OT$K)FKhJ>;)gFB`gED8U`OItp!&ZUuk~5YmE4#z6WO zHBQPiflGg7EI!omN!lLqKpC6?;5drmg{wqFv~IuLJTM1q8dqs1Oy_3gS-xtbUK2h@ zMF~zEz=bK;Jpun{P$?qD;kRPBRFBDmxUb4ETme_OKi9-q=VZXaSHSI+TufX_v#ND= z>kLRPjlOKyE4!ipX0E4H<6yy2cyZ_K?iTZ?u7@(3R$n7IPTLwSd`Sr+;z%*u@C)WWr$Ke;H})R1+FP31ijCJ+RpGN#A1wL z2SD`#9bPOC;mAzKXhTsKdA^!sM)Y=SS+KAZZ{}=kseU)OD;9&&dIhuzHsxmkFn^g~ z90dSCF!+HxOn4AbnX!Sr;YIJ+Kbzgw0*ateqC8|4urj`Hj+*9RbGUvN8dWH^;4F!ihKuHKn*;vmF(uPx^B+?jPHoA87`-1 zs)!$a533+YeJ+xmZ=^tGY=&Pw^d;1NZ+JJn zyl1~P(QfbRDe%@- z`y3eJ?dhVb?N}Wxp9d5DMqQZP@>C;ihafON+V~$I0}M zM9tp9*k{T7=M4Fmggt9J#bT6DmC|LAHTfnHUdRx| zh|~&+EtfxT{A}NDC@r2+uG!&>!!cviC31qpyRY$tWTC-%Te6-u{Q+&t1Cz4&1i@68 zgUR``e}hgwzV_sT zcK?khDtAQl`x|mn;2r4TsVhk(!)V(RGh!ksW%*nUNh`@yaYQ{Q!-HMP2#TW4OJjha4?Bj%$TK79pc>*bEga`>Jz~Hl%aJF^xmm98 ze{7*@Zg>k+y)qM_#$eP6U^#vD-+XXDR5jEog8oX%MuUE-`d zP%80_F1nk&LbbTudLaJNH>wznTyPW9RbfLG`zf<^+9CGEV0n0;MyNlCSr0P9 zVYo299b>b4!$R=(17GI-Mkwb69-Eq;@dhW+knyXrv?cxTuxV>DL(HY)9yCoi$p=a7 ze-j4$jo+oXs@E#7vY2HY949W`R%A-#6Tm#a!cxbMq+ZOf4zkxCR1Y%?b!HGzZc8Ig zY*x_v@$3?2yH^YDwy!EI44;3lD0M}Y3Xh45!N~So%RTdf_uVbvr*3#lFb0pPz%R;; zF_4~n9-%3~-6N$Kou$pi41P3ZwJKw=!_e_0mZH-%kx{?m_#!994{go`yv?UA&!{0+ zS=uSD%CU}+hZ^wqG~ZIO;d_8LV=dhuTfRFW!=S0@g5mwtgfe25Q)q~smKTKW%xP|bf^mC zb!jib4*0|{TALc>n@Nce#6l8}fQSALUJH)+kq7J@E)-&Vd@n*_#D*0vd$#;>EBw!Qu%Z;gMH}#Muk412YP_m0_IWb(I zfd@f*gxQV6$hZ&uB^5liyhupjb^B7op!90RCP}yQb$VlK1C*KP-?e*;VJc(!f8YGp z+KHh3O8k%jxT;Y7rM0C(%IwJ?mflS`%^N;hSwQmW3I*m+R{IGYF9Ji^g2`n-C@y3bjArR5wx2ekDIFhVhT_-Xp_%^Y6HlUfn)IWAvx6+kvIC%TX` zKD30e%@38#uV` zZ!wTL+w!&PJU4sm@RlaVmV8&gY)kXt76(yC%`vS>_zAlyl!BTxS()3fUza(EnLLS_ z0{)84^g-W)r- zny*XEedfLMvYDhG?vmhzyU9{r?Y)ui<1esao^<`I534bgQy43C0A@V5#aAIkY6enY z{2=Pgz{>kr{#g;^c+!wmh*n}$aLRc+=_Z0EGv%P56G{7rC4P3Djo3!jM^u_GjZS(J z($7`h7xFzj`>^Ep2|@l?iJy6AAGT4$B@q9l?=!N!R*{5*CpbZt zI>eXdzoR6vTwze8VNxcfV{ERGTF#RzP^d@)=fziRP|QYo7J zvCNR>l9*P?Ps>=0m|tpJyjwv8d9pCgUh+Woal7v)I@L|!qY-0i$EM1csIPTg_Vdv~ zo*u7ydcCk$c2+8n-54IhGxpTIZ9eL`eZ%KP^-l{d=j8>y@sPuxU_EipEi7N?)?b2l z(LZp=e?sfjvdEaqWMdv_tfjg?u#rQ&m+l*n$PR2)fkOgj5lMKd-6~JT%wNtXQVIp$ z@8dF!@HcPkBvGBa7ma1+lfgNp=Bt}r^?-cvMdN%*N(k;f$1i&eE-d*=O5$wHHT2m= zS>egw0|BXvqa4xKmj`bWxj4_;q&Ypo@+}b}s2r#_vf8$Mu5Kyoz&|2a<|ZO-fk2-XtvlFevIUDdAA5 z=}M*ROzP$Tip+j*s=zlS2T$u%#cP()dlZ}VrqE^qkA0%6`q9*TJ*}1tyaDI0K2n

HQJj$Y|2YbS&06i=} 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 literal 0 HcmV?d00001 diff --git a/src/renderer/components/ChordIntervals/ChordIntervals.module.scss b/src/renderer/components/ChordIntervals/ChordIntervals.module.scss index b88ef59..1ec51dc 100644 --- a/src/renderer/components/ChordIntervals/ChordIntervals.module.scss +++ b/src/renderer/components/ChordIntervals/ChordIntervals.module.scss @@ -3,6 +3,7 @@ .base { display: flex; align-items: center; + justify-content: center; height: 6em; } @@ -35,7 +36,7 @@ } .interval--active { - background-color: $color-success-normal; + background-color: $color-primary-normal; } .interval--played { diff --git a/src/renderer/components/ChordName/ChordName.tsx b/src/renderer/components/ChordName/ChordName.tsx index b843767..a674535 100644 --- a/src/renderer/components/ChordName/ChordName.tsx +++ b/src/renderer/components/ChordName/ChordName.tsx @@ -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 = ({ className, chord, @@ -26,13 +45,9 @@ export const ChordName: React.FC = ({ }) => { 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); diff --git a/src/renderer/components/ChordName/types.ts b/src/renderer/components/ChordName/types.ts index 0aab563..a89471a 100644 --- a/src/renderer/components/ChordName/types.ts +++ b/src/renderer/components/ChordName/types.ts @@ -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; diff --git a/src/renderer/components/Icon/icons/dictionary.react.svg b/src/renderer/components/Icon/icons/dictionary.react.svg new file mode 100644 index 0000000..9cac8f1 --- /dev/null +++ b/src/renderer/components/Icon/icons/dictionary.react.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/renderer/components/Icon/icons/index.tsx b/src/renderer/components/Icon/icons/index.tsx index 574b1d1..531fbb3 100644 --- a/src/renderer/components/Icon/icons/index.tsx +++ b/src/renderer/components/Icon/icons/index.tsx @@ -10,6 +10,7 @@ export const ICON_NAMES = [ 'clock', 'controller', 'cross', + 'dictionary', 'exclamation', 'github', 'heart', diff --git a/src/renderer/components/PianoKeyboard/utils.ts b/src/renderer/components/PianoKeyboard/utils.ts index e3acb37..c4cf963 100644 --- a/src/renderer/components/PianoKeyboard/utils.ts +++ b/src/renderer/components/PianoKeyboard/utils.ts @@ -4,6 +4,7 @@ import { KeySignatureConfig, formatSharpsFlats, getChordDegrees, + getChordNotes, getNoteInKeySignature, } from 'renderer/helpers'; import { Note } from 'tonal'; @@ -160,6 +161,17 @@ export const highlightLabels = ( highlightLabel(containerEl, midi[i], intervals[i]); } } + } else if (keyboard.label === 'chordNote') { + if (chord) { + const notes = getChordNotes( + chord, + midi.map((midiNote) => Note.pitchClass(Note.fromMidi(midiNote))) + ); + + for (let i = 0; i < midi.length; i += 1) { + highlightLabel(containerEl, midi[i], formatSharpsFlats(notes[i])); + } + } } else { for (let i = 0; i < midi.length; i += 1) { const note = Note.get(Note.fromMidi(midi[i])); diff --git a/src/renderer/helpers/chords-data.ts b/src/renderer/helpers/chords-data.ts index ba75a64..d43f1a4 100644 --- a/src/renderer/helpers/chords-data.ts +++ b/src/renderer/helpers/chords-data.ts @@ -12,7 +12,7 @@ */ const CHORDS: string[][] = [ // ==Intervals== - ['1P 5P', 'fifth', '5'], + ['1P 5P', 'fifth', '5 5 5'], // ==Major== // '''Normal''' @@ -36,7 +36,7 @@ const CHORDS: string[][] = [ ['1P 3M 5P* 6M 9M 11A', 'sixth ninth added sharp eleventh', 'maj6/9add#11 6/9add#11 69#11'], ['1P 3M 5P* 7M 11P', 'major seventh added eleventh', 'maj7add11 M7add11 Δ7add11 Δadd11'], - ['1P 3M 5P* 7M 11A', 'major seventh added sharp eleventh', 'maj7add#11 M7add#11 Δadd#11 majadd#4 Δadd#4'], + ['1P 3M 5P* 7M 11A', 'major seventh added sharp eleventh', 'maj7add#11 M7add#11 Δadd#11 maj7add#4 M7add#4 Δadd#4'], ['1P 3M 5P* 7M 13M', 'major seventh added thirteenth', 'maj7add13 M7add13 Δ7add13'], // removed 9M ['1P 3M 5P* 7M 13m', 'major seventh added flat thirteenth', 'maj7addb13 M7addb13 Δ7addb13'], diff --git a/src/renderer/helpers/chords.ts b/src/renderer/helpers/chords.ts index 141c350..f556c58 100644 --- a/src/renderer/helpers/chords.ts +++ b/src/renderer/helpers/chords.ts @@ -8,7 +8,7 @@ export const CHORD_NAME_REGEX = /^(([A-G])([b]+|[#]+)?)(.*?)(\/([A-G]([b]+|[#]+) export const CHORD_TYPE_SPECIALCASE_TOKEN = '6/9|6/11|6/13|no[0-9]{1,2}|quartal'; export const CHORD_TYPE_QUALITY_TOKEN = - '(min|maj|Maj|m/maj?|mM|M|m|-|\\+|aug|dim|dom|sus|o|Δ|^|°|ø|q)(6/9|6/11|6/13|[0-9]{1,2})?'; + '(min|maj|Maj|m/maj?|M|m|-|\\+|aug|dim|dom|sus|o|Δ|^|°|ø|q)(6/9|6/11|6/13|[0-9]{1,2})?'; export const CHORD_TYPE_ALTERATIONS_TOKEN = '(add)?(b|#)?[0-9]{1,2}'; @@ -68,6 +68,20 @@ export function getChordDegrees(chord: TChord, pitchClasses: string[]) { return chord.intervals[i].replace('*', ''); }); } +/** + * Maps a list of pitch classes to the note name in a chord. + * @param chord - the chord + * @param pitchClasses - the notes played + * @returns string[] + */ +export function getChordNotes(chord: TChord, pitchClasses: string[]) { + return pitchClasses.map((pc: string) => { + const i = chord.notes.findIndex((note) => Note.chroma(note) === Note.chroma(pc)); + if (i < 0) return ''; + + return chord.notes[i]; + }); +} export function removeIntervalWildcards(intervals: string[]) { return intervals.map((interval) => interval.replace('*', '')); @@ -80,4 +94,8 @@ export function overrideDictionary() { ); } +export function getChordTypes() { + return ChordType.all(); +} + overrideDictionary(); diff --git a/src/renderer/helpers/note.ts b/src/renderer/helpers/note.ts index b3e3182..58c8b09 100644 --- a/src/renderer/helpers/note.ts +++ b/src/renderer/helpers/note.ts @@ -8,7 +8,7 @@ export type KeySignatureConfig = { scale: string[]; }; -export const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'E#', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; +export const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const REGEX_FLAT = /b/g; const REGEX_SHARP = /#/g; diff --git a/src/renderer/router.tsx b/src/renderer/router.tsx index 1e7940a..786b62f 100644 --- a/src/renderer/router.tsx +++ b/src/renderer/router.tsx @@ -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( @@ -108,6 +110,31 @@ const router = createHashRouter( } /> + , + hasSettings: false, + }} + element={ + + + + } + > + } /> + ( + {params.chordName} + ), + icon: , + }} + element={} + /> + }} diff --git a/src/renderer/views/ChordDictionary/ChordDictionary.module.scss b/src/renderer/views/ChordDictionary/ChordDictionary.module.scss new file mode 100644 index 0000000..4f6ab83 --- /dev/null +++ b/src/renderer/views/ChordDictionary/ChordDictionary.module.scss @@ -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; +} diff --git a/src/renderer/views/ChordDictionary/ChordDictionary.tsx b/src/renderer/views/ChordDictionary/ChordDictionary.tsx new file mode 100644 index 0000000..258e87d --- /dev/null +++ b/src/renderer/views/ChordDictionary/ChordDictionary.tsx @@ -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(null); + const [chordType, setChordType] = useState(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 ( + + } + sidebarProps={{ className: cx('pitchbar') }} + contentProps={{ className: cx('content') }} + size="xs" + open + inset + > + } + sidebarProps={{ className: cx('chordbar') }} + contentProps={{ className: cx('content') }} + size="sm" + open + inset + > + + + + ); +}; + +ChordDictionary.defaultProps = {}; + +export default ChordDictionary; diff --git a/src/renderer/views/ChordDictionary/ChordDictionaryChordMenu.tsx b/src/renderer/views/ChordDictionary/ChordDictionaryChordMenu.tsx new file mode 100644 index 0000000..9fc559c --- /dev/null +++ b/src/renderer/views/ChordDictionary/ChordDictionaryChordMenu.tsx @@ -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 = ({ selected, onSelect }) => { + const ref = useRef(); + 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 ( + + {chordTypes.map((chordType) => ( + onSelect(chordType.aliases[0])} + > + {chordType.aliases[0] || 'maj'} + + ))} + + ); +}; + +export default ChordDictionaryChordMenu; diff --git a/src/renderer/views/ChordDictionary/ChordDictionaryChromaMenu.tsx b/src/renderer/views/ChordDictionary/ChordDictionaryChromaMenu.tsx new file mode 100644 index 0000000..99081a2 --- /dev/null +++ b/src/renderer/views/ChordDictionary/ChordDictionaryChromaMenu.tsx @@ -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 = ({ keySignature, selected, onSelect }) => { + const ref = useRef(); + + 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 ( + + {NOTE_NAMES.map((note, index) => ( + onSelect(index)} + selected={selected === index} + > + + {formatSharpsFlats(getNoteInKeySignature(note, keySignature.notes))} + + + ))} + + ); +}; + +export default ChordDictionaryChromaMenu; diff --git a/src/renderer/views/ChordDictionary/Detail/ChordDetail.module.scss b/src/renderer/views/ChordDictionary/Detail/ChordDetail.module.scss new file mode 100644 index 0000000..2b8cdb2 --- /dev/null +++ b/src/renderer/views/ChordDictionary/Detail/ChordDetail.module.scss @@ -0,0 +1,127 @@ +@import 'tokens'; + +.base { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px; +} + +.chordName { + font-size: min(64px, 6vw); + display: flex; + justify-content: center; + border-bottom: 1px solid rgba(#fff, 0.5); + margin: 0 0 8px; + padding: 0; + width: 100%; + user-select: all; +} + +.title { + display: flex; + margin: $space-lg 0; + text-transform: uppercase; + width: 100%; + align-items: center; + gap: $space-md; + + &::before, &::after { + content: " "; + flex-grow: 1; + flex-basis: 0; + border-bottom: 1px solid $color-neutral-normal; + } +} + +.name { + user-select: all; + text-align: center; + font-size: min(24px, 4vw); +} + +.intervals { + font-size: min(1em, 2vw); +} + +.notation { + margin: auto; + width: 192px; + text-align: center; +} + +.keyboard { + width: 100%; + margin: 16px 0; +} + +.list { + --List-item_textTransform: none; + --List-item_fontSize: 18px; +} + +.columns { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 16px; + width: 100%; +} + +.column { + flex-basis: 320px; + flex-grow: 1; + align-items: center; + justify-content: center; +} + +.inversion { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: wrap; + width: 100%; + gap: 16px; +} + +.inversionInfo { + flex-basis: 200px; + flex-grow: 0; +} + +.inversionChord { + font-size: 24px; +} + +.inversionInterval { + font-style: italic; +} + +.inversionAltChord { + margin-top: 16px; +} + +.inversionKeyboard { + flex-grow: 1; + flex-basis: 400px; +} + +.inversionNotation { + width: 96px; + margin: auto; +} + +.chordSet { + + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 4px; +} + +.chordBadge { + --Badge_textTransform: none; + &:hover { + --Badge_background: #{$color-primary-normal}; + } +} diff --git a/src/renderer/views/ChordDictionary/Detail/ChordDetail.tsx b/src/renderer/views/ChordDictionary/Detail/ChordDetail.tsx new file mode 100644 index 0000000..d833184 --- /dev/null +++ b/src/renderer/views/ChordDictionary/Detail/ChordDetail.tsx @@ -0,0 +1,235 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import classnames from 'classnames/bind'; +import { Chord, Note } from 'tonal'; +import { Badge, Container, Link, List, ListItem } from '@la-jarre-a-son/ui'; + +import { defaultKeyboardSettings } from 'main/store/defaults'; + +import { getKeySignature, getNoteInKeySignature } from 'renderer/helpers'; + +import { ChordIntervals, ChordName, Notation, PianoKeyboard } from 'renderer/components'; +import { KeyboardSettings } from 'main/types'; + +import { NavLink, useNavigate, useParams } from 'react-router-dom'; +import { useSettings } from 'renderer/contexts/Settings'; +import styles from './ChordDetail.module.scss'; +import { + getAlternativeChords, + getChordInversion, + getSubsetChords, + getSupersetChords, +} from './utils'; + +const KEYBOARD_SETTINGS: KeyboardSettings = { + ...defaultKeyboardSettings, + skin: 'classic', + from: 'C4', + to: 'B6', + label: 'chordNote', + keyName: 'none', + keyInfo: 'tonicAndInterval', + textOpacity: 1, + displaySustained: true, + wrap: true, + sizes: { + radius: 0.4, + height: 4, + ratio: 0.6, + bevel: true, + }, +}; + +const NOTATION_LABELS = ['long', 'short', 'symbol']; + +const cx = classnames.bind(styles); + +const ChordDetail: React.FC = () => { + const ref = useRef(null); + const { chordName } = useParams(); + const navigate = useNavigate(); + + const { settings } = useSettings(); + const { key, accidentals } = settings.notation; + const keySignature = useMemo( + () => getKeySignature(key, accidentals === 'sharp'), + [key, accidentals] + ); + + useEffect(() => { + if (ref.current) { + ref.current.scrollIntoView(true); + } + }, [chordName]); + + if (!chordName) { + return 'Select a chord or search'; + } + + const chord = chordName ? Chord.get(chordName) : null; + + if (!chord) { + return `Cannot find a chord named "${chordName}"`; + } + + const midi = getChordInversion(chord, 0); + const alternativeChords = getAlternativeChords(chord, keySignature); + const subsetChords = getSubsetChords(chord); + const supersetChords = getSupersetChords(chord); + const goToChordDetail = (name: string) => { + navigate(`../${encodeURIComponent(name)}`); + }; + + return ( + +

+ +

+
{chord.name}
+ +
+
+

Intervals

+ +
+
+

Notation

+ +
+
+ +
+
+

Aliases

+ + {chord.aliases.map((alias, index) => ( + + {NOTATION_LABELS[index]} + + ) + } + > + + + ))} + +
+ {!!alternativeChords.length && ( +
+

Other interpretations

+ + {alternativeChords.map((altChord) => ( + goToChordDetail(`${altChord.tonic + altChord.aliases[0]}`)} + > + + + ))} + +
+ )} +
+

Inversions

+ {chord.intervals.map((_, index) => { + if (!index) return null; + + const interval = chord.intervals[index].replace('*', ''); + const root = chord.notes[index]; + const slashChord = { ...chord, root, rootDegree: index }; + const inversionMidi = getChordInversion(chord, index); + const altChord = alternativeChords.find( + (c) => c.tonic && Note.chroma(c.tonic) === Note.chroma(root) + ); + const altChordName = + altChord && altChord.tonic + ? getNoteInKeySignature(altChord.tonic, keySignature.notes) + altChord.aliases[0] + : ''; + + return ( +
+
+ +
inversion on {interval}
+ {altChord && ( +
+ {'see also '} + + {altChordName} + +
+ )} +
+ + +
+ ); + })} +
+ {!!subsetChords.length && ( +
+

Simplified

+
+ {subsetChords.map((c, index) => ( + + + + ))} +
+
+ )} + {!!supersetChords.length && ( +
+

Extended

+
+ {supersetChords.map((c, index) => ( + + + + ))} +
+
+ )} +
+ + ); +}; + +export default ChordDetail; diff --git a/src/renderer/views/ChordDictionary/Detail/index.tsx b/src/renderer/views/ChordDictionary/Detail/index.tsx new file mode 100644 index 0000000..f9ca17e --- /dev/null +++ b/src/renderer/views/ChordDictionary/Detail/index.tsx @@ -0,0 +1 @@ +export { default } from './ChordDetail'; diff --git a/src/renderer/views/ChordDictionary/Detail/utils.ts b/src/renderer/views/ChordDictionary/Detail/utils.ts new file mode 100644 index 0000000..2cc85da --- /dev/null +++ b/src/renderer/views/ChordDictionary/Detail/utils.ts @@ -0,0 +1,83 @@ +import { Note, Chord, Interval } from 'tonal'; +import { Chord as TChord } from '@tonaljs/chord'; +import * as ChordType from '@tonaljs/chord-type'; + +import { KeySignatureConfig, getNoteInKeySignature, tokenizeChord } from 'renderer/helpers'; +import { detect } from 'renderer/helpers/chord-detect'; + +const midiC4 = Note.midi('C4') as number; + +export function getChordInversion(chord?: TChord, inversion = 0) { + if (!chord) return []; + + const midi: number[] = []; + + const notes = chord.notes + .slice(inversion % chord.notes.length) + .concat(chord.notes.slice(0, inversion % chord.notes.length)); + + for (let n = 0; n < notes.length; n += 1) { + let newMidi = Note.midi(`${notes[n]}4`); + if (newMidi) { + while (newMidi < (midi.length ? midi[midi.length - 1] : midiC4)) { + newMidi += 12; + } + + midi.push(newMidi); + } + } + + return midi; +} +const getChordInfo = (chord: string, keySignatureNotes?: string[]): TChord | null => { + const [tonic, type, root] = tokenizeChord(chord); + if (tonic) { + const tonicInKey = getNoteInKeySignature(tonic, keySignatureNotes); + const rootInKey = getNoteInKeySignature(root, keySignatureNotes); + const c = Chord.getChord(type, tonicInKey); + const rootInterval = Interval.distance(tonicInKey, rootInKey); + const rootDegree = c.intervals.indexOf(rootInterval) + 1; + return { ...c, symbol: chord, root, rootDegree }; + } + + return null; +}; + +export function getAlternativeChords(chord?: TChord, keySignature?: KeySignatureConfig): TChord[] { + if (!chord) return []; + + const chords = detect(chord.notes, { allowOmissions: false }) + .map((c) => getChordInfo(c, keySignature?.notes)) + .filter((c) => c && c.symbol !== chord.symbol) as TChord[]; + + return chords; +} + +export function getSubsetChords(chord?: TChord): TChord[] { + if (!chord || !chord.tonic) return []; + const subset = ChordType.all() + .filter((chordType) => { + return ( + // eslint-disable-next-line no-bitwise + chord.setNum !== chordType.setNum && (chord.setNum & chordType.setNum) === chordType.setNum + ); + }) + .map((chordType) => Chord.getChord(chordType.aliases[0], chord.tonic as string)); + + return subset; +} + +export function getSupersetChords(chord?: TChord): TChord[] { + if (!chord || !chord.tonic) return []; + + const superset = ChordType.all() + .filter((chordType) => { + return ( + // eslint-disable-next-line no-bitwise + chord.setNum !== chordType.setNum && (chord.setNum & chordType.setNum) === chord.setNum + ); + }) + .map((chordType) => Chord.getChord(chordType.aliases[0], chord.tonic as string)); + + return superset; +} diff --git a/src/renderer/views/ChordDictionary/index.tsx b/src/renderer/views/ChordDictionary/index.tsx new file mode 100644 index 0000000..bf79d55 --- /dev/null +++ b/src/renderer/views/ChordDictionary/index.tsx @@ -0,0 +1 @@ +export { default } from './ChordDictionary'; diff --git a/src/renderer/views/Home/Home.module.scss b/src/renderer/views/Home/Home.module.scss index 2d4fd84..b053e3c 100644 --- a/src/renderer/views/Home/Home.module.scss +++ b/src/renderer/views/Home/Home.module.scss @@ -7,4 +7,6 @@ align-items: center; padding: $space-lg; flex-grow: 1; + + --Card-thumbnail_aspectRatio: 640/429; } diff --git a/src/renderer/views/Home/Home.tsx b/src/renderer/views/Home/Home.tsx index 9641595..2af0704 100644 --- a/src/renderer/views/Home/Home.tsx +++ b/src/renderer/views/Home/Home.tsx @@ -19,6 +19,7 @@ import { Icon, NavButton } from 'renderer/components'; import ThumbnaildChordDisplay from 'renderer/assets/thumbnails/chord-display.jpg'; import ThumbnaildChordQuiz from 'renderer/assets/thumbnails/chord-quiz.jpg'; +import ThumbnaildChordDictionary from 'renderer/assets/thumbnails/chord-dictionary.jpg'; import ThumbnaildCircleOfFifths from 'renderer/assets/thumbnails/circle-of-fifths.jpg'; import ThumbnailRouting from 'renderer/assets/thumbnails/routing.jpg'; import ThumbnailDebugger from 'renderer/assets/thumbnails/debugger.jpg'; @@ -61,7 +62,7 @@ const Home: React.FC = () => { )} } + left={} right={ { Circle of Fifths + + + + {overlayEnabled && ( + + + + )} + + }>Chord Dictionary + diff --git a/src/renderer/views/Settings/About/About.module.scss b/src/renderer/views/Settings/About/About.module.scss index 4fc41e1..ebacae8 100644 --- a/src/renderer/views/Settings/About/About.module.scss +++ b/src/renderer/views/Settings/About/About.module.scss @@ -57,7 +57,6 @@ flex-basis: 0; border-bottom: 1px solid $color-neutral-normal; } - } .description { diff --git a/src/renderer/views/Settings/ChordDisplaySettings/utils.ts b/src/renderer/views/Settings/ChordDisplaySettings/utils.ts index 9f5906b..afe6d40 100644 --- a/src/renderer/views/Settings/ChordDisplaySettings/utils.ts +++ b/src/renderer/views/Settings/ChordDisplaySettings/utils.ts @@ -61,6 +61,7 @@ export const fields = { { value: 'none', label: 'None' }, { value: 'pitchClass', label: 'Pitch Class' }, { value: 'note', label: 'Note' }, + { value: 'chordNote', label: 'Note in Chord' }, { value: 'interval', label: 'Interval' }, ], },