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

@@ -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);
}
})
);
}