feat: 🎸 word lookup, remove dict download (#1038)

This commit is contained in:
divisey
2024-09-03 20:03:07 +08:00
committed by GitHub
parent ef096c2c57
commit e16990a8b4
28 changed files with 259 additions and 406 deletions

View File

@@ -8,6 +8,7 @@ export const DICTS = [
downloadUrl: "https://dl.enjoy.bot/dicts/ccalecd.zip",
size: "13.879MB",
hash: "96940f85e52df4586b287e1859723a39",
sqlFileHash: "e1e7baafaa8bce936409763d27a027f8",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
@@ -20,6 +21,7 @@ export const DICTS = [
downloadUrl: "https://dl.enjoy.bot/dicts/ccabeld.zip",
size: "485.6MB",
hash: "5b53498536f3ce3ed173752b7888ca51",
sqlFileHash: "0eed5c046a006f3fe0c7a4af1bee5da1",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
{
@@ -31,6 +33,7 @@ export const DICTS = [
downloadUrl: "https://dl.enjoy.bot/dicts/ldoce5.zip",
size: "1.63GB",
hash: "4a03ce291ea7b6e0ea46f4c2fc335ad4",
sqlFileHash: "b38a9e55bd97c5acfa267b87c825baf7",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
{
@@ -41,6 +44,7 @@ export const DICTS = [
lang: "En-En",
downloadUrl: "https://dl.enjoy.bot/dicts/oxford_en_mac.zip",
hash: "cffaef4b3ed6ec7d3ee7209b18e05c6f",
sqlFileHash: "3362137bf8e2e2578665db3ad8d49814",
size: "33.6MB",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
@@ -53,6 +57,7 @@ export const DICTS = [
downloadUrl: "https://dl.enjoy.bot/dicts/koen_mac.zip",
size: "52.1MB",
hash: "fa028c585de10e54a7028c6683738499",
sqlFileHash: "e55f771e6acd50d757428fe61f13650e",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
{
@@ -63,6 +68,7 @@ export const DICTS = [
lang: "Ja-En",
downloadUrl: "https://dl.enjoy.bot/dicts/jaen_mac.zip",
hash: "3008e1cd2a8b6f224f90d14a8e1de9cb",
sqlFileHash: "76597b8608ba085b9b88adce10449bf3",
size: "39.8MB",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
@@ -74,6 +80,7 @@ export const DICTS = [
lang: "Ge-En",
downloadUrl: "https://dl.enjoy.bot/dicts/deen_mac.zip",
hash: "3fedde07108236f6e6cfe907bd60faba",
sqlFileHash: "84b3097bfa83cdae8c264ca595923101",
size: "32.1MB",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
@@ -85,6 +92,7 @@ export const DICTS = [
lang: "Ru-En",
downloadUrl: "https://dl.enjoy.bot/dicts/ruen_mac.zip",
hash: "5b98fc0e5c3de9df43189cb79d5bf4cc",
sqlFileHash: "1d72eadd51d82d48b3c772e5580d3524",
size: "18.1MB",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},

View File

@@ -744,7 +744,7 @@
"recorderConfig": "Recorder config",
"recorderConfigSaved": "Recorder config saved",
"recorderConfigDescription": "Advanced settings for recorder",
"lookupOnMouseOver": "Lookup On MouseOver",
"lookupOnMouseOver": "Lookup On Click",
"selectDictFile": "Select Dict Files (extension with .mdx and .mdd files)",
"dictFiles": "Dict Files",
"dictFileRequired": "Dict file (the extension is mdx) not found.",
@@ -771,5 +771,14 @@
"removeDictTitle": "Are you sure you want to delete this dictionary? ",
"removeDictDescription": "It will delete the dictionary file from your local computer and you will have to download it again next time.",
"downloadingDict": "Downloading",
"removeDefault": "No longer as Default"
"removeDefault": "No longer as Default",
"selectAdaptionDictTitle": "Select adapted dictionary folder",
"selectMdictFileOrDirTitle": "Select dict files (.mdx and optional .mdd) or folder",
"dictImportSlowTip": "It may take longer if the dictionary file is large.",
"importAdaptionDict": "Import the adapted dictionary",
"adaptionDictTip": "The adapted dictionaries have better usability.",
"howToDownload": "How to download?",
"selectDir": "Select Folder",
"importMdictFile": "Import the original dictionary file",
"mdictFileTip": "Directly import .mdx .mdd format files (.mdx files are required, .mdd files are optional and can have multiple), but there may be problems with style and usability."
}

View File

@@ -744,7 +744,7 @@
"recorderConfig": "录音设置",
"recorderConfigSaved": "录音设置已保存",
"recorderConfigDescription": "调整录音高级设置",
"lookupOnMouseOver": "鼠标悬停查询单词",
"lookupOnMouseOver": "鼠标点击查询单词",
"selectDictFile": "选择字典文件 (扩展名为 .mdx 和 .mdd 的文件)",
"dictFiles": "字典文件",
"dictFileRequired": "未找到字典文件 (扩展名为 .mdx 的文件是必须的) ",
@@ -771,5 +771,14 @@
"removeDictTitle": "你确定要删除词典吗?",
"removeDictDescription": "此操作将会从本地删除词典文件,下次安装需要重新下载",
"downloadingDict": "正在下载",
"removeDefault": "不再设置为默认"
"removeDefault": "不再设置为默认",
"selectAdaptionDictTitle": "选择预先适配的词典文件夹",
"selectMdictFileOrDirTitle": "选择词典文件 (.mdx 和可选的 .mdd 文件) or 文件夹",
"dictImportSlowTip": "词典文件较大时可能需要的时间比较长",
"importAdaptionDict": "导入已经适配好的词典",
"adaptionDictTip": "已经适配好的词典可用性较好。",
"howToDownload": "如何下载?",
"selectDir": "选择文件夹",
"importMdictFile": "导入原词典文件",
"mdictFileTip": "直接导入 .mdx .mdd 格式的文件 (.mdx 文件是必须的,.mdd 文件是可选的且可以有多个),不过样式和可用性可能存在问题。"
}

View File

@@ -37,6 +37,7 @@
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 10% 3.9%;
--active-word: 201 94% 86%;
--radius: 0.5rem;
}
@@ -69,6 +70,7 @@
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--active-word: 201 93% 30%;
}
}

View File

@@ -6,8 +6,6 @@ import log from "@main/logger";
import { DICTS } from "@/constants/dicts";
import sqlite3, { Database } from "sqlite3";
import settings from "./settings";
import downloader from "./downloader";
import decompresser from "./decompresser";
import { hashFile } from "@/main/utils";
const logger = log.scope("dict");
@@ -25,54 +23,28 @@ export class DictHandler {
return _path;
}
async isDictFileValid(dict: Dict) {
const filePath = path.join(this.dictsPath, dict.fileName);
async import(dir: string) {
const files = await fs.readdir(dir);
if (!fs.existsSync(filePath)) return false;
const hash = await hashFile(filePath, { algo: "md5" });
return hash === dict.hash;
}
async download(dict: Dict) {
const filePath = path.join(this.dictsPath, dict.fileName);
const dictPath = path.join(this.dictsPath, dict.name);
if (fs.existsSync(dictPath)) {
throw new Error("Dictionary already exists");
const sqlFileName = files.find((file) => file.match(/\.sqlite$/));
if (!sqlFileName) {
throw new Error("SQLite file not found");
}
const isDictFileValid = await this.isDictFileValid(dict);
if (isDictFileValid) {
this.decompress(dict);
} else {
if (fs.existsSync(filePath)) {
await fs.remove(filePath);
}
downloader.download(dict.downloadUrl, {
savePath: this.dictsPath,
});
}
}
async decompress(dict: Dict) {
const filePath = path.join(this.dictsPath, dict.fileName);
const dictPath = path.join(this.dictsPath, dict.name);
const isDictFileValid = await this.isDictFileValid(dict);
if (isDictFileValid) {
await decompresser.depress({
filePath,
hash: dict.hash,
destPath: dictPath,
id: `dict-${dict.fileName}`,
});
const sqlFilePath = path.join(dir, sqlFileName);
const hash = await hashFile(sqlFilePath, { algo: "md5" });
const dict = DICTS.find((dict) => dict.sqlFileHash === hash);
if (!dict) {
throw new Error("SQLite file not match with any perset dictionary");
}
downloader.remove(dict.fileName);
if (this.isInstalled(dict)) {
throw new Error("Current dict is already installed");
}
await fs.copy(dir, path.join(this.dictsPath, dict.name), {
recursive: true,
});
}
async remove(dict: Dict) {
@@ -84,6 +56,7 @@ export class DictHandler {
this.db = new sqlite.Database(
path.join(this.dictsPath, dict.name, `${dict.name}.sqlite`)
);
this.currentDict = dict.name;
}
@@ -104,41 +77,17 @@ export class DictHandler {
});
}
isInstalled(dict: Dict) {
const files = fs.readdirSync(this.dictsPath);
return files.find((file) => file === dict.name);
}
async getDicts() {
const dicts = DICTS.map((dict: Dict) => {
let state: DictState = "uninstall";
let downloadState;
let decompressProgress;
const files = fs.readdirSync(this.dictsPath);
const isInstalled = files.find((file) => file === dict.name);
const decompressTask = decompresser.tasks.find(
(task) => task.id === `dict-${dict.fileName}`
);
const downloadTask = downloader.tasks.find(
(task) => task.getFilename() === dict.fileName
);
if (decompressTask) {
state = "decompressing";
decompressProgress = decompressTask.progress;
} else if (isInstalled) {
state = "installed";
} else if (downloadTask) {
state = "downloading";
downloadState = {
name: downloadTask.getFilename(),
state: downloadTask.getState(),
isPaused: downloadTask.isPaused(),
canResume: downloadTask.canResume(),
total: downloadTask.getTotalBytes(),
received: downloadTask.getReceivedBytes(),
};
}
return { ...dict, state, downloadState, decompressProgress };
return {
...dict,
state: this.isInstalled(dict) ? "installed" : "uninstall",
};
});
return dicts;
@@ -161,12 +110,8 @@ export class DictHandler {
}
registerIpcHandlers() {
ipcMain.handle("dict-download", async (_event, dict: Dict) =>
this.download(dict)
);
ipcMain.handle("dict-decompress", async (_event, dict: Dict) =>
this.decompress(dict)
ipcMain.handle("dict-import", async (_event, dir: string) =>
this.import(dir)
);
ipcMain.handle("dict-remove", async (_event, dict: Dict) =>

View File

@@ -281,13 +281,12 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
},
dict: {
getDicts: () => ipcRenderer.invoke("dict-list"),
download: (dict: Dict) => ipcRenderer.invoke("dict-download", dict),
decompress: (dict: Dict) => ipcRenderer.invoke("dict-decompress", dict),
remove: (dict: Dict) => ipcRenderer.invoke("dict-remove", dict),
getResource: (key: string, dict: Dict) =>
ipcRenderer.invoke("dict-read-file", key, dict),
lookup: (word: string, dict: Dict) =>
ipcRenderer.invoke("dict-lookup", word, dict),
import: (path: string) => ipcRenderer.invoke("dict-import", path),
},
audios: {
findAll: (params: {

View File

@@ -33,4 +33,10 @@ declare global {
interface Window {
__ENJOY_APP__: EnjoyAppType;
}
namespace JSX {
interface IntrinsicElements {
vocabulary: any;
}
}
}

View File

@@ -4,6 +4,7 @@ import { Button, ScrollArea, Separator } from "@renderer/components/ui";
import Mark from "mark.js";
import { useHotkeys } from "react-hotkeys-hook";
import { HotKeysSettingsProviderContext } from "@renderer/context";
import { Sentence } from "@renderer/components";
export const MeaningMemorizingCard = (props: { meaning: MeaningType }) => {
const {
@@ -73,7 +74,7 @@ const FrontSide = (props: {
<div ref={ref} className="">
{lookups.map((lookup) => (
<p key={lookup.id} className="mb-8">
{lookup.context}
<Sentence sentence={lookup.context} />
</p>
))}
</div>
@@ -159,8 +160,10 @@ const BackSide = (props: { meaning: MeaningType; onFlip: () => void }) => {
<div ref={ref} className="">
{lookups.map((lookup) => (
<div key={lookup.id} className="mb-8">
<div className="mb-2">{lookup.context}</div>
<div className="text-base">{lookup.contextTranslation}</div>
<Sentence sentence={lookup.context} />
<div className="text-base mt-2">
{lookup.contextTranslation}
</div>
</div>
))}
</div>

View File

@@ -1,4 +1,4 @@
import { useEffect, useState, useContext } from "react";
import { useEffect, useState, useContext, useRef } from "react";
import {
AppSettingsProviderContext,
MediaPlayerProviderContext,
@@ -51,6 +51,7 @@ export const MediaCaption = () => {
const [copied, setCopied] = useState<boolean>(false);
const [caption, setCaption] = useState<TimelineEntry | null>(null);
const [tab, setTab] = useState<string>("translation");
const toggleMultiSelect = (event: KeyboardEvent) => {
setMultiSelecting(event.shiftKey && event.type === "keydown");
@@ -362,12 +363,15 @@ export const MediaCaption = () => {
<div className="h-full flex justify-between space-x-4">
<div className="flex-1 font-serif h-full border shadow-lg rounded-lg">
<MediaCaptionTabs
tab={tab}
setTab={setTab}
caption={caption}
currentSegmentIndex={currentSegmentIndex}
selectedIndices={selectedIndices}
setSelectedIndices={setSelectedIndices}
>
<Caption
tab={tab}
caption={caption}
language={transcription.language}
selectedIndices={selectedIndices}
@@ -481,6 +485,7 @@ export const MediaCaption = () => {
export const Caption = (props: {
caption: TimelineEntry;
tab: string;
language?: string;
selectedIndices?: number[];
currentSegmentIndex: number;
@@ -544,7 +549,7 @@ export const Caption = (props: {
}`}
onClick={() => onClick && onClick(index)}
>
<Vocabulary word={word} context={caption.text} />
{word}
</div>
{displayIpa && (

View File

@@ -13,8 +13,10 @@ import { TabContentNote } from "./tab-content-note";
export const MediaCaptionTabs = (props: {
caption: TimelineEntry;
tab: string;
currentSegmentIndex: number;
selectedIndices: number[];
setTab: (v: string) => void;
setSelectedIndices: (indices: number[]) => void;
children?: React.ReactNode;
}) => {
@@ -24,10 +26,10 @@ export const MediaCaptionTabs = (props: {
selectedIndices,
setSelectedIndices,
children,
tab,
setTab,
} = props;
const [tab, setTab] = useState<string>("translation");
if (!caption) return null;
return (

View File

@@ -29,6 +29,7 @@ import {
MediaTranscriptionPrint,
TranscriptionEditButton,
} from "@renderer/components";
import { Sentence } from "@renderer/components";
export const MediaTranscription = (props: { display?: boolean }) => {
const { display } = props;
@@ -202,7 +203,8 @@ export const MediaTranscription = (props: { display?: boolean }) => {
</span>
</div>
</div>
<p className="">{sentence.text}</p>
<Sentence sentence={sentence.text} />
</div>
)
)}

View File

@@ -1,4 +1,21 @@
import Markdown from "react-markdown";
import { visitParents } from "unist-util-visit-parents";
import { Sentence } from "@renderer/components";
function rehypeWrapText() {
return function wrapTextTransform(tree: any) {
visitParents(tree, "text", (node, ancestors) => {
const parent = ancestors.at(-1);
if (parent.tagName !== "vocabulary") {
node.type = "element";
node.tagName = "vocabulary";
node.properties = { text: node.value };
node.children = [{ type: "text", value: node.value }];
}
});
};
}
export const MarkdownWrapper = ({
children,
@@ -11,6 +28,7 @@ export const MarkdownWrapper = ({
return (
<Markdown
className={className}
rehypePlugins={[rehypeWrapText]}
components={{
a({ node, children, ...props }) {
try {
@@ -21,6 +39,9 @@ export const MarkdownWrapper = ({
return <a {...props}>{children}</a>;
},
vocabulary({ node, children, ...props }) {
return <Sentence sentence={props.text} />;
},
}}
{...props}
>

View File

@@ -3,6 +3,7 @@ import { useContext, useState } from "react";
import { WavesurferPlayer } from "@/renderer/components/misc";
import { AppSettingsProviderContext } from "@/renderer/context";
import { convertWordIpaToNormal } from "@/utils";
import { Vocabulary } from "@renderer/components";
export const NoteSemgent = (props: {
segment: SegmentType;
@@ -51,7 +52,7 @@ export const NoteSemgent = (props: {
}
`}
>
{word}
<Vocabulary word={word} context={caption.text} />
</div>
<div

View File

@@ -3,6 +3,7 @@ import { AppSettingsProviderContext } from "@renderer/context";
import {
PronunciationAssessmentScoreDetail,
WavesurferPlayer,
Sentence,
} from "@renderer/components";
export const PostRecording = (props: {
@@ -63,7 +64,7 @@ export const PostRecording = (props: {
{recording.referenceText && (
<div className="my-2 bg-muted px-4 py-2 rounded">
<div className="text-muted-foreground text-center font-serif select-text">
{recording.referenceText}
<Sentence sentence={recording.referenceText} />
</div>
</div>
)}

View File

@@ -1,4 +1,8 @@
import { useState } from "react";
import { useState, useContext } from "react";
import {
AppSettingsProviderContext,
DictProviderContext,
} from "@/renderer/context";
import {
Button,
Dialog,
@@ -6,20 +10,54 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
ScrollArea,
toast,
} from "@/renderer/components/ui";
import { UninstallDictList } from ".";
import { t } from "i18next";
import { LoaderIcon } from "lucide-react";
export const DictImportButton = () => {
const { reload } = useContext(DictProviderContext);
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [tipVisible, setTipVisible] = useState(false);
const handleOpen = (value: boolean) => {
setOpen(value);
};
const handleDownload = () => {
setOpen(false);
const handleAdaptationDictImport = async () => {
const pathes = await EnjoyApp.dialog.showOpenDialog({
title: t("selectAdaptionDictTitle"),
properties: ["openDirectory"],
});
if (!pathes[0]) return;
setLoading(true);
setTimeout(() => {
if (loading) {
setTipVisible(true);
}
}, 10000);
try {
await EnjoyApp.dict.import(pathes[0]);
setOpen(false);
} catch (err) {
toast.error(err.message);
}
setLoading(false);
setTipVisible(false);
reload();
};
const handleOriginDictImport = async () => {
const pathes = await EnjoyApp.dialog.showOpenDialog({
title: t("selectMdictFileOrDirTitle"),
properties: ["multiSelections", "openFile", "openDirectory"],
});
};
return (
@@ -32,9 +70,56 @@ export const DictImportButton = () => {
<DialogTitle>{t("importDict")}</DialogTitle>
</DialogHeader>
<ScrollArea className="h-72 pr-3">
<UninstallDictList onDownload={handleDownload} />
</ScrollArea>
{loading ? (
<div>
<div className="px-4 py-4 flex justify-center items-center">
<LoaderIcon className="text-muted-foreground animate-spin" />
</div>
{tipVisible && (
<div className="text-xs text-center text-muted-foreground mb-8">
{t("dictImportSlowTip")}
</div>
)}
</div>
) : (
<div>
<div className="flex items-center justify-between py-4">
<div className="mr-4">
<div className="mb-2">{t("importAdaptionDict")}</div>
<div className="text-xs text-muted-foreground mb-2">
{t("adaptionDictTip")}
<a
className="text-blue-600 cursor-pointer"
onClick={() => {
EnjoyApp.shell.openExternal(
"https://1000h.org/enjoy-app/dicts.html"
);
}}
>
{t("howToDownload")}
</a>
</div>
</div>
<Button size="sm" onClick={handleAdaptationDictImport}>
{t("selectDir")}
</Button>
</div>
{/* <div className="flex items-center justify-between py-4">
<div className="mr-4">
<div className="mb-2">{t("importMdictFile")}</div>
<div className="text-xs text-muted-foreground mb-2">
{t("mdictFileTip")}
</div>
</div>
<Button size="sm" onClick={handleOriginDictImport}>
{t("selectDir")}
</Button>
</div> */}
</div>
)}
</DialogContent>
</Dialog>
);

View File

@@ -1,6 +1,6 @@
import { t } from "i18next";
import { DictImportButton } from "./dict-import-button";
import { DownloadingDictList, InstalledDictList } from ".";
import { InstalledDictList } from ".";
export const DictSettings = () => {
return (
@@ -12,7 +12,6 @@ export const DictSettings = () => {
</div>
<div className="my-4">
<DownloadingDictList />
<InstalledDictList />
</div>
</div>

View File

@@ -1,189 +0,0 @@
import {
DictProviderContext,
AppSettingsProviderContext,
} from "@/renderer/context";
import { useContext, useEffect, useState } from "react";
import { Button, toast } from "@renderer/components/ui";
import { t } from "i18next";
import { LoaderIcon } from "lucide-react";
export const DownloadingDictList = function () {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { downloadingDicts, reload } = useContext(DictProviderContext);
useEffect(() => {
listenToDownloadState();
listenDecompressState();
return () => {
EnjoyApp.download.removeAllListeners();
EnjoyApp.decompress.removeAllListeners();
};
}, []);
const listenToDownloadState = () => {
EnjoyApp.download.onState((_event, state) => {
reload();
});
};
const listenDecompressState = () => {
EnjoyApp.decompress.onUpdate((_event, tasks) => {
reload();
});
};
return (
<>
{downloadingDicts.map((item) => (
<DownloadingDictItem key={item.name} dict={item} />
))}
</>
);
};
const DownloadingDictItem = function ({ dict }: { dict: Dict }) {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { reload } = useContext(DictProviderContext);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (dict.downloadState?.state === "completed") {
EnjoyApp.dict.decompress(dict);
}
}, [dict]);
async function handlePause() {
setLoading(true);
try {
await EnjoyApp.download.pause(dict.downloadState.name);
reload();
} catch (err) {
toast.error(err.message);
}
setLoading(false);
}
async function handleResume() {
setLoading(true);
try {
await EnjoyApp.download.resume(dict.downloadState.name);
reload();
} catch (err) {
toast.error(err.message);
}
setLoading(false);
}
async function handleRemove() {
setLoading(true);
try {
await EnjoyApp.download.remove(dict.downloadState.name);
toast.success(t("dictRemoved"));
reload();
} catch (err) {
toast.error(err.message);
}
setLoading(false);
}
function displaySize(bytes: number) {
return Number((bytes / 1024 / 1024).toFixed(0)).toLocaleString() + "MB";
}
function renderDownloadState() {
const text =
dict.downloadState.state === "cancelled"
? t("cancelled")
: dict.downloadState.state === "completed"
? t("completedAndChecking")
: dict.downloadState.state === "interrupted"
? t("interrupted")
: dict.downloadState.isPaused
? t("paused")
: t("downloadingDict");
return (
<div className="text-xs text-muted-foreground">
<span className="mr-2">{text}</span>
<span className="">{displaySize(dict.downloadState.received)}</span>
<span className="mx-1">/</span>
<span className="">{displaySize(dict.downloadState.total)}</span>
</div>
);
}
function renderDecompressState() {
return (
<div className="text-xs text-muted-foreground">
<span>{t("decompressing")}</span>
<span className="ml-2">{dict.decompressProgress ?? "0"}%</span>
</div>
);
}
function renderActions() {
if (loading)
return (
<div>
<LoaderIcon className="text-muted-foreground animate-spin" />
</div>
);
if (
dict.downloadState?.state === "progressing" &&
!dict.downloadState?.isPaused
) {
return (
<Button variant="secondary" size="sm" onClick={handlePause}>
{t("pause")}
</Button>
);
}
if (
dict.downloadState?.state === "cancelled" ||
dict.downloadState?.state === "interrupted" ||
(dict.downloadState?.state === "progressing" &&
dict.downloadState?.isPaused)
) {
return (
<>
{dict.downloadState.canResume && (
<Button
variant="secondary"
size="sm"
className="mr-2"
onClick={handleResume}
>
{t("resume")}
</Button>
)}
<Button variant="secondary" size="sm" onClick={handleRemove}>
{t("delete")}
</Button>
</>
);
}
}
return (
<div key={dict.name} className="flex items-center py-2">
<div className="flex-grow">
<div>{dict.title}</div>
<div className="mt-1">
{dict.state === "decompressing" && renderDecompressState()}
{dict.downloadState && renderDownloadState()}
</div>
</div>
{renderActions()}
</div>
);
};

View File

@@ -1,5 +1,3 @@
export * from "./dict-settings";
export * from "./dict-import-button";
export * from "./downloading-dict-list";
export * from "./installed-dict-list";
export * from "./uninstall-dict-list";

View File

@@ -19,14 +19,13 @@ import {
import { t } from "i18next";
export const InstalledDictList = function () {
const { installedDicts, downloadingDicts, reload } =
useContext(DictProviderContext);
const { installedDicts, reload } = useContext(DictProviderContext);
useEffect(() => {
reload();
}, []);
if (installedDicts.length === 0 && downloadingDicts.length === 0) {
if (installedDicts.length === 0) {
return (
<div className="text-sm text-muted-foreground">{t("dictEmpty")}</div>
);
@@ -119,6 +118,7 @@ const InstalledDictItem = function ({ dict }: { dict: Dict }) {
<AlertDialogAction asChild>
<Button
size="sm"
variant="secondary"
className="text-destructive mr-2"
onClick={handleRemove}
>

View File

@@ -1,76 +0,0 @@
import {
DictProviderContext,
AppSettingsProviderContext,
} from "@/renderer/context";
import { useContext, useState } from "react";
import { Button, toast } from "@renderer/components/ui";
import { LoaderIcon } from "lucide-react";
import { t } from "i18next";
export const UninstallDictList = function ({
onDownload,
}: {
onDownload: () => void;
}) {
const { uninstallDicts } = useContext(DictProviderContext);
return (
<>
{uninstallDicts.map((item) => (
<UninstallDictItem
key={item.name}
dict={item}
onDownload={onDownload}
/>
))}
</>
);
};
const UninstallDictItem = function ({
dict,
onDownload,
}: {
dict: Dict;
onDownload: () => void;
}) {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { reload, removed } = useContext(DictProviderContext);
const [loading, setLoading] = useState(false);
async function handleDownload() {
setLoading(true);
try {
removed(dict);
await EnjoyApp.dict.download(dict);
reload();
onDownload();
} catch (err) {
toast.error(err);
}
setLoading(false);
}
return (
<div key={dict.name} className="flex items-center py-2">
<div className="flex-grow">
<div>{dict.title}</div>
<div className="text-sm mt-1 text-muted-foreground">
<span className="mr-2">{dict.lang}</span>
<span>{dict.size}</span>
</div>
</div>
<Button
variant="secondary"
size="sm"
disabled={loading}
onClick={handleDownload}
>
{loading && <LoaderIcon className="animate-spin w-4 mr-2" />}
{t("download")}
</Button>
</div>
);
};

View File

@@ -5,6 +5,7 @@ import { ChevronLeftIcon, ExternalLinkIcon } from "lucide-react";
import { Button } from "@renderer/components/ui";
import uniq from "lodash/uniq";
import Mark from "mark.js";
import { Vocabulary } from "@/renderer/components";
export const StoryViewer = (props: {
story: Partial<StoryType> & Partial<CreateStoryParamsType>;
@@ -112,11 +113,14 @@ export const StoryViewer = (props: {
key={`paragraph-${i}-sentence-${j}`}
>
{sentence.terms.map((term) => (
<span key={term.id} className="">
<>
{term.pre}
{term.text}
<Vocabulary
word={term.text}
context={sentence.text}
/>
{term.post}
</span>
</>
))}
</span>
);

View File

@@ -1,4 +1,5 @@
export * from "./lookup";
export * from "./translate-widget";
export * from "./vocabulary";
export * from "./sentence";
export * from "./lookup/dict-lookup-result";

View File

@@ -50,6 +50,10 @@ export function DictLookupResult({
revoke();
setLooking(true);
if (autoHeight) {
setHeight(0);
}
const _word = word.trim().indexOf(" ") > -1 ? word : word.toLowerCase();
EnjoyApp.dict
@@ -107,7 +111,7 @@ export function DictLookupResult({
<Frame
initialContent={initialContent}
mountTarget="body"
style={{ height: autoHeight ? `${height}px` : "100%" }}
style={{ minHeight: autoHeight ? `${height}px` : "100%" }}
>
<DictLookupResultInner
text={definition}

View File

@@ -0,0 +1,18 @@
import { Vocabulary } from "@renderer/components";
export const Sentence = ({ sentence }: { sentence: string }) => {
let words = sentence.split(" ");
return (
<span className="break-all align-middle">
{words.map((word, index) => {
return (
<>
<Vocabulary key={index} word={word} context={sentence} />
{index === words.length - 1 ? " " : " "}
</>
);
})}
</span>
);
};

View File

@@ -13,6 +13,19 @@ export const Vocabulary = ({
let [timer, setTimer] = useState<ReturnType<typeof setTimeout>>();
const { vocabularyConfig, EnjoyApp } = useContext(AppSettingsProviderContext);
const handleLookup = (e: any) => {
if (!context) {
context = e.target?.parentElement
.closest(".sentence, h2, p, div")
?.textContent?.trim();
}
const { x, bottom: y } = e.target.getBoundingClientRect();
const _word = word.replace(/[^\w\s]|_/g, "");
EnjoyApp.lookup(_word, context, { x, y });
};
const handleMouseEnter = (e: any) => {
let _timer = setTimeout(() => {
if (!context) {
@@ -25,7 +38,7 @@ export const Vocabulary = ({
const _word = word.replace(/[^\w\s]|_/g, "");
EnjoyApp.lookup(_word, context, { x, y });
}, 1000);
}, 800);
setTimer(_timer);
};
@@ -36,9 +49,8 @@ export const Vocabulary = ({
return vocabularyConfig.lookupOnMouseOver ? (
<span
className="cursor-pointer"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className="cursor-pointer hover:bg-active-word"
onClick={handleLookup}
>
{word || children}
</span>

View File

@@ -5,8 +5,6 @@ import { t } from "i18next";
type DictProviderState = {
settings: DictSettingType;
dicts: Dict[];
downloadingDicts: Dict[];
uninstallDicts: Dict[];
installedDicts: Dict[];
dictSelectItems: { text: string; value: string }[];
reload?: () => void;
@@ -30,8 +28,6 @@ const CamDict = {
const initialState: DictProviderState = {
dicts: [],
downloadingDicts: [],
uninstallDicts: [],
installedDicts: [],
dictSelectItems: [AIDict],
settings: {
@@ -78,16 +74,6 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
];
}, [availableDicts, learningLanguage]);
const downloadingDicts = useMemo(() => {
return dicts.filter(
(dict) => dict.state === "downloading" || dict.state === "decompressing"
);
}, [dicts]);
const uninstallDicts = useMemo(() => {
return dicts.filter((dict) => dict.state === "uninstall");
}, [dicts]);
const installedDicts = useMemo(() => {
return dicts.filter((dict) => dict.state === "installed");
}, [dicts]);
@@ -168,8 +154,6 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
removed,
reload: fetchDicts,
dictSelectItems,
downloadingDicts,
uninstallDicts,
installedDicts,
currentDict,
currentDictValue,

View File

@@ -161,11 +161,10 @@ type EnjoyAppType = {
};
dict: {
getDicts: () => Promise<Dict[]>;
download: (dict: Dict) => Promise<void>;
decompress: (dict: Dict) => Promise<void>;
remove: (dict: Dict) => Promise<void>;
getResource: (key: string, dict: Dict) => Promise<string | null>;
lookup: (word: string, dict: Dict) => Promise<string | null>;
import: (path: string) => Promise<void>;
};
audios: {
findAll: (params: any) => Promise<AudioType[]>;

View File

@@ -20,6 +20,7 @@ module.exports = {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
"active-word": "hsl(var(--active-word))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {