diff --git a/src/app/repo/repo.ts b/src/app/repo/repo.ts index 114da65b3..53ca314aa 100644 --- a/src/app/repo/repo.ts +++ b/src/app/repo/repo.ts @@ -204,6 +204,33 @@ export abstract class Repo { }); } + public getRecord(keys: string[]): Promise>> { + keys = keys.map((key) => this.joinKey(key)); + if (this.useCache) { + return loadCache().then((cache) => { + const record: Partial> = {}; + for (const key of keys) { + if (cache[key]) { + record[key] = Object.assign({}, cache[key]); + } else { + record[key] = cache[key]; + } + } + return record; + }); + } + return new Promise((resolve) => { + chrome.storage.local.get(keys, (result) => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.storage.local.get:", lastError); + // 无视storage API错误,继续执行 + } + resolve(result as Partial>); + }); + }); + } + private filter(data: { [key: string]: T }, filters?: (key: string, value: T) => boolean): T[] { const ret: T[] = []; for (const key in data) { diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index 934b62c29..237434f8b 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -33,7 +33,7 @@ import type { ScriptMenuItemOption, } from "@App/app/service/service_worker/types"; import { popupClient, runtimeClient, scriptClient } from "@App/pages/store/features/script"; -import { i18nLang, i18nName } from "@App/locales/locales"; +import { i18nName } from "@App/locales/locales"; // 用于读取 metadata const scriptDAO = new ScriptDAO(); @@ -329,8 +329,6 @@ type ScriptMenuEntry = ScriptMenu & { metadata: SCMetadata; }; -let scriptDataAsyncCounter = 0; - // Popup 页面使用的脚本/选单清单元件:只负责渲染与互动,状态与持久化交由外部 client 处理。 const ScriptMenuList = React.memo( ({ @@ -346,15 +344,6 @@ const ScriptMenuList = React.memo( currentUrl: string; menuExpandNum: number; }) => { - // extraData 为 undefined 时先等待异步加载完成,避免重复渲染 - const [extraData, setExtraData] = useState< - | { - uuids: string; - lang: string; - metadata: Record; - } - | undefined - >(undefined); const [scriptMenuList, setScriptMenuList] = useState([]); const { t } = useTranslation(); @@ -417,48 +406,34 @@ const ScriptMenuList = React.memo( return url; }, [currentUrl]); - // string memo 避免 uuids 以外的改变影响 - const uuids = useMemo(() => script.map((item) => item.uuid).join("\n"), [script]); - // eslint-disable-next-line react-hooks/exhaustive-deps - const lang = useMemo(() => i18nLang(), [t]); // 当 t 改变时,重新检查当前页面语言 - + const cache = useMemo(() => new Map(), []); // 以 异步方式 取得 metadata 放入 extraData // script 或 extraData 的更新时都会再次执行 useEffect(() => { - if (extraData && extraData.uuids === uuids && extraData.lang === lang) { - // extraData 已取得 - // 把 getPopupData() 的 scriptMenuList 和 异步结果 的 metadata 合并至 scriptMenuList - const metadata = extraData.metadata; - const newScriptMenuList = script.map((item) => ({ ...item, metadata: metadata[item.uuid] || {} })); - updateScriptMenuList(newScriptMenuList); - } else { - // 取得 extraData - scriptDataAsyncCounter = (scriptDataAsyncCounter % 255) + 1; // 轮出 1 ~ 255 - const lastCounter = scriptDataAsyncCounter; - scriptDAO.gets(uuids.split("\n")).then((res) => { - if (lastCounter !== scriptDataAsyncCounter) { - // 由于 state 改变,在结果取得前 useEffect 再次执行,因此需要忽略上次结果 - return; - } - const metadataRecord = {} as Record; - const nameKey = `name:${lang}`; - for (const entry of res) { - if (entry) { - const m = entry.metadata; - const [icon] = m.icon || m.iconurl || m.icon64 || m.icon64url || []; - // metadataRecord 的储存量不影响 storage.session 但影响页面的记忆体 - // 按需要可以增加其他 metadata, 例如 @match @include @exclude - metadataRecord[entry.uuid] = { - icon: [icon], // 只储存单个 icon - [nameKey]: [i18nName(entry)], // 只储存 i18n 的 name - } satisfies SCMetadata; + let isMounted = true; + // 先从 cache 读取,避免重复请求相同 uuid 的 metadata + Promise.all( + script.map(async (item) => { + let metadata = cache.get(item.uuid); + if (!metadata) { + const script = await scriptDAO.get(item.uuid); + if (script) { + metadata = script.metadata || {}; } + cache.set(item.uuid, metadata); } - setExtraData({ uuids, lang, metadata: metadataRecord }); - // 再次触发 useEffect - }); - } - }, [script, uuids, lang, extraData]); + return { ...item, metadata: metadata || {} }; + }) + ).then((newScriptMenuList) => { + if (!isMounted) { + return; + } + updateScriptMenuList(newScriptMenuList); + }); + return () => { + isMounted = false; + }; + }, [cache, script]); useEffect(() => { // 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。 diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index 039c34eb8..901ded12e 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -59,39 +59,23 @@ function App() { const { t } = useTranslation(); const pageTabIdRef = useRef(0); - // 只随 script 数量和启动状态而改变的state - const normalEnables = useMemo(() => { - // 返回字串让 React 比对 state 有否改动 - return scriptList.map((script) => (script.enable ? 1 : 0)).join(","); - }, [scriptList]); - - // 只随 script 数量和启动状态而改变的state - const backEnables = useMemo(() => { - // 返回字串让 React 比对 state 有否改动 - return backScriptList.map((script) => (script.enable ? 1 : 0)).join(","); - }, [backScriptList]); - const normalScriptCounts = useMemo(() => { - // 拆回array - const enables = normalEnables.split(","); // 计算已开启了的数量 - const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0); + const running = scriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0); return { running, - total: enables.length, // 总数 + total: scriptList.length, // 总数 }; - }, [normalEnables]); + }, [scriptList]); const backScriptCounts = useMemo(() => { - // 拆回array - const enables = backEnables.split(","); // 计算已开启了的数量 - const running = enables.reduce((p, c) => p + (+c ? 1 : 0), 0); + const running = backScriptList.reduce((p, c) => p + (c.enable ? 1 : 0), 0); return { running, - total: enables.length, // 总数 + total: backScriptList.length, // 总数 }; - }, [backEnables]); + }, [backScriptList]); const urlHost = useMemo(() => { let url: URL | undefined; diff --git a/tests/pages/popup/App.test.tsx b/tests/pages/popup/App.test.tsx index 348e41f66..9cfe7b175 100644 --- a/tests/pages/popup/App.test.tsx +++ b/tests/pages/popup/App.test.tsx @@ -169,8 +169,8 @@ describe("Popup App Component", () => { await waitFor( () => { // 检查是否存在折叠面板结构 - expect(screen.getByText("current_page_scripts (0/1)")).toBeInTheDocument(); - expect(screen.getByText("enabled_background_scripts (0/1)")).toBeInTheDocument(); + expect(screen.getByText("current_page_scripts (0/0)")).toBeInTheDocument(); + expect(screen.getByText("enabled_background_scripts (0/0)")).toBeInTheDocument(); }, { timeout: 3000 } ); @@ -191,8 +191,8 @@ describe("Popup App Component", () => { await waitFor( () => { expect(screen.getByText("ScriptCat")).toBeInTheDocument(); - expect(screen.getByText("current_page_scripts (0/1)")).toBeInTheDocument(); - expect(screen.getByText("enabled_background_scripts (0/1)")).toBeInTheDocument(); + expect(screen.getByText("current_page_scripts (0/0)")).toBeInTheDocument(); + expect(screen.getByText("enabled_background_scripts (0/0)")).toBeInTheDocument(); expect(screen.getByText("v" + ExtVersion)).toBeInTheDocument(); }, { timeout: 3000 }