Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 33 additions & 17 deletions src/app/service/service_worker/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
obtainBlackList,
sourceMapTo,
} from "@App/pkg/utils/utils";
import { BrowserType, getBrowserInstalledVersion, getBrowserType, isPermissionOk } from "@App/pkg/utils/utils";
import { cacheInstance } from "@App/app/cache";
import { UrlMatch } from "@App/pkg/utils/match";
import { ExtensionContentMessageSend } from "@Packages/message/extension_message";
Expand Down Expand Up @@ -162,22 +163,8 @@ export class RuntimeService {
}
}

showNoDeveloperModeWarning() {
// 判断是否首次
this.localStorageDAO.get("firstShowDeveloperMode").then((res) => {
if (!res) {
this.localStorageDAO.save({
key: "firstShowDeveloperMode",
value: true,
});
// 打开页面
initLocalesPromise.then(() => {
chrome.tabs.create({
url: `${DocumentationSite}${localePath}/docs/use/open-dev/`,
});
});
}
});
async showUserscriptActivationGuide() {
const storageKey = "firstShowDeveloperMode";
chrome.action.setBadgeBackgroundColor({
color: "#ff8c00",
});
Expand Down Expand Up @@ -206,6 +193,35 @@ export class RuntimeService {
});
}
});

const currentInstalledBrowser = getBrowserInstalledVersion();
const lastInstalledBrowser = (await this.localStorageDAO.get(storageKey))?.value as string | boolean | undefined;
// 判断是否安装后的首次,或是浏览器升级后的首次
if (currentInstalledBrowser === lastInstalledBrowser) return; // 非首次则不弹出页面

const savePromise = this.localStorageDAO.save({
key: storageKey,
value: currentInstalledBrowser,
});
await Promise.allSettled([initLocalesPromise, this.initReady, savePromise]); // 等一下语言加载和 isUserScriptsAvailable 检查之类的

const userscript_enabled: boolean = this.isUserScriptsAvailable;
const permission = await isPermissionOk("userScripts");
const browserType = getBrowserType();
const guard =
browserType.chrome & BrowserType.guardedByDeveloperMode
? "developerMode"
: browserType.chrome & BrowserType.guardedByAllowScript
? "allowScript"
: "none";

// 打开页面
const path = `${DocumentationSite}${localePath}/docs/use/open-dev/`;
let search = `?userscript_enabled=${userscript_enabled}&userscript_permission=${permission}&userscript_guard=${guard}`;
if (browserType.chrome & BrowserType.Edge) search += "&browser=edge";
else if (browserType.chrome & BrowserType.Chrome) search += "&browser=chrome";
const hash = `${guard === "developerMode" ? "#enable-developer-mode" : guard === "allowScript" ? "#allow-user-scripts" : ""}`;
chrome.tabs.create({ url: `${path}${search}${hash}` });
}

