From 09fd6d410519a1c13f0dbe2902ac8271189fae83 Mon Sep 17 00:00:00 2001 From: Balamurugan Marimuthu Date: Sun, 8 Mar 2026 18:33:37 -0700 Subject: [PATCH 1/4] Default Link Map to Chrome side panel --- src/background/index.ts | 54 +++++++++++++++++-- src/manifest.ts | 8 ++- src/storage/idb.ts | 6 ++- src/tree/features/help/help.less | 7 +++ src/tree/features/login/Login.tsx | 51 ------------------ .../features/operation-bar/operation-bar.less | 10 ++++ src/tree/features/search/search.less | 16 ++++++ src/tree/features/settings/settings.less | 8 +++ src/tree/features/tab-master-tree/style.less | 20 +++++++ src/typings/index.d.ts | 18 +++++++ 10 files changed, 141 insertions(+), 57 deletions(-) delete mode 100644 src/tree/features/login/Login.tsx diff --git a/src/background/index.ts b/src/background/index.ts index ddbec66..0d95335 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -18,6 +18,11 @@ import { isContentScriptPage, sendMessageToExt } from './event-bus'; try { setLogLevel(); + async function enableSidePanelByDefault() { + if (!chrome.sidePanel?.setPanelBehavior) return; + await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); + } + async function syncTabsCountInBadge() { const allTabs = await browser.tabs.query({}); await browser.action.setBadgeBackgroundColor({ color: '#2b2d31' }); @@ -43,10 +48,15 @@ try { } const db = new TabMasterDB(); await db.initSetting(); + await enableSidePanelByDefault(); await syncTabsCountInBadge(); await removeExtPageInfo(); }); + browser.runtime.onStartup.addListener(async () => { + await enableSidePanelByDefault(); + }); + async function openNewExtWindow() { const displayInfos = await chrome.system.display.getInfo(); const primaryDisplayInfo = displayInfos.find((item) => item.isPrimary); @@ -89,6 +99,28 @@ try { } }; + const openLinkMap = async (windowId?: number) => { + const setting = await new TabMasterDB().getSetting(); + const shouldUseSidePanel = setting?.display === 'embedded-sidebar'; + if (shouldUseSidePanel && chrome.sidePanel?.open) { + try { + if (windowId) { + await chrome.sidePanel.open({ windowId }); + } else { + const lastFocusedWindow = await browser.windows.getLastFocused(); + if (lastFocusedWindow.id) { + await chrome.sidePanel.open({ windowId: lastFocusedWindow.id }); + return; + } + } + return; + } catch (error) { + log.warn('Failed to open side panel, falling back to popup window.', error); + } + } + await focusOrCreateExtWindow(); + }; + /** * 点击插件按钮:打开一个TreeView页面 * 将extIdPair更新到localStorage中 @@ -96,7 +128,7 @@ try { */ browser.action.onClicked.addListener((tab) => { setPrevFocusWindowId(tab.windowId!); - focusOrCreateExtWindow(); + openLinkMap(tab.windowId); }); // #### 浏览器Fire的事件 @@ -138,6 +170,7 @@ try { browser.tabs.onActivated.addListener(({ tabId, windowId }) => { log.debug('[bg]: tab activated!'); + setPrevFocusWindowId(windowId); sendMessageToExt('activated-tab', { windowId, tabId }); }); /** @@ -180,8 +213,19 @@ try { sendMessageToExt('remove-window', { windowId }); }); - browser.windows.onFocusChanged.addListener((windowId) => { + browser.windows.onFocusChanged.addListener(async (windowId) => { log.debug('[bg]: window focus changed!'); + if (windowId !== browser.windows.WINDOW_ID_NONE) { + const [activeTab] = await browser.tabs.query({ active: true, windowId }); + if ( + !activeTab || + isContentScriptPage(activeTab.url) || + isContentScriptPage(activeTab.pendingUrl) + ) { + return; + } + await setPrevFocusWindowId(windowId); + } sendMessageToExt('window-focus', { windowId }); }); @@ -212,9 +256,13 @@ try { browser.commands.onCommand.addListener(async (command) => { if (command === 'openLinkMap') { - await focusOrCreateExtWindow(); + await openLinkMap(); } }); + + enableSidePanelByDefault().catch((error) => { + log.warn('Failed to enable side panel action behavior.', error); + }); } catch (error) { log.error(error); } diff --git a/src/manifest.ts b/src/manifest.ts index e7c41b6..1cb0f30 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -7,12 +7,12 @@ export const commandKeyMap = { openLinkMap: 'openLinkMap', }; -const manifest: Manifest.WebExtensionManifest = { +const manifest: Manifest.WebExtensionManifest & { side_panel: { default_path: string } } = { name: pkg.displayName, version: pkg.version, description: '__MSG_extDesc__', manifest_version: 3, - minimum_chrome_version: pkg.browserslist.split(' ')[2], + minimum_chrome_version: '114', permissions: [ 'tabs', 'storage', @@ -20,6 +20,7 @@ const manifest: Manifest.WebExtensionManifest = { 'windows', 'downloads', 'system.display', + 'sidePanel' as Manifest.Permission, 'favicon', ], content_security_policy: { @@ -60,6 +61,9 @@ const manifest: Manifest.WebExtensionManifest = { '128': 'icons/x128.png', }, }, + side_panel: { + default_path: 'tree.html', + }, // 实现options页面后使用 // options_ui: { // page: 'options.html', diff --git a/src/storage/idb.ts b/src/storage/idb.ts index b7e4ec9..5c65141 100644 --- a/src/storage/idb.ts +++ b/src/storage/idb.ts @@ -21,7 +21,7 @@ export interface Setting { export const DEFAULT_SETTING: Setting = { id: 1, theme: 'dark', - display: 'popup', + display: 'embedded-sidebar', autoScrollToActiveTab: false, createNewTabByLevel: false, }; @@ -57,6 +57,10 @@ export class TabMasterDB extends Dexie { const currentSetting = await this.getSetting(); if (!currentSetting) { await this.setting.put(DEFAULT_SETTING); + return; + } + if (currentSetting.display === 'popup') { + await this.setting.update(1, { display: DEFAULT_SETTING.display }); } } diff --git a/src/tree/features/help/help.less b/src/tree/features/help/help.less index e8480ca..c62569d 100644 --- a/src/tree/features/help/help.less +++ b/src/tree/features/help/help.less @@ -25,6 +25,13 @@ } } +@media (max-width: 420px) { + .help-float-btn { + right: 12px; + bottom: 12px; + } +} + .help-popover { .ant-popover-arrow { display: none; diff --git a/src/tree/features/login/Login.tsx b/src/tree/features/login/Login.tsx deleted file mode 100644 index 15133ae..0000000 --- a/src/tree/features/login/Login.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { Button, Modal } from 'antd'; -import React, { useState } from 'react'; - -const Login: React.FC = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [isLoginWithGoogleLoading, setIsLoginWithGoogleLoading] = useState(false); - - const showModal = () => { - setIsModalOpen(true); - }; - - const handleOk = () => { - setIsModalOpen(false); - }; - - const handleCancel = async () => { - setIsModalOpen(false); - }; - - const handleLoginBtnClick = () => { - setIsLoginWithGoogleLoading(true); - chrome.identity.getAuthToken({ interactive: true }, (token) => { - if (chrome.runtime.lastError) { - console.error(chrome.runtime.lastError); - } - console.log(token); - setIsLoginWithGoogleLoading(false); - }); - }; - - return ( -
- - - - -
- ); -}; - -export default Login; diff --git a/src/tree/features/operation-bar/operation-bar.less b/src/tree/features/operation-bar/operation-bar.less index 6bb43ae..5dbe8a8 100644 --- a/src/tree/features/operation-bar/operation-bar.less +++ b/src/tree/features/operation-bar/operation-bar.less @@ -20,3 +20,13 @@ } } } + +@media (max-width: 420px) { + .operation-bar { + padding: 4px 6px; + + .operation-bar-item { + margin-right: 3px; + } + } +} diff --git a/src/tree/features/search/search.less b/src/tree/features/search/search.less index bb21d03..fc7a3ac 100644 --- a/src/tree/features/search/search.less +++ b/src/tree/features/search/search.less @@ -45,3 +45,19 @@ color: var(--search-input-placeholder-color); } } + +@media (max-width: 420px) { + .search-input { + height: 28px; + padding: 0 6px; + + input.search { + padding-left: 8px; + font-size: 11px; + } + + .search-shortcut-info { + display: none; + } + } +} diff --git a/src/tree/features/settings/settings.less b/src/tree/features/settings/settings.less index 14e67a4..70e4f27 100644 --- a/src/tree/features/settings/settings.less +++ b/src/tree/features/settings/settings.less @@ -27,6 +27,14 @@ } } +@media (max-width: 420px) { + .settings-container { + .settings-btn { + margin-right: 10px; + } + } +} + .settings-modal { .setting-section { margin-bottom: 20px; diff --git a/src/tree/features/tab-master-tree/style.less b/src/tree/features/tab-master-tree/style.less index b0de134..9cb7657 100644 --- a/src/tree/features/tab-master-tree/style.less +++ b/src/tree/features/tab-master-tree/style.less @@ -146,6 +146,26 @@ body { } } +@media (max-width: 420px) { + body { + #header { + height: 52px; + + .search-input { + margin: 0 10px; + } + } + + ul.fancytree-container { + padding: 6px 0 36px 6px; + } + + .tree-container { + height: calc(100% - 44px - 36px); + } + } +} + .search-result { #tree.fancytree-ext-filter-dimm span.fancytree-node { &.fancytree-submatch span.fancytree-title { diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index ed67163..e789469 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -4,6 +4,24 @@ declare var __TARGET__: 'chrome' | 'edge'; declare var __LOG_LEVEL__: string; declare var __VERSION__: string; +declare namespace chrome { + interface SidePanelOpenOptions { + tabId?: number; + windowId?: number; + } + + interface SidePanelPanelBehavior { + openPanelOnActionClick?: boolean; + } + + interface SidePanelStatic { + open(options: SidePanelOpenOptions): Promise; + setPanelBehavior(options: SidePanelPanelBehavior): Promise; + } + + const sidePanel: SidePanelStatic | undefined; +} + declare module '*.scss' { const exports: StyleSheetModule; export default exports; From 7610f2691ae3e56049a9f976f23d28ae2c4ffe47 Mon Sep 17 00:00:00 2001 From: Balamurugan Marimuthu Date: Sun, 8 Mar 2026 18:58:19 -0700 Subject: [PATCH 2/4] Add floating modal Link Map mode --- public/_locales/en/messages.json | 12 +++ public/_locales/zh_CN/messages.json | 12 +++ src/background/event-bus.ts | 2 +- src/background/index.ts | 99 ++++++++++++++++++-- src/storage/idb.ts | 6 +- src/tree/features/App.tsx | 55 ++++++++--- src/tree/features/search/Search.tsx | 11 ++- src/tree/features/search/search.less | 31 ++++++ src/tree/features/settings/Settings.tsx | 33 ++++++- src/tree/features/tab-master-tree/style.less | 70 ++++++++++++++ 10 files changed, 303 insertions(+), 28 deletions(-) diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index f4d20aa..ffac67f 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -134,6 +134,18 @@ "themeDark": { "message": "Dark" }, + "displayMode": { + "message": "Open As" + }, + "displayModeSidebar": { + "message": "Chrome Sidebar" + }, + "displayModeFloatingModal": { + "message": "Floating Modal" + }, + "displayModePopup": { + "message": "Popup Window" + }, "commandTriggerLinkMap": { "message": "Open Link Map" }, diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index c9c44f9..b53419a 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -134,6 +134,18 @@ "themeDark": { "message": "黑暗模式" }, + "displayMode": { + "message": "打开方式" + }, + "displayModeSidebar": { + "message": "Chrome 侧边栏" + }, + "displayModeFloatingModal": { + "message": "悬浮弹窗" + }, + "displayModePopup": { + "message": "弹出窗口" + }, "commandTriggerLinkMap": { "message": "打开Link Map" }, diff --git a/src/background/event-bus.ts b/src/background/event-bus.ts index 324790b..94f5f61 100644 --- a/src/background/event-bus.ts +++ b/src/background/event-bus.ts @@ -25,5 +25,5 @@ export async function sendMessageToExt( } export function isContentScriptPage(url?: string) { - return url === browser.runtime.getURL(EXT_HOME_PAGE_PATH); + return url?.startsWith(browser.runtime.getURL(EXT_HOME_PAGE_PATH)) ?? false; } diff --git a/src/background/index.ts b/src/background/index.ts index 0d95335..7ad6b86 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -18,6 +18,25 @@ import { isContentScriptPage, sendMessageToExt } from './event-bus'; try { setLogLevel(); + async function getPrimaryTabForWindow(windowId?: number) { + if (windowId == null) { + return null; + } + const tabs = await browser.tabs.query({ windowId }); + return tabs[0] ?? null; + } + + async function storeExtPageInfo(window: browser.Windows.Window) { + const extTab = window.tabs?.[0] ?? (await getPrimaryTabForWindow(window.id)); + if (!extTab?.id || !extTab.windowId) { + throw new Error('Failed to resolve Link Map tab for created window.'); + } + await setExtPageInfo({ + windowId: extTab.windowId, + tabId: extTab.id, + }); + } + async function enableSidePanelByDefault() { if (!chrome.sidePanel?.setPanelBehavior) return; await chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); @@ -72,11 +91,30 @@ try { left, focused: true, }); - const extTab = extWindow.tabs![0]; - await setExtPageInfo({ - windowId: extTab.windowId!, - tabId: extTab.id!, + await storeExtPageInfo(extWindow); + } + + async function openFloatingModalWindow() { + const displayInfos = await chrome.system.display.getInfo(); + const primaryDisplayInfo = displayInfos.find((item) => item.isPrimary); + const workAreaWidth = primaryDisplayInfo?.workArea.width ?? 1440; + const workAreaHeight = primaryDisplayInfo?.workArea.height ?? 960; + const workAreaLeft = primaryDisplayInfo?.workArea.left ?? 0; + const workAreaTop = primaryDisplayInfo?.workArea.top ?? 0; + const width = Math.min(1120, Math.floor(workAreaWidth * 0.7)); + const height = Math.min(820, Math.floor(workAreaHeight * 0.82)); + const left = workAreaLeft + Math.max(0, Math.floor((workAreaWidth - width) / 2)); + const top = workAreaTop + Math.max(0, Math.floor((workAreaHeight - height) / 2)); + const extWindow = await browser.windows.create({ + url: 'tree.html?display=floating-modal', + type: 'popup', + width, + height, + top, + left, + focused: true, }); + await storeExtPageInfo(extWindow); } onMessage('tree-ready', async (msg) => { @@ -84,24 +122,55 @@ try { await setExtPageInfo({ windowId, tabId }); }); - const focusOrCreateExtWindow = async () => { + const focusOrCreateExtWindow = async ( + createExtWindow = openNewExtWindow, + shouldReuseWindow: (url?: string) => boolean = (url) => + url === browser.runtime.getURL('tree.html'), + ) => { const extIdPair = await getExtPageInfo(); if (extIdPair == null) { - await openNewExtWindow(); + await createExtWindow(); } else { - // 页面已打开,则窗口focused try { + const extTab = await browser.tabs.get(extIdPair.tabId); + if (!shouldReuseWindow(extTab.url)) { + await createExtWindow(); + return; + } + // 页面已打开,则窗口focused await browser.windows.update(extIdPair.windowId, { focused: true }); } catch { // 防止localStorage数据未清除,但是页面已经关闭的情况 - await openNewExtWindow(); + await createExtWindow(); } } }; - const openLinkMap = async (windowId?: number) => { + const closeFloatingModalIfOpen = async () => { + const extIdPair = await getExtPageInfo(); + if (extIdPair == null) { + return false; + } + try { + const extTab = await browser.tabs.get(extIdPair.tabId); + const isFloatingModal = + extTab.url === browser.runtime.getURL('tree.html?display=floating-modal'); + if (!isFloatingModal) { + return false; + } + await browser.windows.remove(extIdPair.windowId); + await removeExtPageInfo(); + return true; + } catch { + await removeExtPageInfo(); + return false; + } + }; + + const openLinkMap = async (windowId?: number, shouldToggleFloatingModal = false) => { const setting = await new TabMasterDB().getSetting(); const shouldUseSidePanel = setting?.display === 'embedded-sidebar'; + const shouldUseFloatingModal = setting?.display === 'floating-modal'; if (shouldUseSidePanel && chrome.sidePanel?.open) { try { if (windowId) { @@ -118,6 +187,16 @@ try { log.warn('Failed to open side panel, falling back to popup window.', error); } } + if (shouldUseFloatingModal) { + if (shouldToggleFloatingModal && (await closeFloatingModalIfOpen())) { + return; + } + await focusOrCreateExtWindow( + openFloatingModalWindow, + (url) => url === browser.runtime.getURL('tree.html?display=floating-modal'), + ); + return; + } await focusOrCreateExtWindow(); }; @@ -256,7 +335,7 @@ try { browser.commands.onCommand.addListener(async (command) => { if (command === 'openLinkMap') { - await openLinkMap(); + await openLinkMap(undefined, true); } }); diff --git a/src/storage/idb.ts b/src/storage/idb.ts index 5c65141..031d36a 100644 --- a/src/storage/idb.ts +++ b/src/storage/idb.ts @@ -13,7 +13,7 @@ export type ThemeType = 'light' | 'dark' | 'auto'; export interface Setting { id: number; theme: ThemeType; - display: 'popup' | 'tab' | 'embedded-sidebar'; + display: 'popup' | 'tab' | 'embedded-sidebar' | 'floating-modal'; autoScrollToActiveTab: boolean; createNewTabByLevel: boolean; } @@ -21,7 +21,7 @@ export interface Setting { export const DEFAULT_SETTING: Setting = { id: 1, theme: 'dark', - display: 'embedded-sidebar', + display: 'floating-modal', autoScrollToActiveTab: false, createNewTabByLevel: false, }; @@ -59,7 +59,7 @@ export class TabMasterDB extends Dexie { await this.setting.put(DEFAULT_SETTING); return; } - if (currentSetting.display === 'popup') { + if (currentSetting.display === 'popup' || currentSetting.display === 'embedded-sidebar') { await this.setting.update(1, { display: DEFAULT_SETTING.display }); } } diff --git a/src/tree/features/App.tsx b/src/tree/features/App.tsx index f5be0e5..7c17860 100644 --- a/src/tree/features/App.tsx +++ b/src/tree/features/App.tsx @@ -23,6 +23,10 @@ import Welcome from './tutorial/Welcome'; import '../../styles/app.less'; +const closeFloatingModal = () => { + window.close(); +}; + const updateNotification = async (tmTree: FancyTabMasterTree) => { const isUpdate = await getIsUpdate(); if (!isUpdate) return; @@ -34,6 +38,8 @@ const updateNotification = async (tmTree: FancyTabMasterTree) => { const App: React.FC = () => { const [isModalOpen, setIsModalOpen] = useState(false); const [setting, setSetting] = useState(DEFAULT_SETTING); + const displayMode = new URLSearchParams(window.location.search).get('display'); + const isFloatingModal = displayMode === 'floating-modal'; // const matchMediaDark = window.matchMedia('(prefers-color-scheme: dark)'); // const isDarkMode = matchMediaDark.matches; @@ -62,6 +68,21 @@ const App: React.FC = () => { } }, [setting.theme]); + useEffect(() => { + if (!isFloatingModal) { + return; + } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== 'Escape') { + return; + } + event.preventDefault(); + window.close(); + }; + window.addEventListener('keydown', handleKeyDown, true); + return () => window.removeEventListener('keydown', handleKeyDown, true); + }, [isFloatingModal]); + const handleCancel = () => { setIsModalOpen(false); }; @@ -85,19 +106,29 @@ const App: React.FC = () => { return ( -
- - - -