美化主界面
This commit is contained in:
12
package.json
12
package.json
@@ -9,19 +9,19 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.5.3",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"next": "15.5.3"
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.5.3",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
function Link(
|
||||
{href, label}: {href: string, label: string}
|
||||
import Link from "next/link";
|
||||
|
||||
function MyLink(
|
||||
{ href, label }: { href: string, label: string }
|
||||
) {
|
||||
return (
|
||||
<a className="border-2 border-black m-1 p-5 rounded font-bold hover:bg-gray-200" href={href}>{label}</a>
|
||||
<Link className="hover:bg-gray-400 border-2 border-black m-1 p-2 rounded font-bold" href={href}>{label}</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="w-80 m-auto mt-[100px]">
|
||||
<h1 className="mb-8 text-4xl font-bold">学外语</h1>
|
||||
<Link href="/srt-player" label="srt-player"></Link>
|
||||
<Link href="/word-board" label="word-board"></Link>
|
||||
<p className="mt-8">srt-player: 一个基于srt字幕文件的逐句视频播放器,需要上传视频文件与字幕文件使用。</p>
|
||||
<p>word-board: 一个板式单词记忆工具。</p>
|
||||
<div className="bg-gray-50 flex h-screen w-screen items-center justify-center">
|
||||
<div className="bg-white m-4 p-4 rounded-2xl shadow-2xl border-gray-400 border-2 flex-col flex items-center">
|
||||
<span className="text-4xl font-bold">Learn Languages</span>
|
||||
<div className="LinkList flex flex-wrap sm:flex-row">
|
||||
<MyLink href="/srt-player" label="srt-player"></MyLink>
|
||||
<MyLink href="/word-board" label="word-board"></MyLink>
|
||||
<MyLink href={'/changelog.txt'} label="changelog"></MyLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import inspect from "@/utilities";
|
||||
import { inspect } from "@/utilities";
|
||||
|
||||
export default function SubtitleDisplay({ subtitle }: { subtitle: string }) {
|
||||
const words = subtitle.match(/\b[\w']+(?:-[\w']+)*\b/g) || [];
|
||||
|
||||
@@ -1,21 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
|
||||
import Word from "@/interfaces/Word";
|
||||
import inspect from "@/utilities";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
function DraggableWord({ word }: { word: Word }) {
|
||||
return ((<span
|
||||
style={{
|
||||
left: `${Math.floor(word.x * (1000 - 18 * word.word.length))}px`,
|
||||
top: `${Math.floor(word.y * (600 - 30))}px`,
|
||||
}}
|
||||
className={`select-none cursor-pointer absolute font-mono text-[30px] border-amber-100 border-1`}
|
||||
onClick={inspect(word.word)}>{word.word}</span>))
|
||||
}
|
||||
import { Dispatch, SetStateAction, useEffect } from "react";
|
||||
|
||||
export default function WordBoard(
|
||||
{ words, setWords }: {
|
||||
{ words, setWords, selectWord }: {
|
||||
words: [
|
||||
{
|
||||
word: string,
|
||||
@@ -23,20 +13,26 @@ export default function WordBoard(
|
||||
y: number
|
||||
}
|
||||
],
|
||||
setWords: Dispatch<SetStateAction<Word[]>>
|
||||
setWords: Dispatch<SetStateAction<Word[]>>,
|
||||
selectWord: (word: string) => void
|
||||
}
|
||||
) {
|
||||
const inspect = (word: string) => {
|
||||
const goto = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
return () => {
|
||||
word = word.toLowerCase();
|
||||
goto(`https://www.youdao.com/result?word=${word}&lang=en`);
|
||||
}
|
||||
function DraggableWord({ word }: { word: Word }) {
|
||||
return (<span
|
||||
style={{
|
||||
left: `${Math.floor(word.x * (BOARD_WIDTH - TEXT_WIDTH * word.word.length))}px`,
|
||||
top: `${Math.floor(word.y * (BOARD_HEIGHT - TEXT_SIZE))}px`,
|
||||
fontSize: `${TEXT_SIZE}px`
|
||||
}}
|
||||
className="select-none cursor-pointer absolute font-mono border-amber-100 border-1"
|
||||
// onClick={inspect(word.word)}>{word.word}</span>))
|
||||
onClick={() => { selectWord(word.word); }}>{word.word}</span>);
|
||||
}
|
||||
return (
|
||||
<div className="relative rounded bg-white w-[1000px] h-[600px]">
|
||||
<div style={{
|
||||
width: `${BOARD_WIDTH}px`,
|
||||
height: `${BOARD_HEIGHT}px`
|
||||
}} className="relative rounded bg-white">
|
||||
{words.map(
|
||||
(v: {
|
||||
word: string,
|
||||
|
||||
@@ -25,6 +25,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
style={{
|
||||
|
||||
}}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client';
|
||||
import WordBoard from "@/app/word-board/WordBoard";
|
||||
import Button from "../../components/Button";
|
||||
import { useRef, useState } from "react";
|
||||
import { KeyboardEvent, useRef, useState } from "react";
|
||||
import Word from "@/interfaces/Word";
|
||||
import { BOARD_WIDTH, TEXT_WIDTH, BOARD_HEIGHT, TEXT_SIZE } from "@/constants";
|
||||
import { inspect } from "@/utilities";
|
||||
|
||||
export default function Home() {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -26,17 +28,52 @@ export default function Home() {
|
||||
}))
|
||||
);
|
||||
const generateNewWord = (word: string) => {
|
||||
return {
|
||||
const isOK = (w: Word) => {
|
||||
if (words.length === 0) return true;
|
||||
const tf = (ww: Word) => ({
|
||||
word: ww.word,
|
||||
x: Math.floor(ww.x * (BOARD_WIDTH - TEXT_WIDTH * ww.word.length)),
|
||||
y: Math.floor(ww.y * (BOARD_HEIGHT - TEXT_SIZE))
|
||||
} as Word);
|
||||
const tfd_words = words.map(tf);
|
||||
const tfd_w = tf(w);
|
||||
for (const www of tfd_words) {
|
||||
const p1 = {
|
||||
x: (www.x + www.x + TEXT_WIDTH * www.word.length) / 2,
|
||||
y: (www.y + www.y + TEXT_SIZE) / 2
|
||||
}
|
||||
const p2 = {
|
||||
x: (tfd_w.x + tfd_w.x + TEXT_WIDTH * tfd_w.word.length) / 2,
|
||||
y: (tfd_w.y + tfd_w.y + TEXT_SIZE) / 2
|
||||
}
|
||||
if (
|
||||
Math.abs(p1.x - p2.x) < (TEXT_WIDTH * (www.word.length + tfd_w.word.length)) / 2 &&
|
||||
Math.abs(p1.y - p2.y) < TEXT_SIZE
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let new_word;
|
||||
let count = 0;
|
||||
do {
|
||||
new_word = {
|
||||
word: word,
|
||||
x: Math.random(),
|
||||
y: Math.random()
|
||||
} as Word;
|
||||
};
|
||||
if (++count > 1000) return null;
|
||||
} while (!isOK(new_word));
|
||||
return new_word as Word;
|
||||
}
|
||||
const insertWord = () => {
|
||||
if (!inputRef.current) return;
|
||||
const word = inputRef.current.value.trim();
|
||||
if (word === '') return;
|
||||
setWords([...words, generateNewWord(word)]);
|
||||
const new_word = generateNewWord(word);
|
||||
if (!new_word) return;
|
||||
setWords([...words, new_word]);
|
||||
inputRef.current.value = '';
|
||||
}
|
||||
const deleteWord = () => {
|
||||
@@ -72,16 +109,47 @@ export default function Home() {
|
||||
reader.readAsText(files[0]);
|
||||
}
|
||||
}
|
||||
const deleteAll = () => {
|
||||
setWords([] as Array<Word>);
|
||||
}
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||
// e.preventDefault();
|
||||
if (e.key === 'Enter') {
|
||||
insertWord();
|
||||
}
|
||||
}
|
||||
const selectWord = (word: string) => {
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.value = word;
|
||||
}
|
||||
const searchWord = () => {
|
||||
if (!inputRef.current) return;
|
||||
const word = inputRef.current.value.trim();
|
||||
if (word === '') return;
|
||||
inspect(word)();
|
||||
inputRef.current.value = '';
|
||||
}
|
||||
// const readWordAloud = () => {
|
||||
// playFromUrl('https://fanyi.baidu.com/gettts?lan=uk&text=disclose&spd=3')
|
||||
// return;
|
||||
// if (!inputRef.current) return;
|
||||
// const word = inputRef.current.value.trim();
|
||||
// if (word === '') return;
|
||||
// inspect(word)();
|
||||
// inputRef.current.value = '';
|
||||
// }
|
||||
return (
|
||||
<div className="p-5 my-10 mx-auto bg-gray-200 rounded shadow-2xl w-[1050px]">
|
||||
<WordBoard words={words as [Word]} setWords={setWords} />
|
||||
<div onKeyDown={handleKeyDown} className="p-5 my-10 mx-auto bg-gray-200 rounded shadow-2xl w-[1050px]">
|
||||
<WordBoard selectWord={selectWord} words={words as [Word]} setWords={setWords} />
|
||||
<div className="flex justify-center rounded mt-3 w-[1000px]">
|
||||
<input ref={inputRef} placeholder="在此插入/删除单词" type="text" className="focus:outline-none border-b-2 border-black" />
|
||||
<input ref={inputRef} placeholder="word to operate" type="text" className="focus:outline-none border-b-2 border-black" />
|
||||
<Button label="插入" onClick={insertWord}></Button>
|
||||
<Button label="删除" onClick={deleteWord}></Button>
|
||||
<Button label="搜索" onClick={searchWord}></Button>
|
||||
<Button label="导入" onClick={importWords}></Button>
|
||||
<Button label="导出" onClick={exportWords}></Button>
|
||||
<Button label="删光" onClick={()=>{setWords([] as Array<Word>)}}></Button>
|
||||
<Button label="删光" onClick={deleteAll}></Button>
|
||||
{/* <Button label="朗读" onClick={readWordAloud}></Button> */}
|
||||
</div>
|
||||
<input type="file" ref={inputFileRef} className="hidden" accept="application/json" onChange={handleFileChange}></input>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function inspect(word: string) {
|
||||
export function inspect(word: string) {
|
||||
const goto = (url: string) => {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user