Compare commits

..

3 Commits

Author SHA1 Message Date
a88dd2b91a 优化了一些细节
Some checks reported errors
continuous-integration/drone/push Build encountered an error
2025-12-02 17:39:55 +08:00
4cbde97f41 背单词可以设置索引 2025-11-24 16:01:53 +08:00
7bf3fd9b17 ... 2025-11-22 09:24:08 +08:00
7 changed files with 174 additions and 157 deletions

View File

@@ -66,11 +66,11 @@
"description": "Play videos sentence by sentence based on SRT subtitle files to mimic native speaker pronunciation" "description": "Play videos sentence by sentence based on SRT subtitle files to mimic native speaker pronunciation"
}, },
"alphabet": { "alphabet": {
"name": "Memorize Alphabet", "name": "Alphabet",
"description": "Start learning a new language from the alphabet" "description": "Start learning a new language from the alphabet"
}, },
"memorize": { "memorize": {
"name": "Memorize Words", "name": "Memorize",
"description": "Language A to Language B, Language B to Language A, supports dictation" "description": "Language A to Language B, Language B to Language A, supports dictation"
}, },
"moreFeatures": { "moreFeatures": {
@@ -94,7 +94,6 @@
"reverse": "Reverse", "reverse": "Reverse",
"dictation": "Dictation", "dictation": "Dictation",
"noTextPairs": "No text pairs available", "noTextPairs": "No text pairs available",
"progress": "{current}/{total}",
"disorder": "Disorder", "disorder": "Disorder",
"previous": "Previous" "previous": "Previous"
}, },

View File

@@ -62,15 +62,15 @@
"description": "识别并朗读文本,支持循环朗读、朗读速度调节" "description": "识别并朗读文本,支持循环朗读、朗读速度调节"
}, },
"srtPlayer": { "srtPlayer": {
"name": "逐句视频播放器", "name": "逐句视频",
"description": "基于SRT字幕文件逐句播放视频以模仿母语者的发音" "description": "基于SRT字幕文件逐句播放视频以模仿母语者的发音"
}, },
"alphabet": { "alphabet": {
"name": "字母", "name": "字母",
"description": "从字母表开始新语言的学习" "description": "从字母表开始新语言的学习"
}, },
"memorize": { "memorize": {
"name": "背单词", "name": "记忆",
"description": "语言A到语言B语言B到语言A支持听写" "description": "语言A到语言B语言B到语言A支持听写"
}, },
"moreFeatures": { "moreFeatures": {
@@ -98,7 +98,6 @@
"reverse": "反向", "reverse": "反向",
"dictation": "听写", "dictation": "听写",
"noTextPairs": "没有可用的文本对", "noTextPairs": "没有可用的文本对",
"progress": "{current}/{total}",
"disorder": "乱序", "disorder": "乱序",
"previous": "上一个" "previous": "上一个"
}, },

View File