async getInjectJsCode() {
Expand Down Expand Up @@ -570,7 +586,7 @@ export class RuntimeService {
// 检查是否开启了开发者模式
if (!this.isUserScriptsAvailable) {
// 未开启加上警告引导
this.showNoDeveloperModeWarning();
this.showUserscriptActivationGuide();
let cid: ReturnType<typeof setInterval> | number;
cid = setInterval(async () => {
if (!this.isUserScriptsAvailable) {
Expand Down
66 changes: 32 additions & 34 deletions src/pages/components/PopupWarnings/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Alert, Button } from "@arco-design/web-react";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { checkUserScriptsAvailable, getBrowserType, BrowserType } from "@App/pkg/utils/utils";
import { checkUserScriptsAvailable, getBrowserType, BrowserType, isPermissionOk } from "@App/pkg/utils/utils";
import edgeMobileQrCode from "@App/assets/images/edge_mobile_qrcode.png";

interface PopupWarningsProps {
Expand Down Expand Up @@ -39,16 +39,28 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {

const browser = browserType.chrome & BrowserType.Edge ? "edge" : "chrome";

const warningMessageHTML = browserType.firefox
? t("develop_mode_guide", { browser: "firefox" })
: browserType.chrome
? browserType.chrome & BrowserType.chromeA
let warningMessageHTML;

if (browserType.firefox) {
// firefox
warningMessageHTML = t("develop_mode_guide", { browser: "firefox" });
} else if (browserType.chrome) {
// chrome
warningMessageHTML =
browserType.chrome & BrowserType.noUserScriptsAPI
? t("lower_version_browser_guide")
: (browserType.chrome & BrowserType.chromeC && browserType.chrome & BrowserType.Chrome) ||
browserType.chrome & BrowserType.edgeA
? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制
: t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可
: "UNKNOWN";
: // 120+
browserType.chrome & BrowserType.guardedByDeveloperMode
? t("develop_mode_guide", { browser }) // Edge浏览器目前没有允许用户脚本选项,开启开发者模式即可
: // Edge 144+ / Chrome 138+
browserType.chrome & BrowserType.guardedByAllowScript
? t("allow_user_script_guide", { browser }) // Edge 144+ 后使用`允许用户脚本`控制
: // 用于日后扩充更新版本
"UNKNOWN";
} else {
// other browsers
warningMessageHTML = "UNKNOWN";
}

return warningMessageHTML;
}, [isUserScriptsAvailableState, t]);
Expand All @@ -62,28 +74,14 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {

// 权限要求详见:https://github.com/mdn/webextensions-examples/blob/main/userScripts-mv3/options.mjs
useEffect(() => {
//@ts-ignore
if (chrome.permissions?.contains && chrome.permissions?.request) {
chrome.permissions.contains(
{
permissions: ["userScripts"],
},
function (permissionOK) {
const lastError = chrome.runtime.lastError;
if (lastError) {
console.error("chrome.runtime.lastError in chrome.permissions.contains:", lastError.message);
// runtime 错误的话不显示按钮
return;
}
if (permissionOK === false) {
// 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话,
// chrome.permissions.request 应该可以执行
// 因此在这裡显示按钮
setShowRequestButton(true);
}
}
);
}
isPermissionOk("userScripts").then((permissionOK) => {
if (permissionOK === false) {
// 假设browser能支持 `chrome.permissions.contains` 及在 callback返回一个false值的话,
// chrome.permissions.request 应该可以执行
// 因此在这里显示按钮
setShowRequestButton(true);
}
});
}, []);

const handleRequestPermission = () => {
Expand All @@ -103,8 +101,8 @@ function PopupWarnings({ isBlacklist }: PopupWarningsProps) {
if (granted) {
setPermissionReqResult("✅");
// UserScripts API相关的初始化:
// userScripts.LISTEN_CONNECTIONS 進行 Server 通讯初始化
// onUserScriptAPIGrantAdded 進行 腳本注冊
// userScripts.LISTEN_CONNECTIONS 进行 Server 通讯初始化
// onUserScriptAPIGrantAdded 进行 脚本注册
updateIsUserScriptsAvailableState();
} else {
setPermissionReqResult("❎");
Expand Down
8 changes: 3 additions & 5 deletions src/pages/components/RuntimeSetting/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import FileSystemParams from "../FileSystemParams";
import { systemConfig } from "@App/pages/store/global";
import type { FileSystemType } from "@Packages/filesystem/factory";
import FileSystemFactory from "@Packages/filesystem/factory";
import { isPermissionOk } from "@App/pkg/utils/utils";

const CollapseItem = Collapse.Item;

Expand All @@ -24,11 +25,8 @@ const RuntimeSetting: React.FC = () => {
setFilesystemType(res.filesystem);
setFilesystemParam(res.params[res.filesystem] || {});
});
chrome.permissions.contains({ permissions: ["background"] }, (result) => {
if (chrome.runtime.lastError) {
console.error(chrome.runtime.lastError);
return;
}
isPermissionOk("background").then((result) => {
if (result === null) return; // 无法要求 background permission
setEnableBackgroundState(result);
});
}, []);
Expand Down
7 changes: 3 additions & 4 deletions src/pages/install/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import { useSearchParams } from "react-router-dom";
import { CACHE_KEY_SCRIPT_INFO } from "@App/app/cache_key";
import { cacheInstance } from "@App/app/cache";
import { formatBytes, prettyUrl } from "@App/pkg/utils/utils";
import { formatBytes, isPermissionOk, prettyUrl } from "@App/pkg/utils/utils";
import { ScriptIcons } from "../options/routes/utils";
import { bytesDecode, detectEncoding } from "@App/pkg/utils/encoding";

Expand Down Expand Up @@ -330,7 +330,7 @@

useEffect(() => {
!loaded && initAsync();
}, [searchParams, loaded]);

Check warning on line 333 in src/pages/install/App.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has a missing dependency: 'initAsync'. Either include it or remove the dependency array

const [watchFile, setWatchFile] = useState(false);
const metadataLive = useMemo(() => (scriptInfo?.metadata || {}) as SCMetadata, [scriptInfo]);
Expand Down Expand Up @@ -462,9 +462,8 @@

if (hasShown !== "true") {
// 检查是否已经有后台权限
if (!(await chrome.permissions.contains({ permissions: ["background"] }))) {
return true;
}
const permission = await isPermissionOk("background");
if (permission === false) return true; // optional permission "background" 需要显示后台运行提示
}
return false;
};
Expand Down Expand Up @@ -645,7 +644,7 @@
return () => {
unmountFileTrack(handle);
};
}, [memoWatchFile]);

Check warning on line 647 in src/pages/install/App.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has missing dependencies: 'localFileHandle', 'scriptInfo?.uuid', 'setupWatchFile', and 'watchFile'. Either include them or remove the dependency array

// 检查是否有 uuid 或 file
const hasUUIDorFile = useMemo(() => {
Expand Down Expand Up @@ -712,7 +711,7 @@
useEffect(() => {
if (!urlHref) return;
loadURLAsync(urlHref);
}, [urlHref]);

Check warning on line 714 in src/pages/install/App.tsx

View workflow job for this annotation

GitHub Actions / Run tests

React Hook useEffect has a missing dependency: 'loadURLAsync'. Either include it or remove the dependency array

if (!hasUUIDorFile) {
return urlHref ? (
Expand Down
86 changes: 68 additions & 18 deletions src/pkg/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const deferred = <T = void>(): Deferred<T> => {
};

export function isFirefox() {
//@ts-ignore
return typeof mozInnerScreenX !== "undefined";
// @ts-ignore. For both Page & Worker
return typeof mozInnerScreenX !== "undefined" || typeof navigator.mozGetUserMedia === "function";
}

export function InfoNotification(title: string, msg: string) {
Expand Down Expand Up @@ -256,17 +256,25 @@ export function getBrowserVersion(): number {

// 判断是否为Edge浏览器
export function isEdge(): boolean {
return navigator.userAgent.includes("Edg/");
return (
// @ts-ignore; For Extension (Page/Worker), we can check UserSubscriptionState (hidden feature in Edge)
typeof chrome.runtime.UserSubscriptionState === "object" ||
// Fallback to userAgent check
navigator.userAgent.includes("Edg/")
);
}

export enum BrowserType {
Edge = 2,
Chrome = 1,
chromeA = 4, // ~ 120
chromeB = 8, // 121 ~ 137
chromeC = 16, // 138 ~
edgeA = 32, // Edge 144~
}
export const BrowserType = {
Edge: 2,
Chrome: 1,
noUserScriptsAPI: 64,
guardedByDeveloperMode: 128,
guardedByAllowScript: 256,
Mouse: 1, // Desktop, Laptop. Tablet ??
Touch: 2, // Touchscreen Laptop, Mobile, Tablet
} as const;

export type BrowserType = ValueOf<typeof BrowserType>;

export function getBrowserType() {
const o = {
Expand All @@ -275,35 +283,77 @@ export function getBrowserType() {
chrome: 0, // Chrome, Chromium, Brave, Edge
unknown: 0,
chromeVersion: 0,
device: 0,
};
if (isFirefox()) {
// Firefox, Zen
o.firefox = 1;
} else {
//@ts-ignore
const isWebkitBased = typeof webkitIndexedDB === "object";
if (isWebkitBased) {
// Safari, Orion
o.webkit = 1;
} else {
//@ts-ignore
const isChromeBased = typeof webkitRequestAnimationFrame === "function";
const isChromeBased =
typeof requestAnimationFrame === "function"
? // @ts-ignore. For Page only
typeof webkitRequestAnimationFrame === "function"
: // @ts-ignore. Available in Worker (Chrome 74+ Edge 79+)
typeof BackgroundFetchRecord === "function";
if (isChromeBased) {
const isEdgeBrowser = isEdge();
const chromeVersion = getBrowserVersion();
o.chrome |= isEdgeBrowser ? BrowserType.Edge : BrowserType.Chrome;
o.chrome |= chromeVersion < 120 ? BrowserType.chromeA : 0; // Chrome 120 以下
o.chrome |= chromeVersion < 138 ? BrowserType.chromeB : BrowserType.chromeC; // Chrome 121 ~ 137 / 138 以上
if (isEdgeBrowser) {
o.chrome |= chromeVersion >= 144 ? BrowserType.edgeA : 0; // Edge 144 以上
// 由小至大
if (chromeVersion < 120) {
o.chrome |= BrowserType.noUserScriptsAPI;
} else {
// 120+
if (isEdgeBrowser ? chromeVersion < 144 : chromeVersion < 138) {
o.chrome |= BrowserType.guardedByDeveloperMode;
} else {
// Edge 144+ / Chrome 138+
o.chrome |= BrowserType.guardedByAllowScript;
// 如日后再变化,在这里再加条件式
}
}
o.chromeVersion = chromeVersion;
} else {
o.unknown = 1;
}
}
}
// BrowserType.Mouse 未能在 Worker 使用
o.device |= typeof matchMedia === "function" && !matchMedia("(hover: none)").matches ? BrowserType.Mouse : 0;
o.device |= navigator.maxTouchPoints > 0 ? BrowserType.Touch : 0;
return o;
}

export const isPermissionOk = async (
manifestPermission: chrome.runtime.ManifestOptionalPermissions & chrome.runtime.ManifestPermissions
): Promise<boolean | null> => {
// 兼容 Firefox - 避免因为检查 permission 时,该permission不存在于 optional permission 而报错
const manifest = chrome.runtime.getManifest();
if (manifest.optional_permissions?.includes(manifestPermission)) {
try {
return await chrome.permissions.contains({ permissions: [manifestPermission] });
} catch {
// ignored
}
} else if (manifest.permissions?.includes(manifestPermission)) {
// mainfest 而列明有该permission, 不用检查
return true;
}
return null;
};

export const getBrowserInstalledVersion = () => {
// unique for each browser update.
// Usage: Detect whether the browser is upgraded.
return btoa([...navigator.userAgent.matchAll(/[\d._]+/g)].map((e) => e[0]).join(";"));
};

export const makeBlobURL = <T extends { blob: Blob; persistence: boolean }>(
params: T,
fallbackFn?: (params: T) => string | Promise<string>
Expand Down Expand Up @@ -513,7 +563,7 @@ export const normalizeResponseHeaders = (headersString: string) => {
// 遵循 ISO 8601, 一月四日为Week 1,星期一为新一周
// 能应对每年开始和结束(不会因为踏入新一年而重新计算)
// 见 https://wikipedia.org/wiki/ISO_week_date
// 中文說明 https://juejin.cn/post/6921245139855736846
// 中文说明 https://juejin.cn/post/6921245139855736846
export const getISOWeek = (date: Date): number => {
// 使用传入日期的年月日创建 UTC 日期对象,忽略本地时间部分,避免时区影响
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
Expand Down
11 changes: 8 additions & 3 deletions tests/pages/popup/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,15 @@ vi.mock("@App/pkg/utils/utils", () => ({
title: "Example",
}),
BrowserType: {
Chrome: "chrome",
Firefox: "firefox",
Edge: "edge",
Edge: 2,
Chrome: 1,
noUserScriptsAPI: 64,
guardedByDeveloperMode: 128,
guardedByAllowScript: 256,
Mouse: 1,
Touch: 2,
},
isPermissionOk: vi.fn(async (_s: string) => true),
}));

vi.mock("@App/locales/locales", () => ({
Expand Down
Loading