diff --git a/content.ts b/content.ts index c453b4a..3f06235 100644 --- a/content.ts +++ b/content.ts @@ -1,55 +1,48 @@ -import { Page } from "~uitls/bingPage"; -import { DownloadVisitor } from "~uitls/visitor"; +import { Page } from "~utils/bingPage"; +import { DownloadVisitor } from "~utils/visitor"; import type { PlasmoCSConfig } from "plasmo" +import { exportActions, Settings } from "~utils/constants"; +import { handleElementVisibility } from "~utils/viewmodel"; export const config: PlasmoCSConfig = { matches: ["https://www.bing.com/search*"], all_frames: true } - -const PNG = "PNG"; -const JPG = "JPG"; -const MD = "Markdown"; -const JSON = "JSON"; - - const init = async () => { await Page.waitForElm("#b_sydConvCont > cib-serp"); - // remove welcome bar - Page.getWelcome().remove(); - const feedbackGroup = Page.getFeedbackBar(); + await handleElementVisibility(Page.getWelcome(), Settings.WELCOME) + const feedbackGroup =Page.getFeedbackBar(); const feedbackButton = feedbackGroup.querySelector("#fbpgbt"); + addButtonGroups(feedbackGroup, feedbackButton); + + await handleElementVisibility(feedbackButton, Settings.FEEDBACK, 'block'); }; const addButtonGroups = (actionsArea, WaitingButton) => { - // TODO: 加折叠,或者 - addButton(actionsArea, WaitingButton, PNG); - addButton(actionsArea, WaitingButton, JPG); - addButton(actionsArea, WaitingButton, JSON); - addButton(actionsArea, WaitingButton, MD); + // TODO: i18n + addButton(actionsArea, WaitingButton, exportActions.ALL); + addButton(actionsArea, WaitingButton, exportActions.PREVIEW); }; -const addButton = (actionsArea, WaitingButton, type) => { +const addButton = (actionsArea, WaitingButton, action) => { const downloadButton = WaitingButton.cloneNode(true); - downloadButton.id = `${type}-download-button`; - downloadButton.innerText = type; - - const getOnClickByType = (type) => { - if (type === PNG) { - return DownloadVisitor.forPNG; - } else if (type === JPG) { - return DownloadVisitor.forJPG; - } else if (type === MD) { - return DownloadVisitor.forMD; - } else if (type === JSON) { - return DownloadVisitor.forJSON; + downloadButton.id = `${action}-download-button`; + downloadButton.innerText = action; + + const getOnClickByAction = (action) => { + if (action === exportActions.ALL) { + return DownloadVisitor.forAll; + } else if (action === exportActions.PREVIEW) { + return DownloadVisitor.forPreview; + } else { + throw new Error(`There is not action type of ${action}`); } }; - downloadButton.onclick = getOnClickByType(type); + downloadButton.onclick = getOnClickByAction(action); actionsArea.appendChild(downloadButton); }; diff --git a/package.json b/package.json index 7190d06..adafac1 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { "name": "bing-chat-saver", "displayName": "Bing Chat saver", - "version": "0.0.5", + "version": "0.0.6", "description": "Saving Bing Chat for you, for sharing", "author": "gantrol", "scripts": { "dev": "plasmo dev", "test": "jest", "build": "plasmo build", - "package": "plasmo package" }, "dependencies": { - "modern-screenshot": "^4.3.4", + "modern-screenshot": "^4.4.0", "plasmo": "0.65.0", "svelte": "3.55.1", "svelte-preprocess": "5.0.1", @@ -32,6 +31,10 @@ "typescript": "4.9.4" }, "manifest": { - "name": "__MSG_extensionName__" + "name": "__MSG_extensionName__", + "permissions": [ + "storage" + ], + "default_locale": "en" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9e09300..2e77901 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ specifiers: jest: 29.4.1 jest-environment-jsdom: 29.4.1 jest-webextension-mock: 3.8.8 - modern-screenshot: ^4.3.4 + modern-screenshot: ^4.4.0 plasmo: 0.65.0 prettier: 2.8.3 svelte: 3.55.1 @@ -19,7 +19,7 @@ specifiers: typescript: 4.9.4 dependencies: - modern-screenshot: 4.3.4 + modern-screenshot: 4.4.0 plasmo: 0.65.0 svelte: 3.55.1 svelte-preprocess: 5.0.1_atrrhq7vg4ekua4nnyrpuardle @@ -5400,8 +5400,8 @@ packages: resolution: {integrity: sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA==} dev: false - /modern-screenshot/4.3.4: - resolution: {integrity: sha512-zB298wd27E20tkx/fE5BiTz7FiBP/JJk4jP7pBx9NCBnjFnc4QH47I82+pYwPZGjV6PuEbThCXFPL82H6wQ4Ug==} + /modern-screenshot/4.4.0: + resolution: {integrity: sha512-gWU2kaYQAEX/RV28TAfOnvGKvolVVVzlDtZeGX/BXpjlqDrFLuyP1JByG7kgJJP2Db+oGzs4kD3DPno9yfpDUA==} dev: false /ms/2.1.2: diff --git a/popup.svelte b/popup.svelte new file mode 100644 index 0000000..3293be1 --- /dev/null +++ b/popup.svelte @@ -0,0 +1,64 @@ + + +
+ {#await Promise.all(promises)} +

Waiting...

+ {:then _} +

ExportSettings

+
+ + {#each $exportSettings as message, i} +
+ + + +
+ {/each} +
+

UI Settings

+
+
+ + Hide welcome bar +
+
+
+
+ + Hide feedback button +
+
+ {:catch err} +

{err}

+ {/await} +
+ + + + diff --git a/test/parser.test.ts b/test/parser.test.ts index 0bc98ae..677f881 100644 --- a/test/parser.test.ts +++ b/test/parser.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "@jest/globals"; -import { QasJSON2MarkdownParser } from "~uitls/md/parser"; +import { QasJSON2MarkdownParser } from "~utils/md/parser"; // TODO: read a file instead diff --git a/todo.md b/todo.md index 06b0294..c4f3a69 100644 --- a/todo.md +++ b/todo.md @@ -4,16 +4,14 @@ ## main -- [ ] reload chat with JSON +- [ ] reload chat with JSON - [ ] extension main page for HTML - [ ] JSON Viewer (render to HTML) ## design & modified -- [ ] ButtonGroups,改为Export一个按钮,提供下载选项(popup或类似feedback的小窗口) -- [ ] 图片默认下载,并且在右下方生成小窗口 -- [ ] ignore element (`.cib-welcome-container`) instead of remove it. May add a option to remove it -- [ ] icon 改为箭头 +- [ ] Preview改为在右下方生成小窗口 +- [ ] icon 改为箭头? ## others @@ -21,8 +19,20 @@ - [ ] test - [ ] how to add unittest for shadow-roots? - [ ] 改英文文档 +- [ ] markdown add "meta" searching info ## current -- [ ] 将所有的链接的a的 font-weight: 400 -- [ ] WARN | default_locale not set, fallback to en +### main + +- [ ] extension main page for HTML -> Plasmo doc for extension's tab + +### modified + +- [ ] 尝试往导出加一个微软图标 +- [ ] add to zip when download multiply +- [ ] JS doc + +## 废案 + +- [x] ~~实验录屏 (成果上传不了哔哩哔哩,鸡肋;还不如Windows + Alt + R)~~ diff --git a/uitls/bingPage.ts b/utils/bingPage.ts similarity index 97% rename from uitls/bingPage.ts rename to utils/bingPage.ts index c403c9f..fb36c13 100644 --- a/uitls/bingPage.ts +++ b/utils/bingPage.ts @@ -51,7 +51,6 @@ export class Page { const groups_roots = Page.getQAsElement(); for (const groups_root of groups_roots) { for (let group of groups_root.shadowRoot.querySelectorAll("cib-message-group")) { - console.log(`setFontWeightForAllRefs: ${group}`); // check user or bot if (group.getAttribute("source") === "bot") { // Answer @@ -90,6 +89,7 @@ export class Page { }); }; + static getQAsJSON = () => { const qas = new QAList(Page.getQAsElement()); return qas.qaList; @@ -158,7 +158,7 @@ export class QAList { // Question, source is user const html = group.shadowRoot.querySelector("cib-message") .shadowRoot.querySelector("div.content"); - QA.questions.push({ text: html.textContent }); + QA.questions.push({ text: html.textContent?.trim() }); } } return QA; diff --git a/utils/constants.ts b/utils/constants.ts new file mode 100644 index 0000000..f5fa6f2 --- /dev/null +++ b/utils/constants.ts @@ -0,0 +1,23 @@ +export const PNG = "PNG导出"; +export const JPG = "JPG"; +export const MD = "Markdown"; +export const JSON_STR = "JSON"; + +export const exportTypes = { + PNG, + JPG, + MD, + JSON: JSON_STR, +} + +export const exportActions = { + ALL: 'Export All', + PREVIEW: 'Preview', +} + + +export const Settings = { + WELCOME: "welcome-settings", + FEEDBACK: "feedback-settings", + EXPORT: "export-settings", +} diff --git a/uitls/extractHTML.js b/utils/extractHTML.js similarity index 100% rename from uitls/extractHTML.js rename to utils/extractHTML.js diff --git a/uitls/md/parser.ts b/utils/md/parser.ts similarity index 97% rename from uitls/md/parser.ts rename to utils/md/parser.ts index 6abbfb1..0f80c33 100644 --- a/uitls/md/parser.ts +++ b/utils/md/parser.ts @@ -1,4 +1,4 @@ -import { getNowWithFormat } from "~uitls/time"; +import { getNowWithFormat } from "~utils/time"; export {}; diff --git a/utils/store/chrome.ts b/utils/store/chrome.ts new file mode 100644 index 0000000..b04ccf4 --- /dev/null +++ b/utils/store/chrome.ts @@ -0,0 +1,34 @@ +import { writable } from "svelte/store"; + + +export const chromeSyncStorage = (key: string, initial: T) => { + const { subscribe, set, update } = writable(initial); + + return { + subscribe, + set: (value: T) => { + chromeSyncSet(key, value) + return set(value); + }, + update, + init: async () => { + const result = await chromeSyncGet(key); + if (result === null || result === undefined) { + await chromeSyncSet(key, initial); + } + const saved = await chromeSyncGet(key); + set(saved); + } + }; +}; + +export const chromeSyncGet = async (key: string) => { + const obj = await chrome.storage.sync.get([key]); + return obj[key]; +}; + +export const chromeSyncSet = async (key: string, value: T) => { + const setObj = {}; + setObj[key] = value; + await chrome.storage.sync.set(setObj); +}; diff --git a/utils/store/localStore.ts b/utils/store/localStore.ts new file mode 100644 index 0000000..595f6e7 --- /dev/null +++ b/utils/store/localStore.ts @@ -0,0 +1,25 @@ +import { writable } from 'svelte/store' +import type { JsonValue } from '~utils/types/json.type' + +// from: https://svelte.dev/repl/d78d7327830442ab87cc47bcee1033f9?version=3.43.1 +export const localStore = (key: string, initial: T) => { // receives the key of the local storage and an initial value + const toString = (value: T) => JSON.stringify(value, null, 2) // helper function + const toObj = JSON.parse // helper function + + if (localStorage.getItem(key) === null) { // item not present in local storage + localStorage.setItem(key, toString(initial)) // initialize local storage with initial value + } + + const saved = toObj(localStorage.getItem(key)) // convert to object + + const { subscribe, set, update } = writable(saved) // create the underlying writable store + + return { + subscribe, + set: (value: T) => { + localStorage.setItem(key, toString(value)) // save also to local storage as a string + return set(value) + }, + update + } +} diff --git a/utils/store/stores.ts b/utils/store/stores.ts new file mode 100644 index 0000000..a87fe58 --- /dev/null +++ b/utils/store/stores.ts @@ -0,0 +1,20 @@ +import { exportTypes, Settings } from "~utils/constants"; +// import { localStore } from "~utils/store/localStore"; +import { chromeSyncStorage } from "~utils/store/chrome"; + +const defaultSize = 300; + +const defaultNewExportSetting = { on: true, size: defaultSize, type: exportTypes.PNG }; +const defaultExportSettings = [ + { id: 1, ...defaultNewExportSetting}, + { id: 2, on: false, size: defaultSize, type: exportTypes.JPG }, + { id: 3, on: false, size: defaultSize, type: exportTypes.MD }, + { id: 4, on: false, size: defaultSize, type: exportTypes.JSON } +]; + + +// export const exportSettings = localStore("export-settings", defaultExportSettings); +export const exportSettings = chromeSyncStorage(Settings.EXPORT, defaultExportSettings); + +export const welcomeHiddenSetting = chromeSyncStorage(Settings.WELCOME, true); +export const feedbackHiddenSetting = chromeSyncStorage(Settings.FEEDBACK, false); diff --git a/uitls/time.ts b/utils/time.ts similarity index 100% rename from uitls/time.ts rename to utils/time.ts diff --git a/utils/types/json.type.ts b/utils/types/json.type.ts new file mode 100644 index 0000000..60b1ef5 --- /dev/null +++ b/utils/types/json.type.ts @@ -0,0 +1 @@ +export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } diff --git a/utils/viewmodel.ts b/utils/viewmodel.ts new file mode 100644 index 0000000..ef29556 --- /dev/null +++ b/utils/viewmodel.ts @@ -0,0 +1,27 @@ +/** + * Used for connecting UI script and `chrome.storage` api + */ +import { chromeSyncGet } from "~utils/store/chrome"; +import { Settings } from "~utils/constants"; + + +export const handleElementVisibility = async (elem: HTMLElement, key, prevDisplay='flex') => { + const setElementVisibility = async (elem, key) => { + const isHidden = await chromeSyncGet(key); + if (isHidden) { + elem.style.display = 'none'; + } else { + elem.style.display = prevDisplay; + } + } + await setElementVisibility(elem, key); + chrome.storage.onChanged.addListener(async (changes, namespace) => { + if (key in changes) { + await setElementVisibility(elem, key); + } + }); +} + +export const handleExportSetting = async () => { + return await chromeSyncGet(Settings.EXPORT); +} diff --git a/uitls/visitor.ts b/utils/visitor.ts similarity index 55% rename from uitls/visitor.ts rename to utils/visitor.ts index 55f97b3..afa317d 100644 --- a/uitls/visitor.ts +++ b/utils/visitor.ts @@ -1,19 +1,21 @@ import { Page } from "./bingPage"; import { domToJpeg, domToPng } from "modern-screenshot"; -import { QasJSON2MarkdownParser } from "~uitls/md/parser"; +import { QasJSON2MarkdownParser } from "~utils/md/parser"; +import { handleExportSetting } from "~utils/viewmodel"; +import { exportTypes } from "~utils/constants"; export class DownloadVisitor { static async forImage(func, type, way = "newTab") { const main = Page.getMain(); - // 处理链接分行的问题 Page.setFontWeightForAllRefs(); - const dataURL = await func(main, { - backgroundColor: "rgb(217, 230, 249)" + backgroundColor: "rgb(217, 230, 249)", + // filter: (node: HTMLElement) => { + // return node.tagName?.toLowerCase() !== 'cib-welcome-container'; + // } }); - if (way === "newTab") { requestAnimationFrame(() => { const binaryData = atob(dataURL.split("base64,")[1]); @@ -38,14 +40,15 @@ export class DownloadVisitor { } static forPNG = async () => { - await DownloadVisitor.forImage(domToPng, "png"); + await DownloadVisitor.forImage(domToPng, "png", "download"); }; static forJPG = async () => { - await DownloadVisitor.forImage(domToJpeg, "jpeg"); + await DownloadVisitor.forImage(domToJpeg, "jpeg", "download"); }; static forMD = () => { + // TODO: 可以将下载重构 const jsonResult = Page.getQAsJSON(); const qasp = new QasJSON2MarkdownParser(jsonResult); const [md, title] = qasp.md(); @@ -69,4 +72,56 @@ export class DownloadVisitor { downloadAnchorNode.click(); downloadAnchorNode.remove(); }; + /** + * demo input : + * [ + * { + * "id": 1, + * "on": true, + * "size": 300, + * "type": "PNG导出" + * }, + * { + * "id": 2, + * "on": false, + * "size": 300, + * "type": "JPG" + * }, + * { + * "id": 3, + * "on": false, + * "size": 300, + * "type": "Markdown" + * }, + * { + * "id": 4, + * "on": false, + * "size": 300, + * "type": "JSON" + * } + * ] + */ + static forAll = async () => { + const resultJson = await handleExportSetting(); + for (let item of resultJson) { + if (item.on) { + const type = item.type; + if (type === exportTypes.PNG) { + await DownloadVisitor.forPNG(); + } else if (type === exportTypes.JPG) { + await DownloadVisitor.forJPG(); + } else if (type === exportTypes.MD) { + await DownloadVisitor.forMD(); + } else if (type === exportTypes.JSON) { + await DownloadVisitor.forJSON(); + } else { + throw Error(`Not type of ${type}`); + } + } + } + }; + + static forPreview = async () => { + await DownloadVisitor.forImage(domToPng, "png"); + }; }