From da24ac234f0eeae0159dce6c2b346d06fb72eaa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Sat, 1 Nov 2025 12:01:56 +0800 Subject: [PATCH 01/35] =?UTF-8?q?=E2=9C=A8=20=E4=BC=98=E5=8C=96=E8=8F=9C?= =?UTF-8?q?=E5=8D=95=E5=B1=95=E5=BC=80=E9=A1=B9=E4=B8=BA0=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E4=BA=A4=E4=BA=92=E9=80=BB=E8=BE=91=20#868?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/components/ScriptMenuList/index.tsx | 20 +++++++++++++++---- src/pages/popup/App.tsx | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index b40ed8708..d29c8756f 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -177,7 +177,7 @@ const ListMenuItem = React.memo( ({ item, scriptMenus, menuExpandNum, isBackscript, url, onEnableChange, handleDeleteScript }: ListMenuItemProps) => { const { t } = useTranslation(); const [isEffective, setIsEffective] = useState(item.isEffective); - + const [isActive, setIsActive] = useState(false); const [isExpand, setIsExpand] = useState(false); const handleExpandMenu = () => { @@ -185,12 +185,16 @@ const ListMenuItem = React.memo( }; const visibleMenus = useMemo(() => { + // 当menuExpandNum为0时,跟随 isActive 状态显示全部菜单 const m = scriptMenus?.group || []; + if (menuExpandNum === 0 && isActive) { + return m; + } return m.length > menuExpandNum && !isExpand ? m.slice(0, menuExpandNum) : m; - }, [scriptMenus?.group, isExpand, menuExpandNum]); + }, [scriptMenus?.group, isExpand, menuExpandNum, isActive]); const shouldShowMore = useMemo( - () => scriptMenus?.group?.length > menuExpandNum, + () => menuExpandNum > 0 && scriptMenus?.group?.length > menuExpandNum, [scriptMenus?.group, menuExpandNum] ); @@ -201,7 +205,15 @@ const ListMenuItem = React.memo( }; return ( - + { + setIsActive(keys.includes(item.uuid)); + }} + bordered={false} + expandIconPosition="right" + key={item.uuid} + > } name={item.uuid} diff --git a/src/pages/popup/App.tsx b/src/pages/popup/App.tsx index e29c1ae59..251aa9bed 100644 --- a/src/pages/popup/App.tsx +++ b/src/pages/popup/App.tsx @@ -253,7 +253,7 @@ function App() { for (const unhook of unhooks) unhook(); unhooks.length = 0; }; - }, []); + }, [subscribeMessage]); const { handleEnableScriptChange, handleSettingsClick, handleNotificationClick } = { handleEnableScriptChange: (val: boolean) => { From 3e406dc4562adf7d7f3b79b52623b87e87ef1ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Mon, 3 Nov 2025 13:39:36 +0800 Subject: [PATCH 02/35] =?UTF-8?q?=E2=9C=A8=20=E5=85=B3=E9=97=AD=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=8A=9F=E8=83=BD=E5=90=8E=E5=B1=95=E7=A4=BA=E7=81=B0?= =?UTF-8?q?=E8=89=B2=E5=9B=BE=E6=A0=87=20#897?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rspack.config.ts | 6 ++++ src/app/service/service_worker/runtime.ts | 34 ++++++++++++++++++++++ src/assets/logo-32.png | Bin 0 -> 1198 bytes src/assets/logo-beta-32.png | Bin 0 -> 1171 bytes src/assets/logo-gray-32.png | Bin 0 -> 886 bytes src/assets/logo-gray.png | Bin 0 -> 1934 bytes 6 files changed, 40 insertions(+) create mode 100644 src/assets/logo-32.png create mode 100644 src/assets/logo-beta-32.png create mode 100644 src/assets/logo-gray-32.png create mode 100644 src/assets/logo-gray.png diff --git a/rspack.config.ts b/rspack.config.ts index 9efb79444..793afe542 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -136,6 +136,12 @@ export default defineConfig({ from: `${assets}/logo${isDev || isBeta ? "-beta" : ""}.png`, to: `${dist}/ext/assets/logo.png`, }, + { + from: `${assets}/logo${isDev || isBeta ? "-beta" : ""}-32.png`, + to: `${dist}/ext/assets/logo-32.png`, + }, + { from: `${assets}/logo-gray.png`, to: `${dist}/ext/assets/logo-gray.png` }, + { from: `${assets}/logo-gray-32.png`, to: `${dist}/ext/assets/logo-gray-32.png` }, { from: `${assets}/logo`, to: `${dist}/ext/assets/logo` }, { from: `${assets}/_locales`, diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 0129dd99b..82b87c48e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -461,6 +461,7 @@ export class RuntimeService { } else { this.systemConfig.addListener("enable_script", async (enable) => { this.isLoadScripts = enable; + this.updateIcon(enable); await this.unregisterUserscripts(); if (enable) { await this.registerUserscripts(); @@ -543,6 +544,9 @@ export class RuntimeService { this.isLoadScripts = isLoadScripts; this.blacklist = obtainBlackList(strBlacklist); + // 更新 logo + this.updateIcon(this.isLoadScripts); + // 检查是否开启了开发者模式 if (!this.isUserScriptsAvailable) { // 未开启加上警告引导 @@ -570,6 +574,36 @@ export class RuntimeService { })(); } + updateIcon(enableUserscript: boolean) { + if (enableUserscript) { + // 设置正常logo + chrome.action.setIcon( + { + path: { "32": chrome.runtime.getURL("assets/logo-32.png") }, + }, + () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.action.setIcon:", lastError); + } + } + ); + } else { + // 如果未启用脚本,设置灰色的logo + chrome.action.setIcon( + { + path: { "32": chrome.runtime.getURL("assets/logo-gray-32.png") }, + }, + () => { + const lastError = chrome.runtime.lastError; + if (lastError) { + console.error("chrome.runtime.lastError in chrome.action.setIcon:", lastError); + } + } + ); + } + } + public loadBlacklist() { // 设置黑名单match const blacklist = this.blacklist; // 重用cache的blacklist阵列 (immutable) diff --git a/src/assets/logo-32.png b/src/assets/logo-32.png new file mode 100644 index 0000000000000000000000000000000000000000..1fc1b86afeab9e61f9041f4ea4fb781f73202d4d GIT binary patch literal 1198 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#N0|R4xfKP}kP=V02Tf)=t3r)K%GVQkTwA+HyZV64h zD>Utn(A1kkQ-SOog41paPP;8IgYPPbv7sWL>ntmeu6RYbW!kzptM8_11;i%2!{!$DWNdO(A3b z-G^+#GKtIMKLG7yO!9VjDY>7!A{WSEFY)wsWq--d%xo=s`ts@dKjhVYS*`C9BM37dS6kzbq6sN(^S)-5+o=xUMUpGw%PCH6=A!PS?NiBu-*J z7`ZCJVck{Z%dtE!=FL_w^$h7vs#`ak@z^}pLsy;pCo=Bk_}C~=C3&IUwez`m_lwnc zr5|$eceE_rCp~Ri<$+H@%pJ!n9CTLg3$_mnt32?EYpaGqLWlY!8-O{+U$ChJ(GnavOFUa~X;ah&a zwa@;xPrMs9@7{i%`P*Oib{-3>f3JmFh0UJ4yji)hA~5Tlm-tbqm!G;AubXc#|9-Pw zZPAwrd-rd)vvZW4^y=qQjqABPe|%cA?e%OUr}Iy*cP;<*E$nMZ-03YcKQ}g{8Grcq zai&L2-5%yN<8*i3#aTLTy1HvhPXA(R+W6+B{tOLnU_7dpxJHzuB$lLFB^RXvDF!10 zBU4>NLtO*o5CdZ?Ljx-lV{HQiD+2@3eU{r%H00)|WTsW(){yeJ5oCh~+=i0O+|=Td g#M}ZjJ*JjcMuree^iDl60_tJ#boFyt=akR{0J*{cjQ{`u literal 0 HcmV?d00001 diff --git a/src/assets/logo-beta-32.png b/src/assets/logo-beta-32.png new file mode 100644 index 0000000000000000000000000000000000000000..86c2b3306cafa9ffbc477542307092dd0e8050b3 GIT binary patch literal 1171 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7e6l0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#N0|R4xfKP}kP{9p3w(D|iw`ACE0!amqTQcl7eu4k`sWVXtmH^0!1)$Ez+)8B8e{d(uj zY;Gt%W2U}6*PYE*?myflCZGR!`zN5Cj7i?^E+zMKSL6aY>?NMQuIw+lnVGFcPhUPg zA1K}D>Eak-ak}^N^WZ}U5^M$XYnr;G*_oUe8C%6>8qHl{QTWhhj)jBc+5h$Fw?zW< z=h)1jwP(LcW>voX@&Ao%4(i|2H+j#x;!ydn?PLz)mW*kowg?%Sapf>K(|w7Sw$KKHL^U2R3zr)TOkaxxo<^41MeUc=ITE3; z?E=F`7H*YCyc0?`H(0NnSID9%&;29l!IR)VsWmTM#Ui4%NgaE_8y0rmNN2;ojVtFc zMP4$BWX;LdU$=SH%(-`USsj$yA{4fm`@Ns_c*A+7-!o<``OkI2aGlY#$=XI17B*IC zjg$YpoXK=fKdRP6ZF$4#)rU$i>it>A`7P<$vu}Qk-}#>%pY(a{`+<6Kau+Fk;->kent6Y z-p+Wr?K5xSr{`|kJEa?ek)>MV8c~vxSdwa$T$Bo=7>o>zOmz(nbPX&+3@ojT&8&>g zwG9lc3=EcgG-je`$jwj5OsmAL;hL!BRiFk9xD6$lxv9k^iMa((J%+jl#vumAR;HF# YMuree^iDl60_tJ#boFyt=akR{00x?}7ytkO literal 0 HcmV?d00001 diff --git a/src/assets/logo-gray-32.png b/src/assets/logo-gray-32.png new file mode 100644 index 0000000000000000000000000000000000000000..c1d55da008000f4d8d8403c7e70c970a9ac9f7c9 GIT binary patch literal 886 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE0wix1Z>k4zl0AZa85pY67#JE_7#My5g&JNk zFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{XiajiKnkC`%7+SW-D=!6r ze73hmZg^)lPl&UQ>2E*W-NvAuGwIdF;Q5N;hbJHP^0M4>FmdlL)f*|T zVh4ND_qil37yiX#`QDTJ&(yfs$nzi0@T_g>Tz0I?`ki(9-_ZKct@%0m2MjibY)HsC z`Ksh)&|3SY>q|?{I89dQxcEX^YJOF8PuIeIb2IXHda`DR#tfs_2!mWL`E7whE?V8!z8_n{m3pzkEUW3m&Tl!{ zVqM~PldmmH3UfSUwtUUiz=#WvS6%#2TQTGP5#4WMZ3~^-{i1DDQsw>6&f0$JR8!OP zJgdh|Go`?)FK#IZ0z{phB&`{UFIK;r% z%GA`#$W+_Fz{&7%`@p%UUdFvWH}I8;Y}@wvbEAI(gP{X$>tFv2thPbP_Rfm}@L| z4kxWd7b_w(CkN@c7L^h*B9+V8d7kI>yk6(LUe6!D-{FUNwgLdOLJM0w z_Q?`Zrn3VDZ2p+>q*WFvaGhMK3h$I*Fp|#cr;l#~01tDb?59T#&lPflHK>@5tDD}L za_;tiMtq@)5{JQXKcMe{(8Ha=y;JyxNbf_cO>K8lcZo|Z8Z$nGCc=d{CE|UQy06gH z!jTL`!!D?mlKY{|n>R)7u(E70H?~@Oi~Rh~`mbZNDkHrg?9ZM%Hy>5p+uJ+W12j_~ zF|T{=>87#<(SwM$%fFeu;>;xg zidH+0l^TFfgEOuV5})t7-X|J-FZFRW=`5^uF3SxKS0&S@!mp00mRlKG32w#82U3Ic z>K1-q$R6TH{p#g6g8Yi=R$`du7oqz*AmZw+64CfjJbL-4`Y23QXx8@TT9_xZX#bKi z+a@5O>gg*?~+zOy3*VFGr<&7S@bM)Hhe$O>VSToA8 z+gZU^8+)1j;$`0pV)pGTMF2DQq%C{-W*0qK*GtkM8 zX7v?bi@c_QAZ8}Nm6*uPdpH7dWL+XFw~Mc))u{a7%p>`20)EesLM^gT;V!Dxg6}Dp zs9rwK!{uU znM?$)G9N{rqka_Jy5@3tj>>^xFSTJkud9gRV*Ti8NIf$E^c8FgMUCi+YprHt#E%}! zi;p}(l-x%Kx*VfTy2M?52832h3xkcYi_h@lqd_s?4zuhuUphFfE3VhB2CJujhfv;c zsQH@guf`fl$*QASsasWh3bhyL7stWHC$c;o6$=mZ!wi)t77th+al#Ku*B zzzP_c0WS?{#OnmjljbH?8nND=Kk>He@LFrRNq#{cO;X`wkB0UJYYD|M{y3xI^vTqO zsL|HQPxbO+UMz;_VDYDM=>-2}Tq4CIOxMI^%-K9VCa4WN{krdYe6!ts!}S)0G_75u z#Oh{x%C_tm9{uTpV5%|0-!s^EA?y4s3J0Y6A4p4 zH_+H>aunr511pdB{_InI=nSoe9U4ew4jXO{Na>6veEtRC)zCJ2~k|& zSl*?B@7RIwI8fmo;HNOWpfx<%$x8Jb>+aF!MZ_$|8sav$9u6p^UWRVs$8)Zmm& zqm5o8aDA(Dqwl>b_$205`}@MIOOUi*Vh(UVHg2M`*zo<*nTw4`jUL9P3L9EtP=N}X zhBT$^APe!5#pKL`IX7T$kzVKb`Zq;H9~)4C)5^S{&sS$LIb?0D@UiTZ)UVvr+u^Nxb}>wF% zsa{;xajw7Zi7PF*K;5@#`U}7bVX*_E{y)H) sN>r8s`2Tg_vN`_Y+~b_kzcH4Uwq}<9X(DdMHOLs?M0KOo|I_dMAEjYe3;+NC literal 0 HcmV?d00001 From ab36c86b5d031b88e71fbf9151696a42acba86fa Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Mon, 3 Nov 2025 13:44:21 +0800 Subject: [PATCH 03/35] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=84=9A=E6=9C=AC=E5=9B=BE=E6=A0=87=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=20(#893)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ 重构优化脚本图标加载 * 增加单元测试 * 整理代码 * Update src/app/service/service_worker/system.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * 调整代码 (#896) * 根据copilot修改 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> --- src/app/repo/favicon.ts | 27 ++ src/app/service/service_worker/client.ts | 9 - src/app/service/service_worker/fetch.ts | 120 ------ src/app/service/service_worker/index.ts | 12 +- src/app/service/service_worker/script.ts | 5 +- src/app/service/service_worker/system.ts | 74 ++-- src/app/service/service_worker/utils.ts | 35 +- src/pages/options/routes/ScriptList/hooks.tsx | 2 +- src/pages/store/favicons.test.ts | 44 +++ src/pages/store/favicons.ts | 353 ++++++++++++++++++ src/pages/store/utils.ts | 92 ----- src/pkg/utils/favicon.ts | 145 ------- src/service_worker.ts | 34 -- 13 files changed, 495 insertions(+), 457 deletions(-) create mode 100644 src/app/repo/favicon.ts delete mode 100644 src/app/service/service_worker/fetch.ts create mode 100644 src/pages/store/favicons.test.ts create mode 100644 src/pages/store/favicons.ts delete mode 100644 src/pages/store/utils.ts delete mode 100644 src/pkg/utils/favicon.ts diff --git a/src/app/repo/favicon.ts b/src/app/repo/favicon.ts new file mode 100644 index 000000000..5ecb4eb78 --- /dev/null +++ b/src/app/repo/favicon.ts @@ -0,0 +1,27 @@ +import { Repo } from "./repo"; + +export type FaviconRecord = { + match: string; + website: string; + icon?: string; +}; + +export interface Favicon { + uuid: string; + favicons: FaviconRecord[]; +} + +export class FaviconDAO extends Repo { + constructor() { + super("favicon"); + } + + save(key: string, value: Favicon) { + return super._save(key, value); + } +} + +export type FaviconFile = { + dirs: string[]; + filename: string; +}; diff --git a/src/app/service/service_worker/client.ts b/src/app/service/service_worker/client.ts index 2329d5d3c..6af25798b 100644 --- a/src/app/service/service_worker/client.ts +++ b/src/app/service/service_worker/client.ts @@ -21,7 +21,6 @@ import { CACHE_KEY_IMPORT_FILE } from "@App/app/cache_key"; import { type ResourceBackup } from "@App/pkg/backup/struct"; import { type VSCodeConnect } from "../offscreen/vscode-connect"; import type { GMInfoEnv } from "../content/types"; -import { type SystemService } from "./system"; import { type ScriptInfo } from "@App/pkg/utils/scriptInstall"; import type { ScriptService, TCheckScriptUpdateOption } from "./script"; @@ -404,12 +403,4 @@ export class SystemClient extends Client { connectVSCode(params: Parameters[0]): ReturnType { return this.do("connectVSCode", params); } - - loadFavicon(icon: string): Promise { - return this.doThrow("loadFavicon", icon); - } - - getFaviconFromDomain(domain: string): ReturnType { - return this.doThrow("getFaviconFromDomain", domain); - } } diff --git a/src/app/service/service_worker/fetch.ts b/src/app/service/service_worker/fetch.ts deleted file mode 100644 index e81cb3af2..000000000 --- a/src/app/service/service_worker/fetch.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { msgResponse, type TMsgResponse } from "./utils"; - -// AbortSignal.timeout 是较新的功能。如果不支持 AbortSignal.timeout,则返回传统以定时器操作 AbortController -const timeoutAbortSignal = - typeof AbortSignal?.timeout === "function" - ? (milis: number) => { - return AbortSignal.timeout(milis); - } - : (milis: number) => { - let controller: AbortController | null = new AbortController(); - const signal = controller.signal; - setTimeout(() => { - controller!.abort(); // 中断请求 - controller = null; - }, milis); - return signal; - }; - -const getFilename = (url: string) => { - const i = url.lastIndexOf("/"); - if (i >= 0) return url.substring(i + 1); - return url; -}; - -function parseFaviconsNew(html: string, callback: (href: string) => void) { - // Early exit if no link tags - if (!html.toLowerCase().includes("]+rel=["'](?:icon|apple-touch-icon|apple-touch-icon-precomposed)["'][^>]*>/gi; - const hrefRegex = /href=["'](.*?)["']/i; - - // Find all matching link tags - const matches = html.match(faviconRegex); - if (matches) { - for (const match of matches) { - const hrefMatch = match.match(hrefRegex); - if (hrefMatch && hrefMatch[1]) { - callback(hrefMatch[1]); - } - } - } - - return; -} - -const checkFileNameEqual = (a: string, b: string) => { - const name1 = getFilename(a); - const name2 = getFilename(b); - return 0 === name1.localeCompare(name2, "en", { sensitivity: "base" }); -}; - -/** - * 从域名获取favicon - */ -export async function fetchIconByDomain(domain: string): Promise> { - const url = `https://${domain}`; - const icons: string[] = []; - - // 设置超时时间(例如 5 秒) - const timeout = 5000; // 单位:毫秒 - let domainOK = false; - let fetchingUrl = ""; - - try { - // 获取页面HTML - const response = await fetch((fetchingUrl = url), { signal: timeoutAbortSignal(timeout) }); - const html = await response.text(); - const resolvedPageUrl = response.url; - const resolvedUrl = new URL(resolvedPageUrl); - const resolvedOrigin = resolvedUrl.origin; - - parseFaviconsNew(html, (href) => icons.push(resolveUrl(href, resolvedPageUrl))); - domainOK = true; - - // 检查默认favicon位置 - if (icons.length === 0) { - const faviconUrl = `${resolvedOrigin}/favicon.ico`; - icons.push(faviconUrl); - } - - const urls = await Promise.all( - icons.map((icon) => - fetch((fetchingUrl = icon), { method: "HEAD", signal: timeoutAbortSignal(timeout) }) - .then((res) => { - if (res.ok && checkFileNameEqual(res.url, icon)) { - return res.url; - } - }) - .catch(() => { - // 忽略错误 - }) - ) - ); - - return msgResponse(0, urls.filter((url) => !!url) as string[]); - } catch (error: any) { - if (error.name === "TypeError" && error.message === "Failed to fetch" && !domainOK) { - // 網絡錯誤 - return msgResponse(11, { name: "TypeError", message: `Unable to fetch ${domain}` }); - } else if (error.name === "AbortError" || error.name === "TimeoutError") { - // 超时 - return msgResponse(12, { name: "TimeoutError", message: `Timeout while fetching favicon: ${fetchingUrl}` }); - } else { - // 其他错误 - return msgResponse(1, { name: error.name, message: `Error fetching favicon for ${domain}:\n${error.message}` }); - } - } -} - -/** - * 解析相对URL为绝对URL - */ -function resolveUrl(href: string, base: string): string { - try { - return new URL(href, base).href; - } catch { - return href; // 如果解析失败,返回原始href - } -} diff --git a/src/app/service/service_worker/index.ts b/src/app/service/service_worker/index.ts index ee22d0d32..8fbf4c111 100644 --- a/src/app/service/service_worker/index.ts +++ b/src/app/service/service_worker/index.ts @@ -18,6 +18,7 @@ import { localePath, t } from "@App/locales/locales"; import { getCurrentTab, InfoNotification } from "@App/pkg/utils/utils"; import { onTabRemoved, onUrlNavigated, setOnUserActionDomainChanged } from "./url_monitor"; import { LocalStorageDAO } from "@App/app/repo/localStorage"; +import { FaviconDAO } from "@App/app/repo/favicon"; // service worker的管理器 export default class ServiceWorkerManager { @@ -42,6 +43,8 @@ export default class ServiceWorkerManager { }); this.sender.init(); + const faviconDAO = new FaviconDAO(); + const scriptDAO = new ScriptDAO(); scriptDAO.enableCache(); @@ -85,7 +88,14 @@ export default class ServiceWorkerManager { synchronize.init(); const subscribe = new SubscribeService(systemConfig, this.api.group("subscribe"), this.mq, script); subscribe.init(); - const system = new SystemService(systemConfig, this.api.group("system"), this.sender); + const system = new SystemService( + systemConfig, + this.api.group("system"), + this.sender, + this.mq, + scriptDAO, + faviconDAO + ); system.init(); const regularScriptUpdateCheck = async () => { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index c7317bdb3..b0a584ba7 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -26,7 +26,7 @@ import { type IMessageQueue } from "@Packages/message/message_queue"; import { createScriptInfo, type ScriptInfo, type InstallSource } from "@App/pkg/utils/scriptInstall"; import { type ResourceService } from "./resource"; import { type ValueService } from "./value"; -import { compileScriptCode, isEarlyStartScript } from "../content/utils"; +import { compileScriptCode } from "../content/utils"; import { type SystemConfig } from "@App/pkg/config/config"; import { localePath } from "@App/locales/locales"; import { arrayMove } from "@dnd-kit/sortable"; @@ -354,8 +354,7 @@ export class ScriptService { uuid: script.uuid, storageName: getStorageName(script), type: script.type, - isEarlyStart: isEarlyStartScript(script.metadata), - })); + })) as TDeleteScript[]; this.mq.publish("deleteScripts", data); return true; }) diff --git a/src/app/service/service_worker/system.ts b/src/app/service/service_worker/system.ts index 0679228f0..624bec4ad 100644 --- a/src/app/service/service_worker/system.ts +++ b/src/app/service/service_worker/system.ts @@ -1,56 +1,68 @@ import { type SystemConfig } from "@App/pkg/config/config"; import { type Group } from "@Packages/message/server"; import type { MessageSend } from "@Packages/message/types"; -import { createObjectURL, VscodeConnectClient } from "../offscreen/client"; +import { VscodeConnectClient } from "../offscreen/client"; import { cacheInstance } from "@App/app/cache"; +import type { IMessageQueue } from "@Packages/message/message_queue"; +import type { TDeleteScript, TInstallScript } from "../queue"; +import type { ScriptDAO } from "@App/app/repo/scripts"; +import type { FaviconDAO } from "@App/app/repo/favicon"; import { CACHE_KEY_FAVICON } from "@App/app/cache_key"; -import { fetchIconByDomain } from "./fetch"; +import { removeFaviconFolder } from "./utils"; // 一些系统服务 export class SystemService { constructor( private systemConfig: SystemConfig, private group: Group, - private msgSender: MessageSend + private msgSender: MessageSend, + private mq: IMessageQueue, + private scriptDAO: ScriptDAO, + private faviconDAO: FaviconDAO ) {} - getFaviconFromDomain(domain: string) { - return fetchIconByDomain(domain); - } - - async init() { + init() { const vscodeConnect = new VscodeConnectClient(this.msgSender); this.group.on("connectVSCode", (params) => { return vscodeConnect.connect(params); }); - this.group.on("loadFavicon", async (url) => { - // 加载favicon图标 - // 对url做一个缓存 - const cacheKey = `${CACHE_KEY_FAVICON}${url}`; - return cacheInstance.getOrSet(cacheKey, async () => { - return fetch(url) - .then((response) => response.blob()) - .then((blob) => createObjectURL(this.msgSender, blob, true)) - .catch(() => { - return ""; - }); - }); + + // 脚本更新删除favicon缓存 + this.mq.subscribe("installScript", async (messages) => { + if (messages.update) { + // 删除旧的favicon缓存 + await this.faviconDAO.delete(messages.script.uuid); + await cacheInstance.del(`${CACHE_KEY_FAVICON}${messages.script.uuid}`); + } }); - this.group.on("getFaviconFromDomain", this.getFaviconFromDomain.bind(this)); + // 监听脚本删除,清理favicon缓存 + this.mq.subscribe("deleteScripts", async (message) => { + for (const { uuid } of message) { + // 删除数据 + await this.faviconDAO.delete(uuid); + // 删除opfs缓存 + try { + await removeFaviconFolder(uuid); + } catch { + // 忽略错误 + } + } + }); // 如果开启了自动连接vscode,则自动连接 // 使用tx来确保service_worker恢复时不会再执行 - const init = await cacheInstance.get("vscodeReconnect"); - if (!init) { - if (await this.systemConfig.getVscodeReconnect()) { - // 调用连接 - vscodeConnect.connect({ - url: await this.systemConfig.getVscodeUrl(), - reconnect: true, - }); + cacheInstance.get("vscodeReconnect").then(async (init) => { + if (!init) { + if (await this.systemConfig.getVscodeReconnect()) { + // 调用连接 + vscodeConnect.connect({ + url: await this.systemConfig.getVscodeUrl(), + reconnect: true, + }); + } + await cacheInstance.set("vscodeReconnect", true); } - await cacheInstance.set("vscodeReconnect", true); - } + }); } } diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 61b766727..ba77b6099 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -92,27 +92,6 @@ export function parseUrlSRI(url: string): { return { url: urls[0], hash }; } -export type TMsgResponse = - | { - ok: true; - res: T; - } - | { - ok: false; - err: { - name?: string; - message?: string; - errType?: number; - [key: string]: any; - }; - }; - -export function msgResponse(errType: number, t: Error | any, params?: T): TMsgResponse { - if (!errType) return { ok: true, res: t }; - const { name, message } = t; - return { ok: false, err: { name, message, errType, ...t, ...params } }; -} - export async function notificationsUpdate( notificationId: string, options: chrome.notifications.NotificationOptions @@ -281,3 +260,17 @@ export function scriptURLPatternResults(scriptRes: { return { scriptUrlPatterns, originalUrlPatterns }; } + +export const getFaviconFolder = (uuid: string): Promise => { + return navigator.storage + .getDirectory() + .then((opfsRoot) => opfsRoot.getDirectoryHandle(`cached_favicons`, { create: true })) + .then((faviconsFolder) => faviconsFolder.getDirectoryHandle(`${uuid}`, { create: true })); +}; + +export const removeFaviconFolder = (uuid: string): Promise => { + return navigator.storage + .getDirectory() + .then((opfsRoot) => opfsRoot.getDirectoryHandle(`cached_favicons`)) + .then((faviconsFolder) => faviconsFolder.removeEntry(`${uuid}`, { recursive: true })); +}; diff --git a/src/pages/options/routes/ScriptList/hooks.tsx b/src/pages/options/routes/ScriptList/hooks.tsx index 2a0ccc581..bc7d8afcf 100644 --- a/src/pages/options/routes/ScriptList/hooks.tsx +++ b/src/pages/options/routes/ScriptList/hooks.tsx @@ -26,7 +26,7 @@ import { requestRunScript, requestStopScript, } from "@App/pages/store/features/script"; -import { loadScriptFavicons } from "@App/pages/store/utils"; +import { loadScriptFavicons } from "@App/pages/store/favicons"; import { arrayMove } from "@dnd-kit/sortable"; import { useEffect, useMemo, useState } from "react"; import { hashColor } from "../utils"; diff --git a/src/pages/store/favicons.test.ts b/src/pages/store/favicons.test.ts new file mode 100644 index 000000000..6f78f81b0 --- /dev/null +++ b/src/pages/store/favicons.test.ts @@ -0,0 +1,44 @@ +import { extractFaviconsDomain } from "@App/pages/store/favicons"; +import { describe, it, expect } from "vitest"; + +describe("extractFaviconsDomain", () => { + it("应该正确提取各种URL模式的域名", () => { + const result = extractFaviconsDomain( + [ + "https://example.com/*", // 基本的match模式 + "https://*.sub.com/*", // 通配符域名模式 -> sub.com + "https://a.*.domain.com/*", // 多个通配符 -> domain.com + "https://.site.com/*", // 以点开头的域名 -> site.com + "https://web.cn*/*", // 以通配符结尾的域名 -> web.cn + "*://test.com/*", // 通配符协议 + "https://sub.domain.com/*", // 子域名 + "https://host.com:8080/*", // 带端口号 + "simple.com", // 纯域名字符串 + "invalid-pattern", // 无效的模式 + ], + [] + ); + + expect(result).toHaveLength(10); + expect(result[0]).toEqual({ match: "https://example.com/*", domain: "example.com" }); + expect(result[1]).toEqual({ match: "https://*.sub.com/*", domain: "sub.com" }); + expect(result[2]).toEqual({ match: "https://a.*.domain.com/*", domain: "domain.com" }); + expect(result[3]).toEqual({ match: "https://.site.com/*", domain: "site.com" }); + expect(result[4]).toEqual({ match: "https://web.cn*/*", domain: "web.cn" }); + expect(result[5]).toEqual({ match: "*://test.com/*", domain: "test.com" }); + expect(result[6]).toEqual({ match: "https://sub.domain.com/*", domain: "sub.domain.com" }); + expect(result[7]).toEqual({ match: "https://host.com:8080/*", domain: "host.com:8080" }); + expect(result[8]).toEqual({ match: "simple.com", domain: "simple.com" }); + expect(result[9]).toEqual({ match: "invalid-pattern", domain: "" }); + + // 同时处理match和include规则 + const result2 = extractFaviconsDomain(["https://match.com/*"], ["https://include.com/*"]); + expect(result2).toHaveLength(2); + expect(result2[0]).toEqual({ match: "https://match.com/*", domain: "match.com" }); + expect(result2[1]).toEqual({ match: "https://include.com/*", domain: "include.com" }); + + // 空数组和默认参数 + expect(extractFaviconsDomain([], [])).toEqual([]); + expect(extractFaviconsDomain()).toEqual([]); + }); +}); diff --git a/src/pages/store/favicons.ts b/src/pages/store/favicons.ts new file mode 100644 index 000000000..011a26960 --- /dev/null +++ b/src/pages/store/favicons.ts @@ -0,0 +1,353 @@ +import { type Script, ScriptDAO } from "@App/app/repo/scripts"; +import { cacheInstance } from "@App/app/cache"; +import { CACHE_KEY_FAVICON } from "@App/app/cache_key"; +import { FaviconDAO, type FaviconFile, type FaviconRecord } from "@App/app/repo/favicon"; +import { v5 as uuidv5 } from "uuid"; +import { getFaviconFolder } from "@App/app/service/service_worker/utils"; + +let scriptDAO: ScriptDAO | null = null; +let faviconDAO: FaviconDAO | null = null; +const blobCaches = new Map(); + +/** + * 从URL模式中提取域名 + */ +export const extractDomainFromPattern = (pattern: string): string | null => { + try { + // 处理match模式: scheme://host/path + const matchPattern = /^(http|https|\*):\/\/([^/]+)(?:\/(.*))?$/; + const matches = pattern.match(matchPattern); + + if (matches) { + let host = matches[2]; + + // 删除最后的* + // 例如 "example.com*" 变为 "example.com" + while (host.endsWith("*")) { + host = host.slice(0, -1); + } + + // 删除 * 通配符 + // 例如 "*.example.com" 变为 "example.com" + // a.*.example.com 变为 "example.com" + while (host.includes("*")) { + // 从最后一个 * 开始删除 + const lastAsteriskIndex = host.lastIndexOf("*"); + host = host.slice(lastAsteriskIndex + 1); + } + + // 删除第一个. + // 例如 ".example.com" 变为 "example.com" + while (host.startsWith(".")) { + host = host.slice(1); + } + + return host; + } + + // 尝试作为URL解析 + if (pattern.startsWith("http://") || pattern.startsWith("https://")) { + const url = new URL(pattern); + return url.hostname; + } + + // 尝试匹配域名格式 + const domainMatch = pattern.match(/([a-z0-9][-a-z0-9]*\.)+[a-z0-9][-a-z0-9]*/i); + return domainMatch ? domainMatch[0] : null; + } catch { + return null; + } +}; + +// 从脚本的@match和@include规则中提取域名 +export const extractFaviconsDomain = (matche: string[] = [], include: string[] = []) => { + // 提取域名 + const domains = new Map(); + + // 处理match和include规则 + for (const pattern of [...matche, ...include]) { + const domain = extractDomainFromPattern(pattern); + if (domain) { + // 使用match作为key,避免重复 + domains.set(domain, { match: pattern, domain }); + } else { + // 如果无法提取域名,仍然保留原始pattern + domains.set(pattern, { match: pattern, domain: "" }); + } + } + + return Array.from(domains.values()); +}; + +// AbortSignal.timeout 是较新的功能。如果不支持 AbortSignal.timeout,则返回传统以定时器操作 AbortController +export const timeoutAbortSignal = + typeof AbortSignal?.timeout === "function" + ? (milis: number) => { + return AbortSignal.timeout(milis); + } + : (milis: number) => { + let controller: AbortController | null = new AbortController(); + const signal = controller.signal; + setTimeout(() => { + controller!.abort(); // 中断请求 + controller = null; + }, milis); + return signal; + }; + +/** + * 解析相对URL为绝对URL + */ +const resolveUrl = (href: string, base: string): string => { + try { + return new URL(href, base).href; + } catch { + return href; // 如果解析失败,返回原始href + } +}; + +export const parseFaviconsNew = (html: string, callback: (href: string) => void) => { + // Early exit if no link tags + if (!html.toLowerCase().includes("]+rel=["'](?:icon|apple-touch-icon|apple-touch-icon-precomposed)["'][^>]*>/gi; + const hrefRegex = /href=["'](.*?)["']/i; + + // Find all matching link tags + const matches = html.match(faviconRegex); + if (matches) { + for (const match of matches) { + const hrefMatch = match.match(hrefRegex); + if (hrefMatch && hrefMatch[1]) { + callback(hrefMatch[1]); + } + } + } + + return; +}; + +const getFilename = (url: string) => { + const i = url.lastIndexOf("/"); + if (i >= 0) return url.substring(i + 1); + return url; +}; + +const checkFileNameEqual = (a: string, b: string) => { + const name1 = getFilename(a); + const name2 = getFilename(b); + return 0 === name1.localeCompare(name2, "en", { sensitivity: "base" }); +}; + +/** + * 从域名获取favicon + */ +export async function fetchIconByDomain(domain: string): Promise { + const url = `https://${domain}`; + const icons: string[] = []; + + // 设置超时时间(例如 5 秒) + const timeout = 5000; // 单位:毫秒 + + // 获取页面HTML + const response = await fetch(url, { signal: timeoutAbortSignal(timeout) }); + const html = await response.text(); + const resolvedPageUrl = response.url; + const resolvedUrl = new URL(resolvedPageUrl); + const resolvedOrigin = resolvedUrl.origin; + + parseFaviconsNew(html, (href) => icons.push(resolveUrl(href, resolvedPageUrl))); + + // 检查默认favicon位置 + if (icons.length === 0) { + const faviconUrl = `${resolvedOrigin}/favicon.ico`; + icons.push(faviconUrl); + } + + const urls = await Promise.all( + icons.map((icon) => + fetch(icon, { method: "HEAD", signal: timeoutAbortSignal(timeout) }) + .then((res) => { + if (res.ok && checkFileNameEqual(res.url, icon)) { + return res.url; + } + }) + .catch(() => { + // 忽略错误 + }) + ) + ); + + return urls.filter((url) => !!url) as string[]; +} + +// 获取脚本的favicon +export const getScriptFavicon = async (uuid: string): Promise => { + scriptDAO ||= new ScriptDAO(); + faviconDAO ||= new FaviconDAO(); + const script = await scriptDAO.get(uuid); + if (!script) { + return []; + } + const favicon = await faviconDAO.get(uuid); + if (favicon) { + return favicon.favicons; + } + // 提取域名 + const domains = extractFaviconsDomain(script.metadata?.match || [], script.metadata?.include || []); + + // 并发获取favicon + const faviconRecords: FaviconRecord[] = await Promise.all( + domains.map(async (domain) => { + try { + if (domain.domain) { + const icons = await fetchIconByDomain(domain.domain); + const icon = icons.length > 0 ? icons[0] : ""; + return { match: domain.match, website: "http://" + domain.domain, icon }; + } + } catch { + // 忽略错误 + } + return { match: domain.match, website: "", icon: "" }; + }) + ); + // 储存并返回结果 + await faviconDAO.save(uuid, { + uuid, + favicons: faviconRecords, + }); + return faviconRecords; +}; + +// 加载favicon并缓存到OPFS +export const loadFavicon = ({ uuid, url }: { uuid: string; url: string }): Promise => { + // 根据url缓存,防止重复下载 + return cacheInstance.tx(`favicon-url:${url}`, async (val: FaviconFile | undefined, tx) => { + if (val) { + return val; + } + const directoryHandle = await getFaviconFolder(uuid); + // 使用url的uuid作为文件名 + const filename = uuidv5(url, uuidv5.URL); + // 检查文件是否存在 + let fileHandle: FileSystemFileHandle | undefined; + try { + fileHandle = await directoryHandle.getFileHandle(filename); + } catch { + // 文件不存在,继续往下走 + } + if (!fileHandle) { + // 文件不存在,下载并保存 + const newFileHandle = await directoryHandle.getFileHandle(filename, { create: true }); + const response = await fetch(url); + const blob = await response.blob(); + const writable = await newFileHandle.createWritable(); + await writable.write(blob); + await writable.close(); + } + // 返回对象OPFS資料 + const ret = { dirs: ["cached_favicons", uuid], filename: filename }; + tx.set(ret); + return ret; + }); +}; + +const getFileFromOPFS = async (opfsRet: FaviconFile): Promise => { + let dirHandle = await navigator.storage.getDirectory(); + for (const dir of opfsRet.dirs) { + dirHandle = await dirHandle.getDirectoryHandle(dir); + } + const fileHandle = await dirHandle.getFileHandle(opfsRet.filename); + const file = await fileHandle.getFile(); + return file; +}; + +// 处理单个脚本的favicon +const processScriptFavicon = async (script: Script) => { + const cacheKey = `${CACHE_KEY_FAVICON}${script.uuid}`; + return { + uuid: script.uuid, + fav: await cacheInstance.getOrSet(cacheKey, async () => { + const icons = await getScriptFavicon(script.uuid); + if (icons.length === 0) return []; + + const newIcons = await Promise.all( + icons.map(async (icon) => { + let iconUrl = ""; + if (icon.icon) { + try { + const opfsRet = await loadFavicon({ uuid: script.uuid, url: icon.icon }); + const cacheKey = `${opfsRet.dirs.join("/")}/${opfsRet.filename}`; + iconUrl = blobCaches.get(cacheKey) || ""; + if (!iconUrl) { + const file = await getFileFromOPFS(opfsRet); + iconUrl = URL.createObjectURL(file); + blobCaches.set(cacheKey, iconUrl); + } + } catch (_) { + // ignored + } + } + return { + match: icon.match, + website: icon.website, + icon: iconUrl, + }; + }) + ); + return newIcons; + }), + }; +}; + +type FavIconResult = { + uuid: string; + fav: { + match: string; + website?: string; + icon?: string; + }[]; +}; + +// 处理favicon加载,以批次方式处理 +export const loadScriptFavicons = async function* (scripts: Script[]) { + const stack: any[] = []; + const asyncWaiter: { promise?: any; resolve?: any } = {}; + const createPromise = () => { + asyncWaiter.promise = new Promise<{ chunkResults: FavIconResult[]; pendingCount: number }>((resolve) => { + asyncWaiter.resolve = resolve; + }); + }; + createPromise(); + let pendingCount = scripts.length; + if (!pendingCount) return; + const results: FavIconResult[] = []; + let waiting = false; + for (const script of scripts) { + processScriptFavicon(script).then((result: FavIconResult) => { + results.push(result); + // 下一个 MacroTask 执行。 + // 使用 requestAnimationFrame 而非setTimeout 是因为前台才要显示。而且网页绘画中时会延后这个 + if (!waiting) { + requestAnimationFrame(() => { + waiting = false; + const chunkResults: FavIconResult[] = results.slice(0); + results.length = 0; + pendingCount -= chunkResults.length; + stack.push({ chunkResults, pendingCount } as { chunkResults: FavIconResult[]; pendingCount: number }); + asyncWaiter.resolve(); + }); + waiting = true; + } + }); + } + while (true) { + await asyncWaiter.promise; + while (stack.length) { + yield stack.shift(); + } + if (pendingCount <= 0) break; + createPromise(); + } +}; diff --git a/src/pages/store/utils.ts b/src/pages/store/utils.ts deleted file mode 100644 index 61f1c7b57..000000000 --- a/src/pages/store/utils.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type { Script } from "@App/app/repo/scripts"; -import { extractFavicons } from "@App/pkg/utils/favicon"; -import { cacheInstance } from "@App/app/cache"; -import { SystemClient } from "@App/app/service/service_worker/client"; -import { message } from "./global"; -import { CACHE_KEY_FAVICON } from "@App/app/cache_key"; - -// 处理单个脚本的favicon -const processScriptFavicon = async (script: Script) => { - const cacheKey = `${CACHE_KEY_FAVICON}${script.uuid}`; - return { - uuid: script.uuid, - fav: await cacheInstance.getOrSet(cacheKey, async () => { - const icons = await extractFavicons(script.metadata!.match || [], script.metadata!.include || []); - if (icons.length === 0) return []; - - // 从缓存中获取favicon图标 - const systemClient = new SystemClient(message); - const newIcons = await Promise.all( - icons.map(async (icon) => { - let iconUrl = ""; - // 没有的话缓存到本地使用URL.createObjectURL - if (icon.icon) { - try { - // 因为需要持久化URL.createObjectURL,所以需要通过调用到offscreen来创建 - iconUrl = await systemClient.loadFavicon(icon.icon); - } catch (_) { - // ignored - } - } - return { - match: icon.match, - website: icon.website, - icon: iconUrl, - }; - }) - ); - return newIcons; - }), - }; -}; - -type FavIconResult = { - uuid: string; - fav: { - match: string; - website?: string; - icon?: string; - }[]; -}; - -// 在scriptSlice创建后处理favicon加载,以批次方式处理 -export const loadScriptFavicons = async function* (scripts: Script[]) { - const stack: any[] = []; - const asyncWaiter: { promise?: any; resolve?: any } = {}; - const createPromise = () => { - asyncWaiter.promise = new Promise<{ chunkResults: FavIconResult[]; pendingCount: number }>((resolve) => { - asyncWaiter.resolve = resolve; - }); - }; - createPromise(); - let pendingCount = scripts.length; - if (!pendingCount) return; - const results: FavIconResult[] = []; - let waiting = false; - for (const script of scripts) { - processScriptFavicon(script).then((result: FavIconResult) => { - results.push(result); - // 下一个 MacroTask 执行。 - // 使用 requestAnimationFrame 而非setTimeout 是因为前台才要显示。而且网页绘画中时会延后这个 - if (!waiting) { - requestAnimationFrame(() => { - waiting = false; - const chunkResults: FavIconResult[] = results.slice(0); - results.length = 0; - pendingCount -= chunkResults.length; - stack.push({ chunkResults, pendingCount } as { chunkResults: FavIconResult[]; pendingCount: number }); - asyncWaiter.resolve(); - }); - waiting = true; - } - }); - } - while (true) { - await asyncWaiter.promise; - while (stack.length) { - yield stack.shift(); - } - if (pendingCount <= 0) break; - createPromise(); - } -}; diff --git a/src/pkg/utils/favicon.ts b/src/pkg/utils/favicon.ts deleted file mode 100644 index 134f4f85d..000000000 --- a/src/pkg/utils/favicon.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { type TMsgResponse } from "@App/app/service/service_worker/utils"; -import { systemClient } from "@App/pages/store/global"; - -/** - * 从脚本的@match和@include规则中提取favicon图标 - * @param matche match规则数组 - * @param include include规则数组 - * @returns favicon URL数组 - */ -export async function extractFavicons( - matche: string[] = [], - include: string[] = [] -): Promise<{ match: string; website: string; icon?: string }[]> { - // 提取域名 - const domains = new Map(); - - // 处理match和include规则 - for (const pattern of [...matche, ...include]) { - const domain = extractDomainFromPattern(pattern); - if (domain) { - // 使用match作为key,避免重复 - domains.set(domain, { match: pattern, domain }); - } else { - // 如果无法提取域名,仍然保留原始pattern - domains.set(pattern, { match: pattern, domain: "" }); - } - } - - // 将Map转换为数组并去重 - const uniqueDomains = Array.from(domains.values()); - - // 获取favicon - const faviconUrls = new Array<{ match: string; website: string; icon: string }>(); - - // 并发获取favicon - const fetchPromises = uniqueDomains.map(async (domain) => { - try { - if (domain.domain) { - const icons = await getFaviconFromDomain(domain.domain); - if (icons.length > 0) { - faviconUrls.push({ match: domain.match, website: "http://" + domain.domain, icon: icons[0] }); - } else { - faviconUrls.push({ match: domain.match, website: "http://" + domain.domain, icon: "" }); - } - } else { - faviconUrls.push({ match: domain.match, website: "", icon: "" }); - } - } catch (error) { - console.error(`Failed to fetch favicon for ${domain.domain || domain.match}:`, error); - } - }); - // 等待所有favicon获取完成 - await Promise.all(fetchPromises); - - return faviconUrls.slice(); -} - -/** - * 从URL模式中提取域名 - */ -function extractDomainFromPattern(pattern: string): string | null { - try { - // 处理match模式: scheme://host/path - const matchPattern = /^(http|https|\*):\/\/([^/]+)(?:\/(.*))?$/; - const matches = pattern.match(matchPattern); - - if (matches) { - let host = matches[2]; - - // 删除最后的* - // 例如 "example.com*" 变为 "example.com" - while (host.endsWith("*")) { - host = host.slice(0, -1); - } - - // 删除 * 通配符 - // 例如 "*.example.com" 变为 "example.com" - // a.*.example.com 变为 "example.com" - while (host.includes("*")) { - // 从最后一个 * 开始删除 - const lastAsteriskIndex = host.lastIndexOf("*"); - host = host.slice(lastAsteriskIndex + 1); - } - - // 删除第一个. - // 例如 ".example.com" 变为 "example.com" - while (host.startsWith(".")) { - host = host.slice(1); - } - - return host; - } - - // 尝试作为URL解析 - if (pattern.startsWith("http://") || pattern.startsWith("https://")) { - const url = new URL(pattern); - return url.hostname; - } - - // 尝试匹配域名格式 - const domainMatch = pattern.match(/([a-z0-9][-a-z0-9]*\.)+[a-z0-9][-a-z0-9]*/i); - return domainMatch ? domainMatch[0] : null; - } catch { - return null; - } -} - -const localFavIconPromises = new Map>(); - -const makeError = (e: any) => { - const { name } = e; - const o = { - [name]: class extends Error { - constructor(message: any) { - super(message); - this.name = name; - } - }, - }; - return new o[name](e.message); -}; - -function getFaviconFromDomain(domain: string): Promise { - let retPromise = localFavIconPromises.get(domain); - if (retPromise) return retPromise; - retPromise = systemClient.getFaviconFromDomain(domain).then((r: TMsgResponse) => { - if (r.ok) { - return r.res!; - } - const error = r.err!; - if (error.errType === 11) { - // 網絡錯誤 - console.log(`${error.message}`); - } else if (error.errType === 12) { - // 超时 - console.log(`${error.message}`); - } else { - // 其他错误 - console.error(makeError(error)); - } - return []; - }); - localFavIconPromises.set(domain, retPromise); - return retPromise; -} diff --git a/src/service_worker.ts b/src/service_worker.ts index 853d503a9..62ff4d1c4 100644 --- a/src/service_worker.ts +++ b/src/service_worker.ts @@ -7,9 +7,6 @@ import { Server } from "@Packages/message/server"; import { MessageQueue } from "@Packages/message/message_queue"; import { ServiceWorkerMessageSend } from "@Packages/message/window_message"; import migrate, { migrateChromeStorage } from "./app/migrate"; -import { fetchIconByDomain } from "./app/service/service_worker/fetch"; -import { msgResponse } from "./app/service/service_worker/utils"; -import type { RuntimeMessageSender } from "@Packages/message/types"; import { cleanInvalidKeys } from "./app/repo/resource"; migrate(); @@ -80,35 +77,4 @@ function main() { setupOffscreenDocument(); } -const apiActions: { - [key: string]: (message: any, _sender: RuntimeMessageSender) => Promise | any; -} = { - async "fetch-icon-by-domain"(message: any, _sender: RuntimeMessageSender) { - const { domain } = message; - return await fetchIconByDomain(domain); - }, -}; - -chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - const f = apiActions[req.message ?? ""]; - if (f) { - let res; - try { - res = f(req, sender); - } catch (e: any) { - sendResponse(msgResponse(1, e)); - return false; - } - if (typeof res?.then === "function") { - res.then(sendResponse).catch((e: Error) => { - sendResponse(msgResponse(1, e)); - }); - return true; - } else { - sendResponse(msgResponse(0, res)); - return false; - } - } -}); - main(); From b0ea187c2e6d69b60c981aa9b4d068fed7c2c2a2 Mon Sep 17 00:00:00 2001 From: wangyizhi Date: Mon, 3 Nov 2025 13:44:47 +0800 Subject: [PATCH 04/35] =?UTF-8?q?=E2=9C=A8=20=E8=84=9A=E6=9C=AC=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E6=9C=9F=E9=80=89=E9=A1=B9=20(#895)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip: 设置 * 运行时机 * 根据copilot意见修改 * 处理翻译 --- src/locales/ach-UG/translation.json | 21 +-- src/locales/de-DE/translation.json | 9 +- src/locales/en-US/translation.json | 9 +- src/locales/ja-JP/translation.json | 9 +- src/locales/ru-RU/translation.json | 9 +- src/locales/vi-VN/translation.json | 9 +- src/locales/zh-CN/translation.json | 9 +- src/locales/zh-TW/translation.json | 9 +- src/pages/components/ScriptSetting/index.tsx | 134 ++++++++++++------ .../options/routes/script/ScriptEditor.tsx | 64 +++++---- 10 files changed, 184 insertions(+), 98 deletions(-) diff --git a/src/locales/ach-UG/translation.json b/src/locales/ach-UG/translation.json index 63083137c..fcde99459 100644 --- a/src/locales/ach-UG/translation.json +++ b/src/locales/ach-UG/translation.json @@ -191,7 +191,6 @@ "yes": "crwdns8258:0crwdne8258:0", "no": "crwdns8260:0crwdne8260:0", "confirm_delete_permission": "crwdns8262:0crwdne8262:0", - "script_setting": "crwdns8264:0crwdne8264:0", "basic_info": "crwdns8266:0crwdne8266:0", "update_url": "crwdns8268:0crwdne8268:0", "permission_management": "crwdns8270:0crwdne8270:0", @@ -384,12 +383,12 @@ "guide_script_list_enable_content": "crwdns8570:0crwdne8570:0", "guide_script_list_apply_to_run_status_title": "crwdns8572:0crwdne8572:0", "guide_script_list_apply_to_run_status_content": "crwdns8574:0crwdne8574:0", - "guide_script_list_sort_title": "crwdns12858:0crwdne12858:0", - "guide_script_list_sort_content": "crwdns12860:0crwdne12860:0", - "guide_script_list_update_title": "crwdns12862:0crwdne12862:0", - "guide_script_list_update_content": "crwdns12864:0crwdne12864:0", - "guide_script_list_action_title": "crwdns12866:0crwdne12866:0", - "guide_script_list_action_content": "crwdns12868:0crwdne12868:0", + "guide_script_list_sort_title": "crwdns12878:0crwdne12878:0", + "guide_script_list_sort_content": "crwdns12880:0crwdne12880:0", + "guide_script_list_update_title": "crwdns12882:0crwdne12882:0", + "guide_script_list_update_content": "crwdns12884:0crwdne12884:0", + "guide_script_list_action_title": "crwdns12886:0crwdne12886:0", + "guide_script_list_action_content": "crwdns12888:0crwdne12888:0", "guide_tools_title": "crwdns8580:0crwdne8580:0", "guide_tools_content": "crwdns8582:0crwdne8582:0", "guide_tools_backup_title": "crwdns8584:0crwdne8584:0", @@ -489,11 +488,17 @@ "enter_search_value": "crwdns10806:0{{search}}crwdne10806:0", "script_run_env": { "title": "crwdns10808:0crwdne10808:0", - "default": "crwdns10810:0crwdne10810:0", "all": "crwdns10812:0crwdne10812:0", "normal-tabs": "crwdns10814:0crwdne10814:0", "incognito-tabs": "crwdns10816:0crwdne10816:0" }, + "script_run_at": { + "title": "crwdns12876:0crwdne12876:0" + }, + "script_setting": { + "title": "crwdns12890:0crwdne12890:0", + "default": "crwdns12892:0crwdne12892:0" + }, "editor_config": "crwdns10818:0crwdne10818:0", "editor_config_description": "crwdns10820:0crwdne10820:0", "editor_type_definition": "crwdns10822:0crwdne10822:0", diff --git a/src/locales/de-DE/translation.json b/src/locales/de-DE/translation.json index 3ce88d077..60adae2e2 100644 --- a/src/locales/de-DE/translation.json +++ b/src/locales/de-DE/translation.json @@ -191,7 +191,6 @@ "yes": "Ja", "no": "Nein", "confirm_delete_permission": "Diese Autorisierung löschen bestätigen?", - "script_setting": "Skript-Einstellungen", "basic_info": "Grundinformationen", "update_url": "Update-URL", "permission_management": "Berechtigungsverwaltung", @@ -489,11 +488,17 @@ "enter_search_value": "Bitte geben Sie {{search}} für die Suche ein", "script_run_env": { "title": "Laufzeitumgebung", - "default": "default", "all": "Alle Bezeichnungen", "normal-tabs": "Normale Tabs", "incognito-tabs": "Inkognito-Tabs" }, + "script_run_at": { + "title": "Ausführungszeitpunkt" + }, + "script_setting": { + "title": "Skript-Einstellungen", + "default": "default" + }, "editor_config": "Editor-Konfiguration", "editor_config_description": "Sie können sich an den compilerOptions in jsconfig.js orientieren, um die Konfiguration vorzunehmen", "editor_type_definition": "Editor-Typdefinitionen", diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index efa5c867c..275b34980 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -191,7 +191,6 @@ "yes": "Yes", "no": "No", "confirm_delete_permission": "Confirm deletion of this authorization?", - "script_setting": "Script Setting", "basic_info": "Basic Information", "update_url": "Update URL", "permission_management": "Permission Management", @@ -489,11 +488,17 @@ "enter_search_value": "Enter {{search}} to search", "script_run_env": { "title": "Operating environment", - "default": "default", "all": "All", "normal-tabs": "Normal tags", "incognito-tabs": "Incognito tags" }, + "script_run_at": { + "title": "Run Timing" + }, + "script_setting": { + "title": "Script Setting", + "default": "default" + }, "editor_config": "Editor Configuration", "editor_config_description": "You can refer to the compilerOptions in jsconfig.js for configuration", "editor_type_definition": "Editor Type Definition", diff --git a/src/locales/ja-JP/translation.json b/src/locales/ja-JP/translation.json index 0791b0bd1..82cd4a866 100644 --- a/src/locales/ja-JP/translation.json +++ b/src/locales/ja-JP/translation.json @@ -191,7 +191,6 @@ "yes": "はい", "no": "いいえ", "confirm_delete_permission": "この認証を削除してもよろしいですか?", - "script_setting": "スクリプト設定", "basic_info": "基本情報", "update_url": "URLを更新", "permission_management": "権限管理", @@ -489,11 +488,17 @@ "enter_search_value": "{{search}}を入力して検索してください", "script_run_env": { "title": "実行環境", - "default": "デフォルト", "all": "すべてのタブ", "normal-tabs": "通常のタブ", "incognito-tabs": "シークレットタブ" }, + "script_run_at": { + "title": "実行タイミング" + }, + "script_setting": { + "title": "スクリプト設定", + "default": "デフォルト" + }, "editor_config": "エディタ設定", "editor_config_description": "jsconfig.jsのcompilerOptionsを参考に設定できます", "editor_type_definition": "エディタ型定義", diff --git a/src/locales/ru-RU/translation.json b/src/locales/ru-RU/translation.json index 666c22b64..bd68a10c1 100644 --- a/src/locales/ru-RU/translation.json +++ b/src/locales/ru-RU/translation.json @@ -191,7 +191,6 @@ "yes": "Да", "no": "Нет", "confirm_delete_permission": "Подтвердить удаление этого разрешения?", - "script_setting": "Настройки скрипта", "basic_info": "Основная информация", "update_url": "URL обновления", "permission_management": "Управление разрешениями", @@ -489,11 +488,17 @@ "enter_search_value": "Введите {{search}} для поиска", "script_run_env": { "title": "Среда выполнения", - "default": "По умолчанию", "all": "Все вкладки", "normal-tabs": "Обычные вкладки", "incognito-tabs": "Приватные вкладки" }, + "script_run_at": { + "title": "Время выполнения" + }, + "script_setting": { + "title": "Настройки скрипта", + "default": "По умолчанию" + }, "editor_config": "Конфигурация редактора", "editor_config_description": "Вы можете настроить compilerOptions, ссылаясь на jsconfig.js", "editor_type_definition": "Определения типов редактора", diff --git a/src/locales/vi-VN/translation.json b/src/locales/vi-VN/translation.json index 1fe73fab4..54dcbd812 100644 --- a/src/locales/vi-VN/translation.json +++ b/src/locales/vi-VN/translation.json @@ -191,7 +191,6 @@ "yes": "Có", "no": "Không", "confirm_delete_permission": "Xác nhận xóa ủy quyền này?", - "script_setting": "Cài đặt script", "basic_info": "Thông tin cơ bản", "update_url": "Url cập nhật", "permission_management": "Quản lý quyền", @@ -489,11 +488,17 @@ "enter_search_value": "Nhập {{search}} để tìm kiếm", "script_run_env": { "title": "Môi trường chạy", - "default": "Mặc định", "all": "Tất cả tab", "normal-tabs": "Tab thường", "incognito-tabs": "Tab ẩn danh" }, + "script_run_at": { + "title": "Thời điểm chạy" + }, + "script_setting": { + "title": "Cài đặt script", + "default": "Mặc định" + }, "editor_config": "Cấu hình trình soạn thảo", "editor_config_description": "Bạn có thể tham khảo compilerOptions trong jsconfig.js để cấu hình", "editor_type_definition": "Định nghĩa kiểu trình soạn thảo", diff --git a/src/locales/zh-CN/translation.json b/src/locales/zh-CN/translation.json index 9625441f9..23bc852f7 100644 --- a/src/locales/zh-CN/translation.json +++ b/src/locales/zh-CN/translation.json @@ -191,7 +191,6 @@ "yes": "是", "no": "否", "confirm_delete_permission": "确认删除该授权?", - "script_setting": "脚本设置", "basic_info": "基本信息", "update_url": "更新URL", "permission_management": "授权管理", @@ -489,11 +488,17 @@ "enter_search_value": "请输入 {{search}} 进行搜索", "script_run_env": { "title": "运行环境", - "default": "默认", "all": "所有标签", "normal-tabs": "普通标签", "incognito-tabs": "隐身标签" }, + "script_run_at": { + "title": "运行时机" + }, + "script_setting": { + "title": "脚本设置", + "default": "默认" + }, "editor_config": "编辑器配置", "editor_config_description": "你可以参考jsconfig.js中的compilerOptions进行配置", "editor_type_definition": "编辑器类型定义", diff --git a/src/locales/zh-TW/translation.json b/src/locales/zh-TW/translation.json index 739229363..fe39df94f 100644 --- a/src/locales/zh-TW/translation.json +++ b/src/locales/zh-TW/translation.json @@ -191,7 +191,6 @@ "yes": "是", "no": "否", "confirm_delete_permission": "確定要刪除此授權嗎?", - "script_setting": "腳本設定", "basic_info": "基本資訊", "update_url": "更新URL", "permission_management": "授權管理", @@ -489,11 +488,17 @@ "enter_search_value": "請輸入 {{search}} 進行搜尋", "script_run_env": { "title": "運作環境", - "default": "預設", "all": "所有標籤", "normal-tabs": "普通標籤", "incognito-tabs": "隱身標籤" }, + "script_run_at": { + "title": "運行時機" + }, + "script_setting": { + "title": "腳本設定", + "default": "預設" + }, "editor_config": "編輯器設定", "editor_config_description": "你可以參考jsconfig.js中的compilerOptions進行配置", "editor_type_definition": "編輯器類型定義", diff --git a/src/pages/components/ScriptSetting/index.tsx b/src/pages/components/ScriptSetting/index.tsx index d478fbfe8..e5eef9372 100644 --- a/src/pages/components/ScriptSetting/index.tsx +++ b/src/pages/components/ScriptSetting/index.tsx @@ -3,7 +3,7 @@ import { ScriptDAO } from "@App/app/repo/scripts"; import { formatUnixTime } from "@App/pkg/utils/day_format"; import { Checkbox, Descriptions, Divider, Drawer, Input, InputTag, Message, Select, Tag } from "@arco-design/web-react"; import type { ReactNode } from "react"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import Match from "./Match"; import PermissionManager from "./Permission"; @@ -29,30 +29,93 @@ const ScriptSetting: React.FC<{ onOk: () => void; onCancel: () => void; }> = ({ script, visible, onCancel, onOk }) => { - const scriptDAO = new ScriptDAO(); const [scriptTags, setScriptTags] = useState([]); const [checkUpdateUrl, setCheckUpdateUrl] = useState(""); const [checkUpdate, setCheckUpdate] = useState(false); const [scriptRunEnv, setScriptRunEnv] = useState("all"); + const [scriptRunAt, setScriptRunAt] = useState("default"); const { t } = useTranslation(); + const scriptSettingData = useMemo(() => { + const ret = [ + { + label: t("script_run_env.title"), + value: ( + { + setScriptRunAt(value); + const earlyStart: string[] = []; + const runAt: string[] = []; + if (value === "early-start") { + earlyStart.push(""); + runAt.push("document-start"); + } else if (value !== "default") { + runAt.push(value); + } + Promise.all([ + scriptClient.updateMetadata(script.uuid, "early-start", earlyStart), + scriptClient.updateMetadata(script.uuid, "run-at", runAt), + ]).then(() => { + Message.success(t("update_success")); + }); + }} + /> + ), + }, + ]; + return ret; + }, [script, scriptRunEnv, scriptRunAt, t]); + useEffect(() => { - if (script) { - scriptDAO.get(script.uuid).then((v) => { - if (!v) { - return; - } - setCheckUpdateUrl(v.downloadUrl || ""); - setCheckUpdate(v.checkUpdate === false ? false : true); - let metadata = v.metadata; - if (v.selfMetadata) { - metadata = getCombinedMeta(metadata, v.selfMetadata); - } - setScriptRunEnv(metadata["run-in"]?.[0] || "all"); - setScriptTags(parseTags(metadata) || []); - }); - } + const scriptDAO = new ScriptDAO(); + scriptDAO.get(script.uuid).then((v) => { + if (!v) { + return; + } + setCheckUpdateUrl(v.downloadUrl || ""); + setCheckUpdate(v.checkUpdate === false ? false : true); + let metadata = v.metadata; + if (v.selfMetadata) { + metadata = getCombinedMeta(metadata, v.selfMetadata); + } + setScriptRunEnv(metadata["run-in"]?.[0] || "default"); + let runAt = metadata["run-at"]?.[0] || "default"; + if (runAt === "document-start" && metadata["early-start"] && metadata["early-start"].length > 0) { + runAt = "early-start"; + } + setScriptRunAt(runAt); + setScriptTags(parseTags(metadata) || []); + }); }, [script]); return ( @@ -60,7 +123,7 @@ const ScriptSetting: React.FC<{ width={600} title={ - {script?.name} {t("script_setting")} + {script.name} {t("script_setting.title")} } autoFocus={false} @@ -79,11 +142,11 @@ const ScriptSetting: React.FC<{ data={[ { label: t("last_updated"), - value: formatUnixTime((script?.updatetime || script?.createtime || 0) / 1000), + value: formatUnixTime((script.updatetime || script.createtime || 0) / 1000), }, { label: "UUID", - value: script?.uuid, + value: script.uuid, }, { label: t("tags"), @@ -97,7 +160,7 @@ const ScriptSetting: React.FC<{ onChange={(tags) => { setScriptTags(tags); scriptClient.updateMetadata(script.uuid, "tag", tags).then(() => { - Message.success(t("update_success")!); + Message.success(t("update_success")); }); }} /> @@ -110,29 +173,8 @@ const ScriptSetting: React.FC<{ { - setScriptRunEnv(value); - scriptClient.updateMetadata(script.uuid, "run-in", value === "default" ? [] : [value]).then(() => { - Message.success(t("update_success")!); - }); - }} - /> - ), - }, - ]} + title={t("script_setting.title")} + data={scriptSettingData} style={{ marginBottom: 20 }} labelStyle={{ paddingRight: 36 }} /> @@ -150,7 +192,7 @@ const ScriptSetting: React.FC<{ onChange={(val) => { setCheckUpdate(val); scriptClient.setCheckUpdateUrl(script.uuid, val, checkUpdateUrl).then(() => { - Message.success(t("update_success")!); + Message.success(t("update_success")); }); }} /> @@ -166,7 +208,7 @@ const ScriptSetting: React.FC<{ }} onBlur={() => { scriptClient.setCheckUpdateUrl(script.uuid, checkUpdate, checkUpdateUrl).then(() => { - Message.success(t("update_success")!); + Message.success(t("update_success")); }); }} /> diff --git a/src/pages/options/routes/script/ScriptEditor.tsx b/src/pages/options/routes/script/ScriptEditor.tsx index 4e095c201..84dad1c6e 100644 --- a/src/pages/options/routes/script/ScriptEditor.tsx +++ b/src/pages/options/routes/script/ScriptEditor.tsx @@ -710,36 +710,40 @@ function ScriptEditor() { }} > {contextHolder} - { - setShow("scriptStorage", false); - }} - onCancel={() => { - setShow("scriptStorage", false); - }} - /> - { - setShow("scriptResource", false); - }} - onCancel={() => { - setShow("scriptResource", false); - }} - /> - { - setShow("scriptSetting", false); - }} - onCancel={() => { - setShow("scriptSetting", false); - }} - /> + {currentScript && ( + <> + { + setShow("scriptStorage", false); + }} + onCancel={() => { + setShow("scriptStorage", false); + }} + /> + { + setShow("scriptResource", false); + }} + onCancel={() => { + setShow("scriptResource", false); + }} + /> + { + setShow("scriptSetting", false); + }} + onCancel={() => { + setShow("scriptSetting", false); + }} + /> + + )}
Date: Mon, 3 Nov 2025 14:49:35 +0900 Subject: [PATCH 05/35] =?UTF-8?q?=E2=9C=A8=20=E9=98=B2=E6=AD=A2=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=AE=89=E8=A3=85=E9=93=BE=E7=BB=93=E5=9B=A0=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E5=90=8D=E5=AD=97=E6=94=B9=E4=BA=86=E8=80=8C=E8=A2=AB?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E4=B8=BA=E5=AE=89=E8=A3=85=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20(#824)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/repo/scripts.test.ts | 154 ++++++++++++++++++++ src/app/repo/scripts.ts | 95 +++++++++++- src/app/service/service_worker/client.ts | 2 +- src/app/service/service_worker/script.ts | 24 ++- src/app/service/service_worker/subscribe.ts | 2 +- src/pages/install/App.tsx | 4 +- src/pkg/utils/script.ts | 21 ++- 7 files changed, 290 insertions(+), 12 deletions(-) create mode 100644 src/app/repo/scripts.test.ts diff --git a/src/app/repo/scripts.test.ts b/src/app/repo/scripts.test.ts new file mode 100644 index 000000000..54f040456 --- /dev/null +++ b/src/app/repo/scripts.test.ts @@ -0,0 +1,154 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { + ScriptDAO, + type Script, + SCRIPT_TYPE_NORMAL, + SCRIPT_STATUS_ENABLE, + SCRIPT_RUN_STATUS_COMPLETE, +} from "./scripts"; + +const baseMeta = { + name: ["测试脚本"], + namespace: ["test-namespace"], + version: ["1.0.0"], + author: ["测试作者"], + copyright: ["(c) 测试"], + grant: ["GM_xmlhttpRequest"], + match: ["https://example.com/*"], + license: ["MIT"], +}; + +const makeBaseScript = (overrides: Partial - - <% } %> diff --git a/src/pages/popup.html b/src/pages/popup.html index a8149372f..dfccc4a64 100644 --- a/src/pages/popup.html +++ b/src/pages/popup.html @@ -23,8 +23,4 @@
- <% if rspackConfig.mode=="script" { %> - - - <% } %> diff --git a/src/pages/template.html b/src/pages/template.html index ace1929e8..89a30c56d 100644 --- a/src/pages/template.html +++ b/src/pages/template.html @@ -22,8 +22,4 @@
- <% if rspackConfig.mode=="script" { %> - - - <% } %> From 70f67b6bd8cf803d7a18bf26fdccdfa6f8a92893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E4=B8=80=E4=B9=8B?= Date: Thu, 20 Nov 2025 15:18:38 +0800 Subject: [PATCH 24/35] =?UTF-8?q?=F0=9F=8E=A8=20=E6=89=A9=E5=B1=95?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E6=98=BE=E7=A4=BA=E6=95=B0=E5=AD=97=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E4=BF=AE=E6=94=B9=E4=B8=BA=E8=84=9A=E6=9C=AC=E6=95=B0?= =?UTF-8?q?=E9=87=8F=20#989?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/config/config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pkg/config/config.ts b/src/pkg/config/config.ts index 9d7c8e022..22ef14901 100644 --- a/src/pkg/config/config.ts +++ b/src/pkg/config/config.ts @@ -460,7 +460,7 @@ export class SystemConfig { } getBadgeNumberType() { - return this._get<"none" | "run_count" | "script_count">("badge_number_type", "run_count"); + return this._get<"none" | "run_count" | "script_count">("badge_number_type", "script_count"); } setBadgeBackgroundColor(color: string) { From 0efc648257f74591765869dedee5d98f8a1dc610 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:23:04 +0900 Subject: [PATCH 25/35] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20parseMetadata=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=20(#903)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pkg/utils/script.test.ts | 235 +++++++++++++++++++++++++++++++++++ src/pkg/utils/script.ts | 62 ++++----- 2 files changed, 261 insertions(+), 36 deletions(-) diff --git a/src/pkg/utils/script.test.ts b/src/pkg/utils/script.test.ts index 86be9dd54..4640998b7 100644 --- a/src/pkg/utils/script.test.ts +++ b/src/pkg/utils/script.test.ts @@ -107,6 +107,8 @@ console.log('Hello World'); const result = parseMetadata(code); expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); expect(result?.description).toEqual([""]); expect(result?.author).toEqual([""]); }); @@ -133,6 +135,8 @@ console.log('Hello World'); const result = parseMetadata(code); expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); expect(result?.match).toEqual([ "https://example.org/*", "https://test.com/*", @@ -171,6 +175,8 @@ console.log('Hello World'); const result = parseMetadata(code); expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); expect(result?.match).toEqual([ "https://example.org/*", "https://test.com/*", @@ -215,6 +221,8 @@ console.log('Hello World'); const result = parseMetadata(code); expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); expect(result?.match).toEqual([ "https://example.org/*", "https://test.com/*", @@ -259,17 +267,244 @@ console.log('Hello World'); const result = parseMetadata(code); expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual([ + "https://example.org/*", + "https://test.com/*", + "https://demo.com/*", + "https://example.com/*", + ]); + expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); + expect(result?.description).toEqual([""]); + expect(result?.author).toEqual([""]); + }); + + it.concurrent("正確解析元数据(換行空白1)", () => { + const code = ` +// ==UserScript== +// @name 测试脚本 +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// @match https://test.com/* +// @match https://demo.com/* +// @version 1.0.0 +// @description +// @early-start +// @author +// @match https://example.com/* +// @grant + GM_setValue +// @grant GM_getValue +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); expect(result?.match).toEqual([ "https://example.org/*", "https://test.com/*", "https://demo.com/*", "https://example.com/*", ]); + expect(result?.["early-start"]).toEqual([""]); + expect(result?.grant).toEqual(["", "GM_getValue"]); + expect(result?.description).toEqual([""]); + expect(result?.author).toEqual([""]); + }); + + it.concurrent("正確解析元数据(換行空白2)", () => { + const code = ` +// ==UserScript== +// @name 测试脚本 +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// @match https://test.com/* +// +@match https://demo.com/* +// @version 1.0.0 +// @description +// @early-start +// @author +// @match https://example.com/* +// @grant GM_setValue +// @grant GM_getValue +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual(["https://example.org/*", "https://test.com/*", "https://example.com/*"]); + expect(result?.["early-start"]).toEqual([""]); + expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); + expect(result?.description).toEqual([""]); + expect(result?.author).toEqual([""]); + }); + + it.concurrent("正確解析元数据(換行空白3)", () => { + const code = ` +// ==UserScript== +// @name 测试脚本 +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// match https://test.com/* +// match https://demo.com/* +// @version 1.0.0 +// @description +// +@early-start +// @author +// @match https://example.com/* +// @grant GM_setValue +// @grant GM_getValue +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual(["https://example.org/*", "https://example.com/*"]); + expect(result?.["early-start"]).toEqual(undefined); expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); expect(result?.description).toEqual([""]); expect(result?.author).toEqual([""]); }); + it.concurrent("忽略非元数据的注釋", () => { + const code = ` +/* +Copyright + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +// The above is The MIT License. +// ==UserScript== +// Ignore me please +// @name 测试脚本 +// I am a comment +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// match https://test.com/* +// match https://demo.com/* +// @version 1.0.0 +// ------------------------------------------------- +// @description This is Description +// ------------------------------------------------- +// 不要使用 @early-start +// @author +// @match https://example.com/* +// +// @grant GM_setValue +// @grant GM_getValue +// +// This is just a comment. +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual(["https://example.org/*", "https://example.com/*"]); + expect(result?.["early-start"]).toEqual(undefined); + expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); + expect(result?.description).toEqual(["This is Description"]); + expect(result?.author).toEqual([""]); + }); + + it.concurrent("兼容TM: 可不包含空白開首(1)", () => { + const code = ` +//==UserScript== +//@name 测试脚本 +//@namespace http://tampermonkey.net/ +// @match https://example.org/* +// @match https://test.com/* +// @match https://demo.com/* +// @version 1.0.0 +// ------------------------------------------------- +// @description This is Description +// ------------------------------------------------- +// 不要使用 @early-start +// @author +// @match https://example.com/* +// +// @grant GM_setValue +// @grant GM_getValue +// +// This is just a comment. +// ==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual([ + "https://example.org/*", + "https://test.com/*", + "https://demo.com/*", + "https://example.com/*", + ]); + expect(result?.["early-start"]).toEqual(undefined); + expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); + expect(result?.description).toEqual(["This is Description"]); + expect(result?.author).toEqual([""]); + }); + + it.concurrent("兼容TM: 可不包含空白開首(2)", () => { + const code = ` +// ==UserScript== +// @name 测试脚本 +// @namespace http://tampermonkey.net/ +// @match https://example.org/* +// @match https://test.com/* +// @match https://demo.com/* +// @version 1.0.0 +// ------------------------------------------------- +// @description This is Description +// ------------------------------------------------- +// 不要使用 @early-start +// @author +// @match https://example.com/* +// +//@grant GM_setValue +// @grant GM_getValue +// +//This is just a comment. +//==/UserScript== +console.log('Hello World'); +`; + + const result = parseMetadata(code); + expect(result).not.toBeNull(); + expect(result?.name).toEqual(["测试脚本"]); + expect(result?.namespace).toEqual(["http://tampermonkey.net/"]); + expect(result?.match).toEqual([ + "https://example.org/*", + "https://test.com/*", + "https://demo.com/*", + "https://example.com/*", + ]); + expect(result?.["early-start"]).toEqual(undefined); + expect(result?.grant).toEqual(["GM_setValue", "GM_getValue"]); + expect(result?.description).toEqual(["This is Description"]); + expect(result?.author).toEqual([""]); + }); + it.concurrent("缺少name字段应返回null", () => { const code = ` // ==UserScript== diff --git a/src/pkg/utils/script.ts b/src/pkg/utils/script.ts index 60272dc27..b498976e0 100644 --- a/src/pkg/utils/script.ts +++ b/src/pkg/utils/script.ts @@ -16,45 +16,32 @@ import { nextTime } from "./cron"; import { parseUserConfig } from "./yaml"; import { t as i18n_t } from "@App/locales/locales"; +const HEADER_BLOCK = /\/\/[ \t]*==User(Script|Subscribe)==([\s\S]+?)\/\/[ \t]*==\/User\1==/m; +const META_LINE = /\/\/[ \t]*@(\S+)[ \t]*(.*)$/gm; + // 从脚本代码抽出Metadata export function parseMetadata(code: string): SCMetadata | null { - let issub = false; - let regex = /\/\/\s*==UserScript==([\s\S]+?)\/\/\s*==\/UserScript==/m; - let header = regex.exec(code); - if (!header) { - regex = /\/\/\s*==UserSubscribe==([\s\S]+?)\/\/\s*==\/UserSubscribe==/m; - header = regex.exec(code); - if (!header) { - return null; - } - issub = true; - } - regex = /\/\/\s*@(\S+)((.+?)$|$)/gm; - const ret = {} as SCMetadata; - let meta: RegExpExecArray | null = regex.exec(header[1]); - while (meta !== null) { - const [key, val] = [meta[1].toLowerCase().trim(), meta[2].trim()]; - let values = ret[key]; - if (!values) { - values = []; - } - values.push(val); - ret[key] = values; - meta = regex.exec(header[1]); - } - if (ret.name === undefined) { - return null; - } - if (Object.keys(ret).length < 3) { + let isSubscribe = false; + let headerContent: string; + let m: RegExpExecArray | null; + if ((m = HEADER_BLOCK.exec(code))) { + isSubscribe = m[1] === "Subscribe"; + headerContent = m[2]; + } else { return null; } - if (!ret.namespace) { - ret.namespace = [""]; - } - if (issub) { - ret.usersubscribe = []; + const metadata: SCMetadata = {} as SCMetadata; + META_LINE.lastIndex = 0; // 重置正则表达式的lastIndex(用于复用) + while ((m = META_LINE.exec(headerContent)) !== null) { + const key = m[1].toLowerCase(); + const val = m[2]?.trim() ?? ""; + const values = metadata[key] || (metadata[key] = []); + values.push(val); } - return ret; + if (!metadata.name || Object.keys(metadata).length < 3) return null; + if (!metadata.namespace) metadata.namespace = [""]; + if (isSubscribe) metadata.usersubscribe = []; + return metadata; } // 从网址取得脚本代码 @@ -92,12 +79,15 @@ export async function prepareScriptByCode( if (!metadata) { throw new Error(i18n_t("error_metadata_invalid")); } - if (metadata.name === undefined) { + // 不接受空白name + if (!metadata.name?.[0]) { throw new Error(i18n_t("error_script_name_required")); } - if (metadata.version === undefined) { + // 不接受空白version + if (!metadata.version?.[0]) { throw new Error(i18n_t("error_script_version_required")); } + // 可接受空白namespace if (metadata.namespace === undefined) { throw new Error(i18n_t("error_script_namespace_required")); } From 9c149ce5999b7a70375a41c6604c8e8dbd19e9df Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:13:14 +0900 Subject: [PATCH 26/35] =?UTF-8?q?=F0=9F=90=9B=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E5=A4=87=E4=BB=BD=E9=A1=B5=E9=9D=A2=E9=A1=B6=E9=83=A8=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E7=A9=BA=E7=99=BD=20(#995)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/options/routes/Tools.tsx | 550 +++++++++++++++-------------- 1 file changed, 276 insertions(+), 274 deletions(-) diff --git a/src/pages/options/routes/Tools.tsx b/src/pages/options/routes/Tools.tsx index 0283ffe0b..6c0ddcd84 100644 --- a/src/pages/options/routes/Tools.tsx +++ b/src/pages/options/routes/Tools.tsx @@ -38,303 +38,305 @@ function Tools() { const [vscodeReconnect, setVscodeReconnect, submitVscodeReconnect] = useSystemConfig("vscode_reconnect"); return ( - - {contextHolder} - - - {t("local")} - - - - - - {t("cloud")} - { - setBackup({ ...backup, filesystem: type }); - }} - onChangeFileSystemParams={(params) => { - setBackup({ ...backup, params: { ...backup.params, [backup.filesystem]: params } }); - }} - actionButton={[ + <> + + + + {t("local")} + + , + {t("export_file")} + , - ]} - fileSystemType={backup.filesystem} - fileSystemParams={backup.params[backup.filesystem] || {}} - /> - - {t("backup_list")} + {t("import_file")} + + + {t("cloud")} + { + setBackup({ ...backup, filesystem: type }); + }} + onChangeFileSystemParams={(params) => { + setBackup({ ...backup, params: { ...backup.params, [backup.filesystem]: params } }); + }} + actionButton={[ + , -
- } - visible={backupFileList.length !== 0} - onOk={() => { - setBackupFileList([]); - }} - onCancel={() => { - setBackupFileList([]); - }} - > - ( - - - - , + ]} + fileSystemType={backup.filesystem} + fileSystemParams={backup.params[backup.filesystem] || {}} + /> + + {t("backup_list")} + + + } + visible={backupFileList.length !== 0} + onOk={() => { + setBackupFileList([]); + }} + onCancel={() => { + setBackupFileList([]); + }} + > + ( + + + + + - - - - )} - /> - - {t("backup_strategy")} - - { - migrateToChromeStorage(); - }} - > - - - - + }} + > + {t("delete")} + + + + )} + /> + + {t("backup_strategy")} + + { + migrateToChromeStorage(); + }} + > + + + + - - {t("development_debugging")} - - - - + { + setVscodeReconnect(checked); + }} + > + {t("auto_connect_vscode_service")} + + + + + + {contextHolder} + ); } From c78b64b0456ec33f6d54bdf4b925ae7910e2a8fd Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:34:29 +0900 Subject: [PATCH 27/35] =?UTF-8?q?=F0=9F=8E=A8=20=E8=AE=A9=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E9=A1=B5=E9=9D=A2URL=E5=A5=BD=E7=9C=8B=E4=B8=80?= =?UTF-8?q?=E7=82=B9=20(#993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [v1.3] 让安装页面URL好看一点 * Update utils.ts * Update utils.ts --- src/pages/install/App.tsx | 6 +++--- src/pkg/utils/utils.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/pages/install/App.tsx b/src/pages/install/App.tsx index ee282d963..d39fbfd93 100644 --- a/src/pages/install/App.tsx +++ b/src/pages/install/App.tsx @@ -31,7 +31,7 @@ import { intervalExecution, timeoutExecution } from "@App/pkg/utils/timer"; import { useSearchParams } from "react-router-dom"; import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key"; import { cacheInstance } from "@App/app/cache"; -import { formatBytes } from "@App/pkg/utils/utils"; +import { formatBytes, prettyUrl } from "@App/pkg/utils/utils"; type ScriptOrSubscribe = Script | Subscribe; @@ -779,13 +779,13 @@ function App() { bold style={{ overflowWrap: "break-word", - wordBreak: "break-all", + wordBreak: "break-word", maxHeight: "70px", display: "block", overflowY: "auto", }} > - {`${t("source")}: ${scriptInfo?.url}`} + {`${t("source")}: ${prettyUrl(scriptInfo?.url)}`} diff --git a/src/pkg/utils/utils.ts b/src/pkg/utils/utils.ts index e6e1b891b..bb34c0a44 100644 --- a/src/pkg/utils/utils.ts +++ b/src/pkg/utils/utils.ts @@ -439,3 +439,37 @@ export const formatBytes = (bytes: number, decimals: number = 2): string => { return `${value.toFixed(decimals)} ${units[i]}`; }; + +// 把编码URL变成使用者可以阅读的格式 +export const prettyUrl = (s: string | undefined | null, baseUrl?: string) => { + if (s?.includes("://")) { + let u; + try { + u = baseUrl ? new URL(s, baseUrl) : new URL(s); + } catch { + // ignored + } + if (!u) return s; + const pathname = u.pathname; + if (pathname && pathname.includes("%")) { + try { + const raw = decodeURI(pathname); + if ( + raw && + raw.length < pathname.length && + !raw.includes("?") && + !raw.includes("#") && + !raw.includes("&") && + !raw.includes("=") && + !raw.includes("%") && + !raw.includes(":") + ) { + s = s.replace(pathname, raw); + } + } catch { + // ignored + } + } + } + return s; +}; From 723e64cc0c23763dfed322e907c0a960c4f9060e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:39:43 +0900 Subject: [PATCH 28/35] =?UTF-8?q?=F0=9F=90=9B=20UnoCSS=20=E5=8A=A0=20prefi?= =?UTF-8?q?x=20=E8=A7=A3=E5=86=B3=20CSS=E5=86=B2=E7=AA=81=E3=80=81CSS=20?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E4=BF=AE=E6=AD=A3=20(#1013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [v1.3] UnoCSS 加 prefix 解决 CSS冲突 * CSS 布局修正 * inset 兼容修正 * `uno-` -> `tw-` ;符合平常的写法 * presetUno -> presetWind3, 加 extractors 让 vscode 能配对 * Install 頁面 CSS 布局修正 * rspack/postcss/unocss 语法修改 --- postcss.config.mjs | 3 +- rspack.config.ts | 17 +-- src/index.css | 18 ++- src/pages/batchupdate/App.tsx | 38 +++--- src/pages/batchupdate/main.tsx | 2 +- src/pages/components/ScriptMenuList/index.tsx | 18 +-- src/pages/components/ScriptResource/index.tsx | 4 +- src/pages/components/ScriptSetting/Match.tsx | 4 +- .../components/ScriptSetting/Permission.tsx | 4 +- src/pages/components/ScriptStorage/index.tsx | 4 +- src/pages/components/layout/MainLayout.tsx | 18 +-- src/pages/components/layout/Sider.tsx | 34 ++--- src/pages/confirm/App.tsx | 8 +- src/pages/confirm/main.tsx | 2 +- src/pages/import/App.tsx | 16 +-- src/pages/import/main.tsx | 2 +- src/pages/install/App.tsx | 43 +++--- src/pages/install/index.css | 6 +- src/pages/install/main.tsx | 2 +- src/pages/options/main.tsx | 2 +- src/pages/options/routes/Logger.tsx | 4 +- .../options/routes/ScriptList/ScriptCard.tsx | 34 ++--- .../options/routes/ScriptList/ScriptTable.tsx | 8 +- .../options/routes/ScriptList/Sidebar.tsx | 24 ++-- .../options/routes/ScriptList/components.tsx | 2 +- src/pages/options/routes/ScriptList/hooks.tsx | 4 +- src/pages/options/routes/ScriptList/index.tsx | 6 +- src/pages/options/routes/Setting.tsx | 128 +++++++++--------- src/pages/options/routes/SubscribeList.tsx | 2 +- src/pages/options/routes/Tools.tsx | 4 +- .../options/routes/script/ScriptEditor.tsx | 32 ++--- src/pages/popup/App.tsx | 38 +++--- uno.config.ts | 9 +- 33 files changed, 278 insertions(+), 262 deletions(-) diff --git a/postcss.config.mjs b/postcss.config.mjs index cd0d6362c..ec8c02982 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,6 @@ import UnoCSS from "@unocss/postcss"; +import autoprefixer from "autoprefixer"; export default { - plugins: [UnoCSS()], + plugins: [UnoCSS(), autoprefixer()], }; diff --git a/rspack.config.ts b/rspack.config.ts index 793afe542..f0fe226a7 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -66,20 +66,9 @@ export default defineConfig({ module: { rules: [ { - test: /\.css$/, - use: [ - { - loader: "postcss-loader", - options: { - postcssOptions: { - plugins: { - autoprefixer: {}, - }, - }, - }, - }, - ], - type: "css", + test: /\.css$/i, + type: "css/auto", + use: ["postcss-loader"], }, { test: /\.(svg|png)$/, diff --git a/src/index.css b/src/index.css index b183908e9..6eb4944b4 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,6 @@ @unocss preflights; @unocss default; +@unocss; body { scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track); @@ -17,4 +18,19 @@ body[arco-theme='light'] { --color-scrollbar-thumb: #6b6b6b; --color-scrollbar-track: #f0f0f0; --color-scrollbar-thumb-hover: #8c8c8c; -} \ No newline at end of file +} + +:root { + --sc-zero-1: 0; + --sc-zero-2: 0; + --sc-zero-3: 0; + --sc-zero-4: 0; +} + +/* 自定义 .sc-inset-0 避免打包成 inset: 0 使旧浏览器布局错位 */ +.sc-inset-0 { + top: var(--sc-zero-1); + left: var(--sc-zero-2); + right: var(--sc-zero-3); + bottom: var(--sc-zero-4); +} diff --git a/src/pages/batchupdate/App.tsx b/src/pages/batchupdate/App.tsx index 3fb0fa7b6..6bc55872f 100644 --- a/src/pages/batchupdate/App.tsx +++ b/src/pages/batchupdate/App.tsx @@ -256,7 +256,7 @@ function App() { title={ openUpdatePage(item.uuid)} - className="text-clickable text-gray-900 dark:text-gray-100 !hover:text-blue-600 dark:hover:text-blue-400" + className="tw-text-clickable tw-text-gray-900 dark:tw-text-gray-100 !hover:tw-text-blue-600 dark:hover:tw-text-blue-400" > {item.script?.name} @@ -285,7 +285,7 @@ function App() { } > - + {t("updatepage.old_version_")} {t("updatepage.new_version_")} @@ -377,31 +377,31 @@ function App() { return ( <> { -
-
- +
+
+ {t("updatepage.main_header")} onCheckUpdateClick()} - className="cursor-pointer text-gray-700 dark:text-gray-300" + className="tw-cursor-pointer tw-text-gray-700 dark:tw-text-gray-300" />
-
- {mStatusText} +
+ {mStatusText}
{mRecords === null ? ( <> ) : ( <> {mRecords.site.length === 0 && mRecords.other.length === 0 ? ( -
- {t("updatepage.status_no_update")} +
+ {t("updatepage.status_no_update")}
) : ( -
- +
+ {t("updatepage.status_n_update").replace("$0", `${mRecords.site.length + mRecords.other.length}`)}
@@ -410,15 +410,15 @@ function App() { //
{"没有已忽略的更新"}
<> ) : ( -
- +
+ {t("updatepage.status_n_ignored").replace("$0", `${mRecords.ignored.length}`)}
)} {mTimeClose >= 0 ? ( -
- +
+ {t("updatepage.status_autoclose").replace("$0", `${mTimeClose}`)}
@@ -439,9 +439,9 @@ function App() { - + diff --git a/src/pages/components/ScriptMenuList/index.tsx b/src/pages/components/ScriptMenuList/index.tsx index d29c8756f..fc4d6a09f 100644 --- a/src/pages/components/ScriptMenuList/index.tsx +++ b/src/pages/components/ScriptMenuList/index.tsx @@ -89,7 +89,7 @@ const MenuItem = React.memo(({ menuItems, uuid }: MenuItemProps) => { }} > )} {url && isEffective !== null && (
-
+
{/* 依数量与展开状态决定要显示的分组项(收合时只显示前 menuExpandNum 笔) */} {visibleMenus.map(({ uuid, groupKey, menus }) => { // 不同脚本之间可能出现相同的 groupKey;为避免 React key 冲突,需加上 uuid 做区分。 @@ -278,7 +278,7 @@ const ListMenuItem = React.memo( })} {shouldShowMore && (