diff --git a/i18n/locales/en/rightClickRestorer.json b/i18n/locales/en/rightClickRestorer.json index 7bcee1a..897a416 100644 --- a/i18n/locales/en/rightClickRestorer.json +++ b/i18n/locales/en/rightClickRestorer.json @@ -5,6 +5,8 @@ "currentDomain": "Current Domain", "statusLocked": "Locked", "statusUnlocked": "Unlocked", + "unsupported": "Unsupported", + "unsupportedDesc": "The current page is a browser internal or extension page. Right-click unlock is not available. Please switch to a regular webpage.", "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" diff --git a/i18n/locales/zh/rightClickRestorer.json b/i18n/locales/zh/rightClickRestorer.json index fb5cb85..4c6bd34 100644 --- a/i18n/locales/zh/rightClickRestorer.json +++ b/i18n/locales/zh/rightClickRestorer.json @@ -5,6 +5,8 @@ "currentDomain": "当前域名", "statusLocked": "未解锁", "statusUnlocked": "已解锁", + "unsupported": "不支持", + "unsupportedDesc": "当前页面为浏览器内部页面或扩展页面,无法解锁右键功能。请切换到普通网页后重试。", "unlockDesc": "点击下方按钮,为当前网站临时解锁右键菜单。刷新页面后需要重新解锁。", "unlockBtn": "解锁当前网站右键", "alreadyUnlocked": "右键已解锁" diff --git a/pages/RightClickRestorer/__tests__/index.test.tsx b/pages/RightClickRestorer/__tests__/index.test.tsx index b355971..bc450b4 100644 --- a/pages/RightClickRestorer/__tests__/index.test.tsx +++ b/pages/RightClickRestorer/__tests__/index.test.tsx @@ -9,6 +9,7 @@ vi.mock('../useRightClickRestorer', () => ({ domain: 'example.com', isLoading: false, isUnlocked: false, + isUnsupported: false, unlock: mockUnlock, }), })); diff --git a/pages/RightClickRestorer/__tests__/useRightClickRestorer.test.ts b/pages/RightClickRestorer/__tests__/useRightClickRestorer.test.ts index 4f1b414..929051e 100644 --- a/pages/RightClickRestorer/__tests__/useRightClickRestorer.test.ts +++ b/pages/RightClickRestorer/__tests__/useRightClickRestorer.test.ts @@ -27,9 +27,23 @@ describe('useRightClickRestorer', () => { expect(result.current.domain).toBe('example.com'); expect(result.current.isUnlocked).toBe(false); + expect(result.current.isUnsupported).toBe(false); expect(sendMessageToContent).toHaveBeenCalledWith('queryRightClickStatus'); }); + it('should mark internal pages as unsupported', async () => { + mockTabsQuery.mockResolvedValue([{ url: 'chrome://newtab/' }]); + chrome.tabs.query = mockTabsQuery; + + const { result } = renderHook(() => useRightClickRestorer()); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.isUnsupported).toBe(true); + expect(result.current.isUnlocked).toBe(false); + expect(sendMessageToContent).not.toHaveBeenCalled(); + }); + it('should unlock right click', async () => { vi.mocked(sendMessageToContent).mockResolvedValueOnce({ success: true, restored: false }); vi.mocked(sendMessageToContent).mockResolvedValueOnce({ success: true, restored: true }); @@ -46,6 +60,22 @@ describe('useRightClickRestorer', () => { expect(sendMessageToContent).toHaveBeenLastCalledWith('restoreRightClick'); }); + it('should not unlock unsupported pages', async () => { + mockTabsQuery.mockResolvedValue([{ url: 'chrome://settings/' }]); + chrome.tabs.query = mockTabsQuery; + + 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); + expect(sendMessageToContent).not.toHaveBeenCalled(); + }); + it('should handle sendMessage failure gracefully', async () => { vi.mocked(sendMessageToContent).mockRejectedValue(new Error('Connection failed')); diff --git a/pages/RightClickRestorer/index.tsx b/pages/RightClickRestorer/index.tsx index 4b49ccb..31194c2 100644 --- a/pages/RightClickRestorer/index.tsx +++ b/pages/RightClickRestorer/index.tsx @@ -1,13 +1,13 @@ 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 { Shield, ShieldCheck, MousePointerClick, AlertTriangle } 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(); + const { domain, isLoading, isUnlocked, isUnsupported, unlock } = useRightClickRestorer(); if (isLoading) { return ( @@ -29,7 +29,12 @@ export default function RightClickRestorerPage() { {domain || '—'} - {isUnlocked ? ( + {isUnsupported ? ( + + + {t('rightClickRestorer:unsupported')} + + ) : isUnlocked ? ( {t('rightClickRestorer:statusUnlocked')} @@ -47,18 +52,32 @@ export default function RightClickRestorerPage() { {/* Unlock Action */}
-

{t('rightClickRestorer:unlockDesc')}

- + {isUnsupported ? ( + <> +

+ {t('rightClickRestorer:unsupportedDesc')} +

+ + + ) : ( + <> +

{t('rightClickRestorer:unlockDesc')}

+ + + )}
diff --git a/pages/RightClickRestorer/useRightClickRestorer.ts b/pages/RightClickRestorer/useRightClickRestorer.ts index 558d42b..f7abeb4 100644 --- a/pages/RightClickRestorer/useRightClickRestorer.ts +++ b/pages/RightClickRestorer/useRightClickRestorer.ts @@ -1,10 +1,29 @@ import { useCallback, useEffect, useState } from 'react'; import { MessageAction, sendMessageToContent } from '@/utils/messages'; +const UNSUPPORTED_PROTOCOLS = new Set([ + 'chrome:', + 'chrome-extension:', + 'about:', + 'edge:', + 'brave:', +]); + +function isUnsupportedPage(url: string | undefined): boolean { + if (!url) return true; + try { + const protocol = new URL(url).protocol; + return UNSUPPORTED_PROTOCOLS.has(protocol); + } catch { + return true; + } +} + export interface UseRightClickRestorerReturn { domain: string; isLoading: boolean; isUnlocked: boolean; + isUnsupported: boolean; unlock: () => Promise; } @@ -12,19 +31,28 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { const [domain, setDomain] = useState(''); const [isLoading, setIsLoading] = useState(true); const [isUnlocked, setIsUnlocked] = useState(false); + const [isUnsupported, setIsUnsupported] = useState(false); useEffect(() => { const load = async () => { try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (tab?.url) { + const url = tab?.url; + + if (url) { try { - setDomain(new URL(tab.url).hostname); + setDomain(new URL(url).hostname); } catch { setDomain(''); } } + if (isUnsupportedPage(url)) { + setIsUnsupported(true); + setIsLoading(false); + return; + } + const response = await sendMessageToContent(MessageAction.QUERY_RIGHT_CLICK_STATUS); if (response?.success) { setIsUnlocked(response.restored); @@ -40,6 +68,8 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { }, []); const unlock = useCallback(async () => { + if (isUnsupported) return; + try { const response = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK); if (response?.success) { @@ -48,12 +78,13 @@ export function useRightClickRestorer(): UseRightClickRestorerReturn { } catch (err) { console.error('[RightClickRestorer] Failed to unlock:', err); } - }, []); + }, [isUnsupported]); return { domain, isLoading, isUnlocked, + isUnsupported, unlock, }; }