diff --git a/package.json b/package.json index fff4f047..a10f95c5 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@dnd-kit/core": "^6.0.8", "@dnd-kit/modifiers": "^6.0.1", "@dnd-kit/sortable": "^7.0.2", + "@mozilla/readability": "^0.5.0", "antd": "^5.7.3", "bowser": "^2.11.0", "classnames": "^2.2.6", diff --git a/src/assets/svg/clip-page.svg b/src/assets/svg/clip-page.svg new file mode 100644 index 00000000..a3832530 --- /dev/null +++ b/src/assets/svg/clip-page.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/background/actionListener/clip.ts b/src/background/actionListener/clip.ts index ccb79ead..5c0d34fd 100644 --- a/src/background/actionListener/clip.ts +++ b/src/background/actionListener/clip.ts @@ -1,35 +1,61 @@ -import { - OperateClipEnum, - IOperateClipData, -} from '@/isomorphic/background/clip'; -import Chrome from '@/background/core/chrome'; -import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; +import { OperateClipEnum, IOperateClipData } from '@/isomorphic/background/clip'; +import chromeExtension from '@/background/core/chromeExtension'; import { RequestMessage } from './index'; export async function createClipActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { - const { type, isRunningInjectPage } = request.data; + const { type, isRunningHostPage } = request.data; + const currentTab = await chromeExtension.tabs.getCurrentTab(sender.tab); switch (type) { case OperateClipEnum.screenOcr: { - const res = await Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.ScreenOcr, - data: { - isRunningInjectPage, + chromeExtension.scripting.executeScript( + { + target: { tabId: currentTab.id as number }, + args: [{ isRunningHostPage }], + func: args => { + return window._yuque_ext_app.clipScreenOcr({ + isRunningHostPage: args.isRunningHostPage, + }); + }, }, - }); - callback(res); + res => { + callback(res[0].result); + }, + ); break; } case OperateClipEnum.selectArea: { - const res = await Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.SelectArea, - data: { - isRunningInjectPage, + chromeExtension.scripting.executeScript( + { + target: { tabId: currentTab.id as number }, + args: [{ isRunningHostPage }], + func: args => { + return window._yuque_ext_app.clipSelectArea({ + isRunningHostPage: args.isRunningHostPage, + }); + }, + }, + res => { + callback(res[0].result); + }, + ); + break; + } + case OperateClipEnum.clipPage: { + chromeExtension.scripting.executeScript( + { + target: { tabId: currentTab?.id as number }, + func: () => { + return window._yuque_ext_app.parsePage(); + }, + }, + res => { + callback(res[0]?.result); }, - }); - callback(res); + ); break; } default: { diff --git a/src/background/actionListener/configManager.ts b/src/background/actionListener/configManager.ts index 54efe3ac..39b8991a 100644 --- a/src/background/actionListener/configManager.ts +++ b/src/background/actionListener/configManager.ts @@ -1,8 +1,5 @@ import { levitateConfigManager } from '@/background/core/configManager/levitate'; -import { - IOperateConfigManagerData, - OperateConfigManagerEnum, -} from '@/isomorphic/background/configManager'; +import { IOperateConfigManagerData, OperateConfigManagerEnum } from '@/isomorphic/background/configManager'; import { wordMarkConfigManager } from '../core/configManager/wordMark'; import { clipConfigManager } from '../core/configManager/clip'; import { RequestMessage } from './index'; @@ -16,6 +13,7 @@ const managerMap = { export async function createManagerConfigActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { type, value, key, managerType, option = {} } = request.data; const manage = managerMap[managerType]; diff --git a/src/background/actionListener/index.ts b/src/background/actionListener/index.ts index 59d66a33..8960730e 100644 --- a/src/background/actionListener/index.ts +++ b/src/background/actionListener/index.ts @@ -1,4 +1,4 @@ -import Chrome from '@/background/core/chrome'; +import Chrome from '@/background/core/chromeExtension'; import { BackgroundEvents } from '@/isomorphic/background'; import { createStorageActionListener } from './storage'; import { createUserActionListener } from './user'; @@ -8,8 +8,6 @@ import { createSidePanelActionListener } from './sidePanel'; import { createRequestActionListener } from './request'; import { createManagerConfigActionListener } from './configManager'; -type MessageSender = chrome.runtime.MessageSender; - type SendResponse = (response: any) => void; export interface RequestMessage { @@ -19,38 +17,34 @@ export interface RequestMessage { export const initBackGroundActionListener = () => { Chrome.runtime.onMessage.addListener( - ( - request: RequestMessage, - _sender: MessageSender, - sendResponse: SendResponse, - ) => { + (request: RequestMessage, _sender: chrome.runtime.MessageSender, sendResponse: SendResponse) => { switch (request.action) { case BackgroundEvents.OperateUser: { - createUserActionListener(request, sendResponse); + createUserActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateStorage: { - createStorageActionListener(request, sendResponse); + createStorageActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateClip: { - createClipActionListener(request, sendResponse); + createClipActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateTab: { - createTabActionListener(request, sendResponse); + createTabActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateSidePanel: { - createSidePanelActionListener(request, sendResponse); + createSidePanelActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateRequest: { - createRequestActionListener(request, sendResponse); + createRequestActionListener(request, sendResponse, _sender); break; } case BackgroundEvents.OperateManagerConfig: { - createManagerConfigActionListener(request, sendResponse); + createManagerConfigActionListener(request, sendResponse, _sender); break; } default: { diff --git a/src/background/actionListener/request.ts b/src/background/actionListener/request.ts index dfce563b..868038a6 100644 --- a/src/background/actionListener/request.ts +++ b/src/background/actionListener/request.ts @@ -6,6 +6,7 @@ import { RequestMessage } from './index'; export async function createRequestActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { url, config, options = {} } = request.data; diff --git a/src/background/actionListener/sidePanel.ts b/src/background/actionListener/sidePanel.ts index aeaabcf4..c8d555bd 100644 --- a/src/background/actionListener/sidePanel.ts +++ b/src/background/actionListener/sidePanel.ts @@ -1,35 +1,41 @@ -import { - OperateSidePanelEnum, - IOperateSidePanelData, -} from '@/isomorphic/background/sidePanel'; -import Chrome from '@/background/core/chrome'; -import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; +import { OperateSidePanelEnum, IOperateSidePanelData } from '@/isomorphic/background/sidePanel'; +import chromeExtension from '../core/chromeExtension'; import { RequestMessage } from './index'; export async function createSidePanelActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { type } = request.data; + const currentTab = await chromeExtension.tabs.getCurrentTab(sender.tab); switch (type) { case OperateSidePanelEnum.close: { - const res = await Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.ToggleSidePanel, - data: { - forceVisible: false, + chromeExtension.scripting.executeScript( + { + target: { tabId: currentTab?.id as number }, + func: () => { + return window._yuque_ext_app.toggleSidePanel(false); + }, }, - }); - callback(res); + res => { + callback(res[0]?.result); + }, + ); break; } case OperateSidePanelEnum.open: { - const res = await Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.ToggleSidePanel, - data: { - forceVisible: true, + chromeExtension.scripting.executeScript( + { + target: { tabId: currentTab?.id as number }, + func: () => { + return window._yuque_ext_app.toggleSidePanel(true); + }, + }, + res => { + callback(res[0]?.result); }, - }); - callback(res); + ); break; } default: { diff --git a/src/background/actionListener/storage.ts b/src/background/actionListener/storage.ts index 9059bb5f..1f104e24 100644 --- a/src/background/actionListener/storage.ts +++ b/src/background/actionListener/storage.ts @@ -1,13 +1,11 @@ -import { - IOperateStorageData, - OperateStorageEnum, -} from '@/isomorphic/background/storage'; +import { IOperateStorageData, OperateStorageEnum } from '@/isomorphic/background/storage'; import { storage } from '@/isomorphic/storage'; import { RequestMessage } from './index'; export async function createStorageActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { type, key, data } = request.data; switch (type) { diff --git a/src/background/actionListener/tab.ts b/src/background/actionListener/tab.ts index 1838f1f5..9b82f896 100644 --- a/src/background/actionListener/tab.ts +++ b/src/background/actionListener/tab.ts @@ -1,14 +1,12 @@ -import { - OperateTabEnum, - IOperateTabData, -} from '@/isomorphic/background/tab'; -import Chrome from '@/background/core/chrome'; +import { OperateTabEnum, IOperateTabData } from '@/isomorphic/background/tab'; +import Chrome from '@/background/core/chromeExtension'; import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; import { RequestMessage } from './index'; export async function createTabActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { type, url } = request.data; switch (type) { diff --git a/src/background/actionListener/user.ts b/src/background/actionListener/user.ts index a6bf6749..8362b566 100644 --- a/src/background/actionListener/user.ts +++ b/src/background/actionListener/user.ts @@ -1,9 +1,6 @@ import { pick } from 'lodash'; -import Chrome from '@/background/core/chrome'; -import { - IOperateUserData, - OperateUserEnum, -} from '@/isomorphic/background/user'; +import Chrome from '@/background/core/chromeExtension'; +import { IOperateUserData, OperateUserEnum } from '@/isomorphic/background/user'; import { IUser } from '@/isomorphic/interface'; import { storage } from '@/isomorphic/storage'; import requestFn from '@/background/core/request'; @@ -60,6 +57,7 @@ const removeWindow = (windowId: number) => { export async function createUserActionListener( request: RequestMessage, callback: (params: any) => void, + sender: chrome.runtime.MessageSender, ) { const { type } = request.data; switch (type) { @@ -76,12 +74,7 @@ export async function createUserActionListener( }); if (status === 200) { const accountInfo = (data as any).data as IUser; - const value = pick(accountInfo, [ - 'id', - 'login', - 'name', - 'avatar_url', - ]); + const value = pick(accountInfo, ['id', 'login', 'name', 'avatar_url']); const newValue = { ...value, login_at: Date.now(), diff --git a/src/background/browser-action.ts b/src/background/browser-action.ts index 0ef3cbca..83d33bf2 100644 --- a/src/background/browser-action.ts +++ b/src/background/browser-action.ts @@ -1,33 +1,29 @@ -import Chrome from '@/background/core/chrome'; -import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; - -function remindToRefreshPage(tabId: number) { - const msg = __i18n('你需要重新加载该页面才能剪藏。请重新加载页面后再试一次'); - Chrome.scripting.executeScript({ - target: { tabId }, - args: [{ msg }], - func: (args: { msg: string }) => { - window.alert(args.msg); // eslint-disable-line - }, - }); -} +import chromeExtension from './core/chromeExtension'; export function listenBrowserActionEvent() { - Chrome.action.onClicked.addListener(tab => { - Chrome.tabs.sendMessage( - tab.id as number, + chrome.action.onClicked.addListener(async tab => { + const currentTab = await chromeExtension.tabs.getCurrentTab(tab); + chromeExtension.scripting.executeScript( { - action: ContentScriptEvents.ToggleSidePanel, + target: { tabId: currentTab?.id as number }, + func: () => { + try { + return window._yuque_ext_app.toggleSidePanel(); + } catch (e) { + return { error: e }; + } + }, }, - () => { - /** - * 插件更新后会断链接,需要提醒用户手动刷新下页面 - */ - if ( - Chrome.runtime.lastError?.message === - 'Could not establish connection. Receiving end does not exist.' - ) { - remindToRefreshPage(tab.id as number); + res => { + if (res[0]?.result?.error) { + const msg = __i18n('你需要重新加载该页面才能剪藏。请重新加载页面后再试一次'); + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab?.id as number }, + args: [{ msg }], + func: (args: { msg: string }) => { + window.alert(args.msg); // eslint-disable-line + }, + }); } }, ); diff --git a/src/background/context-menu.ts b/src/background/context-menu.ts index 77bc550f..c7ba1466 100644 --- a/src/background/context-menu.ts +++ b/src/background/context-menu.ts @@ -1,6 +1,5 @@ -import Chrome from '@/background/core/chrome'; -import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; import { __i18n } from '@/isomorphic/i18n'; +import chromeExtension from './core/chromeExtension'; interface MenuItem { id: string; @@ -33,32 +32,40 @@ const menuList: MenuItem[] = [ ]; export function createContextMenu() { - menuList.forEach(item => Chrome.contextMenus.create(item)); + menuList.forEach(item => chrome.contextMenus.create(item)); } export function listenContextMenuEvents() { - Chrome.contextMenus.onClicked.addListener((info, tab) => { - if (!tab) return; - + chromeExtension.contextMenus.onClicked.addListener(async (info, tab) => { + const currentTab = await chromeExtension.tabs.getCurrentTab(tab); switch (info.menuItemId) { case menuList[0].id: { const { selectionText } = info; - Chrome.tabs.sendMessage(tab.id as number, { - action: ContentScriptEvents.AddContentToClipAssistant, - data: `${selectionText}
`, + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab?.id as number }, + args: [{ html: `${selectionText}
` }], + func: args => { + window._yuque_ext_app.addContentToClipAssistant(args.html, true); + }, }); break; } case menuList[1].id: - Chrome.tabs.sendMessage(tab.id as number, { - action: ContentScriptEvents.ToggleSidePanel, + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab?.id as number }, + func: () => { + return window._yuque_ext_app.toggleSidePanel(); + }, }); break; case menuList[2].id: { const { srcUrl } = info; - Chrome.tabs.sendMessage(tab.id as number, { - action: ContentScriptEvents.AddContentToClipAssistant, - data: ``, + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab?.id as number }, + args: [{ html: `` }], + func: args => { + window._yuque_ext_app.addContentToClipAssistant(args.html, true); + }, }); break; } diff --git a/src/background/core/chrome.ts b/src/background/core/chrome.ts deleted file mode 100644 index d98caebf..00000000 --- a/src/background/core/chrome.ts +++ /dev/null @@ -1,59 +0,0 @@ -const { - action, - cookies, - contextMenus, - runtime, - storage, - tabs, - webRequest, - declarativeNetRequest, - windows, - downloads, - scripting, - commands, -} = global.chrome; - -export default { - action, - cookies, - contextMenus, - runtime, - storage, - tabs, - webRequest, - declarativeNetRequest, - windows, - downloads, - scripting, - commands, - sendMessageToCurrentTab: (message: any) => - new Promise(resolve => { - tabs.query({ active: true, lastFocusedWindow: true }, res => { - const tabId = res[0]?.id; - if (!tabId) { - resolve(null); - return; - } - tabs.sendMessage(tabId, message, res1 => { - resolve(res1); - }); - }); - }), - sendMessageToAllTab: (message: any) => { - new Promise(resolve => { - tabs.query({ status: 'complete' }, res => { - for (const tab of res) { - if (tab.id) { - try { - tabs.sendMessage(tab.id, message, res1 => { - resolve(res1); - }); - } catch (e) { - // 吞掉抛错 - } - } - } - }); - }); - }, -}; diff --git a/src/background/core/chromeExtension.ts b/src/background/core/chromeExtension.ts new file mode 100644 index 00000000..f2dda4d4 --- /dev/null +++ b/src/background/core/chromeExtension.ts @@ -0,0 +1,70 @@ +const chromeExtension = { + action: chrome.action, + cookies: chrome.cookies, + contextMenus: chrome.contextMenus, + runtime: chrome.runtime, + storage: chrome.storage, + tabs: { + ...chrome.tabs, + getCurrentTab: async (tab?: chrome.tabs.Tab) => { + if (tab?.id) { + return tab; + } + const tabs = await chrome.tabs.query({ lastFocusedWindow: true, active: true }); + return tabs[0]; + }, + sendMessageToCurrentTab: async (message: any, tab?: chrome.tabs.Tab) => { + const currentTab = await chromeExtension.tabs.getCurrentTab(tab); + const response = await chrome.tabs.sendMessage(currentTab.id as number, message); + return response; + }, + sendMessageToAllTab: async (message: any) => { + const tabs = await chrome.tabs.query({}); + for (const tab of tabs) { + try { + await chrome.tabs.sendMessage(tab.id as number, message); + } catch (error) { + // 吞掉错误 + } + } + }, + }, + webRequest: chrome.webRequest, + declarativeNetRequest: chrome.declarativeNetRequest, + windows: chrome.windows, + downloads: chrome.downloads, + scripting: chrome.scripting, + commands: chrome.commands, + sendMessageToCurrentTab: (message: any) => + new Promise(resolve => { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, res => { + const tabId = res[0]?.id; + if (!tabId) { + resolve(null); + return; + } + chrome.tabs.sendMessage(tabId, message, res1 => { + resolve(res1); + }); + }); + }), + sendMessageToAllTab: (message: any) => { + new Promise(resolve => { + chrome.tabs.query({ status: 'complete' }, res => { + for (const tab of res) { + if (tab.id) { + try { + chrome.tabs.sendMessage(tab.id, message, res1 => { + resolve(res1); + }); + } catch (e) { + // 吞掉抛错 + } + } + } + }); + }); + }, +}; + +export default chromeExtension; diff --git a/src/background/core/configManager/clip.ts b/src/background/core/configManager/clip.ts index b89eb127..6b6e67b0 100644 --- a/src/background/core/configManager/clip.ts +++ b/src/background/core/configManager/clip.ts @@ -1,17 +1,12 @@ -import Chrome from '@/background/core/chrome'; +import Chrome from '@/background/core/chromeExtension'; import { STORAGE_KEYS } from '@/config'; -import { - defaultClipConfig, - IClipConfig, - ClipConfigKey, -} from '@/isomorphic/constant/clip'; +import { defaultClipConfig, IClipConfig, ClipConfigKey } from '@/isomorphic/constant/clip'; import { IConfigManagerOption } from '@/isomorphic/background/configManager'; import { storage } from '@/isomorphic/storage'; class ClipConfigManager { async get() { - const config: IClipConfig = - (await storage.get(STORAGE_KEYS.SETTINGS.CLIP_CONFIG)) || {}; + const config: IClipConfig = (await storage.get(STORAGE_KEYS.SETTINGS.CLIP_CONFIG)) || {}; // 做一次 config 的合并,保证获取时一定包含 config 中的每一个元素 for (const _key of Object.keys(defaultClipConfig)) { diff --git a/src/background/core/configManager/levitate.ts b/src/background/core/configManager/levitate.ts index 88bbfd6d..a7131852 100644 --- a/src/background/core/configManager/levitate.ts +++ b/src/background/core/configManager/levitate.ts @@ -1,18 +1,13 @@ -import Chrome from '@/background/core/chrome'; +import Chrome from '@/background/core/chromeExtension'; import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; -import { - defaultLevitateConfig, - ILevitateConfig, - LevitateConfigKey, -} from '@/isomorphic/constant/levitate'; +import { defaultLevitateConfig, ILevitateConfig, LevitateConfigKey } from '@/isomorphic/constant/levitate'; import { IConfigManagerOption } from '@/isomorphic/background/configManager'; import { STORAGE_KEYS } from '@/config'; import { storage } from '@/isomorphic/storage'; class LevitateConfigManager { async get() { - const config: ILevitateConfig = - (await storage.get(STORAGE_KEYS.SETTINGS.LEVITATE_BALL_CONFIG)) || {}; + const config: ILevitateConfig = (await storage.get(STORAGE_KEYS.SETTINGS.LEVITATE_BALL_CONFIG)) || {}; // 做一次 config 的合并,保证获取时一定包含 config 中的每一个元素 for (const _key of Object.keys(defaultLevitateConfig)) { diff --git a/src/background/core/configManager/wordMark.ts b/src/background/core/configManager/wordMark.ts index 60ba25cb..6cbf8db5 100644 --- a/src/background/core/configManager/wordMark.ts +++ b/src/background/core/configManager/wordMark.ts @@ -1,9 +1,5 @@ -import { - IWordMarkConfig, - WordMarkConfigKey, - defaultWordMarkConfig, -} from '@/isomorphic/constant/wordMark'; -import Chrome from '@/background/core/chrome'; +import { IWordMarkConfig, WordMarkConfigKey, defaultWordMarkConfig } from '@/isomorphic/constant/wordMark'; +import chromeExtension from '@/background/core/chromeExtension'; import { IConfigManagerOption } from '@/isomorphic/background/configManager'; import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; import { STORAGE_KEYS } from '@/config'; @@ -16,36 +12,29 @@ class WordMarkConfigManager { return this.transformConfig(config); } - async update( - key: WordMarkConfigKey, - value: any, - option?: IConfigManagerOption, - ) { + async update(key: WordMarkConfigKey, value: any, option?: IConfigManagerOption) { const config = await this.getStorageConfig(); const result: IWordMarkConfig = { ...config, [key]: value, }; - await Chrome.storage.local.set({ + await chromeExtension.storage.local.set({ [STORAGE_KEYS.SETTINGS.WORD_MARK_CONFIG]: result, }); this.noticeWebPage(result, option); return result; } - private noticeWebPage( - config: IWordMarkConfig, - option?: IConfigManagerOption, - ) { + private noticeWebPage(config: IWordMarkConfig, option?: IConfigManagerOption) { const { notice = false } = option || {}; if (!notice) { return; } // 异步通知页面 wordMarkConfig 发生了改变 - Chrome.tabs.query({ status: 'complete' }, tabs => { + chromeExtension.tabs.query({ status: 'complete' }, tabs => { for (const tab of tabs) { if (tab.id) { - Chrome.tabs.sendMessage(tab.id, { + chromeExtension.tabs.sendMessage(tab.id, { action: ContentScriptEvents.WordMarkConfigChange, data: this.transformConfig(config), }); @@ -55,8 +44,7 @@ class WordMarkConfigManager { } private async getStorageConfig() { - const config: IWordMarkConfig = - (await storage.get(STORAGE_KEYS.SETTINGS.WORD_MARK_CONFIG)) || {}; + const config: IWordMarkConfig = (await storage.get(STORAGE_KEYS.SETTINGS.WORD_MARK_CONFIG)) || {}; // 做一次 config 的合并,保证获取时一定包含 config 中的每一个元素 for (const _key of Object.keys(defaultWordMarkConfig)) { @@ -69,10 +57,7 @@ class WordMarkConfigManager { // 由于历史数据可能被写入 string 或者 string[] 如果判断出是这种数据的,将内容置空 if (key === 'disableUrl') { const tempValue = config[key]; - if ( - typeof tempValue === 'string' || - typeof tempValue?.[0] === 'string' - ) { + if (typeof tempValue === 'string' || typeof tempValue?.[0] === 'string') { config[key] = []; } } @@ -81,11 +66,7 @@ class WordMarkConfigManager { * 当缓存中的 toolbars 长度和默认不一致时,说明扩展了 toolbars * 然后将新增的 toolbars 扩展到缓存的最后 */ - if ( - key === 'toolbars' && - value && - config[key].length !== defaultWordMarkConfig.toolbars.length - ) { + if (key === 'toolbars' && value && config[key].length !== defaultWordMarkConfig.toolbars.length) { const newAddToolbars: WordMarkOptionTypeEnum[] = []; for (const item of defaultWordMarkConfig.toolbars) { if (!config[key].includes(item)) { @@ -105,12 +86,8 @@ class WordMarkConfigManager { const result = { ...config, - filterInnerPinList: config.innerPinList.filter( - item => !disableFunction.includes(item), - ), - filterToolbars: config.toolbars.filter( - item => !disableFunction.includes(item), - ), + filterInnerPinList: config.innerPinList.filter(item => !disableFunction.includes(item)), + filterToolbars: config.toolbars.filter(item => !disableFunction.includes(item)), }; return result; } diff --git a/src/background/core/request.ts b/src/background/core/request.ts index a9fccf61..2e87f9f6 100644 --- a/src/background/core/request.ts +++ b/src/background/core/request.ts @@ -1,4 +1,4 @@ -import Chrome from '@/background/core/chrome'; +import Chrome from '@/background/core/chromeExtension'; import { pkg, REQUEST_HEADER_VERSION, @@ -8,10 +8,7 @@ import { EXTENSION_ID, } from '@/config'; import 'whatwg-fetch'; -import { - IRequestConfig, - IRequestOptions, -} from '@/isomorphic/background/request'; +import { IRequestConfig, IRequestOptions } from '@/isomorphic/background/request'; import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; import { getCurrentAccount } from './util'; @@ -26,11 +23,7 @@ const generateQuery = (params: { [key: string]: any }) => { return `?${new URLSearchParams(params).toString()}`; }; -const setCsrfToken = ( - domain: string, - cookieName: string, - value: string, -): Promise => { +const setCsrfToken = (domain: string, cookieName: string, value: string): Promise => { return new Promise(resolve => { Chrome.cookies.set({ url: domain, name: cookieName, value }, cookie => { resolve(cookie as chrome.cookies.Cookie); @@ -42,10 +35,7 @@ const generateRandomToken = (): string => { return Math.random().toString(36).substring(2); }; -export const getCsrfToken = async ( - domain: string, - cookieName: string, -): Promise => { +export const getCsrfToken = async (domain: string, cookieName: string): Promise => { return new Promise(resolve => { Chrome.cookies.get({ url: domain, name: cookieName }, cookie => { if (cookie) { @@ -103,9 +93,7 @@ async function request( } else if (options.method === 'GET' && config.data) { const params = config.data; const paramsArray: string[] = []; - Object.keys(params).forEach(key => - paramsArray.push(`${key}=${params[key]}`), - ); + Object.keys(params).forEach(key => paramsArray.push(`${key}=${params[key]}`)); if (paramsArray.length > 0) { queryString = `?${paramsArray.join('&')}`; } @@ -115,10 +103,7 @@ async function request( ...options, }); const responseJson = await response.json(); - if ( - responseJson.status === 400 && - responseJson.code === 'force_upgrade_version' - ) { + if (responseJson.status === 400 && responseJson.code === 'force_upgrade_version') { Chrome.sendMessageToAllTab({ action: ContentScriptEvents.ForceUpgradeVersion, data: { @@ -143,16 +128,11 @@ async function request( } } -export async function uploadFile( - url: string, - file: File, - attachableType = 'User', - fileType = 'image', -) { +export async function uploadFile(url: string, file: File, attachableType = 'User', fileType = 'image') { const formData = new FormData(); formData.append('file', file); const csrfToken = await getCsrfToken(YUQUE_DOMAIN, YUQUE_CSRF_COOKIE_NAME); - const user = await getCurrentAccount() as any; + const user = (await getCurrentAccount()) as any; const query = generateQuery({ attachable_type: attachableType, attachable_id: user.id, diff --git a/src/background/shortcut-listener.ts b/src/background/shortcut-listener.ts index 21e5ea6e..3b34cfc8 100644 --- a/src/background/shortcut-listener.ts +++ b/src/background/shortcut-listener.ts @@ -1,36 +1,37 @@ -import Chrome from '@/background/core/chrome'; -import { ContentScriptEvents } from '@/isomorphic/event/contentScript'; +import chromeExtension from '@/background/core/chromeExtension'; export function listenShortcut() { - Chrome.commands.onCommand.addListener(command => { + chromeExtension.commands.onCommand.addListener(async (command, tab) => { + const currentTab = await chromeExtension.tabs.getCurrentTab(tab); switch (command) { - case 'openSidePanel': { - Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.ToggleSidePanel, - }); - break; - } case 'selectArea': { - Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.SelectArea, - data: { - formShortcut: true, + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab.id as number }, + func: () => { + return window._yuque_ext_app.clipSelectArea({ + formShortcut: true, + }); }, }); break; } case 'startOcr': { - Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.ScreenOcr, - data: { - formShortcut: true, + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab.id as number }, + func: () => { + return window._yuque_ext_app.clipScreenOcr({ + formShortcut: true, + }); }, }); break; } - case 'collectLink': { - Chrome.sendMessageToCurrentTab({ - action: ContentScriptEvents.CollectLink, + case 'clipPage': { + chromeExtension.scripting.executeScript({ + target: { tabId: currentTab.id as number }, + func: () => { + return window._yuque_ext_app.clipPage(); + }, }); break; } diff --git a/src/components/AccountLayout/Login.tsx b/src/components/AccountLayout/Login.tsx index 95f5744a..e10f8d8e 100644 --- a/src/components/AccountLayout/Login.tsx +++ b/src/components/AccountLayout/Login.tsx @@ -7,7 +7,7 @@ import { __i18n } from '@/isomorphic/i18n'; import LinkHelper from '@/isomorphic/link-helper'; import { VERSION } from '@/config'; import YuqueLogo from '@/assets/images/yuque-logo.png'; -import { isRunningInjectPage } from '@/core/uitl'; +import Env from '@/isomorphic/env'; import LarkIcon from '../LarkIcon'; import Typography from '../Typography'; import styles from './Login.module.less'; @@ -42,7 +42,7 @@ function Login(props: ILoginProps) { return (
- {isRunningInjectPage && ( + {Env.isRunningHostPage && ( void; // 侧边栏宽度更改时触发的函数 + onResizeEnd?: (width: number) => void; // 拖拽结束时触发的回掉函数 + id?: string; + // 拖拽方向 + direction?: 'left' | 'right'; +} + +function DragBar(props: DragBarProps) { + const { + minWidth = 44, + maxWidth = 400, + width, + direction = 'left', + id = 'dragBarContainer', + onResizeAsideWidth, + onResizeEnd, + } = props; + const [dragging, setDragging] = useState(false); + const startClientXRef = useRef(0); + const [sidebarWidth, setSidebarWidth] = useState(width); + const currenSidebarWidthRef = useRef(sidebarWidth); + const pxWidth = `${sidebarWidth}px`; + + const calcSideBarWidth = (e: React.MouseEvent | MouseEvent) => { + const startX = startClientXRef.current; + const endX = e.clientX; + const changeWidth = direction === 'left' ? startX - endX : endX - startX; + const currentSideBarWidth = currenSidebarWidthRef.current + changeWidth; + if (currentSideBarWidth < minWidth) { + return minWidth; + } + if (currentSideBarWidth > maxWidth) { + return maxWidth; + } + return currentSideBarWidth; + }; + + const resizeAsideWidth = (e: React.MouseEvent | MouseEvent) => { + const currentSideBarWidth = calcSideBarWidth(e); + currenSidebarWidthRef.current = currentSideBarWidth; + startClientXRef.current = e.clientX; + onResizeAsideWidth?.({ width: currentSideBarWidth }); + setSidebarWidth(currentSideBarWidth); + }; + + const handleMouseDown = (e: React.MouseEvent | MouseEvent) => { + setDragging(true); + startClientXRef.current = e.clientX; + }; + + const handleMouseUp = () => { + setDragging(false); + onResizeEnd?.(sidebarWidth); + }; + + const handleMouseMove = (e: React.MouseEvent | MouseEvent) => { + if (!dragging) { + return; + } + resizeAsideWidth(e); + }; + + useEffect(() => { + setSidebarWidth(width); + }, [width]); + + return ( +
+
+ {props.children} +
+ {dragging && ( +
{ + setDragging(false); + }} + /> + )} +
+
+ ); +} + +export default DragBar; diff --git a/src/components/LarkIcon/SvgMap.ts b/src/components/LarkIcon/SvgMap.ts index 6379c403..48e3cc2e 100644 --- a/src/components/LarkIcon/SvgMap.ts +++ b/src/components/LarkIcon/SvgMap.ts @@ -9,6 +9,7 @@ export const SvgMaps = { 'arrow-down': import('@/assets/svg/arrow-down.svg'), 'book-logo': import('@/assets/svg/book-logo.svg'), 'clip-assistant': import('@/assets/svg/clip-assistant.svg'), + 'clip-page': import('@/assets/svg/clip-page.svg'), 'clipper': import('@/assets/svg/clipper.svg'), 'clipping': import('@/assets/svg/clipping.svg'), 'close-circle': import('@/assets/svg/close-circle.svg'), @@ -28,3 +29,4 @@ export const SvgMaps = { 'yuque-logo': import('@/assets/svg/yuque-logo.svg'), 'yuque-logo1': import('@/assets/svg/yuque-logo1.svg'), }; + diff --git a/src/components/SuperSideBar/container/Header/index.tsx b/src/components/SuperSideBar/container/Header/index.tsx index e8407e1e..bfad335d 100644 --- a/src/components/SuperSideBar/container/Header/index.tsx +++ b/src/components/SuperSideBar/container/Header/index.tsx @@ -3,11 +3,11 @@ import Icon, { CloseOutlined } from '@ant-design/icons'; import { Tooltip } from 'antd'; import { STORAGE_KEYS } from '@/config'; import { __i18n } from '@/isomorphic/i18n'; -import { isRunningInjectPage } from '@/core/uitl'; import LinkHelper from '@/isomorphic/link-helper'; +import LarkIcon from '@/components/LarkIcon'; +import Env from '@/isomorphic/env'; import { backgroundBridge } from '@/core/bridge/background'; import UserAvatar from '@/components/UserAvatar'; -import YuqueLogoSvg from '@/assets/svg/yuque-logo.svg'; import SettingSvg from '@/assets/svg/setting.svg'; import HomeSvg from '@/assets/svg/home.svg'; import styles from './index.module.less'; @@ -47,8 +47,8 @@ function SuperSidebarHeader() { return (
- - {__i18n('语雀')} + + {Env.isBate ? __i18n('语雀 Beta') : __i18n('语雀')}
@@ -89,7 +89,7 @@ function SuperSidebarHeader() {
- {isRunningInjectPage && ( + {Env.isRunningHostPage && (
diff --git a/src/components/SuperSideBar/impl/ClipAssistant/index.tsx b/src/components/SuperSideBar/impl/ClipAssistant/index.tsx index 054bd8d5..78ac0720 100644 --- a/src/components/SuperSideBar/impl/ClipAssistant/index.tsx +++ b/src/components/SuperSideBar/impl/ClipAssistant/index.tsx @@ -30,8 +30,8 @@ import { ContentScriptMessageActions, ContentScriptMessageKey, } from '@/isomorphic/event/contentScript'; +import Env from '@/isomorphic/env'; import { IClipConfig } from '@/isomorphic/constant/clip'; -import { isRunningInjectPage } from '@/core/uitl'; import LarkIcon from '@/components/LarkIcon'; import { superSidebar } from '@/components/SuperSideBar/index'; import AddTagButton from './component/AddTagButton'; @@ -82,7 +82,7 @@ function ClipContent() { text: string, link: { text: string; href: string }, ) => { - if (isRunningInjectPage) { + if (Env.isRunningHostPage) { window.parent.postMessage( { key: ContentScriptMessageKey, @@ -135,7 +135,7 @@ function ClipContent() { text: __i18n('立即查看'), }); } - if (isRunningInjectPage) { + if (Env.isRunningHostPage) { backgroundBridge.sidePanel.close(); } editor.setContent(''); @@ -172,6 +172,13 @@ function ClipContent() { editorRef.current?.focusToStart(); }; + const onClipPage = async () => { + const html = await backgroundBridge.clip.clipPage(); + await addLinkWhenEmpty(); + editorRef.current?.appendContent(html); + editorRef.current?.insertBreakLine(); + }; + const addLinkWhenEmpty = async () => { if (!editorRef.current?.isEmpty()) { return; @@ -255,39 +262,25 @@ function ClipContent() { }, [selectSavePosition]); useEffect(() => { - const onStartSelectArea = () => { - const div = document.querySelector(`#${ClipSelectAreaId}`); - (div as HTMLDivElement)?.click(); - }; const onStartScreenOcr = () => { const div = document.querySelector(`#${ClipScreenOcrId}`); (div as HTMLDivElement)?.click(); }; - const onStartCollectLink = () => { - const div = document.querySelector(`#${ClipCollectLinkId}`); - (div as HTMLDivElement)?.click(); - }; + const onMessage = (e: any) => { if (e.data.key !== ClipAssistantMessageKey) { return; } switch (e.data.action) { case ClipAssistantMessageActions.addContent: { + addLinkWhenEmpty(); editorRef.current?.appendContent(e.data?.data); break; } - case ClipAssistantMessageActions.startSelectArea: { - onStartSelectArea(); - break; - } case ClipAssistantMessageActions.startScreenOcr: { onStartScreenOcr(); break; } - case ClipAssistantMessageActions.startCollectLink: { - onStartCollectLink(); - break; - } default: { break; } @@ -314,7 +307,7 @@ function ClipContent() { {renderLoading()}
node} >
node} >
node} >
- - {__i18n('链接收藏')} + + {__i18n('全文剪藏')}
diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx index a1c5b6b0..b0323c00 100644 --- a/src/components/UserAvatar/index.tsx +++ b/src/components/UserAvatar/index.tsx @@ -32,7 +32,7 @@ const UserAvatar = () => { onLogout?.(); break; case 'feedback': - window.open(LinkHelper.feedback, '_blank'); + window.open(LinkHelper.feedback(), '_blank'); break; case 'useHelp': { window.open(LinkHelper.helpDoc, '_blank'); diff --git a/src/components/lake-editor/modal-ocr.tsx b/src/components/lake-editor/modal-ocr.tsx index ff84fe28..063b48b3 100644 --- a/src/components/lake-editor/modal-ocr.tsx +++ b/src/components/lake-editor/modal-ocr.tsx @@ -3,6 +3,7 @@ import Modal from 'antd/lib/modal'; import { Button, message } from 'antd/lib'; import './modal-ocr.less'; +import LarkIcon from '../LarkIcon'; export interface IOCRModalProps { src: string; @@ -72,8 +73,6 @@ export default function OCRModal(props: IOCRModalProps) { ), ); - const Icon = props.Icon; - return (
@@ -96,7 +95,7 @@ export default function OCRModal(props: IOCRModalProps) { } }} > - + 复制全文 {props.onInsertText && ( @@ -107,7 +106,6 @@ export default function OCRModal(props: IOCRModalProps) { }} className="ne-ocr-insert" > - 插入文中 )} diff --git a/src/config.ts b/src/config.ts index 92e36ca3..fc8f7883 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ export const STORAGE_KEYS = { WORD_MARK_CONFIG: 'settings/word-mark-config', LEVITATE_BALL_CONFIG: 'settings/levitate-ball-config', CLIP_CONFIG: 'settings/clip-config', + SIDE_PANEL_CONFIG: 'settings/sidePanel-config', }, NOTE: { SELECT_TAGS: 'note/select-tags', diff --git a/src/core/bridge/background/clip.ts b/src/core/bridge/background/clip.ts index b626c51f..2d29ae04 100644 --- a/src/core/bridge/background/clip.ts +++ b/src/core/bridge/background/clip.ts @@ -1,6 +1,6 @@ import { BackgroundEvents } from '@/isomorphic/background'; import { OperateClipEnum } from '@/isomorphic/background/clip'; -import { isRunningInjectPage } from '@/core/uitl'; +import Env from '@/isomorphic/env'; import { ICallBridgeImpl } from './index'; export function createClipBridge(impl: ICallBridgeImpl) { @@ -10,7 +10,7 @@ export function createClipBridge(impl: ICallBridgeImpl) { return new Promise(resolve => { impl( BackgroundEvents.OperateClip, - { type: OperateClipEnum.screenOcr, isRunningInjectPage }, + { type: OperateClipEnum.screenOcr, isRunningHostPage: Env.isRunningHostPage }, (res: string | null) => { resolve(res); }, @@ -22,7 +22,19 @@ export function createClipBridge(impl: ICallBridgeImpl) { return new Promise(resolve => { impl( BackgroundEvents.OperateClip, - { type: OperateClipEnum.selectArea, isRunningInjectPage }, + { type: OperateClipEnum.selectArea, isRunningHostPage: Env.isRunningHostPage }, + (res: string) => { + resolve(res); + }, + ); + }); + }, + + async clipPage(): Promise { + return new Promise(resolve => { + impl( + BackgroundEvents.OperateClip, + { type: OperateClipEnum.clipPage, isRunningHostPage: Env.isRunningHostPage }, (res: string) => { resolve(res); }, diff --git a/src/core/parseDom/index.ts b/src/core/parseDom/index.ts new file mode 100644 index 00000000..2029e021 --- /dev/null +++ b/src/core/parseDom/index.ts @@ -0,0 +1,249 @@ +import { Readability } from '@mozilla/readability'; +import { CodeParsePlugin, ImageParsePlugin, LinkParsePlugin, HexoCodeParsePlugin, CanvasParsePlugin } from './plugin'; +import { BasePlugin } from './plugin/base'; +import { parsePageConfig } from './parsePageConfig'; +import { screenShot } from '../screen-shot'; + +class ParseDom { + private parsePlugin: BasePlugin[] = []; + private filterConfig = parsePageConfig; + + constructor() { + this.registerParsePlugin(); + } + + async parseDom(domArray: Element[]) { + const result: Array = []; + for (const dom of domArray) { + if (this.isYuqueContent(dom)) { + try { + const htmlArray = await this.parsePageYuqueContent(dom); + result.push(htmlArray[0]); + continue; + } catch (error) { + // + } + } + const elements = await this.parseCommonDom([dom]); + result.push(elements[0].outerHTML); + } + return result; + } + + private async parseCommonDom(domArray: Element[]) { + const cloneDomArray: Element[] = []; + for (const originDom of domArray) { + const cloneDom = originDom.cloneNode(true) as Element; + // 对 clone 对一些提前对预处理 + await this.preProcessCloneDom(cloneDom, originDom); + // 将所有 dom 按照 plugin 的注册顺序执行一次转化 + for (const plugin of this.parsePlugin) { + const div = document.createElement('div'); + div.appendChild(cloneDom); + await plugin.parse(div); + } + cloneDomArray.push(cloneDom); + } + return cloneDomArray; + } + + async parsePage() { + // 语雀的全文剪藏 + if (this.isYuqueContent(document.body)) { + try { + const result = await this.parsePageYuqueContent(document.body); + return result.join(''); + } catch (error) { + // + } + } + // 普通页面的全文剪藏 + const result = await this.parseCommonPage(); + return result; + } + + private async parseCommonPage() { + const pageConfig = this.filterConfig; + const rule = pageConfig.find(item => { + const regExp = new RegExp(item.url); + if (regExp.test(window.location.href)) { + return item; + } + return null; + }); + const originDomArray: Element[] = []; + if (rule?.include.length) { + rule.include.forEach(item => { + const doms = document.querySelectorAll(item); + doms.forEach(item => { + originDomArray.push(item); + }); + }); + } else { + originDomArray.push(document.body); + } + // 先对 dom 做一些解析的预处理 + const cloneDomArray = await this.parseCommonDom(originDomArray); + // 创建一个 fragment 片段用于存储 dom + const fragment = document.createElement('div'); + cloneDomArray.forEach(item => fragment.appendChild(item)); + + // Readability 会去除掉 style 并且无法配置,所以将 clone 下来的 dom 中 style 属性转移到 data-style 中去, + const elementsWithStyle = fragment.querySelectorAll('[style]'); + elementsWithStyle.forEach(item => { + const style = item.getAttribute('style'); + item.setAttribute('data-style', style as string); + }); + + // 如果对该网站做了特殊配置,去除掉 dom 里满足去除条件的 dom + rule?.exclude?.forEach(item => { + fragment.querySelectorAll(item).forEach(node => { + node.parentNode?.removeChild(node); + }); + }); + + // 将 document clone 出一份 + const cloneDocument = document.cloneNode(true) as Document; + Array.from(cloneDocument.body.children).forEach(item => item.parentNode?.removeChild(item)); + cloneDocument.body.appendChild(fragment); + + // 将内容交给 Readability 去解析一次 + const result = new Readability(cloneDocument, { + keepClasses: true, + disableJSONLD: true, + serializer: async el => { + // 如果 dom 上包含 data-style 将样式还原 + const elementsWithDataStyle = (el as HTMLElement).querySelectorAll('[data-style]'); + elementsWithDataStyle.forEach(item => { + const dataStyle = item.getAttribute('data-style'); + item.setAttribute('style', dataStyle as string); + item.removeAttribute('data-style'); + }); + return (el as HTMLElement).innerHTML; + }, + }).parse(); + + return result?.content; + } + + private registerParsePlugin() { + // 注册 parsePlugin 的插件,解析插件时会按照顺序执行 + this.parsePlugin.push(new LinkParsePlugin()); + this.parsePlugin.push(new ImageParsePlugin()); + this.parsePlugin.push(new CodeParsePlugin()); + this.parsePlugin.push(new HexoCodeParsePlugin()); + this.parsePlugin.push(new CanvasParsePlugin()); + } + + /** + * 由于 parse 里面会对 clone dom 做一些特殊的处理 + * 会导致 clone dom 原有 dom 之间的结构不一致 + * 对于一些依赖原始 dom 的方法进行预处理,例如 canvas + * 注意,这个方法里的所有处理函数,尽量不要对 clone dom 做节点的更改 + */ + private async preProcessCloneDom(cloneDom: Element, originDom: Element) { + const cloneDomParent = document.createElement('div'); + cloneDomParent.append(cloneDom); + const originDomParent = originDom.parentNode ? originDom.parentNode : document; + // 对 canvas 做处理 + const originCanvasArray = originDomParent.querySelectorAll('canvas'); + const cloneCanvasArray = cloneDomParent.querySelectorAll('canvas'); + for (const [index, originCanvas] of originCanvasArray.entries()) { + const cloneCanvas = cloneCanvasArray[index]; + // 对所有的 canvas 做一些 preset 处理 + try { + const context = cloneCanvas.getContext('2d'); + // 将原始canvas的内容绘制到克隆的canvas中 + context?.drawImage(originCanvas, 0, 0); + } catch (error) { + // + } + } + + const originVideoArray = originDomParent.querySelectorAll('video'); + const cloneVideoArray = cloneDomParent.querySelectorAll('video'); + + for (const [index, originVideo] of originVideoArray.entries()) { + const rect = originVideo.getBoundingClientRect(); + const cloneVideo = cloneVideoArray[index]; + const canvas = await screenShot({ + x: rect.x, + y: rect.top, + width: rect.width, + height: rect.height, + }); + + await new Promise(resolve => { + const image = document.createElement('img'); + image.src = canvas.toDataURL('image/jpeg'); + cloneVideo.parentNode?.replaceChild(image, cloneVideo); + resolve(true); + }); + } + } + + private isYuqueContent(element: Element) { + if (element.closest('.ne-viewer-body') || element.querySelector('.ne-viewer-body')) { + return true; + } + return false; + } + + private async parsePageYuqueContent(element: Element) { + let ids: string[] = []; + if (element.classList.contains('ne-viewer-body')) { + const childIds = this.findYuqueChildId(element); + ids = ids.concat(childIds); + } else if (element.closest('.ne-viewer-body')) { + const id = this.findYuqueNeTag(element)?.id; + if (id) { + ids.push(id); + } + } else if (element.querySelector('.ne-viewer-body')) { + const childIds = this.findYuqueChildId(element.querySelector('.ne-viewer-body')); + ids = ids.concat(childIds); + } + const result = await window._yuque_ext_app.senMessageToPage({ + serviceName: 'YuqueService', + serviceMethod: 'parseContent', + params: { + ids, + }, + }); + return result; + } + + private findYuqueChildId(element: Element | null) { + let ids: string[] = []; + if (!element) { + return ids; + } + element.childNodes.forEach(item => { + const id = (item as Element).id; + if (id) { + ids.push(id); + } else { + const childIds = this.findYuqueChildId(item as Element); + ids = ids.concat(childIds); + } + }); + return ids; + } + + private findYuqueNeTag(element: Element): Element | null { + // 检查当前元素是否以 "ne" 开头的标签 + if (element.tagName.toLowerCase().startsWith('ne')) { + return element; + } + + // 递归查找父元素 + if (element.parentNode) { + return this.findYuqueNeTag(element.parentNode as Element); + } + + // 如果没有找到匹配的标签,则返回 null + return null; + } +} + +export const parseDom = new ParseDom(); diff --git a/src/core/parseDom/parsePageConfig.ts b/src/core/parseDom/parsePageConfig.ts new file mode 100644 index 00000000..76307633 --- /dev/null +++ b/src/core/parseDom/parsePageConfig.ts @@ -0,0 +1,239 @@ +export const parsePageConfig = [ + { + url: 'https?:\\\\/\\\\/www\\\\.toutiao\\\\.com\\\\/w\\\\/([a-zA-Z]\\\\/)?[a-zA-Z]\\\\d{10,}', + include: ['.wtt-content'], + exclude: [ + '.xgplayer', + '#comment-area', + '.content-action', + '.author-info', + '.article-content > h1', + '.weitoutiao-content > h1', + '.music-time', + ], + }, + { + url: 'https?:\\\\/\\\\/www\\\\.toutiao\\\\.com\\\\/article', + include: ['.article-content'], + exclude: ['.xgplayer'], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.toutiao\\\\.com\\\\/([a-zA-Z]\\\\/)?[a-zA-Z]\\\\d{10,}', + include: ['.article-content', '.weitoutiao-content'], + exclude: [ + '#comment-area', + '.content-action', + '.author-info', + '.article-content > h1', + '.weitoutiao-content > h1', + 'xg-mini-layer', + 'xg-center-grid', + '.music-time', + ], + }, + { + url: '^https?:\\\\/\\\\/zhuanlan\\\\.zhihu\\\\.com\\\\/p\\\\/', + include: ['article.Post-Main'], + exclude: ['.Post-Author', '.Post-topicsAndReviewer', '.RichContent-actions', 'img.LinkCard-image', '.Post-Header'], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.zhihu\\\\.com\\\\/question\\\\/\\\\d+', + include: ['.QuestionHeader-title', '.ContentItem.AnswerItem', '.List-item'], + exclude: [ + '.ContentItem-time', + '.ContentItem-actions', + '.UserLink.AuthorInfo-avatarWrapper', + '.ContentItem-expandButton', + '.Voters', + '.ModalWrap', + '.ModalLoading-content', + '.LinkCard-imageCell', + '.QuestionInvitation', + '.QuestionHeader-title', + ], + }, + { + url: '^https?:\\\\/\\\\/mp\\\\.weixin\\\\.qq\\\\.com\\\\/s', + include: ['.rich_media_wrp', '.js_inner', '#js_common_share_desc'], + exclude: [ + '.weapp_text_link', + 'mp-miniprogram', + '.weapp_display_element', + 'mpprofile', + '#js_profile_qrcode_img', + '#js_weapp_without_auth_weappurl', + '.rich_media_title', + '.js_video_play_controll', + '.wx_video_play_opr', + '.video_opr_sns', + '.js_progress_bar', + '.video_poster', + ], + }, + { + url: '^https?:\\\\/\\\\/blog\\\\.csdn\\\\.net', + include: ['.blog-content-box', '.hljs-comment'], + exclude: [ + '.dp-highlighter.bg_cpp .bar', + '.article-info-box', + '#blogColumnPayAdvert', + '#blogExtensionBox', + '#blogVoteBox', + '#treeSkill', + ], + }, + { + url: '^https?:\\\\/\\\\/blog\\\\.csdn\\\\.net', + include: ['.blog-content-box', '.hljs-comment'], + exclude: ['.dp-highlighter.bg_cpp .bar'], + }, + { url: '^https?:\\\\/\\\\/new\\\\.qq\\\\.com', include: ['.content-article'], exclude: ['.videoPlayerWrap'] }, + { url: '^https?:\\\\/\\\\/applet-data\\\\.web\\\\.bytedance\\\\.net', include: ['.app-main'], exclude: [] }, + { url: '^https?:\\\\/\\\\/blog\\\\.whatsapp\\\\.com', include: ['._9t2d'], exclude: [] }, + { + url: '^https?:\\\\/\\\\/gist\\\\.github\\\\.com', + include: ['.js-gist-file-update-container'], + exclude: ['.px-0'], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.lennysnewsletter\\\\.com\\\\/p', + include: ['.single-post-container', '.header-with-anchor-widget', 'h1', 'h2', 'h3', 'h4'], + exclude: [ + '.meta-subheader', + '.share-dialog-title', + '.social-preview-box', + '.share-action-row', + '.paywall', + '.post-footer', + '.comments-section', + '.single-post-section', + '.subscribe-footer', + '.sideBySideWrap', + '.tw-select-none', + '.post-subheader', + '.post-contributor-footer', + ], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.larksuite\\\\.com\\\\/hc\\\\/', + include: ['#js-hc-breadcrumb ~ div'], + exclude: ['.image-wrapper-sizer'], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.feishu\\\\.cn\\\\/hc\\\\/', + include: ['#js-hc-breadcrumb ~ div'], + exclude: ['.image-wrapper-sizer'], + }, + { + url: '^https?:\\\\/\\\\/baijiahao\\\\.baidu\\\\.com\\\\/s', + include: ['#app > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(1) > div:nth-child(1)'], + exclude: ['div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2)', '#commentModule'], + }, + { + url: '^https?:\\\\/\\\\/mbd\\\\.baidu\\\\.com\\\\/newspage\\\\/data\\\\/landingsuper?', + include: ['#app > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(1) > div:nth-child(1)'], + exclude: ['div > div:nth-child(1) > div:nth-child(1) > div:nth-child(2)', '#commentModule'], + }, + { + url: '^https?:\\\\/\\\\/www\\\\.cnbc\\\\.com\\\\/\\\\d+\\\\/', + include: ['.PageBuilder-pageWrapper'], + exclude: [ + '.ArticleHeader-headerContentContainer', + '.WatchLiveRightRail-rightRail', + '#taboolaContainerMix', + '.WatchLiveRightRail-inline', + '.RegularArticle-ArticleHeader-2', + '.JumpLink-container', + '.CNBCGlobalNav-container', + '.nav-menu-navLinks', + '.RelatedContent-relatedContent', + '.InlineVideo-videoFooter', + '.PlayButton-featured', + '.SocialShare-socialShare', + '.LiveBlogHeader-timestampAndShareBarContainer', + ], + }, + { + url: '^https?:\\\\/\\\\/nypost\\\\.com\\\\/\\\\d+\\\\/', + include: ['.single--article'], + exclude: [ + '.sharedaddy', + '.author-flyout__inner', + '.modal__email-author', + '.zergnet-widget', + '.circular-widget', + '.single__sidebar', + '.single__footer', + '.vjs-control-bar', + '.inline-module__inner', + '.membership-reactions-module', + ], + }, + { + url: '^https?:\\\\/\\\\/time\\\\.geekbang\\\\.org\\\\/column\\\\/article\\\\/', + include: ['.Index_contentWidth_3_1Sf'], + exclude: [ + '.recommend-read', + '.Index_comment_1Pwfq', + '.Index_comments_3HIaO', + '.noteDialog_noteDialog_2w-W2', + '.ArticleBottomBuyTipPC_unavailable_Cb50J', + '.noteMenu_noteMenu_zbKHG', + '.GkPlayer_controlWrap_g2M_1', + '.AudioPlayerPC_left_1jNLt', + '.ArticleLikeModulePc_LikeWrapper_HqHD5', + '.AudioPlayerPC_icon_JtBkd', + ], + }, + { + url: '^https?:\\\\/\\\\/dariusforoux\\\\.com', + include: ['#content'], + exclude: [ + 'form', + '#comments', + '#paging', + '.entry-related', + '.formkit-overlay', + '.has-small-font-size', + '.entry-utils', + ], + }, + { + url: '^https?:\\\\/\\\\/cloud\\\\.google\\\\.com\\\\/blog\\\\/', + include: ['#jump-content', 'c-wiz'], + exclude: [ + 'article-sticky-share-block', + 'article-aspect-image-block', + 'article-cta', + 'related-article-tout-block-external', + 'related-article-tout-block', + 'article-tag-list-block', + 'article-author-block', + 'social-links', + '.nRhiJb-kR0ZEf-OWXEXe-GV1x9e-ibL1re', + '.nRhiJb-DbgRPb-c5RTEf-ma6Yeb', + '.glue-modal', + ], + }, + { url: '^https?:\\\\/\\\\/medium\\\\.com\\\\/$', include: ['article'], exclude: [] }, + { + url: '^https?:\\\\/\\\\/medium\\\\.com\\\\/', + include: ['section', 'main'], + exclude: ['header', 'footer', 'button'], + }, + { url: '^https?:\\\\/\\\\/([a-zA-Z0-9]+\\\\.)medium\\\\.com\\\\/([^about])', include: ['section'], exclude: [] }, + { url: '^https?:\\\\/\\\\/([a-zA-Z0-9]+\\\\.)medium\\\\.com\\\\/about', include: ['main'], exclude: [] }, + { url: '^https?:\\\\/\\\\/([a-zA-Z0-9]+\\\\.)medium\\\\.com\\\\/$', include: ['main'], exclude: [] }, + { + url: '^https?:\\\\/\\\\/www\\\\.wogoo\\\\.com\\\\/', + include: ['.leftSide'], + exclude: ['.articleComment', '.zan'], + }, + { url: '^https?:\\\\/\\\\/www\\\\.iocoder\\\\.cn\\\\/', include: ['#main'], exclude: ['.article-nav', 'footer'] }, + { + url: '^https?:\\\\/\\\\/help\\\\.solidworks\\\\.com\\\\/', + include: ['#DSMainContent'], + exclude: ['#DSLeftPane', '#auto_links'], + }, + { url: '^https?:\\\\/\\\\/effecthouse\\\\.tiktok\\\\.com\\\\/', include: ['section', 'article'], exclude: [] }, +]; diff --git a/src/core/parseDom/plugin/base.ts b/src/core/parseDom/plugin/base.ts new file mode 100644 index 00000000..a20e3428 --- /dev/null +++ b/src/core/parseDom/plugin/base.ts @@ -0,0 +1,3 @@ +export abstract class BasePlugin { + public abstract parse(cloneDom: Element): Promise | void; +} diff --git a/src/core/parseDom/plugin/canvas.ts b/src/core/parseDom/plugin/canvas.ts new file mode 100644 index 00000000..3e3f6eb3 --- /dev/null +++ b/src/core/parseDom/plugin/canvas.ts @@ -0,0 +1,11 @@ +import { BasePlugin } from './base'; + +export class CanvasParsePlugin extends BasePlugin { + public parse(cloneDom: HTMLElement): Promise | void { + for (const canvasMap of cloneDom.querySelectorAll('canvas')) { + const imageElement = document.createElement('img'); + imageElement.src = (canvasMap as HTMLCanvasElement).toDataURL(); + canvasMap.parentNode?.replaceChild(imageElement, canvasMap); + } + } +} diff --git a/src/core/parseDom/plugin/code.ts b/src/core/parseDom/plugin/code.ts new file mode 100644 index 00000000..0a7d65ea --- /dev/null +++ b/src/core/parseDom/plugin/code.ts @@ -0,0 +1,37 @@ +import { BasePlugin } from './base'; + +export class CodeParsePlugin extends BasePlugin { + /** + * 查询所有 pre 节点 + * 并将 pre 节点下所有的 code 节点融合成一个新的 code 节点 + *
+   *  1
+   *  
    2
+ *
+ * 转化后 + *
+   *  
+   *    
1
+ *
2
+ *
+ *
+ */ + public parse(cloneDom: HTMLElement): Promise | void { + const preElements = cloneDom.querySelectorAll('pre'); + preElements.forEach(pre => { + // 查询所有的代码块 + const codeElementArray = pre.querySelectorAll('code'); + const code = document.createElement('code'); + for (const codeElement of codeElementArray) { + Array.from(codeElement.childNodes).forEach(item => { + code.appendChild(item); + }); + } + Array.from(pre.childNodes).forEach(item => { + pre.removeChild(item); + }); + pre.appendChild(code); + console.log(pre); + }); + } +} diff --git a/src/core/parseDom/plugin/hexoCode.ts b/src/core/parseDom/plugin/hexoCode.ts new file mode 100644 index 00000000..8dbf07eb --- /dev/null +++ b/src/core/parseDom/plugin/hexoCode.ts @@ -0,0 +1,28 @@ +import { BasePlugin } from './base'; + +export class HexoCodeParsePlugin extends BasePlugin { + public parse(cloneDom: HTMLElement): Promise | void { + const figures = cloneDom.querySelectorAll('figure'); + const processingCodeBlock = (node: HTMLElement) => { + const gutter = node.querySelector('td.gutter'); + const code = node.querySelector('td.code'); + if (!gutter || !code) { + return; + } + const codeElement = code.querySelector('pre'); + if (codeElement) { + node.parentNode?.appendChild(codeElement); + } + node.parentNode?.removeChild(node); + }; + figures.forEach(figure => { + processingCodeBlock(figure); + }); + if (figures.length === 0) { + const tables = cloneDom.querySelectorAll('table'); + tables.forEach(table => { + processingCodeBlock(table); + }); + } + } +} diff --git a/src/core/parseDom/plugin/image.ts b/src/core/parseDom/plugin/image.ts new file mode 100644 index 00000000..da20b21c --- /dev/null +++ b/src/core/parseDom/plugin/image.ts @@ -0,0 +1,11 @@ +import { BasePlugin } from './base'; + +export class ImageParsePlugin extends BasePlugin { + public parse(cloneDom: HTMLElement): Promise | void { + const images = cloneDom.querySelectorAll('img'); + images.forEach(image => { + // 有些 img 采用 srcset 属性去实现,src 中放的其实是小图,所以以 currentSrc 作为渲染的 src + image.setAttribute('src', image.currentSrc || image.src); + }); + } +} diff --git a/src/core/parseDom/plugin/index.tsx b/src/core/parseDom/plugin/index.tsx new file mode 100644 index 00000000..925c6552 --- /dev/null +++ b/src/core/parseDom/plugin/index.tsx @@ -0,0 +1,5 @@ +export * from './link'; +export * from './image'; +export * from './code'; +export * from './hexoCode'; +export * from './canvas'; diff --git a/src/core/parseDom/plugin/link.ts b/src/core/parseDom/plugin/link.ts new file mode 100644 index 00000000..90a556d6 --- /dev/null +++ b/src/core/parseDom/plugin/link.ts @@ -0,0 +1,10 @@ +import { BasePlugin } from './base'; + +export class LinkParsePlugin extends BasePlugin { + public parse(cloneDom: HTMLElement): Promise | void { + const link = cloneDom.querySelectorAll('a'); + link.forEach(item => { + item.setAttribute('href', item.href); + }); + } +} diff --git a/src/core/transform-dom.ts b/src/core/transform-dom.ts deleted file mode 100644 index 57078223..00000000 --- a/src/core/transform-dom.ts +++ /dev/null @@ -1,357 +0,0 @@ -import Chrome from '@/core/chrome'; -import { screenShot } from './screen-shot'; - -function hexoCodeBlock(cloneNode: Element) { - const figures = cloneNode.querySelectorAll('figure'); - const processingCodeBlock = (node: HTMLElement) => { - const gutter = node.querySelector('td.gutter'); - const code = node.querySelector('td.code'); - if (!gutter || !code) { - return; - } - const codeElement = code.querySelector('pre'); - if (codeElement) { - node.parentNode?.appendChild(codeElement); - } - node.parentNode?.removeChild(node); - }; - figures.forEach(figure => { - processingCodeBlock(figure); - }); - if (figures.length === 0) { - const tables = cloneNode.querySelectorAll('table'); - tables.forEach(table => { - processingCodeBlock(table); - }); - } -} - -function commonCodeBlock(node: Element) { - const preElements = node.querySelectorAll('pre'); - /** - * 查询所有 pre 节点 - * 并将 pre 节点下所有的 code 节点融合成一个新的 code 节点 - *
-   *  1
-   *  
    2
- *
- * 转化后 - *
-   *  
-   *    
1
- *
2
- *
- *
- */ - preElements.forEach(pre => { - const codeElementArray = pre.querySelectorAll('code'); - const cleanCode: ChildNode[] = []; - for (const codeElement of codeElementArray) { - if (codeElement) { - const childNodes = pre.childNodes; - const needRemoveNodes: ChildNode[] = []; - const needMergeNodes: ChildNode[] = []; - childNodes.forEach(item => { - if ((item as Element)?.tagName === 'CODE' && item !== codeElement) { - needMergeNodes.push(item); - } - if (item !== codeElement) { - needRemoveNodes.push(item); - } - }); - const div = document.createElement('div'); - codeElement.childNodes.forEach(item => { - div.appendChild(item); - }); - cleanCode.push(div); - } - // 移除掉所有的子节点 - pre.childNodes.forEach(item => { - pre.removeChild(item); - }); - const code = document.createElement('code'); - cleanCode.forEach(item => code.appendChild(item)); - pre.childNodes.forEach(item => { - pre.removeChild(item); - }); - pre.appendChild(code); - } - }); -} - -function transformHTML(html: string): string { - // 清洗掉 span 标签之间的空格标签 - return html.replace(/<\/span> +  { - const id = (item as Element).id; - if (id) { - ids.push(id); - } else { - const childIds = findYuqueChildId(item as Element); - ids = ids.concat(childIds); - } - }); - - return ids; -} - -function isYuqueContent(element: Element) { - if ( - element.closest('.ne-viewer-body') || - document.querySelector('.ne-viewer-body') - ) { - return true; - } - return false; -} - -async function transformYuqueContent(element: Element) { - return new Promise(async (resolve, rejected) => { - const onMessage = (e: MessageEvent) => { - if (e.data?.key !== 'tarnsfromYuqueContentValue') { - return; - } - window.removeEventListener('message', onMessage); - const result = e.data?.data?.result; - if (!result || !result?.length) { - transformError('result is empty'); - } - const title = element.querySelector('#article-title')?.outerHTML; - resolve(`${title || ''}${e.data?.data?.result?.join('\n')}`); - }; - - // 监听消息 - window.addEventListener('message', onMessage); - - const transformError = (params: any) => { - window.removeEventListener('message', onMessage); - rejected(params); - }; - - setTimeout(() => { - transformError('transform timeout'); - }, 3000); - - await new Promise(resolve1 => { - let script = document.querySelector( - '#yuque-content-transform-script', - ) as HTMLScriptElement; - if (script) { - return resolve1(true); - } - script = document.createElement('script') as HTMLScriptElement; - const file = Chrome.runtime.getURL('yuque-transform-script.js'); - script.id = 'yuque-content-transform-script'; - script.setAttribute('src', file); - document.body.append(script); - script.onload = () => { - resolve1(true); - }; - }); - - try { - let ids: string[] = []; - if (element.classList.contains('ne-viewer-body')) { - const childIds = findYuqueChildId(element); - ids = ids.concat(childIds); - } else if (element.closest('.ne-viewer-body')) { - const id = findYuqueNeTag(element)?.id; - if (id) { - ids.push(id); - } - } else if (element.querySelector('.ne-viewer-body')) { - const childIds = findYuqueChildId( - element.querySelector('.ne-viewer-body'), - ); - ids = ids.concat(childIds); - } - - window.postMessage( - { - key: 'tarnsfromYuqueContent', - data: { ids }, - }, - '*', - ); - } catch (error) { - transformError(error); - } - }); -} - -interface IOriginAndCloneDomItem { - origin: Element; - clone: Element; -} - -function generateOriginAndCloneDomArray( - cloneElement: Element, - originElement: Element, - name: keyof HTMLElementTagNameMap, -): Array { - const originDoms = originElement.querySelectorAll(name); - const cloneDoms = cloneElement.querySelectorAll(name); - const result: Array = []; - if (originDoms.length < cloneDoms.length) { - for (let i = 0; i < cloneDoms.length; i++) { - const cloneDom = cloneDoms[i]; - const originDom = i === 0 ? originElement : originDoms[i - 1]; - result.push({ - origin: originDom, - clone: cloneDom, - }); - } - } else { - originDoms.forEach((originDom, index) => { - result.push({ - origin: originDom, - clone: cloneDoms[index], - }); - }); - } - return result; -} - -async function transformVideoToImage(element: Element, originDom: Element) { - const videoMapArray = generateOriginAndCloneDomArray( - element, - originDom, - 'video', - ); - - for (const videoMap of videoMapArray) { - const rect = videoMap.origin.getBoundingClientRect(); - const canvas = await screenShot({ - x: rect.x, - y: rect.top, - width: rect.width, - height: rect.height, - }); - - await new Promise(resolve => { - const image = document.createElement('img'); - image.src = canvas.toDataURL('image/jpeg'); - videoMap.clone.parentNode?.replaceChild(image, videoMap.clone); - - resolve(true); - }); - } -} - -function transformCanvasToImage(element: Element, originDom: Element) { - const canvasMapArray = generateOriginAndCloneDomArray( - element, - originDom, - 'canvas', - ); - - for (const canvasMap of canvasMapArray) { - const imageElement = document.createElement('img'); - imageElement.src = (canvasMap.origin as HTMLCanvasElement).toDataURL(); - canvasMap.clone.parentNode?.replaceChild(imageElement, canvasMap.clone); - } -} - -export async function transformDOM(domArray: Element[]) { - const yuqueDOMIndex: number[] = []; - - const clonedDOMArray: Element[] = []; - - for (const dom of domArray) { - if (isYuqueContent(dom)) { - clonedDOMArray.push(dom); - continue; - } - const cloneDom = dom.cloneNode(true) as Element; - const div = document.createElement('div'); - if (cloneDom.tagName === 'CODE') { - const pre = document.createElement('pre'); - pre.appendChild(cloneDom); - div.appendChild(pre); - } else { - div.appendChild(cloneDom); - } - clonedDOMArray.push(div); - } - - for ( - let clonedDOMIndex = 0; - clonedDOMIndex < clonedDOMArray.length; - clonedDOMIndex++ - ) { - let clonedDOM = clonedDOMArray[clonedDOMIndex]; - - if (isYuqueContent(clonedDOM)) { - try { - clonedDOMArray[clonedDOMIndex] = (await transformYuqueContent( - clonedDOM, - )) as Element; - yuqueDOMIndex.push(clonedDOMIndex); - continue; - } catch (error) { - // 解析失败兜底走默认处理 - const div = document.createElement('div'); - div.appendChild(clonedDOM.cloneNode(true)); - clonedDOM = div; - } - } - - const originDom = domArray[clonedDOMIndex]; - - // 替换 a 标签的链接 - const linkElements = clonedDOM.querySelectorAll('a'); - linkElements.forEach(a => { - a.setAttribute('href', a.href); - }); - - // 替换 img 标签的链接 - const imgElements = clonedDOM.querySelectorAll('img'); - imgElements.forEach(img => { - // 有些 img 采用 srcset 属性去实现,src 中放的其实是小图,所以以 currentSrc 作为渲染的 src - img.setAttribute('src', img.currentSrc || img.src); - }); - - // 移除 pre code 下的兄弟 - commonCodeBlock(clonedDOM); - - // 处理 hexo 代码 - hexoCodeBlock(clonedDOM); - - // 将 video 截屏转为 img - await transformVideoToImage(clonedDOM, originDom); - - // 转化canvas为img - transformCanvasToImage(clonedDOM, originDom); - } - - return clonedDOMArray.map((item, index) => { - if (yuqueDOMIndex.includes(index)) { - return item; - } - return transformHTML(item.innerHTML); - }); -} diff --git a/src/core/uitl.ts b/src/core/uitl.ts index f86f595f..3fbfbcd6 100644 --- a/src/core/uitl.ts +++ b/src/core/uitl.ts @@ -11,5 +11,3 @@ export function findCookieSettingPage() { } return ''; } -export const isRunningInjectPage = - typeof window !== 'undefined' && typeof window; diff --git a/src/injectscript/README.md b/src/injectscript/README.md new file mode 100644 index 00000000..8efcf3c5 --- /dev/null +++ b/src/injectscript/README.md @@ -0,0 +1 @@ +## 通过 script 标签注入到宿主页面的脚本 diff --git a/src/injectscript/index.ts b/src/injectscript/index.ts new file mode 100644 index 00000000..472f6a69 --- /dev/null +++ b/src/injectscript/index.ts @@ -0,0 +1,48 @@ +import { InjectScriptRequestKey, MessageEventRequestData } from '../isomorphic/injectScript'; +import { YuqueService } from './service'; +import { BaseService } from './service/base'; + +class InjectScriptApp { + private serviceMap: { [key: string]: BaseService } = {}; + + constructor() { + this.init(); + } + + init() { + this.registerService(new YuqueService()); + window.addEventListener('message', async (e: MessageEvent) => { + if (e.data.key !== InjectScriptRequestKey) { + return; + } + const { serviceMethod, serviceName, params } = e?.data?.data; + const service = this.serviceMap[serviceName]; + try { + const fn = (service as any)[serviceMethod]; + if (typeof fn !== 'function') { + console.log(`inject script ${serviceName}.${serviceMethod} is not a function`, fn); + return; + } + const result = await fn(params || {}); + service.callbackResponse(result, e.data.requestId); + } catch (e: any) { + console.log(`execute ${serviceName}.${serviceMethod} error`); + service.callbackResponse({ error: e?.message }, e.data.requestId); + } + }); + } + + private registerService(service: BaseService) { + if (this.serviceMap[service.name]) { + console.log(`inject script service map has been register, service name is ${service.name}`); + return; + } + if (!service.enableRegister()) { + return; + } + console.log(`inject script register service success, service name is ${service.name}`); + this.serviceMap[service.name] = service; + } +} + +new InjectScriptApp(); diff --git a/src/injectscript/service/base.ts b/src/injectscript/service/base.ts new file mode 100644 index 00000000..feb0723c --- /dev/null +++ b/src/injectscript/service/base.ts @@ -0,0 +1,22 @@ +import { InjectScriptResponseKey } from '@/isomorphic/injectScript'; + +export abstract class BaseService { + public abstract name: string; + public abstract urlRegExp: string; + + public enableRegister() { + const regExp = new RegExp(this.urlRegExp); + return regExp.test(window.location.href); + } + + public callbackResponse(result: any, requestId: string) { + window.postMessage( + { + key: InjectScriptResponseKey, + requestId, + data: { result }, + }, + '*', + ); + } +} diff --git a/src/injectscript/service/index.ts b/src/injectscript/service/index.ts new file mode 100644 index 00000000..3c4d65ef --- /dev/null +++ b/src/injectscript/service/index.ts @@ -0,0 +1 @@ +export * from './yuque'; diff --git a/src/injectscript/service/yuque.ts b/src/injectscript/service/yuque.ts new file mode 100644 index 00000000..0f90dbbe --- /dev/null +++ b/src/injectscript/service/yuque.ts @@ -0,0 +1,23 @@ +import { BaseService } from './base'; + +export class YuqueService extends BaseService { + public name = 'YuqueService'; + public urlRegExp = '^https://.*yuque.*com/'; + + parseContent(params: { ids: string[] }) { + const { ids } = params; + const editor = document.querySelector('.ne-viewer-body'); + const result: string[] = []; + for (const id of ids) { + try { + const html = (editor as any)?._neRef?.document.viewer.execCommand('getNodeContent', id, 'text/html'); + if (html) { + result.push(html); + } + } catch (error) { + // + } + } + return result; + } +} diff --git a/src/isomorphic/background/clip.ts b/src/isomorphic/background/clip.ts index d1085941..126e1af6 100644 --- a/src/isomorphic/background/clip.ts +++ b/src/isomorphic/background/clip.ts @@ -1,9 +1,10 @@ export enum OperateClipEnum { selectArea = 'selectArea', screenOcr = 'screenOcr', + clipPage = 'clipPage', } export interface IOperateClipData { type: OperateClipEnum; - isRunningInjectPage: boolean; + isRunningHostPage: boolean; } diff --git a/src/isomorphic/env.ts b/src/isomorphic/env.ts index 0f444919..6bb61a91 100644 --- a/src/isomorphic/env.ts +++ b/src/isomorphic/env.ts @@ -17,6 +17,19 @@ class Env { } return true; } + + // 判断是否是 beta 版插件,所有非商店的插件都判断为 beta 版本 + static get isBate() { + if (chrome.runtime.getManifest().name.includes('Beta')) { + return true; + } + return false; + } + + // 是否运行在宿主页面,供 sidePanel 类 iframe 使用 + static get isRunningHostPage() { + return window.self !== window.top; + } } export default Env; diff --git a/src/isomorphic/event/clipAssistant.ts b/src/isomorphic/event/clipAssistant.ts index faf1d53d..2728da73 100644 --- a/src/isomorphic/event/clipAssistant.ts +++ b/src/isomorphic/event/clipAssistant.ts @@ -15,7 +15,5 @@ export enum ClipAssistantMessageActions { */ ready = 'ready', addContent = 'addContent', - startSelectArea = 'startSelectArea', startScreenOcr = 'startScreenOcr', - startCollectLink = 'startCollectLink', } diff --git a/src/isomorphic/event/contentScript.ts b/src/isomorphic/event/contentScript.ts index 89a08032..012cedc1 100644 --- a/src/isomorphic/event/contentScript.ts +++ b/src/isomorphic/event/contentScript.ts @@ -1,11 +1,6 @@ export enum ContentScriptEvents { - ScreenOcr = 'contentScript/screenOcr', - SelectArea = 'contentScript/selectArea', - CollectLink = 'contentScript/collectLink', - ToggleSidePanel = 'contentScript/toggleSidePanel', WordMarkConfigChange = 'contentScript/wordMarkConfigChange', LevitateConfigChange = 'contentScript/levitateConfigChange', - AddContentToClipAssistant = 'contentScript/addContentToClipAssistant', ForceUpgradeVersion = 'contentScript/forceUpgradeVersion', LoginOut = 'contentScript/LoginOut', GetDocument = 'contentScript/getDocument', diff --git a/src/isomorphic/injectScript.ts b/src/isomorphic/injectScript.ts new file mode 100644 index 00000000..64f04f12 --- /dev/null +++ b/src/isomorphic/injectScript.ts @@ -0,0 +1,21 @@ +export const InjectScriptRequestKey = 'inject-script-request-key'; +export const InjectScriptResponseKey = 'inject-script-response-key'; + +export interface MessageEventRequestData { + key: string; + requestId: string; + data: { + serviceName: string; + serviceMethod: string; + params?: { [key: string]: any }; + }; +} + +export interface MessageEventResponseData { + key: string; + requestId: string; + data: { + result: any; + error?: any; + }; +} diff --git a/src/isomorphic/interface.ts b/src/isomorphic/interface.ts index 0f13ff7f..6ab8037d 100644 --- a/src/isomorphic/interface.ts +++ b/src/isomorphic/interface.ts @@ -7,7 +7,7 @@ export interface IUser { } export interface IShortcutMap { - collectLink?: string; + clipPage?: string; openSidePanel?: string; selectArea?: string; startOcr?: string; diff --git a/src/isomorphic/link-helper.ts b/src/isomorphic/link-helper.ts index ab47a3a3..6305356c 100644 --- a/src/isomorphic/link-helper.ts +++ b/src/isomorphic/link-helper.ts @@ -1,22 +1,24 @@ -import { YUQUE_DOMAIN } from '@/config'; +import { YUQUE_DOMAIN, pkg } from '@/config'; +import Env from './env'; const LinkHelper = { goDoc: (doc: { id: number }) => `${YUQUE_DOMAIN}/go/doc/${doc.id}`, goMyNote: () => `${YUQUE_DOMAIN}/dashboard/notes`, goMyPage: (account: { login: string }) => `${YUQUE_DOMAIN}/${account.login}`, + feedback: () => { + return `https://www.yuque.com/feedbacks/new?body=系统信息:浏览器插件/${Env.isBate ? 'Beta版' : '正式版'}/${ + pkg.version + },请详细描述你的问题:&label_ids=20031`; + }, dashboard: `${YUQUE_DOMAIN}/dashboard`, unInstallFeedback: 'https://www.yuque.com/forms/share/c454d5b2-8b2f-4a73-851b-0f3d2ae585fb?chromeExtensionUninstall=true', - introduceExtension: - 'https://www.yuque.com/yuque/yuque-browser-extension/welcome#acYWK', + introduceExtension: 'https://www.yuque.com/yuque/yuque-browser-extension/welcome#acYWK', ocrProxyPage: 'https://www.yuque.com/r/ocr_proxy_page', - updateIframe: - 'https://www.yuque.com/yuque/yuque-browser-extension/install?view=doc_embed', + updateIframe: 'https://www.yuque.com/yuque/yuque-browser-extension/install?view=doc_embed', changelog: 'https://www.yuque.com/yuque/yuque-browser-extension/changelog', helpDoc: 'https://www.yuque.com/yuque/yuque-browser-extension/welcome', - feedback: 'https://www.yuque.com/feedbacks/new', - joinGroup: - 'https://www.yuque.com/yuque/yuque-browser-extension/welcome#BQrrd', + joinGroup: 'https://www.yuque.com/yuque/yuque-browser-extension/welcome#BQrrd', settingPage: chrome.runtime.getURL('setting.html'), wordMarkSettingPage: `${chrome.runtime.getURL('setting.html')}#wordMark`, shortcutSettingPage: `${chrome.runtime.getURL('setting.html')}#shortcut`, diff --git a/src/isomorphic/util.ts b/src/isomorphic/util.ts index eccbd32a..9d3c0bf3 100644 --- a/src/isomorphic/util.ts +++ b/src/isomorphic/util.ts @@ -1,3 +1,5 @@ +import { v4 as uuidv4 } from 'uuid'; + const rChineseChar = /\p{Script=Han}+/gu; const blockquoteID = 'yqextensionblockquoteid'; @@ -93,3 +95,9 @@ export async function transformUrlToFile(data: string | File) { } return file; } + +export function getMsgId(options: { type: string; id?: number; timestamp?: number }) { + const { type, timestamp, id = Date.now() } = options; + const uuid = timestamp || uuidv4().replace(/-/g, '').substring(0, 8); + return `${type}_${id}_${uuid}`; +} diff --git a/src/manifest.json b/src/manifest.json index af82aeac..8ff1b51e 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -38,7 +38,7 @@ "lake-editor-icon.js", "react.production.min.js", "react-dom.production.min.js", - "yuque-transform-script.js" + "inject-content-script.js" ], "matches": [ "", @@ -67,7 +67,7 @@ "sidePanel" ], "side_panel": { - "default_path": "sidePanel.html" + "default_path": "sidePanel.html" }, "commands": { "_execute_action": { @@ -82,8 +82,8 @@ "startOcr": { "description": "OCR 提取" }, - "collectLink": { - "description": "收藏链接" + "clipPage": { + "description": "全文剪藏" } } } diff --git a/src/pages/inject/AreaSelector/app.tsx b/src/pages/inject/AreaSelector/app.tsx index 01b79005..0774ef3c 100644 --- a/src/pages/inject/AreaSelector/app.tsx +++ b/src/pages/inject/AreaSelector/app.tsx @@ -2,10 +2,10 @@ import React, { useEffect, useRef, useCallback, useState } from 'react'; import classnames from 'classnames'; import { InfoCircleOutlined } from '@ant-design/icons'; import { useForceUpdate } from '@/hooks/useForceUpdate'; -import { transformDOM } from '@/core/transform-dom'; +import { useEnterShortcut } from '@/hooks/useEnterShortCut'; +import { parseDom } from '@/core/parseDom'; import { __i18n } from '@/isomorphic/i18n'; import styles from './app.module.less'; -import { useEnterShortcut } from '@/hooks/useEnterShortCut'; type Rect = Pick; @@ -26,9 +26,8 @@ function App(props: IAppProps) { const onSave = useCallback(async () => { setSaving(true); const selections = targetListRef.current.filter(item => item) || []; - const selectAreaElements = await transformDOM(selections); - const HTMLs = Array.from(selectAreaElements); - props.onSelectAreaSuccess(HTMLs.join('')); + const selectAreaElements = await parseDom.parseDom(selections); + props.onSelectAreaSuccess(selectAreaElements.join('')); }, []); useEnterShortcut({ diff --git a/src/pages/inject/LevitateBall/app.tsx b/src/pages/inject/LevitateBall/app.tsx deleted file mode 100644 index d0bf66e3..00000000 --- a/src/pages/inject/LevitateBall/app.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import React, { useState, useEffect, useRef } from 'react'; -import Icon from '@ant-design/icons'; -import classnames from 'classnames'; -import { Button, Radio } from 'antd'; -import CloseCircle from '@/assets/svg/close-circle.svg'; -import YuqueLogoSve from '@/assets/svg/yuque-logo.svg'; -import { backgroundBridge } from '@/core/bridge/background'; -import { useForceUpdate } from '@/hooks/useForceUpdate'; -import { ILevitateConfig } from '@/isomorphic/constant/levitate'; -import { - LevitateBallMessageActions, - LevitateBallMessageKey, -} from '@/isomorphic/event/levitateBall'; -import LinkHelper from '@/isomorphic/link-helper'; -import { useInjectContent } from '../components/InjectLayout'; -import styles from './app.module.less'; - -type DisableType = 'disableUrl' | 'disableOnce' | 'disableForever'; - -const url = `${window.location.origin}${window.location.pathname}`; - -function App() { - const [config, setConfig] = useState({} as ILevitateConfig); - const [isHover, setIsHover] = useState(false); - const [shortKey, setShortKey] = useState(''); - const [dragging, setDragging] = useState(false); - const { forceUpdate } = useForceUpdate(); - const entryStartActionRef = useRef<'click' | 'drag' | ''>(''); - const entryStartActionStratYRef = useRef(0); - const positionRef = useRef({ - top: 0, - }); - const { Modal } = useInjectContent(); - - const handleDrag = (event: React.DragEvent) => { - if (!dragging || !event.clientY) return; - positionRef.current.top = event.clientY; - forceUpdate(); - }; - - const disableLevitateBall = (type: DisableType) => { - switch (type) { - case 'disableOnce': { - break; - } - case 'disableUrl': { - const iconLink = - document.querySelector("link[rel*='icon']") || - document.querySelector("link[rel*='Icon']"); - const iconUrl = (iconLink as HTMLLinkElement)?.href; - backgroundBridge.configManager.update('levitate', 'disableUrl', [ - ...config.disableUrl, - { - origin: url, - icon: iconUrl, - }, - ]); - break; - } - case 'disableForever': { - backgroundBridge.configManager.update('levitate', 'enable', false, { - notice: true, - }); - break; - } - default: - break; - } - window._yuque_ext_app.removeLevitateBall(); - }; - - const onCloseLevitateBall = () => { - let disableType: DisableType = 'disableOnce'; - const modal = Modal.confirm({}); - modal.update({ - content: ( - <> - { - disableType = e.target.value; - }} - > - - {__i18n('在本次访问关闭')} - - {__i18n('在本页关闭')} - {__i18n('全部关闭')} - -
- {__i18n('可在')} -
{ - backgroundBridge.tab.create(LinkHelper.sidePanelSettingPage); - }} - > - {__i18n('设置')} -
- {__i18n('中开启')} -
- - ), - prefixCls: 'yuque-chrome-extension', - closable: true, - title: __i18n('关闭悬浮气泡'), - centered: true, - wrapClassName: styles.disableModal, - maskClosable: true, - footer: ( - <> -
- - -
- - ), - }); - }; - - const handleDragEnd = (event: React.DragEvent) => { - positionRef.current.top = event.clientY; - setDragging(false); - entryStartActionRef.current = ''; - backgroundBridge.configManager.update( - 'levitate', - 'position', - positionRef.current.top, - ); - }; - - useEffect(() => { - backgroundBridge.user.getUserShortCut().then(res => { - setShortKey(res.openSidePanel || ''); - }); - backgroundBridge.configManager - .get('levitate') - .then((res: ILevitateConfig) => { - setConfig(res); - positionRef.current.top = parseInt(res.position); - }); - }, []); - - useEffect(() => { - const onMessage = (e: MessageEvent) => { - const { key, action, data } = e.data || {}; - if (key !== LevitateBallMessageKey) { - return; - } - switch (action) { - case LevitateBallMessageActions.levitateBallConfigUpdate: { - data && setConfig(data); - break; - } - default: - break; - } - }; - window.addEventListener('message', onMessage); - return () => { - window.removeEventListener('message', onMessage); - }; - }, []); - - if (!config.enable || config.disableUrl.find(item => item.origin === url)) { - return null; - } - - return ( - <> -
{ - setIsHover(false); - }} - > -
- -
-
) => { - entryStartActionStratYRef.current = e.clientY; - entryStartActionRef.current = 'click'; - }} - onMouseMove={(e: React.MouseEvent) => { - if (!entryStartActionRef.current) { - return; - } - const isClick = Math.abs(entryStartActionStratYRef.current - e.clientY) < 4; - if (isClick) { - return; - } - entryStartActionRef.current = 'drag'; - setDragging(true); - }} - onMouseUp={() => { - if (entryStartActionRef.current === 'click') { - window._yuque_ext_app.showSidePanel(); - } - entryStartActionRef.current = ''; - }} - onMouseEnter={() => { - setIsHover(true); - }} - > -
- -
{shortKey}
-
-
-
- {dragging && ( -
{ - setDragging(false); - entryStartActionRef.current = ''; - }} - /> - )} - - ); -} - -export default App; diff --git a/src/pages/inject/LevitateBall/app.module.less b/src/pages/inject/LevitateBall/index.module.less similarity index 100% rename from src/pages/inject/LevitateBall/app.module.less rename to src/pages/inject/LevitateBall/index.module.less diff --git a/src/pages/inject/LevitateBall/index.tsx b/src/pages/inject/LevitateBall/index.tsx index 14515abe..4e7c44f5 100644 --- a/src/pages/inject/LevitateBall/index.tsx +++ b/src/pages/inject/LevitateBall/index.tsx @@ -1,28 +1,247 @@ -import React from 'react'; -import { createRoot } from 'react-dom/client'; -import InjectLayout from '../components/InjectLayout'; -import LevitateBallApp from './app'; +import React, { useState, useEffect, useRef } from 'react'; +import Icon from '@ant-design/icons'; +import classnames from 'classnames'; +import { Button, Radio } from 'antd'; +import CloseCircle from '@/assets/svg/close-circle.svg'; +import YuqueLogoSve from '@/assets/svg/yuque-logo.svg'; +import { backgroundBridge } from '@/core/bridge/background'; +import { useForceUpdate } from '@/hooks/useForceUpdate'; +import { ILevitateConfig } from '@/isomorphic/constant/levitate'; +import { + LevitateBallMessageActions, + LevitateBallMessageKey, +} from '@/isomorphic/event/levitateBall'; +import LinkHelper from '@/isomorphic/link-helper'; +import { useInjectContent } from '../components/InjectLayout'; +import styles from './index.module.less'; -interface ICreateWordMarkOption { - dom: HTMLElement; -} +type DisableType = 'disableUrl' | 'disableOnce' | 'disableForever'; + +const url = `${window.location.origin}${window.location.pathname}`; function App() { - return ( - - - - ); -} + const [config, setConfig] = useState({} as ILevitateConfig); + const [isHover, setIsHover] = useState(false); + const [shortKey, setShortKey] = useState(''); + const [dragging, setDragging] = useState(false); + const { forceUpdate } = useForceUpdate(); + const entryStartActionRef = useRef<'click' | 'drag' | ''>(''); + const entryStartActionStratYRef = useRef(0); + const positionRef = useRef({ + top: 0, + }); + const { Modal } = useInjectContent(); -export function createLevitateBall(option: ICreateWordMarkOption) { - const div = document.createElement('div'); - const root = createRoot(div); - root.render(); - option.dom.appendChild(div); + const handleDrag = (event: React.DragEvent) => { + if (!dragging || !event.clientY) return; + positionRef.current.top = event.clientY; + forceUpdate(); + }; + + const disableLevitateBall = (type: DisableType) => { + switch (type) { + case 'disableOnce': { + break; + } + case 'disableUrl': { + const iconLink = + document.querySelector("link[rel*='icon']") || + document.querySelector("link[rel*='Icon']"); + const iconUrl = (iconLink as HTMLLinkElement)?.href; + backgroundBridge.configManager.update('levitate', 'disableUrl', [ + ...config.disableUrl, + { + origin: url, + icon: iconUrl, + }, + ]); + break; + } + case 'disableForever': { + backgroundBridge.configManager.update('levitate', 'enable', false, { + notice: true, + }); + break; + } + default: + break; + } + window._yuque_ext_app.removeLevitateBall(); + }; - return () => { - root.unmount(); - option.dom.removeChild(div); + const onCloseLevitateBall = () => { + let disableType: DisableType = 'disableOnce'; + const modal = Modal.confirm({}); + modal.update({ + content: ( + <> + { + disableType = e.target.value; + }} + > + + {__i18n('在本次访问关闭')} + + {__i18n('在本页关闭')} + {__i18n('全部关闭')} + +
+ {__i18n('可在')} +
{ + backgroundBridge.tab.create(LinkHelper.sidePanelSettingPage); + }} + > + {__i18n('设置')} +
+ {__i18n('中开启')} +
+ + ), + prefixCls: 'yuque-chrome-extension', + closable: true, + title: __i18n('关闭悬浮气泡'), + centered: true, + wrapClassName: styles.disableModal, + maskClosable: true, + footer: ( + <> +
+ + +
+ + ), + }); }; + + const handleDragEnd = (event: React.DragEvent) => { + positionRef.current.top = event.clientY; + setDragging(false); + entryStartActionRef.current = ''; + backgroundBridge.configManager.update( + 'levitate', + 'position', + positionRef.current.top, + ); + }; + + useEffect(() => { + backgroundBridge.user.getUserShortCut().then(res => { + setShortKey(res.openSidePanel || ''); + }); + backgroundBridge.configManager + .get('levitate') + .then((res: ILevitateConfig) => { + setConfig(res); + positionRef.current.top = parseInt(res.position); + }); + }, []); + + useEffect(() => { + const onMessage = (e: MessageEvent) => { + const { key, action, data } = e.data || {}; + if (key !== LevitateBallMessageKey) { + return; + } + switch (action) { + case LevitateBallMessageActions.levitateBallConfigUpdate: { + data && setConfig(data); + break; + } + default: + break; + } + }; + window.addEventListener('message', onMessage); + return () => { + window.removeEventListener('message', onMessage); + }; + }, []); + + if (!config.enable || config.disableUrl.find(item => item.origin === url)) { + return null; + } + + return ( + <> +
{ + setIsHover(false); + }} + > +
+ +
+
) => { + entryStartActionStratYRef.current = e.clientY; + entryStartActionRef.current = 'click'; + }} + onMouseMove={(e: React.MouseEvent) => { + if (!entryStartActionRef.current) { + return; + } + const isClick = Math.abs(entryStartActionStratYRef.current - e.clientY) < 4; + if (isClick) { + return; + } + entryStartActionRef.current = 'drag'; + setDragging(true); + }} + onMouseUp={() => { + if (entryStartActionRef.current === 'click') { + window._yuque_ext_app.toggleSidePanel(true); + } + entryStartActionRef.current = ''; + }} + onMouseEnter={() => { + setIsHover(true); + }} + > +
+ +
{shortKey}
+
+
+
+ {dragging && ( +
{ + setDragging(false); + entryStartActionRef.current = ''; + }} + /> + )} + + ); } + +export default App; diff --git a/src/pages/inject/action-listener.tsx b/src/pages/inject/action-listener.tsx index 017131f3..b1f15a8d 100644 --- a/src/pages/inject/action-listener.tsx +++ b/src/pages/inject/action-listener.tsx @@ -1,18 +1,14 @@ import React from 'react'; import { message } from 'antd'; -import Chrome from '@/background/core/chrome'; import { ContentScriptEvents, ContentScriptMessageKey, ContentScriptMessageActions, IShowMessageData, } from '@/isomorphic/event/contentScript'; -import { ClipAssistantMessageActions } from '@/isomorphic/event/clipAssistant'; import { WordMarkMessageActions } from '@/isomorphic/event/wordMark'; import { AccountLayoutMessageActions } from '@/isomorphic/event/accountLayout'; import { App } from './content-scripts'; -import { showScreenShot } from './ScreenShot'; -import { showSelectArea } from './AreaSelector'; import { LevitateBallMessageActions } from '@/isomorphic/event/levitateBall'; type MessageSender = chrome.runtime.MessageSender; @@ -25,89 +21,13 @@ export interface RequestMessage { } export const initContentScriptActionListener = (context: App) => { - Chrome.runtime.onMessage.addListener( + chrome.runtime.onMessage.addListener( ( request: RequestMessage, _sender: MessageSender, sendResponse: SendResponse, ) => { switch (request.action) { - case ContentScriptEvents.ScreenOcr: { - if (context.isOperateSelecting) { - sendResponse(true); - break; - } - if (request.data?.formShortcut) { - context.sendMessageToClipAssistant( - ClipAssistantMessageActions.startScreenOcr, - ); - sendResponse(true); - break; - } - const { isRunningInjectPage = true } = request.data || {}; - context.isOperateSelecting = true; - isRunningInjectPage && context.hiddenSidePanel(); - new Promise(resolve => { - showScreenShot({ - dom: context.rootContainer, - onScreenCancel: () => resolve(null), - onScreenSuccess: resolve, - }); - }).then(res => { - context.isOperateSelecting = false; - isRunningInjectPage && context.showSidePanel(); - sendResponse(res); - }); - break; - } - case ContentScriptEvents.SelectArea: { - const { isRunningInjectPage = true } = request.data || {}; - if (context.isOperateSelecting) { - sendResponse(true); - break; - } - if (request.data?.formShortcut) { - context.sendMessageToClipAssistant( - ClipAssistantMessageActions.startSelectArea, - ); - sendResponse(true); - break; - } - context.isOperateSelecting = true; - isRunningInjectPage && context.hiddenSidePanel(); - new Promise(resolve => { - showSelectArea({ - dom: context.rootContainer, - onSelectAreaCancel: () => resolve(''), - onSelectAreaSuccess: resolve, - }); - }).then(res => { - context.isOperateSelecting = false; - isRunningInjectPage && context.showSidePanel(); - sendResponse(res); - }); - break; - } - case ContentScriptEvents.CollectLink: { - context.showSidePanel().then(() => { - context.sendMessageToClipAssistant( - ClipAssistantMessageActions.startCollectLink, - ); - }); - sendResponse(true); - break; - } - case ContentScriptEvents.ToggleSidePanel: { - if (typeof request.data?.forceVisible === 'boolean') { - request.data?.forceVisible - ? context.showSidePanel() - : context.hiddenSidePanel(); - } else { - context.toggleSidePanel(); - } - sendResponse(true); - break; - } case ContentScriptEvents.WordMarkConfigChange: { context.sendMessageToWordMark( WordMarkMessageActions.wordMarkConfigUpdate, @@ -124,15 +44,6 @@ export const initContentScriptActionListener = (context: App) => { sendResponse(true); break; } - case ContentScriptEvents.AddContentToClipAssistant: { - context.sendMessageToClipAssistant( - ClipAssistantMessageActions.addContent, - request.data, - ); - context.showSidePanel(); - sendResponse(true); - break; - } case ContentScriptEvents.ForceUpgradeVersion: { context.sendMessageToAccountLayout( AccountLayoutMessageActions.ForceUpdate, diff --git a/src/pages/inject/app.module.less b/src/pages/inject/app.module.less new file mode 100644 index 00000000..c7885ebb --- /dev/null +++ b/src/pages/inject/app.module.less @@ -0,0 +1,32 @@ +@import '~@/styles/parameters.less'; + +.sidePanelWrapper { + position: fixed; + right: 0; + top: 0; + z-index: @z-index-max; + display: none; + + &.sidePanelWrapperVisible { + display: block; + } + + .sidePanelIframeWrapper { + display: flex; + flex-direction: row-reverse; + + .sidePanelIframe { + border: none; + margin: 0; + padding: 0; + min-height: 0; + min-width: 0; + overflow: hidden; + transition: initial; + height: 100vh; + width: 100%; + color-scheme: none; + user-select: none; + } + } +} diff --git a/src/pages/inject/app.tsx b/src/pages/inject/app.tsx new file mode 100644 index 00000000..56c01fbf --- /dev/null +++ b/src/pages/inject/app.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef, useState, createRef, useImperativeHandle, useCallback } from 'react'; +import { createRoot } from 'react-dom/client'; +import classnames from 'classnames'; +import { STORAGE_KEYS } from '@/config'; +import DragBar from '@/components/DragBar'; +import { backgroundBridge } from '@/core/bridge/background'; +import { ClipAssistantMessageActions, ClipAssistantMessageKey } from '@/isomorphic/event/clipAssistant'; +import { SidePanelMessageActions, SidePanelMessageKey } from '@/isomorphic/event/sidePanel'; +import LevitateBallApp from './LevitateBall'; +import InjectLayout from './components/InjectLayout'; +import styles from './app.module.less'; + +export interface ContentScriptAppRef { + toggleSidePanel: (visible?: boolean) => void; + addContentToClipAssistant: (html: string) => Promise; + sendMessageToClipAssistant: (action: ClipAssistantMessageActions, data?: any) => Promise; +} + +const App = React.forwardRef((props, ref) => { + const [sidePanelVisible, setSidePanelVisible] = useState(false); + const [sidePanelWidth, setSidePanelWidth] = useState(418); + const sidePanelIframe = useRef(null); + const [needLoadSidePanel, setNeedLoadSidePanel] = useState(true); + const isUsedSidePanelRef = useRef(false); + const sidePanelPromiseRef = useRef( + new Promise(resolve => { + const onMessage = (e: MessageEvent) => { + if (e.data.key !== ClipAssistantMessageKey) { + return; + } + if (e.data.action === ClipAssistantMessageActions.ready) { + resolve(true); + window.removeEventListener('message', onMessage); + } + }; + window.addEventListener('message', onMessage); + }), + ); + + const arouseSidePanel = useCallback(() => { + // 向 iframe 通知一声 + sidePanelIframe.current?.contentWindow?.postMessage( + { + key: SidePanelMessageKey, + action: SidePanelMessageActions.arouse, + }, + '*', + ); + }, []); + + const toggleSidePanel = useCallback((visible?: boolean) => { + // 向 iframe 通知一声 + arouseSidePanel(); + isUsedSidePanelRef.current = true; + if (typeof visible === 'boolean') { + setSidePanelVisible(visible); + return; + } + setSidePanelVisible(preVisible => !preVisible); + }, []); + + const sendMessageToClipAssistant = useCallback(async (action: ClipAssistantMessageActions, data?: any) => { + // 向 iframe 通知一声 + arouseSidePanel(); + await sidePanelPromiseRef.current; + sidePanelIframe.current?.contentWindow?.postMessage( + { + key: ClipAssistantMessageKey, + action, + data, + }, + '*', + ); + }, []); + + const addContentToClipAssistant = useCallback(async (html: string) => { + arouseSidePanel(); + await sidePanelPromiseRef.current; + sendMessageToClipAssistant(ClipAssistantMessageActions.addContent, html); + }, []); + + useImperativeHandle(ref, () => ({ + toggleSidePanel, + addContentToClipAssistant, + sendMessageToClipAssistant, + })); + + useEffect(() => { + // 当页面可见性发生改变时,如果发现用户未使用过插件,释放掉 sidePanel iframe + const onVisibilitychange = () => { + if (isUsedSidePanelRef.current) { + return; + } + if (document.hidden) { + setNeedLoadSidePanel(false); + } else { + setNeedLoadSidePanel(true); + } + }; + document.addEventListener('visibilitychange', onVisibilitychange); + return () => { + document.removeEventListener('visibilitychange', onVisibilitychange); + }; + }, []); + + useEffect(() => { + backgroundBridge.storage.get(STORAGE_KEYS.SETTINGS.SIDE_PANEL_CONFIG).then(res => { + if (res?.width) { + setSidePanelWidth(res.width); + } + }); + }, []); + + return ( + + { + backgroundBridge.storage.update(STORAGE_KEYS.SETTINGS.SIDE_PANEL_CONFIG, { + width, + }); + }} + > +
+ {needLoadSidePanel && ( +