...
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-26 14:13:34 +08:00
parent 54e0eb452b
commit e8bc064ad5
5 changed files with 1224 additions and 190 deletions

View File

@@ -2,16 +2,14 @@ import Button from "@/components/Button";
import IconClick from "@/components/IconClick"; import IconClick from "@/components/IconClick";
import IMAGES from "@/config/images"; import IMAGES from "@/config/images";
import { Letter, SupportedAlphabets } from "@/interfaces"; import { Letter, SupportedAlphabets } from "@/interfaces";
import { Dispatch, KeyboardEvent, SetStateAction, useEffect, useRef, useState } from "react"; import { Dispatch, KeyboardEvent, SetStateAction, useEffect, useState } from "react";
export default function MemoryCard( export default function MemoryCard(
{ {
alphabet, alphabet,
language,
setChosenAlphabet setChosenAlphabet
}: { }: {
alphabet: Letter[], alphabet: Letter[],
language: string,
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>> setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>
} }
) { ) {
@@ -34,7 +32,7 @@ export default function MemoryCard(
} }
return ( return (
<div className="w-full flex justify-center items-center" onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}> <div className="w-full flex justify-center items-center" onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => e.preventDefault()}>
{/* <div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-2xl shadow border-gray-200 border flex justify-center items-center"> <div className="m-4 p-4 w-full md:w-[60dvw] flex-col rounded-2xl shadow border-gray-200 border flex justify-center items-center">
<div className="w-full flex justify-end items-center"> <div className="w-full flex justify-end items-center">
<IconClick size={32} alt="close" src={IMAGES.close} onClick={() => setChosenAlphabet(null)}></IconClick> <IconClick size={32} alt="close" src={IMAGES.close} onClick={() => setChosenAlphabet(null)}></IconClick>
</div> </div>
@@ -47,12 +45,12 @@ export default function MemoryCard(
<IconClick size={48} alt="more" src={IMAGES.more_horiz} onClick={() => setMore(!more)}></IconClick> <IconClick size={48} alt="more" src={IMAGES.more_horiz} onClick={() => setMore(!more)}></IconClick>
{ {
more ? (<> more ? (<>
<Button className="w-20" label={letterDisplay ? '隐藏字母' : '显示字母'} onClick={() => { setLetterDisplay(!letterDisplay) }}></Button> <Button className="w-20" onClick={() => { setLetterDisplay(!letterDisplay) }}>{letterDisplay ? '隐藏字母' : '显示字母'}</Button>
<Button className="w-20" label={ipaDisplay ? '隐藏IPA' : '显示IPA'} onClick={() => { setIPADisplay(!ipaDisplay) }}></Button> <Button className="w-20" onClick={() => { setIPADisplay(!ipaDisplay) }}>{ipaDisplay ? '隐藏IPA' : '显示IPA'}</Button>
</>) : (<></>) </>) : (<></>)
} }
</div> </div>
</div> */} </div>
</div> </div>
); );
} }

View File

