Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87f64e6574 | ||
|
|
496c831552 | ||
|
|
c5adffb4d7 | ||
|
|
536db4cd15 | ||
|
|
761c730129 | ||
|
|
11e0ba2f93 | ||
|
|
4a92d2f1c1 | ||
|
|
99e694161f | ||
|
|
3d1d45cd49 | ||
|
|
309dd89a8c | ||
|
|
699f49bfa3 | ||
|
|
29bebb3ec7 | ||
|
|
7a47282165 | ||
|
|
2b85b276eb | ||
|
|
0a1bf16d18 | ||
|
|
10259d013f | ||
|
|
e61307a302 | ||
|
|
cb340c53a3 | ||
|
|
3b77c12114 | ||
|
|
b8ed79f48a | ||
|
|
d3a9f1ddba | ||
|
|
18ed2e6f05 | ||
|
|
3b829fce07 | ||
|
|
e77c67bc24 | ||
|
|
14c331ab6e | ||
|
|
5c536786ea | ||
|
|
2e1fce4669 | ||
|
|
d759064373 | ||
|
|
f40e8206f8 | ||
|
|
d2e289e5d5 | ||
|
|
a59db41dc5 | ||
|
|
554309ebd8 | ||
|
|
be38b92647 | ||
|
|
b497d16fd8 |
@@ -42,6 +42,7 @@
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"jsx-a11y/media-has-caption": "off",
|
||||
"jsx-a11y/no-noninteractive-tabindex": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
@@ -77,7 +78,8 @@
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"react/no-unknown-property": "off",
|
||||
"react/jsx-key": "off"
|
||||
"react/jsx-key": "off",
|
||||
"react/jsx-no-undef": "off"
|
||||
},
|
||||
"globals": {
|
||||
"Astro": "readonly"
|
||||
|
||||
156
CHANGELOG.md
@@ -2,6 +2,162 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
## [2.0.0](https://github.com/remvze/moodist/compare/v1.5.1...v2.0.0) (2025-03-25)
|
||||
|
||||
|
||||
### ✅ Testing
|
||||
|
||||
* add Vitest and some tests ([def9a57](https://github.com/remvze/moodist/commit/def9a57e0c6454f0e3ffd74b29153a01b33866be))
|
||||
* write more tests ([9cc0ccd](https://github.com/remvze/moodist/commit/9cc0ccd325cf769d64779f133bd2d59e6ba7ca58))
|
||||
* write tests for motion lib ([d356d77](https://github.com/remvze/moodist/commit/d356d77aa951b84a6ccbd0b1c6590286c042957b))
|
||||
* write tests for random helper ([cad85c7](https://github.com/remvze/moodist/commit/cad85c76676cff7fe8c47ccb8d332809f7276e28))
|
||||
|
||||
|
||||
### ⚡️ Performance Improvements
|
||||
|
||||
* improve the breathing cricle ([3d83a14](https://github.com/remvze/moodist/commit/3d83a1427feaec1e858953899870da06d35b4631))
|
||||
|
||||
|
||||
### ♻️ Code Refactoring
|
||||
|
||||
* add description for events ([2c8135d](https://github.com/remvze/moodist/commit/2c8135db43b1a1dad789277926af0d1be3e987fc))
|
||||
* add JSDoc for custom hooks ([0f50e6a](https://github.com/remvze/moodist/commit/0f50e6ae8b3d1615ed52fb168a48bbb2149090ac))
|
||||
* add JSDoc for helper functions ([4ae0504](https://github.com/remvze/moodist/commit/4ae05049377506f79f5ef9f68fa7cf396d7d0528))
|
||||
* change stores structure ([096251e](https://github.com/remvze/moodist/commit/096251ec0a459efbbe08d88cabab75c4ad775976))
|
||||
* refactor the breathing tool ([d56f8be](https://github.com/remvze/moodist/commit/d56f8be448aa746874c38ba0cc7e00e38d339f59))
|
||||
* relocate folders ([f3cea66](https://github.com/remvze/moodist/commit/f3cea668470ca06b2114a03b54660475cc560d44))
|
||||
* remove extra hook ([a4a31dd](https://github.com/remvze/moodist/commit/a4a31dd43eef5c3e1d2b62cf4bb6e491e382f988))
|
||||
* remove the timer store ([5ffb06b](https://github.com/remvze/moodist/commit/5ffb06be036acb1fe5d8fa4b91e4cbede39ebcc0))
|
||||
* rename components ([d73b2bc](https://github.com/remvze/moodist/commit/d73b2bc1ff7689ff85c6453710b2d89927973066))
|
||||
* rename stores folder ([2a86a88](https://github.com/remvze/moodist/commit/2a86a88ed6a232c4a8c2a10bbb06f586361f732d))
|
||||
* separate the migration ([c35409c](https://github.com/remvze/moodist/commit/c35409ce0a95d8376f0d84c96ed0975c9f3a1301))
|
||||
* use the ID instead of index ([7658842](https://github.com/remvze/moodist/commit/7658842324a92210a6a612c70c5479c6bb7f3c05))
|
||||
* write JSDoc for libs ([fddf75c](https://github.com/remvze/moodist/commit/fddf75cdca1f121160f9054c82a7a1ddedd6f2fa))
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* add active indicator for sleep timer ([82d8240](https://github.com/remvze/moodist/commit/82d8240b9708a9d522f67ae305dc44e004ced6de))
|
||||
* add animation for labels ([48a85b2](https://github.com/remvze/moodist/commit/48a85b26016a8f3cc934e1b2298b0d897ffd9b43))
|
||||
* add basic form ([c272914](https://github.com/remvze/moodist/commit/c27291441625eb6528b28f55af3f88e1debd8a55))
|
||||
* add binary animation ([699f49b](https://github.com/remvze/moodist/commit/699f49bfa33420698962b56db23b49c8e14bb354))
|
||||
* add binaural beat generator without styles ([f40e820](https://github.com/remvze/moodist/commit/f40e8206f8126f1988e0e39ca522ac3c5eb8139f))
|
||||
* add breathing exercise ([1f2b6b9](https://github.com/remvze/moodist/commit/1f2b6b952c65c04828f19506134d783a7491df23))
|
||||
* add breathing exercise shortcut ([a3b794d](https://github.com/remvze/moodist/commit/a3b794d9748d4a9877e5727269178f207fbc03d5))
|
||||
* add breathing exercises and other tools ([eee7553](https://github.com/remvze/moodist/commit/eee755378a14d93d1363e8c265a908d50b9cc332))
|
||||
* add breathing exercises tool ([27f2578](https://github.com/remvze/moodist/commit/27f25785e1cfc0482d7ddd625ac1219fd5bb6863))
|
||||
* add cipher animation ([29bebb3](https://github.com/remvze/moodist/commit/29bebb3ec74d969fb42968696e470db00a07766e))
|
||||
* add confetti ([ace0d6e](https://github.com/remvze/moodist/commit/ace0d6eeccc65c96275a24c8a96e63988cf76134))
|
||||
* add countdown timer ([edd53d8](https://github.com/remvze/moodist/commit/edd53d8102871d53b0a11eaa9bae7323f874d988))
|
||||
* add countdown timer structure ([c5657d0](https://github.com/remvze/moodist/commit/c5657d06425aea84a4ba9a4b2f48e312be8b0271))
|
||||
* add custom checkbox ([cb340c5](https://github.com/remvze/moodist/commit/cb340c53a39917722137a8ee05b779af04a1203d))
|
||||
* add custom slider ([3b77c12](https://github.com/remvze/moodist/commit/3b77c12114e5e37c0a3a17c945a0e69e034a35a4))
|
||||
* add desktop notice ([07f37ef](https://github.com/remvze/moodist/commit/07f37ef17f8be893d3ceba8fbe4427a9ecda5c15))
|
||||
* add done counter ([aa8161a](https://github.com/remvze/moodist/commit/aa8161aac5eb238048c713500a091e9af1c98e6a))
|
||||
* add global volume ([3b829fc](https://github.com/remvze/moodist/commit/3b829fce07ed7adf11ca9993c33e33caab285763))
|
||||
* add header to todos ([c6cc61a](https://github.com/remvze/moodist/commit/c6cc61a17fcb8542ece3caccc0de536d8003b106))
|
||||
* add ID to presets ([78222be](https://github.com/remvze/moodist/commit/78222be011cf93998faed0b7926a5b49dcdeb470))
|
||||
* add isochronic tone generator without styles ([d759064](https://github.com/remvze/moodist/commit/d759064373fe791f641db39549e05341068ae8a2))
|
||||
* add lofi radios ([bb39b4b](https://github.com/remvze/moodist/commit/bb39b4ba98f20da13e1e7a440441f5474a823f32))
|
||||
* add Moodist description to tools ([5b3972b](https://github.com/remvze/moodist/commit/5b3972b3470f3c43903d9a20925ed49321f07440))
|
||||
* add more sounds ([d2e289e](https://github.com/remvze/moodist/commit/d2e289e5d5cccd050ca94860f05f00740b3cf139))
|
||||
* add more sounds ([554309e](https://github.com/remvze/moodist/commit/554309ebd87da2bce4555f09e5c9f34735d0b794))
|
||||
* add more sounds ([be38b92](https://github.com/remvze/moodist/commit/be38b92647209ce17032987b3d6f5d1800322db5))
|
||||
* add more sounds ([b497d16](https://github.com/remvze/moodist/commit/b497d16fd8b7d6ccf34c0c91b596fca75dff2f34))
|
||||
* add move up and down functionality ([3e11fb6](https://github.com/remvze/moodist/commit/3e11fb6123e4c6b6be9668ef4c274390a5acd16a))
|
||||
* add new logo ([c1ece58](https://github.com/remvze/moodist/commit/c1ece582f445906308a0d856181ebaca464ec25a))
|
||||
* add notepad tool ([a80289d](https://github.com/remvze/moodist/commit/a80289db57c1b002edd586b323444d3a474587ad))
|
||||
* add notepad tool page ([1fd02f9](https://github.com/remvze/moodist/commit/1fd02f927c55155ecd8d1af6325995c4635e0a29))
|
||||
* add persist mode to the modal ([4c0f417](https://github.com/remvze/moodist/commit/4c0f417469fb15adbe33cab9bb66459225653e68))
|
||||
* add pomodoro timer ([d2edeb4](https://github.com/remvze/moodist/commit/d2edeb48becef62f1002359a41ebe8ebfa1f34bb))
|
||||
* add pomodoro timer tool ([bee391a](https://github.com/remvze/moodist/commit/bee391acfecdaf36488c48ef1022b16a83059d58))
|
||||
* add PWA ([761c730](https://github.com/remvze/moodist/commit/761c7301295a3e5645326be804225431f823f808))
|
||||
* add reverse timer ([105f53e](https://github.com/remvze/moodist/commit/105f53ea028fadae4bd2ff7d8a1856e94f070b1a))
|
||||
* add shortcut for breathing exercise ([60cb453](https://github.com/remvze/moodist/commit/60cb453847f0968a4d1abc0fbb66773a54ebdfd9))
|
||||
* add simple breathing exercise tool ([fc4f521](https://github.com/remvze/moodist/commit/fc4f52146e2142a0c711b6d6a334c0107b1e1daa))
|
||||
* add store to the notepad ([47a63a7](https://github.com/remvze/moodist/commit/47a63a774ebede5db65f17a29a36f0b76d9ed85a))
|
||||
* add timer for breathing exercises ([5865fc8](https://github.com/remvze/moodist/commit/5865fc867dc97e03d0f0c79ea8c465e0c0f27411))
|
||||
* better heading ([10259d0](https://github.com/remvze/moodist/commit/10259d013f7cb1ae41808f7a78e836ddee3b07f1))
|
||||
* bring back all tools ([6a4dc1e](https://github.com/remvze/moodist/commit/6a4dc1ed95072c402cb553fa5b1becb646062c45))
|
||||
* bring back all tools ([e1de5c4](https://github.com/remvze/moodist/commit/e1de5c48b299e815f071f15c00424ba1b0189419))
|
||||
* change and add shortcuts ([a59db41](https://github.com/remvze/moodist/commit/a59db41dc5eaa7be5ab86c5cc407274eb7b57dfe))
|
||||
* change logos ([3d1d45c](https://github.com/remvze/moodist/commit/3d1d45cd4933335cfbe20381c0e758969a3bdcb9))
|
||||
* change shortcuts ([4f45279](https://github.com/remvze/moodist/commit/4f45279938f60ee6934c3e6047898b9833c2b9c6))
|
||||
* change shortcuts ([251f309](https://github.com/remvze/moodist/commit/251f30930c72a50120412c6b2182fdf4183b9d62))
|
||||
* fix modal and scrollbar layout shift ([e399673](https://github.com/remvze/moodist/commit/e3996734621b33c0598db29e82371f1258396147))
|
||||
* implement countdown timer functionality ([2bfb9b1](https://github.com/remvze/moodist/commit/2bfb9b181c490c9836e2410199e6a1cf8687e7aa))
|
||||
* media session support ([18ed2e6](https://github.com/remvze/moodist/commit/18ed2e6f055d7e32b4a9df33cdb724eaf1f930aa))
|
||||
* remove all extra tools ([973e0df](https://github.com/remvze/moodist/commit/973e0df6fb3a6749fd4b0f8d1cd976c67a7e8c43))
|
||||
* remove all tools ([2bbdc7e](https://github.com/remvze/moodist/commit/2bbdc7e09e053bd6e8bb052abb7aff723cb14eaa))
|
||||
* remove all tools ([b32d8b2](https://github.com/remvze/moodist/commit/b32d8b28034e018eeaf1c544e4128b91f4a95172))
|
||||
* remove lofi modal ([13d26b3](https://github.com/remvze/moodist/commit/13d26b3337b2e79d52c774807795b5924a4dcb76))
|
||||
* remove pre-made binaurals ([b8ed79f](https://github.com/remvze/moodist/commit/b8ed79f48ad2a315b93aedf1f932b6c5f075b157))
|
||||
* remove the breathing exercises ([76fdc74](https://github.com/remvze/moodist/commit/76fdc747100bc15ced92b77b1fefc8cba519d37f))
|
||||
* remove the countdown timer ([d6ed3fd](https://github.com/remvze/moodist/commit/d6ed3fd251df029100caba5df304996e723acd78))
|
||||
* replace reverse timer ([a6c7ac4](https://github.com/remvze/moodist/commit/a6c7ac41ad5210b9a98e0fe62f5cb387fe9c4e9a))
|
||||
* scroll into view after marking favorite ([74f6b58](https://github.com/remvze/moodist/commit/74f6b5851d3a0fac5f97d97cd24f12507c2c3b35))
|
||||
* scroll the new timer into view ([f4c66e3](https://github.com/remvze/moodist/commit/f4c66e309277414951b191e627b1f52aab79af6f))
|
||||
* update the menu items ([1768ba1](https://github.com/remvze/moodist/commit/1768ba1548a444c57dbfd5e351d77838238aed0d))
|
||||
* use custom slider in binaural and isochronic ([e61307a](https://github.com/remvze/moodist/commit/e61307a30263dca8cc016ec5136d52c4b18e5c3c))
|
||||
|
||||
|
||||
### 💄 Styling
|
||||
|
||||
* add animation to presets ([787a9b6](https://github.com/remvze/moodist/commit/787a9b60b51334ec2a7423d489f71c305661039e))
|
||||
* add binary pattern ([ba3cd5c](https://github.com/remvze/moodist/commit/ba3cd5ca5be8435f32b93d5a499e37388340bff8))
|
||||
* add focus state ([af075b3](https://github.com/remvze/moodist/commit/af075b32e64a6ab923d60282558250b79cc12da3))
|
||||
* add min width ([18987cc](https://github.com/remvze/moodist/commit/18987cc33997c7b010aea2d4f1546ddcabe1a46b))
|
||||
* add pattern ([69eb883](https://github.com/remvze/moodist/commit/69eb8832dae026706f76ba21a74fcb248ba4309d))
|
||||
* add style to generators ([5c53678](https://github.com/remvze/moodist/commit/5c536786ea64e9722a67289ab2d7e56e7a259404))
|
||||
* add title to timer ([a3c384d](https://github.com/remvze/moodist/commit/a3c384d1054b81e056265eecd9344496c9b0b5ce))
|
||||
* center icons ([1cf9a85](https://github.com/remvze/moodist/commit/1cf9a85e13d50d3c5335dfb78fa57543ce6fda44))
|
||||
* change border radius ([5c9a2aa](https://github.com/remvze/moodist/commit/5c9a2aa23aa04f9386e7d7ac9a20759a2ed87acc))
|
||||
* change button style ([8a79ccf](https://github.com/remvze/moodist/commit/8a79ccf018cd7ee86b27b8bd187975376abea953))
|
||||
* change description ([9208663](https://github.com/remvze/moodist/commit/9208663050c340fdecf486b4835d30353852fd22))
|
||||
* change gradient ([9e38a8f](https://github.com/remvze/moodist/commit/9e38a8fd7da2d68c8c04c4c21cbda6444e9e247b))
|
||||
* change icons ([2e1fce4](https://github.com/remvze/moodist/commit/2e1fce46695b693c4b6aa11f18506e2f2cd9bb59))
|
||||
* change item order ([9198315](https://github.com/remvze/moodist/commit/919831538fea639eb60c8fb84fa93a79ec2cd9c5))
|
||||
* change logo ([4a92d2f](https://github.com/remvze/moodist/commit/4a92d2f1c12c12b4166500149937be51e6442f71))
|
||||
* change logo color ([4b01501](https://github.com/remvze/moodist/commit/4b015016e7c531afc3f3b1f51d62bf96232e3ea8))
|
||||
* change notice ([9d1d8f8](https://github.com/remvze/moodist/commit/9d1d8f80359097b9122673564d3d57c0827ff3db))
|
||||
* change other assets ([11e0ba2](https://github.com/remvze/moodist/commit/11e0ba2f938fc08984e4acba1ba6b4ac3239cacf))
|
||||
* fix pointer event ([12d3255](https://github.com/remvze/moodist/commit/12d3255d57083ff72ae919b6161922620dc1d6e2))
|
||||
* increase menu width ([96ca376](https://github.com/remvze/moodist/commit/96ca3768856806bbe761e773d5ef626dcd12c968))
|
||||
* minor change ([302a71c](https://github.com/remvze/moodist/commit/302a71cdc6472dd29d75372ddc6a3ef214dd68c4))
|
||||
* minor change ([b73fd0b](https://github.com/remvze/moodist/commit/b73fd0b16e57140350d0743aa98ec6933bdc5c64))
|
||||
* minor changes ([536db4c](https://github.com/remvze/moodist/commit/536db4cd156cb391a0b1ef9bf3e4fbbac06ccc11))
|
||||
* minor changes ([7f3ac26](https://github.com/remvze/moodist/commit/7f3ac26b982e629eef891f706004eca5f14e11c4))
|
||||
* minor changes ([4cc8597](https://github.com/remvze/moodist/commit/4cc85975e54cfd8195596e017c351a227184806b))
|
||||
* minor changes ([b27f24d](https://github.com/remvze/moodist/commit/b27f24d37484a04495a043170ccaf4b4923b31ac))
|
||||
* minor changes ([a29e2c2](https://github.com/remvze/moodist/commit/a29e2c20e4bac276495b409b20a6ffaa079122e2))
|
||||
* remove animation on change ([41845ff](https://github.com/remvze/moodist/commit/41845ffe5e282c07b3c4cdea56607f1668c636bd))
|
||||
* remove animations ([28abc16](https://github.com/remvze/moodist/commit/28abc16b9cbbc3986f7fb3feb17e57e553cda5dd))
|
||||
* remove pointer event ([c12ef12](https://github.com/remvze/moodist/commit/c12ef12b79c6db93c457b77f4bfccb2848dc8067))
|
||||
* reorder menu items ([0052b91](https://github.com/remvze/moodist/commit/0052b917a817ca7f83fe23521077d99ae78e81cd))
|
||||
|
||||
|
||||
### 🚚 Chores
|
||||
|
||||
* add animation to countdown timer ([73a5c21](https://github.com/remvze/moodist/commit/73a5c21be918e1e105214078eaef8d76b168333b))
|
||||
* add library sound ([309dd89](https://github.com/remvze/moodist/commit/309dd89a8c13eb2647217c81d7fc0a82eb3ebaae))
|
||||
* add toolbox copy ([cfd2744](https://github.com/remvze/moodist/commit/cfd2744e92b7a2948597a750275bf9c900248d55))
|
||||
* comment out the banner ([c5adffb](https://github.com/remvze/moodist/commit/c5adffb4d777eda1e2a092e382c1cac616dd60f1))
|
||||
* update logos ([7a47282](https://github.com/remvze/moodist/commit/7a472821652d1359126568836b3040ce1fa454c5))
|
||||
* update logos ([2b85b27](https://github.com/remvze/moodist/commit/2b85b276eb11d862bf1abd1e6f099740d9b85c10))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* add default value ([14c331a](https://github.com/remvze/moodist/commit/14c331ab6e692ea3fcdaa056e32728f0a1cd2772))
|
||||
* better implement shortcuts ([e77c67b](https://github.com/remvze/moodist/commit/e77c67bc24f1831bb6de80a4335c51e5b84009ed))
|
||||
* change icon path ([09c0a6c](https://github.com/remvze/moodist/commit/09c0a6ce93f8b0f62149928218532201e0de16c5))
|
||||
* change shortcuts ([edd15f4](https://github.com/remvze/moodist/commit/edd15f4b9a0291b9794102fbb41048de10b0fd69))
|
||||
* correct link ([496c831](https://github.com/remvze/moodist/commit/496c831552442047d5556376a212698c8931b698))
|
||||
* disable the sleep timer when no sound is selected ([d42eb25](https://github.com/remvze/moodist/commit/d42eb25f7be64b5e77cd0bacd1538949d331aff7))
|
||||
* icons path ([1a1359c](https://github.com/remvze/moodist/commit/1a1359c989268a22cfdba20f198af192726ac2ce))
|
||||
* remove dropdown menu item from slider ([99e6941](https://github.com/remvze/moodist/commit/99e694161f16a3be03cbda0854687a244df42f21))
|
||||
* remove extra hook ([3ef4a07](https://github.com/remvze/moodist/commit/3ef4a076a2b48911d37f75067dc60ea15dd28405))
|
||||
|
||||
### [1.5.1](https://github.com/remvze/moodist/compare/v1.5.0...v1.5.1) (2024-06-14)
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<div align="center">
|
||||
<img src="/assets/banner.svg" alt="Moodist Logo Banner" />
|
||||
<!-- <img src="/assets/banner.svg" alt="Moodist Logo Banner" /> -->
|
||||
<h2>Moodist 🌲</h2>
|
||||
<p>Ambient sounds for focus and calm.</p>
|
||||
<a href="https://moodist.app">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
|
||||
<a href="https://moodist.mvze.net">Visit <strong>Moodist</strong></a> | <a href="https://buymeacoffee.com/remvze">Buy Me a Coffee</a>
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
@@ -24,6 +24,7 @@
|
||||
1. 📓 Notepad for quick notes.
|
||||
1. 🍅 Pomodoro timer.
|
||||
1. ✅ Simple to-do list (soon).
|
||||
1. ⏯️ Media controls.
|
||||
1. ⌨️ Keyboard shortcuts for everything.
|
||||
1. 🥷 Privacy focused: no data collection.
|
||||
1. 💰 Completely free, open-source, and self-hostable.
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import react from '@astrojs/react';
|
||||
import AstroPWA from '@vite-pwa/astro';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [react()],
|
||||
integrations: [
|
||||
react(),
|
||||
AstroPWA({
|
||||
manifest: {
|
||||
background_color: '#09090b',
|
||||
description: 'Ambient sounds for focus and calm.',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
...[72, 128, 144, 152, 192, 256, 512].map(size => ({
|
||||
sizes: `${size}x${size}`,
|
||||
src: `/assets/pwa/${size}.png`,
|
||||
type: 'image/png',
|
||||
})),
|
||||
],
|
||||
name: 'Moodist',
|
||||
orientation: 'any',
|
||||
scope: '/',
|
||||
short_name: 'Moodist',
|
||||
start_url: '/',
|
||||
theme_color: '#09090b',
|
||||
},
|
||||
registerType: 'prompt',
|
||||
workbox: {
|
||||
globPatterns: ['**/*'],
|
||||
maximumFileSizeToCacheInBytes: Number.MAX_SAFE_INTEGER,
|
||||
navigateFallback: '/',
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
1491
package-lock.json
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "moodist",
|
||||
"type": "module",
|
||||
"version": "1.5.1",
|
||||
"version": "2.0.0",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
@@ -27,11 +27,14 @@
|
||||
"@astrojs/react": "3.6.0",
|
||||
"@floating-ui/react": "0.26.0",
|
||||
"@formkit/auto-animate": "0.8.2",
|
||||
"@radix-ui/react-checkbox": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.0.6",
|
||||
"@radix-ui/react-slider": "1.2.3",
|
||||
"@radix-ui/react-tooltip": "1.0.7",
|
||||
"@types/howler": "2.2.10",
|
||||
"@types/react": "^18.2.25",
|
||||
"@types/react-dom": "^18.2.10",
|
||||
"@vite-pwa/astro": "0.5.0",
|
||||
"astro": "4.10.3",
|
||||
"deepmerge": "4.3.1",
|
||||
"focus-trap-react": "10.2.3",
|
||||
|
||||
BIN
public/assets/pwa/128.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/pwa/144.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
public/assets/pwa/152.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
public/assets/pwa/192.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/assets/pwa/256.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
public/assets/pwa/512.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/assets/pwa/72.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,12 +1,4 @@
|
||||
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="128" height="128" rx="25" fill="#09090B"/>
|
||||
<path d="M64 19C76.4264 19 86.5 29.0736 86.5 41.5H75.25C75.25 35.2868 70.2132 30.25 64 30.25C57.7868 30.25 52.75 35.2868 52.75 41.5L41.5 41.5C41.5 29.0736 51.5736 19 64 19Z" fill="white"/>
|
||||
<path d="M41.5 86.5C29.0736 86.5 19 76.4264 19 64C19 51.5736 29.0736 41.5 41.5 41.5V52.75C35.2868 52.75 30.25 57.7868 30.25 64C30.25 70.2132 35.2868 75.25 41.5 75.25V86.5Z" fill="white"/>
|
||||
<path d="M86.5 86.5C86.5 98.9264 76.4264 109 64 109C51.5736 109 41.5 98.9264 41.5 86.5H52.75C52.75 92.7132 57.7868 97.75 64 97.75C70.2132 97.75 75.25 92.7132 75.25 86.5H86.5Z" fill="white"/>
|
||||
<path d="M86.5 86.5C98.9264 86.5 109 76.4264 109 64C109 51.5736 98.9264 41.5 86.5 41.5V52.75C92.7132 52.75 97.75 57.7868 97.75 64C97.75 70.2132 92.7132 75.25 86.5 75.25V86.5Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64 86.5C76.4264 86.5 86.5 76.4264 86.5 64C86.5 51.5736 76.4264 41.5 64 41.5C51.5736 41.5 41.5 51.5736 41.5 64C41.5 76.4264 51.5736 86.5 64 86.5ZM64 75.25C70.2132 75.25 75.25 70.2132 75.25 64C75.25 57.7868 70.2132 52.75 64 52.75C57.7868 52.75 52.75 57.7868 52.75 64C52.75 70.2132 57.7868 75.25 64 75.25Z" fill="white"/>
|
||||
<path d="M30.25 41.5C30.25 35.2868 35.2868 30.25 41.5 30.25V19C29.0736 19 19 29.0736 19 41.5H30.25Z" fill="white"/>
|
||||
<path d="M97.75 41.5C97.75 35.2868 92.7132 30.25 86.5 30.25V19C98.9264 19 109 29.0736 109 41.5H97.75Z" fill="white"/>
|
||||
<path d="M97.75 86.5C97.75 92.7132 92.7132 97.75 86.5 97.75V109C98.9264 109 109 98.9264 109 86.5H97.75Z" fill="white"/>
|
||||
<path d="M30.25 86.5C30.25 92.7132 35.2868 97.75 41.5 97.75V109C29.0736 109 19 98.9264 19 86.5H30.25Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M51.2493 29.2516C58.2929 22.2495 69.7071 22.2495 76.7507 29.2516C77.1507 29.6492 77.5279 30.0608 77.8825 30.4848C78.433 30.4357 78.9908 30.4114 79.5547 30.4131C89.4866 30.4424 97.5576 38.5135 97.587 48.4453C97.5886 49.0092 97.5643 49.567 97.5152 50.1176C97.9392 50.4721 98.3508 50.8493 98.7484 51.2493C105.751 58.2929 105.751 69.7071 98.7484 76.7507C98.3508 77.1507 97.9392 77.5279 97.5152 77.8825C97.5643 78.433 97.5886 78.9908 97.587 79.5547C97.5576 89.4866 89.4865 97.5577 79.5547 97.587C78.9908 97.5886 78.433 97.5643 77.8824 97.5152C77.5279 97.9392 77.1506 98.3508 76.7507 98.7484C69.7071 105.751 58.2929 105.751 51.2493 98.7484C50.8493 98.3508 50.4721 97.9392 50.1175 97.5152C49.567 97.5643 49.0092 97.5886 48.4453 97.5869C38.5134 97.5576 30.4424 89.4865 30.413 79.5547C30.4114 78.9908 30.4357 78.433 30.4848 77.8824C30.0608 77.5279 29.6492 77.1506 29.2516 76.7507C22.2495 69.7071 22.2495 58.2929 29.2516 51.2493C29.6492 50.8493 30.0608 50.4721 30.4848 50.1175C30.4357 49.567 30.4114 49.0092 30.4131 48.4453C30.4424 38.5134 38.5135 30.4423 48.4453 30.413C49.0092 30.4114 49.567 30.4356 50.1176 30.4848C50.4721 30.0608 50.8494 29.6492 51.2493 29.2516ZM47.29 35.173C40.877 35.7508 35.7508 40.8769 35.1731 47.29C38.8003 45.8063 42.8126 45.5647 46.5652 46.5652C45.5647 42.8126 45.8063 38.8002 47.29 35.173ZM53.7297 50.3922C50.2143 46.1861 49.7248 40.3267 52.2613 35.6603C57.3546 37.1664 61.1517 41.6557 61.64 47.1156C61.6129 47.529 61.5997 47.9462 61.6009 48.3669L61.6302 58.2787L54.6421 51.2493C54.3456 50.951 54.0412 50.6653 53.7297 50.3922ZM50.3922 53.7297C46.1861 50.2143 40.3268 49.7248 35.6603 52.2613C37.1665 57.3546 41.6558 61.1517 47.1157 61.64C47.5291 61.6128 47.9462 61.5996 48.3668 61.6009L58.2787 61.6302L51.2493 54.6421C50.951 54.3456 50.6653 54.0412 50.3922 53.7297ZM39.3435 64C35.9825 62.0539 33.3162 59.046 31.8005 55.432C27.6743 60.3752 27.6743 67.6247 31.8005 72.5679C33.3162 68.954 35.9825 65.946 39.3435 64ZM35.6603 75.7387C37.1664 70.6454 41.6558 66.8483 47.1157 66.36C47.5291 66.3871 47.9463 66.4003 48.3669 66.3991L58.2787 66.3698L51.2493 73.3579C50.951 73.6544 50.6653 73.9588 50.3922 74.2703C46.1861 77.7857 40.3268 78.2752 35.6603 75.7387ZM35.1731 80.71C35.7508 87.123 40.8769 92.2492 47.29 92.8269C45.8063 89.1997 45.5647 85.1874 46.5652 81.4348C42.8126 82.4353 38.8003 82.1937 35.1731 80.71ZM53.7297 77.6078C50.2143 81.8138 49.7248 87.6732 52.2613 92.3397C57.3546 90.8336 61.1516 86.3443 61.64 80.8844C61.6128 80.471 61.5996 80.0538 61.6009 79.6332L61.6302 69.7213L54.6421 76.7507C54.3456 77.049 54.0412 77.3347 53.7297 77.6078ZM64 88.6565C62.0539 92.0175 59.046 94.6838 55.4321 96.1995C60.3753 100.326 67.6247 100.326 72.5679 96.1995C68.954 94.6838 65.946 92.0175 64 88.6565ZM75.7387 92.3397C70.6454 90.8336 66.8483 86.3443 66.36 80.8844C66.3871 80.471 66.4004 80.0538 66.3991 79.6331L66.3699 69.7213L73.3579 76.7507C73.6544 77.049 73.9588 77.3347 74.2703 77.6078C77.7857 81.8139 78.2752 87.6733 75.7387 92.3397ZM80.71 92.827C87.1231 92.2492 92.2492 87.1231 92.8269 80.71C89.1997 82.1937 85.1874 82.4353 81.4348 81.4348C82.4353 85.1874 82.1937 89.1997 80.71 92.827ZM77.6078 74.2703C81.8138 77.7857 87.6732 78.2752 92.3397 75.7387C90.8336 70.6454 86.3442 66.8483 80.8843 66.36C80.471 66.3872 80.0538 66.4004 79.6332 66.3991L69.7213 66.3698L76.7507 73.3579C77.049 73.6544 77.3347 73.9588 77.6078 74.2703ZM88.6565 64C92.0175 65.9461 94.6838 68.954 96.1995 72.568C100.326 67.6248 100.326 60.3753 96.1995 55.4321C94.6838 59.046 92.0175 62.054 88.6565 64ZM92.3397 52.2613C90.8336 57.3546 86.3442 61.1517 80.8843 61.64C80.471 61.6129 80.0538 61.5997 79.6331 61.6009L69.7213 61.6302L76.7507 54.6421C77.049 54.3456 77.3347 54.0412 77.6078 53.7297C81.8139 50.2143 87.6732 49.7248 92.3397 52.2613ZM92.8269 47.29C92.2492 40.877 87.1231 35.7508 80.71 35.1731C82.1937 38.8003 82.4353 42.8126 81.4348 46.5652C85.1874 45.5647 89.1997 45.8063 92.8269 47.29ZM74.2703 50.3922C77.7857 46.1861 78.2752 40.3268 75.7387 35.6603C70.6455 37.1664 66.8484 41.6557 66.36 47.1156C66.3872 47.529 66.4004 47.9462 66.3991 48.3668L66.3699 58.2787L73.3579 51.2493C73.6544 50.951 73.9588 50.6653 74.2703 50.3922ZM64 39.3435C62.054 35.9825 59.046 33.3162 55.4321 31.8005C60.3753 27.6743 67.6248 27.6743 72.568 31.8005C68.954 33.3162 65.9461 35.9825 64 39.3435Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
public/logo-dark.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/logo-light.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
@@ -1,10 +1,3 @@
|
||||
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 64.4661C100 58.6219 101.411 53.1072 103.911 48.2441L103.911 48.2429L103.928 48.2107C105.198 45.7464 106.747 43.4498 108.535 41.3629C112.141 37.1521 116.716 33.7949 121.914 31.6361C121.346 30.9518 120.742 30.2877 120.101 29.6465C116.762 26.3072 112.799 23.9724 108.587 22.642C106.178 24.1973 103.912 25.9549 101.813 27.891C101.691 28.0031 101.571 28.1157 101.45 28.229C100.957 28.6929 100.474 29.1667 100 29.6501C99.5262 29.1667 99.0427 28.6929 98.5497 28.229C96.3499 26.1591 93.9616 24.2873 91.4133 22.642C88.8486 20.9862 86.1219 19.5598 83.2621 18.392C77.5239 16.0487 71.2502 14.7462 64.6755 14.7191C64.6057 14.7188 64.5359 14.7186 64.4661 14.7186C61.0306 14.7186 57.6765 15.0669 54.4371 15.73C35.009 19.7068 19.7068 35.009 15.7299 54.4371C21.5092 50.112 27.9257 47.0293 34.6104 45.1892C37.3473 40.959 40.9589 37.3474 45.1892 34.6104C47.9325 32.8355 50.936 31.4284 54.1299 30.4589C57.4006 29.4661 60.871 28.9322 64.4661 28.9322C66.6264 28.9322 68.7416 29.125 70.7956 29.4943C73.3293 29.9498 75.7698 30.6739 78.0864 31.6362C83.2841 33.795 87.8585 37.1521 91.4651 41.3629C93.2606 43.4592 94.8163 45.7672 96.0894 48.2441C96.2311 48.5197 96.3692 48.7973 96.5039 49.0771C98.7445 53.7333 100 58.9531 100 64.4661Z" fill="#FAFAFA"/>
|
||||
<path d="M125.126 74.8737C129.259 70.7413 134.156 67.8394 139.362 66.1682C139.558 66.1053 139.755 66.0441 139.952 65.9846C142.418 65.2401 144.949 64.7693 147.498 64.5724C153.025 64.1451 158.634 65.0059 163.836 67.1547C163.919 66.2697 163.961 65.3729 163.961 64.4661C163.961 59.7437 162.81 55.2903 160.772 51.3713C157.934 50.7602 155.053 50.3994 152.163 50.289C152.034 50.2841 151.905 50.2796 151.775 50.2757C151.099 50.2551 150.422 50.2482 149.745 50.2551C149.752 49.5783 149.745 48.9014 149.724 48.2248C149.632 45.2056 149.267 42.1933 148.629 39.228C147.986 36.2436 147.067 33.3069 145.87 30.4589C143.47 24.7444 139.954 19.3872 135.325 14.7191C135.275 14.6695 135.226 14.6201 135.177 14.5707C132.748 12.1414 130.13 10.0159 127.37 8.1942C110.82 -2.7314 89.1798 -2.7314 72.63 8.19421C79.7748 9.2225 86.4918 11.5799 92.5196 15.0055C97.4462 13.9496 102.554 13.9496 107.48 15.0055C110.675 15.6902 113.794 16.8191 116.738 18.3919C119.753 20.0027 122.584 22.0791 125.126 24.6212C126.654 26.1487 128.013 27.7808 129.204 29.4942C130.674 31.608 131.888 33.8457 132.845 36.1642C134.994 41.366 135.855 46.9746 135.428 52.5022C135.215 55.2542 134.683 57.9862 133.832 60.6379C133.737 60.9329 133.638 61.2269 133.536 61.5199C131.828 66.3968 129.025 70.9754 125.126 74.8737Z" fill="#FAFAFA"/>
|
||||
<path d="M135.534 100C141.378 100 146.893 101.411 151.756 103.911L151.789 103.928C154.254 105.198 156.55 106.747 158.637 108.535C162.848 112.141 166.205 116.716 168.364 121.914C169.048 121.346 169.712 120.742 170.354 120.101C173.693 116.762 176.028 112.799 177.358 108.587C175.713 106.038 173.841 103.65 171.771 101.45C171.307 100.957 170.833 100.474 170.35 100C170.833 99.5263 171.307 99.0427 171.771 98.5497C173.841 96.3499 175.713 93.9616 177.358 91.4133C179.014 88.8486 180.44 86.1218 181.608 83.262C183.951 77.5238 185.254 71.2501 185.281 64.6754C185.281 64.6057 185.281 64.5359 185.281 64.4661C185.281 61.0306 184.933 57.6764 184.27 54.437C180.293 35.009 164.991 19.7069 145.563 15.73C149.888 21.5092 152.971 27.9258 154.811 34.6104C159.041 37.3474 162.653 40.959 165.39 45.1892C167.165 47.9325 168.572 50.936 169.541 54.1299C170.534 57.4006 171.068 60.871 171.068 64.4661C171.068 66.6264 170.875 68.7416 170.506 70.7955C170.05 73.3293 169.326 75.7697 168.364 78.0864C166.205 83.2841 162.848 87.8586 158.637 91.4651C156.541 93.2607 154.233 94.8163 151.756 96.0894C151.48 96.2311 151.203 96.3693 150.923 96.5039C146.267 98.7445 141.047 100 135.534 100Z" fill="#FAFAFA"/>
|
||||
<path d="M125.126 125.126C129.259 129.259 132.161 134.156 133.832 139.362C133.895 139.558 133.956 139.755 134.015 139.952C134.76 142.418 135.231 144.949 135.428 147.498C135.855 153.025 134.994 158.634 132.845 163.836C133.73 163.919 134.627 163.961 135.534 163.961C140.256 163.961 144.71 162.81 148.629 160.772C149.24 157.934 149.601 155.052 149.711 152.163C149.716 152.034 149.72 151.905 149.724 151.775C149.745 151.099 149.752 150.422 149.745 149.745C150.422 149.752 151.099 149.745 151.775 149.724C154.794 149.632 157.807 149.267 160.772 148.629C163.756 147.986 166.693 147.066 169.541 145.87C175.256 143.47 180.613 139.954 185.281 135.325C185.33 135.275 185.38 135.226 185.429 135.177C187.859 132.748 189.984 130.13 191.806 127.37C202.731 110.82 202.731 89.1797 191.806 72.6299C190.777 79.7747 188.42 86.4917 184.995 92.5196C186.05 97.4462 186.05 102.554 184.995 107.48C184.31 110.675 183.181 113.794 181.608 116.738C179.997 119.753 177.921 122.584 175.379 125.126C173.851 126.654 172.219 128.013 170.506 129.204C168.392 130.674 166.154 131.888 163.836 132.845C158.634 134.994 153.025 135.855 147.498 135.428C144.746 135.215 142.014 134.683 139.362 133.832C139.067 133.737 138.773 133.638 138.48 133.536C133.603 131.828 129.025 129.025 125.126 125.126Z" fill="#FAFAFA"/>
|
||||
<path d="M100 135.534C100 141.378 98.5891 146.893 96.0894 151.756L96.0723 151.789C94.8023 154.254 93.2526 156.55 91.4651 158.637C87.8585 162.848 83.284 166.205 78.0864 168.364C78.6536 169.048 79.2578 169.712 79.899 170.354C83.2382 173.693 87.2014 176.028 91.4133 177.358C93.9478 175.722 96.324 173.861 98.514 171.805L98.5497 171.771C99.0427 171.307 99.5262 170.833 100 170.35C100.474 170.833 100.957 171.307 101.45 171.771C103.65 173.841 106.038 175.713 108.587 177.358C111.151 179.014 113.878 180.44 116.738 181.608C122.476 183.951 128.75 185.254 135.325 185.281C135.394 185.281 135.464 185.281 135.534 185.281C138.969 185.281 142.324 184.933 145.563 184.27C164.991 180.293 180.293 164.991 184.27 145.563C178.491 149.888 172.074 152.971 165.39 154.811C162.653 159.041 159.041 162.653 154.811 165.39C152.068 167.164 149.064 168.572 145.87 169.541C142.599 170.534 139.129 171.068 135.534 171.068C133.374 171.068 131.258 170.875 129.204 170.506C126.671 170.05 124.23 169.326 121.914 168.364C116.716 166.205 112.141 162.848 108.535 158.637C106.739 156.541 105.184 154.233 103.911 151.756C103.769 151.48 103.631 151.203 103.496 150.923C101.255 146.267 100 141.047 100 135.534Z" fill="#FAFAFA"/>
|
||||
<path d="M74.8737 125.126C70.7413 129.259 65.8442 132.161 60.6379 133.832C60.4418 133.895 60.2453 133.956 60.0483 134.015C57.582 134.76 55.0507 135.231 52.5022 135.428C46.9745 135.855 41.366 134.994 36.1642 132.845C36.0813 133.73 36.0389 134.627 36.0389 135.534C36.0389 140.256 37.1904 144.71 39.2279 148.629C42.1932 149.267 45.2056 149.632 48.2247 149.724C48.9013 149.745 49.5783 149.752 50.2551 149.745C50.2483 150.422 50.2551 151.099 50.2757 151.775C50.3676 154.794 50.7328 157.807 51.3714 160.772C52.014 163.756 52.9335 166.693 54.1299 169.541C56.5304 175.256 60.0456 180.613 64.6755 185.281C64.7246 185.33 64.7739 185.38 64.8232 185.429C67.2525 187.859 69.8705 189.984 72.63 191.806C89.1798 202.731 110.82 202.731 127.37 191.806C120.225 190.778 113.508 188.42 107.48 184.995C102.554 186.05 97.4462 186.05 92.5196 184.994C89.3248 184.31 86.206 183.181 83.2621 181.608C80.2473 179.997 77.4159 177.921 74.8737 175.379C73.3462 173.851 71.9868 172.219 70.7956 170.506C69.3261 168.392 68.1125 166.154 67.1547 163.836C65.0059 158.634 64.1451 153.025 64.5724 147.498C64.7851 144.746 65.317 142.014 66.1682 139.362C66.2629 139.067 66.3616 138.773 66.4642 138.48C68.1723 133.603 70.9755 129.025 74.8737 125.126Z" fill="#FAFAFA"/>
|
||||
<path d="M41.3629 108.535C43.4592 106.739 45.7671 105.184 48.2441 103.911C48.5197 103.769 48.7973 103.631 49.0771 103.496C53.7333 101.255 58.9531 100 64.4661 100C58.6219 100 53.1072 98.5891 48.2441 96.0894L48.2428 96.0888L48.2107 96.0723C45.7464 94.8023 43.4498 93.2526 41.3629 91.4651C37.1521 87.8586 33.7949 83.2841 31.6361 78.0864C30.9517 78.6536 30.2877 79.2578 29.6465 79.899C26.3072 83.2382 23.9724 87.2015 22.642 91.4134C23.7775 93.1722 25.021 94.8547 26.363 96.4517C26.9654 97.1686 27.5877 97.8682 28.2289 98.5498C28.6928 99.0427 29.1666 99.5263 29.6501 100C29.1666 100.474 28.6928 100.957 28.2289 101.45C26.1591 103.65 24.2873 106.038 22.642 108.587C20.9862 111.151 19.5598 113.878 18.3919 116.738C16.0486 122.476 14.7461 128.75 14.719 135.324C14.7187 135.394 14.7186 135.464 14.7186 135.534C14.7186 138.969 15.0668 142.324 15.7299 145.563C19.7068 164.991 35.009 180.293 54.437 184.27C50.112 178.491 47.0293 172.074 45.1892 165.39C40.959 162.653 37.3474 159.041 34.6104 154.811C32.8354 152.067 31.4283 149.064 30.4588 145.87C29.466 142.599 28.9321 139.129 28.9321 135.534C28.9321 133.374 29.1249 131.258 29.4942 129.204C29.9498 126.671 30.6739 124.23 31.6361 121.914C33.7949 116.716 37.1521 112.141 41.3629 108.535Z" fill="#FAFAFA"/>
|
||||
<path d="M52.5022 64.5724C55.2542 64.7851 57.9862 65.317 60.6379 66.1682C60.9329 66.2629 61.227 66.3616 61.5199 66.4642C66.3968 68.1723 70.9755 70.9754 74.8737 74.8737C70.7413 70.7413 67.8394 65.8442 66.1682 60.6379C66.1053 60.4418 66.0441 60.2453 65.9846 60.0483C65.2401 57.582 64.7694 55.0507 64.5724 52.5022C64.1451 46.9746 65.0059 41.366 67.1547 36.1642C66.2697 36.0813 65.3729 36.039 64.4661 36.039C59.7437 36.039 55.2903 37.1904 51.3714 39.2279C50.7328 42.1933 50.3676 45.2056 50.2757 48.2248C50.2551 48.9014 50.2483 49.5783 50.2551 50.2551C49.5783 50.2482 48.9014 50.2551 48.2248 50.2757C45.2056 50.3676 42.1933 50.7328 39.2279 51.3714C36.2435 52.014 33.3068 52.9335 30.4589 54.1299C24.7444 56.5305 19.3872 60.0457 14.719 64.6755C14.6695 64.7246 14.6201 64.7739 14.5707 64.8232C12.1414 67.2525 10.0159 69.8705 8.19417 72.63C-2.73139 89.1798 -2.73139 110.82 8.19418 127.37C9.22247 120.225 11.5798 113.508 15.0055 107.48C13.9496 102.554 13.9496 97.4463 15.0055 92.5197C15.6902 89.3248 16.8191 86.2061 18.3919 83.2621C20.0027 80.2473 22.0791 77.4159 24.6212 74.8737C26.1487 73.3462 27.7807 71.9868 29.4942 70.7956C31.6079 69.3261 33.8457 68.1125 36.1642 67.1547C41.366 65.0059 46.9745 64.1451 52.5022 64.5724Z" fill="#FAFAFA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M68.1232 13.129C85.7322 -4.37634 114.268 -4.37634 131.877 13.129C132.877 14.1229 133.82 15.1519 134.706 16.212C136.083 16.0892 137.477 16.0285 138.887 16.0326C163.716 16.1059 183.894 36.2836 183.967 61.1133C183.972 62.5231 183.911 63.9175 183.788 65.2939C184.848 66.1803 185.877 67.1234 186.871 68.1232C204.376 85.7322 204.376 114.268 186.871 131.877C185.877 132.877 184.848 133.82 183.788 134.706C183.911 136.083 183.972 137.477 183.967 138.887C183.894 163.716 163.716 183.894 138.887 183.967C137.477 183.972 136.082 183.911 134.706 183.788C133.82 184.848 132.877 185.877 131.877 186.871C114.268 204.376 85.7322 204.376 68.1232 186.871C67.1234 185.877 66.1803 184.848 65.2939 183.788C63.9175 183.911 62.523 183.972 61.1133 183.967C36.2836 183.894 16.1059 163.716 16.0326 138.887C16.0284 137.477 16.0892 136.082 16.212 134.706C15.1519 133.82 14.1229 132.877 13.129 131.877C-4.37634 114.268 -4.37634 85.7322 13.129 68.1232C14.1229 67.1234 15.1519 66.1803 16.212 65.2939C16.0892 63.9175 16.0285 62.523 16.0326 61.1132C16.1059 36.2836 36.2837 16.1059 61.1133 16.0325C62.5231 16.0284 63.9176 16.0891 65.294 16.2119C66.1803 15.1519 67.1234 14.1229 68.1232 13.129ZM58.225 27.9326C42.1924 29.3769 29.377 42.1923 27.9327 58.2249C37.0007 54.5157 47.0315 53.9118 56.413 56.413C53.9118 47.0315 54.5158 37.0006 58.225 27.9326ZM74.3243 65.9805C65.5357 55.4653 64.3121 40.8169 70.6533 29.1507C83.3865 32.916 92.8792 44.1393 94.1001 57.789C94.0322 58.8224 93.9991 59.8655 94.0023 60.9171L94.0754 85.6967L76.6053 68.1232C75.8639 67.3774 75.103 66.6632 74.3243 65.9805ZM65.9805 74.3243C55.4654 65.5357 40.8169 64.3121 29.1508 70.6533C32.9161 83.3864 44.1395 92.8792 57.7893 94.1C58.8226 94.0321 59.8656 93.9991 60.9171 94.0022L85.6967 94.0754L68.1232 76.6053C67.3774 75.8639 66.6632 75.103 65.9805 74.3243ZM38.3587 99.9999C29.9563 95.1348 23.2906 87.6149 19.5013 78.5801C9.18585 90.9381 9.18584 109.062 19.5013 121.42C23.2905 112.385 29.9563 104.865 38.3587 99.9999ZM29.1508 129.347C32.9161 116.613 44.1394 107.121 57.7893 105.9C58.8227 105.968 59.8656 106.001 60.9171 105.998L85.6968 105.925L68.1232 123.395C67.3774 124.136 66.6631 124.897 65.9805 125.676C55.4653 134.464 40.8169 135.688 29.1508 129.347ZM27.9327 141.775C29.377 157.808 42.1924 170.623 58.2249 172.067C54.5157 162.999 53.9118 152.969 56.413 143.587C47.0315 146.088 37.0007 145.484 27.9327 141.775ZM74.3243 134.019C65.5357 144.535 64.3121 159.183 70.6533 170.849C83.3864 167.084 92.8791 155.861 94.1001 142.211C94.0321 141.178 93.9991 140.135 94.0022 139.083L94.0754 114.303L76.6053 131.877C75.8639 132.623 75.103 133.337 74.3243 134.019ZM100 161.641C95.1349 170.044 87.6149 176.709 78.5801 180.499C90.9381 190.814 109.062 190.814 121.42 180.499C112.385 176.71 104.865 170.044 100 161.641ZM129.347 170.849C116.614 167.084 107.121 155.861 105.9 142.211C105.968 141.178 106.001 140.134 105.998 139.083L105.925 114.303L123.395 131.877C124.136 132.623 124.897 133.337 125.676 134.019C134.464 144.535 135.688 159.183 129.347 170.849ZM141.775 172.067C157.808 170.623 170.623 157.808 172.067 141.775C162.999 145.484 152.969 146.088 143.587 143.587C146.088 152.969 145.484 162.999 141.775 172.067ZM134.019 125.676C144.535 134.464 159.183 135.688 170.849 129.347C167.084 116.614 155.861 107.121 142.211 105.9C141.177 105.968 140.134 106.001 139.083 105.998L114.303 105.925L131.877 123.395C132.623 124.136 133.337 124.897 134.019 125.676ZM161.641 100C170.044 104.865 176.709 112.385 180.499 121.42C190.814 109.062 190.814 90.9382 180.499 78.5802C176.709 87.615 170.044 95.135 161.641 100ZM170.849 70.6533C167.084 83.3865 155.861 92.8793 142.211 94.1001C141.177 94.0322 140.134 93.9992 139.083 94.0023L114.303 94.0754L131.877 76.6053C132.623 75.8639 133.337 75.103 134.019 74.3243C144.535 65.5357 159.183 64.3121 170.849 70.6533ZM172.067 58.2249C170.623 42.1924 157.808 29.377 141.775 27.9327C145.484 37.0007 146.088 47.0315 143.587 56.413C152.969 53.9118 162.999 54.5157 172.067 58.2249ZM125.676 65.9805C134.464 55.4653 135.688 40.8169 129.347 29.1508C116.614 32.9161 107.121 44.1393 105.9 57.789C105.968 58.8224 106.001 59.8655 105.998 60.9171L105.925 85.6967L123.395 68.1232C124.136 67.3774 124.897 66.6631 125.676 65.9805ZM100 38.3587C95.135 29.9563 87.615 23.2905 78.5802 19.5012C90.9382 9.18585 109.062 9.18587 121.42 19.5013C112.385 23.2905 104.865 29.9563 100 38.3587Z" fill="#FAFAFA"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 4.4 KiB |
BIN
public/og.png
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 13 KiB |
BIN
public/sounds/animals/beehive.mp3
Normal file
BIN
public/sounds/animals/chickens.mp3
Normal file
BIN
public/sounds/animals/cows.mp3
Normal file
BIN
public/sounds/animals/sheep.mp3
Normal file
BIN
public/sounds/animals/woodpecker.mp3
Normal file
BIN
public/sounds/nature/walk-on-gravel.mp3
Normal file
BIN
public/sounds/places/library.mp3
Normal file
BIN
public/sounds/places/restaurant.mp3
Normal file
BIN
public/sounds/rain/rain-on-car-roof.mp3
Normal file
BIN
public/sounds/things/vinyl-effect.mp3
Normal file
BIN
public/sounds/things/windshield-wipers.mp3
Normal file
@@ -12,6 +12,7 @@ import { Categories } from '@/components/categories';
|
||||
import { SharedModal } from '@/components/modals/shared';
|
||||
import { Toolbar } from '@/components/toolbar';
|
||||
import { SnackbarProvider } from '@/contexts/snackbar';
|
||||
import { MediaControls } from '@/components/media-controls';
|
||||
|
||||
import { sounds } from '@/data/sounds';
|
||||
import { FADE_OUT } from '@/constants/events';
|
||||
@@ -88,6 +89,7 @@ export function App() {
|
||||
return (
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
|
||||
17
src/components/binary.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { generateRandomBinaryString } from '@/helpers/binary';
|
||||
|
||||
export function Binary() {
|
||||
const [binary, setBinary] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setBinary(generateRandomBinaryString(1000));
|
||||
|
||||
setInterval(() => {
|
||||
setBinary(generateRandomBinaryString(1000));
|
||||
}, 200);
|
||||
}, []);
|
||||
|
||||
return <span>{binary}</span>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { BiPause, BiPlay } from 'react-icons/bi/index';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { useSnackbar } from '@/contexts/snackbar';
|
||||
@@ -28,17 +29,7 @@ export function PlayButton() {
|
||||
if (isPlaying && noSelected) pause();
|
||||
}, [isPlaying, pause, noSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey && e.key === ' ') {
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', listener);
|
||||
|
||||
return () => document.removeEventListener('keydown', listener);
|
||||
}, [handleToggle]);
|
||||
useHotkeys('shift+space', handleToggle, {}, [handleToggle]);
|
||||
|
||||
return (
|
||||
<button
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { BiUndo, BiTrash } from 'react-icons/bi/index';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
|
||||
@@ -28,17 +29,7 @@ export function UnselectButton() {
|
||||
else if (!noSelected) unselectAll(true);
|
||||
}, [hasHistory, noSelected, unselectAll, restoreHistory, locked]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: KeyboardEvent) => {
|
||||
if (e.shiftKey && e.key === 'R') {
|
||||
handleToggle();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', listener);
|
||||
|
||||
return () => document.removeEventListener('keydown', listener);
|
||||
}, [handleToggle]);
|
||||
useHotkeys('shift+r', handleToggle, {}, [handleToggle]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
23
src/components/checkbox/checkbox.module.css
Normal file
@@ -0,0 +1,23 @@
|
||||
.checkboxRoot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
background: var(--color-neutral-100);
|
||||
border: 2px solid var(--color-neutral-300);
|
||||
border-radius: 4px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.checkboxRoot[data-state='checked'] {
|
||||
background: var(--color-neutral-950);
|
||||
border: 2px solid var(--color-neutral-950);
|
||||
}
|
||||
|
||||
.checkboxIndicator {
|
||||
font-size: var(--font-2xsm);
|
||||
color: var(--color-neutral-50);
|
||||
transform: translateY(2px);
|
||||
}
|
||||
38
src/components/checkbox/checkbox.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as RadixCheckbox from '@radix-ui/react-checkbox';
|
||||
import { FaCheck } from 'react-icons/fa6/index';
|
||||
|
||||
import styles from './checkbox.module.css';
|
||||
|
||||
type CheckboxInputProps = {
|
||||
checked?: boolean;
|
||||
className?: string;
|
||||
defaultChecked?: boolean;
|
||||
disabled?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
};
|
||||
|
||||
export function Checkbox({
|
||||
checked,
|
||||
className,
|
||||
defaultChecked = false,
|
||||
disabled = false,
|
||||
onChange,
|
||||
}: CheckboxInputProps) {
|
||||
const handleCheckedChange = (checked: boolean) => {
|
||||
if (onChange) onChange(checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadixCheckbox.Root
|
||||
checked={checked}
|
||||
className={`${styles.checkboxRoot} ${className}`}
|
||||
defaultChecked={defaultChecked}
|
||||
disabled={disabled}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
>
|
||||
<RadixCheckbox.Indicator className={styles.checkboxIndicator}>
|
||||
<FaCheck />
|
||||
</RadixCheckbox.Indicator>
|
||||
</RadixCheckbox.Root>
|
||||
);
|
||||
}
|
||||
1
src/components/checkbox/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Checkbox } from './checkbox';
|
||||
59
src/components/cipher.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface CipherTextProps {
|
||||
interval?: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const chars = '-_~`!@#$%^&*()+=[]{}|;:,.<>?';
|
||||
|
||||
export function CipherText({ interval = 50, text }: CipherTextProps) {
|
||||
const [outputText, setOutputText] = useState('');
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
|
||||
if (outputText !== text) {
|
||||
timer = setInterval(() => {
|
||||
if (outputText.length < text.length) {
|
||||
setOutputText(prev => prev + text[prev.length]);
|
||||
} else {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, interval);
|
||||
}
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [text, interval, outputText]);
|
||||
|
||||
useEffect(() => {
|
||||
if (outputText === text) {
|
||||
setTimeout(() => setOutputText(''), 6000);
|
||||
}
|
||||
}, [outputText, text]);
|
||||
|
||||
const remainder =
|
||||
outputText.length < text.length
|
||||
? text
|
||||
.slice(outputText.length)
|
||||
.split('')
|
||||
.map(() => chars[Math.floor(Math.random() * chars.length)])
|
||||
.join('')
|
||||
: '';
|
||||
|
||||
if (!isMounted) {
|
||||
return <span>{text}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-white">
|
||||
{outputText}
|
||||
{remainder}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { BsSoundwave } from 'react-icons/bs/index';
|
||||
|
||||
import { Container } from './container';
|
||||
import { CipherText } from './cipher';
|
||||
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
|
||||
@@ -12,17 +13,23 @@ const count = soundCount();
|
||||
<Container>
|
||||
<div class="wrapper">
|
||||
<div class="pattern"></div>
|
||||
<img
|
||||
alt="Faded Moodist Logo"
|
||||
aria-hidden="true"
|
||||
class="logo"
|
||||
height={45}
|
||||
src="/logo.svg"
|
||||
width={45}
|
||||
/>
|
||||
<div class="logo-wrapper">
|
||||
<img
|
||||
alt="Faded Moodist Logo"
|
||||
aria-hidden="true"
|
||||
class="logo"
|
||||
height={48}
|
||||
src="/logo.svg"
|
||||
width={48}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2 class="title"><span>Moodist</span></h2>
|
||||
<h1 class="desc">Ambient sounds for focus and calm.</h1>
|
||||
<h1 class="title">
|
||||
Ambient Sounds<span class="line">For Focus and Calm</span>
|
||||
</h1>
|
||||
<h2 class="desc">
|
||||
Free and <CipherText client:load text="Open-Source" />.
|
||||
</h2>
|
||||
|
||||
<p class="sounds">
|
||||
<span aria-hidden="true" class="icon">
|
||||
@@ -60,23 +67,28 @@ const count = soundCount();
|
||||
}
|
||||
}
|
||||
|
||||
& .logo {
|
||||
display: block;
|
||||
width: 45px;
|
||||
margin: 0 auto 16px;
|
||||
animation-name: logo;
|
||||
animation-duration: 45s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
& .logo-wrapper {
|
||||
mask-image: linear-gradient(#000, rgb(0 0 0 / 40%), rgb(0 0 0 / 5%));
|
||||
|
||||
& .logo {
|
||||
display: block;
|
||||
width: 48px;
|
||||
margin: 0 auto 20px;
|
||||
opacity: 1;
|
||||
animation-name: logo;
|
||||
animation-duration: 60s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-2xlg);
|
||||
font-size: var(--font-xlg);
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
|
||||
& span {
|
||||
/* & .gradient {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-foreground),
|
||||
@@ -84,11 +96,22 @@ const count = soundCount();
|
||||
);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
} */
|
||||
|
||||
& .line {
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
background: linear-gradient(
|
||||
var(--color-foreground-subtler),
|
||||
var(--color-foreground-subtle)
|
||||
);
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
& .desc {
|
||||
margin-top: 4px;
|
||||
margin-top: 12px;
|
||||
line-height: 1.6;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
1
src/components/media-controls/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MediaControls } from './media-controls';
|
||||
20
src/components/media-controls/media-controls.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MediaSessionTrack } from './media-session-track';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSSR } from '@/hooks/use-ssr';
|
||||
|
||||
export function MediaControls() {
|
||||
const [mediaControlsEnabled, setMediaControlsEnabled] = useState(false);
|
||||
const { isBrowser } = useSSR();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser) return;
|
||||
|
||||
setMediaControlsEnabled('mediaSession' in navigator);
|
||||
}, [isBrowser]);
|
||||
|
||||
if (!mediaControlsEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <MediaSessionTrack />;
|
||||
}
|
||||
104
src/components/media-controls/media-session-track.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { getSilenceDataURL } from '@/helpers/sound';
|
||||
import { BrowserDetect } from '@/helpers/browser-detect';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import { useSSR } from '@/hooks/use-ssr';
|
||||
import { useDarkTheme } from '@/hooks/use-dark-theme';
|
||||
|
||||
const metadata: MediaMetadataInit = {
|
||||
artist: 'Moodist',
|
||||
title: 'Ambient Sounds for Focus and Calm',
|
||||
};
|
||||
|
||||
export function MediaSessionTrack() {
|
||||
const { isBrowser } = useSSR();
|
||||
const isDarkTheme = useDarkTheme();
|
||||
const [isGenerated, setIsGenerated] = useState(false);
|
||||
const isPlaying = useSoundStore(state => state.isPlaying);
|
||||
const play = useSoundStore(state => state.play);
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
const masterAudioSoundRef = useRef<HTMLAudioElement>(null);
|
||||
const artworkURL = isDarkTheme ? '/logo-dark.png' : '/logo-light.png';
|
||||
|
||||
const generateSilence = useCallback(async () => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
masterAudioSoundRef.current.src = await getSilenceDataURL();
|
||||
setIsGenerated(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser || !isPlaying || !isGenerated) return;
|
||||
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
...metadata,
|
||||
artwork: [
|
||||
{
|
||||
sizes: '200x200',
|
||||
src: artworkURL,
|
||||
type: 'image/png',
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [artworkURL, isBrowser, isDarkTheme, isGenerated, isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
generateSilence();
|
||||
}, [generateSilence]);
|
||||
|
||||
const startMasterAudio = useCallback(async () => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
if (!masterAudioSoundRef.current.paused) return;
|
||||
|
||||
try {
|
||||
await masterAudioSoundRef.current.play();
|
||||
|
||||
navigator.mediaSession.playbackState = 'playing';
|
||||
navigator.mediaSession.setActionHandler('play', play);
|
||||
navigator.mediaSession.setActionHandler('pause', pause);
|
||||
} catch {
|
||||
// Do nothing
|
||||
}
|
||||
}, [pause, play]);
|
||||
|
||||
const stopMasterAudio = useCallback(() => {
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
/**
|
||||
* Otherwise in Safari we cannot play the audio again
|
||||
* through the media session controls
|
||||
*/
|
||||
if (BrowserDetect.isSafari()) {
|
||||
masterAudioSoundRef.current.load();
|
||||
} else {
|
||||
masterAudioSoundRef.current.pause();
|
||||
}
|
||||
navigator.mediaSession.playbackState = 'paused';
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGenerated) return;
|
||||
if (!masterAudioSoundRef.current) return;
|
||||
|
||||
if (isPlaying) {
|
||||
startMasterAudio();
|
||||
} else {
|
||||
stopMasterAudio();
|
||||
}
|
||||
}, [isGenerated, isPlaying, startMasterAudio, stopMasterAudio]);
|
||||
|
||||
useEffect(() => {
|
||||
const masterAudioSound = masterAudioSoundRef.current;
|
||||
|
||||
return () => {
|
||||
masterAudioSound?.pause();
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', null);
|
||||
navigator.mediaSession.setActionHandler('pause', null);
|
||||
navigator.mediaSession.playbackState = 'none';
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <audio id="media-session-track" loop ref={masterAudioSoundRef} />;
|
||||
}
|
||||
76
src/components/modals/binaural/binaural.module.css
Normal file
@@ -0,0 +1,76 @@
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
& .title {
|
||||
margin-bottom: 4px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .desc {
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
.fieldWrapper {
|
||||
margin-bottom: 12px;
|
||||
|
||||
& label {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
|
||||
& input,
|
||||
select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 45px;
|
||||
padding: 0 8px;
|
||||
margin-top: 4px;
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& .volume {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
& button {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 45px;
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-200);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--color-neutral-50);
|
||||
background-color: var(--color-neutral-950);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
src/components/modals/binaural/binaural.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
import styles from './binaural.module.css';
|
||||
|
||||
interface BinauralProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
interface Preset {
|
||||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const presets: Preset[] = [
|
||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
||||
];
|
||||
|
||||
export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
|
||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
|
||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
|
||||
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const leftOscillatorRef = useRef<OscillatorNode | null>(null);
|
||||
const rightOscillatorRef = useRef<OscillatorNode | null>(null);
|
||||
const gainNodeRef = useRef<GainNode | null>(null);
|
||||
|
||||
const startSound = () => {
|
||||
if (isPlaying) return;
|
||||
|
||||
// Initialize the AudioContext
|
||||
audioContextRef.current = new window.AudioContext();
|
||||
const audioContext = audioContextRef.current;
|
||||
|
||||
if (!audioContext) return;
|
||||
|
||||
// Create a gain node for volume control
|
||||
gainNodeRef.current = audioContext.createGain();
|
||||
gainNodeRef.current.gain.value = volume; // Set volume based on state
|
||||
|
||||
// Create oscillators for left and right channels
|
||||
leftOscillatorRef.current = audioContext.createOscillator();
|
||||
rightOscillatorRef.current = audioContext.createOscillator();
|
||||
|
||||
if (
|
||||
!leftOscillatorRef.current ||
|
||||
!rightOscillatorRef.current ||
|
||||
!gainNodeRef.current
|
||||
)
|
||||
return;
|
||||
|
||||
leftOscillatorRef.current.frequency.value =
|
||||
baseFrequency - beatFrequency / 2;
|
||||
rightOscillatorRef.current.frequency.value =
|
||||
baseFrequency + beatFrequency / 2;
|
||||
|
||||
// Pan oscillators to left and right
|
||||
const leftPanner = audioContext.createStereoPanner();
|
||||
leftPanner.pan.value = -1;
|
||||
|
||||
const rightPanner = audioContext.createStereoPanner();
|
||||
rightPanner.pan.value = 1;
|
||||
|
||||
// Connect nodes
|
||||
leftOscillatorRef.current.connect(leftPanner).connect(gainNodeRef.current);
|
||||
rightOscillatorRef.current
|
||||
.connect(rightPanner)
|
||||
.connect(gainNodeRef.current);
|
||||
gainNodeRef.current.connect(audioContext.destination);
|
||||
|
||||
// Start oscillators
|
||||
leftOscillatorRef.current.start();
|
||||
rightOscillatorRef.current.start();
|
||||
|
||||
setIsPlaying(true);
|
||||
};
|
||||
|
||||
const stopSound = useCallback(() => {
|
||||
if (!isPlaying) return;
|
||||
|
||||
leftOscillatorRef.current?.stop();
|
||||
rightOscillatorRef.current?.stop();
|
||||
audioContextRef.current?.close();
|
||||
|
||||
setIsPlaying(false);
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update gain node when volume changes
|
||||
if (gainNodeRef.current) {
|
||||
gainNodeRef.current.gain.value = volume;
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup when component unmounts
|
||||
return () => {
|
||||
if (isPlaying) {
|
||||
stopSound();
|
||||
}
|
||||
};
|
||||
}, [isPlaying, stopSound]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update frequencies when a preset is selected
|
||||
if (selectedPreset !== 'Custom') {
|
||||
const preset = presets.find(p => p.name === selectedPreset);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
}
|
||||
}
|
||||
}, [selectedPreset]);
|
||||
|
||||
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selected = e.target.value;
|
||||
setSelectedPreset(selected);
|
||||
|
||||
if (selected === 'Custom') {
|
||||
// Allow user to input custom frequencies
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets.find(p => p.name === selected);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Binaural Beat</h2>
|
||||
<p className={styles.desc}>Binaural beat generator.</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{selectedPreset === 'Custom' && (
|
||||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
<input
|
||||
max="1500"
|
||||
min="20"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={baseFrequency}
|
||||
onChange={e =>
|
||||
setBaseFrequency(parseFloat(e.target.value || '0'))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Beat Frequency (Hz):
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={beatFrequency}
|
||||
onChange={e =>
|
||||
setBeatFrequency(parseFloat(e.target.value || '0'))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
value={volume}
|
||||
onChange={value => setVolume(value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.primary}
|
||||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
1
src/components/modals/binaural/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { BinauralModal } from './binaural';
|
||||
1
src/components/modals/isochronic/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { IsochronicModal } from './isochronic';
|
||||
76
src/components/modals/isochronic/isochornic.module.css
Normal file
@@ -0,0 +1,76 @@
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
|
||||
& .title {
|
||||
margin-bottom: 4px;
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
& .desc {
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
}
|
||||
|
||||
.fieldWrapper {
|
||||
margin-bottom: 12px;
|
||||
|
||||
& label {
|
||||
display: block;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
|
||||
& input,
|
||||
select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 45px;
|
||||
padding: 0 8px;
|
||||
margin-top: 4px;
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-50);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
& .volume {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
|
||||
& button {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 45px;
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-200);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--color-neutral-50);
|
||||
background-color: var(--color-neutral-950);
|
||||
}
|
||||
}
|
||||
}
|
||||
258
src/components/modals/isochronic/isochronic.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
import styles from './isochornic.module.css';
|
||||
|
||||
interface IsochronicProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
}
|
||||
|
||||
interface Preset {
|
||||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const presets: Preset[] = [
|
||||
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' },
|
||||
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' },
|
||||
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' },
|
||||
];
|
||||
|
||||
export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
|
||||
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
|
||||
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
|
||||
const [waveform] = useState<OscillatorType>('sine'); // Default waveform
|
||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
|
||||
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const oscillatorRef = useRef<OscillatorNode | null>(null);
|
||||
const gainNodeRef = useRef<GainNode | null>(null);
|
||||
const beatGainRef = useRef<GainNode | null>(null);
|
||||
const modulatorRef = useRef<OscillatorNode | null>(null);
|
||||
|
||||
const startSound = () => {
|
||||
if (isPlaying) return;
|
||||
|
||||
audioContextRef.current = new window.AudioContext();
|
||||
const audioContext = audioContextRef.current;
|
||||
|
||||
if (!audioContext) return;
|
||||
|
||||
// Main gain node for volume control
|
||||
gainNodeRef.current = audioContext.createGain();
|
||||
gainNodeRef.current.gain.value = volume;
|
||||
|
||||
// Oscillator for the base tone
|
||||
oscillatorRef.current = audioContext.createOscillator();
|
||||
oscillatorRef.current.frequency.value = baseFrequency;
|
||||
oscillatorRef.current.type = waveform;
|
||||
|
||||
// Gain node to create isochronic beats
|
||||
beatGainRef.current = audioContext.createGain();
|
||||
beatGainRef.current.gain.value = 0; // Start with silence
|
||||
|
||||
// Oscillator for modulation
|
||||
modulatorRef.current = audioContext.createOscillator();
|
||||
modulatorRef.current.frequency.value = beatFrequency;
|
||||
modulatorRef.current.type = 'square'; // Square wave for on/off effect
|
||||
|
||||
// Modulator gain to adjust modulation depth
|
||||
const modulatorGain = audioContext.createGain();
|
||||
modulatorGain.gain.value = 0.5; // Modulation depth
|
||||
|
||||
// Connect modulator to the beat gain node
|
||||
modulatorRef.current
|
||||
.connect(modulatorGain)
|
||||
.connect(beatGainRef.current.gain);
|
||||
|
||||
// Connect oscillator through beat gain and main gain to destination
|
||||
oscillatorRef.current
|
||||
.connect(beatGainRef.current)
|
||||
.connect(gainNodeRef.current)
|
||||
.connect(audioContext.destination);
|
||||
|
||||
// Start oscillators
|
||||
oscillatorRef.current.start();
|
||||
modulatorRef.current.start();
|
||||
|
||||
setIsPlaying(true);
|
||||
};
|
||||
|
||||
const stopSound = useCallback(() => {
|
||||
if (!isPlaying) return;
|
||||
|
||||
oscillatorRef.current?.stop();
|
||||
modulatorRef.current?.stop();
|
||||
audioContextRef.current?.close();
|
||||
|
||||
setIsPlaying(false);
|
||||
}, [isPlaying]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update gain when volume changes
|
||||
if (gainNodeRef.current) {
|
||||
gainNodeRef.current.gain.value = volume;
|
||||
}
|
||||
}, [volume]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update base frequency when it changes
|
||||
if (oscillatorRef.current) {
|
||||
oscillatorRef.current.frequency.value = baseFrequency;
|
||||
}
|
||||
}, [baseFrequency]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update beat frequency when it changes
|
||||
if (modulatorRef.current) {
|
||||
modulatorRef.current.frequency.value = beatFrequency;
|
||||
}
|
||||
}, [beatFrequency]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update waveform when it changes
|
||||
if (oscillatorRef.current) {
|
||||
oscillatorRef.current.type = waveform;
|
||||
}
|
||||
}, [waveform]);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup when component unmounts
|
||||
return () => {
|
||||
if (isPlaying) {
|
||||
stopSound();
|
||||
}
|
||||
};
|
||||
}, [isPlaying, stopSound]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update frequencies when a preset is selected
|
||||
if (selectedPreset !== 'Custom') {
|
||||
const preset = presets.find(p => p.name === selectedPreset);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
}
|
||||
}
|
||||
}, [selectedPreset]);
|
||||
|
||||
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selected = e.target.value;
|
||||
setSelectedPreset(selected);
|
||||
|
||||
if (selected === 'Custom') {
|
||||
// Allow user to input custom frequencies
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets.find(p => p.name === selected);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Isochronic Tone</h2>
|
||||
<p className={styles.desc}>Isochronic tone generator.</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{selectedPreset === 'Custom' && (
|
||||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
<input
|
||||
max="2000"
|
||||
min="20"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={baseFrequency}
|
||||
onChange={e =>
|
||||
setBaseFrequency(parseFloat(e.target.value || '0'))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Tone Frequency (Hz):
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
step="0.1"
|
||||
type="number"
|
||||
value={beatFrequency}
|
||||
onChange={e =>
|
||||
setBeatFrequency(parseFloat(e.target.value || '0'))
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{/* <div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Waveform:
|
||||
<select
|
||||
value={waveform}
|
||||
onChange={e => setWaveform(e.target.value as OscillatorType)}
|
||||
>
|
||||
<option value="sine">Sine</option>
|
||||
<option value="square">Square</option>
|
||||
<option value="sawtooth">Sawtooth</option>
|
||||
<option value="triangle">Triangle</option>
|
||||
</select>
|
||||
</label>
|
||||
</div> */}
|
||||
</>
|
||||
)}
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.01}
|
||||
value={volume}
|
||||
onChange={value => setVolume(value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.primary}
|
||||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||
label: 'Shortcuts List',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'P'],
|
||||
keys: ['Shift', 'Alt', 'P'],
|
||||
label: 'Presets',
|
||||
},
|
||||
{
|
||||
@@ -22,9 +22,29 @@ export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||
label: 'Share Sounds',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'T'],
|
||||
keys: ['Shift', 'Alt', 'T'],
|
||||
label: 'Sleep Timer',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'C'],
|
||||
label: 'Countdown Timer',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'P'],
|
||||
label: 'Pomodoro',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'N'],
|
||||
label: 'Notepad',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'T'],
|
||||
label: 'Todo Checklist',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'B'],
|
||||
label: 'Breathing Exercise',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'Space'],
|
||||
label: 'Toggle Play',
|
||||
|
||||
1
src/components/reload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Reload } from './reload';
|
||||
36
src/components/reload/reload-modal.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'; // eslint-disable-line
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './reload.module.css';
|
||||
|
||||
export function ReloadModal() {
|
||||
const {
|
||||
needRefresh: [needRefresh, setNeedRefresh],
|
||||
updateServiceWorker,
|
||||
} = useRegisterSW();
|
||||
|
||||
const close = () => {
|
||||
setNeedRefresh(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal show={needRefresh} onClose={close}>
|
||||
<h2 className={styles.title}>New Content</h2>
|
||||
<p className={styles.desc}>
|
||||
New content available, click on reload button to update.
|
||||
</p>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button onClick={close}>Close</button>
|
||||
|
||||
<button
|
||||
className={styles.primary}
|
||||
onClick={() => updateServiceWorker(true)}
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
38
src/components/reload/reload.module.css
Normal file
@@ -0,0 +1,38 @@
|
||||
.title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: var(--font-md);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin-top: 8px;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
column-gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
|
||||
& button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground-subtle);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-200);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
&.primary {
|
||||
color: var(--color-neutral-50);
|
||||
background-color: var(--color-neutral-950);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/components/reload/reload.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ReloadModal } from './reload-modal';
|
||||
|
||||
export function Reload() {
|
||||
const [isBrowser, setIsBrowser] = useState(false);
|
||||
|
||||
useEffect(() => setIsBrowser(true), []);
|
||||
|
||||
return isBrowser ? <ReloadModal /> : null;
|
||||
}
|
||||
1
src/components/slider/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Slider } from './slider';
|
||||
42
src/components/slider/slider.module.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.sliderRoot {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sliderTrack {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 4px;
|
||||
background: var(--color-neutral-200);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.sliderRange {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: var(--color-neutral-800);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.sliderThumb {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
background: var(--color-neutral-950);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 3px var(--color-neutral-50);
|
||||
}
|
||||
|
||||
.sliderThumb:hover {
|
||||
background: var(--color-neutral-800);
|
||||
}
|
||||
|
||||
.sliderThumb:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--color-neutral-400);
|
||||
}
|
||||
47
src/components/slider/slider.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as RadixSlider from '@radix-ui/react-slider';
|
||||
import styles from './slider.module.css';
|
||||
|
||||
type SliderProps = {
|
||||
className?: string;
|
||||
defaultValue?: number;
|
||||
disabled?: boolean;
|
||||
max?: number;
|
||||
min?: number;
|
||||
onChange?: (value: number) => void;
|
||||
step?: number;
|
||||
value?: number;
|
||||
};
|
||||
|
||||
export function Slider({
|
||||
className,
|
||||
defaultValue = 50,
|
||||
disabled = false,
|
||||
max = 100,
|
||||
min = 0,
|
||||
onChange,
|
||||
step = 1,
|
||||
value,
|
||||
}: SliderProps) {
|
||||
const handleValueChange = (values: number[]) => {
|
||||
if (onChange) onChange(values[0]);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadixSlider.Root
|
||||
className={`${styles.sliderRoot} ${className}`}
|
||||
defaultValue={[defaultValue]}
|
||||
disabled={disabled}
|
||||
max={max}
|
||||
min={min}
|
||||
step={step}
|
||||
tabIndex={0}
|
||||
value={value !== undefined ? [value] : undefined}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<RadixSlider.Track className={styles.sliderTrack}>
|
||||
<RadixSlider.Range className={styles.sliderRange} />
|
||||
</RadixSlider.Track>
|
||||
<RadixSlider.Thumb className={styles.sliderThumb} />
|
||||
</RadixSlider.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, forwardRef } from 'react';
|
||||
import { useCallback, useEffect, forwardRef, useMemo } from 'react';
|
||||
import { ImSpinner9 } from 'react-icons/im/index';
|
||||
|
||||
import { Range } from './range';
|
||||
@@ -31,13 +31,19 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
||||
const selectSound = useSoundStore(state => state.select);
|
||||
const unselectSound = useSoundStore(state => state.unselect);
|
||||
const setVolume = useSoundStore(state => state.setVolume);
|
||||
const volume = useSoundStore(state => state.sounds[id].volume);
|
||||
const isSelected = useSoundStore(state => state.sounds[id].isSelected);
|
||||
const locked = useSoundStore(state => state.locked);
|
||||
|
||||
const volume = useSoundStore(state => state.sounds[id].volume);
|
||||
const globalVolume = useSoundStore(state => state.globalVolume);
|
||||
const adjustedVolume = useMemo(
|
||||
() => volume * globalVolume,
|
||||
[volume, globalVolume],
|
||||
);
|
||||
|
||||
const isLoading = useLoadingStore(state => state.loaders[src]);
|
||||
|
||||
const sound = useSound(src, { loop: true, volume });
|
||||
const sound = useSound(src, { loop: true, volume: adjustedVolume });
|
||||
|
||||
useEffect(() => {
|
||||
if (locked) return;
|
||||
|
||||
@@ -3,10 +3,7 @@ import { FaGithub } from 'react-icons/fa/index';
|
||||
|
||||
import { SpecialButton } from './special-button';
|
||||
import { Container } from './container';
|
||||
|
||||
import { generateRandomBinaryString } from '@/helpers/binary';
|
||||
|
||||
const binary = generateRandomBinaryString(1000);
|
||||
import { Binary } from './binary';
|
||||
---
|
||||
|
||||
<div class="source">
|
||||
@@ -28,7 +25,7 @@ const binary = generateRandomBinaryString(1000);
|
||||
</SpecialButton>
|
||||
</div>
|
||||
|
||||
<div class="binary">{binary}</div>
|
||||
<div class="binary"><Binary client:load /></div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
13
src/components/toolbar/menu/items/binaural.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FaHeadphonesAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
interface BinauralProps {
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export function Binaural({ open }: BinauralProps) {
|
||||
return (
|
||||
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
|
||||
);
|
||||
}
|
||||
@@ -10,3 +10,5 @@ export { Pomodoro as PomodoroItem } from './pomodoro';
|
||||
export { Notepad as NotepadItem } from './notepad';
|
||||
export { Todo as TodoItem } from './todo';
|
||||
export { Countdown as CountdownItem } from './countdown';
|
||||
export { Binaural as BinauralItem } from './binaural';
|
||||
export { Isochronic as IsochronicItem } from './isochronic';
|
||||
|
||||
11
src/components/toolbar/menu/items/isochronic.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TbWaveSine } from 'react-icons/tb/index';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
interface IsochronicProps {
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export function Isochronic({ open }: IsochronicProps) {
|
||||
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function Presets({ open }: PresetsProps) {
|
||||
<Item
|
||||
icon={<RiPlayListFill />}
|
||||
label="Your Presets"
|
||||
shortcut="Shift + P"
|
||||
shortcut="Shift + Alt + P"
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SleepTimer({ open }: SleepTimerProps) {
|
||||
active={active}
|
||||
icon={<IoMoonSharp />}
|
||||
label="Sleep Timer"
|
||||
shortcut="Shift + T"
|
||||
shortcut="Shift + Alt + T"
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -40,3 +40,21 @@
|
||||
border: 1px solid var(--color-neutral-300);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.globalVolume {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
|
||||
& label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: var(--font-sm);
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground-subtle);
|
||||
}
|
||||
|
||||
& input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
NotepadItem,
|
||||
TodoItem,
|
||||
CountdownItem,
|
||||
BinauralItem,
|
||||
IsochronicItem,
|
||||
} from './items';
|
||||
import { Divider } from './divider';
|
||||
import { ShareLinkModal } from '@/components/modals/share-link';
|
||||
@@ -24,7 +26,11 @@ import { PresetsModal } from '@/components/modals/presets';
|
||||
import { ShortcutsModal } from '@/components/modals/shortcuts';
|
||||
import { SleepTimerModal } from '@/components/modals/sleep-timer';
|
||||
import { BreathingExerciseModal } from '@/components/modals/breathing';
|
||||
import { BinauralModal } from '@/components/modals/binaural';
|
||||
import { IsochronicModal } from '@/components/modals/isochronic';
|
||||
import { Pomodoro, Notepad, Todo, Countdown } from '@/components/toolbox';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
import { fade, mix, slideY } from '@/lib/motion';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
@@ -36,11 +42,15 @@ export function Menu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
const globalVolume = useSoundStore(state => state.globalVolume);
|
||||
const setGlobalVolume = useSoundStore(state => state.setGlobalVolume);
|
||||
|
||||
const initial = useMemo(
|
||||
() => ({
|
||||
binaural: false,
|
||||
breathing: false,
|
||||
countdown: false,
|
||||
isochronic: false,
|
||||
notepad: false,
|
||||
pomodoro: false,
|
||||
presets: false,
|
||||
@@ -124,8 +134,23 @@ export function Menu() {
|
||||
<TodoItem open={() => open('todo')} />
|
||||
<BreathingExerciseItem open={() => open('breathing')} />
|
||||
|
||||
<Divider />
|
||||
<BinauralItem open={() => open('binaural')} />
|
||||
<IsochronicItem open={() => open('isochronic')} />
|
||||
|
||||
<Divider />
|
||||
<ShortcutsItem open={() => open('shortcuts')} />
|
||||
<Divider />
|
||||
|
||||
<div className={styles.globalVolume}>
|
||||
<label htmlFor="global-volume">Global Volume</label>
|
||||
<Slider
|
||||
max={100}
|
||||
min={0}
|
||||
value={globalVolume * 100}
|
||||
onChange={value => setGlobalVolume(value / 100)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<DonateItem />
|
||||
@@ -163,6 +188,11 @@ export function Menu() {
|
||||
show={modals.sleepTimer}
|
||||
onClose={() => close('sleepTimer')}
|
||||
/>
|
||||
<BinauralModal show={modals.binaural} onClose={() => close('binaural')} />
|
||||
<IsochronicModal
|
||||
show={modals.isochronic}
|
||||
onClose={() => close('isochronic')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
& button {
|
||||
& .delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FaRegTrashAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { Checkbox } from '@/components/checkbox';
|
||||
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
import { cn } from '@/helpers/styles';
|
||||
|
||||
@@ -21,19 +23,16 @@ export function Todo({ done, id, todo }: TodoProps) {
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<input
|
||||
checked={done}
|
||||
className={styles.checkbox}
|
||||
type="checkbox"
|
||||
onChange={handleCheck}
|
||||
/>
|
||||
<div className={styles.checkbox}>
|
||||
<Checkbox checked={done} onChange={handleCheck} />
|
||||
</div>
|
||||
<input
|
||||
className={cn(styles.textbox, done && styles.done)}
|
||||
type="text"
|
||||
value={todo}
|
||||
onChange={e => editTodo(id, e.target.value)}
|
||||
/>
|
||||
<button onClick={handleDelete}>
|
||||
<button className={styles.delete} onClick={handleDelete}>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,22 +6,11 @@ import { places } from './sounds/places';
|
||||
import { transport } from './sounds/transport';
|
||||
import { things } from './sounds/things';
|
||||
import { noise } from './sounds/noise';
|
||||
import { binaural } from './sounds/binaural';
|
||||
|
||||
import type { Categories } from './types';
|
||||
|
||||
export const sounds: {
|
||||
categories: Categories;
|
||||
} = {
|
||||
categories: [
|
||||
nature,
|
||||
rain,
|
||||
animals,
|
||||
urban,
|
||||
places,
|
||||
transport,
|
||||
things,
|
||||
noise,
|
||||
binaural,
|
||||
],
|
||||
categories: [nature, rain, animals, urban, places, transport, things, noise],
|
||||
};
|
||||
|
||||
@@ -4,6 +4,11 @@ import {
|
||||
GiWolfHead,
|
||||
GiOwl,
|
||||
GiWhaleTail,
|
||||
GiTreeBeehive,
|
||||
GiEgyptianBird,
|
||||
GiChicken,
|
||||
GiCow,
|
||||
GiSheep,
|
||||
} from 'react-icons/gi/index';
|
||||
import {
|
||||
FaDog,
|
||||
@@ -86,6 +91,36 @@ export const animals: Category = {
|
||||
label: 'Whale',
|
||||
src: '/sounds/animals/whale.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiTreeBeehive />,
|
||||
id: 'beehive',
|
||||
label: 'Beehive',
|
||||
src: '/sounds/animals/beehive.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiEgyptianBird />,
|
||||
id: 'woodpecker',
|
||||
label: 'Woodpecker',
|
||||
src: '/sounds/animals/woodpecker.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiChicken />,
|
||||
id: 'chickens',
|
||||
label: 'Chickens',
|
||||
src: '/sounds/animals/chickens.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiCow />,
|
||||
id: 'cows',
|
||||
label: 'Cows',
|
||||
src: '/sounds/animals/cows.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiSheep />,
|
||||
id: 'sheep',
|
||||
label: 'Sheep',
|
||||
src: '/sounds/animals/sheep.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Animals',
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GiWaterfall } from 'react-icons/gi/index';
|
||||
import { GiWaterfall, GiStonePile } from 'react-icons/gi/index';
|
||||
import { BsFire, BsFillDropletFill } from 'react-icons/bs/index';
|
||||
import { BiSolidTree, BiWater } from 'react-icons/bi/index';
|
||||
import {
|
||||
@@ -69,6 +69,12 @@ export const nature: Category = {
|
||||
label: 'Walk on Leaves',
|
||||
src: '/sounds/nature/walk-on-leaves.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiStonePile />,
|
||||
id: 'walk-on-gravel',
|
||||
label: 'Walk on Gravel',
|
||||
src: '/sounds/nature/walk-on-gravel.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsFillDropletFill />,
|
||||
id: 'droplets',
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
} from 'react-icons/md/index';
|
||||
import { HiOfficeBuilding } from 'react-icons/hi/index';
|
||||
import { AiFillExperiment } from 'react-icons/ai/index';
|
||||
import { IoRestaurant } from 'react-icons/io5/index';
|
||||
import { FaBookOpen } from 'react-icons/fa6/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
@@ -104,6 +106,18 @@ export const places: Category = {
|
||||
label: 'Laundry Room',
|
||||
src: '/sounds/places/laundry-room.mp3',
|
||||
},
|
||||
{
|
||||
icon: <IoRestaurant />,
|
||||
id: 'restaurant',
|
||||
label: 'Restaurant',
|
||||
src: '/sounds/places/restaurant.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaBookOpen />,
|
||||
id: 'library',
|
||||
label: 'Library',
|
||||
src: '/sounds/places/library.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Places',
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
BsUmbrellaFill,
|
||||
} from 'react-icons/bs/index';
|
||||
import { GiWindow } from 'react-icons/gi/index';
|
||||
import { FaLeaf } from 'react-icons/fa/index';
|
||||
import { FaLeaf, FaCarSide } from 'react-icons/fa/index';
|
||||
import { PiTentFill } from 'react-icons/pi/index';
|
||||
import { MdOutlineThunderstorm } from 'react-icons/md/index';
|
||||
|
||||
@@ -38,6 +38,12 @@ export const rain: Category = {
|
||||
label: 'Rain on Window',
|
||||
src: '/sounds/rain/rain-on-window.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaCarSide />,
|
||||
id: 'rain-on-car-roof',
|
||||
label: 'Rain on Car Roof',
|
||||
src: '/sounds/rain/rain-on-car-roof.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsUmbrellaFill />,
|
||||
id: 'rain-on-umbrella',
|
||||
|
||||
@@ -6,10 +6,11 @@ import {
|
||||
import { BsFillKeyboardFill } from 'react-icons/bs/index';
|
||||
import { FaKeyboard, FaClock, FaFan } from 'react-icons/fa/index';
|
||||
import { MdSmartToy, MdWaterDrop, MdRadio } from 'react-icons/md/index';
|
||||
import { TbBowlFilled } from 'react-icons/tb/index';
|
||||
import { TbBowlFilled, TbWiper } from 'react-icons/tb/index';
|
||||
import { RiFilePaper2Fill, RiBubbleChartFill } from 'react-icons/ri/index';
|
||||
import { BiSolidDryer } from 'react-icons/bi/index';
|
||||
import { IoIosRadio } from 'react-icons/io/index';
|
||||
import { PiVinylRecord } from 'react-icons/pi/index';
|
||||
|
||||
import type { Category } from '../types';
|
||||
|
||||
@@ -101,6 +102,18 @@ export const things: Category = {
|
||||
label: 'Washing Machine',
|
||||
src: '/sounds/things/washing-machine.mp3',
|
||||
},
|
||||
{
|
||||
icon: <PiVinylRecord />,
|
||||
id: 'vinyl-effect',
|
||||
label: 'Vinyl Effect',
|
||||
src: '/sounds/things/vinyl-effect.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbWiper />,
|
||||
id: 'windshield-wipers',
|
||||
label: 'Windshield Wipers',
|
||||
src: '/sounds/things/windshield-wipers.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Things',
|
||||
};
|
||||
|
||||
16
src/helpers/browser-detect.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export class BrowserDetect {
|
||||
private static _isSafari: boolean | undefined;
|
||||
|
||||
public static isSafari(): boolean {
|
||||
if (typeof BrowserDetect._isSafari !== 'undefined') {
|
||||
return BrowserDetect._isSafari;
|
||||
}
|
||||
|
||||
// Source: https://github.com/goldfire/howler.js/blob/v2.2.4/src/howler.core.js#L270
|
||||
BrowserDetect._isSafari =
|
||||
navigator.userAgent.indexOf('Safari') !== -1 &&
|
||||
navigator.userAgent.indexOf('Chrome') === -1;
|
||||
|
||||
return BrowserDetect._isSafari;
|
||||
}
|
||||
}
|
||||
82
src/helpers/sound.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
function blobToDataURL(blob: Blob) {
|
||||
return new Promise<string>(resolve => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
if (typeof reader.result !== 'string') return;
|
||||
resolve(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function writeString(view: DataView, offset: number, string: string) {
|
||||
for (let i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWAV(audioBuffer: AudioBuffer) {
|
||||
const numChannels = audioBuffer.numberOfChannels;
|
||||
const sampleRate = audioBuffer.sampleRate;
|
||||
const length = audioBuffer.length * numChannels * 2 + 44; // Header + PCM data
|
||||
const wavBuffer = new ArrayBuffer(length);
|
||||
const view = new DataView(wavBuffer);
|
||||
|
||||
// WAV file header
|
||||
writeString(view, 0, 'RIFF');
|
||||
// File size - 8
|
||||
view.setUint32(4, 36 + audioBuffer.length * numChannels * 2, true);
|
||||
writeString(view, 8, 'WAVE');
|
||||
writeString(view, 12, 'fmt ');
|
||||
// Subchunk1Size
|
||||
view.setUint32(16, 16, true);
|
||||
// Audio format (PCM)
|
||||
view.setUint16(20, 1, true);
|
||||
// NumChannels
|
||||
view.setUint16(22, numChannels, true);
|
||||
// SampleRate
|
||||
view.setUint32(24, sampleRate, true);
|
||||
// ByteRate
|
||||
view.setUint32(28, sampleRate * numChannels * 2, true);
|
||||
// BlockAlign
|
||||
view.setUint16(32, numChannels * 2, true);
|
||||
// BitsPerSample
|
||||
view.setUint16(34, 16, true);
|
||||
writeString(view, 36, 'data');
|
||||
// Subchunk2Size
|
||||
view.setUint32(40, audioBuffer.length * numChannels * 2, true);
|
||||
|
||||
// Write interleaved PCM samples
|
||||
let offset = 44;
|
||||
|
||||
for (let i = 0; i < audioBuffer.length; i++) {
|
||||
for (let channel = 0; channel < numChannels; channel++) {
|
||||
const sample = audioBuffer.getChannelData(channel)[i];
|
||||
const clampedSample = Math.max(-1, Math.min(1, sample));
|
||||
view.setInt16(offset, clampedSample * 0x7fff, true);
|
||||
offset += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return wavBuffer;
|
||||
}
|
||||
|
||||
export async function getSilenceDataURL(seconds: number = 60) {
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
const sampleRate = 44100;
|
||||
const length = sampleRate * seconds;
|
||||
const buffer = audioContext.createBuffer(1, length, sampleRate);
|
||||
const channelData = buffer.getChannelData(0);
|
||||
|
||||
/**
|
||||
* - Firefox ignores audio for Media Session without any actual sound in the beginning.
|
||||
* - Add a small value to the end to prevent clipping.
|
||||
*/
|
||||
channelData[0] = 0.001;
|
||||
channelData[channelData.length - 1] = 0.001;
|
||||
|
||||
return await blobToDataURL(
|
||||
new Blob([encodeWAV(buffer)], { type: 'audio/wav' }),
|
||||
);
|
||||
}
|
||||
27
src/hooks/use-dark-theme.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSSR } from './use-ssr';
|
||||
|
||||
const themeMatch = '(prefers-color-scheme: dark)';
|
||||
|
||||
export function useDarkTheme() {
|
||||
const { isBrowser } = useSSR();
|
||||
const [isDarkTheme, setIsDarkTheme] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser) return;
|
||||
|
||||
const themeMediaQuery = window.matchMedia(themeMatch);
|
||||
|
||||
function handleThemeChange(event: MediaQueryListEvent) {
|
||||
setIsDarkTheme(event.matches);
|
||||
}
|
||||
|
||||
themeMediaQuery.addEventListener('change', handleThemeChange);
|
||||
setIsDarkTheme(themeMediaQuery.matches);
|
||||
|
||||
return () =>
|
||||
themeMediaQuery.removeEventListener('change', handleThemeChange);
|
||||
}, [isBrowser]);
|
||||
|
||||
return isDarkTheme;
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
---
|
||||
import { pwaInfo } from 'virtual:pwa-info'; // eslint-disable-line
|
||||
|
||||
import { Reload } from '@/components/reload';
|
||||
|
||||
import { count } from '@/lib/sounds';
|
||||
|
||||
import '@/styles/global.css';
|
||||
@@ -35,8 +39,12 @@ const description =
|
||||
<meta content="https://moodist.app/og.png" property="og:image" />
|
||||
|
||||
<meta content="summary_large_image" name="twitter:card" />
|
||||
|
||||
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
||||
<Reload client:load />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -19,7 +19,10 @@ export const useSoundStore = create<SoundState & SoundActions>()(
|
||||
persisted,
|
||||
),
|
||||
name: 'moodist-sounds',
|
||||
partialize: state => ({ sounds: state.sounds }),
|
||||
partialize: state => ({
|
||||
globalVolume: state.globalVolume,
|
||||
sounds: state.sounds,
|
||||
}),
|
||||
skipHydration: true,
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
version: 0,
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface SoundActions {
|
||||
play: () => void;
|
||||
restoreHistory: () => void;
|
||||
select: (id: string) => void;
|
||||
setGlobalVolume: (volume: number) => void;
|
||||
setVolume: (id: string, volume: number) => void;
|
||||
shuffle: () => void;
|
||||
toggleFavorite: (id: string) => void;
|
||||
@@ -72,6 +73,12 @@ export const createActions: StateCreator<
|
||||
});
|
||||
},
|
||||
|
||||
setGlobalVolume(volume) {
|
||||
set({
|
||||
globalVolume: volume,
|
||||
});
|
||||
},
|
||||
|
||||
setVolume(id, volume) {
|
||||
set({
|
||||
sounds: {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { sounds } from '@/data/sounds';
|
||||
|
||||
export interface SoundState {
|
||||
getFavorites: () => Array<string>;
|
||||
globalVolume: number;
|
||||
history: {
|
||||
[id: string]: {
|
||||
isFavorite: boolean;
|
||||
@@ -39,6 +40,7 @@ export const createState: StateCreator<
|
||||
|
||||
return favorites;
|
||||
},
|
||||
globalVolume: 1,
|
||||
history: null,
|
||||
isPlaying: false,
|
||||
locked: false,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"types": ["vite-plugin-pwa/react", "vite-plugin-pwa/info"]
|
||||
}
|
||||
}
|
||||
|
||||