From 9a947d7431dbd9ea2d0a3cb3d2ee8b9f507a77ca Mon Sep 17 00:00:00 2001 From: LingandRX <56020800+LingandRX@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:05:34 +0800 Subject: [PATCH] Develop fastapi (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add side panel with navigation and storage cleaner functionality * feat: add OpenUrlPage and integrate into app navigation * feat: 添加 GlobalSnackbar 组件并在多个页面中集成,替换原有 Snackbar 实现 * feat: fix OpenUrl sidebar issue with architecture refactor - 修复原问题:不再直接替换侧边栏 URL,保持插件导航可见 - 采用配置页 + 查看页分离架构:OpenUrlPage (配置) + OpenUrlViewerPage (查看) - 支持多个 URL 快捷方式管理(添加/删除) - 每个 URL 提供两种打开方式:在侧边栏打开 / 在新标签页打开 - 侧边栏查看页使用 iframe 占满全部剩余空间 - 更新 TypeScript 类型定义 - 保留原有混合内容警告检查 - 数据持久化到 Chrome Storage * refactor: centralized route management - consolidate routing config into single source * feat: 更换logo * refactor: code review fixes - security and race condition improvements Co-Authored-By: Claude Opus 4.6 Changes: - wxt.config.ts: Remove unused `debugger` permission (no code uses it) - OpenUrlPage.tsx: Fix unreachable showMessage after window.close() - OpenUrlPage.tsx: Replace unnecessary div wrapper with Fragment to reduce DOM nesting - OpenUrlViewerPage.tsx: Add URL validation to prevent XSS via javascript:/data: URLs - OpenUrlViewerPage.tsx: Add sandbox attribute to iframe for security isolation - OpenUrlViewerPage.tsx: Add error handling for invalid URLs - StorageCleanerPage.tsx: Fix race condition in handleOptionChange preference saving - StorageCleanerPage.tsx: Remove unnecessary storage reads when saving preferences (use state directly) - StorageCleanerPage.tsx: Add timeout cleanup for setTimeout to follow React best practices * feat: implement drill-down navigation with master-detail dashboard * feat: enhance dashboard dynamism and refine options UI * feat: overhaul storage cleaner UI with real-time size estimation and modern aesthetics * feat: overhaul TimestampPage UI/UX and fix GlobalSnackbar positioning * fix: decouple popup routing from storage sync and enhance OpenUrlPage UI * fix: avoid closing sidepanel when opening URL preview from within sidepanel * fix: enhance storage cleaner size detection and optimize UI layout --- .../popup/pages/StorageCleanerPage.tsx | 68 ++++++++++-- utils/storageCleaner.ts | 100 ++++++++++++++++-- 2 files changed, 150 insertions(+), 18 deletions(-) diff --git a/entrypoints/popup/pages/StorageCleanerPage.tsx b/entrypoints/popup/pages/StorageCleanerPage.tsx index 8bd210e..a71a762 100644 --- a/entrypoints/popup/pages/StorageCleanerPage.tsx +++ b/entrypoints/popup/pages/StorageCleanerPage.tsx @@ -29,6 +29,9 @@ import { getCookieSize, getLocalStorageSize, getSessionStorageSize, + getIndexedDBSize, + getCacheStorageSize, + getServiceWorkerCount, formatSize, } from '@/utils/storageCleaner'; @@ -78,11 +81,14 @@ export default function StorageCleanerPage() { const tabId = tab.id!; setDomain(new URL(url).hostname); - const [savedPrefs, cSize, lsSize, ssSize] = await Promise.all([ + const [savedPrefs, cSize, lsSize, ssSize, idbSize, cacheCount, swCount] = await Promise.all([ storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES), getCookieSize(url), getLocalStorageSize(tabId), getSessionStorageSize(tabId), + getIndexedDBSize(tabId), + getCacheStorageSize(tabId), + getServiceWorkerCount(tabId), ]); setAutoRefresh(savedPrefs?.autoRefresh ?? DEFAULT_PREFERENCES.autoRefresh); @@ -91,6 +97,9 @@ export default function StorageCleanerPage() { cookies: cSize, localStorage: lsSize, sessionStorage: ssSize, + indexedDB: idbSize, + cacheStorage: cacheCount, + serviceWorkers: swCount, }); }; @@ -181,17 +190,20 @@ export default function StorageCleanerPage() { ); } - const totalSize = Object.values(sizes).reduce((acc, curr) => acc + curr, 0); + // 这里的总大小仅包含以字节计算的项 + const totalSize = (sizes.cookies || 0) + (sizes.localStorage || 0) + (sizes.sessionStorage || 0) + (sizes.indexedDB || 0); const OptionItem = ({ label, checked, size, + isCount = false, onChange, }: { label: string; checked: boolean; size?: number; + isCount?: boolean; onChange: () => void; }) => ( - + {label} - {size !== undefined && size > 0 && ( + {size !== undefined && size > 0 ? ( - {formatSize(size)} + {isCount ? `${size} 个` : formatSize(size)} + + ) : ( + + 无数据 )} - + handleOptionChange('indexedDB')} /> @@ -340,6 +382,8 @@ export default function StorageCleanerPage() { handleOptionChange('cacheStorage')} /> @@ -347,7 +391,9 @@ export default function StorageCleanerPage() { handleOptionChange('serviceWorkers')} + size={sizes.serviceWorkers} + isCount + onChange={() => handleOptionChange('serviceWorkers')} /> diff --git a/utils/storageCleaner.ts b/utils/storageCleaner.ts index 0a125ae..f097f60 100644 --- a/utils/storageCleaner.ts +++ b/utils/storageCleaner.ts @@ -11,7 +11,8 @@ const RESTRICTED_PROTOCOLS = [ ] as const; export async function getCurrentTab() { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + // 使用 lastFocusedWindow: true 确保在 Side Panel 或 Popup 中都能获取到用户正在查看的标签页 + const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); return tab; } @@ -23,8 +24,10 @@ export function isRestrictedUrl(url?: string): boolean { export async function getCookieSize(url: string): Promise { try { const cookies = await chrome.cookies.getAll({ url }); - return cookies.reduce((acc, c) => acc + c.name.length + c.value.length, 0); - } catch { + // 估算:名称 + 值 + 域名 + 路径 的长度 + return cookies.reduce((acc, c) => acc + c.name.length + c.value.length + (c.domain?.length || 0) + (c.path?.length || 0), 0); + } catch (error) { + console.error('Failed to get cookie size:', error); return 0; } } @@ -34,11 +37,16 @@ export async function getLocalStorageSize(tabId: number): Promise { const [result] = await chrome.scripting.executeScript({ target: { tabId }, func: () => { - return Object.entries(localStorage).reduce((acc, [k, v]) => acc + k.length + v.length, 0); + try { + return Object.entries(localStorage).reduce((acc, [k, v]) => acc + k.length + v.length, 0); + } catch { + return 0; + } }, }); return (result?.result as number) || 0; - } catch { + } catch (error) { + console.error('Failed to get LocalStorage size:', error); return 0; } } @@ -48,17 +56,95 @@ export async function getSessionStorageSize(tabId: number): Promise { const [result] = await chrome.scripting.executeScript({ target: { tabId }, func: () => { - return Object.entries(sessionStorage).reduce((acc, [k, v]) => acc + k.length + v.length, 0); + try { + return Object.entries(sessionStorage).reduce((acc, [k, v]) => acc + k.length + v.length, 0); + } catch { + return 0; + } }, }); return (result?.result as number) || 0; - } catch { + } catch (error) { + console.error('Failed to get SessionStorage size:', error); + return 0; + } +} + +export async function getIndexedDBSize(tabId: number): Promise { + try { + const [result] = await chrome.scripting.executeScript({ + target: { tabId }, + func: async () => { + try { + // 注意:navigator.storage.estimate() 返回的是整个 Origin 的估算值 + // 包含 IndexedDB, CacheStorage, ServiceWorker 注册等 + if (navigator.storage && navigator.storage.estimate) { + const estimate = await navigator.storage.estimate(); + return estimate.usage || 0; + } + return 0; + } catch { + return 0; + } + }, + }); + return (result?.result as number) || 0; + } catch (error) { + console.error('Failed to get IndexedDB size:', error); + return 0; + } +} + +export async function getCacheStorageSize(tabId: number): Promise { + try { + const [result] = await chrome.scripting.executeScript({ + target: { tabId }, + func: async () => { + try { + if ('caches' in window) { + const keys = await caches.keys(); + return keys.length; // 对于 CacheStorage,我们先返回缓存库的数量 + } + return 0; + } catch { + return 0; + } + }, + }); + // 由于获取具体字节数较慢,这里返回的是缓存条目的数量标识,UI 上可以特殊处理 + return (result?.result as number) || 0; + } catch (error) { + console.error('Failed to get CacheStorage size:', error); + return 0; + } +} + +export async function getServiceWorkerCount(tabId: number): Promise { + try { + const [result] = await chrome.scripting.executeScript({ + target: { tabId }, + func: async () => { + try { + if ('serviceWorker' in navigator) { + const regs = await navigator.serviceWorker.getRegistrations(); + return regs.length; + } + return 0; + } catch { + return 0; + } + }, + }); + return (result?.result as number) || 0; + } catch (error) { + console.error('Failed to get ServiceWorker count:', error); return 0; } } export function formatSize(bytes: number): string { if (bytes === 0) return '0 B'; + if (bytes < 1024) return `${bytes} B`; // 处理小于 1KB 的情况 const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k));