feat: 🎸 update dicts (#1026)

This commit is contained in:
divisey
2024-08-30 18:08:02 +08:00
committed by GitHub
parent 271dfc603e
commit b6af2d18e8
12 changed files with 187 additions and 60 deletions

View File

@@ -1,4 +1,27 @@
export const DICTS = [
{
name: "ccalecd",
fileName: "ccalecd.zip",
title: "Collins COBUILD Advanced British EN-CN Dictionary",
pronunciation: true,
lang: "En-CN",
downloadUrl: "https://dl.enjoy.bot/dicts/ccalecd.zip",
size: "13.879MB",
hash: "96940f85e52df4586b287e1859723a39",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
{
name: "ccabeld",
fileName: "ccabeld.zip",
title: "Collins COBUILD Advanced British English Learners Dictionary",
pronunciation: true,
lang: "En-En",
downloadUrl: "https://dl.enjoy.bot/dicts/ccabeld.zip",
size: "485.6MB",
hash: "5b53498536f3ce3ed173752b7888ca51",
addition: '<link href="theme.css" rel="stylesheet" type="text/css" />',
},
{
name: "ldoce5",
fileName: "ldoce5.zip",
@@ -13,7 +36,7 @@ export const DICTS = [
{
name: "oxford_en_mac",
fileName: "oxford_en_mac.zip",
title: "Oxford Dictionary of English (Mac)",
title: "Oxford Dictionary of English",
pronunciation: false,
lang: "En-En",
downloadUrl: "https://dl.enjoy.bot/dicts/oxford_en_mac.zip",
@@ -24,7 +47,7 @@ export const DICTS = [
{
name: "koen_mac",
fileName: "koen_mac.zip",
title: "Korean English Dictionary (Mac)",
title: "Korean English Dictionary",
pronunciation: false,
lang: "Ko-En",
downloadUrl: "https://dl.enjoy.bot/dicts/koen_mac.zip",
@@ -46,7 +69,7 @@ export const DICTS = [
{
name: "deen_mac",
fileName: "deen_mac.zip",
title: "German English Dictionary (Mac)",
title: "German English Dictionary",
pronunciation: false,
lang: "Ge-En",
downloadUrl: "https://dl.enjoy.bot/dicts/deen_mac.zip",
@@ -57,7 +80,7 @@ export const DICTS = [
{
name: "ruen_mac",
fileName: "ruen_mac.zip",
title: "Russian English Dictionary (Mac)",
title: "Russian English Dictionary",
pronunciation: false,
lang: "Ru-En",
downloadUrl: "https://dl.enjoy.bot/dicts/ruen_mac.zip",

View File

@@ -751,7 +751,8 @@
"dictFileExist": "{{name}} has already been imported",
"dictFileAddSuccess": "Add {{name}} successfully",
"dictFileRemoveSuccess": "Remove {{name}} successfully",
"dictFileSetDefaultSuccess": "Set default successfully",
"dictFileSetDefaultSuccess": "Successfully set as default dictionary",
"dictFileRemoveDefaultSuccess": "Operation successful",
"dictionaries": "Dictionaries",
"import": "Import",
"default": "Default",
@@ -769,5 +770,6 @@
"removing": "Removing",
"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"
"downloadingDict": "Downloading",
"removeDefault": "No longer as Default"
}

View File

@@ -751,7 +751,8 @@
"dictFileExist": "{{name}} 已经存在了",
"dictFileAddSuccess": "已添加 {{name}}",
"dictFileRemoveSuccess": "已删除 {{name}}",
"dictFileSetDefaultSuccess": "设置成功",
"dictFileSetDefaultSuccess": "成功设置为默认词典",
"dictFileRemoveDefaultSuccess": "操作成功",
"dictionaries": "词典",
"import": "导入",
"default": "默认",
@@ -769,5 +770,6 @@
"removing": "正在删除",
"removeDictTitle": "你确定要删除词典吗?",
"removeDictDescription": "此操作将会从本地删除词典文件,下次安装需要重新下载",
"downloadingDict": "正在下载"
"downloadingDict": "正在下载",
"removeDefault": "不再设置为默认"
}

View File

@@ -130,7 +130,9 @@ class Downloader {
resume(filename: string) {
this.tasks
.filter(
(t) => t.getFilename() === filename && t.getState() === "progressing"
(t) =>
t.getFilename() === filename &&
["progressing", "interrupted"].includes(t.getState())
)
.forEach((t) => {
t.resume();

View File

@@ -2,6 +2,7 @@ import { useContext } from "react";
import {
AppSettingsProviderContext,
MediaPlayerProviderContext,
DictProviderContext,
} from "@renderer/context";
import { TabsContent, Separator } from "@renderer/components/ui";
import { t } from "i18next";
@@ -9,6 +10,7 @@ import { TimelineEntry } from "echogarden/dist/utilities/Timeline.d.js";
import { convertWordIpaToNormal } from "@/utils";
import {
CamdictLookupResult,
DictLookupResult,
AiLookupResult,
TranslateResult,
} from "@renderer/components";
@@ -40,6 +42,7 @@ const SelectedWords = (props: {
}) => {
const { selectedIndices, caption } = props;
const { currentDictValue } = useContext(DictProviderContext);
const { transcription } = useContext(MediaPlayerProviderContext);
const { learningLanguage, ipaMappings } = useContext(
AppSettingsProviderContext
@@ -100,11 +103,16 @@ const SelectedWords = (props: {
})}
</div>
{learningLanguage.startsWith("en") && (
{currentDictValue === "cambridge" ? (
<>
<Separator className="my-2" />
<CamdictLookupResult word={word} />
</>
) : (
<>
<Separator className="my-2" />
<DictLookupResult word={word} autoHeight={true} />
</>
)}
<Separator className="my-2" />

View File

@@ -5,7 +5,7 @@ import {
import { useContext, useEffect, useState } from "react";
import { Button, toast } from "@renderer/components/ui";
import { t } from "i18next";
import { LoaderSpin } from "@renderer/components";
import { LoaderIcon } from "lucide-react";
export const DownloadingDictList = function () {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
@@ -129,7 +129,12 @@ const DownloadingDictItem = function ({ dict }: { dict: Dict }) {
}
function renderActions() {
if (loading) return <LoaderSpin />;
if (loading)
return (
<div>
<LoaderIcon className="text-muted-foreground animate-spin" />
</div>
);
if (
dict.downloadState?.state === "progressing" &&

View File

@@ -62,6 +62,15 @@ const InstalledDictItem = function ({ dict }: { dict: Dict }) {
}
}
async function handleRemoveDefault() {
try {
await setDefault(null);
toast.success(t("dictFileRemoveDefaultSuccess"));
} catch (err) {
toast.error(err.message);
}
}
async function handleRemove() {
setRemoving(true);
@@ -120,9 +129,15 @@ const InstalledDictItem = function ({ dict }: { dict: Dict }) {
</AlertDialogContent>
</AlertDialog>
<Button size="sm" variant="secondary" onClick={handleSetDefault}>
{t("setDefault")}
</Button>
{settings.default === dict.name ? (
<Button size="sm" variant="secondary" onClick={handleRemoveDefault}>
{t("removeDefault")}
</Button>
) : (
<Button size="sm" variant="secondary" onClick={handleSetDefault}>
{t("setDefault")}
</Button>
)}
</div>
);
}

View File

@@ -23,10 +23,12 @@ const MIME: Record<string, string> = {
export function DictLookupResult({
word,
autoHeight,
onJump,
}: {
word: string;
onJump: (v: string) => void;
autoHeight?: boolean;
onJump?: (v: string) => void;
}) {
const { colorScheme } = useContext(ThemeProviderContext);
const initialContent = `<!DOCTYPE html><html class=${colorScheme}><head></head><body></body></html>`;
@@ -36,6 +38,7 @@ export function DictLookupResult({
const [looking, setLooking] = useState(false);
const [notFound, setNotFound] = useState(false);
const [error, setError] = useState(false);
const [height, setHeight] = useState<number>();
useEffect(() => {
if (currentDict && word) {
@@ -72,6 +75,10 @@ export function DictLookupResult({
setError(false);
}
function handleResize(h: number) {
setHeight(h);
}
if (looking) {
return (
<div className="text-center">
@@ -100,9 +107,14 @@ export function DictLookupResult({
<Frame
initialContent={initialContent}
mountTarget="body"
className="h-full"
style={{ height: autoHeight ? `${height}px` : "100%" }}
>
<DictLookupResultInner text={definition} onJump={onJump} />
<DictLookupResultInner
text={definition}
onJump={onJump}
autoHeight={autoHeight}
onResize={handleResize}
/>
</Frame>
);
}
@@ -110,17 +122,38 @@ export function DictLookupResult({
export const DictLookupResultInner = ({
text,
onJump,
onResize,
autoHeight,
}: {
text: string;
onJump: (v: string) => void;
autoHeight: boolean;
onJump?: (v: string) => void;
onResize?: (v: number) => void;
}) => {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { currentDict } = useContext(DictProviderContext);
const { document: innerDocument } = useFrame();
const [html, setHtml] = useState("");
const [hash, setHash] = useState("");
useEffect(() => {
normalize();
if (autoHeight) {
const resizeObserver = new ResizeObserver(() => {
const html = innerDocument.getElementsByTagName("html")[0];
onResize(html.scrollHeight);
});
resizeObserver.observe(innerDocument.getElementById("inner-dict"));
}
}, []);
useEffect(() => {
normalize().then(() => {
if (hash) {
handleScroll();
setHash("");
}
});
return () => {
normalizer.revoke();
@@ -166,7 +199,18 @@ export const DictLookupResultInner = ({
function handleJump(el: Element) {
const word = el.getAttribute("data-word");
onJump(word);
const hash = el.getAttribute("data-hash");
onJump?.(word);
setHash(hash);
}
function handleScroll() {
setTimeout(() => {
const el = innerDocument.querySelector(`a[name='${hash}']`);
if (el) {
el.scrollIntoView();
}
}, 200);
}
const registerAudioHandler = () => {
@@ -199,5 +243,5 @@ export const DictLookupResultInner = ({
};
};
return <div dangerouslySetInnerHTML={{ __html: html }}></div>;
return <div id="inner-dict" dangerouslySetInnerHTML={{ __html: html }}></div>;
};

View File

@@ -14,6 +14,7 @@ import {
DictLookupResult,
DictSelect,
AiLookupResult,
CamdictLookupResult,
} from "@renderer/components";
import { ChevronLeft, ChevronFirst } from "lucide-react";
@@ -41,12 +42,12 @@ export const LookupWidget = () => {
) => {
let word = _word;
let context = _context;
let sourceType;
let sourceId;
if (word) {
if (word.indexOf(" ") > -1) return;
setSelected({ word, context, position });
} else {
const selection = document.getSelection();
const selection = document.getSelection();
if (!word) {
if (!selection?.anchorNode?.parentElement) return;
word = selection
@@ -54,24 +55,23 @@ export const LookupWidget = () => {
.trim()
.replace(/[.,/#!$%^&*;:{}=\-_`~()]+$/, "");
if (!word) return;
// can only lookup single word
if (word.indexOf(" ") > -1) return;
if (!word || word.indexOf(" ") > -1) return;
}
if (!context) {
context = selection?.anchorNode.parentElement
.closest(".sentence, h2, p, div")
?.textContent?.trim();
const sourceType = selection?.anchorNode.parentElement
sourceType = selection?.anchorNode.parentElement
.closest("[data-source-type]")
?.getAttribute("data-source-type");
const sourceId = selection?.anchorNode.parentElement
sourceId = selection?.anchorNode.parentElement
.closest("[data-source-id]")
?.getAttribute("data-source-id");
setSelected({ word, context, position, sourceType, sourceId });
}
setSelected({ word, context, position, sourceType, sourceId });
handleLookup(word);
setOpen(true);
};
@@ -102,7 +102,7 @@ export const LookupWidget = () => {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverAnchor
className="absolute w-0 h-0"
className="fixed w-0 h-0"
style={{
top: selected?.position?.y,
left: selected?.position?.x,
@@ -152,6 +152,8 @@ export const LookupWidget = () => {
sourceId={selected?.sourceId}
sourceType={selected?.sourceType}
/>
) : currentDictValue === "cambridge" ? (
<CamdictLookupResult word={selected?.word} />
) : (
<DictLookupResult word={current} onJump={handleLookup} />
)}

View File

@@ -1,4 +1,4 @@
import React, { useContext } from "react";
import React, { useContext, useState } from "react";
import { AppSettingsProviderContext } from "@renderer/context";
export const Vocabulary = ({
@@ -10,18 +10,28 @@ export const Vocabulary = ({
context?: string;
children?: React.ReactNode;
}) => {
let timeout: ReturnType<typeof setTimeout>;
let [timer, setTimer] = useState<ReturnType<typeof setTimeout>>();
const { vocabularyConfig, EnjoyApp } = useContext(AppSettingsProviderContext);
const handleMouseEnter = (e: React.MouseEvent) => {
timeout = setTimeout(() => {
EnjoyApp.lookup(word, context, { x: e.clientX, y: e.clientY });
}, 500);
const handleMouseEnter = (e: any) => {
let _timer = setTimeout(() => {
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 });
}, 1000);
setTimer(_timer);
};
const handleMouseLeave = () => {
clearTimeout(timeout);
clearTimeout(timer);
};
return vocabularyConfig.lookupOnMouseOver ? (
@@ -30,7 +40,7 @@ export const Vocabulary = ({
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<span>{word || children}</span>
{word || children}
</span>
) : (
<span>{word || children}</span>

View File

@@ -23,6 +23,11 @@ const AIDict = {
value: "ai",
};
const CamDict = {
text: t("cambridgeDictionary"),
value: "cambridge",
};
const initialState: DictProviderState = {
dicts: [],
downloadingDicts: [],
@@ -39,7 +44,7 @@ export const DictProviderContext =
createContext<DictProviderState>(initialState);
export const DictProvider = ({ children }: { children: React.ReactNode }) => {
const { EnjoyApp } = useContext(AppSettingsProviderContext);
const { EnjoyApp, learningLanguage } = useContext(AppSettingsProviderContext);
const [dicts, setDicts] = useState<Dict[]>([]);
const [settings, setSettings] = useState<DictSettingType>({
default: "",
@@ -60,14 +65,18 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
);
const dictSelectItems = useMemo(() => {
const presets = learningLanguage.startsWith("en")
? [CamDict, AIDict]
: [AIDict];
return [
AIDict,
...presets,
...availableDicts.map((item) => ({
text: item.title,
value: item.name,
})),
];
}, [availableDicts]);
}, [availableDicts, learningLanguage]);
const downloadingDicts = useMemo(() => {
return dicts.filter(
@@ -84,18 +93,16 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
}, [dicts]);
useEffect(() => {
if (availableDicts.length) {
const _currentDict = availableDicts.find(
(dict) => dict.name === settings.default
);
const defaultDict = availableDicts.find(
(dict) => dict.name === settings.default
);
if (_currentDict) {
handleSetCurrentDict(_currentDict.name);
} else {
setDefault(availableDicts[0]);
}
if (defaultDict) {
handleSetCurrentDict(defaultDict.name);
} else {
setCurrentDictValue(AIDict.value);
setCurrentDictValue(
learningLanguage.startsWith("en") ? CamDict.value : AIDict.value
);
}
}, [availableDicts, settings]);
@@ -123,7 +130,7 @@ export const DictProvider = ({ children }: { children: React.ReactNode }) => {
if (dict) setCurrentDict(dict);
};
const setDefault = async (dict: Dict) => {
const setDefault = async (dict: Dict | null) => {
const _settings = { ...settings, default: dict?.name ?? "" };
EnjoyApp.settings

View File

@@ -104,9 +104,16 @@ export class DictDefinitionNormalizer {
...this.$('a[href^="bword://"]').toArray(),
].map((link) => {
const $link = this.$(link);
const word = $link.attr("_href") || $link.attr("href").substring(8);
$link.attr("data-type", "jump").attr("data-word", word);
const href = $link.attr("_href") || $link.attr("href").substring(8);
const [word, hash] = href.split("#");
if (word) {
$link
.attr("data-type", "jump")
.attr("data-word", word)
.attr("data-hash", hash);
}
})
);
}