From 297b458a72a95496f15423c65530583f62fa6894 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Wed, 5 Nov 2025 18:23:47 +0800 Subject: [PATCH 1/8] feat(safari): support safari extension --- chrome-extension/manifest.ts | 37 +++++++++++++++- chrome-extension/src/background/index.ts | 42 +++++++++++++------ .../utils/plugins/make-manifest-plugin.ts | 4 +- package.json | 3 ++ .../dev-utils/lib/manifest-parser/impl.ts | 9 ++-- .../dev-utils/lib/manifest-parser/types.ts | 4 +- packages/env/lib/const.ts | 1 + packages/shared/lib/utils/axios.ts | 1 + 8 files changed, 80 insertions(+), 21 deletions(-) diff --git a/chrome-extension/manifest.ts b/chrome-extension/manifest.ts index e3c2958..ca2d3a5 100644 --- a/chrome-extension/manifest.ts +++ b/chrome-extension/manifest.ts @@ -1,6 +1,7 @@ import { readFileSync } from 'node:fs'; const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); +const isSafari = process.env['CLI_CEB_SAFARI'] === 'true'; /** * @prop default_locale @@ -17,7 +18,37 @@ const packageJson = JSON.parse(readFileSync('./package.json', 'utf8')); * @prop content_scripts * css: ['content.css'], // public folder */ -const manifest = { + +// Manifest V2 for Safari +const manifestV2 = { + manifest_version: 2, + default_locale: 'en', + name: '__MSG_extensionName__', + version: packageJson.version, + description: '__MSG_extensionDescription__', + permissions: ['storage', 'tabs', ''], + options_page: 'options/index.html', + background: { + scripts: ['background.js'], + persistent: false, + }, + browser_action: { + default_icon: 'icon-34.png', + }, + icons: { + '128': 'icon-128.png', + }, + content_scripts: [ + { + matches: ['http://*/*', 'https://*/*', ''], + js: ['content/index.iife.js'], + }, + ], + web_accessible_resources: ['*.js', '*.css', '*.svg', 'icon-128.png', 'icon-34.png'], +} satisfies chrome.runtime.ManifestV2; + +// Manifest V3 for Chrome/Firefox +const manifestV3 = { manifest_version: 3, default_locale: 'en', name: '__MSG_extensionName__', @@ -55,4 +86,6 @@ const manifest = { ], } satisfies chrome.runtime.ManifestV3; -export default manifest; +const manifest = isSafari ? manifestV2 : manifestV3; + +export default manifest as chrome.runtime.ManifestV3 | chrome.runtime.ManifestV2; diff --git a/chrome-extension/src/background/index.ts b/chrome-extension/src/background/index.ts index 85ea829..18c7b65 100644 --- a/chrome-extension/src/background/index.ts +++ b/chrome-extension/src/background/index.ts @@ -11,7 +11,8 @@ chrome.runtime.onInstalled.addListener(async () => { ); }); -// Track extension icon pinning action +// Track extension icon pinning action (V3 only) +// This API is not available in V2 if (chrome.action && chrome.action.onUserSettingsChanged) { chrome.action.onUserSettingsChanged.addListener(async userSettings => { if (userSettings.isOnToolbar !== undefined) { @@ -22,20 +23,37 @@ if (chrome.action && chrome.action.onUserSettingsChanged) { }); } -self.addEventListener('unhandledrejection', e => { - e.preventDefault(); - console.log(e); -}); +// Handle unhandled promise rejections for both V2 and V3 +if (typeof self !== 'undefined' && self.addEventListener) { + // V3 Service Worker + self.addEventListener('unhandledrejection', e => { + e.preventDefault(); + console.log(e); + }); +} else if (typeof window !== 'undefined' && window.addEventListener) { + // V2 Background Page + window.addEventListener('unhandledrejection', e => { + e.preventDefault(); + console.log(e); + }); +} // Handle action icon click to toggle popup in content script -chrome.action.onClicked.addListener(() => { - chrome.tabs.query({ active: true, currentWindow: true }, tabs => { - const tab = tabs[0]; - if (tab?.id && tab.url && !isInternalUrl(tab.url)) { - chrome.tabs.sendMessage(tab.id, { action: 'toggle-popup' }); - } +// Support both V2 (browserAction) and V3 (action) +const actionAPI = + chrome.action || + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (chrome as any).browserAction; +if (actionAPI && actionAPI.onClicked) { + actionAPI.onClicked.addListener(() => { + chrome.tabs.query({ active: true, currentWindow: true }, tabs => { + const tab = tabs[0]; + if (tab?.id && tab.url && !isInternalUrl(tab.url)) { + chrome.tabs.sendMessage(tab.id, { action: 'toggle-popup' }); + } + }); }); -}); +} chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'collect') { diff --git a/chrome-extension/utils/plugins/make-manifest-plugin.ts b/chrome-extension/utils/plugins/make-manifest-plugin.ts index b882920..91d22fb 100644 --- a/chrome-extension/utils/plugins/make-manifest-plugin.ts +++ b/chrome-extension/utils/plugins/make-manifest-plugin.ts @@ -5,7 +5,7 @@ import { platform } from 'node:process'; import type { Manifest } from '@extension/dev-utils'; import { colorLog, ManifestParser } from '@extension/dev-utils'; import type { PluginOption } from 'vite'; -import { IS_DEV, IS_FIREFOX } from '@extension/env'; +import { IS_DEV, IS_FIREFOX, IS_SAFARI } from '@extension/env'; const manifestFile = resolve(import.meta.dirname, '..', '..', 'manifest.js'); const refreshFilePath = resolve( @@ -51,7 +51,7 @@ export default (config: { outDir: string }): PluginOption => { addRefreshContentScript(manifest); } - writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX)); + writeFileSync(manifestPath, ManifestParser.convertManifestToString(manifest, IS_FIREFOX, IS_SAFARI)); const refreshFileString = readFileSync(refreshFilePath, 'utf-8'); diff --git a/package.json b/package.json index a5dd9a4..483e453 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,15 @@ "base-build": "pnpm clean:bundle && turbo build", "build": "pnpm set-global-env && pnpm base-build", "build:firefox": "pnpm set-global-env CLI_CEB_FIREFOX=true && pnpm base-build", + "build:safari": "pnpm set-global-env CLI_CEB_SAFARI=true && pnpm base-build", "base-dev": "pnpm clean:bundle && turbo ready && turbo watch dev --concurrency 20", "dev": "pnpm set-global-env CLI_CEB_DEV=true && pnpm base-dev", "dev:firefox": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_FIREFOX=true && pnpm base-dev", + "dev:safari": "pnpm set-global-env CLI_CEB_DEV=true CLI_CEB_SAFARI=true && pnpm base-dev", "build:eslint": "tsc -b", "zip": "pnpm build && pnpm -F zipper zip", "zip:firefox": "pnpm build:firefox && pnpm -F zipper zip", + "zip:safari": "pnpm build:safari && pnpm -F zipper zip", "lint": "turbo lint --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", "lint:fix": "turbo lint:fix --continue -- --fix --cache --cache-location node_modules/.cache/.eslintcache", "prettier": "turbo prettier --continue -- --cache --cache-location node_modules/.cache/.prettiercache", diff --git a/packages/dev-utils/lib/manifest-parser/impl.ts b/packages/dev-utils/lib/manifest-parser/impl.ts index 9acc91c..2404085 100644 --- a/packages/dev-utils/lib/manifest-parser/impl.ts +++ b/packages/dev-utils/lib/manifest-parser/impl.ts @@ -1,8 +1,10 @@ import type { Manifest, ManifestParserInterface } from './types.js'; export const ManifestParserImpl: ManifestParserInterface = { - convertManifestToString: (manifest, isFirefox) => { - if (isFirefox) { + convertManifestToString: (manifest, isFirefox, isSafari = false) => { + // Safari uses Manifest V2 directly from manifest.ts, no conversion needed + // Firefox needs specific conversions for V3 + if (isFirefox && !isSafari) { manifest = convertToFirefoxCompatibleManifest(manifest); } @@ -15,7 +17,8 @@ const convertToFirefoxCompatibleManifest = (manifest: Manifest) => { ...manifest, } as { [key: string]: unknown }; - if (manifest.background?.service_worker) { + // Only convert if it's a V3 manifest with service_worker + if (manifest.background && 'service_worker' in manifest.background && manifest.background.service_worker) { manifestCopy.background = { scripts: [manifest.background.service_worker], type: 'module', diff --git a/packages/dev-utils/lib/manifest-parser/types.ts b/packages/dev-utils/lib/manifest-parser/types.ts index 7882a04..124768d 100644 --- a/packages/dev-utils/lib/manifest-parser/types.ts +++ b/packages/dev-utils/lib/manifest-parser/types.ts @@ -1,5 +1,5 @@ -export type Manifest = chrome.runtime.ManifestV3; +export type Manifest = chrome.runtime.ManifestV3 | chrome.runtime.ManifestV2; export interface ManifestParserInterface { - convertManifestToString: (manifest: Manifest, isFirefox: boolean) => string; + convertManifestToString: (manifest: Manifest, isFirefox: boolean, isSafari?: boolean) => string; } diff --git a/packages/env/lib/const.ts b/packages/env/lib/const.ts index 25650dc..326bbb7 100644 --- a/packages/env/lib/const.ts +++ b/packages/env/lib/const.ts @@ -1,4 +1,5 @@ export const IS_DEV = process.env['CLI_CEB_DEV'] === 'true'; export const IS_PROD = !IS_DEV; export const IS_FIREFOX = process.env['CLI_CEB_FIREFOX'] === 'true'; +export const IS_SAFARI = process.env['CLI_CEB_SAFARI'] === 'true'; export const IS_CI = process.env['CEB_CI'] === 'true'; diff --git a/packages/shared/lib/utils/axios.ts b/packages/shared/lib/utils/axios.ts index 22a3b5a..739b248 100644 --- a/packages/shared/lib/utils/axios.ts +++ b/packages/shared/lib/utils/axios.ts @@ -51,6 +51,7 @@ export function axios( redirect: 'manual', headers: params.headers, method: params.method || 'GET', + credentials: 'include', }; return fetch(params.url, options).then(response => { if (!response.ok) { From 13efaeff0e0a6d5927cb6aaa1757441241957935 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Wed, 5 Nov 2025 19:02:02 +0800 Subject: [PATCH 2/8] feat(merge): merge remote main --- chrome-extension/src/background/index.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/chrome-extension/src/background/index.ts b/chrome-extension/src/background/index.ts index 18c7b65..fb408e1 100644 --- a/chrome-extension/src/background/index.ts +++ b/chrome-extension/src/background/index.ts @@ -49,7 +49,20 @@ if (actionAPI && actionAPI.onClicked) { chrome.tabs.query({ active: true, currentWindow: true }, tabs => { const tab = tabs[0]; if (tab?.id && tab.url && !isInternalUrl(tab.url)) { - chrome.tabs.sendMessage(tab.id, { action: 'toggle-popup' }); + const tabId = tab.id; + chrome.tabs.sendMessage(tabId, { action: 'toggle-popup' }, () => { + // Check if there was an error sending the message + const lastError = chrome.runtime.lastError; + if (lastError) { + // If the receiving end does not exist (content script not loaded), + // reload the tab to inject the content script + if (lastError.message?.includes('Receiving end does not exist')) { + chrome.tabs.reload(tabId); + } else { + console.error('Error sending message to content script:', lastError); + } + } + }); } }); }); From 3a40ec07ec2b4cb73838eec35f4b444dbbb27644 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Fri, 14 Nov 2025 15:27:24 +0800 Subject: [PATCH 3/8] fix(comment): fix comments --- pages/content/src/widgets/popup/Page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/content/src/widgets/popup/Page.tsx b/pages/content/src/widgets/popup/Page.tsx index 716f6f7..1131abd 100644 --- a/pages/content/src/widgets/popup/Page.tsx +++ b/pages/content/src/widgets/popup/Page.tsx @@ -25,7 +25,7 @@ export function Page(props: IProps) { chrome.runtime.sendMessage( { action: 'fetch', - url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/api/v1/namespaces/${data.namespaceId}/root`, + url: `${baseUrl}/api/v1/namespaces/${data.namespaceId}/root`, }, response => { let match = false; From 44c260120ace7c2a9f3212111a3340e9ea3e5589 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Fri, 14 Nov 2025 15:38:10 +0800 Subject: [PATCH 4/8] feat(popup): add loading for popup --- pages/content/src/widgets/popup/Page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pages/content/src/widgets/popup/Page.tsx b/pages/content/src/widgets/popup/Page.tsx index 1131abd..bbcbd2a 100644 --- a/pages/content/src/widgets/popup/Page.tsx +++ b/pages/content/src/widgets/popup/Page.tsx @@ -5,6 +5,7 @@ import Collect from './Collect'; import { useEffect } from 'react'; import { Wrapper } from './Wrapper'; import { Section } from './Section'; +import { Loader2 } from 'lucide-react'; import { useUser } from '@src/hooks/useUser'; import type { Response } from '@extension/shared'; @@ -14,7 +15,7 @@ interface IProps extends Response { export function Page(props: IProps) { const { onPopup, data, loading, onChange } = props; - const { user } = useUser({ baseUrl: loading ? '' : data.apiBaseUrl }); + const { user, loading: userLoading } = useUser({ baseUrl: loading ? '' : data.apiBaseUrl }); useEffect(() => { if (loading || !user.id || !data.apiBaseUrl) { @@ -101,6 +102,16 @@ export function Page(props: IProps) { ); }, [loading, data.apiBaseUrl, data.namespaceId, data.resourceId, user.id, onChange]); + if (userLoading) { + return ( + +
+ +
+
+ ); + } + return (
From 0627c5823520ef591674181abdc84c327be8f884 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Tue, 18 Nov 2025 20:08:20 +0800 Subject: [PATCH 5/8] fix(shadowdom): fix shadowDOM bug --- pages/content/src/widgets/toolbar/Wrapper.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pages/content/src/widgets/toolbar/Wrapper.tsx b/pages/content/src/widgets/toolbar/Wrapper.tsx index 5a5af1c..00ed5b8 100644 --- a/pages/content/src/widgets/toolbar/Wrapper.tsx +++ b/pages/content/src/widgets/toolbar/Wrapper.tsx @@ -53,6 +53,10 @@ export function Wrapper(props: IProps) { const range = selection.getRangeAt(0); if (range) { const rangeRect = range.getBoundingClientRect(); + if (rangeRect.width <= 0) { + // Dealing with nested shadowdom, https://www.bilibili.com/video/BV1LN15BLE6f + return; + } if (x < rangeRect.left || x > rangeRect.right) { x = rangeRect.right + window.scrollX; y = rangeRect.bottom + window.scrollY; From db8d4b2865a7a449c01c149ea421ea5616f5d03a Mon Sep 17 00:00:00 2001 From: hewenguang Date: Tue, 18 Nov 2025 20:21:01 +0800 Subject: [PATCH 6/8] fix(options): add save button for access input --- pages/options/src/Wrapper.tsx | 4 +-- pages/options/src/advance/Button.tsx | 46 ++++++++++++++++++++++++++ pages/options/src/i18n/locales/en.json | 3 +- pages/options/src/i18n/locales/zh.json | 3 +- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 pages/options/src/advance/Button.tsx diff --git a/pages/options/src/Wrapper.tsx b/pages/options/src/Wrapper.tsx index 15dff19..ae08233 100644 --- a/pages/options/src/Wrapper.tsx +++ b/pages/options/src/Wrapper.tsx @@ -1,9 +1,9 @@ import { Toaster } from 'sonner'; import { Header } from './header'; -import { Advance } from './advance'; import { Button } from '@extension/ui'; import { useUser } from '@src/hooks/useUser'; import { useTranslation } from 'react-i18next'; +import { AdvanceButton } from './advance/Button'; import type { Storage } from '@extension/shared'; interface IProps { @@ -33,7 +33,7 @@ export function Wrapper(props: IProps) {
- +
+
+
+ ); +} diff --git a/pages/options/src/i18n/locales/en.json b/pages/options/src/i18n/locales/en.json index f14682b..16d07d1 100644 --- a/pages/options/src/i18n/locales/en.json +++ b/pages/options/src/i18n/locales/en.json @@ -24,5 +24,6 @@ "loading": "Loading...", "error_occurred": "Error occurred", "shortcut_placeholder": "Enter shortcut", - "hold_option_key": "Hold" + "hold_option_key": "Hold", + "save": "Save" } diff --git a/pages/options/src/i18n/locales/zh.json b/pages/options/src/i18n/locales/zh.json index 7777b83..3011aea 100644 --- a/pages/options/src/i18n/locales/zh.json +++ b/pages/options/src/i18n/locales/zh.json @@ -24,5 +24,6 @@ "loading": "加载中...", "error_occurred": "发生错误", "shortcut_placeholder": "输入快捷键", - "hold_option_key": "按住" + "hold_option_key": "按住", + "save": "保存" } From f6e99ac9b358a169d001be39d6087993aa171410 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Thu, 20 Nov 2025 12:11:26 +0800 Subject: [PATCH 7/8] fix(comment): fix comments --- pages/content/src/utils/zindex.ts | 9 +++-- pages/content/src/widgets/popup/Header.tsx | 6 +-- pages/options/src/Wrapper.tsx | 4 +- pages/options/src/advance/Access.tsx | 24 ----------- pages/options/src/advance/Button.tsx | 46 ---------------------- pages/options/src/advance/index.tsx | 44 ++++++++++++++++++++- pages/options/src/hooks/useUser.tsx | 4 +- 7 files changed, 56 insertions(+), 81 deletions(-) delete mode 100644 pages/options/src/advance/Access.tsx delete mode 100644 pages/options/src/advance/Button.tsx diff --git a/pages/content/src/utils/zindex.ts b/pages/content/src/utils/zindex.ts index 6d188cf..b36f76f 100644 --- a/pages/content/src/utils/zindex.ts +++ b/pages/content/src/utils/zindex.ts @@ -13,9 +13,12 @@ export default function zIndex() { } else { let value = 0; document.body.querySelectorAll('*').forEach(item => { - const zindex = Number.parseInt(window.getComputedStyle(item, null).getPropertyValue('z-index'), 10); - if (zindex > value) { - value = zindex; + const rawZindex = window.getComputedStyle(item, null).getPropertyValue('z-index'); + if (!['auto', '0'].includes(rawZindex)) { + const zindex = Number.parseInt(rawZindex, 10); + if (zindex > value) { + value = zindex; + } } }); if (value <= 0) { diff --git a/pages/content/src/widgets/popup/Header.tsx b/pages/content/src/widgets/popup/Header.tsx index 1d51c86..e06a642 100644 --- a/pages/content/src/widgets/popup/Header.tsx +++ b/pages/content/src/widgets/popup/Header.tsx @@ -13,17 +13,17 @@ export default function Header(props: IProps) { const { baseUrl, namespaceId } = props; const { container } = useApp(); const [target, onTarget] = useState(null); - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const handleNamespace = () => { chrome.runtime.sendMessage({ action: 'create-tab', - url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/${namespaceId}/chat`, + url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/${namespaceId}/chat?lang=${i18n.language}`, }); }; const handleFeedback = () => { chrome.runtime.sendMessage({ action: 'create-tab', - url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/feedback`, + url: `${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/feedback?lang=${i18n.language}`, }); }; const handleSetting = () => { diff --git a/pages/options/src/Wrapper.tsx b/pages/options/src/Wrapper.tsx index ae08233..15dff19 100644 --- a/pages/options/src/Wrapper.tsx +++ b/pages/options/src/Wrapper.tsx @@ -1,9 +1,9 @@ import { Toaster } from 'sonner'; import { Header } from './header'; +import { Advance } from './advance'; import { Button } from '@extension/ui'; import { useUser } from '@src/hooks/useUser'; import { useTranslation } from 'react-i18next'; -import { AdvanceButton } from './advance/Button'; import type { Storage } from '@extension/shared'; interface IProps { @@ -33,7 +33,7 @@ export function Wrapper(props: IProps) {
- +
-
-
- ); -} diff --git a/pages/options/src/advance/index.tsx b/pages/options/src/advance/index.tsx index d4536b1..5709a84 100644 --- a/pages/options/src/advance/index.tsx +++ b/pages/options/src/advance/index.tsx @@ -1,6 +1,46 @@ -import Access from './Access'; +import { useState, useEffect } from 'react'; +import { axios } from '@extension/shared'; +import { Input, Button } from '@extension/ui'; import type { IProps } from '@src/types'; +import { useTranslation } from 'react-i18next'; export function Advance(props: IProps) { - return ; + const { data, onChange } = props; + const { t } = useTranslation(); + const [value, onValue] = useState(data.apiBaseUrl); + const handleApiBaseUrlChange = () => { + if (!value) { + return; + } + onChange({ + apiBaseUrl: value, + namespaceId: '', + resourceId: '', + }); + axios(`${value.endsWith('/') ? value.slice(0, -1) : value}/api/v1/user/me`); + }; + const handleChange = (event: React.ChangeEvent) => { + const val = event.target.value; + if (val.length <= 0) { + onValue(''); + } else { + onValue(val); + } + }; + + useEffect(() => { + onValue(data.apiBaseUrl); + }, [data.apiBaseUrl]); + + return ( +
+ {t('access')} +
+ + +
+
+ ); } diff --git a/pages/options/src/hooks/useUser.tsx b/pages/options/src/hooks/useUser.tsx index d29736d..52e3eff 100644 --- a/pages/options/src/hooks/useUser.tsx +++ b/pages/options/src/hooks/useUser.tsx @@ -17,7 +17,9 @@ export function useUser(props: IProps) { } axios(`${baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl}/api/v1/user/me`) .then(response => { - setUser(response); + if (response) { + setUser(response); + } }) .catch(() => { setUser({ id: '' }); From 6225903f489f15b2d3a023549a387fa98f719f34 Mon Sep 17 00:00:00 2001 From: hewenguang Date: Thu, 20 Nov 2025 14:32:05 +0800 Subject: [PATCH 8/8] fix(comment): fix comments --- packages/shared/lib/utils/axios.ts | 95 +++++++++++++++++--------- pages/options/src/advance/index.tsx | 2 + pages/options/src/i18n/locales/en.json | 3 +- pages/options/src/i18n/locales/zh.json | 3 +- 4 files changed, 69 insertions(+), 34 deletions(-) diff --git a/packages/shared/lib/utils/axios.ts b/packages/shared/lib/utils/axios.ts index 739b248..761afab 100644 --- a/packages/shared/lib/utils/axios.ts +++ b/packages/shared/lib/utils/axios.ts @@ -46,50 +46,81 @@ export function axios( }; } params.headers['From'] = 'extension'; + + // Set up timeout using AbortController + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); + const options: RequestInit = { body: params.body, redirect: 'manual', headers: params.headers, method: params.method || 'GET', credentials: 'include', + signal: controller.signal, }; - return fetch(params.url, options).then(response => { - if (!response.ok) { - if (response.type === 'opaqueredirect') { - const parsedUrl = new URL(params.url); - parsedUrl.hostname = `www.${parsedUrl.hostname}`; - return fetch(parsedUrl.toString(), options).then(innerResponse => { - if (!innerResponse.ok) { - return Promise.reject(new Error(`HTTP error! status: ${innerResponse.status}`)); - } else { - return innerResponse.text().then(data => { - if (!data) { - return null; + + return fetch(params.url, options) + .then(response => { + clearTimeout(timeoutId); + if (!response.ok) { + if (response.type === 'opaqueredirect') { + const parsedUrl = new URL(params.url); + parsedUrl.hostname = `www.${parsedUrl.hostname}`; + + // Create new controller for retry request + const retryController = new AbortController(); + const retryTimeoutId = setTimeout(() => retryController.abort(), 10000); + const retryOptions = { ...options, signal: retryController.signal }; + + return fetch(parsedUrl.toString(), retryOptions) + .then(innerResponse => { + clearTimeout(retryTimeoutId); + if (!innerResponse.ok) { + return Promise.reject(new Error(`HTTP error! status: ${innerResponse.status}`)); + } else { + return innerResponse.text().then(data => { + if (!data) { + return null; + } + try { + return JSON.parse(data); + } catch { + return null; + } + }); } - try { - return JSON.parse(data); - } catch { - return null; + }) + .catch(error => { + clearTimeout(retryTimeoutId); + if (error.name === 'AbortError') { + return Promise.reject(new Error('Request timeout after 10 seconds')); } + throw error; }); + } + if (response.status === 401) { + chrome.storage.sync.remove(['namespaceId', 'resourceId']); + } + return Promise.reject(new Error(`HTTP error! status: ${response.status}`)); + } else { + return response.text().then(data => { + if (!data) { + return null; + } + try { + return JSON.parse(data); + } catch { + return null; } }); } - if (response.status === 401) { - chrome.storage.sync.remove(['namespaceId', 'resourceId']); + }) + .catch(error => { + clearTimeout(timeoutId); + if (error.name === 'AbortError') { + return Promise.reject(new Error('Request timeout after 10 seconds')); } - return Promise.reject(new Error(`HTTP error! status: ${response.status}`)); - } else { - return response.text().then(data => { - if (!data) { - return null; - } - try { - return JSON.parse(data); - } catch { - return null; - } - }); - } - }); + throw error; + }); } diff --git a/pages/options/src/advance/index.tsx b/pages/options/src/advance/index.tsx index 5709a84..8d07665 100644 --- a/pages/options/src/advance/index.tsx +++ b/pages/options/src/advance/index.tsx @@ -1,3 +1,4 @@ +import { toast } from 'sonner'; import { useState, useEffect } from 'react'; import { axios } from '@extension/shared'; import { Input, Button } from '@extension/ui'; @@ -18,6 +19,7 @@ export function Advance(props: IProps) { resourceId: '', }); axios(`${value.endsWith('/') ? value.slice(0, -1) : value}/api/v1/user/me`); + toast(t('save_success'), { position: 'bottom-right' }); }; const handleChange = (event: React.ChangeEvent) => { const val = event.target.value; diff --git a/pages/options/src/i18n/locales/en.json b/pages/options/src/i18n/locales/en.json index 16d07d1..7ebdb7a 100644 --- a/pages/options/src/i18n/locales/en.json +++ b/pages/options/src/i18n/locales/en.json @@ -25,5 +25,6 @@ "error_occurred": "Error occurred", "shortcut_placeholder": "Enter shortcut", "hold_option_key": "Hold", - "save": "Save" + "save": "Save", + "save_success": "Saved successfully" } diff --git a/pages/options/src/i18n/locales/zh.json b/pages/options/src/i18n/locales/zh.json index 3011aea..d984be1 100644 --- a/pages/options/src/i18n/locales/zh.json +++ b/pages/options/src/i18n/locales/zh.json @@ -25,5 +25,6 @@ "error_occurred": "发生错误", "shortcut_placeholder": "输入快捷键", "hold_option_key": "按住", - "save": "保存" + "save": "保存", + "save_success": "保存成功" }