@@ -75,7 +75,6 @@ export default function Alphabet() {
return (<> return (<>
<Navbar></Navbar> <Navbar></Navbar>
<MemoryCard <MemoryCard
language={chosenAlphabet}
alphabet={alphabetData[chosenAlphabet]} alphabet={alphabetData[chosenAlphabet]}
setChosenAlphabet={setChosenAlphabet}> setChosenAlphabet={setChosenAlphabet}>
</MemoryCard> </MemoryCard>

View File

@@ -10,6 +10,7 @@ import SaveList from "./SaveList";
import { TextSpeakerItemSchema } from "@/interfaces"; import { TextSpeakerItemSchema } from "@/interfaces";
import z from "zod"; import z from "zod";
import { Navbar } from "@/components/Navbar"; import { Navbar } from "@/components/Navbar";
import { VOICES } from "@/config/locales";
export default function TextSpeaker() { export default function TextSpeaker() {
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -25,19 +26,7 @@ export default function TextSpeaker() {
const [ipa, setIPA] = useState<string>(''); const [ipa, setIPA] = useState<string>('');
const objurlRef = useRef<string | null>(null); const objurlRef = useRef<string | null>(null);
const [processing, setProcessing] = useState(false); const [processing, setProcessing] = useState(false);
const [voicesData, setVoicesData] = useState<{
locale: string,
short_name: string
}[] | null>(null);
const [loading, setLoading] = useState(true);
const { playAudio, stopAudio, audioRef } = useAudioPlayer(); const { playAudio, stopAudio, audioRef } = useAudioPlayer();
useEffect(() => {
fetch('/list_of_voices.json')
.then(res => res.json())
.then(setVoicesData)
.catch(() => setVoicesData(null))
.finally(() => setLoading(false));
}, []);
useEffect(() => { useEffect(() => {
const audio = audioRef.current; const audio = audioRef.current;
if (!audio) return; if (!audio) return;
@@ -56,11 +45,6 @@ export default function TextSpeaker() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [audioRef, autopause]); }, [audioRef, autopause]);
if (loading) return <div>...</div>;
if (!voicesData) return <div></div>;
const speak = async () => { const speak = async () => {
if (processing) return; if (processing) return;
setProcessing(true); setProcessing(true);
@@ -103,7 +87,7 @@ export default function TextSpeaker() {
theLocale = textinfo.locale as string; theLocale = textinfo.locale as string;
} }
const voice = voicesData.find(v => v.locale.startsWith(theLocale)); const voice = VOICES.find(v => v.locale.startsWith(theLocale));
if (!voice) throw 'Voice not found.'; if (!voice) throw 'Voice not found.';
objurlRef.current = await getTTSAudioUrl( objurlRef.current = await getTTSAudioUrl(

View File

@@ -7,15 +7,11 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import IMAGES from "@/config/images"; import IMAGES from "@/config/images";
import { getTTSAudioUrl } from "@/utils"; import { getTTSAudioUrl } from "@/utils";
import { Navbar } from "@/components/Navbar"; import { Navbar } from "@/components/Navbar";
import { VOICES } from "@/config/locales";
export default function Translator() { export default function Translator() {
const [ipaEnabled, setIPAEnabled] = useState(true); const [ipaEnabled, setIPAEnabled] = useState(true);
const [voicesData, setVoicesData] = useState<{ const [targetLang, setTargetLang] = useState('Chinese');
locale: string,
short_name: string
}[] | null>(null);
const [loading, setLoading] = useState(true);
const [targetLang, setTargetLang] = useState('Italian');
const [sourceText, setSourceText] = useState(''); const [sourceText, setSourceText] = useState('');
const [targetText, setTargetText] = useState(''); const [targetText, setTargetText] = useState('');
@@ -23,21 +19,10 @@ export default function Translator() {
const [targetIPA, setTargetIPA] = useState(''); const [targetIPA, setTargetIPA] = useState('');
const [sourceLocale, setSourceLocale] = useState<string | null>(null); const [sourceLocale, setSourceLocale] = useState<string | null>(null);
const [targetLocale, setTargetLocale] = useState<string | null>(null); const [targetLocale, setTargetLocale] = useState<string | null>(null);
const [translating, setTranslating] = useState(false); const [translating, setTranslating] = useState(false);
const { playAudio } = useAudioPlayer(); const { playAudio } = useAudioPlayer();
useEffect(() => { const tl = ['Chinese', 'English', 'Italian'];
fetch('/list_of_voices.json')
.then(res => res.json())
.then(setVoicesData)
.catch(() => setVoicesData(null))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>...</div>;
if (!voicesData) return <div></div>;
const tl = ['English', 'Italian', 'Japanese'];
const inputLanguage = () => { const inputLanguage = () => {
const lang = prompt('Input a language.')?.trim(); const lang = prompt('Input a language.')?.trim();
@@ -136,7 +121,7 @@ export default function Translator() {
const info = await res.json(); const info = await res.json();
setSourceLocale(info.locale); setSourceLocale(info.locale);
const voice = voicesData.find(v => v.locale.startsWith(info.locale)); const voice = VOICES.find(v => v.locale.startsWith(info.locale));
if (!voice) { if (!voice) {
return; return;
} }
@@ -150,7 +135,7 @@ export default function Translator() {
return; return;
} }
} else { } else {
const voice = voicesData.find(v => v.locale.startsWith(sourceLocale!)); const voice = VOICES.find(v => v.locale.startsWith(sourceLocale!));
if (!voice) { if (!voice) {
return; return;
} }
@@ -178,7 +163,7 @@ export default function Translator() {
}) })
} }
const voice = voicesData.find(v => v.locale.startsWith(targetLocale!)); const voice = VOICES.find(v => v.locale.startsWith(targetLocale!));
if (!voice) return; if (!voice) return;
const url = await getTTSAudioUrl(targetText, voice.short_name); const url = await getTTSAudioUrl(targetText, voice.short_name);
@@ -227,9 +212,9 @@ export default function Translator() {
</div> </div>
<div className="option2 w-full flex gap-1 items-center flex-wrap"> <div className="option2 w-full flex gap-1 items-center flex-wrap">
<span>translate into</span> <span>translate into</span>
<Button onClick={() => { setTargetLang('Chinese') }} selected={targetLang === 'Chinese'}>Chinese</Button>
<Button onClick={() => { setTargetLang('English') }} selected={targetLang === 'English'}>English</Button> <Button onClick={() => { setTargetLang('English') }} selected={targetLang === 'English'}>English</Button>
<Button onClick={() => { setTargetLang('Italian') }} selected={targetLang === 'Italian'}>Italian</Button> <Button onClick={() => { setTargetLang('Italian') }} selected={targetLang === 'Italian'}>Italian</Button>
<Button onClick={() => { setTargetLang('Japanese') }} selected={targetLang === 'Japanese'}>Japanese</Button>
<Button onClick={inputLanguage} selected={!(tl.includes(targetLang))}>{'Other' + (tl.includes(targetLang) ? '' : ': ' + targetLang)}</Button> <Button onClick={inputLanguage} selected={!(tl.includes(targetLang))}>{'Other' + (tl.includes(targetLang) ? '' : ': ' + targetLang)}</Button>
</div> </div>
</div> </div>

File diff suppressed because it is too large Load Diff