Compare commits
3 Commits
e8f5ce9751
...
a88dd2b91a
| Author | SHA1 | Date | |
|---|---|---|---|
| a88dd2b91a | |||
| 4cbde97f41 | |||
| 7bf3fd9b17 |
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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": "上一个"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user