@@ -3,13 +3,14 @@
import { Center } from "@/components/Center"; import { Center } from "@/components/Center";
import { text_pair } from "../../../../generated/prisma/browser"; import { text_pair } from "../../../../generated/prisma/browser";
import Container from "@/components/cards/Container"; import Container from "@/components/cards/Container";
import { useEffect, useRef, useState } from "react"; import { useEffect, useState } from "react";
import LightButton from "@/components/buttons/LightButton"; import LightButton from "@/components/buttons/LightButton";
import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import { getTTSAudioUrl } from "@/lib/browser/tts"; import { getTTSAudioUrl } from "@/lib/browser/tts";
import { VOICES } from "@/config/locales"; import { VOICES } from "@/config/locales";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import localFont from "next/font/local"; import localFont from "next/font/local";
import { isNonNegativeInteger } from "@/lib/utils";
const myFont = localFont({ const myFont = localFont({
src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf", src: "../../../../public/fonts/NotoNaskhArabic-VariableFont_wght.ttf",
@@ -44,119 +45,134 @@ const Memorize: React.FC<MemorizeProps> = ({ textPairs }) => {
}; };
return ( return (
<Center> <>
<Container className="p-6 flex flex-col gap-8 h-96 justify-center items-center"> {(getTextPairs().length > 0 && (
{(getTextPairs().length > 0 && ( <>
<> <div className="text-center">
<div <div
className={`h-36 flex flex-col gap-2 justify-start items-center ${myFont.className} text-3xl`} className="text-sm text-gray-500"
onClick={() => {
const newIndex = prompt("Input a index number.")?.trim();
if (
newIndex &&
isNonNegativeInteger(newIndex) &&
parseInt(newIndex) <= textPairs.length &&
parseInt(newIndex) > 0
) {
setIndex(parseInt(newIndex) - 1);
}
}}
> >
<div className="text-sm text-gray-500"> {index + 1}
{t("progress", { {"/" + getTextPairs().length}
current: index + 1,
total: getTextPairs().length,
})}
</div>
{dictation ? (
show === "question" ? (
""
) : (
<>
<div>
{reverse
? getTextPairs()[index].text2
: getTextPairs()[index].text1}
</div>
<div>
{reverse
? getTextPairs()[index].text1
: getTextPairs()[index].text2}
</div>
</>
)
) : (
<>
<div>
{reverse
? getTextPairs()[index].text2
: getTextPairs()[index].text1}
</div>
<div>
{show === "answer"
? reverse
? getTextPairs()[index].text1
: getTextPairs()[index].text2
: ""}
</div>
</>
)}
</div> </div>
<div className="flex flex-row gap-2 items-center justify-center"> <div className="h-[40dvh] px-16">
<LightButton {(() => {
className="w-20" const createText = (text: string) => {
onClick={async () => { return (
if (show === "answer") { <div className="text-gray-900 text-xl border-y border-y-gray-200 p-4 md:text-3xl h-[20dvh] overflow-y-auto">
const newIndex = (index + 1) % getTextPairs().length; {text}
setIndex(newIndex); </div>
if (dictation)
getTTSAudioUrl(
getTextPairs()[newIndex][reverse ? "text2" : "text1"],
VOICES.find(
(v) =>
v.locale ===
getTextPairs()[newIndex][
reverse ? "locale2" : "locale1"
],
)!.short_name,
).then((url) => {
load(url);
play();
});
}
setShow(show === "question" ? "answer" : "question");
}}
>
{show === "question" ? t("answer") : t("next")}
</LightButton>
<LightButton
onClick={() => {
setIndex(
(index - 1 + getTextPairs().length) % getTextPairs().length,
); );
setShow("question"); };
}}
> const [text1, text2] = reverse
{t("previous")} ? [getTextPairs()[index].text2, getTextPairs()[index].text1]
</LightButton> : [getTextPairs()[index].text1, getTextPairs()[index].text2];
<LightButton
onClick={() => { if (dictation) {
setReverse(!reverse); // dictation
}} if (show === "question") {
selected={reverse} return createText("");
> } else {
{t("reverse")} return (
</LightButton> <>
<LightButton {createText(text1)}
onClick={() => { {createText(text2)}
setDictation(!dictation); </>
}} );
selected={dictation} }
> } else {
{t("dictation")} // non-dictation
</LightButton> if (show === "question") {
<LightButton return createText(text1);
onClick={() => { } else {
setDisorder(!disorder); return (
}} <>
selected={disorder} {createText(text1)}
> {createText(text2)}
{t("disorder")} </>
</LightButton> );
}
}
})()}
</div> </div>
</> </div>
)) || <p>{t("noTextPairs")}</p>} <div className="flex flex-row gap-2 items-center justify-center flex-wrap">
</Container> <LightButton
</Center> className="w-20"
onClick={async () => {
if (show === "answer") {
const newIndex = (index + 1) % getTextPairs().length;
setIndex(newIndex);
if (dictation)
getTTSAudioUrl(
getTextPairs()[newIndex][reverse ? "text2" : "text1"],
VOICES.find(
(v) =>
v.locale ===
getTextPairs()[newIndex][
reverse ? "locale2" : "locale1"
],
)!.short_name,
).then((url) => {
load(url);
play();
});
}
setShow(show === "question" ? "answer" : "question");
}}
>
{show === "question" ? t("answer") : t("next")}
</LightButton>
<LightButton
onClick={() => {
setIndex(
(index - 1 + getTextPairs().length) % getTextPairs().length,
);
setShow("question");
}}
>
{t("previous")}
</LightButton>
<LightButton
onClick={() => {
setReverse(!reverse);
}}
selected={reverse}
>
{t("reverse")}
</LightButton>
<LightButton
onClick={() => {
setDictation(!dictation);
}}
selected={dictation}
>
{t("dictation")}
</LightButton>
<LightButton
onClick={() => {
setDisorder(!disorder);
}}
selected={disorder}
>
{t("disorder")}
</LightButton>
</div>
</>
)) || <p>{t("noTextPairs")}</p>}
</>
); );
}; };

View File

@@ -23,7 +23,7 @@ const FolderSelector: React.FC<FolderSelectorProps> = ({
getFoldersByOwner(username) getFoldersByOwner(username)
.then(setFolders) .then(setFolders)
.then(() => setLoading(false)); .then(() => setLoading(false));
}, []); }, [username]);
return ( return (
<div <div

View File

@@ -325,35 +325,36 @@ export default function TranslatorPage() {
<h1 className="text-2xl font-light">{t("history")}</h1> <h1 className="text-2xl font-light">{t("history")}</h1>
<div className="border border-gray-200 rounded-2xl m-4"> <div className="border border-gray-200 rounded-2xl m-4">
{history.toReversed().map((item, index) => ( {history.toReversed().map((item, index) => (
<div key={index}> <div
<div className="border-b border-gray-200 p-2 group hover:bg-gray-50 flex gap-2 flex-row justify-between items-start"> key={index}
<div className="flex-1 flex flex-col"> className="border-b border-gray-200 p-2 group hover:bg-gray-50 flex gap-2 flex-row justify-between items-start"
<p className="text-sm font-light">{item.text1}</p> >
<p className="text-sm font-light">{item.text2}</p> <div className="flex-1 flex flex-col">
</div> <p className="text-sm font-light">{item.text1}</p>
<div className="flex gap-2"> <p className="text-sm font-light">{item.text2}</p>
<button </div>
onClick={() => { <div className="flex gap-2">
setShowAddToFolder(true); <button
setAddToFolderItem(item); onClick={() => {
}} setShowAddToFolder(true);
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center" setAddToFolderItem(item);
> }}
<Plus /> className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center"
</button> >
<button <Plus />
onClick={() => { </button>
setHistory( <button
tlso.set( onClick={() => {
tlso.get().filter((v) => !shallowEqual(v, item)), setHistory(
) || [], tlso.set(
); tlso.get().filter((v) => !shallowEqual(v, item)),
}} ) || [],
className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center" );
> }}
<Trash /> className="hover:bg-gray-200 hover:cursor-pointer rounded-4xl border border-gray-200 w-8 h-8 flex justify-center items-center"
</button> >
</div> <Trash />
</button>
</div> </div>
</div> </div>
))} ))}

View File

@@ -51,8 +51,16 @@ export default function TextPairCard({
</div> </div>
</div> </div>
<div className="text-gray-900 grid grid-cols-2 gap-4 w-3/4"> <div className="text-gray-900 grid grid-cols-2 gap-4 w-3/4">
<div>{textPair.text1}</div> <div>
<div>{textPair.text2}</div> {textPair.text1.length > 30
? textPair.text1.substring(0, 30) + "..."
: textPair.text1}
</div>
<div>
{textPair.text2.length > 30
? textPair.text2.substring(0, 30) + "..."
: textPair.text2}
</div>
</div> </div>
</div> </div>
<UpdateTextPairModal <UpdateTextPairModal

View File

@@ -29,8 +29,8 @@ export default function HomePage() {
className={`h-32 md:h-64 flex md:justify-center items-center`} className={`h-32 md:h-64 flex md:justify-center items-center`}
> >
<div className="text-white m-8"> <div className="text-white m-8">
<h1 className="text-4xl">{name}</h1> <h1 className="md:text-4xl text-3xl">{name}</h1>
<p className="text-xl">{description}</p> <p className="md:text-xl">{description}</p>
</div> </div>
</Link> </Link>
); );
@@ -50,11 +50,6 @@ export default function HomePage() {
description={t("textSpeaker.description")} description={t("textSpeaker.description")}
color="#578aad" color="#578aad"
></LinkArea> ></LinkArea>
{/* <LinkArea
href="/word-board"
name="词墙"
description="将单词固定到一片区域,高效便捷地记忆单词"
color="#e9b353"></LinkArea> */}
<LinkArea <LinkArea
href="/srt-player" href="/srt-player"
name={t("srtPlayer.name")} name={t("srtPlayer.name")}
@@ -92,8 +87,7 @@ export default function HomePage() {
} }
function Explore() { function Explore() {
return ( return (
<div className="bg-[#bbbbbb] w-full flex justify-center items-center flex-col h-52"> <div className="bg-[#bbbbbb] w-full flex justify-center items-center flex-col h-32">
<span className="text-[100px] text-white">{t("explore")}</span>
<div className="w-0 h-0 border-l-40 border-r-40 border-t-30 border-l-transparent border-r-transparent border-t-white"></div> <div className="w-0 h-0 border-l-40 border-r-40 border-t-30 border-l-transparent border-r-transparent border-t-white"></div>
</div> </div>
); );