mirror of
https://github.com/primedigitaltech/azon_seeker.git
synced 2026-01-19 13:13:22 +08:00
Initial Repo
This commit is contained in:
commit
bdc3d0d512
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vite-ssg-dist
|
||||||
|
.vite-ssg-temp
|
||||||
|
*.crx
|
||||||
|
*.local
|
||||||
|
*.log
|
||||||
|
*.pem
|
||||||
|
*.xpi
|
||||||
|
*.zip
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
extension/manifest.json
|
||||||
|
node_modules
|
||||||
|
src/auto-imports.d.ts
|
||||||
|
src/components.d.ts
|
||||||
|
.eslintcache
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
|||||||
|
pnpm exec lint-staged
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Ignore artifacts:
|
||||||
|
build
|
||||||
|
coverage
|
||||||
3
.prettierrc
Normal file
3
.prettierrc
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
9
.vscode/extensions.json
vendored
Normal file
9
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"vue.volar"
|
||||||
|
// "antfu.iconify",
|
||||||
|
// "antfu.unocss",
|
||||||
|
// "dbaeumer.vscode-eslint",
|
||||||
|
// "csstools.postcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"cSpell.words": ["Vitesse"],
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"vite.autoStart": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit"
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "postcss"
|
||||||
|
},
|
||||||
|
"prettier.tabWidth": 2
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2021 Anthony Fu
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
138
README_Template.md
Normal file
138
README_Template.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# WebExtension Vite Starter
|
||||||
|
|
||||||
|
A [Vite](https://vitejs.dev/) powered WebExtension ([Chrome](https://developer.chrome.com/docs/extensions/reference/), [FireFox](https://addons.mozilla.org/en-US/developers/), etc.) starter template.
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<sub>Popup</sub><br/>
|
||||||
|
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741643-813b3773-17ff-4281-9737-f319e00feddc.png"><br/>
|
||||||
|
<sub>Options Page</sub><br/>
|
||||||
|
<img width="655" src="https://user-images.githubusercontent.com/11247099/126741653-43125b62-6578-4452-83a7-bee19be2eaa2.png"><br/>
|
||||||
|
<sub>Inject Vue App into the Content Script</sub><br/>
|
||||||
|
<img src="https://user-images.githubusercontent.com/11247099/130695439-52418cf0-e186-4085-8e19-23fe808a274e.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- ⚡️ **Instant HMR** - use **Vite** on dev (no more refresh!)
|
||||||
|
- 🥝 Vue 3 - Composition API, [`<script setup>` syntax](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0040-script-setup.md) and more!
|
||||||
|
- 💬 Effortless communications - powered by [`webext-bridge`](https://github.com/antfu/webext-bridge) and [VueUse](https://github.com/antfu/vueuse) storage
|
||||||
|
- 🌈 [UnoCSS](https://github.com/unocss/unocss) - The instant on-demand Atomic CSS engine.
|
||||||
|
- 🦾 [TypeScript](https://www.typescriptlang.org/) - type safe
|
||||||
|
- 📦 [Components auto importing](./src/components)
|
||||||
|
- 🌟 [Icons](./src/components) - Access to icons from any iconset directly
|
||||||
|
- 🖥 Content Script - Use Vue even in content script
|
||||||
|
- 🌍 WebExtension - isomorphic extension for Chrome, Firefox, and others
|
||||||
|
- 📃 Dynamic `manifest.json` with full type support
|
||||||
|
|
||||||
|
## Pre-packed
|
||||||
|
|
||||||
|
### WebExtension Libraries
|
||||||
|
|
||||||
|
- [`webextension-polyfill`](https://github.com/mozilla/webextension-polyfill) - WebExtension browser API Polyfill with types
|
||||||
|
- [`webext-bridge`](https://github.com/antfu/webext-bridge) - effortlessly communication between contexts
|
||||||
|
|
||||||
|
### Vite Plugins
|
||||||
|
|
||||||
|
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - Directly use `browser` and Vue Composition API without importing
|
||||||
|
- [`unplugin-vue-components`](https://github.com/antfu/vite-plugin-components) - components auto import
|
||||||
|
- [`unplugin-icons`](https://github.com/antfu/unplugin-icons) - icons as components
|
||||||
|
- [Iconify](https://iconify.design) - use icons from any icon sets [🔍Icônes](https://icones.netlify.app/)
|
||||||
|
|
||||||
|
### Vue Plugins
|
||||||
|
|
||||||
|
- [VueUse](https://github.com/antfu/vueuse) - collection of useful composition APIs
|
||||||
|
|
||||||
|
### UI Frameworks
|
||||||
|
|
||||||
|
- [UnoCSS](https://github.com/unocss/unocss) - the instant on-demand Atomic CSS engine
|
||||||
|
|
||||||
|
### Coding Style
|
||||||
|
|
||||||
|
- Use Composition API with [`<script setup>` SFC syntax](https://github.com/vuejs/rfcs/pull/227)
|
||||||
|
- [ESLint](https://eslint.org/) with [@antfu/eslint-config](https://github.com/antfu/eslint-config), single quotes, no semi
|
||||||
|
|
||||||
|
### Dev tools
|
||||||
|
|
||||||
|
- [TypeScript](https://www.typescriptlang.org/)
|
||||||
|
- [pnpm](https://pnpm.js.org/) - fast, disk space efficient package manager
|
||||||
|
- [esno](https://github.com/antfu/esno) - TypeScript / ESNext node runtime powered by esbuild
|
||||||
|
- [npm-run-all](https://github.com/mysticatea/npm-run-all) - Run multiple npm-scripts in parallel or sequential
|
||||||
|
- [web-ext](https://github.com/mozilla/web-ext) - Streamlined experience for developing web extensions
|
||||||
|
|
||||||
|
## Use the Template
|
||||||
|
|
||||||
|
### GitHub Template
|
||||||
|
|
||||||
|
[Create a repo from this template on GitHub](https://github.com/antfu/vitesse-webext/generate).
|
||||||
|
|
||||||
|
### Clone to local
|
||||||
|
|
||||||
|
If you prefer to do it manually with the cleaner git history
|
||||||
|
|
||||||
|
> If you don't have pnpm installed, run: npm install -g pnpm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx degit antfu/vitesse-webext my-webext
|
||||||
|
cd my-webext
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Folders
|
||||||
|
|
||||||
|
- `src` - main source.
|
||||||
|
- `contentScript` - scripts and components to be injected as `content_script`
|
||||||
|
- `background` - scripts for background.
|
||||||
|
- `components` - auto-imported Vue components that are shared in popup and options page.
|
||||||
|
- `styles` - styles shared in popup and options page
|
||||||
|
- `assets` - assets used in Vue components
|
||||||
|
- `manifest.ts` - manifest for the extension.
|
||||||
|
- `extension` - extension package root.
|
||||||
|
- `assets` - static assets (mainly for `manifest.json`).
|
||||||
|
- `dist` - built files, also serve stub entry for Vite on development.
|
||||||
|
- `scripts` - development and bundling helper scripts.
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **load extension in browser with the `extension/` folder**.
|
||||||
|
|
||||||
|
For Firefox developers, you can run the following command instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev-firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
`web-ext` auto reload the extension when `extension/` files changed.
|
||||||
|
|
||||||
|
> While Vite handles HMR automatically in the most of the case, [Extensions Reloader](https://chrome.google.com/webstore/detail/fimgfedafeadlieiabdeeaodndnlbhid) is still recommended for cleaner hard reloading.
|
||||||
|
|
||||||
|
## Using Gitpod
|
||||||
|
|
||||||
|
If you have a web browser, you can get a fully pre-configured development environment with one click:
|
||||||
|
|
||||||
|
[](https://gitpod.io/#https://github.com/antfu/vitesse-webext)
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
To build the extension, run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
And then pack files under `extension`, you can upload `extension.crx` or `extension.xpi` to appropriate extension store.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
[](https://volta.net)
|
||||||
|
|
||||||
|
This template is originally made for the [volta.net](https://volta.net) browser extension.
|
||||||
|
|
||||||
|
## Variations
|
||||||
|
|
||||||
|
This is a variant of [Vitesse](https://github.com/antfu/vitesse), check out the [full variations list](https://github.com/antfu/vitesse#variations).
|
||||||
BIN
extension/assets/icon-512.png
Normal file
BIN
extension/assets/icon-512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
3
extension/assets/icon.svg
Normal file
3
extension/assets/icon.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#888888"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
10
modules.d.ts
vendored
Normal file
10
modules.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
declare module 'vue' {
|
||||||
|
interface ComponentCustomProperties {
|
||||||
|
$app: {
|
||||||
|
context: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/64189046/479957
|
||||||
|
export {};
|
||||||
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "azon-seeker",
|
||||||
|
"displayName": "Azon Seeker",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"description": "Starter modify by honestfox101",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "npm run clear && cross-env NODE_ENV=development run-p dev:*",
|
||||||
|
"dev-firefox": "npm run clear && cross-env NODE_ENV=development EXTENSION=firefox run-p dev:*",
|
||||||
|
"dev:prepare": "esno scripts/prepare.ts",
|
||||||
|
"dev:background": "npm run build:background -- --mode development",
|
||||||
|
"dev:web": "vite",
|
||||||
|
"dev:js": "npm run build:js -- --mode development",
|
||||||
|
"build": "cross-env NODE_ENV=production run-s clear build:web build:prepare build:background build:js",
|
||||||
|
"build:prepare": "esno scripts/prepare.ts",
|
||||||
|
"build:background": "vite build --config vite.config.background.mts",
|
||||||
|
"build:web": "vite build",
|
||||||
|
"build:js": "vite build --config vite.config.content.mts",
|
||||||
|
"pack": "cross-env NODE_ENV=production run-p pack:*",
|
||||||
|
"pack:zip": "rimraf extension.zip && jszip-cli add extension/* -o ./extension.zip",
|
||||||
|
"pack:crx": "crx pack extension -o ./extension.crx",
|
||||||
|
"pack:xpi": "cross-env WEB_EXT_ARTIFACTS_DIR=./ web-ext build --source-dir ./extension --filename extension.xpi --overwrite-dest",
|
||||||
|
"start:chromium": "web-ext run --source-dir ./extension --target=chromium",
|
||||||
|
"start:firefox": "web-ext run --source-dir ./extension --target=firefox-desktop",
|
||||||
|
"clear": "rimraf --glob extension/dist extension/manifest.json extension.*",
|
||||||
|
"test": "vitest test",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"prepare": "husky"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@ffflorian/jszip-cli": "^3.8.2",
|
||||||
|
"@iconify/json": "^2.2.293",
|
||||||
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@types/webextension-polyfill": "^0.12.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/test-utils": "^2.4.6",
|
||||||
|
"@vueuse/core": "^12.3.0",
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"crx": "^5.0.1",
|
||||||
|
"esno": "^4.8.0",
|
||||||
|
"fs-extra": "^11.2.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
|
"jsdom": "^26.0.0",
|
||||||
|
"kolorist": "^1.8.0",
|
||||||
|
"lint-staged": "^15.5.0",
|
||||||
|
"naive-ui": "^2.41.0",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "3.5.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"sass-embedded": "^1.86.2",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"unplugin-auto-import": "^19.1.2",
|
||||||
|
"unplugin-icons": "^22.1.0",
|
||||||
|
"unplugin-vue-components": "^28.4.1",
|
||||||
|
"vite": "^6.2.4",
|
||||||
|
"vitest": "^3.1.1",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-demi": "^0.14.10",
|
||||||
|
"web-ext": "^8.5.0",
|
||||||
|
"webext-bridge": "^6.0.1",
|
||||||
|
"webextension-polyfill": "^0.12.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"**/*": "prettier --write --ignore-unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
7363
pnpm-lock.yaml
generated
Normal file
7363
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
12
scripts/manifest.ts
Normal file
12
scripts/manifest.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
import { getManifest } from '../src/manifest';
|
||||||
|
import { log, r } from './utils';
|
||||||
|
|
||||||
|
export async function writeManifest() {
|
||||||
|
await fs.writeJSON(r('extension/manifest.json'), await getManifest(), {
|
||||||
|
spaces: 2,
|
||||||
|
});
|
||||||
|
log('PRE', 'write manifest.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
writeManifest();
|
||||||
41
scripts/prepare.ts
Normal file
41
scripts/prepare.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
// generate stub index.html files for dev entry
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import fs from 'fs-extra';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import { isDev, log, port, r } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub index.html to use Vite in development
|
||||||
|
*/
|
||||||
|
async function stubIndexHtml() {
|
||||||
|
const views = ['options', 'popup', '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>',
|
||||||
|
);
|
||||||
|
await fs.writeFile(r(`extension/dist/${view}/index.html`), data, 'utf-8');
|
||||||
|
log('PRE', `stub ${view}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeManifest() {
|
||||||
|
execSync('npx esno ./scripts/manifest.ts', { stdio: 'inherit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
writeManifest();
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
stubIndexHtml();
|
||||||
|
chokidar.watch(r('src/**/*.html')).on('change', () => {
|
||||||
|
stubIndexHtml();
|
||||||
|
});
|
||||||
|
chokidar.watch([r('src/manifest.ts'), r('package.json')]).on('change', () => {
|
||||||
|
writeManifest();
|
||||||
|
});
|
||||||
|
}
|
||||||
12
scripts/utils.ts
Normal file
12
scripts/utils.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { resolve } from 'node:path';
|
||||||
|
import process from 'node:process';
|
||||||
|
import { bgCyan, black } from 'kolorist';
|
||||||
|
|
||||||
|
export const port = Number(process.env.PORT || '') || 3303;
|
||||||
|
export const r = (...args: string[]) => resolve(__dirname, '..', ...args);
|
||||||
|
export const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
export const isFirefox = process.env.EXTENSION === 'firefox';
|
||||||
|
|
||||||
|
export function log(name: string, message: string) {
|
||||||
|
console.log(black(bgCyan(` ${name} `)), message);
|
||||||
|
}
|
||||||
8
shim.d.ts
vendored
Normal file
8
shim.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { ProtocolWithReturn } from 'webext-bridge';
|
||||||
|
|
||||||
|
declare module 'webext-bridge' {
|
||||||
|
export interface ProtocolMap {
|
||||||
|
// define message protocol types
|
||||||
|
// see https://github.com/antfu/webext-bridge#type-safe-protocols
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/assets/logo.svg
Normal file
3
src/assets/logo.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#69717d"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
18
src/background/contentScriptHMR.ts
Normal file
18
src/background/contentScriptHMR.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { isFirefox, isForbiddenUrl } from '~/env';
|
||||||
|
|
||||||
|
// Firefox fetch files from cache instead of reloading changes from disk,
|
||||||
|
// hmr will not work as Chromium based browser
|
||||||
|
browser.webNavigation.onCommitted.addListener(({ tabId, frameId, url }) => {
|
||||||
|
// Filter out non main window events.
|
||||||
|
if (frameId !== 0) return;
|
||||||
|
|
||||||
|
if (isForbiddenUrl(url)) return;
|
||||||
|
|
||||||
|
// inject the latest scripts
|
||||||
|
browser.tabs
|
||||||
|
.executeScript(tabId, {
|
||||||
|
file: `${isFirefox ? '' : '.'}/dist/contentScripts/index.global.js`,
|
||||||
|
runAt: 'document_end',
|
||||||
|
})
|
||||||
|
.catch((error) => console.error(error));
|
||||||
|
});
|
||||||
23
src/background/main.ts
Normal file
23
src/background/main.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
// only on dev mode
|
||||||
|
if (import.meta.hot) {
|
||||||
|
// @ts-expect-error for background HMR
|
||||||
|
import('/@vite/client');
|
||||||
|
// load latest content script
|
||||||
|
import('./contentScriptHMR');
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove or turn this off if you don't use side panel
|
||||||
|
const USE_SIDE_PANEL = true;
|
||||||
|
|
||||||
|
// to toggle the sidepanel with the action button in chromium:
|
||||||
|
if (USE_SIDE_PANEL) {
|
||||||
|
// @ts-expect-error missing types
|
||||||
|
browser.sidePanel
|
||||||
|
.setPanelBehavior({ openPanelOnActionClick: true })
|
||||||
|
.catch((error: unknown) => console.error(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
browser.runtime.onInstalled.addListener((): void => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Azon Seeker installed');
|
||||||
|
});
|
||||||
19
src/components/Logo.vue
Normal file
19
src/components/Logo.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<template>
|
||||||
|
<a
|
||||||
|
class="icon"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="https://github.com/antfu/vitesse-webext"
|
||||||
|
target="_blank"
|
||||||
|
title="GitHub"
|
||||||
|
>
|
||||||
|
<pixelarticons-power width="3rem" height="3rem" />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
src/components/README.md
Normal file
11
src/components/README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
## Components
|
||||||
|
|
||||||
|
Components in this dir will be auto-registered and on-demand, powered by [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components).
|
||||||
|
|
||||||
|
Components can be shared in all views.
|
||||||
|
|
||||||
|
### Icons
|
||||||
|
|
||||||
|
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||||
|
|
||||||
|
It will only bundle the icons you use. Check out [unplugin-icons](https://github.com/unplugin/unplugin-icons) for more details.
|
||||||
10
src/components/SharedSubtitle.vue
Normal file
10
src/components/SharedSubtitle.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
<p class="shared-subtitle">This is the {{ $app.context }} page</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shared-subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
155
src/composables/useWebExtensionStorage.ts
Normal file
155
src/composables/useWebExtensionStorage.ts
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
import { StorageSerializers } from '@vueuse/core';
|
||||||
|
import { pausableWatch, toValue, tryOnScopeDispose } from '@vueuse/shared';
|
||||||
|
import { ref, shallowRef } from 'vue-demi';
|
||||||
|
import { storage } from 'webextension-polyfill';
|
||||||
|
|
||||||
|
import type { StorageLikeAsync, UseStorageAsyncOptions } from '@vueuse/core';
|
||||||
|
import type { MaybeRefOrGetter, RemovableRef } from '@vueuse/shared';
|
||||||
|
import type { Ref } from 'vue-demi';
|
||||||
|
import type { Storage } from 'webextension-polyfill';
|
||||||
|
|
||||||
|
export type WebExtensionStorageOptions<T> = UseStorageAsyncOptions<T>;
|
||||||
|
|
||||||
|
// https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorage/guess.ts
|
||||||
|
export function guessSerializerType(rawInit: unknown) {
|
||||||
|
return rawInit == null
|
||||||
|
? 'any'
|
||||||
|
: rawInit instanceof Set
|
||||||
|
? 'set'
|
||||||
|
: rawInit instanceof Map
|
||||||
|
? 'map'
|
||||||
|
: rawInit instanceof Date
|
||||||
|
? 'date'
|
||||||
|
: typeof rawInit === 'boolean'
|
||||||
|
? 'boolean'
|
||||||
|
: typeof rawInit === 'string'
|
||||||
|
? 'string'
|
||||||
|
: typeof rawInit === 'object'
|
||||||
|
? 'object'
|
||||||
|
: Number.isNaN(rawInit)
|
||||||
|
? 'any'
|
||||||
|
: 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
const storageInterface: StorageLikeAsync = {
|
||||||
|
removeItem(key: string) {
|
||||||
|
return storage.local.remove(key);
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem(key: string, value: string) {
|
||||||
|
return storage.local.set({ [key]: value });
|
||||||
|
},
|
||||||
|
|
||||||
|
async getItem(key: string) {
|
||||||
|
const storedData = await storage.local.get(key);
|
||||||
|
|
||||||
|
return storedData[key] as string;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/vueuse/vueuse/blob/658444bf9f8b96118dbd06eba411bb6639e24e88/packages/core/useStorageAsync/index.ts
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param initialValue
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
export function useWebExtensionStorage<T>(
|
||||||
|
key: string,
|
||||||
|
initialValue: MaybeRefOrGetter<T>,
|
||||||
|
options: WebExtensionStorageOptions<T> = {},
|
||||||
|
): RemovableRef<T> {
|
||||||
|
const {
|
||||||
|
flush = 'pre',
|
||||||
|
deep = true,
|
||||||
|
listenToStorageChanges = true,
|
||||||
|
writeDefaults = true,
|
||||||
|
mergeDefaults = false,
|
||||||
|
shallow,
|
||||||
|
eventFilter,
|
||||||
|
onError = (e) => {
|
||||||
|
console.error(e);
|
||||||
|
},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const rawInit: T = toValue(initialValue);
|
||||||
|
const type = guessSerializerType(rawInit);
|
||||||
|
|
||||||
|
const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T>;
|
||||||
|
const serializer = options.serializer ?? StorageSerializers[type];
|
||||||
|
|
||||||
|
async function read(event?: { key: string; newValue: string | null }) {
|
||||||
|
if (event && event.key !== key) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawValue = event
|
||||||
|
? event.newValue
|
||||||
|
: await storageInterface.getItem(key);
|
||||||
|
if (rawValue == null) {
|
||||||
|
data.value = rawInit;
|
||||||
|
if (writeDefaults && rawInit !== null)
|
||||||
|
await storageInterface.setItem(key, await serializer.write(rawInit));
|
||||||
|
} else if (mergeDefaults) {
|
||||||
|
const value = (await serializer.read(rawValue)) as T;
|
||||||
|
if (typeof mergeDefaults === 'function')
|
||||||
|
data.value = mergeDefaults(value, rawInit);
|
||||||
|
else if (type === 'object' && !Array.isArray(value))
|
||||||
|
data.value = {
|
||||||
|
...(rawInit as Record<keyof unknown, unknown>),
|
||||||
|
...(value as Record<keyof unknown, unknown>),
|
||||||
|
} as T;
|
||||||
|
else data.value = value;
|
||||||
|
} else {
|
||||||
|
data.value = (await serializer.read(rawValue)) as T;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void read();
|
||||||
|
|
||||||
|
async function write() {
|
||||||
|
try {
|
||||||
|
await (data.value == null
|
||||||
|
? storageInterface.removeItem(key)
|
||||||
|
: storageInterface.setItem(key, await serializer.write(data.value)));
|
||||||
|
} catch (error) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
|
||||||
|
data,
|
||||||
|
write,
|
||||||
|
{
|
||||||
|
flush,
|
||||||
|
deep,
|
||||||
|
eventFilter,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (listenToStorageChanges) {
|
||||||
|
const listener = async (changes: Record<string, Storage.StorageChange>) => {
|
||||||
|
try {
|
||||||
|
pauseWatch();
|
||||||
|
for (const [key, change] of Object.entries(changes)) {
|
||||||
|
await read({
|
||||||
|
key,
|
||||||
|
newValue: change.newValue as string | null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
resumeWatch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
storage.onChanged.addListener(listener);
|
||||||
|
|
||||||
|
tryOnScopeDispose(() => {
|
||||||
|
storage.onChanged.removeListener(listener);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as RemovableRef<T>;
|
||||||
|
}
|
||||||
36
src/contentScripts/index.ts
Normal file
36
src/contentScripts/index.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import App from './views/App.vue';
|
||||||
|
import { setupApp } from '~/logic/common-setup';
|
||||||
|
|
||||||
|
// 是否挂在ContentScript Vue APP
|
||||||
|
const MOUNT_COMPONENT = false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
styleEl.setAttribute('rel', 'stylesheet');
|
||||||
|
styleEl.setAttribute(
|
||||||
|
'href',
|
||||||
|
browser.runtime.getURL('dist/contentScripts/index.css'),
|
||||||
|
);
|
||||||
|
shadowDOM.appendChild(styleEl);
|
||||||
|
shadowDOM.appendChild(root);
|
||||||
|
document.body.appendChild(container);
|
||||||
|
const app = createApp(App);
|
||||||
|
setupApp(app);
|
||||||
|
app.mount(root);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
|
||||||
|
(() => {
|
||||||
|
if (MOUNT_COMPONENT) {
|
||||||
|
mountComponent();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
28
src/contentScripts/views/App.vue
Normal file
28
src/contentScripts/views/App.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { useToggle } from '@vueuse/core';
|
||||||
|
|
||||||
|
const [show, toggle] = useToggle(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="injected-content">
|
||||||
|
<button @click="toggle()">
|
||||||
|
<pixelarticons-power />
|
||||||
|
</button>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.injected-content {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/env.ts
Normal file
14
src/env.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const forbiddenProtocols = [
|
||||||
|
'chrome-extension://',
|
||||||
|
'chrome-search://',
|
||||||
|
'chrome://',
|
||||||
|
'devtools://',
|
||||||
|
'edge://',
|
||||||
|
'https://chrome.google.com/webstore',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isForbiddenUrl(url: string): boolean {
|
||||||
|
return forbiddenProtocols.some((protocol) => url.startsWith(protocol));
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isFirefox = navigator.userAgent.includes('Firefox');
|
||||||
10
src/global.d.ts
vendored
Normal file
10
src/global.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __DEV__: boolean;
|
||||||
|
/** Extension name, defined in packageJson.name */
|
||||||
|
declare const __NAME__: string;
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
const component: any;
|
||||||
|
export default component;
|
||||||
|
}
|
||||||
15
src/logic/common-setup.ts
Normal file
15
src/logic/common-setup.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { App } from 'vue';
|
||||||
|
|
||||||
|
export function setupApp(app: App) {
|
||||||
|
// Inject a globally available `$app` object in template
|
||||||
|
app.config.globalProperties.$app = {
|
||||||
|
context: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Provide access to `app` in script setup with `const app = inject('app')`
|
||||||
|
app.provide('app', app.config.globalProperties.$app);
|
||||||
|
|
||||||
|
// Here you can install additional plugins for all contexts: popup, options page and content-script.
|
||||||
|
// example: app.use(i18n)
|
||||||
|
// example excluding content-script context: if (context !== 'content-script') app.use(i18n)
|
||||||
|
}
|
||||||
1
src/logic/index.ts
Normal file
1
src/logic/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
64
src/logic/page-worker/index.ts
Normal file
64
src/logic/page-worker/index.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
class AmazonPageWorkerImpl implements AmazonPageWorker {
|
||||||
|
public async doSearch(keywords: string): Promise<string> {
|
||||||
|
const url = new URL('https://www.amazon.com/s');
|
||||||
|
url.searchParams.append('k', keywords);
|
||||||
|
|
||||||
|
const tab = await browser.tabs
|
||||||
|
.query({ active: true, currentWindow: true })
|
||||||
|
.then((tabs) => tabs[0]);
|
||||||
|
const currentUrl = new URL(tab.url!);
|
||||||
|
if (
|
||||||
|
currentUrl.hostname !== url.hostname ||
|
||||||
|
currentUrl.searchParams.get('k') !== keywords
|
||||||
|
) {
|
||||||
|
await browser.tabs.update(tab.id, { url: url.toString() });
|
||||||
|
}
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async wanderSearchList(): Promise<void> {
|
||||||
|
const tab = await browser.tabs
|
||||||
|
.query({ active: true, currentWindow: true })
|
||||||
|
.then((tabs) => tabs[0]);
|
||||||
|
const results = await browser.scripting.executeScript({
|
||||||
|
target: { tabId: tab.id! },
|
||||||
|
func: async () => {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 500 + ~~(500 * Math.random())),
|
||||||
|
);
|
||||||
|
while (!document.querySelector('.s-pagination-strip')) {
|
||||||
|
window.scrollBy(0, ~~(Math.random() * 500) + 500);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
}
|
||||||
|
const nextButton =
|
||||||
|
document.querySelector<HTMLLinkElement>('.s-pagination-next');
|
||||||
|
if (
|
||||||
|
nextButton &&
|
||||||
|
!nextButton.classList.contains('s-pagination-disabled')
|
||||||
|
) {
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
setTimeout(resolve, 500 + ~~(500 * Math.random())),
|
||||||
|
);
|
||||||
|
nextButton.click();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('results', results);
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageWorkerFactory {
|
||||||
|
public createAmazonPageWorker(): AmazonPageWorker {
|
||||||
|
return new AmazonPageWorkerImpl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageWorkerFactory = new PageWorkerFactory();
|
||||||
|
|
||||||
|
export default pageWorkerFactory;
|
||||||
14
src/logic/page-worker/types.d.ts
vendored
Normal file
14
src/logic/page-worker/types.d.ts
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
interface AmazonPageWorker {
|
||||||
|
/**
|
||||||
|
* Search for a list of items on Amazon
|
||||||
|
* @param keywords - The keywords to search for on Amazon.
|
||||||
|
* @returns A promise that resolves to a string representing the search URL.
|
||||||
|
*/
|
||||||
|
doSearch(keywords: string): Promise<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browsing item search page and collect links to those items.
|
||||||
|
* @param entryUrl - The URL of the Amazon search page to start from.
|
||||||
|
*/
|
||||||
|
wanderSearchList(): Promise<void>;
|
||||||
|
}
|
||||||
3
src/logic/storage.ts
Normal file
3
src/logic/storage.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { useWebExtensionStorage } from '~/composables/useWebExtensionStorage';
|
||||||
|
|
||||||
|
export const keywords = useWebExtensionStorage<string>('keywords', '');
|
||||||
81
src/manifest.ts
Normal file
81
src/manifest.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import fs from 'fs-extra';
|
||||||
|
import type { Manifest } from 'webextension-polyfill';
|
||||||
|
import type PkgType from '../package.json';
|
||||||
|
import { isDev, isFirefox, port, r } from '../scripts/utils';
|
||||||
|
|
||||||
|
export async function getManifest() {
|
||||||
|
const pkg = (await fs.readJSON(r('package.json'))) as typeof PkgType;
|
||||||
|
|
||||||
|
// update this file to update this manifest.json
|
||||||
|
// can also be conditional based on your need
|
||||||
|
const manifest: Manifest.WebExtensionManifest = {
|
||||||
|
manifest_version: 3,
|
||||||
|
name: pkg.displayName || pkg.name,
|
||||||
|
version: pkg.version,
|
||||||
|
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
|
||||||
|
? {
|
||||||
|
scripts: ['dist/background/index.mjs'],
|
||||||
|
type: 'module',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
service_worker: './dist/background/index.mjs',
|
||||||
|
},
|
||||||
|
icons: {
|
||||||
|
16: './assets/icon-512.png',
|
||||||
|
48: './assets/icon-512.png',
|
||||||
|
128: './assets/icon-512.png',
|
||||||
|
},
|
||||||
|
permissions: ['tabs', 'storage', 'activeTab', 'sidePanel', 'scripting'],
|
||||||
|
host_permissions: ['*://*/*'],
|
||||||
|
content_scripts: [
|
||||||
|
{
|
||||||
|
matches: ['<all_urls>'],
|
||||||
|
js: ['dist/contentScripts/index.global.js'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
web_accessible_resources: [
|
||||||
|
{
|
||||||
|
resources: ['dist/contentScripts/index.css'],
|
||||||
|
matches: ['<all_urls>'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
content_security_policy: {
|
||||||
|
extension_pages: isDev
|
||||||
|
? // this is required on dev for Vite script to load
|
||||||
|
`script-src \'self\' http://localhost:${port}; object-src \'self\'`
|
||||||
|
: "script-src 'self'; object-src 'self'",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// add sidepanel
|
||||||
|
if (isFirefox) {
|
||||||
|
manifest.sidebar_action = {
|
||||||
|
default_panel: 'dist/sidepanel/index.html',
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// the sidebar_action does not work for chromium based
|
||||||
|
(manifest as any).side_panel = {
|
||||||
|
default_path: 'dist/sidepanel/index.html',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: not work in MV3
|
||||||
|
if (isDev && false) {
|
||||||
|
// for content script, as browsers will cache them for each reload,
|
||||||
|
// we use a background script to always inject the latest version
|
||||||
|
// see src/background/contentScriptHMR.ts
|
||||||
|
delete manifest.content_scripts;
|
||||||
|
manifest.permissions?.push('webNavigation');
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
39
src/options/Options.vue
Normal file
39
src/options/Options.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<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>
|
||||||
12
src/options/index.html
Normal file
12
src/options/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
8
src/options/main.ts
Normal file
8
src/options/main.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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');
|
||||||
43
src/popup/Popup.vue
Normal file
43
src/popup/Popup.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<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>
|
||||||
12
src/popup/index.html
Normal file
12
src/popup/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!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>
|
||||||
7
src/popup/main.ts
Normal file
7
src/popup/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import App from './Popup.vue';
|
||||||
|
import { setupApp } from '~/logic/common-setup';
|
||||||
|
import '../styles';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
setupApp(app);
|
||||||
|
app.mount('#app');
|
||||||
63
src/sidepanel/Sidepanel.vue
Normal file
63
src/sidepanel/Sidepanel.vue
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { keywords } from '~/logic/storage';
|
||||||
|
import pageWorker from '~/logic/page-worker';
|
||||||
|
|
||||||
|
const onSearch = async () => {
|
||||||
|
if (keywords.value.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const worker = pageWorker.createAmazonPageWorker();
|
||||||
|
await worker.doSearch(keywords.value);
|
||||||
|
await worker.wanderSearchList();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="side-panel">
|
||||||
|
<n-space>
|
||||||
|
<mdi-cat style="font-size: 60px; color: black" />
|
||||||
|
<h1>Azon Seeker</h1>
|
||||||
|
</n-space>
|
||||||
|
<n-space>
|
||||||
|
<n-input
|
||||||
|
v-model:value="keywords"
|
||||||
|
class="search-input-box"
|
||||||
|
autosize
|
||||||
|
size="large"
|
||||||
|
round
|
||||||
|
placeholder="请输入关键词"
|
||||||
|
/>
|
||||||
|
<n-button round size="large" @click="onSearch">搜索</n-button>
|
||||||
|
</n-space>
|
||||||
|
<div style="height: 10px"></div>
|
||||||
|
<n-card class="result-content-container" title="结果框">
|
||||||
|
<n-empty description="还没有结果哦">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :size="50">
|
||||||
|
<solar-cat-linear />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
</n-empty>
|
||||||
|
</n-card>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.side-panel {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.search-input-box {
|
||||||
|
min-width: 270px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content-container {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
12
src/sidepanel/index.html
Normal file
12
src/sidepanel/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<base target="_blank" />
|
||||||
|
<title>Sidepanel</title>
|
||||||
|
</head>
|
||||||
|
<body style="min-width: 100px">
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7
src/sidepanel/main.ts
Normal file
7
src/sidepanel/main.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import App from './Sidepanel.vue';
|
||||||
|
import { setupApp } from '~/logic/common-setup';
|
||||||
|
import '../styles';
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
setupApp(app);
|
||||||
|
app.mount('#app');
|
||||||
1
src/styles/index.ts
Normal file
1
src/styles/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import './main.css';
|
||||||
20
src/styles/main.css
Normal file
20
src/styles/main.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply px-4 py-1 rounded inline-block
|
||||||
|
bg-teal-600 text-white cursor-pointer
|
||||||
|
hover:bg-teal-700
|
||||||
|
disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
@apply inline-block cursor-pointer select-none
|
||||||
|
opacity-75 transition duration-200 ease-in-out
|
||||||
|
hover:opacity-100 hover:text-teal-600;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
7
src/tests/demo.spec.ts
Normal file
7
src/tests/demo.spec.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('demo', () => {
|
||||||
|
it('should work', () => {
|
||||||
|
expect(1 + 1).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": false,
|
||||||
|
"target": "es2016",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"exclude": ["dist", "node_modules"]
|
||||||
|
}
|
||||||
36
vite.config.background.mts
Normal file
36
vite.config.background.mts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { sharedConfig } from './vite.config.mjs';
|
||||||
|
import { isDev, r } from './scripts/utils';
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// bundling the content script using Vite
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
define: {
|
||||||
|
__DEV__: isDev,
|
||||||
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
// https://github.com/vitejs/vite/issues/9320
|
||||||
|
// https://github.com/vitejs/vite/issues/9186
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(
|
||||||
|
isDev ? 'development' : 'production',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
watch: isDev ? {} : undefined,
|
||||||
|
outDir: r('extension/dist/background'),
|
||||||
|
cssCodeSplit: false,
|
||||||
|
emptyOutDir: false,
|
||||||
|
sourcemap: isDev ? 'inline' : false,
|
||||||
|
lib: {
|
||||||
|
entry: r('src/background/main.ts'),
|
||||||
|
name: packageJson.name,
|
||||||
|
formats: ['iife'],
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'index.mjs',
|
||||||
|
extend: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
37
vite.config.content.mts
Normal file
37
vite.config.content.mts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { sharedConfig } from './vite.config.mjs';
|
||||||
|
import { isDev, r } from './scripts/utils';
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
// bundling the content script using Vite
|
||||||
|
export default defineConfig({
|
||||||
|
...sharedConfig,
|
||||||
|
define: {
|
||||||
|
__DEV__: isDev,
|
||||||
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
// https://github.com/vitejs/vite/issues/9320
|
||||||
|
// https://github.com/vitejs/vite/issues/9186
|
||||||
|
'process.env.NODE_ENV': JSON.stringify(
|
||||||
|
isDev ? 'development' : 'production',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
watch: isDev ? {} : undefined,
|
||||||
|
outDir: r('extension/dist/contentScripts'),
|
||||||
|
cssCodeSplit: false,
|
||||||
|
emptyOutDir: false,
|
||||||
|
sourcemap: isDev ? 'inline' : false,
|
||||||
|
lib: {
|
||||||
|
entry: r('src/contentScripts/index.ts'),
|
||||||
|
name: packageJson.name,
|
||||||
|
formats: ['iife'],
|
||||||
|
cssFileName: 'index',
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
entryFileNames: 'index.global.js',
|
||||||
|
extend: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
114
vite.config.mts
Normal file
114
vite.config.mts
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
/// <reference types="vitest" />
|
||||||
|
|
||||||
|
import { dirname, relative } from 'node:path';
|
||||||
|
import type { UserConfig } from 'vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import Vue from '@vitejs/plugin-vue';
|
||||||
|
import Icons from 'unplugin-icons/vite';
|
||||||
|
import IconsResolver from 'unplugin-icons/resolver';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
|
||||||
|
import { isDev, port, r } from './scripts/utils';
|
||||||
|
import packageJson from './package.json';
|
||||||
|
|
||||||
|
export const sharedConfig: UserConfig = {
|
||||||
|
root: r('src'),
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~/': `${r('src')}/`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
__DEV__: isDev,
|
||||||
|
__NAME__: JSON.stringify(packageJson.name),
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
Vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
{
|
||||||
|
'webextension-polyfill': [['=', 'browser']],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'naive-ui': [
|
||||||
|
'useDialog',
|
||||||
|
'useMessage',
|
||||||
|
'useNotification',
|
||||||
|
'useLoadingBar',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dts: r('src/auto-imports.d.ts'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// https://github.com/antfu/unplugin-vue-components
|
||||||
|
Components({
|
||||||
|
dirs: [r('src/components')],
|
||||||
|
// generate `components.d.ts` for ts support with Volar
|
||||||
|
dts: r('src/components.d.ts'),
|
||||||
|
resolvers: [
|
||||||
|
// auto import icons
|
||||||
|
IconsResolver({
|
||||||
|
prefix: '',
|
||||||
|
}),
|
||||||
|
NaiveUiResolver(),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// https://github.com/antfu/unplugin-icons
|
||||||
|
Icons(),
|
||||||
|
|
||||||
|
// rewrite assets to use relative path
|
||||||
|
{
|
||||||
|
name: 'assets-rewrite',
|
||||||
|
enforce: 'post',
|
||||||
|
apply: 'build',
|
||||||
|
transformIndexHtml(html, { path }) {
|
||||||
|
return html.replace(
|
||||||
|
/"\/assets\//g,
|
||||||
|
`"${relative(dirname(path), '/assets')}/`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['vue', '@vueuse/core', 'webextension-polyfill'],
|
||||||
|
exclude: ['vue-demi'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineConfig(({ command }) => ({
|
||||||
|
...sharedConfig,
|
||||||
|
base: command === 'serve' ? `http://localhost:${port}/` : '/dist/',
|
||||||
|
server: {
|
||||||
|
port,
|
||||||
|
hmr: {
|
||||||
|
host: 'localhost',
|
||||||
|
},
|
||||||
|
origin: `http://localhost:${port}`,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
watch: isDev ? {} : undefined,
|
||||||
|
outDir: r('extension/dist'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
sourcemap: isDev ? 'inline' : false,
|
||||||
|
// https://developer.chrome.com/docs/webstore/program_policies/#:~:text=Code%20Readability%20Requirements
|
||||||
|
terserOptions: {
|
||||||
|
mangle: false,
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
input: {
|
||||||
|
options: r('src/options/index.html'),
|
||||||
|
popup: r('src/popup/index.html'),
|
||||||
|
sidepanel: r('src/sidepanel/index.html'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
globals: true,
|
||||||
|
environment: 'jsdom',
|
||||||
|
},
|
||||||
|
}));
|
||||||
Loading…
x
Reference in New Issue
Block a user