@@ -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);
- }}
- >
-
-
-
- {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);
+ }}
+ >
+
+
+
+ {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 && (
+
+ )}
+
+
+
+
+ );
+});
+
+export function createContentScriptApp() {
+ const div = document.createElement('div');
+ const contentScriptRef = createRef();
+ const root = createRoot(div);
+ root.render();
+ window._yuque_ext_app.rootContainer.appendChild(div);
+ return {
+ ref: contentScriptRef,
+ remove: () => {
+ root.unmount();
+ window._yuque_ext_app.rootContainer.removeChild(div);
+ },
+ };
+}
diff --git a/src/pages/inject/content-scripts.ts b/src/pages/inject/content-scripts.ts
index 80e7a467..2bf2ed92 100644
--- a/src/pages/inject/content-scripts.ts
+++ b/src/pages/inject/content-scripts.ts
@@ -1,51 +1,22 @@
-import Chrome from '@/core/chrome';
import { initI18N } from '@/isomorphic/i18n';
-import {
- ClipAssistantMessageActions,
- ClipAssistantMessageKey,
-} from '@/isomorphic/event/clipAssistant';
-import {
- SidePanelMessageActions,
- SidePanelMessageKey,
-} from '@/isomorphic/event/sidePanel';
-import {
- WordMarkMessageActions,
- WordMarkMessageKey,
-} from '@/isomorphic/event/wordMark';
-import {
- AccountLayoutMessageActions,
- AccountLayoutMessageKey,
-} from '@/isomorphic/event/accountLayout';
-import {
- LevitateBallMessageActions,
- LevitateBallMessageKey,
-} from '@/isomorphic/event/levitateBall';
-import {
- initContentScriptActionListener,
- initContentScriptMessageListener,
-} from './action-listener';
+import { ClipAssistantMessageActions } from '@/isomorphic/event/clipAssistant';
+import { WordMarkMessageActions, WordMarkMessageKey } from '@/isomorphic/event/wordMark';
+import { AccountLayoutMessageActions, AccountLayoutMessageKey } from '@/isomorphic/event/accountLayout';
+import { LevitateBallMessageActions, LevitateBallMessageKey } from '@/isomorphic/event/levitateBall';
+import { initContentScriptActionListener, initContentScriptMessageListener } from './action-listener';
import { createWordMark } from './WordMark';
-import { createLevitateBall } from './LevitateBall';
import '@/styles/inject.less';
-
-enum SidePanelStatus {
- // 还没有初始化
- UnInit = 'UnInit',
-
- // 正在加载中
- Loading = 'Loading',
-
- // 初始化完成
- InitReady = 'InitReady',
-
- // 展开
- Visible = 'Visible',
-
- // 隐藏
- Hidden = 'Hidden',
-}
-
-const YQ_SANDBOX_BOARD_IFRAME = 'yq-sandbox-board-iframe';
+import {
+ InjectScriptRequestKey,
+ InjectScriptResponseKey,
+ MessageEventRequestData,
+ MessageEventResponseData,
+} from '@/isomorphic/injectScript';
+import { getMsgId } from '@/isomorphic/util';
+import { parseDom } from '@/core/parseDom';
+import { ContentScriptAppRef, createContentScriptApp } from './app';
+import { showSelectArea } from './AreaSelector';
+import { showScreenShot } from './ScreenShot';
export class App {
/**
@@ -58,18 +29,11 @@ export class App {
*/
private _shadowRoot: ShadowRoot | null = null;
- private rootDiv: HTMLDivElement | null = null;
-
+ private contentScriptAppRef: React.RefObject | null = null;
/**
* sidePanel iframe
*/
private sidePanelIframe: HTMLIFrameElement | null = null;
- /**
- * sidePanelIframe 加载状态
- */
- private _sidePanelStatus: SidePanelStatus = SidePanelStatus.UnInit;
- private initSidePanelPromise: Promise | undefined;
- private sidePanelClipReadyPromise: Promise | undefined;
/**
* 是否在操作选取,如 ocr 截屏、dom 选取
*/
@@ -79,12 +43,15 @@ export class App {
//
};
- public removeLevitateBall: VoidCallback = () => {
- //
- };
-
constructor() {
this.initRoot();
+ // 注入 inject-content-script.js
+ const script = document.createElement('script');
+ script.src = chrome.runtime.getURL('inject-content-script.js');
+ document.body.append(script);
+ script.onload = () => {
+ script.parentNode?.removeChild(script);
+ };
}
get rootContainer(): HTMLDivElement {
@@ -95,20 +62,11 @@ export class App {
return this._shadowRoot as ShadowRoot;
}
- get sidePanelStatus() {
- return this._sidePanelStatus;
- }
-
- get sidePanelIsReady() {
- return this.sidePanelStatus !== SidePanelStatus.UnInit;
- }
-
private initRoot() {
const div = document.createElement('div');
- this.rootDiv = div;
div.id = 'yuque-extension-root-container';
div.classList.add('yuque-extension-root-container-class');
- const css = Chrome.runtime.getURL('content-scripts.css');
+ const css = chrome.runtime.getURL('content-scripts.css');
fetch(css)
.then(response => response.text())
.then(cssContent => {
@@ -131,166 +89,16 @@ export class App {
});
initContentScriptActionListener(this);
initContentScriptMessageListener();
- this.initSidePanel();
- this.removeLevitateBall = createLevitateBall({
- dom: root,
- });
+ const { ref } = createContentScriptApp();
+ this.contentScriptAppRef = ref;
});
}
- get iframeCSSFieldContent() {
- return `
- #${YQ_SANDBOX_BOARD_IFRAME} {
- display: none;
- border: none;
- margin: 0;
- padding: 0;
- min-height: 0;
- min-width: 0;
- overflow: hidden;
- position: fixed;
- transition: initial;
- max-width: 418px;
- max-height: 100vh;
- width: 418px;
- height: 100vh;
- right: 0;
- top: 0;
- z-index: 2147483645;
- color-scheme: none;
- user-select: none;
- }
- #${YQ_SANDBOX_BOARD_IFRAME}.show {
- display: block;
- color-scheme: auto;
- }
- `;
+ async toggleSidePanel(visible?: boolean) {
+ this.contentScriptAppRef?.current?.toggleSidePanel(visible);
}
- private async addListenClipAssistantReady(): Promise {
- if (this.sidePanelClipReadyPromise) {
- return this.sidePanelClipReadyPromise;
- }
- this.sidePanelClipReadyPromise = 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);
- });
- return this.sidePanelClipReadyPromise;
- }
-
- private async initSidePanel(): Promise {
- if (this.initSidePanelPromise) {
- // 如果 dom 被卸载掉,延迟 1s 重新将其挂载上去
- if (!document.querySelector('#yuque-extension-root-container') && this.rootDiv) {
- document.body.appendChild(this.rootDiv);
- await new Promise(resolve => {
- setTimeout(() => {
- resolve(true);
- }, 1000);
- });
- }
- return this.initSidePanelPromise;
- }
- this.initSidePanelPromise = new Promise(resolve => {
- // 先注入 iframe 对样式
- const style = document.createElement('style');
- style.textContent = this.iframeCSSFieldContent;
- this.shadowRoot.appendChild(style);
- // 创建 iframe
- const iframe = document.createElement('iframe');
- iframe.src = Chrome.runtime.getURL('sidePanel.html');
- iframe.id = YQ_SANDBOX_BOARD_IFRAME;
- this.rootContainer.appendChild(iframe);
- this.addListenClipAssistantReady();
- iframe.onload = () => {
- this._sidePanelStatus = SidePanelStatus.InitReady;
- this.sidePanelIframe = iframe;
- resolve(true);
- };
- });
- return this.initSidePanelPromise;
- }
-
- async removeIframe() {
- await this.initSidePanel();
- this.sidePanelIframe?.classList.remove('show');
- this.sidePanelIframe?.blur();
- }
-
- async hiddenSidePanel() {
- await this.initSidePanel();
- this.sidePanelIframe?.classList.remove('show');
- this.sidePanelIframe?.blur();
- this._sidePanelStatus = SidePanelStatus.Hidden;
- }
-
- private arouseSidePanel() {
- this.sidePanelIframe?.contentWindow?.postMessage(
- {
- key: SidePanelMessageKey,
- action: SidePanelMessageActions.arouse,
- },
- '*',
- );
- }
-
- async showSidePanel() {
- await this.initSidePanel();
- this.arouseSidePanel();
- this.sidePanelIframe?.classList.add('show');
- this._sidePanelStatus = SidePanelStatus.Visible;
- }
-
- async toggleSidePanel() {
- await this.initSidePanel();
- switch (this.sidePanelStatus) {
- case SidePanelStatus.InitReady: {
- this.showSidePanel();
- break;
- }
- case SidePanelStatus.Visible: {
- this.hiddenSidePanel();
- break;
- }
- case SidePanelStatus.Hidden: {
- this.showSidePanel();
- break;
- }
- default: {
- break;
- }
- }
- }
-
- async sendMessageToClipAssistant(
- action: ClipAssistantMessageActions,
- data?: any,
- ) {
- this.arouseSidePanel();
- await this.initSidePanel();
- await this.addListenClipAssistantReady();
- this.sidePanelIframe?.contentWindow?.postMessage(
- {
- key: ClipAssistantMessageKey,
- action,
- data,
- },
- '*',
- );
- }
-
- async sendMessageToAccountLayout(
- action: AccountLayoutMessageActions,
- data?: any,
- ) {
+ async sendMessageToAccountLayout(action: AccountLayoutMessageActions, data?: any) {
// 通知所有的 accountLayout 去触发强制更新,使用到的地方有 setting 页,sidePanel 页
this.sidePanelIframe?.contentWindow?.postMessage(
{
@@ -313,10 +121,7 @@ export class App {
);
}
- async sendMessageToLevitateBall(
- action: LevitateBallMessageActions,
- data?: any,
- ) {
+ async sendMessageToLevitateBall(action: LevitateBallMessageActions, data?: any) {
window.postMessage(
{
key: LevitateBallMessageKey,
@@ -326,6 +131,115 @@ export class App {
'*',
);
}
+
+ async senMessageToPage(params: MessageEventRequestData['data']) {
+ return new Promise((resolve, rejected) => {
+ const requestId = getMsgId({ type: `${params.serviceName}-${params.serviceMethod}` });
+ const onMessage = (e: MessageEvent) => {
+ if (e.data?.key !== InjectScriptResponseKey) {
+ return;
+ }
+ if (e.data?.requestId !== requestId) {
+ return;
+ }
+ // 发生了错误
+ if (e.data.data.error) {
+ rejected(new Error(e.data.data.error));
+ } else {
+ resolve(e.data.data.result);
+ }
+ window.removeEventListener('message', onMessage);
+ };
+
+ // 监听消息
+ window.addEventListener('message', onMessage);
+
+ window.postMessage(
+ {
+ key: InjectScriptRequestKey,
+ requestId,
+ data: {
+ serviceName: params.serviceName,
+ serviceMethod: params.serviceMethod,
+ params: params.params,
+ },
+ },
+ '*',
+ );
+
+ setTimeout(() => {
+ // 30s 还没有返回中断请求
+ window.removeEventListener('message', onMessage);
+ rejected(new Error('request timeout'));
+ }, 30000);
+ });
+ }
+
+ async clipPage() {
+ const result = await this.parsePage();
+ this.toggleSidePanel(true);
+ this.addContentToClipAssistant(result);
+ }
+
+ async parsePage() {
+ const result = await parseDom.parsePage();
+ return result;
+ }
+
+ async clipSelectArea(params?: { isRunningHostPage: boolean; formShortcut: boolean }) {
+ if (this.isOperateSelecting) {
+ return;
+ }
+ const { isRunningHostPage = true, formShortcut = false } = params || {};
+ this.isOperateSelecting = true;
+ isRunningHostPage && this.toggleSidePanel(false);
+ const result: string = await new Promise(resolve => {
+ showSelectArea({
+ dom: this.rootContainer,
+ onSelectAreaCancel: () => resolve(''),
+ onSelectAreaSuccess: resolve,
+ });
+ });
+ this.isOperateSelecting = false;
+ isRunningHostPage && this.toggleSidePanel(true);
+ if (formShortcut) {
+ await this.addContentToClipAssistant(result);
+ }
+ return result;
+ }
+
+ async clipScreenOcr(params?: { isRunningHostPage: boolean; formShortcut: boolean }) {
+ if (this.isOperateSelecting) {
+ return;
+ }
+ const { isRunningHostPage = true, formShortcut = false } = params || {};
+
+ if (formShortcut) {
+ this.contentScriptAppRef?.current?.sendMessageToClipAssistant(ClipAssistantMessageActions.startScreenOcr);
+ return;
+ }
+
+ this.isOperateSelecting = true;
+ isRunningHostPage && this.toggleSidePanel(false);
+ const result: string | null = await new Promise(resolve => {
+ showScreenShot({
+ dom: this.rootContainer,
+ onScreenCancel: () => resolve(null),
+ onScreenSuccess: resolve,
+ });
+ });
+ this.isOperateSelecting = false;
+ isRunningHostPage && this.toggleSidePanel(true);
+
+ return result;
+ }
+
+ async addContentToClipAssistant(html: string, expandSidePanel?: boolean) {
+ if (expandSidePanel) {
+ this.toggleSidePanel(true);
+ }
+ return await this.contentScriptAppRef?.current?.addContentToClipAssistant(html);
+ }
}
function initSandbox() {
diff --git a/src/pages/inject/yuque-transform-script.ts b/src/pages/inject/yuque-transform-script.ts
deleted file mode 100644
index 38c7485c..00000000
--- a/src/pages/inject/yuque-transform-script.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-window.addEventListener('message', e => {
- const messageKey = 'tarnsfromYuqueContent';
- if (e.data?.key !== messageKey) {
- return;
- }
- const { data } = e.data || {};
- const editorBody = document.querySelector('.ne-viewer-body');
- const ids = data.ids || [];
- const result: string[] = [];
- for (const id of ids) {
- try {
- const html = (editorBody as any)?._neRef?.document.viewer.execCommand('getNodeContent', id, 'text/html');
- if (html) {
- result.push(html);
- }
- } catch (error) {
- //
- }
- }
- window.postMessage(
- {
- key: 'tarnsfromYuqueContentValue',
- data: { result },
- },
- '*',
- );
-});
diff --git a/src/pages/setting/help/index.tsx b/src/pages/setting/help/index.tsx
index 52e819f6..78155255 100644
--- a/src/pages/setting/help/index.tsx
+++ b/src/pages/setting/help/index.tsx
@@ -15,7 +15,7 @@ function Help() {
- window.open(LinkHelper.feedback)}>
+ window.open(LinkHelper.feedback())}>
{__i18n('提交反馈与建议')}
diff --git a/src/pages/setting/wordMark/index.tsx b/src/pages/setting/wordMark/index.tsx
index 5fa3f603..43b424c9 100644
--- a/src/pages/setting/wordMark/index.tsx
+++ b/src/pages/setting/wordMark/index.tsx
@@ -222,7 +222,7 @@ function WordMark() {
-
+
{__i18n('需要更多划词快捷指令?')}
diff --git a/src/pages/sidePanel/app.tsx b/src/pages/sidePanel/app.tsx
index 8cbb70f7..a09b41b6 100644
--- a/src/pages/sidePanel/app.tsx
+++ b/src/pages/sidePanel/app.tsx
@@ -6,7 +6,6 @@ import {
SidePanelMessageActions,
SidePanelMessageKey,
} from '@/isomorphic/event/sidePanel';
-import Chrome from '@/core/chrome';
import { backgroundBridge } from '@/core/bridge/background';
import {
EXTENSION_ID,
@@ -17,7 +16,7 @@ import {
VERSION,
} from '@/config';
import { IUser } from '@/isomorphic/interface';
-import { isRunningInjectPage } from '@/core/uitl';
+import Env from '@/isomorphic/env';
import { useForceUpdate } from '@/hooks/useForceUpdate';
import styles from './app.module.less';
import '@/styles/global.less';
@@ -32,7 +31,7 @@ function App() {
const disableRef = useRef(window.innerWidth < MiniWidth);
useEffect(() => {
- if (isRunningInjectPage) {
+ if (Env.isRunningHostPage) {
return;
}
const onResize = () => {
@@ -51,7 +50,7 @@ function App() {
useEffect(() => {
if (sidePanelIsReady) {
const script = document.createElement('script');
- script.src = Chrome.runtime.getURL('tracert_a385.js');
+ script.src = chrome.runtime.getURL('tracert_a385.js');
document.body.appendChild(script);
script.onload = () => {
backgroundBridge.tab.getCurrent().then(async tabInfo => {
@@ -64,7 +63,7 @@ function App() {
role_id: (info as IUser)?.id,
mdata: {
[REQUEST_HEADER_VERSION]: VERSION,
- [EXTENSION_ID]: Chrome.runtime.id,
+ [EXTENSION_ID]: chrome.runtime.id,
[REFERER_URL]: tabInfo?.url,
},
});
@@ -86,7 +85,7 @@ function App() {
break;
}
};
- if (!isRunningInjectPage) {
+ if (!Env.isRunningHostPage) {
setSidePanelIsReady(true);
return;
}
@@ -102,7 +101,7 @@ function App() {
{sidePanelIsReady &&
}
- {disableRef.current && !isRunningInjectPage && (
+ {disableRef.current && !Env.isRunningHostPage && (
diff --git a/webpack.config.js b/webpack.config.js
index 745faab9..5c789ac1 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -39,7 +39,7 @@ const entries = {
contentScript: 'content-scripts',
background: 'background',
extensionPage: ['setting', 'editor', 'sidePanel'],
- yuqueTransformScript: 'yuque-transform-script',
+ injectContentScript: 'inject-content-script',
};
// editor 页面是一个隐藏页面,不需要加入埋点日志
@@ -68,8 +68,8 @@ const plugins = [
name: pkg.description,
}, origin);
if (isBeta) {
- value.name = `${value.name} BETA`;
- value.description = `${value.description} (THIS EXTENSION IS FOR BETA TESTING)`;
+ value.name = `${value.name} Beta`;
+ value.description = `${value.description}`;
}
return Buffer.from(JSON.stringify(value, null, 2));
},
@@ -108,7 +108,7 @@ if (isProd) {
const entry = {
[entries.background]: path.join(srcPath, entries.background),
[entries.contentScript]: path.join(pagesPath, 'inject', entries.contentScript),
- [entries.yuqueTransformScript]: path.join(pagesPath, 'inject', entries.yuqueTransformScript),
+ [entries.injectContentScript]: path.join(srcPath, 'injectscript', 'index'),
};
entries.extensionPage.forEach(item => {