mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 09:24:14 +00:00
Merge f25c77e56f into b171793040
This commit is contained in:
commit
6f8e434ed6
83 changed files with 2750 additions and 569 deletions
|
|
@ -5,18 +5,22 @@
|
|||
"stylelint-config-html",
|
||||
"stylelint-prettier/recommended"
|
||||
],
|
||||
|
||||
"rules": {
|
||||
"import-notation": "string",
|
||||
"selector-class-pattern": null,
|
||||
"no-descending-specificity": null
|
||||
},
|
||||
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.astro"],
|
||||
"files": ["*.astro", "**/*.astro"],
|
||||
"rules": {
|
||||
"prettier/prettier": null
|
||||
"prettier/prettier": null,
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ import react from '@astrojs/react';
|
|||
import AstroPWA from '@vite-pwa/astro';
|
||||
|
||||
export default defineConfig({
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'zh-CN', 'zh-TW', 'ja'],
|
||||
routing: {
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
integrations: [
|
||||
react(),
|
||||
AstroPWA({
|
||||
|
|
|
|||
80
package-lock.json
generated
80
package-lock.json
generated
|
|
@ -24,10 +24,12 @@
|
|||
"focus-trap-react": "10.2.3",
|
||||
"framer-motion": "10.16.4",
|
||||
"howler": "2.2.4",
|
||||
"i18next": "25.0.0",
|
||||
"js-confetti": "0.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "3.2.1",
|
||||
"react-i18next": "15.4.1",
|
||||
"react-icons": "4.11.0",
|
||||
"react-wrap-balancer": "1.1.0",
|
||||
"uuid": "10.0.0",
|
||||
|
|
@ -2042,9 +2044,10 @@
|
|||
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
|
||||
"integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
|
|
@ -17486,6 +17489,15 @@
|
|||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
|
||||
|
|
@ -17570,6 +17582,37 @@
|
|||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.0.tgz",
|
||||
"integrity": "sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
|
@ -22247,6 +22290,28 @@
|
|||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
|
||||
"integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
|
||||
|
|
@ -26633,6 +26698,15 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -40,10 +40,12 @@
|
|||
"focus-trap-react": "10.2.3",
|
||||
"framer-motion": "10.16.4",
|
||||
"howler": "2.2.4",
|
||||
"i18next": "25.0.0",
|
||||
"js-confetti": "0.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "3.2.1",
|
||||
"react-i18next": "15.4.1",
|
||||
"react-icons": "4.11.0",
|
||||
"react-wrap-balancer": "1.1.0",
|
||||
"uuid": "10.0.0",
|
||||
|
|
|
|||
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Light.woff2
Normal file
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Medium.woff2
Normal file
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Medium.woff2
Normal file
Binary file not shown.
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Regular.woff2
Normal file
BIN
public/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Regular.woff2
Normal file
Binary file not shown.
|
|
@ -2,26 +2,18 @@
|
|||
import { Container } from '@/components/container';
|
||||
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
|
||||
const currentLocale = Astro.currentLocale;
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const count = soundCount();
|
||||
|
||||
const paragraphs = [
|
||||
{
|
||||
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations – with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
|
||||
title: 'Free Ambient Sounds',
|
||||
},
|
||||
{
|
||||
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
|
||||
title: 'Carefully Curated Sounds',
|
||||
},
|
||||
{
|
||||
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
|
||||
title: 'Create Your Soundscape',
|
||||
},
|
||||
{
|
||||
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
|
||||
title: 'Sounds for Every Moment',
|
||||
},
|
||||
const paragraphKeys = [
|
||||
'about.section1',
|
||||
'about.section2',
|
||||
'about.section3',
|
||||
'about.section4',
|
||||
];
|
||||
---
|
||||
|
||||
|
|
@ -30,19 +22,17 @@ const paragraphs = [
|
|||
|
||||
<Container tight>
|
||||
{
|
||||
paragraphs.map((paragraph, index) => (
|
||||
paragraphKeys.map((key, index) => (
|
||||
<div class="paragraph">
|
||||
<div class="counter">
|
||||
<span>0{index + 1}</span> / 0{paragraphs.length}
|
||||
<span>0{index + 1}</span> / 0{paragraphKeys.length}
|
||||
</div>
|
||||
|
||||
<h2 class="title">{paragraph.title}</h2>
|
||||
<p class="body">{paragraph.body}</p>
|
||||
<h2 class="title">{t(`${key}.title`)}</h2>
|
||||
<p class="body">{t(`${key}.body`, { count: count })}</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<button class="button" id="use-moodist"> Use Moodist</button>
|
||||
<button class="button" id="use-moodist">{t('use-moodist')}</button>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
|
|
@ -70,8 +60,7 @@ const paragraphs = [
|
|||
& .paragraph {
|
||||
padding: 30px 0;
|
||||
background: linear-gradient(
|
||||
transparent,
|
||||
var(--color-neutral-50) 10%,
|
||||
transparent var(--color-neutral-50) 10%,
|
||||
var(--color-neutral-50) 90%,
|
||||
transparent
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useMemo, useEffect } from 'react';
|
|||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { BiSolidHeart } from 'react-icons/bi/index';
|
||||
import { Howler } from 'howler';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
|
|
@ -17,11 +19,18 @@ import { MediaControls } from '@/components/media-controls';
|
|||
import { sounds } from '@/data/sounds';
|
||||
import { FADE_OUT } from '@/constants/events';
|
||||
|
||||
import type { Sound } from '@/data/types';
|
||||
import type { Sound, Category as CategoryType } from '@/data/types';
|
||||
import { subscribe } from '@/lib/event';
|
||||
|
||||
export function App() {
|
||||
const categories = useMemo(() => sounds.categories, []);
|
||||
interface AppProps {
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export function App({ locale }: AppProps) {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
}
|
||||
const categoriesData = useMemo(() => sounds.categories, []);
|
||||
|
||||
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
|
|
@ -29,18 +38,19 @@ export function App() {
|
|||
const unlock = useSoundStore(state => state.unlock);
|
||||
|
||||
const favoriteSounds = useMemo(() => {
|
||||
const favoriteSounds = categories
|
||||
const allFlatSounds = categoriesData
|
||||
.map(category => category.sounds)
|
||||
.flat()
|
||||
.filter(sound => favorites.includes(sound.id));
|
||||
|
||||
/**
|
||||
* Reorder based on the order of favorites
|
||||
*/
|
||||
return favorites.map(favorite =>
|
||||
favoriteSounds.find(sound => sound.id === favorite),
|
||||
.flat();
|
||||
const favoriteSoundsData = allFlatSounds.filter(sound =>
|
||||
favorites.includes(sound.id),
|
||||
);
|
||||
}, [favorites, categories]);
|
||||
|
||||
return favorites
|
||||
.map(favoriteId =>
|
||||
favoriteSoundsData.find(sound => sound.id === favoriteId),
|
||||
)
|
||||
.filter((s): s is Sound => s !== undefined);
|
||||
}, [favorites, categoriesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
|
|
@ -72,33 +82,33 @@ export function App() {
|
|||
}, [pause, lock, unlock]);
|
||||
|
||||
const allCategories = useMemo(() => {
|
||||
const favorites = [];
|
||||
|
||||
const favs: CategoryType[] = [];
|
||||
if (favoriteSounds.length) {
|
||||
favorites.push({
|
||||
favs.push({
|
||||
icon: <BiSolidHeart />,
|
||||
id: 'favorites',
|
||||
sounds: favoriteSounds as Array<Sound>,
|
||||
title: 'Favorites',
|
||||
sounds: favoriteSounds,
|
||||
titleKey: 'sounds.favorites.title',
|
||||
});
|
||||
}
|
||||
|
||||
return [...favorites, ...categories];
|
||||
}, [favoriteSounds, categories]);
|
||||
return [...favs, ...categoriesData];
|
||||
}, [favoriteSounds, categoriesData]);
|
||||
|
||||
return (
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
<Categories categories={allCategories} />
|
||||
</Container>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
<Categories categories={allCategories} />
|
||||
</Container>
|
||||
|
||||
<Toolbar />
|
||||
<SharedModal />
|
||||
</StoreConsumer>
|
||||
</SnackbarProvider>
|
||||
<Toolbar />
|
||||
<SharedModal />
|
||||
</StoreConsumer>
|
||||
</SnackbarProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { BiPause, BiPlay } from 'react-icons/bi/index';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { useSnackbar } from '@/contexts/snackbar';
|
||||
|
|
@ -9,6 +10,7 @@ import { cn } from '@/helpers/styles';
|
|||
import styles from './play.module.css';
|
||||
|
||||
export function PlayButton() {
|
||||
const { t } = useTranslation();
|
||||
const isPlaying = useSoundStore(state => state.isPlaying);
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
const toggle = useSoundStore(state => state.togglePlay);
|
||||
|
|
@ -20,10 +22,10 @@ export function PlayButton() {
|
|||
const handleToggle = useCallback(() => {
|
||||
if (locked) return;
|
||||
|
||||
if (noSelected) return showSnackbar('Please first select a sound to play.');
|
||||
if (noSelected) return showSnackbar(t('buttons.playError'));
|
||||
|
||||
toggle();
|
||||
}, [showSnackbar, toggle, noSelected, locked]);
|
||||
}, [showSnackbar, toggle, noSelected, locked, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying && noSelected) pause();
|
||||
|
|
@ -42,14 +44,14 @@ export function PlayButton() {
|
|||
<span aria-hidden="true">
|
||||
<BiPause />
|
||||
</span>{' '}
|
||||
Pause
|
||||
{t('common.pause')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span aria-hidden="true">
|
||||
<BiPlay />
|
||||
</span>{' '}
|
||||
Play
|
||||
{t('common.play')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ import { fade, mix, slideX } from '@/lib/motion';
|
|||
|
||||
import styles from './unselect.module.css';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function UnselectButton() {
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
const restoreHistory = useSoundStore(state => state.restoreHistory);
|
||||
const hasHistory = useSoundStore(state => !!state.history);
|
||||
const unselectAll = useSoundStore(state => state.unselectAll);
|
||||
const locked = useSoundStore(state => state.locked);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const variants = {
|
||||
...mix(fade(), slideX(15)),
|
||||
exit: { opacity: 0 },
|
||||
|
|
@ -45,16 +47,16 @@ export function UnselectButton() {
|
|||
showDelay={0}
|
||||
content={
|
||||
hasHistory
|
||||
? 'Restore unselected sounds.'
|
||||
: 'Unselect all sounds.'
|
||||
? t('unselect.restore.tooltip')
|
||||
: t('unselect.tooltip')
|
||||
}
|
||||
>
|
||||
<button
|
||||
disabled={noSelected && !hasHistory}
|
||||
aria-label={
|
||||
hasHistory
|
||||
? 'Restore Unselected Sounds'
|
||||
: 'Unselect All Sounds'
|
||||
? t('unselect.restore.aria-label')
|
||||
: t('unselect.aria-label')
|
||||
}
|
||||
className={cn(
|
||||
styles.unselectButton,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Sounds } from '@/components/sounds';
|
||||
|
||||
import styles from './category.module.css';
|
||||
import type { Category as CategoryType } from '@/data/types';
|
||||
|
||||
import type { Category } from '@/data/types';
|
||||
|
||||
interface CategoryProps extends Category {
|
||||
interface CategoryProps extends Omit<CategoryType, 'title'> {
|
||||
functional?: boolean;
|
||||
titleKey: string;
|
||||
}
|
||||
|
||||
export function Category({
|
||||
|
|
@ -13,8 +14,10 @@ export function Category({
|
|||
icon,
|
||||
id,
|
||||
sounds,
|
||||
title,
|
||||
titleKey,
|
||||
}: CategoryProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.category} id={`category-${id}`}>
|
||||
<div className={styles.iconContainer}>
|
||||
|
|
@ -24,7 +27,7 @@ export function Category({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>{title}</div>
|
||||
<div className={styles.title}>{t(titleKey)}</div>
|
||||
|
||||
<Sounds functional={functional} id={id} sounds={sounds} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { FaCoffee } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SpecialButton } from '@/components/special-button';
|
||||
|
||||
import styles from './donate.module.css';
|
||||
|
||||
export function Donate() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={styles.donate}>
|
||||
<div className={styles.iconContainer}>
|
||||
|
|
@ -15,14 +17,14 @@ export function Donate() {
|
|||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<span>Support Me</span>
|
||||
<span>{t('donate.section-title')}</span>
|
||||
</div>
|
||||
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
|
||||
<p className={styles.desc}>{t('donate.section-desc')}</p>
|
||||
<SpecialButton
|
||||
className={styles.button}
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
>
|
||||
Donate Today
|
||||
{t('donate.section-button')}
|
||||
</SpecialButton>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
---
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
import { Container } from './container';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
const t = await getTranslator(currentLocale);
|
||||
---
|
||||
|
||||
<Container>
|
||||
<section class="wrapper">
|
||||
<p class="text">
|
||||
Enjoy Moodist?{' '}
|
||||
{t('donate.prompt')}{' '}
|
||||
<a
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Support with a donation!
|
||||
{t('donate.link-text')}
|
||||
</a>
|
||||
</p>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
---
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
import { Container } from './container';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
const t = await getTranslator(currentLocale);
|
||||
---
|
||||
|
||||
<footer class="footer">
|
||||
<Container>
|
||||
<p>
|
||||
Created by <a href="https://twitter.com/remvze">Maze ✦</a>
|
||||
</p>
|
||||
<p
|
||||
set:html={t('created-by', {
|
||||
authorLink: '<a href="https://twitter.com/remvze">Maze ✦</a>',
|
||||
})}
|
||||
/>
|
||||
</Container>
|
||||
</footer>
|
||||
|
||||
|
|
@ -15,17 +21,18 @@ import { Container } from './container';
|
|||
display: flex;
|
||||
align-items: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground-subtle);
|
||||
text-align: center;
|
||||
.footer p {
|
||||
margin: 0;
|
||||
font-size: var(--font-sm);
|
||||
color: var(--color-foreground-subtle);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
& a {
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
.footer p :global(a) {
|
||||
font-weight: 500;
|
||||
color: var(--color-foreground);
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
---
|
||||
import { BsSoundwave } from 'react-icons/bs/index';
|
||||
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
import { Container } from './container';
|
||||
import { CipherText } from './cipher';
|
||||
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const count = soundCount();
|
||||
---
|
||||
|
||||
|
|
@ -15,7 +17,7 @@ const count = soundCount();
|
|||
<div class="pattern"></div>
|
||||
<div class="logo-wrapper">
|
||||
<img
|
||||
alt="Faded Moodist Logo"
|
||||
alt={t('hero.logo-alt')}
|
||||
aria-hidden="true"
|
||||
class="logo"
|
||||
height={48}
|
||||
|
|
@ -25,17 +27,19 @@ const count = soundCount();
|
|||
</div>
|
||||
|
||||
<h1 class="title">
|
||||
Ambient Sounds<span class="line">For Focus and Calm</span>
|
||||
{t('hero.title-line1')}<span class="line">{t('hero.title-line2')}</span>
|
||||
</h1>
|
||||
<h2 class="desc">
|
||||
Free and <CipherText client:load text="Open-Source" />.
|
||||
{t('hero.desc-prefix')}
|
||||
<CipherText client:load text={t('hero.desc-open-source')} />
|
||||
.
|
||||
</h2>
|
||||
|
||||
<p class="sounds">
|
||||
<span aria-hidden="true" class="icon">
|
||||
<BsSoundwave />
|
||||
</span>
|
||||
<span>{count} Sounds</span>
|
||||
<span>{t('hero.sounds-count', { count: count })}</span>
|
||||
</p>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
|||
171
src/components/language-switcher.astro
Normal file
171
src/components/language-switcher.astro
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
---
|
||||
import { getSupportedLangs, getTranslator } from '@/i18n/utils';
|
||||
import { IoChevronDown } from 'react-icons/io5';
|
||||
|
||||
const { url } = Astro;
|
||||
const defaultLocaleCode = 'en'; // 明确默认语言代码
|
||||
|
||||
const currentLocale = Astro.currentLocale || defaultLocaleCode;
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const supportedLangs = getSupportedLangs(); // 如 ['en', 'zh-CN-CN', 'zh-CN-TW', 'ja']
|
||||
|
||||
let basePath = url.pathname;
|
||||
const currentLangPrefix = `/${currentLocale}`;
|
||||
|
||||
if (
|
||||
currentLocale !== defaultLocaleCode &&
|
||||
basePath.startsWith(currentLangPrefix)
|
||||
) {
|
||||
basePath = basePath.substring(currentLangPrefix.length) || '/'; // 获取前缀后的部分,空则为根
|
||||
}
|
||||
if (basePath !== '/' && !basePath.startsWith('/')) {
|
||||
basePath = '/' + basePath;
|
||||
}
|
||||
|
||||
const currentLangName =
|
||||
t(`languages.${currentLocale}`) || currentLocale.toUpperCase();
|
||||
---
|
||||
|
||||
<div class="language-switcher-wrapper">
|
||||
<details class="language-details">
|
||||
<summary class="language-summary">
|
||||
<span>{currentLangName}</span>
|
||||
<IoChevronDown className="chevron-icon" />
|
||||
</summary>
|
||||
<ul class="language-list">
|
||||
{
|
||||
supportedLangs.map(langCode => {
|
||||
if (langCode === currentLocale) return null;
|
||||
|
||||
const isDefaultLang = langCode === defaultLocaleCode;
|
||||
let targetPath = isDefaultLang ? basePath : `/${langCode}${basePath}`;
|
||||
targetPath = targetPath.replace('//', '/');
|
||||
if (isDefaultLang && targetPath === '') targetPath = '/';
|
||||
if (!isDefaultLang && targetPath === `/${langCode}/`)
|
||||
targetPath = `/${langCode}`;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<a href={targetPath} hreflang={langCode}>
|
||||
{t(`languages.${langCode}`) || langCode.toUpperCase()}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.language-switcher-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.language-details {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-summary {
|
||||
display: flex;
|
||||
gap: 0.3rem;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1;
|
||||
color: var(--color-foreground-subtle);
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.language-summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.language-summary:hover,
|
||||
.language-details[open] .language-summary {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-200);
|
||||
border-color: var(--color-neutral-300);
|
||||
}
|
||||
|
||||
.language-summary:focus-visible {
|
||||
outline: 2px solid var(--color-neutral-400);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
font-size: 1em;
|
||||
opacity: 0.8;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.language-details[open] .chevron-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-list {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
|
||||
/* 根据需要调整 left/right */
|
||||
right: 0;
|
||||
left: auto;
|
||||
z-index: 60;
|
||||
display: none;
|
||||
min-width: 100%;
|
||||
max-height: 200px;
|
||||
padding: 0.4rem 0;
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
background-color: var(--color-neutral-100);
|
||||
border: 1px solid var(--color-neutral-200);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 15%);
|
||||
}
|
||||
|
||||
.language-details[open] .language-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.language-list li a {
|
||||
display: block;
|
||||
padding: 0.5rem 0.8rem;
|
||||
font-size: var(--font-sm);
|
||||
line-height: 1;
|
||||
color: var(--color-foreground-subtle);
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
}
|
||||
|
||||
.language-list li a:hover,
|
||||
.language-list li a:focus {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-200);
|
||||
}
|
||||
|
||||
.language-list li a:focus-visible {
|
||||
color: var(--color-foreground);
|
||||
background-color: var(--color-neutral-200);
|
||||
outline: 2px solid var(--color-neutral-400);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { IoClose } from 'react-icons/io5/index';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Portal } from '@/components/portal';
|
||||
|
||||
|
|
@ -29,6 +30,7 @@ export function Modal({
|
|||
show,
|
||||
wide,
|
||||
}: ModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const variants = {
|
||||
modal: mix(fade(), slideY(20)),
|
||||
overlay: fade(),
|
||||
|
|
@ -38,7 +40,6 @@ export function Modal({
|
|||
if (show && lockBody) {
|
||||
document.body.style.overflowY = 'hidden';
|
||||
} else if (lockBody) {
|
||||
// Wait for transition to finish before allowing scrollbar to return
|
||||
setTimeout(() => {
|
||||
document.body.style.overflowY = 'auto';
|
||||
}, TRANSITION_DURATION);
|
||||
|
|
@ -85,7 +86,11 @@ export function Modal({
|
|||
transition={{ duration: TRANSITION_DURATION / 1000 }}
|
||||
variants={variants.modal}
|
||||
>
|
||||
<button className={styles.close} onClick={onClose}>
|
||||
<button
|
||||
aria-label={t('common.close')}
|
||||
className={styles.close}
|
||||
onClick={onClose}
|
||||
>
|
||||
<IoClose />
|
||||
</button>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
|
|
@ -14,15 +14,46 @@ interface Preset {
|
|||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
translationKey: 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' },
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 2,
|
||||
name: 'Delta (Deep Sleep) 2 Hz',
|
||||
translationKey: 'modals.generators.presets.delta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 5,
|
||||
name: 'Theta (Meditation) 5 Hz',
|
||||
translationKey: 'modals.generators.presets.theta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 10,
|
||||
name: 'Alpha (Relaxation) 10 Hz',
|
||||
translationKey: 'modals.generators.presets.alpha',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 20,
|
||||
name: 'Beta (Focus) 20 Hz',
|
||||
translationKey: 'modals.generators.presets.beta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 40,
|
||||
name: 'Gamma (Cognition) 40 Hz',
|
||||
translationKey: 'modals.generators.presets.gamma',
|
||||
},
|
||||
{
|
||||
baseFrequency: 440,
|
||||
beatFrequency: 10,
|
||||
name: 'Custom',
|
||||
translationKey: 'modals.generators.presets.custom',
|
||||
},
|
||||
];
|
||||
|
||||
function computeBinauralBeatOscillatorFrequencies(
|
||||
|
|
@ -36,6 +67,7 @@ function computeBinauralBeatOscillatorFrequencies(
|
|||
}
|
||||
|
||||
export function BinauralModal({ onClose, show }: BinauralProps) {
|
||||
const { t } = useTranslation();
|
||||
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%
|
||||
|
|
@ -145,15 +177,14 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
}, [selectedPreset]);
|
||||
|
||||
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selected = e.target.value;
|
||||
setSelectedPreset(selected);
|
||||
const selectedName = e.target.value;
|
||||
setSelectedPreset(selectedName);
|
||||
|
||||
if (selected === 'Custom') {
|
||||
// Allow user to input custom frequencies
|
||||
if (selectedName === 'Custom') {
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = presets.find(p => p.name === selected);
|
||||
const preset = presets.find(p => p.name === selectedName);
|
||||
if (preset) {
|
||||
setBaseFrequency(preset.baseFrequency);
|
||||
setBeatFrequency(preset.beatFrequency);
|
||||
|
|
@ -163,17 +194,17 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
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>
|
||||
<h2 className={styles.title}>{t('modals.binaural.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.binaural.description')}</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
{t('modals.generators.presets-label')}
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
{t(preset.translationKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -183,7 +214,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
{t('modals.generators.base-frequency-label')}
|
||||
<input
|
||||
max="1500"
|
||||
min="20"
|
||||
|
|
@ -198,7 +229,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Beat Frequency (Hz):
|
||||
{t('modals.binaural.beat-frequency-label')}
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
|
|
@ -213,9 +244,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
{t('modals.generators.volume-label')}
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
|
|
@ -232,10 +264,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
|
|||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
{t('common.stop')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Exercise } from './exercise';
|
||||
|
||||
|
|
@ -9,9 +10,12 @@ interface TimerProps {
|
|||
}
|
||||
|
||||
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
|
||||
const { t } = useTranslation(); // Get t function
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Breathing Exercise</h2>
|
||||
<h2 className={styles.title || 'modal-title'}>
|
||||
{t('modals.breathing.title')}
|
||||
</h2>
|
||||
<Exercise />
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
import { useTranslation } from 'react-i18next'; // Import
|
||||
import { padNumber } from '@/helpers/number';
|
||||
|
||||
import styles from './exercise.module.css';
|
||||
|
||||
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
|
||||
|
|
@ -17,21 +16,27 @@ const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
|
|||
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
|
||||
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
|
||||
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 },
|
||||
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale
|
||||
'Resonant Breathing': { exhale: 5, inhale: 5 },
|
||||
};
|
||||
|
||||
const PHASE_LABELS: Record<Phase, string> = {
|
||||
exhale: 'Exhale',
|
||||
holdExhale: 'Hold',
|
||||
holdInhale: 'Hold',
|
||||
inhale: 'Inhale',
|
||||
const PHASE_LABEL_KEYS: Record<Phase, string> = {
|
||||
exhale: 'modals.breathing.phases.exhale',
|
||||
holdExhale: 'modals.breathing.phases.hold',
|
||||
holdInhale: 'modals.breathing.phases.hold',
|
||||
inhale: 'modals.breathing.phases.inhale',
|
||||
};
|
||||
|
||||
const EXERCISE_SELECT_OPTIONS: { key: string; value: Exercise }[] = [
|
||||
{ key: 'modals.breathing.exercises.478', value: '4-7-8 Breathing' },
|
||||
{ key: 'modals.breathing.exercises.box', value: 'Box Breathing' },
|
||||
{ key: 'modals.breathing.exercises.resonant', value: 'Resonant Breathing' },
|
||||
];
|
||||
|
||||
export function Exercise() {
|
||||
const { t } = useTranslation();
|
||||
const [selectedExercise, setSelectedExercise] =
|
||||
useState<Exercise>('4-7-8 Breathing');
|
||||
const [phaseIndex, setPhaseIndex] = useState(0);
|
||||
|
||||
const phases = useMemo(
|
||||
() => EXERCISE_PHASES[selectedExercise],
|
||||
[selectedExercise],
|
||||
|
|
@ -92,6 +97,8 @@ export function Exercise() {
|
|||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const currentPhaseLabel = t(PHASE_LABEL_KEYS[currentPhase]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.exercise}>
|
||||
|
|
@ -105,7 +112,7 @@ export function Exercise() {
|
|||
key={selectedExercise}
|
||||
variants={animationVariants}
|
||||
/>
|
||||
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
|
||||
<p className={styles.phase}>{currentPhaseLabel}</p>
|
||||
</div>
|
||||
|
||||
<div className={styles.selectWrapper}>
|
||||
|
|
@ -114,9 +121,9 @@ export function Exercise() {
|
|||
value={selectedExercise}
|
||||
onChange={e => setSelectedExercise(e.target.value as Exercise)}
|
||||
>
|
||||
{Object.keys(EXERCISE_PHASES).map(exercise => (
|
||||
<option key={exercise} value={exercise}>
|
||||
{exercise}
|
||||
{EXERCISE_SELECT_OPTIONS.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{t(option.key)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Slider } from '@/components/slider';
|
||||
|
||||
|
|
@ -14,18 +14,50 @@ interface Preset {
|
|||
baseFrequency: number;
|
||||
beatFrequency: number;
|
||||
name: string;
|
||||
translationKey: 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' },
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 2,
|
||||
name: 'Delta (Deep Sleep) 2 Hz',
|
||||
translationKey: 'modals.generators.presets.delta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 5,
|
||||
name: 'Theta (Meditation) 5 Hz',
|
||||
translationKey: 'modals.generators.presets.theta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 10,
|
||||
name: 'Alpha (Relaxation) 10 Hz',
|
||||
translationKey: 'modals.generators.presets.alpha',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 20,
|
||||
name: 'Beta (Focus) 20 Hz',
|
||||
translationKey: 'modals.generators.presets.beta',
|
||||
},
|
||||
{
|
||||
baseFrequency: 100,
|
||||
beatFrequency: 40,
|
||||
name: 'Gamma (Cognition) 40 Hz',
|
||||
translationKey: 'modals.generators.presets.gamma',
|
||||
},
|
||||
{
|
||||
baseFrequency: 440,
|
||||
beatFrequency: 10,
|
||||
name: 'Custom',
|
||||
translationKey: 'modals.generators.presets.custom',
|
||||
},
|
||||
];
|
||||
|
||||
export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
||||
const { t } = useTranslation();
|
||||
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%
|
||||
|
|
@ -164,17 +196,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
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>
|
||||
<h2 className={styles.title}>{t('modals.isochronic.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.isochronic.description')}</p>
|
||||
</header>
|
||||
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Presets:
|
||||
{t('modals.generators.presets-label')} {/* Use common key */}
|
||||
<select value={selectedPreset} onChange={handlePresetChange}>
|
||||
{presets.map(preset => (
|
||||
<option key={preset.name} value={preset.name}>
|
||||
{preset.name}
|
||||
{t(preset.translationKey)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
|
@ -184,7 +216,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
<>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Base Frequency (Hz):
|
||||
{t('modals.generators.base-frequency-label')}{' '}
|
||||
{/* Use common key */}
|
||||
<input
|
||||
max="2000"
|
||||
min="20"
|
||||
|
|
@ -199,7 +232,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
</div>
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Tone Frequency (Hz):
|
||||
{t('modals.isochronic.tone-frequency-label')}{' '}
|
||||
{/* Use isochronic specific key */}
|
||||
<input
|
||||
max="40"
|
||||
min="0.1"
|
||||
|
|
@ -230,7 +264,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
)}
|
||||
<div className={styles.fieldWrapper}>
|
||||
<label>
|
||||
Volume:
|
||||
{t('modals.generators.volume-label')} {/* Use common key */}
|
||||
<Slider
|
||||
className={styles.volume}
|
||||
max={1}
|
||||
|
|
@ -241,16 +275,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
|
|||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button
|
||||
className={styles.primary}
|
||||
disabled={isPlaying}
|
||||
onClick={startSound}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
<button disabled={!isPlaying} onClick={stopSound}>
|
||||
Stop
|
||||
{t('common.stop')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styles from './list.module.css';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { usePresetStore } from '@/stores/preset';
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
|
||||
interface ListProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export function List({ close }: ListProps) {
|
||||
const { t } = useTranslation();
|
||||
const presets = usePresetStore(state => state.presets);
|
||||
const changeName = usePresetStore(state => state.changeName);
|
||||
const deletePreset = usePresetStore(state => state.deletePreset);
|
||||
|
|
@ -19,34 +21,57 @@ export function List({ close }: ListProps) {
|
|||
return (
|
||||
<div className={styles.list}>
|
||||
<h3 className={styles.title}>
|
||||
Your Presets {presets.length > 0 && `(${presets.length})`}
|
||||
{t('modals.presets.your-presets-title')}{' '}
|
||||
{presets.length > 0 && `(${presets.length})`}
|
||||
</h3>
|
||||
|
||||
{!presets.length && (
|
||||
<p className={styles.empty}>You don't have any presets yet.</p>
|
||||
<p className={styles.empty}>{t('modals.presets.empty')}</p>
|
||||
)}
|
||||
|
||||
{presets.map(preset => (
|
||||
<div className={styles.preset} key={preset.id}>
|
||||
<input
|
||||
placeholder="Untitled"
|
||||
placeholder={t('common.untitled')}
|
||||
type="text"
|
||||
value={preset.label}
|
||||
onChange={e => changeName(preset.id, e.target.value)}
|
||||
/>
|
||||
<button onClick={() => deletePreset(preset.id)}>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
<button
|
||||
className={styles.primary}
|
||||
onClick={() => {
|
||||
override(preset.sounds);
|
||||
play();
|
||||
close();
|
||||
}}
|
||||
<Tooltip
|
||||
showDelay={0}
|
||||
content={
|
||||
t('modals.presets.delete-button-tooltip') || 'Delete preset'
|
||||
}
|
||||
>
|
||||
<FaPlay />
|
||||
</button>
|
||||
<button
|
||||
aria-label={
|
||||
t('modals.presets.delete-button-aria-label') ||
|
||||
`Delete preset ${preset.label || t('common.untitled')}`
|
||||
}
|
||||
onClick={() => deletePreset(preset.id)}
|
||||
>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
content={t('modals.presets.play-button-tooltip') || 'Play preset'}
|
||||
showDelay={0}
|
||||
>
|
||||
<button
|
||||
className={styles.primary}
|
||||
aria-label={
|
||||
t('modals.presets.play-button-aria-label') ||
|
||||
`Play preset ${preset.label || t('common.untitled')}`
|
||||
}
|
||||
onClick={() => {
|
||||
override(preset.sounds);
|
||||
play();
|
||||
close();
|
||||
}}
|
||||
>
|
||||
<FaPlay />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, type FormEvent } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { cn } from '@/helpers/styles';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { usePresetStore } from '@/stores/preset';
|
||||
|
|
@ -7,6 +7,7 @@ import { usePresetStore } from '@/stores/preset';
|
|||
import styles from './new.module.css';
|
||||
|
||||
export function New() {
|
||||
const { t } = useTranslation();
|
||||
const [name, setName] = useState('');
|
||||
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
|
@ -33,7 +34,7 @@ export function New() {
|
|||
|
||||
return (
|
||||
<div className={styles.new}>
|
||||
<h3 className={styles.title}>New Preset</h3>
|
||||
<h3 className={styles.title}>{t('modals.presets.new-preset-title')}</h3>
|
||||
|
||||
<form
|
||||
className={cn(styles.form, noSelected && styles.disabled)}
|
||||
|
|
@ -41,18 +42,18 @@ export function New() {
|
|||
>
|
||||
<input
|
||||
disabled={noSelected}
|
||||
placeholder="Preset's Name"
|
||||
placeholder={t('modals.presets.placeholder')}
|
||||
required
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
<button disabled={noSelected}>Save</button>
|
||||
<button disabled={noSelected}>{t('common.save')}</button>
|
||||
</form>
|
||||
|
||||
{noSelected && (
|
||||
<p className={styles.noSelected}>
|
||||
To make a preset, first select some sounds.
|
||||
{t('modals.presets.no-selected-warning')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { New } from './new';
|
||||
import { List } from './list';
|
||||
|
|
@ -10,9 +11,11 @@ interface PresetsModalProps {
|
|||
}
|
||||
|
||||
export function PresetsModal({ onClose, show }: PresetsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Presets</h2>
|
||||
<h2 className={styles.title}>{t('modals.presets.title')}</h2>
|
||||
<New />
|
||||
<div className={styles.divider} />
|
||||
<List close={onClose} />
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useMemo, useEffect, useState } from 'react';
|
||||
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useCopy } from '@/hooks/use-copy';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import styles from './share-link.module.css';
|
||||
import { Tooltip } from '@/components/tooltip'; // Import Tooltip
|
||||
|
||||
interface ShareLinkModalProps {
|
||||
onClose: () => void;
|
||||
|
|
@ -14,6 +15,7 @@ interface ShareLinkModalProps {
|
|||
}
|
||||
|
||||
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
const sounds = useSoundStore(state => state.sounds);
|
||||
const { copy, copying } = useCopy();
|
||||
|
|
@ -51,16 +53,25 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
|
|||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1 className={styles.heading}>Share your sound selection!</h1>
|
||||
<p className={styles.desc}>
|
||||
Copy and send the following link to the person you want to share your
|
||||
selection with.
|
||||
</p>
|
||||
<h1 className={styles.heading}>{t('modals.share-link.title')}</h1>
|
||||
<p className={styles.desc}>{t('modals.share-link.description')}</p>
|
||||
<div className={styles.inputWrapper}>
|
||||
<input readOnly type="text" value={url} />
|
||||
<button onClick={() => copy(url)}>
|
||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
||||
</button>
|
||||
<Tooltip
|
||||
content={copying ? t('common.copied') : t('common.copy')}
|
||||
showDelay={0}
|
||||
>
|
||||
<button
|
||||
aria-label={
|
||||
copying
|
||||
? t('common.copied')
|
||||
: t('modals.share-link.copy-button-aria-label')
|
||||
}
|
||||
onClick={() => copy(url)}
|
||||
>
|
||||
{copying ? <IoCheckmark /> : <IoCopyOutline />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
|
@ -11,14 +11,16 @@ import { sounds } from '@/data/sounds';
|
|||
import styles from './shared.module.css';
|
||||
|
||||
export function SharedModal() {
|
||||
const { t } = useTranslation(); // Get t function
|
||||
const override = useSoundStore(state => state.override);
|
||||
const showSnackbar = useSnackbar();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [sharedSounds, setSharedSounds] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
volume: number;
|
||||
}>
|
||||
>([]);
|
||||
|
|
@ -30,26 +32,26 @@ export function SharedModal() {
|
|||
if (share) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(share));
|
||||
const allSounds: Record<string, string> = {};
|
||||
|
||||
// Map sound IDs to their labelKeys for quick lookup
|
||||
const allSoundLabelKeys: Record<string, string> = {};
|
||||
sounds.categories.forEach(category => {
|
||||
category.sounds.forEach(sound => {
|
||||
allSounds[sound.id] = sound.label;
|
||||
allSoundLabelKeys[sound.id] = sound.labelKey; // Get the labelKey
|
||||
});
|
||||
});
|
||||
|
||||
const _sharedSounds: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
volume: number;
|
||||
}> = [];
|
||||
|
||||
Object.keys(parsed).forEach(sound => {
|
||||
if (allSounds[sound]) {
|
||||
Object.keys(parsed).forEach(soundId => {
|
||||
// Check if the soundId exists and has a labelKey
|
||||
if (allSoundLabelKeys[soundId]) {
|
||||
_sharedSounds.push({
|
||||
id: sound,
|
||||
label: allSounds[sound],
|
||||
volume: Number(parsed[sound]),
|
||||
id: soundId,
|
||||
labelKey: allSoundLabelKeys[soundId], // Store the key
|
||||
volume: Number(parsed[soundId]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -59,12 +61,13 @@ export function SharedModal() {
|
|||
setSharedSounds(_sharedSounds);
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
console.error('Error parsing shared URL:', error); // Log error
|
||||
return; // Stop execution if parsing fails
|
||||
} finally {
|
||||
history.pushState({}, '', location.href.split('?')[0]);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}, []); // Run only once on mount
|
||||
|
||||
const handleOverride = () => {
|
||||
const newSounds: Record<string, number> = {};
|
||||
|
|
@ -75,34 +78,31 @@ export function SharedModal() {
|
|||
|
||||
override(newSounds);
|
||||
setIsOpen(false);
|
||||
showSnackbar('Done! You can now play the new selection.');
|
||||
showSnackbar(t('modals.shared.snackbar-message'));
|
||||
};
|
||||
|
||||
useCloseListener(() => setIsOpen(false));
|
||||
|
||||
return (
|
||||
<Modal show={isOpen} onClose={() => setIsOpen(false)}>
|
||||
<h1 className={styles.heading}>New sound mix detected!</h1>
|
||||
<p className={styles.desc}>
|
||||
Someone has shared the following mix with you. Would you want to
|
||||
override your current selection?
|
||||
</p>
|
||||
<h1 className={styles.heading}>{t('modals.shared.title')}</h1>
|
||||
<p className={styles.desc}>{t('modals.shared.description')}</p>
|
||||
<div className={styles.sounds}>
|
||||
{sharedSounds.map(sound => (
|
||||
<div className={styles.sound} key={sound.id}>
|
||||
{sound.label}
|
||||
{t(sound.labelKey)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.footer}>
|
||||
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={handleOverride}
|
||||
>
|
||||
Override
|
||||
{t('common.override')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,69 +1,47 @@
|
|||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './shortcuts.module.css';
|
||||
// src/components/modals/shortcuts/shortcuts.tsx
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal'; // Assuming Modal component is stable
|
||||
import styles from './shortcuts.module.css'; // Assuming styles are correct
|
||||
|
||||
interface ShortcutsModalProps {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
onClose: () => void; // Function to close the modal
|
||||
show: boolean; // Boolean to control modal visibility
|
||||
}
|
||||
|
||||
interface ShortcutItem {
|
||||
keys: string[];
|
||||
labelKey: string;
|
||||
}
|
||||
|
||||
const shortcutsList: ShortcutItem[] = [
|
||||
{ keys: ['Shift', 'H'], labelKey: 'toolbar.items.shortcuts' }, // Reusing toolbar item key
|
||||
{ keys: ['Shift', 'Alt', 'P'], labelKey: 'toolbar.items.presets' },
|
||||
{ keys: ['Shift', 'S'], labelKey: 'toolbar.items.share' },
|
||||
{ keys: ['Shift', 'Alt', 'T'], labelKey: 'toolbar.items.sleep-timer' },
|
||||
{ keys: ['Shift', 'C'], labelKey: 'toolbar.items.countdown' },
|
||||
{ keys: ['Shift', 'P'], labelKey: 'toolbar.items.pomodoro' },
|
||||
{ keys: ['Shift', 'N'], labelKey: 'toolbar.items.notepad' },
|
||||
{ keys: ['Shift', 'T'], labelKey: 'toolbar.items.todo' },
|
||||
{ keys: ['Shift', 'B'], labelKey: 'toolbar.items.breathing' },
|
||||
{ keys: ['Shift', 'Space'], labelKey: 'modals.shortcuts.labels.toggle-play' }, // Specific key
|
||||
{ keys: ['Shift', 'R'], labelKey: 'modals.shortcuts.labels.unselect-all' }, // Specific key
|
||||
];
|
||||
|
||||
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
|
||||
const shortcuts = [
|
||||
{
|
||||
keys: ['Shift', 'H'],
|
||||
label: 'Shortcuts List',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'Alt', 'P'],
|
||||
label: 'Presets',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'S'],
|
||||
label: 'Share Sounds',
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
{
|
||||
keys: ['Shift', 'R'],
|
||||
label: 'Unselect All Sounds',
|
||||
},
|
||||
];
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<h1 className={styles.heading}>Keyboard Shortcuts</h1>
|
||||
<h1 className={styles.heading}>{t('modals.shortcuts.title')}</h1>
|
||||
<div className={styles.shortcuts}>
|
||||
{shortcuts.map(shortcut => (
|
||||
{shortcutsList.map(shortcut => (
|
||||
// Render a Row for each shortcut item
|
||||
// Use the labelKey as the React key for stability if IDs aren't available
|
||||
<Row
|
||||
key={shortcut.label}
|
||||
key={shortcut.labelKey}
|
||||
keys={shortcut.keys}
|
||||
label={shortcut.label}
|
||||
// Get the translated label using the defined labelKey
|
||||
label={t(shortcut.labelKey)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -90,10 +68,13 @@ function Row({ keys, label }: RowProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// Props for the Key component
|
||||
interface KeyProps {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode; // The text content (key name)
|
||||
}
|
||||
|
||||
// Component to render a single keyboard key representation
|
||||
function Key({ children }: KeyProps) {
|
||||
// Simple div with styling for a key
|
||||
return <div className={styles.key}>{children}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState, useRef, useMemo } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Timer } from './timer';
|
||||
import { dispatch } from '@/lib/event';
|
||||
|
|
@ -15,7 +15,37 @@ interface SleepTimerModalProps {
|
|||
show: boolean;
|
||||
}
|
||||
|
||||
interface FieldProps {
|
||||
labelKey: string;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function Field({ labelKey, onChange, value }: FieldProps) {
|
||||
const { t } = useTranslation();
|
||||
const label = t(labelKey);
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={labelKey}>
|
||||
{' '}
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={labelKey}
|
||||
max="59"
|
||||
min="0"
|
||||
required
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const setActive = useSleepTimerStore(state => state.set);
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
|
|
@ -91,21 +121,27 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Sleep Timer</h2>
|
||||
<p className={styles.desc}>
|
||||
Stop sounds after a certain amount of time.
|
||||
</p>
|
||||
<h2 className={styles.title}>{t('modals.sleep-timer.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.sleep-timer.description')}</p>
|
||||
</header>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.inputs}>
|
||||
{!running && (
|
||||
<Field label="Hours" value={hours} onChange={setHours} />
|
||||
<Field
|
||||
labelKey="modals.sleep-timer.hours-label"
|
||||
value={hours}
|
||||
onChange={setHours}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!running && (
|
||||
<Field label="Minutes" value={minutes} onChange={setMinutes} />
|
||||
<Field
|
||||
labelKey="modals.sleep-timer.minutes-label"
|
||||
value={minutes}
|
||||
onChange={setMinutes}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -118,7 +154,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
type="button"
|
||||
onClick={handleReset}
|
||||
>
|
||||
Reset
|
||||
{t('common.reset')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
|
@ -127,7 +163,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
className={cn(styles.button, styles.primary)}
|
||||
type="submit"
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -136,29 +172,3 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
|
|||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface FieldProps {
|
||||
label: string;
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function Field({ label, onChange, value }: FieldProps) {
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={label.toLocaleLowerCase()}>
|
||||
{label}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={label.toLocaleLowerCase()}
|
||||
max="59"
|
||||
min="0"
|
||||
required
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { useRegisterSW } from 'virtual:pwa-register/react'; // eslint-disable-line
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './reload.module.css';
|
||||
|
||||
export function ReloadModal() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
needRefresh: [needRefresh, setNeedRefresh],
|
||||
updateServiceWorker,
|
||||
|
|
@ -16,19 +18,17 @@ export function ReloadModal() {
|
|||
|
||||
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>
|
||||
<h2 className={styles.title}>{t('modals.reload.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.reload.description')}</p>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button onClick={close}>Close</button>
|
||||
<button onClick={close}>{t('common.close')}</button>
|
||||
|
||||
<button
|
||||
className={styles.primary}
|
||||
onClick={() => updateServiceWorker(true)}
|
||||
>
|
||||
Reload
|
||||
{t('common.reload')}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { BiShuffle } from 'react-icons/bi/index';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
|
@ -6,12 +7,14 @@ import { useSoundStore } from '@/stores/sound';
|
|||
import styles from './shuffle.module.css';
|
||||
|
||||
export function Shuffle() {
|
||||
const { t } = useTranslation();
|
||||
const shuffle = useSoundStore(state => state.shuffle);
|
||||
const shuffleLabel = t('toolbar.items.shuffle'); // Get translated label
|
||||
|
||||
return (
|
||||
<Tooltip content="Shuffle sounds" showDelay={0}>
|
||||
<Tooltip content={shuffleLabel} showDelay={0}>
|
||||
<button
|
||||
aria-label="Shuffle sounds"
|
||||
aria-label={shuffleLabel}
|
||||
className={styles.button}
|
||||
onClick={shuffle}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { BiHeart, BiSolidHeart } from 'react-icons/bi/index';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { cn } from '@/helpers/styles';
|
||||
|
|
@ -16,7 +17,8 @@ interface FavoriteProps {
|
|||
}
|
||||
|
||||
export function Favorite({ id, label }: FavoriteProps) {
|
||||
const isFavorite = useSoundStore(state => state.sounds[id].isFavorite);
|
||||
const { t } = useTranslation();
|
||||
const isFavorite = useSoundStore(state => state.sounds[id]?.isFavorite);
|
||||
const toggleFavorite = useSoundStore(state => state.toggleFavorite);
|
||||
|
||||
const handleToggle = async () => {
|
||||
|
|
@ -36,18 +38,21 @@ export function Favorite({ id, label }: FavoriteProps) {
|
|||
};
|
||||
|
||||
const variants = fade();
|
||||
|
||||
const handleKeyDown = useKeyboardButton(handleToggle);
|
||||
|
||||
const ariaLabel = isFavorite
|
||||
? t('favorite.remove.aria-label', { label: label })
|
||||
: t('favorite.add.aria-label', { label: label });
|
||||
|
||||
if (useSoundStore.getState().sounds[id] === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<button
|
||||
aria-label={ariaLabel}
|
||||
className={cn(styles.favoriteButton, isFavorite && styles.isFavorite)}
|
||||
aria-label={
|
||||
isFavorite
|
||||
? `Remove ${label} Sound from Favorites`
|
||||
: `Add ${label} Sound to Favorites`
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import styles from './range.module.css';
|
||||
|
|
@ -8,14 +9,20 @@ interface RangeProps {
|
|||
}
|
||||
|
||||
export function Range({ id, label }: RangeProps) {
|
||||
const { t } = useTranslation();
|
||||
const setVolume = useSoundStore(state => state.setVolume);
|
||||
const volume = useSoundStore(state => state.sounds[id].volume);
|
||||
const isSelected = useSoundStore(state => state.sounds[id].isSelected);
|
||||
const soundState = useSoundStore(state => state.sounds[id]);
|
||||
const locked = useSoundStore(state => state.locked);
|
||||
|
||||
if (!soundState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { isSelected, volume } = soundState;
|
||||
|
||||
return (
|
||||
<input
|
||||
aria-label={`${label} sound volume`}
|
||||
aria-label={t('volume.aria-label', { label: label })}
|
||||
autoComplete="off"
|
||||
className={styles.range}
|
||||
disabled={!isSelected}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useCallback, useEffect, forwardRef, useMemo } from 'react';
|
||||
import { ImSpinner9 } from 'react-icons/im/index';
|
||||
import { useTranslation } from 'react-i18next'; // 导入 useTranslation
|
||||
|
||||
import { Range } from './range';
|
||||
import { Favorite } from './favorite';
|
||||
|
|
@ -15,17 +16,21 @@ import type { Sound as SoundType } from '@/data/types';
|
|||
|
||||
import { useKeyboardButton } from '@/hooks/use-keyboard-button';
|
||||
|
||||
interface SoundProps extends SoundType {
|
||||
interface SoundProps extends Omit<SoundType, 'label'> {
|
||||
functional: boolean;
|
||||
hidden: boolean;
|
||||
labelKey: string;
|
||||
selectHidden: (key: string) => void;
|
||||
unselectHidden: (key: string) => void;
|
||||
}
|
||||
|
||||
export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
||||
{ functional, hidden, icon, id, label, selectHidden, src, unselectHidden },
|
||||
{ functional, hidden, icon, id, labelKey, selectHidden, src, unselectHidden },
|
||||
ref,
|
||||
) {
|
||||
const { t } = useTranslation(); // 获取 t 函数
|
||||
const translatedLabel = useMemo(() => t(labelKey), [t, labelKey]);
|
||||
|
||||
const isPlaying = useSoundStore(state => state.isPlaying);
|
||||
const play = useSoundStore(state => state.play);
|
||||
const selectSound = useSoundStore(state => state.select);
|
||||
|
|
@ -43,22 +48,22 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
|||
|
||||
const isLoading = useLoadingStore(state => state.loaders[src]);
|
||||
|
||||
const sound = useSound(src, { loop: true, volume: adjustedVolume });
|
||||
const soundControls = useSound(src, { loop: true, volume: adjustedVolume });
|
||||
|
||||
useEffect(() => {
|
||||
if (locked) return;
|
||||
|
||||
if (isSelected && isPlaying && functional) {
|
||||
sound?.play();
|
||||
soundControls?.play();
|
||||
} else {
|
||||
sound?.pause();
|
||||
soundControls?.pause();
|
||||
}
|
||||
}, [isSelected, sound, isPlaying, functional, locked]);
|
||||
}, [isSelected, soundControls, isPlaying, functional, locked]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hidden && isSelected) selectHidden(label);
|
||||
else if (hidden && !isSelected) unselectHidden(label);
|
||||
}, [label, isSelected, hidden, selectHidden, unselectHidden]);
|
||||
if (hidden && isSelected) selectHidden(labelKey);
|
||||
else if (hidden && !isSelected) unselectHidden(labelKey);
|
||||
}, [labelKey, isSelected, hidden, selectHidden, unselectHidden]);
|
||||
|
||||
const select = useCallback(() => {
|
||||
if (locked) return;
|
||||
|
|
@ -82,13 +87,11 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
|||
toggle();
|
||||
}, [toggle]);
|
||||
|
||||
const handleKeyDown = useKeyboardButton(() => {
|
||||
toggle();
|
||||
});
|
||||
const handleKeyDown = useKeyboardButton(toggle);
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label={`${label} sound`}
|
||||
aria-label={t('sounds.aria-label', { name: translatedLabel })}
|
||||
ref={ref}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
|
@ -100,7 +103,7 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
|||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Favorite id={id} label={label} />
|
||||
<Favorite id={id} label={translatedLabel} />
|
||||
<div className={styles.icon}>
|
||||
{isLoading ? (
|
||||
<span aria-hidden="true" className={styles.spinner}>
|
||||
|
|
@ -111,9 +114,9 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
|
|||
)}
|
||||
</div>
|
||||
<div className={styles.label} id={id}>
|
||||
{label}
|
||||
{translatedLabel}
|
||||
</div>
|
||||
<Range id={id} label={label} />
|
||||
<Range id={id} label={translatedLabel} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Sound } from './sound';
|
||||
import { useLocalStorage } from '@/hooks/use-local-storage';
|
||||
import { cn } from '@/helpers/styles';
|
||||
import { fade, scale, mix } from '@/lib/motion';
|
||||
|
||||
import styles from './sounds.module.css';
|
||||
|
||||
import type { Sounds } from '@/data/types';
|
||||
import type { Sounds as SoundsType } from '@/data/types';
|
||||
|
||||
interface SoundsProps {
|
||||
functional: boolean;
|
||||
id: string;
|
||||
sounds: Sounds;
|
||||
sounds: SoundsType;
|
||||
}
|
||||
|
||||
export function Sounds({ functional, id, sounds }: SoundsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false);
|
||||
const [clickedMore, setClickedMore] = useState(false);
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
|
|||
<div className={styles.sounds}>
|
||||
{sounds.map((sound, index) => (
|
||||
<Sound
|
||||
key={sound.label}
|
||||
key={sound.id}
|
||||
{...sound}
|
||||
functional={functional}
|
||||
hidden={!showAll && index > 5}
|
||||
|
|
@ -84,7 +84,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
|
|||
{sounds.length < 2 &&
|
||||
new Array(2 - sounds.length)
|
||||
.fill(null)
|
||||
.map((_, index) => <div key={index} />)}
|
||||
.map((_, index) => <div key={`placeholder-${index}`} />)}
|
||||
</div>
|
||||
|
||||
{sounds.length > 6 && (
|
||||
|
|
@ -106,7 +106,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
|
|||
onAnimationComplete={() => setIsAnimating(false)}
|
||||
onAnimationStart={() => setIsAnimating(true)}
|
||||
>
|
||||
{showAll ? 'Show Less' : 'Show More'}
|
||||
{showAll ? t('sounds.show-less') : t('sounds.show-more')}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
---
|
||||
import { FaGithub } from 'react-icons/fa/index';
|
||||
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
import { SpecialButton } from './special-button';
|
||||
import { Container } from './container';
|
||||
import { Binary } from './binary';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
const t = await getTranslator(currentLocale);
|
||||
---
|
||||
|
||||
<div class="source">
|
||||
|
|
@ -16,12 +19,12 @@ import { Binary } from './binary';
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="title"><span>Open Source</span></h2>
|
||||
<p class="desc">Moodist is free and open-source!</p>
|
||||
<h2 class="title"><span>{t('source.title')}</span></h2>
|
||||
<p class="desc">{t('source.desc')}</p>
|
||||
|
||||
<div class="button">
|
||||
<SpecialButton href="https://github.com/remvze/moodist">
|
||||
Source Code
|
||||
{t('toolbar.items.source-code')}
|
||||
</SpecialButton>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { FaHeadphonesAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface BinauralProps {
|
||||
|
|
@ -7,7 +7,13 @@ interface BinauralProps {
|
|||
}
|
||||
|
||||
export function Binaural({ open }: BinauralProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
|
||||
<Item
|
||||
icon={<FaHeadphonesAlt />}
|
||||
label={t('toolbar.items.binaural')}
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoMdFlower } from 'react-icons/io/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface BreathingExerciseProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface BreathingExerciseProps {
|
|||
}
|
||||
|
||||
export function BreathingExercise({ open }: BreathingExerciseProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<IoMdFlower />}
|
||||
label="Breathing Exercise"
|
||||
label={t('toolbar.items.breathing')}
|
||||
shortcut="Shift + B"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdOutlineTimer } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface CountdownProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface CountdownProps {
|
|||
}
|
||||
|
||||
export function Countdown({ open }: CountdownProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdOutlineTimer />}
|
||||
label="Countdown Timer"
|
||||
label={t('toolbar.items.countdown')}
|
||||
shortcut="Shift + C"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { SiBuymeacoffee } from 'react-icons/si/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Donate() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
href="https://buymeacoffee.com/remvze"
|
||||
icon={<SiBuymeacoffee />}
|
||||
label="Buy Me a Coffee"
|
||||
icon={<SiBuymeacoffee />} // Icon
|
||||
label={t('toolbar.items.buy-me-a-coffee')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { TbWaveSine } from 'react-icons/tb/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface IsochronicProps {
|
||||
|
|
@ -7,5 +7,13 @@ interface IsochronicProps {
|
|||
}
|
||||
|
||||
export function Isochronic({ open }: IsochronicProps) {
|
||||
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
|
||||
const { t } = useTranslation(); // Get t function
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<TbWaveSine />}
|
||||
label={t('toolbar.items.isochronic')}
|
||||
onClick={open}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdNotes } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useNoteStore } from '@/stores/note';
|
||||
|
|
@ -9,13 +9,14 @@ interface NotepadProps {
|
|||
}
|
||||
|
||||
export function Notepad({ open }: NotepadProps) {
|
||||
const { t } = useTranslation();
|
||||
const note = useNoteStore(state => state.note);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={!!note.length}
|
||||
icon={<MdNotes />}
|
||||
label="Notepad"
|
||||
label={t('toolbar.items.notepad')}
|
||||
shortcut="Shift + N"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { MdOutlineAvTimer } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||
|
||||
interface PomodoroProps {
|
||||
|
|
@ -9,13 +8,14 @@ interface PomodoroProps {
|
|||
}
|
||||
|
||||
export function Pomodoro({ open }: PomodoroProps) {
|
||||
const { t } = useTranslation();
|
||||
const running = usePomodoroStore(state => state.running);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={running}
|
||||
icon={<MdOutlineAvTimer />}
|
||||
label="Pomodoro"
|
||||
label={t('toolbar.items.pomodoro')}
|
||||
shortcut="Shift + P"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { RiPlayListFill } from 'react-icons/ri/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface PresetsProps {
|
||||
|
|
@ -7,10 +7,11 @@ interface PresetsProps {
|
|||
}
|
||||
|
||||
export function Presets({ open }: PresetsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Item
|
||||
icon={<RiPlayListFill />}
|
||||
label="Your Presets"
|
||||
label={t('toolbar.items.presets')}
|
||||
shortcut="Shift + Alt + P"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoShareSocialSharp } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
|
@ -9,13 +9,14 @@ interface ShareProps {
|
|||
}
|
||||
|
||||
export function Share({ open }: ShareProps) {
|
||||
const { t } = useTranslation();
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
||||
return (
|
||||
<Item
|
||||
disabled={noSelected}
|
||||
icon={<IoShareSocialSharp />}
|
||||
label="Share Sounds"
|
||||
label={t('toolbar.items.share')}
|
||||
shortcut="Shift + S"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdKeyboardCommandKey } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface ShortcutsProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface ShortcutsProps {
|
|||
}
|
||||
|
||||
export function Shortcuts({ open }: ShortcutsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdKeyboardCommandKey />}
|
||||
label="Shortcuts"
|
||||
label={t('toolbar.items.shortcuts')}
|
||||
shortcut="Shift + H"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { BiShuffle } from 'react-icons/bi/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Shuffle() {
|
||||
const { t } = useTranslation();
|
||||
const shuffle = useSoundStore(state => state.shuffle);
|
||||
const locked = useSoundStore(state => state.locked);
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export function Shuffle() {
|
|||
<Item
|
||||
disabled={locked}
|
||||
icon={<BiShuffle />}
|
||||
label="Shuffle Sounds"
|
||||
label={t('toolbar.items.shuffle')}
|
||||
onClick={shuffle}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { IoMoonSharp } from 'react-icons/io5/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSleepTimerStore } from '@/stores/sleep-timer';
|
||||
import { Item } from '../item';
|
||||
|
||||
|
|
@ -8,13 +8,14 @@ interface SleepTimerProps {
|
|||
}
|
||||
|
||||
export function SleepTimer({ open }: SleepTimerProps) {
|
||||
const { t } = useTranslation();
|
||||
const active = useSleepTimerStore(state => state.active);
|
||||
|
||||
return (
|
||||
<Item
|
||||
active={active}
|
||||
icon={<IoMoonSharp />}
|
||||
label="Sleep Timer"
|
||||
label={t('toolbar.items.sleep-timer')}
|
||||
shortcut="Shift + Alt + T"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
import { LuGithub } from 'react-icons/lu/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
export function Source() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
href="https://github.com/remvze/moodist"
|
||||
icon={<LuGithub />}
|
||||
label="Source Code"
|
||||
label={t('toolbar.items.source-code')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { MdTaskAlt } from 'react-icons/md/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Item } from '../item';
|
||||
|
||||
interface TodoProps {
|
||||
|
|
@ -7,10 +7,12 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ open }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Item
|
||||
icon={<MdTaskAlt />}
|
||||
label="Todo Checklist"
|
||||
label={t('toolbar.items.todo')}
|
||||
shortcut="Shift + T"
|
||||
onClick={open}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { IoMenu, IoClose } from 'react-icons/io5/index';
|
|||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
ShuffleItem,
|
||||
ShareItem,
|
||||
|
|
@ -39,6 +39,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
|
|||
import { closeModals } from '@/lib/modal';
|
||||
|
||||
export function Menu() {
|
||||
const { t } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const noSelected = useSoundStore(state => state.noSelected());
|
||||
|
|
@ -100,7 +101,10 @@ export function Menu() {
|
|||
<div className={styles.wrapper}>
|
||||
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button aria-label="Menu" className={styles.menuButton}>
|
||||
<button
|
||||
aria-label={t('toolbar.menu-aria-label')}
|
||||
className={styles.menuButton}
|
||||
>
|
||||
{isOpen ? <IoClose /> : <IoMenu />}
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
|
@ -143,7 +147,9 @@ export function Menu() {
|
|||
<Divider />
|
||||
|
||||
<div className={styles.globalVolume}>
|
||||
<label htmlFor="global-volume">Global Volume</label>
|
||||
<label htmlFor="global-volume" id="global-volume-label">
|
||||
{t('toolbar.global-volume-label')}
|
||||
</label>
|
||||
<Slider
|
||||
max={100}
|
||||
min={0}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { BiUpArrowAlt } from 'react-icons/bi/index';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { mix, fade, slideY } from '@/lib/motion';
|
||||
|
||||
import styles from './scroll-to-top.module.css';
|
||||
|
||||
export function ScrollToTop() {
|
||||
const { t } = useTranslation();
|
||||
const TOP = 50;
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
|
@ -34,7 +36,7 @@ export function ScrollToTop() {
|
|||
{isVisible ? (
|
||||
<motion.button
|
||||
animate="show"
|
||||
aria-label="Scroll to top"
|
||||
aria-label={t('toolbar.scroll-to-top.aria-label')}
|
||||
className={styles.button}
|
||||
exit="hidden"
|
||||
initial="hidden"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||
|
|
@ -14,6 +14,7 @@ interface CountdownProps {
|
|||
}
|
||||
|
||||
export function Countdown({ onClose, show }: CountdownProps) {
|
||||
const { t } = useTranslation();
|
||||
const [hours, setHours] = useState(0);
|
||||
const [minutes, setMinutes] = useState(0);
|
||||
const [seconds, setSeconds] = useState(0);
|
||||
|
|
@ -73,8 +74,8 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Countdown Timer</h2>
|
||||
<p className={styles.desc}>Super simple countdown timer.</p>
|
||||
<h2 className={styles.title}>{t('modals.countdown.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.countdown.description')}</p>
|
||||
</header>
|
||||
|
||||
{isFormVisible ? (
|
||||
|
|
@ -82,21 +83,11 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
<div className={styles.inputContainer}>
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="HH"
|
||||
placeholder={t('modals.countdown.placeholder-hh') || 'HH'} // Placeholder
|
||||
type="number"
|
||||
value={hours}
|
||||
onChange={e => setHours(Math.max(0, parseInt(e.target.value)))}
|
||||
/>
|
||||
|
||||
<span>:</span>
|
||||
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="MM"
|
||||
type="number"
|
||||
value={minutes}
|
||||
value={hours === 0 ? '' : hours} // Show empty if 0
|
||||
onChange={e =>
|
||||
setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value))))
|
||||
setHours(Math.max(0, parseInt(e.target.value || '0')))
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -104,21 +95,34 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder="SS"
|
||||
placeholder={t('modals.countdown.placeholder-mm') || 'MM'}
|
||||
type="number"
|
||||
value={seconds}
|
||||
value={minutes === 0 ? '' : minutes} // Show empty if 0
|
||||
onChange={e =>
|
||||
setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value))))
|
||||
setMinutes(
|
||||
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<span>:</span>
|
||||
<input
|
||||
className={styles.input}
|
||||
placeholder={t('modals.countdown.placeholder-ss') || 'SS'}
|
||||
type="number"
|
||||
value={seconds === 0 ? '' : seconds} // Show empty if 0
|
||||
onChange={e =>
|
||||
setSeconds(
|
||||
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={handleStart}
|
||||
>
|
||||
Start
|
||||
{t('common.start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -128,17 +132,15 @@ export function Countdown({ onClose, show }: CountdownProps) {
|
|||
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
|
||||
<span>{formatTime(timeLeft)}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<button className={styles.button} onClick={handleBack}>
|
||||
Back
|
||||
{t('common.back')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={cn(styles.button, styles.primary)}
|
||||
onClick={toggleTimer}
|
||||
>
|
||||
{isActive ? 'Pause' : 'Start'}
|
||||
{isActive ? t('common.pause') : t('common.start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { BiTrash } from 'react-icons/bi/index';
|
|||
import { LuCopy, LuDownload } from 'react-icons/lu/index';
|
||||
import { FaCheck } from 'react-icons/fa6/index';
|
||||
import { FaUndo } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Button } from './button';
|
||||
|
||||
|
|
@ -19,6 +19,7 @@ interface NotepadProps {
|
|||
}
|
||||
|
||||
export function Notepad({ onClose, show }: NotepadProps) {
|
||||
const { t } = useTranslation();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const note = useNoteStore(state => state.note);
|
||||
|
|
@ -45,26 +46,42 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
const counterOptions = {
|
||||
chars: characters,
|
||||
chars_plural:
|
||||
characters !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
|
||||
words: words,
|
||||
words_plural:
|
||||
words !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
|
||||
};
|
||||
|
||||
const clearOrRestoreTooltip = history
|
||||
? t('modals.notepad.restore-tooltip')
|
||||
: t('modals.notepad.clear-tooltip');
|
||||
const copyTooltip = copying
|
||||
? t('common.copied')
|
||||
: t('modals.notepad.copy-tooltip');
|
||||
|
||||
return (
|
||||
<Modal show={show} wide onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.label}>Your Note</h2>
|
||||
<h2 className={styles.label}>{t('modals.notepad.title-label')}</h2>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={copying ? <FaCheck /> : <LuCopy />}
|
||||
tooltip="Copy Note"
|
||||
tooltip={copyTooltip}
|
||||
onClick={() => copy(note)}
|
||||
/>
|
||||
<Button
|
||||
icon={<LuDownload />}
|
||||
tooltip="Download Note"
|
||||
onClick={() => download('Moodit Note.txt', note)}
|
||||
tooltip={t('modals.notepad.download-tooltip')}
|
||||
onClick={() => download('Moodist Note.txt', note)}
|
||||
/>
|
||||
<Button
|
||||
critical={!history}
|
||||
icon={history ? <FaUndo /> : <BiTrash />}
|
||||
recommended={!!history}
|
||||
tooltip={history ? 'Restore Note' : 'Clear Note'}
|
||||
tooltip={clearOrRestoreTooltip}
|
||||
onClick={() => (history ? restore() : clear())}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -73,7 +90,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
<textarea
|
||||
className={styles.textarea}
|
||||
dir="auto"
|
||||
placeholder="What is on your mind?"
|
||||
placeholder={t('modals.notepad.placeholder')}
|
||||
ref={textareaRef}
|
||||
spellCheck={false}
|
||||
value={note}
|
||||
|
|
@ -82,8 +99,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
|
|||
/>
|
||||
|
||||
<p className={styles.counter}>
|
||||
{characters} character{characters !== 1 && 's'} • {words} word
|
||||
{words !== 1 && 's'}
|
||||
{t('modals.notepad.counter-stats', counterOptions)}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
|
||||
import { IoMdSettings } from 'react-icons/io/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Button } from '../generics/button';
|
||||
import { Timer } from './timer';
|
||||
|
|
@ -12,7 +12,6 @@ import { useLocalStorage } from '@/hooks/use-local-storage';
|
|||
import { useSoundEffect } from '@/hooks/use-sound-effect';
|
||||
import { usePomodoroStore } from '@/stores/pomodoro';
|
||||
import { useCloseListener } from '@/hooks/use-close-listener';
|
||||
|
||||
import styles from './pomodoro.module.css';
|
||||
|
||||
interface PomodoroProps {
|
||||
|
|
@ -22,6 +21,7 @@ interface PomodoroProps {
|
|||
}
|
||||
|
||||
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
||||
const { t } = useTranslation();
|
||||
const [showSetting, setShowSetting] = useState(false);
|
||||
|
||||
const [selectedTab, setSelectedTab] = useState('pomodoro');
|
||||
|
|
@ -56,11 +56,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
|
||||
const tabs = useMemo(
|
||||
() => [
|
||||
{ id: 'pomodoro', label: 'Pomodoro' },
|
||||
{ id: 'short', label: 'Break' },
|
||||
{ id: 'long', label: 'Long Break' },
|
||||
{ id: 'pomodoro', label: t('modals.pomodoro.tabs.pomodoro') },
|
||||
{ id: 'short', label: t('modals.pomodoro.tabs.short-break') },
|
||||
{ id: 'long', label: t('modals.pomodoro.tabs.long-break') },
|
||||
],
|
||||
[],
|
||||
[t],
|
||||
);
|
||||
|
||||
useCloseListener(() => setShowSetting(false));
|
||||
|
|
@ -123,12 +123,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
<>
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Pomodoro Timer</h2>
|
||||
|
||||
<h2 className={styles.title}>{t('modals.pomodoro.title')}</h2>
|
||||
<div className={styles.button}>
|
||||
<Button
|
||||
icon={<IoMdSettings />}
|
||||
tooltip="Change Times"
|
||||
tooltip={t('modals.pomodoro.settings-tooltip')}
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setShowSetting(true);
|
||||
|
|
@ -142,19 +141,21 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
|
|||
|
||||
<div className={styles.control}>
|
||||
<p className={styles.completed}>
|
||||
{completions[selectedTab] || 0} completed
|
||||
{t('modals.pomodoro.completed', {
|
||||
count: completions[selectedTab] || 0,
|
||||
})}
|
||||
</p>
|
||||
<div className={styles.buttons}>
|
||||
<Button
|
||||
icon={<FaUndo />}
|
||||
smallIcon
|
||||
tooltip="Restart"
|
||||
tooltip={t('common.restart')}
|
||||
onClick={restart}
|
||||
/>
|
||||
<Button
|
||||
icon={running ? <FaPause /> : <FaPlay />}
|
||||
smallIcon
|
||||
tooltip={running ? 'Pause' : 'Start'}
|
||||
tooltip={running ? t('common.pause') : t('common.start')}
|
||||
onClick={toggleRunning}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
|
||||
import styles from './setting.module.css';
|
||||
|
|
@ -12,6 +12,7 @@ interface SettingProps {
|
|||
}
|
||||
|
||||
export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
||||
const { t } = useTranslation();
|
||||
const [values, setValues] = useState<Record<string, number | string>>(times);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -46,34 +47,34 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
|||
|
||||
return (
|
||||
<Modal lockBody={false} show={show} onClose={onClose}>
|
||||
<h2 className={styles.title}>Change Times</h2>
|
||||
<h2 className={styles.title}>{t('modals.pomodoro.settings.title')}</h2>
|
||||
|
||||
<form className={styles.form} onSubmit={handleSubmit}>
|
||||
<Field
|
||||
id="pomodoro"
|
||||
label="Pomodoro"
|
||||
labelKey="modals.pomodoro.settings.pomodoro-label"
|
||||
value={values.pomodoro}
|
||||
onChange={handleChange('pomodoro')}
|
||||
/>
|
||||
<Field
|
||||
id="short"
|
||||
label="Short Break"
|
||||
labelKey="modals.pomodoro.settings.short-break-label"
|
||||
value={values.short}
|
||||
onChange={handleChange('short')}
|
||||
/>
|
||||
<Field
|
||||
id="long"
|
||||
label="Long Break"
|
||||
labelKey="modals.pomodoro.settings.long-break-label"
|
||||
value={values.long}
|
||||
onChange={handleChange('long')}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<button type="button" onClick={handleCancel}>
|
||||
Cancel
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button className={styles.primary} type="submit">
|
||||
Save
|
||||
{t('common.save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -83,19 +84,22 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
|
|||
|
||||
interface FieldProps {
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
onChange: (value: number | string) => void;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
function Field({ id, label, onChange, value }: FieldProps) {
|
||||
function Field({ id, labelKey, onChange, value }: FieldProps) {
|
||||
const { t } = useTranslation(); // 获取翻译函数
|
||||
return (
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label} htmlFor={id}>
|
||||
{label} <span>(minutes)</span>
|
||||
{t(labelKey)}{' '}
|
||||
<span>({t('modals.pomodoro.settings.minutes-unit')})</span>{' '}
|
||||
</label>
|
||||
<input
|
||||
className={styles.input}
|
||||
id={id}
|
||||
max={120}
|
||||
min={1}
|
||||
required
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
|
||||
import styles from './form.module.css';
|
||||
|
||||
export function Form() {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const addTodo = useTodoStore(state => state.addTodo);
|
||||
|
|
@ -22,12 +23,12 @@ export function Form() {
|
|||
<form onSubmit={handleSubmit}>
|
||||
<div className={styles.wrapper}>
|
||||
<input
|
||||
placeholder="I have to ..."
|
||||
placeholder={t('modals.todo.add-placeholder')}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<button type="submit">Add</button>
|
||||
<button type="submit">{t('modals.todo.add-button')}</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal } from '@/components/modal';
|
||||
import { Form } from './form';
|
||||
import { Todos } from './todos';
|
||||
|
|
@ -10,11 +11,13 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ onClose, show }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal show={show} onClose={onClose}>
|
||||
<header className={styles.header}>
|
||||
<h2 className={styles.title}>Todo Checklist</h2>
|
||||
<p className={styles.desc}>Super simple todo list.</p>
|
||||
<h2 className={styles.title}>{t('modals.todo.title')}</h2>
|
||||
<p className={styles.desc}>{t('modals.todo.description')}</p>
|
||||
</header>
|
||||
|
||||
<Form />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { FaRegTrashAlt } from 'react-icons/fa/index';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Checkbox } from '@/components/checkbox';
|
||||
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
import { cn } from '@/helpers/styles';
|
||||
|
||||
import styles from './todo.module.css';
|
||||
import { Tooltip } from '@/components/tooltip';
|
||||
|
||||
interface TodoProps {
|
||||
done: boolean;
|
||||
|
|
@ -14,6 +15,7 @@ interface TodoProps {
|
|||
}
|
||||
|
||||
export function Todo({ done, id, todo }: TodoProps) {
|
||||
const { t } = useTranslation();
|
||||
const deleteTodo = useTodoStore(state => state.deleteTodo);
|
||||
const toggleTodo = useTodoStore(state => state.toggleTodo);
|
||||
const editTodo = useTodoStore(state => state.editTodo);
|
||||
|
|
@ -32,9 +34,17 @@ export function Todo({ done, id, todo }: TodoProps) {
|
|||
value={todo}
|
||||
onChange={e => editTodo(id, e.target.value)}
|
||||
/>
|
||||
<button className={styles.delete} onClick={handleDelete}>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
<Tooltip content={t('common.delete')} showDelay={0}>
|
||||
<button
|
||||
className={styles.delete}
|
||||
aria-label={
|
||||
t('modals.todo.delete-button-aria-label') || `Delete todo: ${todo}`
|
||||
}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<FaRegTrashAlt />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { Todo } from './todo';
|
||||
|
||||
import { useTodoStore } from '@/stores/todo';
|
||||
|
|
@ -5,13 +6,14 @@ import { useTodoStore } from '@/stores/todo';
|
|||
import styles from './todos.module.css';
|
||||
|
||||
export function Todos() {
|
||||
const { t } = useTranslation();
|
||||
const todos = useTodoStore(state => state.todos);
|
||||
const doneCount = useTodoStore(state => state.doneCount());
|
||||
|
||||
return (
|
||||
<div className={styles.todos}>
|
||||
<header>
|
||||
<p className={styles.label}>Your Todos</p>
|
||||
<p className={styles.label}>{t('modals.todo.your-todos-label')}</p>
|
||||
<div className={styles.divider} />
|
||||
<p className={styles.counter}>
|
||||
{doneCount} / {todos.length}
|
||||
|
|
@ -30,7 +32,7 @@ export function Todos() {
|
|||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className={styles.empty}>You don't have any todos.</p>
|
||||
<p className={styles.empty}>{t('modals.todo.empty')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,103 +24,104 @@ import type { Category } from '../types';
|
|||
export const animals: Category = {
|
||||
icon: <FaDog />,
|
||||
id: 'animals',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <PiBirdFill />,
|
||||
id: 'birds',
|
||||
label: 'Birds',
|
||||
labelKey: 'sounds.animals.birds',
|
||||
src: '/sounds/animals/birds.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiSeagull />,
|
||||
id: 'seagulls',
|
||||
label: 'Seagulls',
|
||||
labelKey: 'sounds.animals.seagulls',
|
||||
src: '/sounds/animals/seagulls.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiCricket />,
|
||||
id: 'crickets',
|
||||
label: 'Crickets',
|
||||
labelKey: 'sounds.animals.crickets',
|
||||
src: '/sounds/animals/crickets.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWolfHead />,
|
||||
id: 'wolf',
|
||||
label: 'Wolf',
|
||||
labelKey: 'sounds.animals.wolf',
|
||||
src: '/sounds/animals/wolf.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiOwl />,
|
||||
id: 'owl',
|
||||
label: 'Owl',
|
||||
labelKey: 'sounds.animals.owl',
|
||||
src: '/sounds/animals/owl.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaFrog />,
|
||||
id: 'frog',
|
||||
label: 'Frog',
|
||||
labelKey: 'sounds.animals.frog',
|
||||
src: '/sounds/animals/frog.mp3',
|
||||
},
|
||||
{
|
||||
icon: <PiDogBold />,
|
||||
id: 'dog-barking',
|
||||
label: 'Dog Barking',
|
||||
labelKey: 'sounds.animals.dog-barking',
|
||||
src: '/sounds/animals/dog-barking.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaHorseHead />,
|
||||
id: 'horse-galopp',
|
||||
label: 'Horse Galopp',
|
||||
labelKey: 'sounds.animals.horse-galopp',
|
||||
src: '/sounds/animals/horse-galopp.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaCat />,
|
||||
id: 'cat-purring',
|
||||
label: 'Cat Purring',
|
||||
labelKey: 'sounds.animals.cat-purring',
|
||||
src: '/sounds/animals/cat-purring.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaCrow />,
|
||||
id: 'crows',
|
||||
label: 'Crows',
|
||||
labelKey: 'sounds.animals.crows',
|
||||
src: '/sounds/animals/crows.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWhaleTail />,
|
||||
id: 'whale',
|
||||
label: 'Whale',
|
||||
labelKey: 'sounds.animals.whale',
|
||||
src: '/sounds/animals/whale.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiTreeBeehive />,
|
||||
id: 'beehive',
|
||||
label: 'Beehive',
|
||||
labelKey: 'sounds.animals.beehive',
|
||||
src: '/sounds/animals/beehive.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiEgyptianBird />,
|
||||
id: 'woodpecker',
|
||||
label: 'Woodpecker',
|
||||
labelKey: 'sounds.animals.woodpecker',
|
||||
src: '/sounds/animals/woodpecker.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiChicken />,
|
||||
id: 'chickens',
|
||||
label: 'Chickens',
|
||||
labelKey: 'sounds.animals.chickens',
|
||||
src: '/sounds/animals/chickens.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiCow />,
|
||||
id: 'cows',
|
||||
label: 'Cows',
|
||||
labelKey: 'sounds.animals.cows',
|
||||
src: '/sounds/animals/cows.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiSheep />,
|
||||
id: 'sheep',
|
||||
label: 'Sheep',
|
||||
labelKey: 'sounds.animals.sheep',
|
||||
src: '/sounds/animals/sheep.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Animals',
|
||||
titleKey: 'sounds.animals.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,37 +6,38 @@ import type { Category } from '../types';
|
|||
export const binaural: Category = {
|
||||
icon: <TbWaveSine />,
|
||||
id: 'binaural',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-delta',
|
||||
label: 'Delta',
|
||||
labelKey: 'sounds.binaural.binaural-delta',
|
||||
src: '/sounds/binaural/binaural-delta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-theta',
|
||||
label: 'Theta',
|
||||
labelKey: 'sounds.binaural.binaural-theta',
|
||||
src: '/sounds/binaural/binaural-theta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-alpha',
|
||||
label: 'Alpha',
|
||||
labelKey: 'sounds.binaural.binaural-alpha',
|
||||
src: '/sounds/binaural/binaural-alpha.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-beta',
|
||||
label: 'Beta',
|
||||
labelKey: 'sounds.binaural.binaural-beta',
|
||||
src: '/sounds/binaural/binaural-beta.wav',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'binaural-gamma',
|
||||
label: 'Gamma',
|
||||
labelKey: 'sounds.binaural.binaural-gamma',
|
||||
src: '/sounds/binaural/binaural-gamma.wav',
|
||||
},
|
||||
],
|
||||
title: 'Binaural Beats',
|
||||
titleKey: 'sounds.binaural.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,79 +14,80 @@ import type { Category } from '../types';
|
|||
export const nature: Category = {
|
||||
icon: <BiSolidTree />,
|
||||
id: 'nature',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BiWater />,
|
||||
id: 'river',
|
||||
label: 'River',
|
||||
labelKey: 'sounds.nature.river',
|
||||
src: '/sounds/nature/river.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaWater />,
|
||||
id: 'waves',
|
||||
label: 'Waves',
|
||||
labelKey: 'sounds.nature.waves',
|
||||
src: '/sounds/nature/waves.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsFire />,
|
||||
id: 'campfire',
|
||||
label: 'Campfire',
|
||||
labelKey: 'sounds.nature.campfire',
|
||||
src: '/sounds/nature/campfire.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaWind />,
|
||||
id: 'wind',
|
||||
label: 'Wind',
|
||||
labelKey: 'sounds.nature.wind',
|
||||
src: '/sounds/nature/wind.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaWind />,
|
||||
id: 'howling-wind',
|
||||
label: 'Howling Wind',
|
||||
labelKey: 'sounds.nature.howling-wind',
|
||||
src: '/sounds/nature/howling-wind.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidTree />,
|
||||
id: 'wind-in-trees',
|
||||
label: 'Wind in Trees',
|
||||
labelKey: 'sounds.nature.wind-in-trees',
|
||||
src: '/sounds/nature/wind-in-trees.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWaterfall />,
|
||||
id: 'waterfall',
|
||||
label: 'Waterfall',
|
||||
labelKey: 'sounds.nature.waterfall',
|
||||
src: '/sounds/nature/waterfall.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaRegSnowflake />,
|
||||
id: 'walk-in-snow',
|
||||
label: 'Walk in Snow',
|
||||
labelKey: 'sounds.nature.walk-in-snow',
|
||||
src: '/sounds/nature/walk-in-snow.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaLeaf />,
|
||||
id: 'walk-on-leaves',
|
||||
label: 'Walk on Leaves',
|
||||
labelKey: 'sounds.nature.walk-on-leaves',
|
||||
src: '/sounds/nature/walk-on-leaves.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiStonePile />,
|
||||
id: 'walk-on-gravel',
|
||||
label: 'Walk on Gravel',
|
||||
labelKey: 'sounds.nature.walk-on-gravel',
|
||||
src: '/sounds/nature/walk-on-gravel.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsFillDropletFill />,
|
||||
id: 'droplets',
|
||||
label: 'Droplets',
|
||||
labelKey: 'sounds.nature.droplets',
|
||||
src: '/sounds/nature/droplets.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaTree />,
|
||||
id: 'jungle',
|
||||
label: 'Jungle',
|
||||
labelKey: 'sounds.nature.jungle',
|
||||
src: '/sounds/nature/jungle.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Nature',
|
||||
titleKey: 'sounds.nature.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,25 +6,26 @@ import type { Category } from '../types';
|
|||
export const noise: Category = {
|
||||
icon: <BsSoundwave />,
|
||||
id: 'noise',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <GiSoundWaves />,
|
||||
id: 'white-noise',
|
||||
label: 'White Noise',
|
||||
labelKey: 'sounds.noise.white-noise',
|
||||
src: '/sounds/noise/white-noise.wav',
|
||||
},
|
||||
{
|
||||
icon: <GiSoundWaves />,
|
||||
id: 'pink-noise',
|
||||
label: 'Pink Noise',
|
||||
labelKey: 'sounds.noise.pink-noise',
|
||||
src: '/sounds/noise/pink-noise.wav',
|
||||
},
|
||||
{
|
||||
icon: <GiSoundWaves />,
|
||||
id: 'brown-noise',
|
||||
label: 'Brown Noise',
|
||||
labelKey: 'sounds.noise.brown-noise',
|
||||
src: '/sounds/noise/brown-noise.wav',
|
||||
},
|
||||
],
|
||||
title: 'Noise',
|
||||
titleKey: 'sounds.noise.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,103 +21,104 @@ import type { Category } from '../types';
|
|||
export const places: Category = {
|
||||
icon: <MdLocationPin />,
|
||||
id: 'places',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BiSolidCoffeeAlt />,
|
||||
id: 'cafe',
|
||||
label: 'Cafe',
|
||||
labelKey: 'sounds.places.cafe',
|
||||
src: '/sounds/places/cafe.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidPlaneAlt />,
|
||||
id: 'airport',
|
||||
label: 'Airport',
|
||||
labelKey: 'sounds.places.airport',
|
||||
src: '/sounds/places/airport.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaChurch />,
|
||||
id: 'church',
|
||||
label: 'Church',
|
||||
labelKey: 'sounds.places.church',
|
||||
src: '/sounds/places/church.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdTempleBuddhist />,
|
||||
id: 'temple',
|
||||
label: 'Temple',
|
||||
labelKey: 'sounds.places.temple',
|
||||
src: '/sounds/places/temple.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdConstruction />,
|
||||
id: 'construction-site',
|
||||
label: 'Construction Site',
|
||||
labelKey: 'sounds.places.construction-site',
|
||||
src: '/sounds/places/construction-site.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbScubaMask />,
|
||||
id: 'underwater',
|
||||
label: 'Underwater',
|
||||
labelKey: 'sounds.places.underwater',
|
||||
src: '/sounds/places/underwater.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbBeerFilled />,
|
||||
id: 'crowded-bar',
|
||||
label: 'Crowded Bar',
|
||||
labelKey: 'sounds.places.crowded-bar',
|
||||
src: '/sounds/places/crowded-bar.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiVillage />,
|
||||
id: 'night-village',
|
||||
label: 'Night Village',
|
||||
labelKey: 'sounds.places.night-village',
|
||||
src: '/sounds/places/night-village.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaSubway />,
|
||||
id: 'subway-station',
|
||||
label: 'Subway Station',
|
||||
labelKey: 'sounds.places.subway-station',
|
||||
src: '/sounds/places/subway-station.mp3',
|
||||
},
|
||||
{
|
||||
icon: <HiOfficeBuilding />,
|
||||
id: 'office',
|
||||
label: 'Office',
|
||||
labelKey: 'sounds.places.office',
|
||||
src: '/sounds/places/office.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaShoppingBasket />,
|
||||
id: 'supermarket',
|
||||
label: 'Supermarket',
|
||||
labelKey: 'sounds.places.supermarket',
|
||||
src: '/sounds/places/supermarket.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiCarousel />,
|
||||
id: 'carousel',
|
||||
label: 'Carousel',
|
||||
labelKey: 'sounds.places.carousel',
|
||||
src: '/sounds/places/carousel.mp3',
|
||||
},
|
||||
{
|
||||
icon: <AiFillExperiment />,
|
||||
id: 'laboratory',
|
||||
label: 'Laboratory',
|
||||
labelKey: 'sounds.places.laboratory',
|
||||
src: '/sounds/places/laboratory.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidDryer />,
|
||||
id: 'laundry-room',
|
||||
label: 'Laundry Room',
|
||||
labelKey: 'sounds.places.laundry-room',
|
||||
src: '/sounds/places/laundry-room.mp3',
|
||||
},
|
||||
{
|
||||
icon: <IoRestaurant />,
|
||||
id: 'restaurant',
|
||||
label: 'Restaurant',
|
||||
labelKey: 'sounds.places.restaurant',
|
||||
src: '/sounds/places/restaurant.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaBookOpen />,
|
||||
id: 'library',
|
||||
label: 'Library',
|
||||
labelKey: 'sounds.places.library',
|
||||
src: '/sounds/places/library.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Places',
|
||||
titleKey: 'sounds.places.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,55 +13,56 @@ import type { Category } from '../types';
|
|||
export const rain: Category = {
|
||||
icon: <BsFillCloudRainFill />,
|
||||
id: 'rain',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BsFillCloudRainFill />,
|
||||
id: 'light-rain',
|
||||
label: 'Light Rain',
|
||||
labelKey: 'sounds.rain.light-rain',
|
||||
src: '/sounds/rain/light-rain.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsFillCloudRainHeavyFill />,
|
||||
id: 'heavy-rain',
|
||||
label: 'Heavy Rain',
|
||||
labelKey: 'sounds.rain.heavy-rain',
|
||||
src: '/sounds/rain/heavy-rain.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdOutlineThunderstorm />,
|
||||
id: 'thunder',
|
||||
label: 'Thunder',
|
||||
labelKey: 'sounds.rain.thunder',
|
||||
src: '/sounds/rain/thunder.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWindow />,
|
||||
id: 'rain-on-window',
|
||||
label: 'Rain on Window',
|
||||
labelKey: 'sounds.rain.rain-on-window',
|
||||
src: '/sounds/rain/rain-on-window.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaCarSide />,
|
||||
id: 'rain-on-car-roof',
|
||||
label: 'Rain on Car Roof',
|
||||
labelKey: 'sounds.rain.rain-on-car-roof',
|
||||
src: '/sounds/rain/rain-on-car-roof.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsUmbrellaFill />,
|
||||
id: 'rain-on-umbrella',
|
||||
label: 'Rain on Umbrella',
|
||||
labelKey: 'sounds.rain.rain-on-umbrella',
|
||||
src: '/sounds/rain/rain-on-umbrella.mp3',
|
||||
},
|
||||
{
|
||||
icon: <PiTentFill />,
|
||||
id: 'rain-on-tent',
|
||||
label: 'Rain on Tent',
|
||||
labelKey: 'sounds.rain.rain-on-tent',
|
||||
src: '/sounds/rain/rain-on-tent.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaLeaf />,
|
||||
id: 'rain-on-leaves',
|
||||
label: 'Rain on Leaves',
|
||||
labelKey: 'sounds.rain.rain-on-leaves',
|
||||
src: '/sounds/rain/rain-on-leaves.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Rain',
|
||||
titleKey: 'sounds.rain.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,103 +17,104 @@ import type { Category } from '../types';
|
|||
export const things: Category = {
|
||||
icon: <MdSmartToy />,
|
||||
id: 'things',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BsFillKeyboardFill />,
|
||||
id: 'keyboard',
|
||||
label: 'Keyboard',
|
||||
labelKey: 'sounds.things.keyboard',
|
||||
src: '/sounds/things/keyboard.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaKeyboard />,
|
||||
id: 'typewriter',
|
||||
label: 'Typewriter',
|
||||
labelKey: 'sounds.things.typewriter',
|
||||
src: '/sounds/things/typewriter.mp3',
|
||||
},
|
||||
{
|
||||
icon: <RiFilePaper2Fill />,
|
||||
id: 'paper',
|
||||
label: 'Paper',
|
||||
labelKey: 'sounds.things.paper',
|
||||
src: '/sounds/things/paper.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaClock />,
|
||||
id: 'clock',
|
||||
label: 'Clock',
|
||||
labelKey: 'sounds.things.clock',
|
||||
src: '/sounds/things/clock.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWindchimes />,
|
||||
id: 'wind-chimes',
|
||||
label: 'Wind Chimes',
|
||||
labelKey: 'sounds.things.wind-chimes',
|
||||
src: '/sounds/things/wind-chimes.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbBowlFilled />,
|
||||
id: 'singing-bowl',
|
||||
label: 'Singing Bowl',
|
||||
labelKey: 'sounds.things.singing-bowl',
|
||||
src: '/sounds/things/singing-bowl.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaFan />,
|
||||
id: 'ceiling-fan',
|
||||
label: 'Ceiling Fan',
|
||||
labelKey: 'sounds.things.ceiling-fan',
|
||||
src: '/sounds/things/ceiling-fan.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidDryer />,
|
||||
id: 'dryer',
|
||||
label: 'Dryer',
|
||||
labelKey: 'sounds.things.dryer',
|
||||
src: '/sounds/things/dryer.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiFilmProjector />,
|
||||
id: 'slide-projector',
|
||||
label: 'Slide Projector',
|
||||
labelKey: 'sounds.things.slide-projector',
|
||||
src: '/sounds/things/slide-projector.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdWaterDrop />,
|
||||
id: 'boiling-water',
|
||||
label: 'Boiling Water',
|
||||
labelKey: 'sounds.things.boiling-water',
|
||||
src: '/sounds/things/boiling-water.mp3',
|
||||
},
|
||||
{
|
||||
icon: <RiBubbleChartFill />,
|
||||
id: 'bubbles',
|
||||
label: 'Bubbles',
|
||||
labelKey: 'sounds.things.bubbles',
|
||||
src: '/sounds/things/bubbles.mp3',
|
||||
},
|
||||
{
|
||||
icon: <MdRadio />,
|
||||
id: 'tuning-radio',
|
||||
label: 'Tuning Radio',
|
||||
labelKey: 'sounds.things.tuning-radio',
|
||||
src: '/sounds/things/tuning-radio.mp3',
|
||||
},
|
||||
{
|
||||
icon: <IoIosRadio />,
|
||||
id: 'morse-code',
|
||||
label: 'Morse Code',
|
||||
labelKey: 'sounds.things.morse-code',
|
||||
src: '/sounds/things/morse-code.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiWashingMachine />,
|
||||
id: 'washing-machine',
|
||||
label: 'Washing Machine',
|
||||
labelKey: 'sounds.things.washing-machine',
|
||||
src: '/sounds/things/washing-machine.mp3',
|
||||
},
|
||||
{
|
||||
icon: <PiVinylRecord />,
|
||||
id: 'vinyl-effect',
|
||||
label: 'Vinyl Effect',
|
||||
labelKey: 'sounds.things.vinyl-effect',
|
||||
src: '/sounds/things/vinyl-effect.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbWiper />,
|
||||
id: 'windshield-wipers',
|
||||
label: 'Windshield Wipers',
|
||||
labelKey: 'sounds.things.windshield-wipers',
|
||||
src: '/sounds/things/windshield-wipers.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Things',
|
||||
titleKey: 'sounds.things.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -8,43 +8,44 @@ import type { Category } from '../types';
|
|||
export const transport: Category = {
|
||||
icon: <FaCarSide />,
|
||||
id: 'transport',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <BiSolidTrain />,
|
||||
id: 'train',
|
||||
label: 'Train',
|
||||
labelKey: 'sounds.transport.train',
|
||||
src: '/sounds/transport/train.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidTrain />,
|
||||
id: 'inside-a-train',
|
||||
label: 'Inside a Train',
|
||||
labelKey: 'sounds.transport.inside-a-train',
|
||||
src: '/sounds/transport/inside-a-train.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidPlaneAlt />,
|
||||
id: 'airplane',
|
||||
label: 'Airplane',
|
||||
labelKey: 'sounds.transport.airplane',
|
||||
src: '/sounds/transport/airplane.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiSubmarine />,
|
||||
id: 'submarine',
|
||||
label: 'Submarine',
|
||||
labelKey: 'sounds.transport.submarine',
|
||||
src: '/sounds/transport/submarine.mp3',
|
||||
},
|
||||
{
|
||||
icon: <GiSailboat />,
|
||||
id: 'sailboat',
|
||||
label: 'Sailboat',
|
||||
labelKey: 'sounds.transport.sailboat',
|
||||
src: '/sounds/transport/sailboat.mp3',
|
||||
},
|
||||
{
|
||||
icon: <TbSailboat />,
|
||||
id: 'rowing-boat',
|
||||
label: 'Rowing Boat',
|
||||
labelKey: 'sounds.transport.rowing-boat',
|
||||
src: '/sounds/transport/rowing-boat.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Transport',
|
||||
titleKey: 'sounds.transport.title',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,49 +9,50 @@ import type { Category } from '../types';
|
|||
export const urban: Category = {
|
||||
icon: <FaCity />,
|
||||
id: 'urban',
|
||||
// 修改
|
||||
sounds: [
|
||||
{
|
||||
icon: <PiRoadHorizonFill />,
|
||||
id: 'highway',
|
||||
label: 'Highway',
|
||||
labelKey: 'sounds.urban.highway',
|
||||
src: '/sounds/urban/highway.mp3',
|
||||
},
|
||||
{
|
||||
icon: <FaRoad />,
|
||||
id: 'road',
|
||||
label: 'Road',
|
||||
labelKey: 'sounds.urban.road',
|
||||
src: '/sounds/urban/road.mp3',
|
||||
},
|
||||
{
|
||||
icon: <PiSirenBold />,
|
||||
id: 'ambulance-siren',
|
||||
label: 'Ambulance Siren',
|
||||
labelKey: 'sounds.urban.ambulance-siren',
|
||||
src: '/sounds/urban/ambulance-siren.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsSoundwave />,
|
||||
id: 'busy-street',
|
||||
label: 'Busy Street',
|
||||
labelKey: 'sounds.urban.busy-street',
|
||||
src: '/sounds/urban/busy-street.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BsPeopleFill />,
|
||||
id: 'crowd',
|
||||
label: 'Crowd',
|
||||
labelKey: 'sounds.urban.crowd',
|
||||
src: '/sounds/urban/crowd.mp3',
|
||||
},
|
||||
{
|
||||
icon: <BiSolidTraffic />,
|
||||
id: 'traffic',
|
||||
label: 'Traffic',
|
||||
labelKey: 'sounds.urban.traffic',
|
||||
src: '/sounds/urban/traffic.mp3',
|
||||
},
|
||||
{
|
||||
icon: <RiSparkling2Fill />,
|
||||
id: 'fireworks',
|
||||
label: 'Fireworks',
|
||||
labelKey: 'sounds.urban.fireworks',
|
||||
src: '/sounds/urban/fireworks.mp3',
|
||||
},
|
||||
],
|
||||
title: 'Urban',
|
||||
titleKey: 'sounds.urban.title',
|
||||
};
|
||||
|
|
|
|||
4
src/data/types.d.ts
vendored
4
src/data/types.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
|||
export interface Sound {
|
||||
icon: React.ReactNode;
|
||||
id: string;
|
||||
label: string;
|
||||
labelKey: string;
|
||||
src: string;
|
||||
}
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ export interface Category {
|
|||
icon: React.ReactNode;
|
||||
id: string;
|
||||
sounds: Sounds;
|
||||
title: string;
|
||||
titleKey: string;
|
||||
}
|
||||
|
||||
export type Categories = Array<Category>;
|
||||
|
|
|
|||
53
src/i18n.ts
Normal file
53
src/i18n.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import enTranslation from './locales/en/translation.json';
|
||||
import zh_CN_Translation from './locales/zh-CN/translation.json';
|
||||
import zh_TW_Translation from './locales/zh-TW/translation.json';
|
||||
import jaTranslation from './locales/ja/translation.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
ja: {
|
||||
translation: jaTranslation,
|
||||
},
|
||||
'zh-CN': {
|
||||
translation: zh_CN_Translation,
|
||||
},
|
||||
'zh-TW': {
|
||||
translation: zh_TW_Translation,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
fallbackLng: 'en',
|
||||
interpolation: { escapeValue: false },
|
||||
missingKeyHandler: (lngs, ns, key, fallbackValue, updateMissing, options) => {
|
||||
const resolvedLng = lngs && lngs.length > 0 ? lngs[0] : i18n.language;
|
||||
const value = i18n.getResource(resolvedLng, ns || 'translation', key);
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
console.warn(
|
||||
`i18next: Key '${key}' in namespace '${
|
||||
ns || 'translation'
|
||||
}' resolved to an object, but expected a string. Check your t() call.`,
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`i18next: Missing key '${key}' in namespace '${
|
||||
ns || 'translation'
|
||||
}' for language(s) '${lngs.join(', ')}'.`,
|
||||
);
|
||||
}
|
||||
|
||||
return fallbackValue || key;
|
||||
},
|
||||
react: { useSuspense: false },
|
||||
resources,
|
||||
returnObjects: true,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
17
src/i18n/utils.ts
Normal file
17
src/i18n/utils.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import i18n from '@/i18n';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
export async function getTranslator(lng?: string): Promise<TFunction> {
|
||||
const targetLng =
|
||||
lng || i18n.language || (i18n.options.fallbackLng as string[])[0];
|
||||
|
||||
if (i18n.language !== targetLng) {
|
||||
await i18n.changeLanguage(targetLng);
|
||||
}
|
||||
|
||||
return i18n.t;
|
||||
}
|
||||
|
||||
export function getSupportedLangs(): string[] {
|
||||
return Object.keys(i18n.options.resources || {});
|
||||
}
|
||||
|
|
@ -3,23 +3,26 @@ import { pwaInfo } from 'virtual:pwa-info'; // eslint-disable-line
|
|||
|
||||
import { Reload } from '@/components/reload';
|
||||
|
||||
import { count } from '@/lib/sounds';
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
|
||||
import '@/styles/global.css';
|
||||
|
||||
import LanguageSwitcher from '@/components/language-switcher.astro';
|
||||
interface Props {
|
||||
description?: string;
|
||||
title?: string;
|
||||
}
|
||||
const currentLocale = Astro.currentLocale;
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const title = Astro.props.title || 'Moodist: Ambient Sounds for Focus and Calm';
|
||||
const description =
|
||||
Astro.props.description ||
|
||||
`Moodist is a free and open-source ambient sound generator featuring ${count()} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.`;
|
||||
const count = soundCount();
|
||||
const title = Astro.props.title || t('site.title');
|
||||
const description = Astro.props.description || t('site.description', { count });
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang={currentLocale}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width" name="viewport" />
|
||||
|
|
@ -33,7 +36,7 @@ const description =
|
|||
|
||||
<meta content={title} property="og:title" />
|
||||
<meta content={description} property="og:description" />
|
||||
<meta content="Moodist" property="og:site_name" />
|
||||
<meta content={t('site.og-site-name')} property="og:site_name" />
|
||||
<meta content="https://moodist.app" property="og:url" />
|
||||
<meta content="website" property="og:type" />
|
||||
<meta content="https://moodist.app/og.png" property="og:image" />
|
||||
|
|
@ -43,8 +46,22 @@ const description =
|
|||
{pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} />}
|
||||
</head>
|
||||
<body>
|
||||
<header class="page-header-controls">
|
||||
<LanguageSwitcher />
|
||||
</header>
|
||||
<slot />
|
||||
|
||||
<Reload client:load />
|
||||
</body>
|
||||
</html>
|
||||
<style is:global>
|
||||
.page-header-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
360
src/locales/en/translation.json
Normal file
360
src/locales/en/translation.json
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist: Ambient Sounds for Focus and Calm",
|
||||
"description": "Moodist is a free and open-source ambient sound generator featuring {{count}} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.",
|
||||
"og-site-name": "Moodist"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"ja": "日本語"
|
||||
},
|
||||
"common": {
|
||||
"save": "Save",
|
||||
"untitled": "Untitled",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
"override": "Override",
|
||||
"done": "Done!",
|
||||
"reset": "Reset",
|
||||
"play": "Play",
|
||||
"close": "Close",
|
||||
"reload": "Reload",
|
||||
"start": "Start",
|
||||
"pause": "Pause",
|
||||
"stop": "Stop",
|
||||
"back": "Back",
|
||||
"restart": "Restart",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"plural-suffix": "s"
|
||||
},
|
||||
"hero": {
|
||||
"logo-alt": "Faded Moodist Logo",
|
||||
"title-line1": "Ambient Sounds",
|
||||
"title-line2": "For Focus and Calm",
|
||||
"desc-prefix": "Free and",
|
||||
"desc-open-source": "Open-Source",
|
||||
"sounds-count": "{{count}} Sounds"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"title": "Free Ambient Sounds",
|
||||
"body": "Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations – with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free."
|
||||
},
|
||||
"section2": {
|
||||
"title": "Carefully Curated Sounds",
|
||||
"body": "Dive into an expansive library of {{count}} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind."
|
||||
},
|
||||
"section3": {
|
||||
"title": "Create Your Soundscape",
|
||||
"body": "The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options – just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis."
|
||||
},
|
||||
"section4": {
|
||||
"title": "Sounds for Every Moment",
|
||||
"body": "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"prompt": "Enjoy Moodist?",
|
||||
"link-text": "Support with a donation!",
|
||||
"section-title": "Support Me",
|
||||
"section-desc": "Help me keep Moodist ad-free.",
|
||||
"section-button": "Donate Today"
|
||||
},
|
||||
"source": {
|
||||
"title": "Open Source",
|
||||
"desc": "Moodist is free and open-source!"
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "Menu",
|
||||
"global-volume-label": "Global Volume",
|
||||
"items": {
|
||||
"presets": "Your Presets",
|
||||
"share": "Share Sounds",
|
||||
"shuffle": "Shuffle Sounds",
|
||||
"sleep-timer": "Sleep Timer",
|
||||
"countdown": "Countdown Timer",
|
||||
"pomodoro": "Pomodoro",
|
||||
"notepad": "Notepad",
|
||||
"todo": "Todo Checklist",
|
||||
"breathing": "Breathing Exercise",
|
||||
"binaural": "Binaural Beats",
|
||||
"isochronic": "Isochronic Tones",
|
||||
"shortcuts": "Shortcuts",
|
||||
"buy-me-a-coffee": "Buy Me a Coffee",
|
||||
"source-code": "Source Code"
|
||||
}
|
||||
},
|
||||
"scroll-to-top": {
|
||||
"aria-label": "Scroll to top"
|
||||
},
|
||||
"unselect": {
|
||||
"tooltip": "Unselect all sounds.",
|
||||
"aria-label": "Unselect All Sounds",
|
||||
"restore": {
|
||||
"tooltip": "Restore unselected sounds.",
|
||||
"aria-label": "Restore Unselected Sounds"
|
||||
}
|
||||
},
|
||||
"favorite": {
|
||||
"add": {
|
||||
"aria-label": "Add {{label}} Sound to Favorites"
|
||||
},
|
||||
"remove": {
|
||||
"aria-label": "Remove {{label}} Sound from Favorites"
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"aria-label": "{{label}} sound volume"
|
||||
},
|
||||
"play-error": "Please select a sound to play.",
|
||||
"use-moodist": "Use Moodist",
|
||||
"modals": {
|
||||
"presets": {
|
||||
"title": "Presets",
|
||||
"your-presets-title": "Your Presets",
|
||||
"empty": "You don't have any presets yet.",
|
||||
"new-preset-title": "New Preset",
|
||||
"placeholder": "Preset's Name",
|
||||
"play-button-tooltip": "Play preset",
|
||||
"play-button-aria-label": "Play preset {{label}}",
|
||||
"delete-button-tooltip": "Delete preset",
|
||||
"delete-button-aria-label": "Delete preset {{label}}",
|
||||
"no-selected-warning": "To make a preset, first select some sounds."
|
||||
},
|
||||
"share-link": {
|
||||
"title": "Share your sound selection!",
|
||||
"description": "Copy and send the following link to the person you want to share your selection with.",
|
||||
"copy-button-aria-label": "Copy link"
|
||||
},
|
||||
"shared": {
|
||||
"title": "New sound mix detected!",
|
||||
"description": "Someone has shared the following mix with you. Would you want to override your current selection?",
|
||||
"snackbar-message": "Done! You can now play the new selection."
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "Sleep Timer",
|
||||
"description": "Stop sounds after a certain amount of time.",
|
||||
"hours-label": "Hours",
|
||||
"minutes-label": "Minutes"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "Countdown Timer",
|
||||
"description": "Super simple countdown timer.",
|
||||
"placeholder-hh": "HH",
|
||||
"placeholder-mm": "MM",
|
||||
"placeholder-ss": "SS"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "Pomodoro Timer",
|
||||
"settings-tooltip": "Change Times",
|
||||
"completed": "{{count}} completed",
|
||||
"tabs": {
|
||||
"pomodoro": "Pomodoro",
|
||||
"short-break": "Break",
|
||||
"long-break": "Long Break"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Change Times",
|
||||
"pomodoro-label": "Pomodoro",
|
||||
"short-break-label": "Short Break",
|
||||
"long-break-label": "Long Break",
|
||||
"minutes-unit": "minutes"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "Your Note",
|
||||
"copy-tooltip": "Copy Note",
|
||||
"download-tooltip": "Download Note",
|
||||
"clear-tooltip": "Clear Note",
|
||||
"restore-tooltip": "Restore Note",
|
||||
"placeholder": "What is on your mind?",
|
||||
"counter-stats": "{{chars}} character{{chars_plural}} • {{words}} word{{words_plural}}"
|
||||
},
|
||||
"todo": {
|
||||
"title": "Todo Checklist",
|
||||
"description": "Super simple todo list.",
|
||||
"add-placeholder": "I have to ...",
|
||||
"add-button": "Add",
|
||||
"your-todos-label": "Your Todos",
|
||||
"empty": "You don't have any todos.",
|
||||
"delete-button-aria-label": "Delete todo"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "Breathing Exercise",
|
||||
"phases": {
|
||||
"inhale": "Inhale",
|
||||
"exhale": "Exhale",
|
||||
"hold": "Hold"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "Box Breathing",
|
||||
"resonant": "Resonant Breathing",
|
||||
"478": "4-7-8 Breathing"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "Presets:",
|
||||
"base-frequency-label": "Base Frequency (Hz):",
|
||||
"volume-label": "Volume:",
|
||||
"presets": {
|
||||
"delta": "Delta (Deep Sleep) 2 Hz",
|
||||
"theta": "Theta (Meditation) 5 Hz",
|
||||
"alpha": "Alpha (Relaxation) 10 Hz",
|
||||
"beta": "Beta (Focus) 20 Hz",
|
||||
"gamma": "Gamma (Cognition) 40 Hz",
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "Binaural Beat",
|
||||
"description": "Binaural beat generator.",
|
||||
"beat-frequency-label": "Beat Frequency (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "Isochronic Tone",
|
||||
"description": "Isochronic tone generator.",
|
||||
"tone-frequency-label": "Tone Frequency (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"labels": {
|
||||
"toggle-play": "Toggle Play",
|
||||
"unselect-all": "Unselect All Sounds"
|
||||
}
|
||||
},
|
||||
"reload": {
|
||||
"title": "New Content Available",
|
||||
"description": "New content is available, click the reload button to update.",
|
||||
"closeButton": "Close",
|
||||
"reloadButton": "Reload"
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"show-less": "Show Less",
|
||||
"show-more": "Show More",
|
||||
"aria-label": "{{name}} sound",
|
||||
"favorites": {
|
||||
"title": "Favorites"
|
||||
},
|
||||
"animals": {
|
||||
"title": "Animals",
|
||||
"birds": "Birds",
|
||||
"seagulls": "Seagulls",
|
||||
"crickets": "Crickets",
|
||||
"wolf": "Wolf",
|
||||
"owl": "Owl",
|
||||
"frog": "Frog",
|
||||
"dog-barking": "Dog Barking",
|
||||
"horse-galopp": "Horse Galopp",
|
||||
"cat-purring": "Cat Purring",
|
||||
"crows": "Crows",
|
||||
"whale": "Whale",
|
||||
"beehive": "Beehive",
|
||||
"woodpecker": "Woodpecker",
|
||||
"chickens": "Chickens",
|
||||
"cows": "Cows",
|
||||
"sheep": "Sheep"
|
||||
},
|
||||
"binaural": {
|
||||
"title": "Binaural Beats",
|
||||
"binaural-delta": "Delta",
|
||||
"binaural-theta": "Theta",
|
||||
"binaural-alpha": "Alpha",
|
||||
"binaural-beta": "Beta",
|
||||
"binaural-gamma": "Gamma"
|
||||
},
|
||||
"nature": {
|
||||
"title": "Nature",
|
||||
"river": "River",
|
||||
"waves": "Waves",
|
||||
"campfire": "Campfire",
|
||||
"wind": "Wind",
|
||||
"howling-wind": "Howling Wind",
|
||||
"wind-in-trees": "Wind in Trees",
|
||||
"waterfall": "Waterfall",
|
||||
"walk-in-snow": "Walk in Snow",
|
||||
"walk-on-leaves": "Walk on Leaves",
|
||||
"walk-on-gravel": "Walk on Gravel",
|
||||
"droplets": "Droplets",
|
||||
"jungle": "Jungle"
|
||||
},
|
||||
"noise": {
|
||||
"title": "Noise",
|
||||
"white-noise": "White Noise",
|
||||
"pink-noise": "Pink Noise",
|
||||
"brown-noise": "Brown Noise"
|
||||
},
|
||||
"places": {
|
||||
"title": "Places",
|
||||
"cafe": "Cafe",
|
||||
"airport": "Airport",
|
||||
"church": "Church",
|
||||
"temple": "Temple",
|
||||
"construction-site": "Construction Site",
|
||||
"underwater": "Underwater",
|
||||
"crowded-bar": "Crowded Bar",
|
||||
"night-village": "Night Village",
|
||||
"subway-station": "Subway Station",
|
||||
"office": "Office",
|
||||
"supermarket": "Supermarket",
|
||||
"carousel": "Carousel",
|
||||
"laboratory": "Laboratory",
|
||||
"laundry-room": "Laundry Room",
|
||||
"restaurant": "Restaurant",
|
||||
"library": "Library"
|
||||
},
|
||||
"rain": {
|
||||
"title": "Rain",
|
||||
"light-rain": "Light Rain",
|
||||
"heavy-rain": "Heavy Rain",
|
||||
"thunder": "Thunder",
|
||||
"rain-on-window": "Rain on Window",
|
||||
"rain-on-car-roof": "Rain on Car Roof",
|
||||
"rain-on-umbrella": "Rain on Umbrella",
|
||||
"rain-on-tent": "Rain on Tent",
|
||||
"rain-on-leaves": "Rain on Leaves"
|
||||
},
|
||||
"things": {
|
||||
"title": "Things",
|
||||
"keyboard": "Keyboard",
|
||||
"typewriter": "Typewriter",
|
||||
"paper": "Paper",
|
||||
"clock": "Clock",
|
||||
"wind-chimes": "Wind Chimes",
|
||||
"singing-bowl": "Singing Bowl",
|
||||
"ceiling-fan": "Ceiling Fan",
|
||||
"dryer": "Dryer",
|
||||
"slide-projector": "Slide Projector",
|
||||
"boiling-water": "Boiling Water",
|
||||
"bubbles": "Bubbles",
|
||||
"tuning-radio": "Tuning Radio",
|
||||
"morse-code": "Morse Code",
|
||||
"washing-machine": "Washing Machine",
|
||||
"vinyl-effect": "Vinyl Effect",
|
||||
"windshield-wipers": "Windshield Wipers"
|
||||
},
|
||||
"transport": {
|
||||
"title": "Transport",
|
||||
"train": "Train",
|
||||
"inside-a-train": "Inside a Train",
|
||||
"airplane": "Airplane",
|
||||
"submarine": "Submarine",
|
||||
"sailboat": "Sailboat",
|
||||
"rowing-boat": "Rowing Boat"
|
||||
},
|
||||
"urban": {
|
||||
"title": "Urban",
|
||||
"highway": "Highway",
|
||||
"road": "Road",
|
||||
"ambulance-siren": "Ambulance Siren",
|
||||
"busy-street": "Busy Street",
|
||||
"crowd": "Crowd",
|
||||
"traffic": "Traffic",
|
||||
"fireworks": "Fireworks"
|
||||
}
|
||||
},
|
||||
"created-by": "Created by {{authorLink}}"
|
||||
}
|
||||
360
src/locales/ja/translation.json
Normal file
360
src/locales/ja/translation.json
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist:集中とリラックスのための環境サウンド",
|
||||
"description": "Moodistは、厳選された{{count}}種類のサウンドを備えた、無料のオープンソース環境サウンドジェネレーターです。この多機能ツールで、リラックス、集中、または創造性のための理想的な雰囲気を作りましょう。",
|
||||
"ogSiteName": "Moodist"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"ja": "日本語"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"untitled": "無題",
|
||||
"copy": "コピー",
|
||||
"copied": "コピーしました!",
|
||||
"override": "上書き",
|
||||
"done": "完了!",
|
||||
"reset": "リセット",
|
||||
"play": "再生",
|
||||
"close": "閉じる",
|
||||
"reload": "再読み込み",
|
||||
"start": "開始",
|
||||
"pause": "一時停止",
|
||||
"stop": "停止",
|
||||
"back": "戻る",
|
||||
"restart": "再開",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"plural-suffix": ""
|
||||
},
|
||||
"hero": {
|
||||
"logo-alt": "Moodist ロゴ",
|
||||
"title-line1": "Moodist",
|
||||
"title-line2": "集中とリラックスのための環境音",
|
||||
"desc-prefix": "無料、そして",
|
||||
"desc-open-source": "オープンソース",
|
||||
"sounds-count": "{{count}} 種類のサウンド"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"title": "無料の環境サウンド",
|
||||
"body": "日々の喧騒から逃れて、心を落ち着かせたいですか?集中力を高めたり、安らかな眠りに誘う完璧なサウンドスケープが必要ですか?Moodistは、あなたのための無料オープンソース環境サウンドジェネレーターです!サブスクリプションや登録は不要。Moodistで、心地よく没入感のあるオーディオ体験の世界を完全に無料で解き放ちましょう。"
|
||||
},
|
||||
"section2": {
|
||||
"title": "厳選されたサウンド",
|
||||
"body": "厳選された{{count}}種類のサウンドの広範なライブラリに飛び込みましょう。自然愛好家は、小川の穏やかなせせらぎ、リズミカルな波の音、またはキャンプファイヤーのパチパチと燃える暖かさの中に安らぎを見つけるでしょう。カフェの柔らかなざわめき、電車の規則的なガタンゴトンという音、または交通の静かなホワイトノイズで、街の風景が生き生きと蘇ります。より深い集中やリラクゼーションを求める方のために、Moodistはあなたの心の状態を高めるように設計されたバイノーラルビートとカラーノイズを提供します。"
|
||||
},
|
||||
"section3": {
|
||||
"title": "あなたのサウンドスケープを作成",
|
||||
"body": "Moodistの魅力は、そのシンプルさとカスタマイズ性にあります。複雑なメニューや紛らわしいオプションはありません。希望のサウンドを選択し、音量バランスを調整して再生ボタンを押すだけです。鳥の優しいさえずりと心地よい雨音をブレンドしたいですか?問題ありません!好きなだけサウンドを重ねて、あなただけのサウンドスケープ・オアシスを作りましょう。"
|
||||
},
|
||||
"section4": {
|
||||
"title": "あらゆる瞬間のためのサウンド",
|
||||
"body": "長い一日の後にリラックスしたいとき、仕事中に集中力を高めたいとき、または安らかな眠りに就きたいとき、Moodistはあなたにぴったりのサウンドスケープを用意しています。そして何よりも素晴らしいのは、完全に無料でオープンソースであるため、何の制約もなくその利点を享受できることです。今日からMoodistを使い始めて、あなたの新しい静寂と集中の聖域を発見してください!"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"prompt": "Moodistをお楽しみですか?",
|
||||
"link-text": "寄付でサポートをお願いします!",
|
||||
"section-title": "プロジェクトを支援",
|
||||
"section-desc": "Moodistを広告なしで無料に保つためにご協力ください。",
|
||||
"section-button": "コーヒー代を寄付する"
|
||||
},
|
||||
"source": {
|
||||
"title": "オープンソース",
|
||||
"desc": "Moodistは無料でオープンソースです!"
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "メニュー",
|
||||
"global-volume-label": "全体の音量",
|
||||
"items": {
|
||||
"presets": "プリセット",
|
||||
"share": "サウンドを共有",
|
||||
"shuffle": "サウンドをシャッフル",
|
||||
"sleep-timer": "スリープタイマー",
|
||||
"countdown": "カウントダウンタイマー",
|
||||
"pomodoro": "ポモドーロタイマー",
|
||||
"notepad": "メモ帳",
|
||||
"todo": "ToDoリスト",
|
||||
"breathing": "呼吸エクササイズ",
|
||||
"binaural": "バイノーラルビート",
|
||||
"isochronic": "アイソクロニックトーン",
|
||||
"shortcuts": "ショートカット",
|
||||
"buy-me-a-coffee": "コーヒーをおごる",
|
||||
"source-code": "ソースコード"
|
||||
}
|
||||
},
|
||||
"scroll-to-top": {
|
||||
"aria-label": "トップへスクロール"
|
||||
},
|
||||
"unselect": {
|
||||
"tooltip": "すべてのサウンドの選択を解除します。",
|
||||
"aria-label": "すべてのサウンドの選択を解除",
|
||||
"restore": {
|
||||
"tooltip": "選択解除したサウンドを復元します。",
|
||||
"aria-label": "選択解除したサウンドを復元"
|
||||
}
|
||||
},
|
||||
"favorite": {
|
||||
"add": {
|
||||
"aria-label": "{{label}} サウンドをお気に入りに追加"
|
||||
},
|
||||
"remove": {
|
||||
"aria-label": "{{label}} サウンドをお気に入りから削除"
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"aria-label": "{{label}} サウンドの音量"
|
||||
},
|
||||
"play-error": "再生するサウンドを選択してください。",
|
||||
"use-moodist": "Moodistを使う",
|
||||
"modals": {
|
||||
"presets": {
|
||||
"title": "サウンドプリセット",
|
||||
"your-presets-title": "マイプリセット",
|
||||
"empty": "まだプリセットがありません。",
|
||||
"new-preset-title": "新規プリセット",
|
||||
"placeholder": "プリセット名",
|
||||
"play-button-tooltip": "プリセットを再生",
|
||||
"play-button-aria-label": "プリセット {{label}} を再生",
|
||||
"delete-button-tooltip": "プリセットを削除",
|
||||
"delete-button-aria-label": "プリセット {{label}} を削除",
|
||||
"no-selected-warning": "プリセットを作成するには、まずいくつかのサウンドを選択してください。"
|
||||
},
|
||||
"share-link": {
|
||||
"title": "サウンドセレクションを共有!",
|
||||
"description": "以下のリンクをコピーして、共有したい相手に送ってください。",
|
||||
"copy-button-aria-label": "リンクをコピー"
|
||||
},
|
||||
"shared": {
|
||||
"title": "新しいサウンドミックスが検出されました!",
|
||||
"description": "誰かが以下のミックスをあなたと共有しました。現在のセレクションを上書きしますか?",
|
||||
"snackbar-message": "完了!新しいセレクションを再生できます。"
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "スリープタイマー",
|
||||
"description": "一定時間後にサウンドを停止します。",
|
||||
"hours-label": "時間",
|
||||
"minutes-label": "分"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "カウントダウンタイマー",
|
||||
"description": "シンプルなカウントダウンタイマーです。",
|
||||
"placeholder-hh": "時",
|
||||
"placeholder-mm": "分",
|
||||
"placeholder-ss": "秒"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "ポモドーロタイマー",
|
||||
"settings-tooltip": "時間を変更",
|
||||
"completed": "{{count}} 回完了",
|
||||
"tabs": {
|
||||
"pomodoro": "作業",
|
||||
"short-break": "短い休憩",
|
||||
"long-break": "長い休憩"
|
||||
},
|
||||
"settings": {
|
||||
"title": "時間を変更",
|
||||
"pomodoro-label": "作業時間",
|
||||
"short-break-label": "短い休憩時間",
|
||||
"long-break-label": "長い休憩時間",
|
||||
"minutes-unit": "分"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "メモ帳",
|
||||
"copy-tooltip": "メモをコピー",
|
||||
"download-tooltip": "メモをダウンロード",
|
||||
"clear-tooltip": "メモをクリア",
|
||||
"restore-tooltip": "メモを復元",
|
||||
"placeholder": "考え事を書き留めましょう...",
|
||||
"counter-stats": "{{chars}} 文字 • {{words}} 単語"
|
||||
},
|
||||
"todo": {
|
||||
"title": "ToDoリスト",
|
||||
"description": "シンプルなToDoリストです。",
|
||||
"add-placeholder": "やることを入力...",
|
||||
"add-button": "追加",
|
||||
"your-todos-label": "あなたのToDo",
|
||||
"empty": "ToDoはまだありません。",
|
||||
"delete-button-aria-label": "ToDoを削除"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "呼吸エクササイズ",
|
||||
"phases": {
|
||||
"inhale": "吸って",
|
||||
"exhale": "吐いて",
|
||||
"hold": "止めて"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "ボックス呼吸法",
|
||||
"resonant": "レゾナント呼吸法",
|
||||
"478": "4-7-8呼吸法"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "プリセット:",
|
||||
"base-frequency-label": "基本周波数 (Hz):",
|
||||
"volume-label": "音量:",
|
||||
"presets": {
|
||||
"delta": "デルタ波 (深い睡眠) 2 Hz",
|
||||
"theta": "シータ波 (瞑想) 5 Hz",
|
||||
"alpha": "アルファ波 (リラックス) 10 Hz",
|
||||
"beta": "ベータ波 (集中) 20 Hz",
|
||||
"gamma": "ガンマ波 (認知) 40 Hz",
|
||||
"custom": "カスタム"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "バイノーラルビート",
|
||||
"description": "バイノーラルビートジェネレーター。",
|
||||
"beat-frequency-label": "ビート周波数 (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "アイソクロニックトーン",
|
||||
"description": "アイソクロニックトーンジェネレーター。",
|
||||
"tone-frequency-label": "トーン周波数 (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "キーボードショートカット",
|
||||
"labels": {
|
||||
"toggle-play": "再生/一時停止 切り替え",
|
||||
"unselect-all": "全サウンドの選択を解除"
|
||||
}
|
||||
},
|
||||
"reload": {
|
||||
"title": "新しいコンテンツが利用可能です",
|
||||
"description": "新しいコンテンツが利用可能です。更新するには再読み込みボタンをクリックしてください。",
|
||||
"closeButton": "閉じる",
|
||||
"reloadButton": "再読み込み"
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"show-less": "少なく表示",
|
||||
"show-more": "もっと表示",
|
||||
"aria-label": "{{name}} サウンド",
|
||||
"favorites": {
|
||||
"title": "お気に入り"
|
||||
},
|
||||
"animals": {
|
||||
"title": "動物",
|
||||
"birds": "鳥のさえずり",
|
||||
"seagulls": "カモメ",
|
||||
"crickets": "コオロギ",
|
||||
"wolf": "オオカミの遠吠え",
|
||||
"owl": "フクロウ",
|
||||
"frog": "カエル",
|
||||
"dog-barking": "犬の吠え声",
|
||||
"horse-galopp": "馬のギャロップ",
|
||||
"cat-purring": "猫のゴロゴロ",
|
||||
"crows": "カラス",
|
||||
"whale": "クジラ",
|
||||
"beehive": "ミツバチの巣",
|
||||
"woodpecker": "キツツキ",
|
||||
"chickens": "ニワトリ",
|
||||
"cows": "牛",
|
||||
"sheep": "羊"
|
||||
},
|
||||
"binaural": {
|
||||
"title": "バイノーラルビート",
|
||||
"binaural-delta": "デルタ波 (深い睡眠)",
|
||||
"binaural-theta": "シータ波 (瞑想)",
|
||||
"binaural-alpha": "アルファ波 (リラックス)",
|
||||
"binaural-beta": "ベータ波 (集中)",
|
||||
"binaural-gamma": "ガンマ波 (認知)"
|
||||
},
|
||||
"nature": {
|
||||
"title": "自然",
|
||||
"river": "川",
|
||||
"waves": "波",
|
||||
"campfire": "キャンプファイヤー",
|
||||
"wind": "風",
|
||||
"howling-wind": "風のうなり",
|
||||
"wind-in-trees": "木々の間の風",
|
||||
"waterfall": "滝",
|
||||
"walk-in-snow": "雪の上を歩く",
|
||||
"walk-on-leaves": "落ち葉の上を歩く",
|
||||
"walk-on-gravel": "砂利の上を歩く",
|
||||
"droplets": "水滴",
|
||||
"jungle": "ジャングル"
|
||||
},
|
||||
"noise": {
|
||||
"title": "ノイズ",
|
||||
"white-noise": "ホワイトノイズ",
|
||||
"pink-noise": "ピンクノイズ",
|
||||
"brown-noise": "ブラウンノイズ"
|
||||
},
|
||||
"places": {
|
||||
"title": "場所",
|
||||
"cafe": "カフェ",
|
||||
"airport": "空港",
|
||||
"church": "教会",
|
||||
"temple": "寺院",
|
||||
"construction-site": "建設現場",
|
||||
"underwater": "水中",
|
||||
"crowded-bar": "混雑したバー",
|
||||
"night-village": "夜の村",
|
||||
"subway-station": "地下鉄の駅",
|
||||
"office": "オフィス",
|
||||
"supermarket": "スーパーマーケット",
|
||||
"carousel": "メリーゴーランド",
|
||||
"laboratory": "研究室",
|
||||
"laundry-room": "ランドリールーム",
|
||||
"restaurant": "レストラン",
|
||||
"library": "図書館"
|
||||
},
|
||||
"rain": {
|
||||
"title": "雨",
|
||||
"light-rain": "小雨",
|
||||
"heavy-rain": "大雨",
|
||||
"thunder": "雷",
|
||||
"rain-on-window": "窓に当たる雨",
|
||||
"rain-on-car-roof": "車の屋根に当たる雨",
|
||||
"rain-on-umbrella": "傘に当たる雨",
|
||||
"rain-on-tent": "テントに当たる雨",
|
||||
"rain-on-leaves": "葉に当たる雨"
|
||||
},
|
||||
"things": {
|
||||
"title": "物",
|
||||
"keyboard": "キーボード",
|
||||
"typewriter": "タイプライター",
|
||||
"paper": "紙",
|
||||
"clock": "時計",
|
||||
"wind-chimes": "風鈴",
|
||||
"singing-bowl": "シンギングボウル",
|
||||
"ceiling-fan": "シーリングファン",
|
||||
"dryer": "乾燥機",
|
||||
"slide-projector": "スライドプロジェクター",
|
||||
"boiling-water": "沸騰したお湯",
|
||||
"bubbles": "泡",
|
||||
"tuning-radio": "ラジオのチューニング",
|
||||
"morse-code": "モールス信号",
|
||||
"washing-machine": "洗濯機",
|
||||
"vinyl-effect": "レコードノイズ",
|
||||
"windshield-wipers": "ワイパー"
|
||||
},
|
||||
"transport": {
|
||||
"title": "乗り物",
|
||||
"train": "電車",
|
||||
"inside-a-train": "電車の中",
|
||||
"airplane": "飛行機",
|
||||
"submarine": "潜水艦",
|
||||
"sailboat": "帆船",
|
||||
"rowing-boat": "手漕ぎボート"
|
||||
},
|
||||
"urban": {
|
||||
"title": "都市",
|
||||
"highway": "高速道路",
|
||||
"road": "道路",
|
||||
"ambulance-siren": "救急車のサイレン",
|
||||
"busy-street": "賑やかな通り",
|
||||
"crowd": "人混み",
|
||||
"traffic": "交通",
|
||||
"fireworks": "花火"
|
||||
}
|
||||
},
|
||||
"created-by": "{{authorLink}} によって作成されました"
|
||||
}
|
||||
360
src/locales/zh-CN/translation.json
Normal file
360
src/locales/zh-CN/translation.json
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist:保持专注,静下心来",
|
||||
"description": "Moodist 是一款免费且开源的环境音生成器,精选{{count}}种声音。无论是想放松身心、专注工作,还是激发创造力,都可以使用这款多用途小工具来营造理想氛围。",
|
||||
"ogSiteName": "Moodist"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"ja": "日本語"
|
||||
},
|
||||
"common": {
|
||||
"save": "保存",
|
||||
"untitled": "未命名",
|
||||
"copy": "复制",
|
||||
"copied": "已复制!",
|
||||
"override": "覆盖",
|
||||
"done": "已完成!",
|
||||
"reset": "重置",
|
||||
"play": "播放",
|
||||
"close": "关闭",
|
||||
"reload": "刷新",
|
||||
"start": "开始",
|
||||
"pause": "暂停",
|
||||
"stop": "停止",
|
||||
"back": "返回",
|
||||
"restart": "重新开始",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"plural-suffix": ""
|
||||
},
|
||||
"hero": {
|
||||
"logo-alt": "渐变色 Moodist 标志",
|
||||
"title-line1": "Moodist",
|
||||
"title-line2": "保持专注,静下心来",
|
||||
"desc-prefix": "免费且",
|
||||
"desc-open-source": "完全开源",
|
||||
"sounds-count": "{{count}} 种声音"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"title": "免费环境声音",
|
||||
"body": "渴望逃离日常喧嚣,寻求一隅宁静?是否需要提升专注,或帮助入眠的理想声音?千万别错过 Moodist,一款免费开源的环境声音生成器!无需订阅注册,即刻带您进入一个舒缓、沉浸式的音频世界,且完全免费。"
|
||||
},
|
||||
"section2": {
|
||||
"title": "经过精心挑选",
|
||||
"body": "这里有{{count}}种精心挑选的声音。如果您是自然爱好者,可以在潺潺的溪流声、规律的海浪声、温暖的篝火声中找到心灵慰藉。如果您偏爱城市景观,咖啡馆的轻语声、火车的行进声、平和的交通白噪音,都能令人置身其中。如果您渴望深度专注与彻底放松,Moodist 还提供双耳节拍和彩色噪音,有助于调整心境。"
|
||||
},
|
||||
"section3": {
|
||||
"title": "打造专属音景",
|
||||
"body": "Moodist 的魅力在于它界面简洁,内容可个性定制。它没有复杂的菜单,也没有眼花缭乱的选项,您只需选择自己喜欢的声音,调整音量大小,然后点击播放即可。想将温婉的鸟鸣和舒适的雨声混在一起?完全没问题!您可以随心叠加多个声音,创造属于自己的个性化声音盛宴。"
|
||||
},
|
||||
"section4": {
|
||||
"title": "满足不同需求",
|
||||
"body": "无论是想消除一整天的疲惫,提升工作学习时的专注力,还是辅助安然入眠,Moodist 都能给您带来完美的声音环境。最棒的一点是,Moodist 完全免费且开源,您可以无条件享受它带来的益处。现在就开始使用 Moodist 吧,打造属于自己的全新专注静心空间!"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"prompt": "喜欢 Moodist 吗?",
|
||||
"link-text": "欢迎捐助!",
|
||||
"section-title": "支持项目",
|
||||
"section-desc": "帮助 Moodist 保持免费和无广告。",
|
||||
"section-button": "请我喝杯咖啡"
|
||||
},
|
||||
"source": {
|
||||
"title": "开源",
|
||||
"desc": "Moodist 是免费且开源的!"
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "菜单",
|
||||
"global-volume-label": "全局音量",
|
||||
"items": {
|
||||
"presets": "预设",
|
||||
"share": "分享声音",
|
||||
"shuffle": "随机组合",
|
||||
"sleep-timer": "睡眠定时器",
|
||||
"countdown": "倒计时器",
|
||||
"pomodoro": "番茄钟",
|
||||
"notepad": "记事本",
|
||||
"todo": "待办清单",
|
||||
"breathing": "呼吸练习",
|
||||
"binaural": "双耳节拍",
|
||||
"isochronic": "等时声频",
|
||||
"shortcuts": "快捷键",
|
||||
"buy-me-a-coffee": "请我喝杯咖啡",
|
||||
"source-code": "源代码"
|
||||
}
|
||||
},
|
||||
"scroll-to-top": {
|
||||
"aria-label": "滚动到顶部"
|
||||
},
|
||||
"unselect": {
|
||||
"tooltip": "取消所有已选声音",
|
||||
"aria-label": "取消所有已选声音",
|
||||
"restore": {
|
||||
"tooltip": "恢复已选声音",
|
||||
"aria-label": "恢复已选声音"
|
||||
}
|
||||
},
|
||||
"favorite": {
|
||||
"add": {
|
||||
"aria-label": "将{{label}}加入收藏"
|
||||
},
|
||||
"remove": {
|
||||
"aria-label": "将{{label}}取消收藏"
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"aria-label": "{{label}} 音量"
|
||||
},
|
||||
"play-error": "请先选择至少一种声音进行播放。",
|
||||
"use-moodist": "开始使用 Moodist",
|
||||
"modals": {
|
||||
"presets": {
|
||||
"title": "声音预设",
|
||||
"your-presets-title": "我的预设",
|
||||
"empty": "您还没有保存任何预设。",
|
||||
"new-preset-title": "新建预设",
|
||||
"placeholder": "预设名称",
|
||||
"play-button-tooltip": "播放预设",
|
||||
"play-button-aria-label": "播放预设 {{label}}",
|
||||
"delete-button-tooltip": "删除预设",
|
||||
"delete-button-aria-label": "删除预设 {{label}}",
|
||||
"no-selected-warning": "请先选择声音,再创建预设。"
|
||||
},
|
||||
"share-link": {
|
||||
"title": "分享声音组合!",
|
||||
"description": "复制下方链接,将已选声音组合分享给他人。",
|
||||
"copy-button-aria-label": "复制链接"
|
||||
},
|
||||
"shared": {
|
||||
"title": "检测到声音组合!",
|
||||
"description": "有人向您分享了以下声音组合,是否覆盖当前选择?",
|
||||
"snackbar-message": "已完成!现在可以播放新的声音组合了。"
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "睡眠定时器",
|
||||
"description": "设置时间后自动停止播放声音。",
|
||||
"hours-label": "小时",
|
||||
"minutes-label": "分钟"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "倒计时器",
|
||||
"description": "简洁实用的倒计时器。",
|
||||
"placeholder-hh": "时",
|
||||
"placeholder-mm": "分",
|
||||
"placeholder-ss": "秒"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "番茄钟",
|
||||
"settings-tooltip": "设置时长",
|
||||
"completed": "已完成 {{count}} 个番茄钟",
|
||||
"tabs": {
|
||||
"pomodoro": "工作",
|
||||
"short-break": "短休息",
|
||||
"long-break": "长休息"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置时长",
|
||||
"pomodoro-label": "工作时长",
|
||||
"short-break-label": "短休息时长",
|
||||
"long-break-label": "长休息时长",
|
||||
"minutes-unit": "分钟"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "随手记",
|
||||
"copy-tooltip": "复制笔记",
|
||||
"download-tooltip": "下载笔记",
|
||||
"clear-tooltip": "清空笔记",
|
||||
"restore-tooltip": "恢复笔记",
|
||||
"placeholder": "记录您的想法...",
|
||||
"counter-stats": "{{chars}}个字符"
|
||||
},
|
||||
"todo": {
|
||||
"title": "待办清单",
|
||||
"description": "简洁实用的待办事项列表。",
|
||||
"add-placeholder": "添加待办事项...",
|
||||
"add-button": "添加",
|
||||
"your-todos-label": "我的待办",
|
||||
"empty": "尚无待办事项。",
|
||||
"delete-button-aria-label": "删除此待办事项"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "呼吸练习",
|
||||
"phases": {
|
||||
"inhale": "吸气",
|
||||
"exhale": "呼气",
|
||||
"hold": "屏息"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "方形呼吸法",
|
||||
"resonant": "同步呼吸法",
|
||||
"478": "4-7-8 呼吸法"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "预设:",
|
||||
"base-frequency-label": "基础频率 (Hz):",
|
||||
"volume-label": "音量:",
|
||||
"presets": {
|
||||
"delta": "Delta 波 (深度睡眠) 2 Hz",
|
||||
"theta": "Theta 波 (冥想) 5 Hz",
|
||||
"alpha": "Alpha 波 (放松) 10 Hz",
|
||||
"beta": "Beta 波 (专注) 20 Hz",
|
||||
"gamma": "Gamma 波 (认知) 40 Hz",
|
||||
"custom": "自定义"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "双耳节拍",
|
||||
"description": "生成双耳节拍声音。",
|
||||
"beat-frequency-label": "节拍频率 (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "等时声频",
|
||||
"description": "生成等时声频声音。",
|
||||
"tone-frequency-label": "声频频率 (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "键盘快捷键",
|
||||
"labels": {
|
||||
"toggle-play": "播放/暂停",
|
||||
"unselect-all": "取消全部已选声音"
|
||||
}
|
||||
},
|
||||
"reload": {
|
||||
"title": "有新的更新可用",
|
||||
"description": "检测到新版本,点击“刷新”按钮以更新。",
|
||||
"closeButton": "关闭",
|
||||
"reloadButton": "刷新"
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"show-less": "收起",
|
||||
"show-more": "展开",
|
||||
"aria-label": "{{name}}",
|
||||
"favorites": {
|
||||
"title": "我的收藏"
|
||||
},
|
||||
"animals": {
|
||||
"title": "动物",
|
||||
"birds": "鸟鸣",
|
||||
"seagulls": "海鸥",
|
||||
"crickets": "蟋蟀",
|
||||
"wolf": "狼嚎",
|
||||
"owl": "猫头鹰",
|
||||
"frog": "蛙鸣",
|
||||
"dog-barking": "狗吠",
|
||||
"horse-galopp": "马蹄声",
|
||||
"cat-purring": "猫打呼噜",
|
||||
"crows": "乌鸦",
|
||||
"whale": "鲸",
|
||||
"beehive": "蜂巢",
|
||||
"woodpecker": "啄木鸟",
|
||||
"chickens": "鸡",
|
||||
"cows": "牛",
|
||||
"sheep": "羊"
|
||||
},
|
||||
"binaural": {
|
||||
"title": "双耳节拍",
|
||||
"binaural-delta": "Delta 波 (深度睡眠)",
|
||||
"binaural-theta": "Theta 波 (冥想)",
|
||||
"binaural-alpha": "Alpha 波 (放松)",
|
||||
"binaural-beta": "Beta 波 (专注)",
|
||||
"binaural-gamma": "Gamma 波 (认知)"
|
||||
},
|
||||
"nature": {
|
||||
"title": "自然",
|
||||
"river": "河流",
|
||||
"waves": "海浪",
|
||||
"campfire": "篝火",
|
||||
"wind": "风声",
|
||||
"howling-wind": "呼啸风声",
|
||||
"wind-in-trees": "林间风声",
|
||||
"waterfall": "瀑布",
|
||||
"walk-in-snow": "走在雪地上",
|
||||
"walk-on-leaves": "踩在落叶上",
|
||||
"walk-on-gravel": "走在砂石路上",
|
||||
"droplets": "水滴",
|
||||
"jungle": "丛林"
|
||||
},
|
||||
"noise": {
|
||||
"title": "噪音",
|
||||
"white-noise": "白噪音",
|
||||
"pink-noise": "粉噪音",
|
||||
"brown-noise": "棕噪音"
|
||||
},
|
||||
"places": {
|
||||
"title": "场所",
|
||||
"cafe": "咖啡馆",
|
||||
"airport": "机场",
|
||||
"church": "教堂",
|
||||
"temple": "寺庙",
|
||||
"construction-site": "建筑工地",
|
||||
"underwater": "水下",
|
||||
"crowded-bar": "嘈杂酒吧",
|
||||
"night-village": "夜晚乡村",
|
||||
"subway-station": "地铁站",
|
||||
"office": "办公室",
|
||||
"supermarket": "超市",
|
||||
"carousel": "旋转木马",
|
||||
"laboratory": "实验室",
|
||||
"laundry-room": "洗衣房",
|
||||
"restaurant": "餐厅",
|
||||
"library": "图书馆"
|
||||
},
|
||||
"rain": {
|
||||
"title": "雨声",
|
||||
"light-rain": "小雨",
|
||||
"heavy-rain": "大雨",
|
||||
"thunder": "雷声",
|
||||
"rain-on-window": "雨打窗户",
|
||||
"rain-on-car-roof": "雨打车顶",
|
||||
"rain-on-umbrella": "雨打雨伞",
|
||||
"rain-on-tent": "雨打帐篷",
|
||||
"rain-on-leaves": "雨打树叶"
|
||||
},
|
||||
"things": {
|
||||
"title": "物品",
|
||||
"keyboard": "键盘声",
|
||||
"typewriter": "打字机",
|
||||
"paper": "纸张翻动",
|
||||
"clock": "时钟",
|
||||
"wind-chimes": "风铃",
|
||||
"singing-bowl": "颂钵",
|
||||
"ceiling-fan": "吊扇",
|
||||
"dryer": "烘干机",
|
||||
"slide-projector": "幻灯机",
|
||||
"boiling-water": "烧水声",
|
||||
"bubbles": "气泡声",
|
||||
"tuning-radio": "收音机调频",
|
||||
"morse-code": "摩尔斯电码",
|
||||
"washing-machine": "洗衣机",
|
||||
"vinyl-effect": "黑胶唱片噪音",
|
||||
"windshield-wipers": "雨刮器"
|
||||
},
|
||||
"transport": {
|
||||
"title": "交通",
|
||||
"train": "火车",
|
||||
"inside-a-train": "火车车厢",
|
||||
"airplane": "飞机",
|
||||
"submarine": "潜水艇",
|
||||
"sailboat": "帆船",
|
||||
"rowing-boat": "划艇"
|
||||
},
|
||||
"urban": {
|
||||
"title": "城市",
|
||||
"highway": "高速公路",
|
||||
"road": "街道",
|
||||
"ambulance-siren": "救护车鸣笛",
|
||||
"busy-street": "繁忙街道",
|
||||
"crowd": "嘈杂人群",
|
||||
"traffic": "交通",
|
||||
"fireworks": "烟花"
|
||||
}
|
||||
},
|
||||
"created-by": "由 {{authorLink}} 创建"
|
||||
}
|
||||
360
src/locales/zh-TW/translation.json
Normal file
360
src/locales/zh-TW/translation.json
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist:保持專注,靜下心來",
|
||||
"description": "Moodist 是一款免費且開源的環境音產生器,精選 {{count}} 種聲音。無論是想放鬆身心、專注工作,還是激發創造力,都可以使用這款多用途小工具來營造理想氛圍。",
|
||||
"ogSiteName": "Moodist"
|
||||
},
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"zh-CN": "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
"ja": "日本語"
|
||||
},
|
||||
"common": {
|
||||
"save": "儲存",
|
||||
"untitled": "未命名",
|
||||
"copy": "複製",
|
||||
"copied": "已複製!",
|
||||
"override": "覆蓋",
|
||||
"done": "已完成!",
|
||||
"reset": "重設",
|
||||
"play": "播放",
|
||||
"close": "關閉",
|
||||
"reload": "重新載入",
|
||||
"start": "開始",
|
||||
"pause": "暫停",
|
||||
"stop": "停止",
|
||||
"back": "返回",
|
||||
"restart": "重新開始",
|
||||
"cancel": "取消",
|
||||
"delete": "刪除",
|
||||
"plural-suffix": ""
|
||||
},
|
||||
"hero": {
|
||||
"logo-alt": "漸層色 Moodist 標誌",
|
||||
"title-line1": "Moodist",
|
||||
"title-line2": "保持專注,靜下心來",
|
||||
"desc-prefix": "免費且",
|
||||
"desc-open-source": "完全開源",
|
||||
"sounds-count": "{{count}} 種聲音"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"title": "免費環境聲音",
|
||||
"body": "渴望逃離日常喧囂,尋求一隅寧靜?是否需要提升專注,或幫助入眠的理想聲音?千萬別錯過 Moodist,一款免費開源的環境聲音產生器!無需訂閱註冊,即刻帶您進入一個舒緩、沉浸式的音訊世界,且完全免費。"
|
||||
},
|
||||
"section2": {
|
||||
"title": "經過精心挑選",
|
||||
"body": "這裡有 {{count}} 種精心挑選的聲音。如果您是自然愛好者,可以在潺潺的溪流聲、規律的海浪聲、溫暖的營火聲中找到心靈慰藉。如果您偏愛城市景觀,咖啡館的輕語聲、火車的行進聲、平和的交通白噪音,都能令人置身其中。如果您渴望深度專注與徹底放鬆,Moodist 還提供雙耳節拍和彩色噪音,有助於調整心境。"
|
||||
},
|
||||
"section3": {
|
||||
"title": "打造專屬音景",
|
||||
"body": "Moodist 的魅力在於它介面簡潔,內容可客製化。它沒有複雜的選單,也沒有眼花撩亂的選項,您只需選擇自己喜歡的聲音,調整音量大小,然後點擊播放即可。想將溫婉的鳥鳴和舒適的雨聲混在一起?完全沒問題!您可以隨心疊加多個聲音,創造屬於自己的個人化聲音饗宴。"
|
||||
},
|
||||
"section4": {
|
||||
"title": "滿足不同需求",
|
||||
"body": "無論是想消除一整天的疲憊,提升工作學習時的專注力,還是輔助安然入眠,Moodist 都能給您帶來完美的聲音環境。最棒的一點是,Moodist 完全免費且開源,您可以無條件享受它帶來的好處。現在就開始使用 Moodist 吧,打造屬於自己的全新專注靜心空間!"
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
"prompt": "喜歡 Moodist 嗎?",
|
||||
"link-text": "歡迎贊助!",
|
||||
"section-title": "支持專案",
|
||||
"section-desc": "幫助 Moodist 保持免費且無廣告。",
|
||||
"section-button": "請我喝杯咖啡"
|
||||
},
|
||||
"source": {
|
||||
"title": "開源",
|
||||
"desc": "Moodist 是免費且開源的!"
|
||||
},
|
||||
"toolbar": {
|
||||
"menu-aria-label": "選單",
|
||||
"global-volume-label": "整體音量",
|
||||
"items": {
|
||||
"presets": "預設",
|
||||
"share": "分享聲音",
|
||||
"shuffle": "隨機組合",
|
||||
"sleep-timer": "睡眠定時器",
|
||||
"countdown": "倒數計時器",
|
||||
"pomodoro": "番茄鐘",
|
||||
"notepad": "記事本",
|
||||
"todo": "待辦清單",
|
||||
"breathing": "呼吸練習",
|
||||
"binaural": "雙耳節拍",
|
||||
"isochronic": "等時聲頻",
|
||||
"shortcuts": "快速鍵",
|
||||
"buy-me-a-coffee": "請我喝杯咖啡",
|
||||
"source-code": "原始碼"
|
||||
}
|
||||
},
|
||||
"scroll-to-top": {
|
||||
"aria-label": "捲動到頂部"
|
||||
},
|
||||
"unselect": {
|
||||
"tooltip": "取消選取所有聲音",
|
||||
"aria-label": "取消選取所有聲音",
|
||||
"restore": {
|
||||
"tooltip": "回復選取的聲音",
|
||||
"aria-label": "回復選取的聲音"
|
||||
}
|
||||
},
|
||||
"favorite": {
|
||||
"add": {
|
||||
"aria-label": "將 {{label}} 加入收藏"
|
||||
},
|
||||
"remove": {
|
||||
"aria-label": "將 {{label}} 取消收藏"
|
||||
}
|
||||
},
|
||||
"volume": {
|
||||
"aria-label": "{{label}} 音量"
|
||||
},
|
||||
"play-error": "請先選擇至少一種聲音進行播放。",
|
||||
"use-moodist": "開始使用 Moodist",
|
||||
"modals": {
|
||||
"presets": {
|
||||
"title": "聲音預設",
|
||||
"your-presets-title": "我的預設",
|
||||
"empty": "您還沒有儲存任何預設。",
|
||||
"new-preset-title": "新增預設",
|
||||
"placeholder": "預設名稱",
|
||||
"play-button-tooltip": "播放預設",
|
||||
"play-button-aria-label": "播放預設 {{label}}",
|
||||
"delete-button-tooltip": "刪除預設",
|
||||
"delete-button-aria-label": "刪除預設 {{label}}",
|
||||
"no-selected-warning": "請先選擇聲音,再建立預設。"
|
||||
},
|
||||
"share-link": {
|
||||
"title": "分享聲音組合!",
|
||||
"description": "複製下方連結,將選定的聲音組合分享給他人。",
|
||||
"copy-button-aria-label": "複製連結"
|
||||
},
|
||||
"shared": {
|
||||
"title": "偵測到聲音組合!",
|
||||
"description": "有人向您分享了以下聲音組合,是否要覆蓋目前的選擇?",
|
||||
"snackbar-message": "已完成!現在可以播放新的聲音組合了。"
|
||||
},
|
||||
"sleep-timer": {
|
||||
"title": "睡眠定時器",
|
||||
"description": "設定時間後自動停止播放聲音。",
|
||||
"hours-label": "小時",
|
||||
"minutes-label": "分鐘"
|
||||
},
|
||||
"countdown": {
|
||||
"title": "倒數計時器",
|
||||
"description": "簡潔實用的倒數計時器。",
|
||||
"placeholder-hh": "時",
|
||||
"placeholder-mm": "分",
|
||||
"placeholder-ss": "秒"
|
||||
},
|
||||
"pomodoro": {
|
||||
"title": "番茄鐘",
|
||||
"settings-tooltip": "設定時長",
|
||||
"completed": "已完成 {{count}} 個番茄鐘",
|
||||
"tabs": {
|
||||
"pomodoro": "工作",
|
||||
"short-break": "短休息",
|
||||
"long-break": "長休息"
|
||||
},
|
||||
"settings": {
|
||||
"title": "設定時長",
|
||||
"pomodoro-label": "工作時長",
|
||||
"short-break-label": "短休息時長",
|
||||
"long-break-label": "長休息時長",
|
||||
"minutes-unit": "分鐘"
|
||||
}
|
||||
},
|
||||
"notepad": {
|
||||
"title-label": "隨手記",
|
||||
"copy-tooltip": "複製筆記",
|
||||
"download-tooltip": "下載筆記",
|
||||
"clear-tooltip": "清空筆記",
|
||||
"restore-tooltip": "還原筆記",
|
||||
"placeholder": "記錄您的想法...",
|
||||
"counter-stats": "{{chars}} 個字元"
|
||||
},
|
||||
"todo": {
|
||||
"title": "待辦清單",
|
||||
"description": "簡潔實用的待辦事項列表。",
|
||||
"add-placeholder": "新增待辦事項...",
|
||||
"add-button": "新增",
|
||||
"your-todos-label": "我的待辦",
|
||||
"empty": "尚無待辦事項。",
|
||||
"delete-button-aria-label": "刪除此待辦事項"
|
||||
},
|
||||
"breathing": {
|
||||
"title": "呼吸練習",
|
||||
"phases": {
|
||||
"inhale": "吸氣",
|
||||
"exhale": "吐氣",
|
||||
"hold": "屏息"
|
||||
},
|
||||
"exercises": {
|
||||
"box": "方形呼吸法",
|
||||
"resonant": "同步呼吸法",
|
||||
"478": "4-7-8 呼吸法"
|
||||
}
|
||||
},
|
||||
"generators": {
|
||||
"presets-label": "預設:",
|
||||
"base-frequency-label": "基礎頻率 (Hz):",
|
||||
"volume-label": "音量:",
|
||||
"presets": {
|
||||
"delta": "Delta 波 (深度睡眠) 2 Hz",
|
||||
"theta": "Theta 波 (冥想) 5 Hz",
|
||||
"alpha": "Alpha 波 (放鬆) 10 Hz",
|
||||
"beta": "Beta 波 (專注) 20 Hz",
|
||||
"gamma": "Gamma 波 (認知) 40 Hz",
|
||||
"custom": "自訂"
|
||||
}
|
||||
},
|
||||
"binaural": {
|
||||
"title": "雙耳節拍",
|
||||
"description": "產生雙耳節拍聲音。",
|
||||
"beat-frequency-label": "節拍頻率 (Hz):"
|
||||
},
|
||||
"isochronic": {
|
||||
"title": "等時聲頻",
|
||||
"description": "產生等時聲頻聲音。",
|
||||
"tone-frequency-label": "聲頻頻率 (Hz):"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "鍵盤快速鍵",
|
||||
"labels": {
|
||||
"toggle-play": "播放/暫停",
|
||||
"unselect-all": "取消選取所有聲音"
|
||||
}
|
||||
},
|
||||
"reload": {
|
||||
"title": "有新的更新可用",
|
||||
"description": "偵測到新版本,點擊「重新載入」按鈕以更新。",
|
||||
"closeButton": "關閉",
|
||||
"reloadButton": "重新載入"
|
||||
}
|
||||
},
|
||||
"sounds": {
|
||||
"show-less": "收合",
|
||||
"show-more": "展開更多",
|
||||
"aria-label": "{{name}} 聲音",
|
||||
"favorites": {
|
||||
"title": "我的收藏"
|
||||
},
|
||||
"animals": {
|
||||
"title": "動物",
|
||||
"birds": "鳥鳴",
|
||||
"seagulls": "海鷗",
|
||||
"crickets": "蟋蟀",
|
||||
"wolf": "狼嚎",
|
||||
"owl": "貓頭鷹",
|
||||
"frog": "蛙鳴",
|
||||
"dog-barking": "狗吠",
|
||||
"horse-galopp": "馬蹄聲",
|
||||
"cat-purring": "貓呼嚕聲",
|
||||
"crows": "烏鴉",
|
||||
"whale": "鯨魚",
|
||||
"beehive": "蜂巢",
|
||||
"woodpecker": "啄木鳥",
|
||||
"chickens": "雞鳴",
|
||||
"cows": "牛叫",
|
||||
"sheep": "羊叫"
|
||||
},
|
||||
"binaural": {
|
||||
"title": "雙耳節拍",
|
||||
"binaural-delta": "Delta 波 (深度睡眠)",
|
||||
"binaural-theta": "Theta 波 (冥想)",
|
||||
"binaural-alpha": "Alpha 波 (放鬆)",
|
||||
"binaural-beta": "Beta 波 (專注)",
|
||||
"binaural-gamma": "Gamma 波 (認知)"
|
||||
},
|
||||
"nature": {
|
||||
"title": "自然",
|
||||
"river": "河流",
|
||||
"waves": "海浪",
|
||||
"campfire": "營火",
|
||||
"wind": "風聲",
|
||||
"howling-wind": "風嘯聲",
|
||||
"wind-in-trees": "林間風聲",
|
||||
"waterfall": "瀑布",
|
||||
"walk-in-snow": "雪地腳步聲",
|
||||
"walk-on-leaves": "落葉腳步聲",
|
||||
"walk-on-gravel": "碎石路腳步聲",
|
||||
"droplets": "水滴聲",
|
||||
"jungle": "叢林聲"
|
||||
},
|
||||
"noise": {
|
||||
"title": "噪音",
|
||||
"white-noise": "白噪音",
|
||||
"pink-noise": "粉噪音",
|
||||
"brown-noise": "棕噪音"
|
||||
},
|
||||
"places": {
|
||||
"title": "場所",
|
||||
"cafe": "咖啡館",
|
||||
"airport": "機場",
|
||||
"church": "教堂",
|
||||
"temple": "寺廟",
|
||||
"construction-site": "建築工地",
|
||||
"underwater": "水下",
|
||||
"crowded-bar": "吵雜酒吧",
|
||||
"night-village": "鄉村夜晚",
|
||||
"subway-station": "地鐵站",
|
||||
"office": "辦公室",
|
||||
"supermarket": "超級市場",
|
||||
"carousel": "旋轉木馬",
|
||||
"laboratory": "實驗室",
|
||||
"laundry-room": "洗衣間",
|
||||
"restaurant": "餐廳",
|
||||
"library": "圖書館"
|
||||
},
|
||||
"rain": {
|
||||
"title": "雨聲",
|
||||
"light-rain": "小雨",
|
||||
"heavy-rain": "大雨",
|
||||
"thunder": "雷聲",
|
||||
"rain-on-window": "雨打窗戶",
|
||||
"rain-on-car-roof": "雨打車頂",
|
||||
"rain-on-umbrella": "雨打雨傘",
|
||||
"rain-on-tent": "雨打帳篷",
|
||||
"rain-on-leaves": "雨打樹葉"
|
||||
},
|
||||
"things": {
|
||||
"title": "物品",
|
||||
"keyboard": "鍵盤聲",
|
||||
"typewriter": "打字機",
|
||||
"paper": "紙張翻動",
|
||||
"clock": "時鐘",
|
||||
"wind-chimes": "風鈴",
|
||||
"singing-bowl": "頌缽",
|
||||
"ceiling-fan": "吊扇",
|
||||
"dryer": "烘衣機",
|
||||
"slide-projector": "幻燈機",
|
||||
"boiling-water": "沸水聲",
|
||||
"bubbles": "氣泡聲",
|
||||
"tuning-radio": "收音機調頻",
|
||||
"morse-code": "摩斯電碼",
|
||||
"washing-machine": "洗衣機",
|
||||
"vinyl-effect": "黑膠唱片雜音",
|
||||
"windshield-wipers": "雨刷"
|
||||
},
|
||||
"transport": {
|
||||
"title": "交通",
|
||||
"train": "火車",
|
||||
"inside-a-train": "火車車廂",
|
||||
"airplane": "飛機",
|
||||
"submarine": "潛水艇",
|
||||
"sailboat": "帆船",
|
||||
"rowing-boat": "划船"
|
||||
},
|
||||
"urban": {
|
||||
"title": "城市",
|
||||
"highway": "高速公路",
|
||||
"road": "街道",
|
||||
"ambulance-siren": "救護車警笛",
|
||||
"busy-street": "繁忙街道",
|
||||
"crowd": "人群吵雜聲",
|
||||
"traffic": "車流聲",
|
||||
"fireworks": "煙火"
|
||||
}
|
||||
},
|
||||
"created-by": "由 {{authorLink}} 建立"
|
||||
}
|
||||
|
|
@ -1,20 +1,78 @@
|
|||
---
|
||||
// src/pages/index.astro
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
import Source from '@/components/source.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
|
||||
import { App } from '@/components/app';
|
||||
|
||||
// !! Make sure the defaultLocale and locales here are consistent with astro.config.mjs !!
|
||||
const defaultLocale = 'en';
|
||||
const supportedLocales = ['en', 'zh-CN', 'zh-TW', 'ja'];
|
||||
|
||||
const currentLocale = Astro.currentLocale || defaultLocale;
|
||||
---
|
||||
|
||||
<Layout title="Moodist: Ambient Sounds for Focus and Calm">
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<script define:vars={{ defaultLocale, supportedLocales }}>
|
||||
(function () {
|
||||
if (typeof window !== 'undefined' && window.location.pathname === '/') {
|
||||
const sessionRedirectKey = 'lang_redirect_attempted';
|
||||
|
||||
if (sessionStorage.getItem(sessionRedirectKey)) {
|
||||
console.log('Language redirect already attempted this session.');
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.setItem(sessionRedirectKey, 'true');
|
||||
|
||||
const browserLangs = navigator.languages || [navigator.language]; // navigator.language 作为备选
|
||||
|
||||
let preferredLocale = null;
|
||||
|
||||
for (const lang of browserLangs) {
|
||||
if (supportedLocales.includes(lang)) {
|
||||
preferredLocale = lang;
|
||||
break;
|
||||
}
|
||||
const baseLang = lang.split('-')[0];
|
||||
if (supportedLocales.includes(baseLang)) {
|
||||
preferredLocale = baseLang;
|
||||
}
|
||||
if (
|
||||
preferredLocale &&
|
||||
lang.split('-')[0] !== preferredLocale.split('-')[0]
|
||||
) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Browser Languages:', browserLangs);
|
||||
console.log('Detected Preferred Locale:', preferredLocale);
|
||||
|
||||
if (preferredLocale && preferredLocale !== defaultLocale) {
|
||||
const targetPath = `/${preferredLocale}${
|
||||
window.location.pathname === '/' ? '' : window.location.pathname
|
||||
}${window.location.search}${window.location.hash}`;
|
||||
console.log(
|
||||
`Redirecting to preferred locale: ${preferredLocale} at ${targetPath}`,
|
||||
);
|
||||
window.location.replace(targetPath);
|
||||
} else {
|
||||
console.log(
|
||||
'No preferred non-default locale found or already on preferred locale. No redirect needed.',
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
20
src/pages/ja/index.astro
Normal file
20
src/pages/ja/index.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
import Source from '@/components/source.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import { App } from '@/components/app';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
</Layout>
|
||||
20
src/pages/zh-CN/index.astro
Normal file
20
src/pages/zh-CN/index.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
import Source from '@/components/source.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import { App } from '@/components/app';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
</Layout>
|
||||
20
src/pages/zh-TW/index.astro
Normal file
20
src/pages/zh-TW/index.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
import Source from '@/components/source.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import { App } from '@/components/app';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
|
@ -42,3 +42,29 @@
|
|||
src: url('/fonts/inter-tight-v7-latin-700.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'LXGW WenKai Lite';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Light.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'LXGW WenKai Lite';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Regular.woff2')
|
||||
format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'LXGW WenKai Lite';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('/fonts/lxgw-wenkai-lite/LXGWWenKaiLite-Medium.woff2')
|
||||
format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@
|
|||
--font-base-size: 1rem;
|
||||
--font-pos-ratio: 1.2;
|
||||
--font-neg-ratio: 1.125;
|
||||
--font-3xlg: calc(var(--font-xxlg) * var(--font-pos-ratio));
|
||||
--font-2xlg: calc(var(--font-xlg) * var(--font-pos-ratio));
|
||||
--font-xlg: calc(var(--font-lg) * var(--font-pos-ratio));
|
||||
--font-lg: calc(var(--font-md) * var(--font-pos-ratio));
|
||||
--font-md: calc(var(--font-base) * var(--font-pos-ratio));
|
||||
--font-base: var(--font-base-size);
|
||||
--font-base: var(--font-base-size); /* Default base size */
|
||||
--font-sm: calc(var(--font-base) / var(--font-neg-ratio));
|
||||
--font-md: calc(var(--font-base) * var(--font-pos-ratio));
|
||||
--font-lg: calc(var(--font-md) * var(--font-pos-ratio));
|
||||
--font-xlg: calc(var(--font-lg) * var(--font-pos-ratio));
|
||||
--font-2xlg: calc(var(--font-xlg) * var(--font-pos-ratio));
|
||||
--font-3xlg: calc(var(--font-2xlg) * var(--font-pos-ratio));
|
||||
--font-xsm: calc(var(--font-sm) / var(--font-neg-ratio));
|
||||
--font-2xsm: calc(var(--font-xsm) / var(--font-neg-ratio));
|
||||
--font-3xsm: calc(var(--font-xxsm) / var(--font-neg-ratio));
|
||||
--font-3xsm: calc(var(--font-2xsm) / var(--font-neg-ratio));
|
||||
}
|
||||
|
||||
html:lang(zh-CN),
|
||||
html:lang(zh-TW),
|
||||
html:lang(ja) {
|
||||
--font-body: 'LXGW WenKai Lite', sans-serif;
|
||||
--font-heading: 'LXGW WenKai Lite', sans-serif;
|
||||
--font-base: calc(var(--font-base-size) * 1.15);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue