diff --git a/src/composables/useWebExtensionStorage.ts b/src/composables/useWebExtensionStorage.ts index 8cdb40f..d940ad2 100644 --- a/src/composables/useWebExtensionStorage.ts +++ b/src/composables/useWebExtensionStorage.ts @@ -139,7 +139,7 @@ export function useWebExtensionStorage( }); } - pullFromStorage(); // Init + void pullFromStorage(); // Init return data; } diff --git a/src/options/Options.vue b/src/options/Options.vue index b2cc050..ec8af2f 100644 --- a/src/options/Options.vue +++ b/src/options/Options.vue @@ -27,6 +27,7 @@ watch(opt, (val) => { site.value = 'amazon'; break; case '/homedepot': + case '/homedepot-reviews': site.value = 'homedepot'; break; default: diff --git a/src/page-worker/composables/amazon.ts b/src/page-worker/composables/amazon.ts index e3abe16..f9d2527 100644 --- a/src/page-worker/composables/amazon.ts +++ b/src/page-worker/composables/amazon.ts @@ -142,9 +142,7 @@ function buildAmazonPageWorker(): WorkerComposable { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; + return () => unsubscribes.forEach((unsubscribe) => unsubscribe()); } // Register event handlers on mount diff --git a/src/page-worker/composables/homedepot.ts b/src/page-worker/composables/homedepot.ts index 5e58c2e..ec90832 100644 --- a/src/page-worker/composables/homedepot.ts +++ b/src/page-worker/composables/homedepot.ts @@ -37,9 +37,7 @@ function buildHomedepotWorker(): WorkerComposable { - unsubscribes.forEach((unsubscribe) => unsubscribe()); - }; + return () => unsubscribes.forEach((unsubscribe) => unsubscribe()); } const unsubscribe = registerWorkerEvents(); diff --git a/src/page-worker/composables/lowes.ts b/src/page-worker/composables/lowes.ts new file mode 100644 index 0000000..a0c2e29 --- /dev/null +++ b/src/page-worker/composables/lowes.ts @@ -0,0 +1,94 @@ +import { createGlobalState } from '@vueuse/core'; +import lowes from '../impls/lowes'; +import { useLongTask } from '~/composables/useLongTask'; +import { detailItems } from '~/storages/lowes'; +import { taskOptionBase, WorkerComposable } from '../interfaces/common'; +import { LowesWorker } from '../interfaces/lowes'; + +export interface LowesWorkerSettings { + objects?: 'detail'[]; + commitChangeIngerval?: number; +} + +function buildLowesWorkerComposable(): WorkerComposable { + const settings = shallowRef({}); + const worker = lowes.getLowesWorker(); + const { isRunning, startTask } = useLongTask(); + + const detailCache = new Map(); + + function registerWorkerEvent() { + const unsubscribes = [ + worker.on('error', () => worker.stop()), + worker.on('detail-item-collected', (ev) => { + const { item } = ev; + if (detailCache.has(item.link)) { + const origin = detailCache.get(item.link); + detailCache.set(item.link, { ...origin, ...item }); + } else { + detailCache.set(item.link, item); + } + }), + ]; + return () => unsubscribes.forEach((unsbuscribe) => unsbuscribe()); + } + + const unsbuscribe = registerWorkerEvent(); + + onUnmounted(() => { + unsbuscribe(); + }); + + const commitChange = async () => { + const { objects } = settings.value; + if (objects?.includes('detail')) { + for (const [k, v] of detailCache.entries()) { + if (detailItems.value.has(k)) { + const origin = detailItems.value.get(k)!; + detailItems.value.set(k, { ...origin, ...v }); + } else { + detailItems.value.set(k, v); + } + } + detailCache.clear(); + } + }; + + const taskWrapper2 = Promise>( + func: T, + ) => { + return (...params: Parameters) => + startTask(async () => { + if (!params?.[1]) { + params[1] = {}; + } + const progressReporter = params[1].progress; + if (progressReporter) { + params[1].progress = async (...p: Parameters) => { + await commitChange(); + return progressReporter(...p); + }; + } else { + params[1].progress = async () => { + await commitChange(); + }; + } + await func(params[0], params[1]); + await commitChange(); + }); + }; + + const runDetailPageTask = taskWrapper2(worker.runDetailPageTask.bind(worker)); + + return { + settings, + isRunning, + runDetailPageTask, + on: worker.on.bind(worker), + off: worker.off.bind(worker), + once: worker.once.bind(worker), + stop: worker.stop.bind(worker), + }; +} + +export const useLowesWorker = createGlobalState(buildLowesWorkerComposable); diff --git a/src/page-worker/impls/lowes.ts b/src/page-worker/impls/lowes.ts index 07e4b68..42c4f05 100644 --- a/src/page-worker/impls/lowes.ts +++ b/src/page-worker/impls/lowes.ts @@ -1,8 +1,12 @@ import { taskOptionBase } from '../interfaces/common'; import { BaseWorker } from './base'; import { LowesEvents, LowesWorker } from '../interfaces/lowes'; +import { LowesDetailPageInjector } from '../web-injectors/lowes'; -class LowesWorkerImpl extends BaseWorker implements LowesWorker { +class LowesWorkerImpl + extends BaseWorker + implements LowesWorker +{ private static instance: LowesWorker | null = null; public static getInstance() { if (!this.instance) { @@ -14,12 +18,27 @@ class LowesWorkerImpl extends BaseWorker implements LowesWorker { super(); } - runDetailPageTask(urls: string[], options?: taskOptionBase): Promise { - throw new Error('Method not implemented.'); + async runDetailPageTask(urls: string[], options: taskOptionBase = {}): Promise { + const { progress } = options; + let interrupt = false; + const remains = [...urls]; + this.on('interrupt', () => { + interrupt = true; + }); + while (remains.length > 0 && !interrupt) { + const url = remains.shift()!; + const tab = await browser.tabs.create({ url }); + const injector = new LowesDetailPageInjector(tab); + await injector.waitForPageLoad(); + const baseInfo = await injector.getBaseInfo(); + await this.emit('detail-item-collected', { item: { ...baseInfo, link: url } }); + progress && progress(remains); + setTimeout(() => browser.tabs.remove(tab.id!), 1500); + } } stop(): Promise { - throw new Error('Method not implemented.'); + return this.emit('interrupt'); } } diff --git a/src/page-worker/index.ts b/src/page-worker/index.ts index 48922f6..c4e78bb 100644 --- a/src/page-worker/index.ts +++ b/src/page-worker/index.ts @@ -1,5 +1,6 @@ import { AmazonPageWorkerSettings, useAmazonWorker } from './composables/amazon'; import { HomedepotWorkerSettings, useHomedepotWorker } from './composables/homedepot'; +import { LowesWorkerSettings, useLowesWorker } from './composables/lowes'; export function usePageWorker( type: 'amazon', @@ -9,6 +10,10 @@ export function usePageWorker( type: 'homedepot', settings?: HomedepotWorkerSettings, ): ReturnType; +export function usePageWorker( + type: 'homedepot', + settings?: LowesWorkerSettings, +): ReturnType; export function usePageWorker(type: Website, settings: any) { let worker = null; switch (type) { @@ -18,6 +23,9 @@ export function usePageWorker(type: Website, settings: any) { case 'homedepot': worker = useHomedepotWorker(); break; + case 'lowes': + worker = useLowesWorker(); + break; default: throw new Error(`Unsupported page worker type: ${type}`); } diff --git a/src/page-worker/interfaces/lowes.ts b/src/page-worker/interfaces/lowes.ts index d7a8f67..d1165e4 100644 --- a/src/page-worker/interfaces/lowes.ts +++ b/src/page-worker/interfaces/lowes.ts @@ -8,7 +8,7 @@ export interface LowesEvents { ['error']: { message: string; url?: string }; } -export interface LowesWorker { +export interface LowesWorker extends Listener { /** * Browsing item detail page and collect target information */ diff --git a/src/page-worker/web-injectors/homedepot.ts b/src/page-worker/web-injectors/homedepot.ts index a972f22..8a2e4a6 100644 --- a/src/page-worker/web-injectors/homedepot.ts +++ b/src/page-worker/web-injectors/homedepot.ts @@ -114,7 +114,7 @@ export class HomedepotDetailPageInjector extends BaseInjector { 'script#thd-helmet__script--productStructureData', )!.innerText; const obj = JSON.parse(text); - return (obj['image'] as string[]).map((url) => url.slice(1, -1)); + return obj['image'] as string[]; }); } @@ -157,7 +157,9 @@ export class HomedepotDetailPageInjector extends BaseInjector { .filter((t) => t.length !== 0); const imageUrls = Array.from( root.querySelectorAll('.media-carousel__media > button'), - ).map((el) => el.style.backgroundImage.split(/[\(\)]/, 3)[1]); + ) + .map((el) => el.style.backgroundImage.split(/[\(\)]/, 3)[1]) + .map((url) => url.slice(1, -1)); return { title, content, username, dateInfo, rating, badges, imageUrls } as HomedepotReview; }); }); diff --git a/src/page-worker/web-injectors/lowes.ts b/src/page-worker/web-injectors/lowes.ts index 0198d52..c5162af 100644 --- a/src/page-worker/web-injectors/lowes.ts +++ b/src/page-worker/web-injectors/lowes.ts @@ -49,12 +49,12 @@ export class LowesDetailPageInjector extends BaseInjector { // 获取标题 const title = document.querySelector( `h1.product-brand-description`, - )?.innerText; + )!.innerText; // 获取价格 const price = document - .querySelector(`.screen-reader`) - ?.innerText.replaceAll('\n', ''); + .querySelector(`.screen-reader`)! + .innerText.replaceAll('\n', ''); // 获取评分 const rate = document.querySelector(`.avgrating`)?.innerText; @@ -68,7 +68,7 @@ export class LowesDetailPageInjector extends BaseInjector { // 获取图片URL const mainImageUrl = document.querySelector( `#mfe-gallery .productImage.tile-img`, - )?.src; + )!.src; return { brandName, diff --git a/src/sidepanel/views/LowesSidepanel.vue b/src/sidepanel/views/LowesSidepanel.vue new file mode 100644 index 0000000..4b877a4 --- /dev/null +++ b/src/sidepanel/views/LowesSidepanel.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/storages/lowes.ts b/src/storages/lowes.ts new file mode 100644 index 0000000..e5b97b0 --- /dev/null +++ b/src/storages/lowes.ts @@ -0,0 +1,6 @@ +import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage'; + +export const detailItems = useWebExtensionStorage>( + 'lowes-details', + new Map(), +); diff --git a/src/types/lowes.d.ts b/src/types/lowes.d.ts index 1cfecd2..9cacdcc 100644 --- a/src/types/lowes.d.ts +++ b/src/types/lowes.d.ts @@ -1,11 +1,9 @@ declare type LowesDetailItem = { - OSMID: string; link: string; brandName?: string; title: string; price: string; rate?: string; - innerText: string; reviewCount?: number; mainImageUrl: string; modelInfo?: string; diff --git a/src/types/misc.ts b/src/types/misc.ts index 3123b5d..153dfad 100644 --- a/src/types/misc.ts +++ b/src/types/misc.ts @@ -19,7 +19,7 @@ declare module '*.md' { declare type AppContext = 'options' | 'sidepanel' | 'background' | 'content script'; -declare type Website = 'amazon' | 'homedepot'; +declare type Website = 'amazon' | 'homedepot' | 'lowes'; declare const appContext: AppContext;