diff --git a/package.json b/package.json index e55d0df6..55bde866 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "yuque-chrome-extension", - "version": "0.5.5", + "version": "0.5.9", "description": "语雀浏览器插件", "private": true, "releaseNotes": [ - "[特性] 支持全文剪藏", - "[优化] 优化插件性能", - "[修复] 修复上传图片失败的异常处理" + "[特性] 支持行内代码块", + "[优化] 优化图片剪藏逻辑" ], "repository": { "type": "git", diff --git a/scripts/preset-editor.js b/scripts/preset-editor.js index 667309da..4155c2d9 100644 --- a/scripts/preset-editor.js +++ b/scripts/preset-editor.js @@ -14,9 +14,9 @@ const { const { distFolder, sleep } = require('./common'); -const LAKE_EDITOR_VERSION = '1.11.0'; +const LAKE_EDITOR_VERSION = '1.17.0'; -const lakeIconURL = 'https://mdn.alipayobjects.com/design_kitchencore/afts/file/HWy0Q7LuuV0AAAAAAAAAAAAADhulAQBr'; +const lakeIconURL = 'https://mdn.alipayobjects.com/design_kitchencore/afts/file/GxqwR5_S3xIAAAAAAAAAAAAADhulAQBr'; // 必须包含 name 名称 const remoteAssetsUrls = { diff --git a/src/background/core/httpClient.ts b/src/background/core/httpClient.ts index a1287a8c..e011a351 100644 --- a/src/background/core/httpClient.ts +++ b/src/background/core/httpClient.ts @@ -11,6 +11,7 @@ import { getMsgId, transformUrlToFile } from '@/isomorphic/util'; import { PageEventTypes } from '@/isomorphic/event/pageEvent'; import ExtensionMessage from '@/isomorphic/extensionMessage/extensionMessage'; import { ExtensionMessageListener } from '@/isomorphic/extensionMessage/interface'; +import Env from '@/isomorphic/env'; import chromeExtension from './chromeExtension'; import { getCurrentAccount } from './util'; @@ -59,6 +60,10 @@ export default class HttpClient { private decoder = new TextDecoder(); constructor() { + // 只在后台监听,其余系统页都不监听 + if (!Env.isBackground) { + return; + } // 监听关闭的请求 ExtensionMessage.addListener(ExtensionMessageListener.callRequestService, async (data, requestId, { tab }) => { const { type, methodParams = {}, callbackFnId } = data; diff --git a/src/components/AccountLayout/Login.tsx b/src/components/AccountLayout/Login.tsx index 500be8c3..620b85af 100644 --- a/src/components/AccountLayout/Login.tsx +++ b/src/components/AccountLayout/Login.tsx @@ -13,13 +13,15 @@ import styles from './Login.module.less'; interface ILoginProps { forceUpgradeHtml?: string; + setUser: (user: any) => void; } function Login(props: ILoginProps) { - const { forceUpgradeHtml } = props; + const { forceUpgradeHtml, setUser } = props; const onLogin = async () => { const user = await backgroundBridge.user.login(); + setUser(user); if (!user) { message.error(__i18n('登录失败')); return; diff --git a/src/components/AccountLayout/index.tsx b/src/components/AccountLayout/index.tsx index dacd5c25..83d63fb5 100644 --- a/src/components/AccountLayout/index.tsx +++ b/src/components/AccountLayout/index.tsx @@ -80,7 +80,7 @@ function AccountLayout(props: IAccountLayoutProps) { {isLogined && !forceUpgradeHtml ? ( props.children ) : ( - + )} ); diff --git a/src/components/AntdLayout/index.tsx b/src/components/AntdLayout/index.tsx index ca870721..db60e06e 100644 --- a/src/components/AntdLayout/index.tsx +++ b/src/components/AntdLayout/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { ConfigProvider } from 'antd'; import { ConfigProviderProps } from 'antd/es/config-provider'; +import { SidePanelZIndex } from '@/config'; interface IAntdLayoutProps { children: React.ReactNode; @@ -25,6 +26,9 @@ function AntdLayout(props: IAntdLayoutProps) { colorBgSpotlight: 'rgba(0, 0, 0, 0.75)', fontSize: 12, }, + Message: { + zIndexPopup: SidePanelZIndex, + }, }, }} {...props.config} diff --git a/src/components/SuperSideBar/impl/ClipAssistant/index.tsx b/src/components/SuperSideBar/impl/ClipAssistant/index.tsx index f864ad5e..cab9b376 100644 --- a/src/components/SuperSideBar/impl/ClipAssistant/index.tsx +++ b/src/components/SuperSideBar/impl/ClipAssistant/index.tsx @@ -29,11 +29,16 @@ import AddTagButton from './component/AddTagButton'; import TagList from './component/TagList'; import styles from './index.module.less'; +const LoadingMessage = { + ocr: __i18n('正在识别中'), + parse: __i18n('正在解析中'), +}; + function ClipContent() { const editorRef = useRef(null); const shortcutMap = useClipShortCut(); const [loading, setLoading] = useState<{ - type?: 'ocr'; + type?: keyof typeof LoadingMessage; loading: boolean; }>({ loading: false, @@ -127,12 +132,13 @@ function ClipContent() { setLoading({ loading: false }); }; - const onUploadImage = useCallback(async (params: { data: string }) => { + const onUploadImage = useCallback(async (params: { data: any }) => { const file = await transformUrlToFile(params.data); - const res = await Promise.all([webProxy.upload.attach(params.data), ocrManager.startOCR('file', file)]); + const res = await webProxy.upload.attach(params.data); + const ocrTask = ocrManager.startOCR('file', file); return { - ...(res[0] || {}), - ocrLocations: res[1], + ...(res || {}), + ocrTask, }; }, []); @@ -148,12 +154,18 @@ function ClipContent() { }; const onClipPage = async () => { + setLoading({ loading: true, type: 'parse' }); const html = await backgroundBridge.clip.clipPage(); + if (!html) { + setLoading({ loading: false }); + return; + } const isAddLink = await addLinkWhenEmpty(); if (!isAddLink) { editorRef.current?.insertBreakLine(); } editorRef.current?.appendContent(html); + setLoading({ loading: false }); }; const addLinkWhenEmpty = async () => { @@ -167,12 +179,18 @@ function ClipContent() { }; const onSelectArea = async () => { + setLoading({ loading: true, type: 'parse' }); const html = await backgroundBridge.clip.selectArea(); + if (!html) { + setLoading({ loading: false }); + return; + } const isAddLink = await addLinkWhenEmpty(); if (!isAddLink) { editorRef.current?.insertBreakLine(); } editorRef.current?.appendContent(html); + setLoading({ loading: false }); }; const onScreenOcr = async () => { @@ -199,6 +217,10 @@ function ClipContent() { }; }); const text = textArray?.map(item => item.text)?.join('') || ''; + if (!text) { + setLoading({ loading: false }); + return; + } const isAddLink = await addLinkWhenEmpty(); if (!isAddLink) { editorRef.current?.insertBreakLine(); @@ -224,7 +246,7 @@ function ClipContent() { if (!loading.loading) { return null; } - const text = loading.type === 'ocr' ? '正在识别中' : ''; + const text = LoadingMessage[loading.type as keyof typeof LoadingMessage] || ''; return (
diff --git a/src/components/lake-editor/editor-plugin.ts b/src/components/lake-editor/editor-plugin.ts index 8500de18..a176fa3f 100644 --- a/src/components/lake-editor/editor-plugin.ts +++ b/src/components/lake-editor/editor-plugin.ts @@ -175,6 +175,22 @@ export function InjectEditorPlugin({ EditorPlugin, KernelPlugin, PositionUtil, O }, }, ); + htmlService.registerHTMLNodeReader( + ['code'], + { + readNode(context: any, node: any) { + context.setNode({ + id: node.attrs.id || '', + type: 'element', + name: 'code', + attrs: {}, + }); + }, + leaveNode() { + // ignore empty + }, + }, + ); } } } diff --git a/src/components/lake-editor/editor.tsx b/src/components/lake-editor/editor.tsx index 97b4db32..686bf138 100644 --- a/src/components/lake-editor/editor.tsx +++ b/src/components/lake-editor/editor.tsx @@ -136,38 +136,22 @@ export default forwardRef((props, ref) => { return !url?.startsWith('https://cdn.nlark.com/yuque'); }, createUploadPromise: props.uploadImage, + editUI: class extends win.Doc.EditCardUI.extend(win.Doc.Plugins.Image.editImageUIAddon) { + init(...args: any[]) { + super.init(...args); + this.on('uploadSuccess', (data: { ocrTask: Promise}) => { + data.ocrTask.then(res => { + this.cardData.setImageInfo({ ...data, ocrLocations: res }); + }); + }); + } + }, innerButtonWidgets: [ { name: 'ocr', title: 'OCR', icon: , enable: (cardUI: any) => { - cardUI.on('uploadSuccess', () => { - setTimeout(() => { - if (!cardUI.cardData._cardValue?.ocr?.length) { - return; - } - cardUI.uiViewProxy.rerender({ - innerButtonWidgets: - cardUI.pluginOption.innerButtonWidgets.map( - (widget: any) => ({ - ...widget, - execute: () => { - widget.execute(cardUI); - }, - enable: - typeof widget.enable === 'function' - ? () => { - return widget.enable(cardUI); - } - : () => { - return !!widget.enable; - }, - }), - ), - }); - }, 500); - }); return cardUI.cardData.getOcrLocations()?.length > 0; }, execute: (cardUI: any) => { diff --git a/src/config.ts b/src/config.ts index fc8f7883..2a226a24 100644 --- a/src/config.ts +++ b/src/config.ts @@ -39,3 +39,6 @@ export const TRACERT_CONFIG = { spmAPos: 'a385', spmBPos: 'b65721', }; + +// sidePanel 的 zIndex 层级 +export const SidePanelZIndex = 2147483646; diff --git a/src/core/ocr-manager.ts b/src/core/ocr-manager.ts index c9499d76..cc705b9f 100644 --- a/src/core/ocr-manager.ts +++ b/src/core/ocr-manager.ts @@ -31,6 +31,14 @@ class OCRManager { private ocrIframeId = 'yq-ocr-iframe-id'; private sendMessageRef: ((requestData: { action: string; data?: any }) => Promise) | undefined; private initSidePanelPromise: Promise | undefined; + private ocrQueue: Array<{ + task: () => Promise>; + resolve: (res: Array) => void; + }> = []; + // 当前运行数 + private runningTasks = 0; + // 最大的任务运行数 + private concurrentTasks = 5; async init() { if (this.initSidePanelPromise) { @@ -78,7 +86,7 @@ class OCRManager { return this.initSidePanelPromise; } - async startOCR(type: 'file' | 'blob' | 'url', content: File | Blob | string) { + private async executeOcr(type: 'file' | 'blob' | 'url', content: File | Blob | string) { // 调用 ocr 时,开始 ocr 等预热 await this.init(); const enableOcrStatus = await ocrManager.isWebOcrReady(); @@ -109,6 +117,26 @@ class OCRManager { return []; } + async startOCR(type: 'file' | 'blob' | 'url', content: File | Blob | string) { + return await new Promise(resolve => { + this.ocrQueue.push({ resolve, task: () => this.executeOcr(type, content) }); + this.dequeue(); + }); + } + + private dequeue() { + if (this.runningTasks >= this.concurrentTasks || !this.ocrQueue.length) { + return; + } + const { task, resolve } = this.ocrQueue.shift()!; + this.runningTasks++; + task().then((res: any) => { + this.runningTasks--; + resolve(res); + this.dequeue(); + }); + } + async isWebOcrReady() { const result = await this.sendMessage('isWebOcrReady'); if (!result) { diff --git a/src/core/parseDom/plugin/code.ts b/src/core/parseDom/plugin/code.ts index 74c5b88f..83f2ddd2 100644 --- a/src/core/parseDom/plugin/code.ts +++ b/src/core/parseDom/plugin/code.ts @@ -23,6 +23,14 @@ export class CodeParsePlugin extends BasePlugin { preElements.forEach(pre => { // 查询所有的代码块 const codeElementArray = pre.querySelectorAll('code'); + /** + * 有些页面的代码块不是 + *
xxxx
+ * 对于这类不处理 + */ + if (!codeElementArray.length) { + return; + } Array.from(pre.childNodes).forEach(item => { pre.removeChild(item); }); diff --git a/src/core/parseDom/plugin/hexoCode.ts b/src/core/parseDom/plugin/hexoCode.ts index 8dbf07eb..c4eb79f2 100644 --- a/src/core/parseDom/plugin/hexoCode.ts +++ b/src/core/parseDom/plugin/hexoCode.ts @@ -11,16 +11,15 @@ export class HexoCodeParsePlugin extends BasePlugin { } const codeElement = code.querySelector('pre'); if (codeElement) { - node.parentNode?.appendChild(codeElement); + node.parentNode?.replaceChild(codeElement, node); } - node.parentNode?.removeChild(node); }; - figures.forEach(figure => { + Array.from(figures).forEach(figure => { processingCodeBlock(figure); }); if (figures.length === 0) { const tables = cloneDom.querySelectorAll('table'); - tables.forEach(table => { + Array.from(tables).forEach(table => { processingCodeBlock(table); }); } diff --git a/src/core/parseDom/plugin/image.ts b/src/core/parseDom/plugin/image.ts index 44ef5213..299ac85f 100644 --- a/src/core/parseDom/plugin/image.ts +++ b/src/core/parseDom/plugin/image.ts @@ -1,15 +1,35 @@ import { BasePlugin } from './base'; export class ImageParsePlugin extends BasePlugin { - public parse(cloneDom: HTMLElement): Promise | void { + public async parse(cloneDom: HTMLElement): Promise { const images = cloneDom.querySelectorAll('img'); - images.forEach(image => { - /** - * data-src 占位图 - * currentSrc 真实渲染的图片 - * src - */ - image.setAttribute('src', image.getAttribute('data-src') || image.currentSrc || image.src); + const requestArray = Array.from(images).map(image => { + return new Promise(async resolve => { + image.setAttribute('src', image.getAttribute('data-src') || image.currentSrc || image.src); + const isOriginImage = /^(http|https):\/\//.test(image.src); + if (!isOriginImage) { + resolve(true); + return; + } + try { + const response = await fetch(image.src); + if (response.status !== 200) { + throw new Error('Error fetching image'); + } + const blob = await response.blob(); // 将响应体转换为 Blob + const reader = new FileReader(); + reader.readAsDataURL(blob); // 读取 Blob 数据并编码为 Base64 + reader.onloadend = () => { + // 获取 Base64 编码的数据 + const base64data = reader.result; + image.src = base64data as string; + resolve(true); + }; + } catch (e: any) { + resolve(true); + } + }); }); + await Promise.all(requestArray); } } diff --git a/src/core/webProxy/base.ts b/src/core/webProxy/base.ts index a222948c..8682197e 100644 --- a/src/core/webProxy/base.ts +++ b/src/core/webProxy/base.ts @@ -1,11 +1,14 @@ import { getMsgId } from '@/isomorphic/util'; import type { IRequestOptions, IRequestConfig } from '@/background/core/httpClient'; import ExtensionMessage from '@/isomorphic/extensionMessage/extensionMessage'; +import Env from '@/isomorphic/env'; +import HttpClient from '@/background/core/httpClient'; import { ExtensionMessageListener } from '@/isomorphic/extensionMessage/interface'; // http 请求单独走一个通道 class HttpProxy { private callServiceMethodCallbackFn: { [id: string]: (...rest: any[]) => void } = {}; + private httpClient = Env.isExtensionPage ? new HttpClient() : null; constructor() { this.init(); } @@ -22,6 +25,19 @@ class HttpProxy { type?: 'abort' | 'request', id?: string, ) { + if (Env.isExtensionPage) { + if (methodParams?.options?.isFileUpload) { + this.httpClient?.uploadFile(methodParams.url, methodParams.config).then(res => callback?.(res)); + return; + } + this.httpClient + ?.handleRequest(methodParams?.url, methodParams?.config, { + ...methodParams?.options, + streamCallback: callback, + }) + .then(res => callback?.(res)); + return; + } const callbackFnId = id ? id : this.generateCallbackFnId('', ''); if (type !== 'abort') { this.callServiceMethodCallbackFn[callbackFnId] = (response: any) => { diff --git a/src/injectscript/index.ts b/src/injectscript/index.ts index 472f6a69..3a182c83 100644 --- a/src/injectscript/index.ts +++ b/src/injectscript/index.ts @@ -1,5 +1,5 @@ import { InjectScriptRequestKey, MessageEventRequestData } from '../isomorphic/injectScript'; -import { YuqueService } from './service'; +import { YuqueService, CommonService } from './service'; import { BaseService } from './service/base'; class InjectScriptApp { @@ -11,6 +11,7 @@ class InjectScriptApp { init() { this.registerService(new YuqueService()); + this.registerService(new CommonService()); window.addEventListener('message', async (e: MessageEvent) => { if (e.data.key !== InjectScriptRequestKey) { return; diff --git a/src/injectscript/service/common.ts b/src/injectscript/service/common.ts new file mode 100644 index 00000000..e4c7b9df --- /dev/null +++ b/src/injectscript/service/common.ts @@ -0,0 +1,13 @@ +import { BaseService } from './base'; + +export class CommonService extends BaseService { + public name = 'CommonService'; + public urlRegExp = ''; + + enableDocumentCopy() { + if (typeof (window as any)?.appData?.book?.enable_document_copy === 'boolean') { + return (window as any).appData.book.enable_document_copy; + } + return true; + } +} diff --git a/src/injectscript/service/index.ts b/src/injectscript/service/index.ts index 3c4d65ef..77397a32 100644 --- a/src/injectscript/service/index.ts +++ b/src/injectscript/service/index.ts @@ -1 +1,2 @@ export * from './yuque'; +export * from './common'; diff --git a/src/pages/inject/AreaSelector/app.tsx b/src/pages/inject/AreaSelector/app.tsx index 23bc62c3..4c102222 100644 --- a/src/pages/inject/AreaSelector/app.tsx +++ b/src/pages/inject/AreaSelector/app.tsx @@ -5,6 +5,7 @@ import { useForceUpdate } from '@/hooks/useForceUpdate'; import { useEnterShortcut } from '@/hooks/useEnterShortCut'; import { parseDom } from '@/core/parseDom'; import { __i18n } from '@/isomorphic/i18n'; +import Env from '@/isomorphic/env'; import styles from './app.module.less'; type Rect = Pick; @@ -26,6 +27,7 @@ function App(props: IAppProps) { const onSave = useCallback(async () => { setSaving(true); const selections = targetListRef.current.filter(item => item) || []; + Env.isRunningHostPage && window._yuque_ext_app.toggleSidePanel(true); const selectAreaElements = await parseDom.parseDom(selections); props.onSelectAreaSuccess(selectAreaElements.join('')); }, []); diff --git a/src/pages/inject/content-scripts.ts b/src/pages/inject/content-scripts.ts index 02c509c6..574defc4 100644 --- a/src/pages/inject/content-scripts.ts +++ b/src/pages/inject/content-scripts.ts @@ -133,6 +133,14 @@ export class App { } async parsePage() { + const enableDocumentCopy = await this.senMessageToPage({ + serviceName: 'CommonService', + serviceMethod: 'enableDocumentCopy', + }); + if (!enableDocumentCopy) { + this.showMessage({ type: 'error', text: __i18n('当前页面已开启防复制,不支持剪藏') }); + return; + } const result = await parseDom.parsePage(); return result; } @@ -141,6 +149,14 @@ export class App { if (this.isOperateSelecting) { return; } + const enableDocumentCopy = await this.senMessageToPage({ + serviceName: 'CommonService', + serviceMethod: 'enableDocumentCopy', + }); + if (!enableDocumentCopy) { + this.showMessage({ type: 'error', text: __i18n('当前页面已开启防复制,不支持剪藏') }); + return; + } const { isRunningHostPage = true, formShortcut = false } = params || {}; this.isOperateSelecting = true; isRunningHostPage && this.toggleSidePanel(false); diff --git a/tools/dev-tools/generate-svg-map.js b/tools/dev-tools/generate-svg-map.js index 3ba64cb2..3fd726dc 100644 --- a/tools/dev-tools/generate-svg-map.js +++ b/tools/dev-tools/generate-svg-map.js @@ -19,15 +19,25 @@ function nameWithoutExt(name) { .join('.'); } +function convertToCamelCase(str) { + const words = str.split('-').map(word => { + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }); + return words.join(''); +} + function updateAssetsMap({ targetFilePath, }) { const svgMap = {}; + const importArray = []; walkDirSync(svgAssetPath, (filePath, name) => { const key = nameWithoutExt(name); if (!key) return; - svgMap[key] = `import('@/assets/svg/${name}')`; + const componentName = convertToCamelCase(key); + svgMap[key] = componentName; + importArray.push(`import ${componentName} from '@/assets/svg/${name}';`); }); const generateArray = map => { @@ -38,6 +48,10 @@ function updateAssetsMap({ return `{\n${result}}`; }; + const generateImportant = array => { + return array.join('\n'); + }; + // 写入文件 fs.writeFileSync( @@ -46,9 +60,9 @@ function updateAssetsMap({ /* eslint-disable @typescript-eslint/indent */ // 本文件为自动生成,不要手动修改 // npm run update:assets +${generateImportant(importArray)} export const SvgMaps = ${generateArray(svgMap)}; - ` );