@@ -21,7 +21,8 @@ import {
2121 IconPlus ,
2222 IconSettings ,
2323} from "@arco-design/web-react/icon" ;
24- import { SCRIPT_RUN_STATUS_RUNNING } from "@App/app/repo/scripts" ;
24+ import type { SCMetadata } from "@App/app/repo/scripts" ;
25+ import { SCRIPT_RUN_STATUS_RUNNING , ScriptDAO } from "@App/app/repo/scripts" ;
2526import { RiPlayFill , RiStopFill } from "react-icons/ri" ;
2627import { useTranslation } from "react-i18next" ;
2728import { ScriptIcons } from "@App/pages/options/routes/utils" ;
@@ -32,7 +33,10 @@ import type {
3233 ScriptMenuItemOption ,
3334} from "@App/app/service/service_worker/types" ;
3435import { popupClient , runtimeClient , scriptClient } from "@App/pages/store/features/script" ;
35- import { i18nName } from "@App/locales/locales" ;
36+ import { i18nLang , i18nName } from "@App/locales/locales" ;
37+
38+ // 用于读取 metadata
39+ const scriptDAO = new ScriptDAO ( ) ;
3640
3741const CollapseItem = Collapse . Item ;
3842
@@ -114,8 +118,8 @@ const MenuItem = React.memo(({ menuItems, uuid }: MenuItemProps) => {
114118MenuItem . displayName = "MenuItem" ;
115119
116120interface CollapseHeaderProps {
117- item : ScriptMenu ;
118- onEnableChange : ( item : ScriptMenu , checked : boolean ) => void ;
121+ item : ScriptMenuEntry ;
122+ onEnableChange : ( item : ScriptMenuEntry , checked : boolean ) => void ;
119123}
120124
121125const CollapseHeader = React . memo (
@@ -164,12 +168,12 @@ const CollapseHeader = React.memo(
164168CollapseHeader . displayName = "CollapseHeader" ;
165169
166170interface ListMenuItemProps {
167- item : ScriptMenu ;
171+ item : ScriptMenuEntry ;
168172 scriptMenus : GroupScriptMenuItemsProp ;
169173 menuExpandNum : number ;
170174 isBackscript : boolean ;
171175 url : URL | null ;
172- onEnableChange : ( item : ScriptMenu , checked : boolean ) => void ;
176+ onEnableChange : ( item : ScriptMenuEntry , checked : boolean ) => void ;
173177 handleDeleteScript : ( uuid : string ) => void ;
174178}
175179
@@ -320,6 +324,13 @@ ListMenuItem.displayName = "ListMenuItem";
320324
321325type TGrouppedMenus = Record < string , GroupScriptMenuItemsProp > & { __length__ ?: number } ;
322326
327+ type ScriptMenuEntry = ScriptMenu & {
328+ menuUpdated ?: number ;
329+ metadata : SCMetadata ;
330+ } ;
331+
332+ let scriptDataAsyncCounter = 0 ;
333+
323334// Popup 页面使用的脚本/选单清单元件:只负责渲染与互动,状态与持久化交由外部 client 处理。
324335const ScriptMenuList = React . memo (
325336 ( {
@@ -335,23 +346,30 @@ const ScriptMenuList = React.memo(
335346 currentUrl : string ;
336347 menuExpandNum : number ;
337348 } ) => {
338- const [ list , setList ] = useState (
339- [ ] as ( ScriptMenu & {
340- menuUpdated ?: number ;
341- } ) [ ]
342- ) ;
349+ // extraData 为 undefined 时先等待异步加载完成,避免重复渲染
350+ const [ extraData , setExtraData ] = useState <
351+ | {
352+ uuids : string ;
353+ lang : string ;
354+ metadata : Record < string , SCMetadata > ;
355+ }
356+ | undefined
357+ > ( undefined ) ;
358+ const [ scriptMenuList , setScriptMenuList ] = useState < ScriptMenuEntry [ ] > ( [ ] ) ;
343359 const { t } = useTranslation ( ) ;
344360
345361 const [ grouppedMenus , setGrouppedMenus ] = useState < TGrouppedMenus > ( { } ) ;
346362
347- // 依 groupKey 进行聚合:将同语义(mainframe/subframe)命令合并为单一分组以供 UI 呈现。
348- useEffect ( ( ) => {
349- const list_ = list ;
363+ const updateScriptMenuList = ( scriptMenuList : ScriptMenuEntry [ ] ) => {
364+ setScriptMenuList ( scriptMenuList ) ;
365+ // 因为 scriptMenuList 的修改只在这处。
366+ // 直接在这里呼叫 setGrouppedMenus, 不需要 useEffect
350367 setGrouppedMenus ( ( prev ) => {
368+ // 依 groupKey 进行聚合:将同语义(mainframe/subframe)命令合并为单一分组以供 UI 呈现。
351369 const ret = { } as TGrouppedMenus ;
352370 let changed = false ;
353371 let retLen = 0 ;
354- for ( const { uuid, menus, menuUpdated : m } of list_ ) {
372+ for ( const { uuid, menus, menuUpdated : m } of scriptMenuList ) {
355373 retLen ++ ;
356374 const menuUpdated = m || 0 ;
357375 if ( prev [ uuid ] ?. menuUpdated === menuUpdated ) {
@@ -383,7 +401,7 @@ const ScriptMenuList = React.memo(
383401 // 若无引用变更则维持原物件以降低重渲染
384402 return changed ? ret : prev ;
385403 } ) ;
386- } , [ list ] ) ;
404+ } ;
387405
388406 const url = useMemo ( ( ) => {
389407 let url : URL ;
@@ -399,8 +417,50 @@ const ScriptMenuList = React.memo(
399417 return url ;
400418 } , [ currentUrl ] ) ;
401419
420+ // string memo 避免 uuids 以外的改变影响
421+ const uuids = useMemo ( ( ) => script . map ( ( item ) => item . uuid ) . join ( "\n" ) , [ script ] ) ;
422+ // eslint-disable-next-line react-hooks/exhaustive-deps
423+ const lang = useMemo ( ( ) => i18nLang ( ) , [ t ] ) ; // 当 t 改变时,重新检查当前页面语言
424+
425+ // 以 异步方式 取得 metadata 放入 extraData
426+ // script 或 extraData 的更新时都会再次执行
427+ useEffect ( ( ) => {
428+ if ( extraData && extraData . uuids === uuids && extraData . lang === lang ) {
429+ // extraData 已取得
430+ // 把 getPopupData() 的 scriptMenuList 和 异步结果 的 metadata 合并至 scriptMenuList
431+ const metadata = extraData . metadata ;
432+ const newScriptMenuList = script . map ( ( item ) => ( { ...item , metadata : metadata [ item . uuid ] || { } } ) ) ;
433+ updateScriptMenuList ( newScriptMenuList ) ;
434+ } else {
435+ // 取得 extraData
436+ scriptDataAsyncCounter = ( scriptDataAsyncCounter % 255 ) + 1 ; // 轮出 1 ~ 255
437+ const lastCounter = scriptDataAsyncCounter ;
438+ scriptDAO . gets ( uuids . split ( "\n" ) ) . then ( ( res ) => {
439+ if ( lastCounter !== scriptDataAsyncCounter ) {
440+ // 由于 state 改变,在结果取得前 useEffect 再次执行,因此需要忽略上次结果
441+ return ;
442+ }
443+ const metadataRecord = { } as Record < string , SCMetadata > ;
444+ const nameKey = `name:${ lang } ` ;
445+ for ( const entry of res ) {
446+ if ( entry ) {
447+ const m = entry . metadata ;
448+ const [ icon ] = m . icon || m . iconurl || m . icon64 || m . icon64url || [ ] ;
449+ // metadataRecord 的储存量不影响 storage.session 但影响页面的记忆体
450+ // 按需要可以增加其他 metadata, 例如 @match @include @exclude
451+ metadataRecord [ entry . uuid ] = {
452+ icon : [ icon ] , // 只储存单个 icon
453+ [ nameKey ] : [ i18nName ( entry ) ] , // 只储存 i18n 的 name
454+ } satisfies SCMetadata ;
455+ }
456+ }
457+ setExtraData ( { uuids, lang, metadata : metadataRecord } ) ;
458+ // 再次触发 useEffect
459+ } ) ;
460+ }
461+ } , [ script , uuids , lang , extraData ] ) ;
462+
402463 useEffect ( ( ) => {
403- setList ( script ) ;
404464 // 注册菜单快速键(accessKey):以各分组第一个项目的 accessKey 作为触发条件。
405465 const checkItems = new Map ( ) ;
406466 for ( const [ _uuid , menus ] of Object . entries ( grouppedMenus ) ) {
@@ -429,7 +489,7 @@ const ScriptMenuList = React.memo(
429489 checkItems . clear ( ) ;
430490 document . removeEventListener ( "keypress" , sharedKeyPressListner ) ;
431491 } ;
432- } , [ script , grouppedMenus ] ) ;
492+ } , [ grouppedMenus ] ) ;
433493
434494 const handleDeleteScript = ( uuid : string ) => {
435495 // 本地先行移除列表项(乐观更新);若删除失败会显示错误讯息。
@@ -438,18 +498,18 @@ const ScriptMenuList = React.memo(
438498 } ) ;
439499 } ;
440500
441- const onEnableChange = ( item : ScriptMenu , checked : boolean ) => {
501+ const onEnableChange = ( item : ScriptMenuEntry , checked : boolean ) => {
442502 scriptClient . enable ( item . uuid , checked ) . catch ( ( err ) => {
443503 Message . error ( err ) ;
444504 } ) ;
445505 } ;
446506
447507 return (
448508 < >
449- { list . length === 0 ? (
509+ { scriptMenuList . length === 0 ? (
450510 < Empty description = { t ( "no_data" ) } />
451511 ) : (
452- list . map ( ( item , _index ) => (
512+ scriptMenuList . map ( ( item , _index ) => (
453513 < ListMenuItem
454514 key = { `${ item . uuid } ` }
455515 url = { url }
0 commit comments