diff --git a/package.json b/package.json index e8db2f1..1b92d4d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "esno": "^4.8.0", "exceljs": "^4.4.0", "fs-extra": "^11.2.0", + "html-to-image": "^1.11.13", "husky": "^9.1.7", "jsdom": "^26.0.0", "kolorist": "^1.8.0", @@ -65,8 +66,8 @@ "vue-demi": "^0.14.10", "vue-router": "^4.5.1", "web-ext": "^8.5.0", - "webext-bridge": "^6.0.1", - "webextension-polyfill": "^0.12.0" + "webextension-polyfill": "^0.12.0", + "webext-bridge": "link:webext-bridge" }, "lint-staged": { "**/*": "prettier --write --ignore-unknown" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8025534..ee07671 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: fs-extra: specifier: ^11.2.0 version: 11.2.0 + html-to-image: + specifier: ^1.11.13 + version: 1.11.13 husky: specifier: ^9.1.7 version: 9.1.7 @@ -116,8 +119,8 @@ importers: specifier: ^8.5.0 version: 8.5.0 webext-bridge: - specifier: ^6.0.1 - version: 6.0.1 + specifier: link:webext-bridge + version: link:webext-bridge webextension-polyfill: specifier: ^0.12.0 version: 0.12.0 @@ -1373,12 +1376,6 @@ packages: integrity: sha512-xPTFWwQ8BxPevPF2IKsf4hpZNss4LxaOLZXypQH4E63BDLmcwX/RMGdI4tB4VO4Nb6xDBH3F/p4gz4wvof1o9w==, } - '@types/webextension-polyfill@0.8.3': - resolution: - { - integrity: sha512-GN+Hjzy9mXjWoXKmaicTegv3FJ0WFZ3aYz77Wk8TMp1IY3vEzvzj1vnsa0ggV7vMI1i+PUxe4qqnIJKCzf9aTg==, - } - '@types/yauzl@2.10.3': resolution: { @@ -3456,6 +3453,12 @@ packages: } engines: { node: '>=18' } + html-to-image@1.11.13: + resolution: + { + integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==, + } + htmlparser2@8.0.2: resolution: { @@ -4496,13 +4499,6 @@ packages: peerDependencies: vue: ^3.0.0 - nanoevents@6.0.2: - resolution: - { - integrity: sha512-FRS2otuFcPPYDPYViNWQ42+1iZqbXydinkRHTHFxrF4a1CpBfmydR9zkI44WSXAXCyPrkcGtPk5CnpW6Y3lFKQ==, - } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } - nanoid@3.3.11: resolution: { @@ -5567,13 +5563,6 @@ packages: engines: { node: '>=10' } hasBin: true - serialize-error@9.1.1: - resolution: - { - integrity: sha512-6uZQLGyUkNA4N+Zii9fYukmNu9PEA1F5rqcwXzN/3LtBjwl2dFBbVZ1Zyn08/CGkB4H440PIemdOQBt1Wvjbrg==, - } - engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } - set-function-length@1.2.2: resolution: { @@ -6032,12 +6021,6 @@ packages: integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==, } - tiny-uid@1.1.2: - resolution: - { - integrity: sha512-0beRFXR+fv4C40ND2PqgNjq6iyB1dKXciKJjslLw0kPYCcR82aNd2b+Tt2yy06LimIlvtoehgvrm/fUZCutSfg==, - } - tinybench@2.9.0: resolution: { @@ -6166,13 +6149,6 @@ packages: } engines: { node: '>=10' } - type-fest@2.19.0: - resolution: - { - integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==, - } - engines: { node: '>=12.20' } - type-fest@3.13.1: resolution: { @@ -6573,24 +6549,12 @@ packages: engines: { node: '>=18.0.0', npm: '>=8.0.0' } hasBin: true - webext-bridge@6.0.1: - resolution: - { - integrity: sha512-GruIrN+vNwbxVCi8UW4Dqk5YkcGA9V0ZfJ57jXP9JXHbrsDs5k2N6NNYQR5e+wSCnQpGYOGAGihwUpKlhg8QIw==, - } - webextension-polyfill@0.12.0: resolution: { integrity: sha512-97TBmpoWJEE+3nFBQ4VocyCdLKfw54rFaJ6EVQYLBCXqCIpLSZkwGgASpv4oPt9gdKCJ80RJlcmNzNn008Ag6Q==, } - webextension-polyfill@0.9.0: - resolution: - { - integrity: sha512-LTtHb0yR49xa9irkstDxba4GATDAcDw3ncnFH9RImoFwDlW47U95ME5sn5IiQX2ghfaECaf6xyXM8yvClIBkkw==, - } - webidl-conversions@7.0.0: resolution: { @@ -7522,8 +7486,6 @@ snapshots: '@types/webextension-polyfill@0.12.1': {} - '@types/webextension-polyfill@0.8.3': {} - '@types/yauzl@2.10.3': dependencies: '@types/node': 22.10.5 @@ -8962,6 +8924,8 @@ snapshots: dependencies: whatwg-encoding: 3.1.1 + html-to-image@1.11.13: {} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -9526,8 +9490,6 @@ snapshots: vue: 3.5.13(typescript@5.8.2) vueuc: 0.4.64(vue@3.5.13(typescript@5.8.2)) - nanoevents@6.0.2: {} - nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -10143,10 +10105,6 @@ snapshots: semver@7.7.1: {} - serialize-error@9.1.1: - dependencies: - type-fest: 2.19.0 - set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -10416,8 +10374,6 @@ snapshots: through@2.3.8: {} - tiny-uid@1.1.2: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -10474,8 +10430,6 @@ snapshots: type-fest@0.20.2: {} - type-fest@2.19.0: {} - type-fest@3.13.1: {} type-fest@4.32.0: {} @@ -10813,18 +10767,8 @@ snapshots: - supports-color - utf-8-validate - webext-bridge@6.0.1: - dependencies: - '@types/webextension-polyfill': 0.8.3 - nanoevents: 6.0.2 - serialize-error: 9.1.1 - tiny-uid: 1.1.2 - webextension-polyfill: 0.9.0 - webextension-polyfill@0.12.0: {} - webextension-polyfill@0.9.0: {} - webidl-conversions@7.0.0: {} webpack-virtual-modules@0.6.2: {} diff --git a/shim.d.ts b/shim.d.ts index e96ad6d..df546da 100644 --- a/shim.d.ts +++ b/shim.d.ts @@ -4,6 +4,17 @@ declare module 'webext-bridge' { export interface ProtocolMap { // define message protocol types // see https://github.com/antfu/webext-bridge#type-safe-protocols + 'html-to-image': ProtocolWithReturn< + | { + type: 'CSS'; + selector: string; + } + | { + type: 'XPath'; + xpath: string; + }, + { b64: string } + >; } } diff --git a/src/background/main.ts b/src/background/main.ts index f6c8f4a..271f8ed 100644 --- a/src/background/main.ts +++ b/src/background/main.ts @@ -1,3 +1,6 @@ +// https://github.com/serversideup/webext-bridge/issues/67#issuecomment-2676094094 +import('webext-bridge/background'); + // only on dev mode if (import.meta.hot) { // @ts-expect-error for background HMR diff --git a/src/components/IdsInput.vue b/src/components/IdsInput.vue index 2e08afc..6d13ed5 100644 --- a/src/components/IdsInput.vue +++ b/src/components/IdsInput.vue @@ -24,7 +24,7 @@ const message = useMessage(); const formItemRef = useTemplateRef('detail-form-item'); const formItemRule: FormItemRule = { required: true, - trigger: ['submit', 'blur'], + trigger: ['submit'], message: props.validateMessage, validator: () => { return props.matchPattern.exec(modelValue.value) !== null; diff --git a/src/contentScripts/html-to-image.ts b/src/contentScripts/html-to-image.ts new file mode 100644 index 0000000..6f49bc5 --- /dev/null +++ b/src/contentScripts/html-to-image.ts @@ -0,0 +1,13 @@ +import { toPng } from 'html-to-image'; +import { onMessage } from 'webext-bridge/content-script'; + +onMessage('html-to-image', async (ev) => { + const params = ev.data; + const targetNode = + params.type == 'CSS' + ? document.querySelector(params.selector)! + : (document.evaluate(params.xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE) + .singleNodeValue as HTMLElement); + const imgData = await toPng(targetNode); + return { b64: imgData }; +}); diff --git a/src/contentScripts/index.ts b/src/contentScripts/index.ts index a90fd2b..a757005 100644 --- a/src/contentScripts/index.ts +++ b/src/contentScripts/index.ts @@ -1,2 +1,4 @@ // Firefox `browser.tabs.executeScript()` requires scripts return a primitive value -(() => {})(); +(() => { + import('./html-to-image'); +})(); diff --git a/src/logic/page-worker/amazon.ts b/src/logic/page-worker/amazon.ts index 7aa4348..02d5db1 100644 --- a/src/logic/page-worker/amazon.ts +++ b/src/logic/page-worker/amazon.ts @@ -43,21 +43,16 @@ class AmazonPageWorkerImpl implements AmazonPageWorker { private async wanderSearchSinglePage(tab: Tabs.Tab) { const injector = new AmazonSearchPageInjector(tab); - // #region Wait for the Next button to appear, indicating that the product items have finished loading + // Wait for the Next button to appear, indicating that the product items have finished loading await injector.waitForPageLoaded(); - // #endregion - // #region Determine the type of product search page https://github.com/primedigitaltech/azon_seeker/issues/1 + // Determine the type of product search page https://github.com/primedigitaltech/azon_seeker/issues/1 const pagePattern = await injector.getPagePattern(); - // #endregion - // #region Retrieve key nodes and their information from the critical product search page + // Retrieve key nodes and their information from the critical product search page const data = await injector.getPageData(pagePattern); - // #endregion - // #region get current page + // get current page const page = await injector.getCurrentPage(); - // #endregion - // #region Determine if it is the last page, otherwise navigate to the next page + // Determine if it is the last page, otherwise navigate to the next page const hasNextPage = await injector.determineHasNextPage(); - // #endregion await new Promise((resolve) => setTimeout(resolve, 1000)); if (data === null || typeof hasNextPage !== 'boolean') { this.channel.emit('error', { message: '爬取单页信息失败', url: tab.url }); @@ -135,21 +130,13 @@ class AmazonPageWorkerImpl implements AmazonPageWorker { //#endregion //#region Fetch Base Info const baseInfo = await injector.getBaseInfo(); + const ratingInfo = await injector.getRatingInfo(); this.channel.emit('item-base-info-collected', { asin: params.asin, - title: baseInfo.title, - price: baseInfo.price, + ...baseInfo, + ...ratingInfo, }); //#endregion - //#region Fetch Rating Info - const ratingInfo = await injector.getRatingInfo(); - if (ratingInfo && (ratingInfo.rating !== 0 || ratingInfo.ratingCount !== 0)) { - this.channel.emit('item-rating-collected', { - asin: params.asin, - ...ratingInfo, - }); - } - //#endregion //#region Fetch Category Rank Info let rawRankingText: string | null = await injector.getRankText(); if (rawRankingText) { @@ -194,6 +181,9 @@ class AmazonPageWorkerImpl implements AmazonPageWorker { // topReviews: reviews, // }); //#endregion + // #region Get APlus Sreen shot + await injector.scanAPlus(); + // #endregion } @withErrorHandling diff --git a/src/logic/page-worker/types.d.ts b/src/logic/page-worker/types.d.ts index 5045267..a41bc0e 100644 --- a/src/logic/page-worker/types.d.ts +++ b/src/logic/page-worker/types.d.ts @@ -47,11 +47,10 @@ interface AmazonPageWorkerEvents { /** * The event is fired when worker collected goods' base info on the Amazon detail page. */ - ['item-base-info-collected']: Pick; - /** - * The event is fired when worker collected goods' rating on the Amazon detail page. - */ - ['item-rating-collected']: Pick; + ['item-base-info-collected']: Pick< + AmazonDetailItem, + 'asin' | 'title' | 'price' | 'rating' | 'ratingCount' + >; /** * The event is fired when worker */ diff --git a/src/logic/web-injectors/amazon.ts b/src/logic/web-injectors/amazon.ts index 6f5d1a0..005ccc3 100644 --- a/src/logic/web-injectors/amazon.ts +++ b/src/logic/web-injectors/amazon.ts @@ -299,6 +299,24 @@ export class AmazonDetailPageInjector extends BaseInjector { return items; }); } + + public async scanAPlus() { + return this.run(async () => { + const aplusEl = document.querySelector('#aplus')!; + while (aplusEl.getClientRects().length === 0) { + await new Promise((resolve) => setTimeout(resolve, 500)); + } + aplusEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); + while (true) { + const rect = aplusEl.getClientRects()[0]; + if (rect.top + rect.height < 100) { + break; + } + window.scrollBy({ top: 100, behavior: 'smooth' }); + await new Promise((resolve) => setTimeout(resolve, 100 + ~~(100 * Math.random()))); + } + }); + } } export class AmazonReviewPageInjector extends BaseInjector { diff --git a/src/options/views/AmazonResultTable.vue b/src/options/views/AmazonResultTable.vue index b55f5f9..c9ede7c 100644 --- a/src/options/views/AmazonResultTable.vue +++ b/src/options/views/AmazonResultTable.vue @@ -369,6 +369,10 @@ const handleClearData = async () => {