diff --git a/src/config/i18n.js b/src/config/i18n.js index 5dc47a6..3854818 100644 --- a/src/config/i18n.js +++ b/src/config/i18n.js @@ -555,7 +555,7 @@ export const I18N = { zh: `全局规则`, en: `Global Rule`, }, - input_box_translation: { + input_translate: { zh: `输入框翻译`, en: `Input Box Translation`, }, @@ -639,4 +639,8 @@ export const I18N = { zh: `原文`, en: `Original Text`, }, + favorite_words: { + zh: `收藏词汇`, + en: `Favorite Words`, + }, }; diff --git a/src/config/index.js b/src/config/index.js index cd8915b..753983d 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -23,6 +23,7 @@ export const STOKEY_MSAUTH = `${APP_NAME}_msauth`; export const STOKEY_BDAUTH = `${APP_NAME}_bdauth`; export const STOKEY_SETTING = `${APP_NAME}_setting`; export const STOKEY_RULES = `${APP_NAME}_rules`; +export const STOKEY_WORDS = `${APP_NAME}_words`; export const STOKEY_SYNC = `${APP_NAME}_sync`; export const STOKEY_FAB = `${APP_NAME}_fab`; export const STOKEY_RULESCACHE_PREFIX = `${APP_NAME}_rulescache_`; @@ -40,6 +41,7 @@ export const CLIENT_USERSCRIPT = "userscript"; export const CLIENT_EXTS = [CLIENT_CHROME, CLIENT_EDGE, CLIENT_FIREFOX]; export const KV_RULES_KEY = "kiss-rules.json"; +export const KV_WORDS_KEY = "kiss-words.json"; export const KV_RULES_SHARE_KEY = "kiss-rules-share.json"; export const KV_SETTING_KEY = "kiss-setting.json"; export const KV_SALT_SYNC = "KISS-Translator-SYNC"; diff --git a/src/hooks/FavWords.js b/src/hooks/FavWords.js new file mode 100644 index 0000000..6c833d5 --- /dev/null +++ b/src/hooks/FavWords.js @@ -0,0 +1,40 @@ +import { KV_WORDS_KEY } from "../config"; +import { useCallback, useEffect, useState } from "react"; +import { trySyncWords } from "../libs/sync"; +import { getWordsWithDefault, setWords } from "../libs/storage"; +import { useSyncMeta } from "./Sync"; + +export function useFavWords() { + const [favWords, setFavWords] = useState({}); + const { updateSyncMeta } = useSyncMeta(); + + const toggleFav = useCallback( + async (word) => { + const favs = { ...favWords }; + if (favs[word]) { + delete favs[word]; + } else { + favs[word] = { createdAt: Date.now() }; + } + await setWords(favs); + await updateSyncMeta(KV_WORDS_KEY); + await trySyncWords(); + setFavWords(favs); + }, + [updateSyncMeta, favWords] + ); + + useEffect(() => { + (async () => { + try { + await trySyncWords(); + const favWords = await getWordsWithDefault(); + setFavWords(favWords); + } catch (err) { + console.log("[query fav]", err); + } + })(); + }, []); + + return { favWords, toggleFav }; +} diff --git a/src/libs/storage.js b/src/libs/storage.js index e1e9c9f..6802ae3 100644 --- a/src/libs/storage.js +++ b/src/libs/storage.js @@ -1,6 +1,7 @@ import { STOKEY_SETTING, STOKEY_RULES, + STOKEY_WORDS, STOKEY_FAB, STOKEY_SYNC, STOKEY_MSAUTH, @@ -97,6 +98,13 @@ export const getRulesWithDefault = async () => (await getRules()) || DEFAULT_RULES; export const setRules = (val) => setObj(STOKEY_RULES, val); +/** + * 词汇列表 + */ +export const getWords = () => getObj(STOKEY_WORDS); +export const getWordsWithDefault = async () => (await getWords()) || {}; +export const setWords = (val) => setObj(STOKEY_WORDS, val); + /** * 订阅规则 */ diff --git a/src/libs/sync.js b/src/libs/sync.js index c3e2e55..7d9743a 100644 --- a/src/libs/sync.js +++ b/src/libs/sync.js @@ -2,6 +2,7 @@ import { APP_LCNAME, KV_SETTING_KEY, KV_RULES_KEY, + KV_WORDS_KEY, KV_RULES_SHARE_KEY, KV_SALT_SHARE, OPT_SYNCTYPE_WEBDAV, @@ -11,8 +12,10 @@ import { updateSync, getSettingWithDefault, getRulesWithDefault, + getWordsWithDefault, setSetting, setRules, + setWords, } from "./storage"; import { apiSyncData } from "../apis"; import { sha256, removeEndchar } from "./utils"; @@ -135,6 +138,25 @@ export const trySyncRules = async () => { } }; +/** + * 同步词汇 + * @returns + */ +const syncWords = async () => { + const res = await syncData(KV_WORDS_KEY, getWordsWithDefault); + if (res?.isNew) { + await setWords(res.value); + } +}; + +export const trySyncWords = async () => { + try { + await syncWords(); + } catch (err) { + console.log("[sync fav words]", err); + } +}; + /** * 同步分享规则 * @param {*} param0 @@ -163,9 +185,11 @@ export const syncShareRules = async ({ rules, syncUrl, syncKey }) => { export const syncSettingAndRules = async () => { await syncSetting(); await syncRules(); + await syncWords(); }; export const trySyncSettingAndRules = async () => { await trySyncSetting(); await trySyncRules(); + await trySyncWords(); }; diff --git a/src/views/Options/FavWords.js b/src/views/Options/FavWords.js new file mode 100644 index 0000000..cd0963d --- /dev/null +++ b/src/views/Options/FavWords.js @@ -0,0 +1,90 @@ +import Stack from "@mui/material/Stack"; +import { OPT_TRANS_BAIDU } from "../../config"; +import { useEffect, useState } from "react"; +import Typography from "@mui/material/Typography"; +import Accordion from "@mui/material/Accordion"; +import AccordionSummary from "@mui/material/AccordionSummary"; +import AccordionDetails from "@mui/material/AccordionDetails"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import CircularProgress from "@mui/material/CircularProgress"; +import Alert from "@mui/material/Alert"; +import { apiTranslate } from "../../apis"; +import Box from "@mui/material/Box"; +import { useFavWords } from "../../hooks/FavWords"; +import { DictCont } from "../Selection/TranCont"; + +function DictField({ word }) { + const [dictResult, setDictResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + useEffect(() => { + (async () => { + try { + setLoading(true); + setError(""); + const dictRes = await apiTranslate({ + text: word, + translator: OPT_TRANS_BAIDU, + fromLang: "en", + toLang: "zh-CN", + }); + setDictResult(dictRes[2].dict_result); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + })(); + }, [word]); + + if (loading) { + return ; + } + + if (error) { + return {error}; + } + + return ; +} + +function FavAccordion({ word }) { + const [expanded, setExpanded] = useState(false); + + const handleChange = (e) => { + setExpanded((pre) => !pre); + }; + + return ( + + }> + {/* {`[${new Date( + createdAt + ).toLocaleString()}] ${word}`} */} + {word} + + + {expanded && } + + + ); +} + +export default function FavWords() { + const { favWords } = useFavWords(); + const favList = Object.entries(favWords).sort((a, b) => + a[0].localeCompare(b[0]) + ); + return ( + + + + {favList.map(([word, { createdAt }]) => ( + + ))} + + + + ); +} diff --git a/src/views/Options/Navigator.js b/src/views/Options/Navigator.js index 8ef401f..bd00c1e 100644 --- a/src/views/Options/Navigator.js +++ b/src/views/Options/Navigator.js @@ -14,6 +14,7 @@ import ApiIcon from "@mui/icons-material/Api"; import SendTimeExtensionIcon from "@mui/icons-material/SendTimeExtension"; import InputIcon from "@mui/icons-material/Input"; import SelectAllIcon from '@mui/icons-material/SelectAll'; +import EventNoteIcon from '@mui/icons-material/EventNote'; function LinkItem({ label, url, icon }) { const match = useMatch(url); @@ -70,6 +71,12 @@ export default function Navigator(props) { url: "/webfix", icon: , }, + { + id: "words", + label: i18n("favorite_words"), + url: "/words", + icon: , + }, { id: "about", label: i18n("about"), url: "/about", icon: }, ]; return ( diff --git a/src/views/Options/index.js b/src/views/Options/index.js index 8085d77..6170369 100644 --- a/src/views/Options/index.js +++ b/src/views/Options/index.js @@ -21,6 +21,7 @@ import Apis from "./Apis"; import Webfix from "./Webfix"; import InputSetting from "./InputSetting"; import Tranbox from "./Tranbox"; +import FavWords from "./FavWords"; export default function Options() { const [error, setError] = useState(""); @@ -125,6 +126,7 @@ export default function Options() { } /> } /> } /> + } /> } /> diff --git a/src/views/Selection/FavBtn.js b/src/views/Selection/FavBtn.js new file mode 100644 index 0000000..0f0f352 --- /dev/null +++ b/src/views/Selection/FavBtn.js @@ -0,0 +1,31 @@ +import IconButton from "@mui/material/IconButton"; +import FavoriteIcon from "@mui/icons-material/Favorite"; +import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder"; +import { useState } from "react"; +import { useFavWords } from "../../hooks/FavWords"; + +export default function FavBtn({ word }) { + const { favWords, toggleFav } = useFavWords(); + const [loading, setLoading] = useState(false); + + const handleClick = async () => { + try { + setLoading(true); + await toggleFav(word); + } catch (err) { + console.log("[set fav]", err); + } finally { + setLoading(false); + } + }; + + return ( + + {favWords[word] ? ( + + ) : ( + + )} + + ); +} diff --git a/src/views/Selection/TranCont.js b/src/views/Selection/TranCont.js index 75da168..95c9068 100644 --- a/src/views/Selection/TranCont.js +++ b/src/views/Selection/TranCont.js @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; import { apiTranslate } from "../../apis"; import { isValidWord } from "../../libs/utils"; import CopyBtn from "./CopyBtn"; +import FavBtn from "./FavBtn"; const exchangeMap = { word_third: "第三人称单数", @@ -20,16 +21,24 @@ const exchangeMap = { word_proto: "原词", }; -function DictCont({ dictResult }) { +export function DictCont({ dictResult }) { if (!dictResult) { return; } return ( -
- {dictResult.simple_means?.word_name} -
+ +
+ {dictResult.simple_means?.word_name} +
+ +
+ {dictResult.simple_means?.symbols?.map(({ ph_en, ph_am, parts }, idx) => (
{`英[${ph_en}] 美[${ph_am}]`}
@@ -42,11 +51,13 @@ function DictCont({ dictResult }) {
))} +
{Object.entries(dictResult.simple_means?.exchange || {}) .map(([key, val]) => `${exchangeMap[key] || key}: ${val.join(", ")}`) .join("; ")}
+ {Object.values(dictResult.simple_means?.tags || {}) .flat()