Adjust Config & Update wanderDetailPage

This commit is contained in:
johnathan 2025-04-23 18:06:57 +08:00
parent 407ebaaa75
commit 063aca6b94
18 changed files with 77 additions and 157 deletions

2
.vscode/launch.json vendored
View File

@ -10,7 +10,7 @@
"name": "Attach to side panel",
"webRoot": "${workspaceFolder}/src/",
"port": 9222,
"urlFilter": "chrome-extension://*"
"urlFilter": "chrome-extension://fmalpbpehdilmjhnanhpjmnkgbahopfj/*"
}
]
}

View File

@ -8,17 +8,14 @@ import { isDev, log, port, r } from './utils';
* Stub index.html to use Vite in development
*/
async function stubIndexHtml() {
const views = ['options', 'popup', 'sidepanel'];
const views = ['sidepanel'];
for (const view of views) {
await fs.ensureDir(r(`extension/dist/${view}`));
let data = await fs.readFile(r(`src/${view}/index.html`), 'utf-8');
data = data
.replace('"./main.ts"', `"http://localhost:${port}/${view}/main.ts"`)
.replace(
'<div id="app"></div>',
'<div id="app">Vite server did not start</div>',
);
.replace('<div id="app"></div>', '<div id="app">Vite server did not start</div>');
await fs.writeFile(r(`extension/dist/${view}/index.html`), data, 'utf-8');
log('PRE', `stub ${view}`);
}

1
shim.d.ts vendored
View File

@ -4,6 +4,5 @@ declare module 'webext-bridge' {
export interface ProtocolMap {
// define message protocol types
// see https://github.com/antfu/webext-bridge#type-safe-protocols
'tab-update': { tabId: number };
}
}

View File

@ -2,23 +2,20 @@
import App from './views/App.vue';
import { setupApp } from '~/logic/common-setup';
// 是否挂在ContentScript Vue APP
// 是否在ContentScript挂载Vue APP
const MOUNT_COMPONENT = false;
/**
* mount component to context window
*/
const mountComponent = () => {
// mount component to context window
const container = document.createElement('div');
container.id = __NAME__;
const root = document.createElement('div');
const styleEl = document.createElement('link');
const shadowDOM =
container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) ||
container;
const shadowDOM = container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container;
styleEl.setAttribute('rel', 'stylesheet');
styleEl.setAttribute(
'href',
browser.runtime.getURL('dist/contentScripts/index.css'),
);
styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/index.css'));
shadowDOM.appendChild(styleEl);
shadowDOM.appendChild(root);
document.body.appendChild(container);
@ -33,4 +30,3 @@ const mountComponent = () => {
mountComponent();
}
})();

View File

