diff --git a/config/routes.ts b/config/routes.ts index 6490ae9..ebed160 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -56,3 +56,7 @@ export function getDefaultVisibleRoutes(): PageType[] { export function getAllRouteKeys(): PageType[] { return ROUTES.map(route => route.key); } + +export function getDefaultPageOrder(): PageType[] { + return ROUTES.filter(route => route.key !== 'dashboard' && route.key !== 'openUrlViewer').map(route => route.key); +} diff --git a/entrypoints/options/App.tsx b/entrypoints/options/App.tsx index 8b7c235..c478cd6 100644 --- a/entrypoints/options/App.tsx +++ b/entrypoints/options/App.tsx @@ -9,14 +9,18 @@ import { Alert, CircularProgress, Stack, + IconButton, } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import type { PageType } from '@/types/storage'; import { storageUtil } from '@/utils/chromeStorage'; -import { ROUTES } from '@/config/routes'; +import { getRouteByKey, getDefaultPageOrder } from '@/config/routes'; function App() { const [visiblePages, setVisiblePages] = useState([]); + const [pageOrder, setPageOrder] = useState([]); const [isLoaded, setIsLoaded] = useState(false); const [toast, setToast] = useState(null); const [toastSeverity, setToastSeverity] = useState<'success' | 'info' | 'warning'>('info'); @@ -27,15 +31,20 @@ function App() { const loadConfig = async () => { try { - const saved = await storageUtil.get('app/visiblePages', [ - 'timestamp', - 'storageCleaner', - 'openUrl', - ] as PageType[]); - setVisiblePages(saved ?? ['timestamp', 'storageCleaner', 'openUrl']); + const [savedVisible, savedOrder] = await Promise.all([ + storageUtil.get('app/visiblePages', [ + 'timestamp', + 'storageCleaner', + 'openUrl', + ] as PageType[]), + storageUtil.get('app/pageOrder', getDefaultPageOrder()), + ]); + setVisiblePages(savedVisible ?? ['timestamp', 'storageCleaner', 'openUrl']); + setPageOrder(savedOrder && savedOrder.length > 0 ? savedOrder : getDefaultPageOrder()); } catch (error) { console.error('Failed to load config:', error); setVisiblePages(['timestamp', 'storageCleaner', 'openUrl']); + setPageOrder(getDefaultPageOrder()); } finally { setIsLoaded(true); } @@ -58,7 +67,7 @@ function App() { try { await storageUtil.set('app/visiblePages', newPages); setVisiblePages(newPages); - const route = ROUTES.find((r) => r.key === page); + const route = getRouteByKey(page); showToast(`已${isCurrentlyVisible ? '隐藏' : '显示'} ${route?.label || page}`, 'success'); } catch (error) { console.error('Failed to save config:', error); @@ -66,11 +75,36 @@ function App() { } }; + const handleMove = async (index: number, direction: 'up' | 'down') => { + if (direction === 'up' && index === 0) return; + if (direction === 'down' && index === pageOrder.length - 1) return; + + const newOrder = [...pageOrder]; + const swapIndex = direction === 'up' ? index - 1 : index + 1; + [newOrder[index], newOrder[swapIndex]] = [newOrder[swapIndex], newOrder[index]]; + + try { + await storageUtil.set('app/pageOrder', newOrder); + setPageOrder(newOrder); + } catch (error) { + console.error('Failed to save order:', error); + showToast('排序保存失败', 'warning'); + } + }; + const handleRestoreDefaults = async () => { try { - const defaults = ROUTES.filter((route) => route.defaultVisible).map((route) => route.key); - await storageUtil.set('app/visiblePages', defaults); + const { getDefaultVisibleRoutes } = await import('@/config/routes'); + const defaults = getDefaultVisibleRoutes(); + const defaultOrder = getDefaultPageOrder(); + + await Promise.all([ + storageUtil.set('app/visiblePages', defaults), + storageUtil.set('app/pageOrder', defaultOrder), + ]); + setVisiblePages(defaults); + setPageOrder(defaultOrder); showToast('已恢复默认', 'success'); } catch (error) { console.error('Failed to restore defaults:', error); @@ -120,43 +154,62 @@ function App() { }} > - {ROUTES.filter((route) => route.key !== 'dashboard' && route.key !== 'openUrlViewer').map( - (route, index, array) => { - const isChecked = visiblePages.includes(route.key); - const isDisabled = isChecked && visiblePages.length === 1; + {pageOrder.map((key, index, array) => { + const route = getRouteByKey(key); + if (!route) return null; - return ( - - - - {route.label} - - - {isChecked ? '已在 Dashboard 启用' : '已在 Dashboard 隐藏'} - - + const isChecked = visiblePages.includes(key); + const isDisabled = isChecked && visiblePages.length === 1; + + return ( + + + + {route.label} + + + {isChecked ? '已在 Dashboard 启用' : '已在 Dashboard 隐藏'} + + + + handleMove(index, 'up')} + disabled={index === 0} + sx={{ color: 'text.secondary' }} + > + + + handleMove(index, 'down')} + disabled={index === array.length - 1} + sx={{ color: 'text.secondary' }} + > + + handlePageToggle(route.key)} + onChange={() => handlePageToggle(key)} disabled={isDisabled} /> - ); - }, - )} + + ); + })} diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index b45eacf..a9e55e7 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -9,7 +9,7 @@ function App() { }; return ( - +
diff --git a/entrypoints/popup/index.html b/entrypoints/popup/index.html index ed4cb94..42d17e8 100644 --- a/entrypoints/popup/index.html +++ b/entrypoints/popup/index.html @@ -3,7 +3,7 @@ - Default Popup Title + 我是独立窗口 diff --git a/entrypoints/popup/pages/DashboardPage.tsx b/entrypoints/popup/pages/DashboardPage.tsx index 89c7da9..e76ad73 100644 --- a/entrypoints/popup/pages/DashboardPage.tsx +++ b/entrypoints/popup/pages/DashboardPage.tsx @@ -9,7 +9,7 @@ import { useEffect, useState } from 'react'; import dayjs from '@/utils/dayjs'; export default function DashboardPage() { - const { navigateTo, visiblePages } = useRouter(); + const { navigateTo, visiblePages, pageOrder } = useRouter(); const [now, setNow] = useState(dayjs()); useEffect(() => { @@ -19,58 +19,68 @@ export default function DashboardPage() { const isVisible = (key: string) => visiblePages.includes(key as PageType); + const renderCard = (key: PageType) => { + switch (key) { + case 'timestamp': + return ( + } + onClick={() => navigateTo('timestamp')} + snapshot={ + + + {now.valueOf()} + + + {now.format('HH:mm:ss')} + + + } + /> + ); + case 'storageCleaner': + return ( + } + onClick={() => navigateTo('storageCleaner')} + /> + ); + case 'openUrl': + return ( + } + onClick={() => navigateTo('openUrl')} + /> + ); + default: + return null; + } + }; + return ( - {isVisible('timestamp') && ( - } - onClick={() => navigateTo('timestamp')} - snapshot={ - - - {now.valueOf()} - - - {now.format('HH:mm:ss')} - - - } - /> - )} - - {isVisible('storageCleaner') && ( - } - onClick={() => navigateTo('storageCleaner')} - /> - )} - - {isVisible('openUrl') && ( - } - onClick={() => navigateTo('openUrl')} - /> - )} + {pageOrder.map((key) => (isVisible(key) ? renderCard(key) : null))} diff --git a/entrypoints/sidepanel/App.tsx b/entrypoints/sidepanel/App.tsx index ff45fd0..3738505 100644 --- a/entrypoints/sidepanel/App.tsx +++ b/entrypoints/sidepanel/App.tsx @@ -9,7 +9,7 @@ function App() { }; return ( - +
diff --git a/providers/RouterProvider.tsx b/providers/RouterProvider.tsx index 7c653f4..c272034 100644 --- a/providers/RouterProvider.tsx +++ b/providers/RouterProvider.tsx @@ -1,17 +1,19 @@ import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; -import type { PageType } from '@/types/storage'; +import type { PageType, StorageSchema } from '@/types/storage'; import { storageUtil } from '@/utils/chromeStorage'; -import { getDefaultVisibleRoutes } from '@/config/routes'; +import { getDefaultVisibleRoutes, getDefaultPageOrder } from '@/config/routes'; interface RouterContextType { currentPage: PageType; visiblePages: PageType[]; + pageOrder: PageType[]; isLoaded: boolean; navigateTo: (page: PageType) => void; navigateLocal: (page: PageType) => void; syncNavigation: (page: PageType) => void; goBack: () => void; setVisiblePages: (pages: PageType[]) => void; + setPageOrder: (pages: PageType[]) => void; } const RouterContext = createContext(null); @@ -20,34 +22,37 @@ interface RouterProviderProps { children: ReactNode; defaultRoute?: PageType; syncRoute?: boolean; + syncKey?: keyof StorageSchema; } export function RouterProvider({ children, defaultRoute = 'dashboard', - syncRoute = true + syncRoute = true, + syncKey = 'app/currentRoute' }: RouterProviderProps) { const [currentPage, setCurrentPage] = useState(defaultRoute); const [visiblePages, setVisiblePages] = useState(getDefaultVisibleRoutes()); + const [pageOrder, setPageOrder] = useState(getDefaultPageOrder()); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { loadInitialData(); - }, []); + }, [syncKey]); useEffect(() => { if (isLoaded && syncRoute) { - storageUtil.set('app/currentRoute', currentPage); + storageUtil.set(syncKey, currentPage as any); } - }, [currentPage, isLoaded, syncRoute]); + }, [currentPage, isLoaded, syncRoute, syncKey]); // Listen for storage changes if sync is enabled useEffect(() => { if (!syncRoute) return; const handleStorageChange = (changes: { [key: string]: chrome.storage.StorageChange }) => { - if (changes['app/currentRoute']) { - const newRoute = changes['app/currentRoute'].newValue as PageType; + if (changes[syncKey as string]) { + const newRoute = changes[syncKey as string].newValue as PageType; if (newRoute && newRoute !== currentPage) { setCurrentPage(newRoute); } @@ -56,21 +61,25 @@ export function RouterProvider({ chrome.storage.onChanged.addListener(handleStorageChange); return () => chrome.storage.onChanged.removeListener(handleStorageChange); - }, [syncRoute, currentPage]); + }, [syncRoute, currentPage, syncKey]); const loadInitialData = async () => { try { - const [savedRoute, savedVisiblePages] = await Promise.all([ - storageUtil.get('app/currentRoute', defaultRoute), + const [savedRoute, savedVisiblePages, savedPageOrder] = await Promise.all([ + storageUtil.get(syncKey, defaultRoute), storageUtil.get('app/visiblePages', getDefaultVisibleRoutes()), + storageUtil.get('app/pageOrder', getDefaultPageOrder()), ]); if (savedRoute && syncRoute) { - setCurrentPage(savedRoute); + setCurrentPage(savedRoute as PageType); } if (savedVisiblePages) { setVisiblePages(savedVisiblePages); } + if (savedPageOrder && savedPageOrder.length > 0) { + setPageOrder(savedPageOrder); + } } catch (error) { console.error('Failed to load initial routing data:', error); } finally { @@ -87,7 +96,7 @@ export function RouterProvider({ }; const syncNavigation = (page: PageType) => { - storageUtil.set('app/currentRoute', page); + storageUtil.set(syncKey, page as any); }; const goBack = () => { @@ -99,12 +108,14 @@ export function RouterProvider({ value={{ currentPage, visiblePages, + pageOrder, isLoaded, navigateTo, navigateLocal, syncNavigation, goBack, - setVisiblePages + setVisiblePages, + setPageOrder }} > {children} diff --git a/types/storage.d.ts b/types/storage.d.ts index b40c5a9..925ba49 100644 --- a/types/storage.d.ts +++ b/types/storage.d.ts @@ -2,7 +2,10 @@ export type PageType = 'dashboard' | 'timestamp' | 'storageCleaner' | 'openUrl' export interface StorageSchema { 'app/currentRoute': PageType; + 'app/popupRoute': PageType; + 'app/sidepanelRoute': PageType; 'app/visiblePages': PageType[]; + 'app/pageOrder': PageType[]; 'app/lastRoute': string; 'app/theme': string; 'storageCleaner/preferences': StorageCleanerPreferences; diff --git a/utils/storageCleaner.ts b/utils/storageCleaner.ts index f097f60..4340734 100644 --- a/utils/storageCleaner.ts +++ b/utils/storageCleaner.ts @@ -11,9 +11,26 @@ const RESTRICTED_PROTOCOLS = [ ] as const; export async function getCurrentTab() { - // 使用 lastFocusedWindow: true 确保在 Side Panel 或 Popup 中都能获取到用户正在查看的标签页 - const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); - return tab; + // First, try the active tab in the last focused window (works for standard popups and side panels) + const [lastFocusedTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true }); + + // If the tab is valid and NOT an extension page/restricted URL, use it + if (lastFocusedTab && !isRestrictedUrl(lastFocusedTab.url)) { + return lastFocusedTab; + } + + // Fallback: If we're in a standalone extension window (which is focused), + // find the active tab in the most recently focused 'normal' browser window. + const [normalTab] = await chrome.tabs.query({ + active: true, + windowType: 'normal', + lastFocusedWindow: true, + }); + if (normalTab) return normalTab; + + // Final fallback: any active normal tab (if multiple windows exist, it returns all active tabs) + const normalTabs = await chrome.tabs.query({ active: true, windowType: 'normal' }); + return normalTabs[0]; } export function isRestrictedUrl(url?: string): boolean {