diff --git a/package.json b/package.json index 79d2711..a41e337 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "link-map", "displayName": "Link Map", - "version": "1.0.9", + "version": "1.0.10", "browserslist": "Chrome >= 96", "description": "Vertical Tabs Sidebar, But In Tree Structure", "author": "Garin", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index f1ae5e0..f4d20aa 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -168,55 +168,49 @@ "message": "Link Map feels stronger!" }, "updateNoteContent": { - "message": "That's right Link Map has been updated again, this time with some additional features and optimizations based on your feedback, check it out (yes, the details are still in the top node)" + "message": "In the v1.0.0, there is some little improvements to give your more detail control of Link Map’s behavior. If you are interested, just checkout the top node in the tree." }, "updateTutorialNode": { - "message": "New features in v1.0.7" + "message": "New features in v1.0.10" }, "updateTutorialFeature1": { - "message": "Save: keep the node in the tree at all times" + "message": "Options: Just besides the “Expand All“ button" }, "updateTutorialFeature1Desc1": { - "message": "Save ensures that tags are not deleted in the tree when it is closed" + "message": "Auto Scroll to Active Tab" }, - "updateTutorialFeature1Desc2": { - "message": "Save can be used using the right-click drop-down menu or shortcut keys" - }, - "updateTutorialFeature1Desc3": { - "message": "To Save a batch of nodes, collapse first and then Save" + "updateTutorialFeature1Desc11": { + "message": "Enable: When switch tab, tree will autoscroll to active tab, and make it visible" }, - "updateTutorialFeature2": { - "message": "Notes shortcut: use LinkMap like RoamResearch" - }, - "updateTutorialFeature2Desc1": { - "message": "Notes nodes can now be created via shortcuts" + "updateTutorialFeature1Desc12": { + "message": "Disable: Tree won’t draw attention" }, - "updateTutorialFeature2Desc2": { - "message": "Quickly record ideas while browsing the web, like RoamResearch" + "updateTutorialFeature1Desc2": { + "message": "Create New Tab by Level" }, - "updateTutorialFeature3": { - "message": "Show the number of open tabs next to the LinkMap extension icon" + "updateTutorialFeature1Desc21": { + "message": "Enable: When create new empty tab, new tab created as the child of the opener(if opener is the last node)" }, - "updateTutorialFeature4": { - "message": "Expand/Collapse All: Make LinkMap more tidy" + "updateTutorialFeature1Desc22": { + "message": "Disable: New tab will always create flatten as the last child of Window" }, - "updateTutorialFeature4Desc1": { - "message": "Below the search box you can find use them" + "updateTutorialFeature1Desc23": { + "message": "May confusing, try it" }, - "updateTutorialFeature4Desc2": { - "message": "In the right-click menu you can expand/collapse all for the current node only" + "updateTutorialFeature2": { + "message": "Copy All Subtree as TXT/Markdown: added in the context-menu" }, - "updateTutorialFeature5": { - "message": "Some minor optimizations" + "updateTutorialFeature2Desc1": { + "message": "It’s useful when putting your links to note-taking app" }, - "updateTutorialFeature5Desc1": { - "message": "'New tabs' will now be created in the same level in the tree" + "updateTutorialFeature3": { + "message": "Optimization for 'Locate'" }, - "updateTutorialFeature5Desc2": { - "message": "Some options have been added to the 'Delete/Close' in the right-click menu" + "updateTutorialFeature3Desc1": { + "message": "Now it will only locate the active tab of the top window" }, - "updateTutorialFeature5Desc3": { - "message": "The theme has the 'auto' option to automatically switch between light/dark modes" + "updateTutorialFeature3Desc2": { + "message": "It still doesn't perform perfectly sometime, I will continue optimizing" }, "reviewLinkMap": { "message": "Do you like Link Map or have any suggestions?" @@ -241,5 +235,32 @@ }, "save": { "message": "Save" + }, + "copySubtreeAsText": { + "message": "Copy Subtree as Text" + }, + "copySubtreeAsMarkdown": { + "message": "Copy Subtree as Markdown" + }, + "options": { + "message": "Options" + }, + "autoScrollToActiveTab": { + "message": "Auto Scroll to Active Tab" + }, + "createNewTabByLevel": { + "message": "Create New Tab by Level" + }, + "deleteNode": { + "message": "Delete Node" + }, + "deleteSubtree": { + "message": "Delete Subtree" + }, + "closeNode": { + "message": "Close Node" + }, + "closeSubtree": { + "message": "Close Subtree" } } diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 7deb15e..f281e0f 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -168,55 +168,49 @@ "message": "Link Map感觉自己更强大了!" }, "updateNoteContent": { - "message": "没错Link Map又更新了,这次根据大家的反馈增加了一些功能和优化,快看看吧(对,细节还是在最顶部的节点)" + "message": "在v1.0.0版本中,增加了一些小的改进,可以更细粒度的控制Link Map的行为。如果你感兴趣,只需查看树中的第一个节点来获取细节" }, "updateTutorialNode": { - "message": "新功能!(v1.0.7)" + "message": "v1.0.10中的新功能" }, "updateTutorialFeature1": { - "message": "保存:让节点一直保留在树中" + "message": "选项:就在'展开全部'按钮旁边" }, "updateTutorialFeature1Desc1": { - "message": "保存可以保证标签不会在关闭时在树中被删除" + "message": "自动滚动到激活的标签" }, - "updateTutorialFeature1Desc2": { - "message": "可以使用右键下拉菜单或快捷键使用保存" - }, - "updateTutorialFeature1Desc3": { - "message": "收起节点时保存,就可以批量保存一批节点" + "updateTutorialFeature1Desc11": { + "message": "启用:切换标签时,树将自动滚动到激活的标签节点,并使其可见" }, - "updateTutorialFeature2": { - "message": "笔记快捷键:像RoamResearch一样使用LinkMap" - }, - "updateTutorialFeature2Desc1": { - "message": "现在可以通过快捷键创建笔记节点" + "updateTutorialFeature1Desc12": { + "message": "禁用:树啥也不做,就那么呆着" }, - "updateTutorialFeature2Desc2": { - "message": "在浏览网页时可以快速记录想法,就像RoamResearch" + "updateTutorialFeature1Desc2": { + "message": "根据层级创建新标签" }, - "updateTutorialFeature3": { - "message": "LinkMap扩展图标旁展示已打开标签数量" + "updateTutorialFeature1Desc21": { + "message": "启用:如果从最后一个标签创建的空标签页,新标签页会作为子节点创建" }, - "updateTutorialFeature4": { - "message": "展开/收起全部:让LinkMap更加整洁" + "updateTutorialFeature1Desc22": { + "message": "禁用:新标签将始终作为窗口的最后一个子节点创建" }, - "updateTutorialFeature4Desc1": { - "message": "在搜索框的下方你可以发现使用它们" + "updateTutorialFeature1Desc23": { + "message": "我的描述可能会让你感到困惑,请尝试一下" }, - "updateTutorialFeature4Desc2": { - "message": "在右键菜单中可以只对当前节点展开/收起全部" + "updateTutorialFeature2": { + "message": "将子树复制为TXT/Markdown:功能在右键下拉菜单中" }, - "updateTutorialFeature5": { - "message": "一些小优化" + "updateTutorialFeature2Desc1": { + "message": "将链接复制到笔记应用程序中时很有用" }, - "updateTutorialFeature5Desc1": { - "message": "“新标签”现在会在树中平级创建" + "updateTutorialFeature3": { + "message": "'定位标签节点'优化" }, - "updateTutorialFeature5Desc2": { - "message": "右键菜单中的“删除/关闭”增加了一些选项" + "updateTutorialFeature3Desc1": { + "message": "现在它只会定位顶级窗口中处于激活状态的标签" }, - "updateTutorialFeature5Desc3": { - "message": "主题中可以选择“跟随系统”自动切换亮/暗模式" + "updateTutorialFeature3Desc2": { + "message": "它仍然不完美,有时会有问题,我将继续优化" }, "reviewLinkMap": { "message": "Link Map有没有帮到你或者有什么想吐槽的?" @@ -241,5 +235,32 @@ }, "save": { "message": "保存" + }, + "copySubtreeAsText": { + "message": "复制全部为txt" + }, + "copySubtreeAsMarkdown": { + "message": "复制全部为Markdown" + }, + "options": { + "message": "选项" + }, + "autoScrollToActiveTab": { + "message": "自动滚动到激活的标签" + }, + "createNewTabByLevel": { + "message": "根据层级创建新标签" + }, + "deleteNode": { + "message": "删除节点" + }, + "deleteSubtree": { + "message": "删除子树" + }, + "closeNode": { + "message": "关闭节点" + }, + "closeSubtree": { + "message": "关闭子树" } } diff --git a/src/background/event-bus.ts b/src/background/event-bus.ts index e7c7861..324790b 100644 --- a/src/background/event-bus.ts +++ b/src/background/event-bus.ts @@ -3,7 +3,7 @@ import { sendMessage } from '@garinz/webext-bridge'; import type { JsonValue } from 'type-fest'; import browser from 'webextension-polyfill'; -import { getExtPageInfo } from '../storage/ext-page-info'; +import { getExtPageInfo } from '../storage/basic'; const EXT_HOME_PAGE_PATH = 'tree.html'; diff --git a/src/background/index.ts b/src/background/index.ts index 26543e2..caf19f9 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -2,15 +2,21 @@ import { onMessage } from '@garinz/webext-bridge'; import log from 'loglevel'; import browser from 'webextension-polyfill'; +import { setLogLevel } from '../config/log-config'; import type { LocalStorageImportData } from '../import/App'; -import { getExtPageInfo, removeExtPageInfo, setExtPageInfo } from '../storage/ext-page-info'; +import { + getExtPageInfo, + removeExtPageInfo, + setExtPageInfo, + setPrevFocusWindowId, +} from '../storage/basic'; import { TabMasterDB } from '../storage/idb'; import { setIsNewUser, setIsUpdate } from '../storage/user-journey'; import type { ExportJsonData } from '../tree/features/settings/Settings'; import { isContentScriptPage, sendMessageToExt } from './event-bus'; try { - log.setLevel(__ENV__ === 'development' ? 'debug' : 'silent'); + setLogLevel(); async function syncTabsCountInBadge() { const allTabs = await browser.tabs.query({}); @@ -28,9 +34,10 @@ try { await setIsNewUser(true); } if ( - details.reason === 'update' && - details.previousVersion !== '1.0.7' && - browser.runtime.getManifest().version === '1.0.7' + details.reason === 'update' + // && + // details.previousVersion !== '1.0.10' && + // browser.runtime.getManifest().version === '1.0.10' ) { // chrome.runtime.getManifest().version await setIsUpdate(true); @@ -88,7 +95,10 @@ try { * 将extIdPair更新到localStorage中 * This Method Wouldn't Fire if popup has benn set */ - browser.action.onClicked.addListener(focusOrCreateExtWindow); + browser.action.onClicked.addListener((tab) => { + setPrevFocusWindowId(tab.windowId!); + focusOrCreateExtWindow(); + }); // #### 浏览器Fire的事件 browser.tabs.onCreated.addListener(async (tab) => { @@ -150,7 +160,8 @@ try { }); browser.tabs.onReplaced.addListener((addedTabId, removedTabId) => { - log.debug(`Tab replaced, added tabId: ${addedTabId}`, `removed tabId: ${removedTabId}`); + log.debug('[bg]: replaced, tabId:', addedTabId); + sendMessageToExt('replace-tab', { addedTabId, removedTabId }); }); /** * detach tab的时候会触发这个事件 diff --git a/src/config/log-config.ts b/src/config/log-config.ts new file mode 100644 index 0000000..87cd5e4 --- /dev/null +++ b/src/config/log-config.ts @@ -0,0 +1,5 @@ +import log from 'loglevel'; + +export const setLogLevel = () => { + log.setLevel(__ENV__ === 'development' ? 'debug' : 'error'); +}; diff --git a/src/import/index.tsx b/src/import/index.tsx index 944cae3..de5b4af 100644 --- a/src/import/index.tsx +++ b/src/import/index.tsx @@ -1,12 +1,12 @@ -import log from 'loglevel'; import { createRoot } from 'react-dom/client'; import { HashRouter } from 'react-router-dom'; +import { setLogLevel } from '../config/log-config'; import App from './App'; const container = document.querySelector('#root'); const root = createRoot(container!); -log.setLevel(__ENV__ === 'development' ? 'debug' : 'silent'); +setLogLevel(); root.render( diff --git a/src/storage/basic.ts b/src/storage/basic.ts new file mode 100644 index 0000000..eea03e1 --- /dev/null +++ b/src/storage/basic.ts @@ -0,0 +1,46 @@ +import { isEmpty } from 'lodash'; +import { storage } from 'webextension-polyfill'; + +const EXT_PAGE_INFO = 'extPageInfo'; + +export interface Basic { + windowId: number; + tabId: number; + ready: boolean; +} + +export const getExtPageInfo = async (): Promise => { + const extPageInfo = await storage.local.get(EXT_PAGE_INFO); + // 如果key不存在,返回的localStorage为空对象 + return isEmpty(extPageInfo) ? null : JSON.parse(extPageInfo[EXT_PAGE_INFO]); +}; + +export const setExtPageInfo = async (extPageInfo: Partial): Promise => { + const oldData = (await getExtPageInfo()) ?? {}; + return await storage.local.set({ + [EXT_PAGE_INFO]: JSON.stringify({ + ...oldData, + ...extPageInfo, + }), + }); +}; + +export const removeExtPageInfo = () => { + return storage.local.remove(EXT_PAGE_INFO); +}; + +const PREV_FOCUS_WINDOW_ID = 'prevFocusWindowId'; +export const getPrevFocusWindowId = async (): Promise => { + const prevFocusWindowId = await storage.local.get(PREV_FOCUS_WINDOW_ID); + return isEmpty(prevFocusWindowId) ? null : prevFocusWindowId[PREV_FOCUS_WINDOW_ID]; +}; + +export const setPrevFocusWindowId = async (prevFocusWindowId: number): Promise => { + return await storage.local.set({ + [PREV_FOCUS_WINDOW_ID]: prevFocusWindowId, + }); +}; + +export const removePrevFocusWindowId = () => { + return storage.local.remove(PREV_FOCUS_WINDOW_ID); +}; diff --git a/src/storage/ext-page-info.ts b/src/storage/ext-page-info.ts deleted file mode 100644 index 15c523c..0000000 --- a/src/storage/ext-page-info.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { isEmpty } from 'lodash'; -import { storage } from 'webextension-polyfill'; - -const EXT_PAGE_INFO = 'extPageInfo'; - -export interface ExtPageInfo { - windowId: number; - tabId: number; - ready: boolean; -} - -export const getExtPageInfo = async (): Promise => { - const extPageInfo = await storage.local.get(EXT_PAGE_INFO); - // 如果key不存在,返回的localStorage为空对象 - return isEmpty(extPageInfo) ? null : JSON.parse(extPageInfo[EXT_PAGE_INFO]); -}; - -export const setExtPageInfo = async (extPageInfo: Partial): Promise => { - const oldData = (await getExtPageInfo()) ?? {}; - return await storage.local.set({ - [EXT_PAGE_INFO]: JSON.stringify({ - ...oldData, - ...extPageInfo, - }), - }); -}; - -export const removeExtPageInfo = () => { - return storage.local.remove(EXT_PAGE_INFO); -}; diff --git a/src/storage/idb.ts b/src/storage/idb.ts index 6289253..b7e4ec9 100644 --- a/src/storage/idb.ts +++ b/src/storage/idb.ts @@ -9,16 +9,21 @@ interface Snapshot { } export type ThemeType = 'light' | 'dark' | 'auto'; + export interface Setting { id: number; theme: ThemeType; display: 'popup' | 'tab' | 'embedded-sidebar'; + autoScrollToActiveTab: boolean; + createNewTabByLevel: boolean; } export const DEFAULT_SETTING: Setting = { id: 1, theme: 'dark', display: 'popup', + autoScrollToActiveTab: false, + createNewTabByLevel: false, }; export class TabMasterDB extends Dexie { diff --git a/src/styles/dark-mode.less b/src/styles/dark-mode.less index 16ea2ed..784ed8c 100644 --- a/src/styles/dark-mode.less +++ b/src/styles/dark-mode.less @@ -49,6 +49,7 @@ --btn-icon-color: var(--main-font-color); --btn-border-color: var(--main-border-color); --ctx-menu-bg-color: #343134; + --ctx-menu-border-color: #4b4b4b; --ctx-menu-font-color: white; --ctx-menu-hover-bg-color: #4e525b; --scrollbar-bgc: rgb(56 56 56 / 50%); diff --git a/src/styles/iconfont/iconfont.css b/src/styles/iconfont/iconfont.css index 7d6bdeb..69d720d 100644 --- a/src/styles/iconfont/iconfont.css +++ b/src/styles/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: 'iconfont'; /* Project id 3889192 */ - src: url('iconfont.woff2?t=1680598855409') format('woff2'), - url('iconfont.woff?t=1680598855409') format('woff'), - url('iconfont.ttf?t=1680598855409') format('truetype'); + src: url('iconfont.woff2?t=1681142411790') format('woff2'), + url('iconfont.woff?t=1681142411790') format('woff'), + url('iconfont.ttf?t=1681142411790') format('truetype'); } .iconfont { @@ -13,6 +13,14 @@ -moz-osx-font-smoothing: grayscale; } +.icon-check:before { + content: '\e627'; +} + +.icon-more:before { + content: '\e613'; +} + .icon-lock:before { content: '\e69e'; } diff --git a/src/styles/iconfont/iconfont.ttf b/src/styles/iconfont/iconfont.ttf index 469c440..0ca9026 100644 Binary files a/src/styles/iconfont/iconfont.ttf and b/src/styles/iconfont/iconfont.ttf differ diff --git a/src/styles/iconfont/iconfont.woff b/src/styles/iconfont/iconfont.woff index 9c1eb31..672c143 100644 Binary files a/src/styles/iconfont/iconfont.woff and b/src/styles/iconfont/iconfont.woff differ diff --git a/src/styles/iconfont/iconfont.woff2 b/src/styles/iconfont/iconfont.woff2 index 5c7ddd3..22c1a5c 100644 Binary files a/src/styles/iconfont/iconfont.woff2 and b/src/styles/iconfont/iconfont.woff2 differ diff --git a/src/styles/light-mode.less b/src/styles/light-mode.less index fdeb3ca..67b898a 100644 --- a/src/styles/light-mode.less +++ b/src/styles/light-mode.less @@ -49,6 +49,7 @@ --btn-icon-color: #545454; --btn-border-color: var(--main-border-color); --ctx-menu-bg-color: white; + --ctx-menu-border-color: #d9d9d9; --ctx-menu-font-color: black; --ctx-menu-hover-bg-color: var(--primary-color); --scrollbar-bgc: rgb(56 56 56 / 30%); diff --git a/src/tree/features/App.tsx b/src/tree/features/App.tsx index b73d933..ff9584d 100644 --- a/src/tree/features/App.tsx +++ b/src/tree/features/App.tsx @@ -1,4 +1,5 @@ import { Modal } from 'antd'; +import { merge } from 'lodash'; import React, { useEffect, useState } from 'react'; import browser from 'webextension-polyfill'; @@ -37,9 +38,10 @@ const App: React.FC = () => { // const matchMediaDark = window.matchMedia('(prefers-color-scheme: dark)'); // const isDarkMode = matchMediaDark.matches; + // init setting in memory from indexedDB useEffect(() => { store.db.getSetting().then((setting) => { - setting && setSetting(setting); + setting && setSetting(merge(DEFAULT_SETTING, setting)); }); }, []); diff --git a/src/tree/features/locate/Locate.tsx b/src/tree/features/locate/Locate.tsx deleted file mode 100644 index b8a0801..0000000 --- a/src/tree/features/locate/Locate.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Button } from 'antd'; -import browser from 'webextension-polyfill'; - -import store from '../store'; -import type { WindowData } from '../tab-master-tree/nodes/window-node-operations'; - -import './locate.less'; - -const handleLocate = async () => { - const tree = store.tree; - if (!tree) { - return; - } - const tabs = await browser.tabs.query({ active: true }); - tabs.forEach((tab) => { - const activeNode = tree.getNodeByKey(tab.id!.toString()); - if (activeNode.parent && (activeNode.parent.data as WindowData).isBackgroundPage) { - return; - } - activeNode.makeVisible(); - activeNode.setActive(); - }); -}; - -const Locate = () => { - return ( -
-
- ); -}; - -export default Locate; diff --git a/src/tree/features/locate/locate.less b/src/tree/features/locate/locate.less deleted file mode 100644 index 41ea974..0000000 --- a/src/tree/features/locate/locate.less +++ /dev/null @@ -1,27 +0,0 @@ -@import url('../../../styles/common.less'); - -.locate { - .locate-btn { - width: 30px; - height: 30px; - padding: 0; - margin-right: 15px; - line-height: 30px; - color: var(--btn-icon-color); - //vertical-align: center; - text-align: center; - //border-radius: 4px; - cursor: pointer; - background-color: var(--btn-bg-color); - border: 1px solid var(--btn-border-color); - - &:focus-visible { - outline: 0; - } - - &:hover { - color: var(--main-font-color); - background-color: var(--btn-hover-bg-color); - } - } -} diff --git a/src/tree/features/operation-bar/OperationBar.tsx b/src/tree/features/operation-bar/OperationBar.tsx index b73611a..5500967 100644 --- a/src/tree/features/operation-bar/OperationBar.tsx +++ b/src/tree/features/operation-bar/OperationBar.tsx @@ -1,9 +1,11 @@ -import { Tooltip } from 'antd'; +import { Popover, Tooltip } from 'antd'; import React from 'react'; import browser from 'webextension-polyfill'; +import { getPrevFocusWindowId } from '../../../storage/basic'; import store from '../store'; import type { WindowData } from '../tab-master-tree/nodes/window-node-operations'; +import OptionPanel from './options-panel/OptionsPanel'; import './operation-bar.less'; @@ -24,6 +26,16 @@ const handleLocate = async () => { return; } const tabs = await browser.tabs.query({ active: true }); + const prevFocusWindowId = await getPrevFocusWindowId(); + if (prevFocusWindowId) { + const toActiveTabs = tabs.filter((tab) => tab.windowId === prevFocusWindowId); + if (toActiveTabs.length > 0) { + const activeNode = tree.getNodeByKey(toActiveTabs[0].id!.toString()); + activeNode.makeVisible(); + activeNode.setActive(); + return; + } + } tabs.forEach((tab) => { const activeNode = tree.getNodeByKey(tab.id!.toString()); if (activeNode.parent && (activeNode.parent.data as WindowData).isBackgroundPage) { @@ -79,6 +91,24 @@ const OperationBar: React.FC = () => { + + } + trigger="click" + showArrow={false} + overlayClassName={'options-panel-overlay'} + > + + + ); }; diff --git a/src/tree/features/operation-bar/options-panel/OptionsPanel.tsx b/src/tree/features/operation-bar/options-panel/OptionsPanel.tsx new file mode 100644 index 0000000..833f28e --- /dev/null +++ b/src/tree/features/operation-bar/options-panel/OptionsPanel.tsx @@ -0,0 +1,63 @@ +import React, { useContext } from 'react'; +import browser from 'webextension-polyfill'; + +import { SettingContext } from '../../../context'; +import store from '../../store'; + +import './option-panel.less'; + +const OptionPanel: React.FC = () => { + const { setting, setSetting } = useContext(SettingContext); + const { autoScrollToActiveTab, createNewTabByLevel } = setting; + + const handleAutoScrollToActiveTab = async () => { + const newState = !autoScrollToActiveTab; + await store.db.updateSettingPartial({ autoScrollToActiveTab: newState }); + setSetting({ + ...setting, + autoScrollToActiveTab: newState, + }); + }; + + const handleCreateNewTabByLevel = async () => { + const newState = !createNewTabByLevel; + await store.db.updateSettingPartial({ createNewTabByLevel: newState }); + setSetting({ + ...setting, + createNewTabByLevel: newState, + }); + }; + + return ( +
+
+
+
+ {browser.i18n.getMessage('autoScrollToActiveTab')} +
+
+
+
+
+ {browser.i18n.getMessage('createNewTabByLevel')} +
+
+
+ ); +}; + +export default OptionPanel; diff --git a/src/tree/features/operation-bar/options-panel/option-panel.less b/src/tree/features/operation-bar/options-panel/option-panel.less new file mode 100644 index 0000000..d2b6d66 --- /dev/null +++ b/src/tree/features/operation-bar/options-panel/option-panel.less @@ -0,0 +1,37 @@ +.option-panel { + display: flex; + flex-direction: column; + color: var(--ctx-menu-font-color); + background-color: var(--ctx-menu-bg-color); + + .option-panel-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0 10px 0 5px; + cursor: pointer; + border-radius: 5px; + + &:hover { + background-color: var(--ctx-menu-hover-bg-color); + } + + .option-panel-item-label { + flex: 1; + margin-left: 5px; + font-size: 12px; + } + + .option-panel-item-icon { + font-size: 11px; + } + } +} + +.options-panel-overlay { + .ant-popover-inner { + padding: 5px; + background-color: var(--ctx-menu-bg-color); + border: 1px solid var(--ctx-menu-border-color); + } +} diff --git a/src/tree/features/tab-master-tree/TabMasterTree.tsx b/src/tree/features/tab-master-tree/TabMasterTree.tsx index 6d56ff4..1687d5d 100644 --- a/src/tree/features/tab-master-tree/TabMasterTree.tsx +++ b/src/tree/features/tab-master-tree/TabMasterTree.tsx @@ -1,6 +1,7 @@ import { onMessage } from '@garinz/webext-bridge'; -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; +import { SettingContext } from '../../context'; import registerShortcuts from '../shortcuts/shortcuts'; import Store from '../store'; import type { FancyTabMasterTreeConfig } from './fancy-tab-master-tree'; @@ -47,6 +48,10 @@ const registerBrowserEventHandlers = (tmTree: FancyTabMasterTree) => { onMessage('add-window', (msg) => { tmTree.createWindow(msg.data); }); + onMessage('replace-tab', (msg) => { + const { addedTabId, removedTabId } = msg.data; + tmTree.replaceTab(addedTabId, removedTabId); + }); registerShortcuts(tmTree); }; @@ -59,10 +64,11 @@ export interface TabMasterTreeProps extends FancyTabMasterTreeConfig { export const TabMasterTree: React.FC = ({ source, onInit, ...otherProps }) => { let treeContainer: HTMLElement | null = null; const [tabMasterTree, setTabMasterTree] = useState(null); + const { setting } = useContext(SettingContext); useEffect(() => { const $el = $(treeContainer!); const config: FancyTabMasterTreeConfig = { ...otherProps }; - const tmTree = new FancyTabMasterTree($el, config); + const tmTree = new FancyTabMasterTree($el, config, setting); setTabMasterTree(tmTree); Store.tree = tmTree.tree; const loadedPromise = tmTree.initTree(source).then(() => { @@ -76,6 +82,11 @@ export const TabMasterTree: React.FC = ({ source, onInit, .. }); } }, []); + useEffect(() => { + if (tabMasterTree) { + tabMasterTree.settings = setting; + } + }, [setting.autoScrollToActiveTab, setting.createNewTabByLevel, tabMasterTree]); useEffect(() => { if (source && tabMasterTree) { diff --git a/src/tree/features/tab-master-tree/fancy-tab-master-tree.ts b/src/tree/features/tab-master-tree/fancy-tab-master-tree.ts index f62b4c2..1840094 100644 --- a/src/tree/features/tab-master-tree/fancy-tab-master-tree.ts +++ b/src/tree/features/tab-master-tree/fancy-tab-master-tree.ts @@ -3,7 +3,9 @@ import log from 'loglevel'; import type { Tabs, Windows } from 'webextension-polyfill'; import browser from 'webextension-polyfill'; -import { TabMasterDB } from '../../../storage/idb'; +import { setPrevFocusWindowId } from '../../../storage/basic'; +import type { Setting } from '../../../storage/idb'; +import { DEFAULT_SETTING, TabMasterDB } from '../../../storage/idb'; import { dataCheckAndSupply } from './nodes/data-check'; import type { TreeData, TreeNode } from './nodes/nodes'; import { NoteNodeOperations } from './nodes/note-node-operations'; @@ -11,7 +13,7 @@ import type { TabData } from './nodes/tab-node-operations'; import { TabNodeOperations } from './nodes/tab-node-operations'; import { NodeUtils } from './nodes/utils'; import type { WindowData } from './nodes/window-node-operations'; -import { WindowNodeOperations } from './nodes/window-node-operations'; +import { isCurrentWindow, WindowNodeOperations } from './nodes/window-node-operations'; import { registerContextMenu } from './plugins/context-menu'; import { DND5_CONFIG } from './plugins/dnd'; import { EDIT_OPTIONS } from './plugins/edit'; @@ -54,6 +56,7 @@ export class FancyTabMasterTree { tree: Fancytree.Fancytree; db?: TabMasterDB; enablePersist: boolean; + settings?: Setting; static closeNodes: (targetNode: FancytreeNode, mode?: OperationTarget) => void; static onClick: (event: JQueryEventObject, data: Fancytree.EventData) => boolean; static onDbClick: (targetNode: FancytreeNode) => Promise; @@ -81,12 +84,19 @@ export class FancyTabMasterTree { mode: 'parent' | 'child' | 'firstChild' | 'after', ) => void; - constructor($container: JQuery, config: FancyTabMasterTreeConfig = DefaultConfig) { + static copySubtree: (node: Fancytree.FancytreeNode, mode: 'txt' | 'md') => void; + + constructor( + $container: JQuery, + config: FancyTabMasterTreeConfig = DefaultConfig, + setting?: Setting, + ) { config = merge({}, DefaultConfig, config); const extensions = ['dnd5', 'filter']; if (config.enableEdit) { extensions.push('edit'); } + this.settings = setting ?? DEFAULT_SETTING; $container.fancytree({ active: true, extensions, @@ -223,7 +233,13 @@ export class FancyTabMasterTree { const targetNode = this.tree.getNodeByKey(`${tab.id}`); if (targetNode) return targetNode; const newNodeData = TabNodeOperations.createData(tab); - return TabNodeOperations.add(this.tree, newNodeData, tab.active); + log.debug('createNewTabByLevel', this.settings?.createNewTabByLevel); + return TabNodeOperations.add( + this.tree, + newNodeData, + tab.active, + this.settings?.createNewTabByLevel, + ); } public createWindow(window: Windows.Window): FancytreeNode { @@ -237,7 +253,14 @@ export class FancyTabMasterTree { // devtools的windowId为-1,不做处理 const tabNode = this.tree.getNodeByKey(`${tabId}`); if (windowId < 0 || !tabNode) return; - TabNodeOperations.updatePartial(this.tree.getNodeByKey(`${tabId}`), { active: true }); + TabNodeOperations.updatePartial(tabNode, { active: true }); + if (this.settings?.autoScrollToActiveTab) { + browser.windows.getCurrent().then((currentWindow) => { + if (currentWindow.id === windowId) return; + tabNode.makeVisible({ scrollIntoView: true }); + tabNode.setActive(true); + }); + } } public moveTab(_windowId: number, tabId: number, fromIndex: number, toIndex: number): void { @@ -287,7 +310,11 @@ export class FancyTabMasterTree { } public replaceTab(addedTabId: number, removedTabId: number): void { - throw new Error(`replaceTab Method not implemented. ${addedTabId} ${removedTabId}`); + const replacedTabNode = this.tree.getNodeByKey(`${removedTabId}`); + if (!replacedTabNode) return; + browser.tabs.get(addedTabId).then((tab) => { + TabNodeOperations.updatePartial(replacedTabNode, tab); + }); } public removeWindow(windowId: number): void { @@ -298,11 +325,11 @@ export class FancyTabMasterTree { public async windowFocus(windowId: number): Promise { // devtools的windowId为-1,不做处理 if (windowId < 0) return; - // const windowNode = this.tree.getNodeByKey(`${windowId}`); - // if (!windowNode.data.isBackgroundPage) { - // windowNode.scrollIntoView(); - // } - await this.syncActiveTab(windowId); + const isExtWindow = await isCurrentWindow(windowId); + if (!isExtWindow) { + setPrevFocusWindowId(windowId); + } + this.syncActiveTab(windowId); } public toJsonObj(includeRoot = false): TreeNode[] { @@ -354,9 +381,15 @@ FancyTabMasterTree.onDbClick = async (targetNode: FancytreeNode): Promise if (targetNode.data.nodeType === 'tab') { // 1. 如果TabNode是打开状态,直接激活 if (!targetNode.data.closed) { - await browser.tabs.update(targetNode.data.id, { active: true }); - await browser.windows.update(targetNode.data.windowId, { focused: true }); - return; + try { + await browser.tabs.update(targetNode.data.id, { active: true }); + await browser.windows.update(targetNode.data.windowId, { focused: true }); + return; + } catch (error) { + location.reload(); + log.error('db-click error: ', error); + return; + } } // 2. TabNode关闭 const windowNode = TabNodeOperations.findWindowNode(targetNode); @@ -544,6 +577,11 @@ FancyTabMasterTree.insertTag = ( } }; +FancyTabMasterTree.copySubtree = (node: FancytreeNode, mode: 'txt' | 'md') => { + const text = mode === 'txt' ? NodeUtils.convertToText(node) : NodeUtils.convertToMarkdown(node); + navigator.clipboard.writeText(text); +}; + function getOperationMode(targetNode: FancytreeNode, mode: OperationTarget = 'auto') { let closeMode: 'item' | 'all'; if (mode === 'item' || mode === 'all') { diff --git a/src/tree/features/tab-master-tree/nodes/tab-node-operations.ts b/src/tree/features/tab-master-tree/nodes/tab-node-operations.ts index 6c049f0..65c160e 100644 --- a/src/tree/features/tab-master-tree/nodes/tab-node-operations.ts +++ b/src/tree/features/tab-master-tree/nodes/tab-node-operations.ts @@ -72,7 +72,12 @@ export const TabNodeOperations = { }, }; }, - add(tree: Fancytree.Fancytree, newNode: TreeNode, active: boolean): FancytreeNode { + add( + tree: Fancytree.Fancytree, + newNode: TreeNode, + active: boolean, + createNewTabByLevel = false, + ): FancytreeNode { const { windowId, index, openerTabId, pendingUrl, url } = newNode.data; const windowNode = tree.getNodeByKey(`${windowId}`); // 1. 先根据index - 1找到前一个节点 @@ -81,7 +86,7 @@ export const TabNodeOperations = { ); // 2. 如果index - 1不存在,说明是第一个节点,直接添加为windowNode的子节点 let createdNode = null; - if (pendingUrl === NEW_TAB_URL || url === NEW_TAB_URL) { + if (!createNewTabByLevel && (pendingUrl === NEW_TAB_URL || url === NEW_TAB_URL)) { createdNode = windowNode.addChildren(newNode); } else if (prevNode === null) { createdNode = windowNode.addNode(newNode, 'firstChild'); diff --git a/src/tree/features/tab-master-tree/nodes/utils.ts b/src/tree/features/tab-master-tree/nodes/utils.ts index e08da48..1553f67 100644 --- a/src/tree/features/tab-master-tree/nodes/utils.ts +++ b/src/tree/features/tab-master-tree/nodes/utils.ts @@ -59,4 +59,30 @@ export const NodeUtils = { const extraClasses = node.extraClasses ? node.extraClasses.split(' ') : []; node.extraClasses = extraClasses.filter((item) => !removeClasses.includes(item)).join(' '); }, + convertToText(node: FancytreeNode, level = 0) { + const indent = ' '.repeat(level); + const { nodeType, url, pendingUrl } = node.data; + let text = ''; + text += + nodeType === 'tab' + ? `${indent}${node.title} (${url ?? pendingUrl})` + : `${indent}${node.title}`; + const childrenText: string = node.children + ? node.children.map((child) => NodeUtils.convertToText(child, level + 1)).join('') + : ''; + return `${text}\n${childrenText}`; + }, + convertToMarkdown(node: FancytreeNode, level = 0) { + const indent = ' '.repeat(level); + const { nodeType, url, pendingUrl } = node.data; + let text = ''; + text += + nodeType === 'tab' + ? `${indent}- [${node.title}](${url ?? pendingUrl})` + : `${indent}- ${node.title}`; + const childrenText: string = node.children + ? node.children.map((child) => NodeUtils.convertToMarkdown(child, level + 1)).join('') + : ''; + return `${text}\n${childrenText}`; + }, }; diff --git a/src/tree/features/tab-master-tree/nodes/window-node-operations.ts b/src/tree/features/tab-master-tree/nodes/window-node-operations.ts index 68649c4..a0732a5 100644 --- a/src/tree/features/tab-master-tree/nodes/window-node-operations.ts +++ b/src/tree/features/tab-master-tree/nodes/window-node-operations.ts @@ -25,6 +25,11 @@ export function isExtensionPages(window: Windows.Window) { return url.origin === new URL(browser.runtime.getURL('')).origin; } +export async function isCurrentWindow(windowId: number) { + const currentWindow = await browser.windows.getCurrent(); + return currentWindow.id === windowId; +} + export const generateWindowTitle = (windowType = 'normal') => { return `Window${windowType === 'normal' ? '' : `(${windowType})`}`; }; diff --git a/src/tree/features/tab-master-tree/plugins/context-menu.ts b/src/tree/features/tab-master-tree/plugins/context-menu.ts index 3a9aff1..eb2f2ad 100644 --- a/src/tree/features/tab-master-tree/plugins/context-menu.ts +++ b/src/tree/features/tab-master-tree/plugins/context-menu.ts @@ -22,11 +22,11 @@ export const registerContextMenu = () => { icon: () => 'iconfont icon-trash context-menu-icon', items: { deleteNode: { - name: 'Delete Node', + name: browser.i18n.getMessage('deleteNode'), icon: () => 'iconfont icon-pointer context-menu-icon', }, deleteSubTree: { - name: 'Delete Subtree', + name: browser.i18n.getMessage('deleteSubTree'), icon: () => 'iconfont icon-node-multiple context-menu-icon', }, }, @@ -36,11 +36,11 @@ export const registerContextMenu = () => { icon: () => 'iconfont icon-roundclosefill context-menu-icon', items: { closeNode: { - name: 'Close Node', + name: browser.i18n.getMessage('closeNode'), icon: () => 'iconfont icon-pointer context-menu-icon', }, closeSubTree: { - name: 'Close Subtree', + name: browser.i18n.getMessage('closeSubTree'), icon: () => 'iconfont icon-node-multiple context-menu-icon', }, }, @@ -64,6 +64,14 @@ export const registerContextMenu = () => { name: browser.i18n.getMessage('ctxMenuCopyMarkDownLink'), icon: () => 'iconfont icon-markdown context-menu-icon', }, + copySubtreeAsText: { + name: browser.i18n.getMessage('copySubtreeAsText'), + icon: () => 'iconfont icon-URLguanli context-menu-icon', + }, + copySubtreeAsMarkdown: { + name: browser.i18n.getMessage('copySubtreeAsMarkdown'), + icon: () => 'iconfont icon-markdown context-menu-icon', + }, }, }, notes: { @@ -139,6 +147,12 @@ export const registerContextMenu = () => { case 'copyMarkdownLink': navigator.clipboard.writeText(`[${node.data.title}](${node.data.url})`); break; + case 'copySubtreeAsText': + FancyTabMasterTree.copySubtree(node, 'txt'); + break; + case 'copySubtreeAsMarkdown': + FancyTabMasterTree.copySubtree(node, 'md'); + break; case 'expandAll': node.visit((node) => node.setExpanded(true), true); break; diff --git a/src/tree/features/tab-master-tree/style.less b/src/tree/features/tab-master-tree/style.less index 3f081e3..b0de134 100644 --- a/src/tree/features/tab-master-tree/style.less +++ b/src/tree/features/tab-master-tree/style.less @@ -429,8 +429,7 @@ body { .context-menu-list { padding: 8px 4px; background-color: var(--ctx-menu-bg-color); - //border-color: #4e525b; - border-color: transparent; + border-color: var(--ctx-menu-border-color); border-radius: 8px; box-shadow: 0 1px #0000000d, 0 4px 10px rgb(0 0 0 / 30%); diff --git a/src/tree/features/tutorial/tutorial-utils.ts b/src/tree/features/tutorial/tutorial-utils.ts index 9ebd782..8d61c1f 100644 --- a/src/tree/features/tutorial/tutorial-utils.ts +++ b/src/tree/features/tutorial/tutorial-utils.ts @@ -54,14 +54,14 @@ export const openUpdateNotification = () => { }); }; -const V1_0_7 = { +const V1_0_10 = { title: `🎁 ${browser.i18n.getMessage('updateTutorialNode')}`, icon: { html: '' }, expanded: true, data: { nodeType: 'note' }, children: [ { - title: `1. 🔒 ${browser.i18n.getMessage('updateTutorialFeature1')}`, + title: `1. ✅ ${browser.i18n.getMessage('updateTutorialFeature1')}`, icon: { html: '' }, data: { nodeType: 'note' }, children: [ @@ -69,21 +69,45 @@ const V1_0_7 = { title: browser.i18n.getMessage('updateTutorialFeature1Desc1'), icon: { html: '' }, data: { nodeType: 'note' }, + children: [ + { + title: `${browser.i18n.getMessage('updateTutorialFeature1Desc11')}`, + icon: { html: '' }, + data: { nodeType: 'note' }, + }, + { + title: `${browser.i18n.getMessage('updateTutorialFeature1Desc12')}`, + icon: { html: '' }, + data: { nodeType: 'note' }, + }, + ], }, { title: browser.i18n.getMessage('updateTutorialFeature1Desc2'), icon: { html: '' }, data: { nodeType: 'note' }, - }, - { - title: browser.i18n.getMessage('updateTutorialFeature1Desc3'), - icon: { html: '' }, - data: { nodeType: 'note' }, + children: [ + { + title: `${browser.i18n.getMessage('updateTutorialFeature1Desc21')}`, + icon: { html: '' }, + data: { nodeType: 'note' }, + }, + { + title: `${browser.i18n.getMessage('updateTutorialFeature1Desc22')}`, + icon: { html: '' }, + data: { nodeType: 'note' }, + }, + { + title: `${browser.i18n.getMessage('updateTutorialFeature1Desc23')}`, + icon: { html: '' }, + data: { nodeType: 'note' }, + }, + ], }, ], }, { - title: `2. ⌨️ ${browser.i18n.getMessage('updateTutorialFeature2')}`, + title: `2. 🗒️ ${browser.i18n.getMessage('updateTutorialFeature2')}`, icon: { html: '' }, data: { nodeType: 'note' }, children: [ @@ -92,52 +116,20 @@ const V1_0_7 = { icon: { html: '' }, data: { nodeType: 'note' }, }, - { - title: browser.i18n.getMessage('updateTutorialFeature2Desc2'), - icon: { html: '' }, - data: { nodeType: 'note' }, - }, - ], - }, - { - title: `3. 🔟 ${browser.i18n.getMessage('updateTutorialFeature3')}`, - icon: { html: '' }, - data: { nodeType: 'note' }, - }, - { - title: `4. 📂 ${browser.i18n.getMessage('updateTutorialFeature4')}`, - icon: { html: '' }, - data: { nodeType: 'note' }, - children: [ - { - title: browser.i18n.getMessage('updateTutorialFeature4Desc1'), - icon: { html: '' }, - data: { nodeType: 'note' }, - }, - { - title: browser.i18n.getMessage('updateTutorialFeature4Desc2'), - icon: { html: '' }, - data: { nodeType: 'note' }, - }, ], }, { - title: `5. 🌻 ${browser.i18n.getMessage('updateTutorialFeature5')}`, + title: `3. 📍 ${browser.i18n.getMessage('updateTutorialFeature3')}`, icon: { html: '' }, data: { nodeType: 'note' }, children: [ { - title: browser.i18n.getMessage('updateTutorialFeature5Desc1'), - icon: { html: '' }, - data: { nodeType: 'note' }, - }, - { - title: browser.i18n.getMessage('updateTutorialFeature5Desc2'), + title: browser.i18n.getMessage('updateTutorialFeature3Desc1'), icon: { html: '' }, data: { nodeType: 'note' }, }, { - title: browser.i18n.getMessage('updateTutorialFeature5Desc3'), + title: browser.i18n.getMessage('updateTutorialFeature3Desc2'), icon: { html: '' }, data: { nodeType: 'note' }, }, @@ -148,5 +140,5 @@ const V1_0_7 = { export const buildUpdateTutorialNodes = (tmTree: FancyTabMasterTree): void => { const rootNote = tmTree.tree.getRootNode(); - rootNote.addNode(V1_0_7, 'firstChild'); + rootNote.addNode(V1_0_10, 'firstChild'); }; diff --git a/src/tree/index.tsx b/src/tree/index.tsx index f60f81e..32a13a7 100644 --- a/src/tree/index.tsx +++ b/src/tree/index.tsx @@ -2,11 +2,12 @@ import log from 'loglevel'; import { createRoot } from 'react-dom/client'; import { HashRouter } from 'react-router-dom'; +import { setLogLevel } from '../config/log-config'; import App from './features/App'; const container = document.querySelector('#root'); const root = createRoot(container!); -log.setLevel(__ENV__ === 'development' ? 'debug' : 'silent'); +setLogLevel(); try { root.render( diff --git a/src/typings/shim.d.ts b/src/typings/shim.d.ts index 62339d1..018b313 100644 --- a/src/typings/shim.d.ts +++ b/src/typings/shim.d.ts @@ -34,12 +34,7 @@ declare module '@garinz/webext-bridge' { 'add-window': Windows.Window; 'remove-window': { windowId: number }; 'window-focus': { windowId: number }; - // tree event - 'focus-node': number; - 'remove-node': { - windowId: number; - tabId: number; - }; + 'replace-tab': { addedTabId: number; removedTabId: number }; 'import-data': ExportJsonData; 'import-tabOutliner-data': TabOutliner.ExportData; 'tree-ready': { windowId: number; tabId: number };