feat: add right-click restorer for current site

- Add RightClickRestorer page with active unlock trigger
- Content script with dual-world strategy (isolated + main world)
- Monkey patch preventDefault/stopPropagation via background executeScript
- Overlay penetration for pointer-events blocking layers
- Delegated main-world injection to avoid CSP restrictions
- Async message handling for reliable state updates
- i18n translations (zh/en) and unit tests
This commit is contained in:
雨霖铃
2026-05-23 12:39:14 +08:00
parent 2d7bb8fc2c
commit fa0f1bdcad
16 changed files with 570 additions and 13 deletions
+6 -5
View File
@@ -9,8 +9,8 @@ import {
describe('features', () => { describe('features', () => {
describe('FEATURES', () => { describe('FEATURES', () => {
it('should have 10 features defined', () => { it('should have 11 features defined', () => {
expect(FEATURES).toHaveLength(10); expect(FEATURES).toHaveLength(11);
}); });
it('should have all required properties for each feature', () => { it('should have all required properties for each feature', () => {
@@ -95,7 +95,7 @@ describe('features', () => {
describe('getAllFeatureKeys', () => { describe('getAllFeatureKeys', () => {
it('should return all feature keys', () => { it('should return all feature keys', () => {
const allKeys = getAllFeatureKeys(); const allKeys = getAllFeatureKeys();
expect(allKeys).toHaveLength(10); expect(allKeys).toHaveLength(11);
expect(allKeys).toContain('dashboard'); expect(allKeys).toContain('dashboard');
expect(allKeys).toContain('timestamp'); expect(allKeys).toContain('timestamp');
expect(allKeys).toContain('storageCleaner'); expect(allKeys).toContain('storageCleaner');
@@ -106,6 +106,7 @@ describe('features', () => {
expect(allKeys).toContain('base64Converter'); expect(allKeys).toContain('base64Converter');
expect(allKeys).toContain('markdownToHtml'); expect(allKeys).toContain('markdownToHtml');
expect(allKeys).toContain('htmlToMarkdown'); expect(allKeys).toContain('htmlToMarkdown');
expect(allKeys).toContain('rightClickRestorer');
}); });
}); });
@@ -122,9 +123,9 @@ describe('features', () => {
expect(pageOrder).toContain('qrCode'); expect(pageOrder).toContain('qrCode');
}); });
it('should have 9 items in page order', () => { it('should have 10 items in page order', () => {
const pageOrder = getDefaultPageOrder(); const pageOrder = getDefaultPageOrder();
expect(pageOrder).toHaveLength(9); expect(pageOrder).toHaveLength(10);
}); });
}); });
}); });
+15
View File
@@ -11,6 +11,7 @@ import {
ArrowLeftRight, ArrowLeftRight,
Code, Code,
File, File,
MousePointerClick,
} from 'lucide-react'; } from 'lucide-react';
export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info'; export type PaletteColorKey = 'primary' | 'success' | 'warning' | 'error' | 'secondary' | 'info';
@@ -26,6 +27,7 @@ const JsonToolsPage = lazy(() => import('@/pages/JsonTools'));
const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter')); const Base64ConverterPage = lazy(() => import('@/pages/Base64Converter'));
const MarkdownToHtmlPage = lazy(() => import('@/pages/MarkdownToHtml')); const MarkdownToHtmlPage = lazy(() => import('@/pages/MarkdownToHtml'));
const HtmlToMarkdownPage = lazy(() => import('@/pages/HtmlToMarkdown')); const HtmlToMarkdownPage = lazy(() => import('@/pages/HtmlToMarkdown'));
const RightClickRestorerPage = lazy(() => import('@/pages/RightClickRestorer'));
export interface FeatureConfig { export interface FeatureConfig {
key: PageType; key: PageType;
@@ -170,6 +172,19 @@ export const FEATURES: FeatureConfig[] = [
tab: HtmlToMarkdownPage, tab: HtmlToMarkdownPage,
}, },
}, },
{
key: 'rightClickRestorer',
labelKey: 'features:rightClickRestorer.title',
descriptionKey: 'features:rightClickRestorer.description',
themeColorKey: 'success',
icon: MousePointerClick,
defaultVisible: true,
components: {
popup: RightClickRestorerPage,
sidepanel: RightClickRestorerPage,
tab: RightClickRestorerPage,
},
},
]; ];
export function getFeatureByKey(key: PageType): FeatureConfig | undefined { export function getFeatureByKey(key: PageType): FeatureConfig | undefined {
+90 -1
View File
@@ -58,7 +58,96 @@ export default defineBackground(() => {
} }
}); });
// 3. 异步刷新请求监听(统一使用 alarms API,避免 MV3 Service Worker 被销毁导致任务丢失 // 3. 注入主环境脚本(content script 无权访问 chrome.tabs/scripting,委托 background 执行
onMessage(MessageAction.INJECT_MAIN_WORLD_SCRIPT, async (message) => {
const sender = message.sender as chrome.runtime.MessageSender | undefined;
const tabId = sender?.tab?.id;
if (!tabId) {
console.warn('[RightClickRestorer] Injection request missing tabId');
return { success: false, message: 'Missing tabId' };
}
try {
await browser.scripting.executeScript({
target: { tabId },
func: () => {
'use strict';
const w = window as unknown as Record<string, unknown>;
if (w.__testingToolsRightClickPatched) return;
w.__testingToolsRightClickPatched = true;
const PROTECTED = ['contextmenu', 'copy', 'paste', 'cut', 'selectstart'];
/* 1. 屏蔽 MouseEvent.prototype.preventDefault(含 mousedown 右键) */
const _origPreventDefault = MouseEvent.prototype.preventDefault;
Object.defineProperty(MouseEvent.prototype, 'preventDefault', {
value: function (this: MouseEvent) {
const t = this.type;
if (PROTECTED.includes(t) || (t === 'mousedown' && this.button === 2)) {
return;
}
return _origPreventDefault.call(this);
},
writable: true,
configurable: true,
});
/* 2. 屏蔽 Event.prototype.stopPropagation / stopImmediatePropagation */
const _origStopPropagation = Event.prototype.stopPropagation;
Object.defineProperty(Event.prototype, 'stopPropagation', {
value: function (this: Event) {
if (PROTECTED.includes(this.type)) return;
return _origStopPropagation.call(this);
},
writable: true,
configurable: true,
});
const _origStopImmediatePropagation = Event.prototype.stopImmediatePropagation;
Object.defineProperty(Event.prototype, 'stopImmediatePropagation', {
value: function (this: Event) {
if (PROTECTED.includes(this.type)) return;
return _origStopImmediatePropagation.call(this);
},
writable: true,
configurable: true,
});
/* 3. 拦截 document.oncontextmenu(处理 return false 方式) */
let _docOnContextMenu: unknown = null;
Object.defineProperty(document, 'oncontextmenu', {
get() {
return _docOnContextMenu;
},
set(fn: unknown) {
if (typeof fn === 'function') {
_docOnContextMenu = function (this: GlobalEventHandlers, e: MouseEvent) {
const r = (fn as (this: GlobalEventHandlers, ev: MouseEvent) => unknown).call(
this,
e,
);
return r === false ? true : r;
};
} else {
_docOnContextMenu = fn;
}
},
configurable: true,
});
},
world: 'MAIN',
});
return { success: true };
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
console.error('[RightClickRestorer] executeScript failed:', errorMsg);
return { success: false, message: errorMsg };
}
});
// 4. 异步刷新请求监听(统一使用 alarms API,避免 MV3 Service Worker 被销毁导致任务丢失)
onMessage(MessageAction.RELOAD_TAB, async (message) => { onMessage(MessageAction.RELOAD_TAB, async (message) => {
const { tabId, delay = 0 } = message.data; const { tabId, delay = 0 } = message.data;
+1 -6
View File
@@ -3,7 +3,6 @@ import { MessageAction, onMessage } from '@/utils/messages';
import { getTextStats } from '@/utils/textStatistics'; import { getTextStats } from '@/utils/textStatistics';
import { hidePopover, showTextStatsResult, showTimestampResult } from './uiPopover'; import { hidePopover, showTextStatsResult, showTimestampResult } from './uiPopover';
// 💡 1. 国际化超进化:对接 chrome.i18n 插件标准 API,如果环境不支持则安全降级,拒绝硬编码中文
function getI18nText(key: string, fallback: string): string { function getI18nText(key: string, fallback: string): string {
if (typeof chrome !== 'undefined' && chrome.i18n) { if (typeof chrome !== 'undefined' && chrome.i18n) {
return chrome.i18n.getMessage(key) || fallback; return chrome.i18n.getMessage(key) || fallback;
@@ -39,20 +38,16 @@ function convertTimestamp(input: string): string {
let lastClickX = 0; let lastClickX = 0;
let lastClickY = 0; let lastClickY = 0;
// 💡 使用 capture: true 确保在任何极其复杂的单页应用(SPA)中都能精准捕获右键坐标
document.addEventListener( document.addEventListener(
'contextmenu', 'contextmenu',
(e) => { (e) => {
lastClickX = e.clientX; lastClickX = e.clientX;
lastClickY = e.clientY; lastClickY = e.clientY;
}, },
{ capture: true, passive: true }, // 优化滚动与捕获性能 { capture: true, passive: true },
); );
export function initContextMenuHandler(): void { export function initContextMenuHandler(): void {
// 💡 2. 全局自净化大闸(Global Auto-Purge Grid):
// 当用户在网页上进行左键点击、滚动视视口、或调整大小时,
// 证明心流已经移开,自发隐退所有浮动的 Popover 弹窗,体验顺滑得丝丝入扣!
const dismissPopover = (): void => { const dismissPopover = (): void => {
hidePopover(); hidePopover();
}; };
+194
View File
@@ -0,0 +1,194 @@
import { MessageAction, onMessage, sendMessage } from '@/utils/messages';
const BADGE_ID = 'testing-tools-right-click-restorer-badge';
const BADGE_STYLE_ID = 'testing-tools-right-click-restorer-badge-style';
let isRestored = false;
function updateBadge(): void {
const badge = document.getElementById(BADGE_ID);
if (badge) {
badge.style.opacity = isRestored ? '1' : '0';
}
}
function createBadge(): void {
if (document.getElementById(BADGE_ID)) return;
if (!document.getElementById(BADGE_STYLE_ID)) {
const style = document.createElement('style');
style.id = BADGE_STYLE_ID;
style.textContent = `
#${BADGE_ID} {
position: fixed;
bottom: 16px;
right: 16px;
z-index: 2147483646;
padding: 6px 12px;
background: #2e7d32;
color: #ffffff;
border-radius: 20px;
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
opacity: 0;
transition: opacity 0.2s ease-out;
pointer-events: none;
user-select: none;
}
@media (prefers-color-scheme: dark) {
#${BADGE_ID} {
background: #4caf50;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
}
`;
document.head.appendChild(style);
}
const badge = document.createElement('div');
badge.id = BADGE_ID;
badge.textContent = '\u53f3\u952e\u5df2\u89e3\u9501';
document.body.appendChild(badge);
}
/* ======================== Main World 注入 ======================== */
async function injectMainWorldScript(): Promise<void> {
try {
const response = await sendMessage(MessageAction.INJECT_MAIN_WORLD_SCRIPT);
if (!response.success) {
console.error('[RightClickRestorer] Background injection failed:', response.message);
}
} catch (err) {
console.error('[RightClickRestorer] Failed to request main world injection:', err);
}
}
/* ======================== Isolated World 事件拦截 ======================== */
function installEventIntercepts(): void {
window.addEventListener(
'contextmenu',
(e) => {
if (!isRestored) return;
e.stopPropagation();
},
true,
);
// 部分网站在 mousedown 阶段阻止 contextmenu
window.addEventListener(
'mousedown',
(e) => {
if (!isRestored) return;
if (e.button === 2 || (e.buttons & 2) !== 0) {
e.stopPropagation();
}
},
true,
);
}
/* ======================== 遮罩层穿透 ======================== */
const MEDIA_TAGS = new Set(['IMG', 'VIDEO', 'CANVAS', 'SVG']);
function initMousePenetration(): void {
window.addEventListener(
'mousedown',
(e) => {
if (!isRestored || e.button !== 2) return;
const elements = document.elementsFromPoint(e.clientX, e.clientY);
if (!elements.length) return;
let targetMedia: HTMLElement | null = null;
for (const el of elements) {
if (MEDIA_TAGS.has(el.tagName)) {
targetMedia = el as HTMLElement;
break;
}
}
if (!targetMedia) return;
const modified: Array<{ el: HTMLElement; original: string | null }> = [];
let foundMedia = false;
for (const el of elements) {
const htmlEl = el as HTMLElement;
if (el === targetMedia) {
foundMedia = true;
const original = htmlEl.style.pointerEvents || null;
htmlEl.style.setProperty('pointer-events', 'all', 'important');
modified.push({ el: htmlEl, original });
continue;
}
if (!foundMedia) {
const original = htmlEl.style.pointerEvents || null;
htmlEl.style.setProperty('pointer-events', 'none', 'important');
modified.push({ el: htmlEl, original });
}
}
setTimeout(() => {
for (const { el, original } of modified) {
if (original === null || original === '') {
el.style.removeProperty('pointer-events');
} else {
el.style.pointerEvents = original;
}
}
}, 300);
},
true,
);
}
/* ======================== 激活保护 ======================== */
async function activateProtection(): Promise<void> {
if (isRestored) return;
await injectMainWorldScript();
// 再次检查,防止并发调用导致重复注册
if (isRestored) return;
installEventIntercepts();
initMousePenetration();
isRestored = true;
updateBadge();
}
/* ======================== 消息通信 ======================== */
function initMessaging(): void {
onMessage(MessageAction.RESTORE_RIGHT_CLICK, async () => {
await activateProtection();
return { success: true, restored: isRestored };
});
onMessage(MessageAction.QUERY_RIGHT_CLICK_STATUS, () => {
return { success: true, restored: isRestored };
});
}
/* ======================== 入口 ======================== */
export default defineContentScript({
matches: ['<all_urls>'],
runAt: 'document_start',
main() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createBadge, { once: true });
} else {
createBadge();
}
initMessaging();
},
});
+4
View File
@@ -37,5 +37,9 @@
"htmlToMarkdown": { "htmlToMarkdown": {
"title": "HTML to Markdown", "title": "HTML to Markdown",
"description": "Real-time HTML conversion and Markdown preview" "description": "Real-time HTML conversion and Markdown preview"
},
"rightClickRestorer": {
"title": "Right Click Restorer",
"description": "Detect and restore disabled browser right-click menus"
} }
} }
+11
View File
@@ -0,0 +1,11 @@
{
"title": "Right Click Restorer",
"description": "Detect and restore disabled browser right-click menus",
"loading": "Loading...",
"currentDomain": "Current Domain",
"statusLocked": "Locked",
"statusUnlocked": "Unlocked",
"unlockDesc": "Click the button below to temporarily unlock the right-click menu for the current website. You will need to unlock again after refreshing the page.",
"unlockBtn": "Unlock Right Click",
"alreadyUnlocked": "Right Click Unlocked"
}
+4
View File
@@ -37,5 +37,9 @@
"htmlToMarkdown": { "htmlToMarkdown": {
"title": "HTML 转 Markdown", "title": "HTML 转 Markdown",
"description": "实时 HTML 转换与 Markdown 预览" "description": "实时 HTML 转换与 Markdown 预览"
},
"rightClickRestorer": {
"title": "右键恢复",
"description": "检测并恢复被网站禁用的浏览器右键菜单"
} }
} }
+11
View File
@@ -0,0 +1,11 @@
{
"title": "右键恢复",
"description": "检测并恢复被网站禁用的浏览器右键菜单",
"loading": "正在加载...",
"currentDomain": "当前域名",
"statusLocked": "未解锁",
"statusUnlocked": "已解锁",
"unlockDesc": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。",
"unlockBtn": "解锁当前网站右键",
"alreadyUnlocked": "右键已解锁"
}
@@ -0,0 +1,37 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import RightClickRestorerPage from '../index';
const mockUnlock = vi.fn();
vi.mock('../useRightClickRestorer', () => ({
useRightClickRestorer: () => ({
domain: 'example.com',
isLoading: false,
isUnlocked: false,
unlock: mockUnlock,
}),
}));
describe('RightClickRestorerPage', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render current domain', () => {
render(<RightClickRestorerPage />);
expect(screen.getByText(/example\.com/)).toBeInTheDocument();
});
it('should render locked status', () => {
render(<RightClickRestorerPage />);
expect(screen.getByText(/statusLocked/)).toBeInTheDocument();
});
it('should call unlock when button clicked', () => {
render(<RightClickRestorerPage />);
const button = screen.getByRole('button');
fireEvent.click(button);
expect(mockUnlock).toHaveBeenCalled();
});
});
@@ -0,0 +1,62 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { useRightClickRestorer } from '../useRightClickRestorer';
import { sendMessageToContent } from '@/utils/messages';
const mockTabsQuery = vi.fn();
vi.mock('@/utils/messages', () => ({
MessageAction: {
RESTORE_RIGHT_CLICK: 'restoreRightClick',
QUERY_RIGHT_CLICK_STATUS: 'queryRightClickStatus',
},
sendMessageToContent: vi.fn(),
}));
beforeEach(() => {
vi.clearAllMocks();
mockTabsQuery.mockResolvedValue([{ url: 'https://example.com/path' }]);
chrome.tabs.query = mockTabsQuery;
vi.mocked(sendMessageToContent).mockResolvedValue({ success: true, restored: false });
});
describe('useRightClickRestorer', () => {
it('should load domain and query status', async () => {
const { result } = renderHook(() => useRightClickRestorer());
await waitFor(() => expect(result.current.isLoading).toBe(false));
expect(result.current.domain).toBe('example.com');
expect(result.current.isUnlocked).toBe(false);
expect(sendMessageToContent).toHaveBeenCalledWith('queryRightClickStatus');
});
it('should unlock right click', async () => {
vi.mocked(sendMessageToContent).mockResolvedValueOnce({ success: true, restored: false });
vi.mocked(sendMessageToContent).mockResolvedValueOnce({ success: true, restored: true });
const { result } = renderHook(() => useRightClickRestorer());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.unlock();
});
expect(result.current.isUnlocked).toBe(true);
expect(sendMessageToContent).toHaveBeenLastCalledWith('restoreRightClick');
});
it('should handle sendMessage failure gracefully', async () => {
vi.mocked(sendMessageToContent).mockRejectedValue(new Error('Connection failed'));
const { result } = renderHook(() => useRightClickRestorer());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.unlock();
});
expect(result.current.isUnlocked).toBe(false);
});
});
+64
View File
@@ -0,0 +1,64 @@
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Shield, ShieldCheck, MousePointerClick } from 'lucide-react';
import { useRightClickRestorer } from './useRightClickRestorer';
import { useLazyTranslation } from '@/utils/useLazyTranslation';
export default function RightClickRestorerPage() {
const { t } = useLazyTranslation('rightClickRestorer');
const { domain, isLoading, isUnlocked, unlock } = useRightClickRestorer();
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center py-12 min-h-[280px] w-full animate-in fade-in duration-200">
<span className="text-xs text-muted-foreground mt-2 font-medium tracking-wide">
{t('rightClickRestorer:loading')}
</span>
</div>
);
}
return (
<div className="p-4 w-full flex flex-col space-y-4 animate-in fade-in duration-300">
{/* Current Domain */}
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden transition-all">
<div className="p-4">
<Label className="text-sm font-medium">{t('rightClickRestorer:currentDomain')}</Label>
<div className="mt-2 flex items-center justify-between">
<code className="text-sm bg-muted px-2 py-1 rounded">{domain || '—'}</code>
{isUnlocked ? (
<Badge variant="default" className="gap-1 bg-green-600 hover:bg-green-700">
<ShieldCheck className="h-3 w-3" />
{t('rightClickRestorer:statusUnlocked')}
</Badge>
) : (
<Badge variant="secondary" className="gap-1">
<Shield className="h-3 w-3" />
{t('rightClickRestorer:statusLocked')}
</Badge>
)}
</div>
</div>
</div>
{/* Unlock Action */}
<div className="w-full rounded-xl border border-border bg-card text-card-foreground shadow-sm overflow-hidden transition-all">
<div className="p-4 space-y-3">
<p className="text-xs text-muted-foreground">{t('rightClickRestorer:unlockDesc')}</p>
<Button
className="w-full gap-2"
onClick={() => void unlock()}
disabled={isUnlocked}
variant={isUnlocked ? 'secondary' : 'default'}
>
<MousePointerClick className="h-4 w-4" />
{isUnlocked
? t('rightClickRestorer:alreadyUnlocked')
: t('rightClickRestorer:unlockBtn')}
</Button>
</div>
</div>
</div>
);
}
@@ -0,0 +1,59 @@
import { useCallback, useEffect, useState } from 'react';
import { MessageAction, sendMessageToContent } from '@/utils/messages';
export interface UseRightClickRestorerReturn {
domain: string;
isLoading: boolean;
isUnlocked: boolean;
unlock: () => Promise<void>;
}
export function useRightClickRestorer(): UseRightClickRestorerReturn {
const [domain, setDomain] = useState<string>('');
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isUnlocked, setIsUnlocked] = useState<boolean>(false);
useEffect(() => {
const load = async () => {
try {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (tab?.url) {
try {
setDomain(new URL(tab.url).hostname);
} catch {
setDomain('');
}
}
const response = await sendMessageToContent(MessageAction.QUERY_RIGHT_CLICK_STATUS);
if (response?.success) {
setIsUnlocked(response.restored);
}
} catch (err) {
console.error('[RightClickRestorer] Failed to load state:', err);
} finally {
setIsLoading(false);
}
};
void load();
}, []);
const unlock = useCallback(async () => {
try {
const response = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
if (response?.success) {
setIsUnlocked(response.restored);
}
} catch (err) {
console.error('[RightClickRestorer] Failed to unlock:', err);
}
}, []);
return {
domain,
isLoading,
isUnlocked,
unlock,
};
}
+2 -1
View File
@@ -11,7 +11,8 @@ export type PageType =
| 'jsonDiff' // JSON 差异比较工具 | 'jsonDiff' // JSON 差异比较工具
| 'base64Converter' // Base64 转换器工具 | 'base64Converter' // Base64 转换器工具
| 'markdownToHtml' // Markdown 转 HTML 工具 | 'markdownToHtml' // Markdown 转 HTML 工具
| 'htmlToMarkdown'; // HTML 转 Markdown 工具 | 'htmlToMarkdown' // HTML 转 Markdown 工具
| 'rightClickRestorer'; // 右键菜单恢复工具
/** /**
* JSON 工具页面子模式类型定义 * JSON 工具页面子模式类型定义
+8
View File
@@ -4,6 +4,9 @@ export enum MessageAction {
RELOAD_TAB = 'reloadTab', RELOAD_TAB = 'reloadTab',
SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged', SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged',
CONTEXT_MENU_CLICKED = 'contextMenuClicked', CONTEXT_MENU_CLICKED = 'contextMenuClicked',
RESTORE_RIGHT_CLICK = 'restoreRightClick',
QUERY_RIGHT_CLICK_STATUS = 'queryRightClickStatus',
INJECT_MAIN_WORLD_SCRIPT = 'injectMainWorldScript',
} }
export interface MessageResponse { export interface MessageResponse {
@@ -21,6 +24,11 @@ export interface ProtocolMap {
[MessageAction.RELOAD_TAB](data: { tabId: number; delay?: number }): MessageResponse; [MessageAction.RELOAD_TAB](data: { tabId: number; delay?: number }): MessageResponse;
[MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void; [MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void;
[MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void; [MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void;
[MessageAction.RESTORE_RIGHT_CLICK](data: undefined): MessageResponse & { restored: boolean };
[MessageAction.QUERY_RIGHT_CLICK_STATUS](
data: undefined,
): MessageResponse & { restored: boolean };
[MessageAction.INJECT_MAIN_WORLD_SCRIPT](data: undefined): MessageResponse;
} }
export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>(); export const { sendMessage, onMessage } = defineExtensionMessaging<ProtocolMap>();
+2
View File
@@ -18,6 +18,7 @@ const localeModules: Record<
base64Converter: () => import('@/i18n/locales/zh/base64Converter.json'), base64Converter: () => import('@/i18n/locales/zh/base64Converter.json'),
markdownToHtml: () => import('@/i18n/locales/zh/markdownToHtml.json'), markdownToHtml: () => import('@/i18n/locales/zh/markdownToHtml.json'),
htmlToMarkdown: () => import('@/i18n/locales/zh/htmlToMarkdown.json'), htmlToMarkdown: () => import('@/i18n/locales/zh/htmlToMarkdown.json'),
rightClickRestorer: () => import('@/i18n/locales/zh/rightClickRestorer.json'),
}, },
en: { en: {
timestamp: () => import('@/i18n/locales/en/timestamp.json'), timestamp: () => import('@/i18n/locales/en/timestamp.json'),
@@ -30,6 +31,7 @@ const localeModules: Record<
base64Converter: () => import('@/i18n/locales/en/base64Converter.json'), base64Converter: () => import('@/i18n/locales/en/base64Converter.json'),
markdownToHtml: () => import('@/i18n/locales/en/markdownToHtml.json'), markdownToHtml: () => import('@/i18n/locales/en/markdownToHtml.json'),
htmlToMarkdown: () => import('@/i18n/locales/en/htmlToMarkdown.json'), htmlToMarkdown: () => import('@/i18n/locales/en/htmlToMarkdown.json'),
rightClickRestorer: () => import('@/i18n/locales/en/rightClickRestorer.json'),
}, },
}; };