@ -1,5 +1,9 @@
import type { App } from 'vue';
/**
* Setup Vue app
* @param app Vue app
*/
export function setupApp(app: App) {
// Inject a globally available `$app` object in template
app.config.globalProperties.$app = {

View File

@ -1,13 +1,41 @@
/**
* Execute Script on Document
* @param tabId
* @param func
* @returns
* Executes a provided asynchronous function in the context of a specific browser tab.
* @param tabId - The ID of the browser tab where the script will be executed.
* @param func - The asynchronous function to execute in the tab's context. This function
* should be serializable and must not rely on external closures.
* @param payload - An optional payload object to pass as an argument to the executed function.
*
* @returns A promise that resolves to the result of the executed function, or `null` if an error occurs.
*
* @throws This function does not throw directly but logs an error to the console if the script injection fails.
*
* @example
* ```typescript
* const result = await exec<number, { value: number }>(
* tabId,
* async (payload) => {
* return payload?.value ?? 0;
* },
* { value: 42 }
* );
* console.log(result); // Outputs: 42
* ```
*/
export async function exec<T>(tabId: number, func: () => Promise<T>): Promise<T | null> {
export async function exec<T>(tabId: number, func: () => Promise<T>): Promise<T | null>;
export async function exec<T, P extends Record<string, unknown>>(
tabId: number,
func: (payload: P) => Promise<T>,
payload: P,
): Promise<T | null>;
export async function exec<T, P extends Record<string, unknown>>(
tabId: number,
func: (payload?: P) => Promise<T>,
payload?: P,
): Promise<T | null> {
const injectResults = await browser.scripting.executeScript({
target: { tabId },
func,
args: payload ? [payload] : undefined,
});
const ret = injectResults.pop();
if (ret?.error) {

View File

@ -1 +0,0 @@

View File

@ -1,6 +1,6 @@
import Emittery from 'emittery';
import type { AmazonGoodsLinkItem, AmazonPageWorker, AmazonPageWorkerEvents } from './types';
import Browser from 'webextension-polyfill';
import type { Tabs } from 'webextension-polyfill';
import { exec } from '../execute-script';
/**
@ -19,7 +19,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
readonly channel = new Emittery<AmazonPageWorkerEvents>();
private async getCurrentTab(): Promise<Browser.Tabs.Tab> {
private async getCurrentTab(): Promise<Tabs.Tab> {
const tab = await browser.tabs
.query({ active: true, currentWindow: true })
.then((tabs) => tabs[0]);
@ -41,7 +41,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
return url.toString();
}
private async wanderSearchSinglePage(tab: Browser.Tabs.Tab) {
private async wanderSearchSinglePage(tab: Tabs.Tab) {
const tabId = tab.id!;
// #region Wait for the Next button to appear, indicating that the product items have finished loading
await exec(tabId, async () => {
@ -134,7 +134,7 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
return { data, hasNextPage };
}
public async wanderSearchList(): Promise<void> {
public async wanderSearchPage(): Promise<void> {
const tab = await this.getCurrentTab();
let stopSignal = false;
const stop = async (_: unknown): Promise<void> => {
@ -240,6 +240,23 @@ class AmazonPageWorkerImpl implements AmazonPageWorker {
});
}
//#endregion
//#region Fetch Goods' Images
const imageUrls = await exec(tab.id!, async () => {
const node = document.evaluate(
`//div[@id='imgTagWrapperId']/img`,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
).singleNodeValue as HTMLImageElement | null;
return node ? [node.getAttribute('src')!] : null;
});
imageUrls &&
this.channel.emit('item-images-collected', {
asin,
urls: imageUrls,
});
//#endregion
}
}

View File

@ -26,6 +26,14 @@ interface AmazonPageWorkerEvents {
category2?: { name: string; rank: number };
};
/**
* The event is fired when images collected
*/
['item-images-collected']: {
asin: string;
urls: string[];
};
/**
* Error event that occurs when there is an issue with the Amazon page worker.
*/
@ -49,7 +57,7 @@ interface AmazonPageWorker {
/**
* Browsing goods search page and collect links to those goods.
*/
wanderSearchList(): Promise<void>;
wanderSearchPage(): Promise<void>;
/**
* Browsing goods detail page and collect target information.

View File

@ -15,11 +15,6 @@ export async function getManifest() {
description: pkg.description,
action: {
default_icon: './assets/icon-512.png',
default_popup: './dist/popup/index.html',
},
options_ui: {
page: './dist/options/index.html',
open_in_tab: true,
},
background: isFirefox
? {

View File

@ -1,39 +0,0 @@
<script setup lang="ts">
import logo from '~/assets/logo.svg';
</script>
<template>
<main class="option-page">
<img :src="logo" class="" alt="extension icon" />
<div class="title">Options Page</div>
<SharedSubtitle />
<div class="footer">
Powered by Vite <pixelarticons-zap class="align-middle inline-block" />
</div>
</main>
</template>
<style lang="scss" scoped>
.option-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #a3a8d4;
.title {
font-size: 2rem;
}
.input-field {
padding: 0.5rem;
border: 1px solid #333;
border-radius: 0.25rem;
}
.footer {
margin-top: 1rem;
}
}
</style>

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<base target="_blank" />
<title>Options</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@ -1,8 +0,0 @@
import { createApp } from 'vue';
import App from './Options.vue';
import { setupApp } from '~/logic/common-setup';
import '../styles';
const app = createApp(App);
setupApp(app);
app.mount('#app');

View File

@ -1,43 +0,0 @@
<script setup lang="ts">
function openOptionsPage() {
browser.runtime.openOptionsPage();
}
</script>
<template>
<main class="popup">
<Logo />
<div class="title">Popup</div>
<SharedSubtitle />
<button @click="openOptionsPage">Open Options</button>
</main>
</template>
<style lang="scss" scoped>
.popup {
min-width: 240px;
min-height: 300px;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.title {
font-size: 2rem;
}
button {
padding: 0.5rem 1rem;
margin-top: 1rem;
border: 1px solid #333;
border-radius: 0.25rem;
background-color: #fff;
cursor: pointer;
}
.footer {
margin-top: 1rem;
}
}
</style>

View File

@ -1,12 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<base target="_blank" />
<title>Popup</title>
</head>
<body style="min-width: 100px">
<div id="app"></div>
<script type="module" src="./main.ts"></script>
</body>
</html>

View File

@ -1,7 +0,0 @@
import App from './Popup.vue';
import { setupApp } from '~/logic/common-setup';
import '../styles';
const app = createApp(App);
setupApp(app);
app.mount('#app');

View File

@ -72,7 +72,7 @@ const onCollect = async () => {
message.error(msg);
});
await worker.doSearch(keywords.value);
await worker.wanderSearchList();
await worker.wanderSearchPage();
message.info('完成');
};
</script>

View File

@ -94,8 +94,6 @@ export default defineConfig(({ command }) => ({
},
rollupOptions: {
input: {
options: r('src/options/index.html'),
popup: r('src/popup/index.html'),
sidepanel: r('src/sidepanel/index.html'),
},
},