├── .browserslistrc
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── config.yml
│ └── feature_request.md
├── label-actions.yml
├── pull_request_template.md
└── workflows
│ ├── ci.yml
│ ├── label-actions.yml
│ └── lockdown.yml
├── .gitignore
├── .nvmrc
├── .prettierignore
├── .prettierrc.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── babel.config.js
├── gulpfile.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── action
│ ├── App.vue
│ ├── index.html
│ └── main.js
├── assets
│ ├── fonts
│ │ └── roboto.css
│ ├── icons
│ │ ├── app
│ │ │ └── icon.svg
│ │ ├── engines
│ │ │ ├── allEngines.svg
│ │ │ ├── archiveIs-dark.svg
│ │ │ ├── archiveIs.svg
│ │ │ ├── archiveOrg-dark.svg
│ │ │ ├── archiveOrg.svg
│ │ │ ├── ghostarchive.png
│ │ │ ├── megalodon.svg
│ │ │ ├── memento.svg
│ │ │ ├── permacc.svg
│ │ │ ├── webcite-dark.svg
│ │ │ ├── webcite.svg
│ │ │ └── yandex.svg
│ │ └── misc
│ │ │ ├── error.svg
│ │ │ ├── favorite-filled.svg
│ │ │ ├── favorite-light.svg
│ │ │ ├── help-light.svg
│ │ │ ├── keep-filled-light.svg
│ │ │ ├── keep-light.svg
│ │ │ ├── link-light.svg
│ │ │ ├── more-vert.svg
│ │ │ ├── open-in-new-light.svg
│ │ │ ├── open-in-new.svg
│ │ │ ├── settings-light.svg
│ │ │ ├── settings.svg
│ │ │ ├── spinner.svg
│ │ │ └── tab-light.svg
│ ├── locales
│ │ └── en
│ │ │ ├── messages-firefox.json
│ │ │ ├── messages-safari.json
│ │ │ ├── messages-samsung.json
│ │ │ └── messages.json
│ └── manifest
│ │ ├── chrome.json
│ │ ├── edge.json
│ │ ├── firefox.json
│ │ ├── opera.json
│ │ ├── safari.json
│ │ └── samsung.json
├── background
│ ├── index.html
│ └── main.js
├── base
│ └── main.js
├── contribute
│ ├── App.vue
│ ├── index.html
│ └── main.js
├── engines
│ ├── ghostarchive.js
│ ├── megalodon.js
│ └── yandex.js
├── options
│ ├── App.vue
│ ├── index.html
│ └── main.js
├── search
│ ├── App.vue
│ ├── index.html
│ └── main.js
├── storage
│ ├── config.json
│ ├── init.js
│ ├── revisions
│ │ ├── local
│ │ │ ├── 20211228050445_support_event_pages.js
│ │ │ ├── 20220102035029_add_showengineicons.js
│ │ │ ├── 20220102051642_add_search_engines.js
│ │ │ ├── 20220114113759_add_opencurrentdoc.js
│ │ │ ├── 20221220055629_add_theme_support.js
│ │ │ ├── 20230201232239_remove_search_engines.js
│ │ │ ├── 20230703074609_remove_gigablast.js
│ │ │ ├── 20230704125533_remove_opencurrentdocaction.js
│ │ │ ├── 20230713165504_add_perma.cc.js
│ │ │ ├── 20230715152710_add_ghostarchive.js
│ │ │ ├── 20230718120215_add_webcite.js
│ │ │ ├── 20240514170322_add_appversion.js
│ │ │ ├── 20240619180111_add_menuchangeevent.js
│ │ │ ├── 20240928183956_remove_search_engines.js
│ │ │ ├── 20241213110403_remove_bing.js
│ │ │ ├── SJltHx2rW.js
│ │ │ ├── SkhmnNhMG.js
│ │ │ ├── rJXbW1ZHmM.js
│ │ │ └── yjRtkzy.js
│ │ └── session
│ │ │ └── 20240514122825_initial_version.js
│ └── storage.js
├── tab
│ ├── index.html
│ └── main.js
├── tools
│ └── main.js
└── utils
│ ├── app.js
│ ├── common.js
│ ├── config.js
│ ├── data.js
│ ├── engines.js
│ ├── registry.js
│ ├── scripts.js
│ └── vuetify.js
└── webpack.config.js
/.browserslistrc:
--------------------------------------------------------------------------------
1 | [chrome]
2 | Chrome >= 123
3 | Edge >= 123
4 | Opera >= 109
5 |
6 | [edge]
7 | Edge >= 123
8 |
9 | [firefox]
10 | Firefox >= 115
11 | FirefoxAndroid >= 115
12 |
13 | [opera]
14 | Opera >= 109
15 |
16 | [safari]
17 | Safari >= 17
18 | iOS >= 17
19 |
20 | [samsung]
21 | Samsung >= 14
22 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: dessant
2 | patreon: dessant
3 | custom:
4 | - https://armin.dev/go/paypal
5 | - https://armin.dev/go/bitcoin
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report if something isn't working as expected
4 |
5 | ---
6 |
7 | **System**
8 |
9 |
10 | * OS name: [e.g. Windows, Ubuntu]
11 | * OS version: [e.g. 10]
12 | * Browser name: [e.g. Chrome, Firefox]
13 | * Browser version: [e.g. 60]
14 | * Extension version: [e.g. 1.0.0]
15 |
16 | **Bug description**
17 |
23 |
24 | **Logs**
25 |
26 |
30 | ```
31 | // REPLACE WITH LOGS
32 | ```
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 |
9 |
10 | **Describe the solution you'd like**
11 |
12 |
13 | **Describe alternatives you've considered**
14 |
15 |
16 | **Additional context**
17 |
18 |
--------------------------------------------------------------------------------
/.github/label-actions.yml:
--------------------------------------------------------------------------------
1 | # Configuration for Label Actions - https://github.com/dessant/label-actions
2 |
3 | incomplete:
4 | issues:
5 | comment: >
6 | @{issue-author}, the issue does not contain enough information
7 | to reproduce the bug. Please open a new bug report and fill out
8 | the issue template with the requested data.
9 | close: true
10 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | This project does not accept pull requests. Please use issues to report bugs or suggest new features.
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on: push
4 |
5 | jobs:
6 | build:
7 | name: Build
8 | runs-on: ubuntu-22.04
9 | permissions:
10 | contents: read
11 | steps:
12 | - name: Clone repository
13 | uses: actions/checkout@v4
14 | with:
15 | persist-credentials: 'false'
16 | - name: Setup Node.js
17 | uses: actions/setup-node@v4
18 | with:
19 | node-version-file: '.nvmrc'
20 | cache: 'npm'
21 | - name: Install dependencies
22 | run: npm install --legacy-peer-deps
23 | - name: Build artifacts
24 | run: |
25 | npm run build:prod:zip:chrome
26 | npm run build:prod:zip:edge
27 | npm run build:prod:zip:firefox
28 | npm run build:prod:zip:opera
29 | ENABLE_CONTRIBUTIONS=false npm run build:prod:zip:safari
30 | - name: Hash artifacts
31 | run: sha256sum artifacts/*/*
32 | if: startsWith(github.ref, 'refs/tags/v')
33 | - name: Upload artifacts
34 | uses: actions/upload-artifact@v4
35 | if: startsWith(github.ref, 'refs/tags/v')
36 | with:
37 | name: artifacts
38 | path: artifacts/
39 | retention-days: 1
40 | release:
41 | name: Release on GitHub
42 | runs-on: ubuntu-22.04
43 | needs: [build]
44 | if: startsWith(github.ref, 'refs/tags/v')
45 | permissions:
46 | contents: write
47 | steps:
48 | - name: Download artifacts
49 | uses: actions/download-artifact@v4
50 | with:
51 | name: artifacts
52 | path: artifacts/
53 | - name: Hash artifacts
54 | run: sha256sum artifacts/*/*
55 | - name: Create GitHub release
56 | uses: softprops/action-gh-release@v2
57 | with:
58 | tag_name: ${{ github.ref_name }}
59 | name: ${{ github.ref_name }}
60 | body: |
61 | Download and install the extension from the [extension store](https://github.com/dessant/web-archives#readme) of your browser.
62 |
63 | Learn more about this release from the [changelog](https://github.com/dessant/web-archives/blob/main/CHANGELOG.md#changelog).
64 | files: artifacts/*/*
65 | fail_on_unmatched_files: true
66 | draft: true
67 |
--------------------------------------------------------------------------------
/.github/workflows/label-actions.yml:
--------------------------------------------------------------------------------
1 | name: 'Label Actions'
2 |
3 | on:
4 | issues:
5 | types: [labeled, unlabeled]
6 |
7 | permissions:
8 | contents: read
9 | issues: write
10 |
11 | jobs:
12 | action:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: dessant/label-actions@v4
16 |
--------------------------------------------------------------------------------
/.github/workflows/lockdown.yml:
--------------------------------------------------------------------------------
1 | name: 'Repo Lockdown'
2 |
3 | on:
4 | pull_request_target:
5 | types: opened
6 |
7 | permissions:
8 | pull-requests: write
9 |
10 | jobs:
11 | action:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: dessant/repo-lockdown@v4
15 | with:
16 | exclude-pr-created-before: '2022-01-01T00:00:00Z'
17 | pr-comment: 'This project does not accept pull requests. Please use issues to report bugs or suggest new features.'
18 | process-only: 'prs'
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.assets/
2 | /app/
3 | /artifacts/
4 | /dist/
5 |
6 | /report.json
7 | /report.html
8 |
9 | /web-ext-config.mjs
10 |
11 | node_modules/
12 | /npm-debug.log
13 |
14 | /.vscode
15 |
16 | xcuserdata/
17 |
18 | .DS_Store
19 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 22.12.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /package.json
2 | *.md
3 |
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | singleQuote: true
2 | bracketSpacing: false
3 | arrowParens: 'avoid'
4 | trailingComma: 'none'
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | Web Archives
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## Supporting the Project
33 |
34 | Web Archives is an open source project made possible thanks to a community
35 | of awesome supporters. If you'd like to support the continued development
36 | of the extension, please consider contributing with
37 | [Patreon](https://armin.dev/go/patreon?pr=web-archives&src=repo),
38 | [PayPal](https://armin.dev/go/paypal?pr=web-archives&src=repo) or
39 | [Bitcoin](https://armin.dev/go/bitcoin?pr=web-archives&src=repo).
40 |
41 | ## Description
42 |
43 | Web Archives is a browser extension that enables you to find archived
44 | and cached versions of web pages, and comes with support for various
45 | search engines. Searches can be initiated from the context menu
46 | and the browser toolbar.
47 |
48 | #### Search Engines
49 |
50 | A diverse set of archive and cache sources are supported,
51 | which can be toggled and reordered from the extension's options.
52 | Visit the wiki for the full list of supported search engines.
53 |
54 | https://github.com/dessant/web-archives/wiki/Search-engines
55 |
56 | ## Screenshots
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | ## License
66 |
67 | Copyright (c) 2017-2024 Armin Sebastian
68 |
69 | This software is released under the terms of the GNU General Public License v3.0.
70 | See the [LICENSE](LICENSE) file for further information.
71 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 |
3 | const corejsVersion = require(
4 | path.join(path.dirname(require.resolve('core-js')), 'package.json')
5 | ).version;
6 |
7 | module.exports = function (api) {
8 | api.cache(true);
9 |
10 | const presets = [
11 | [
12 | '@babel/env',
13 | {
14 | modules: false,
15 | bugfixes: true,
16 | useBuiltIns: 'usage',
17 | corejs: {version: corejsVersion}
18 | }
19 | ]
20 | ];
21 |
22 | const plugins = [];
23 |
24 | const ignore = [
25 | new RegExp(`node_modules\\${path.sep}(?!(vueton|wesa)\\${path.sep}).*`)
26 | ];
27 |
28 | const parserOpts = {plugins: ['importAttributes']};
29 |
30 | return {presets, plugins, ignore, parserOpts};
31 | };
32 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const {exec} = require('node:child_process');
3 | const {lstat, readdir, readFile, writeFile, rm} = require('node:fs/promises');
4 |
5 | const {series, parallel, src, dest} = require('gulp');
6 | const postcss = require('gulp-postcss');
7 | const gulpif = require('gulp-if');
8 | const jsonMerge = require('gulp-merge-json');
9 | const jsonmin = require('gulp-jsonmin');
10 | const htmlmin = require('gulp-htmlmin');
11 | const imagemin = require('gulp-imagemin');
12 | const {ensureDir} = require('fs-extra');
13 | const recursiveReadDir = require('recursive-readdir');
14 | const sharp = require('sharp');
15 |
16 | const targetEnv = process.env.TARGET_ENV || 'chrome';
17 | const isProduction = process.env.NODE_ENV === 'production';
18 | const enableContributions =
19 | (process.env.ENABLE_CONTRIBUTIONS || 'true') === 'true';
20 |
21 | const mv3 = ['chrome', 'edge', 'opera', 'safari'].includes(targetEnv);
22 |
23 | const distDir = path.join(__dirname, 'dist', targetEnv);
24 |
25 | function initEnv() {
26 | process.env.BROWSERSLIST_ENV = targetEnv;
27 | }
28 |
29 | async function init() {
30 | initEnv();
31 |
32 | await rm(distDir, {recursive: true, force: true});
33 | await ensureDir(distDir);
34 | }
35 |
36 | function js(done) {
37 | exec(
38 | `webpack-cli build --color --env mv3=${mv3}`,
39 | function (err, stdout, stderr) {
40 | console.log(stdout);
41 | console.log(stderr);
42 | done(err);
43 | }
44 | );
45 | }
46 |
47 | function html() {
48 | const htmlSrc = ['src/**/*.html'];
49 |
50 | if (mv3 && !['safari'].includes(targetEnv)) {
51 | htmlSrc.push('!src/background/*.html');
52 | }
53 |
54 | if (!enableContributions) {
55 | htmlSrc.push('!src/contribute/*.html');
56 | }
57 |
58 | return src(htmlSrc, {base: '.'})
59 | .pipe(gulpif(isProduction, htmlmin({collapseWhitespace: true})))
60 | .pipe(dest(distDir));
61 | }
62 |
63 | async function images(done) {
64 | await ensureDir(path.join(distDir, 'src/assets/icons/app'));
65 | const appIconSvg = await readFile('src/assets/icons/app/icon.svg');
66 | const appIconSizes = [16, 19, 24, 32, 38, 48, 64, 96, 128];
67 | if (targetEnv === 'safari') {
68 | appIconSizes.push(256, 512, 1024);
69 | }
70 | for (const size of appIconSizes) {
71 | await sharp(appIconSvg, {density: (72 * size) / 24})
72 | .resize(size)
73 | .toFile(path.join(distDir, `src/assets/icons/app/icon-${size}.png`));
74 | }
75 | // Chrome Web Store does not correctly display optimized icons
76 | if (isProduction && targetEnv !== 'chrome') {
77 | await new Promise(resolve => {
78 | src(path.join(distDir, 'src/assets/icons/app/*.png'), {
79 | base: '.',
80 | encoding: false
81 | })
82 | .pipe(imagemin())
83 | .pipe(dest('.'))
84 | .on('error', done)
85 | .on('finish', resolve);
86 | });
87 | }
88 |
89 | if (targetEnv === 'firefox') {
90 | await ensureDir(path.join(distDir, 'src/assets/icons/engines'));
91 | const pngPaths = await recursiveReadDir('src/assets/icons/engines', [
92 | '*.!(png)'
93 | ]);
94 | const menuIconSizes = [16, 32];
95 | for (const pngPath of pngPaths) {
96 | for (const size of menuIconSizes) {
97 | await sharp(pngPath)
98 | .resize(size)
99 | .toFile(path.join(distDir, `${pngPath.slice(0, -4)}-${size}.png`));
100 | }
101 | }
102 | if (isProduction) {
103 | await new Promise(resolve => {
104 | src(path.join(distDir, 'src/assets/icons/engines/*.png'), {
105 | base: '.',
106 | encoding: false
107 | })
108 | .pipe(imagemin())
109 | .pipe(dest('.'))
110 | .on('error', done)
111 | .on('finish', resolve);
112 | });
113 | }
114 | }
115 |
116 | await new Promise(resolve => {
117 | src('src/assets/icons/@(app|engines|misc)/*.@(png|svg)', {
118 | base: '.',
119 | encoding: false
120 | })
121 | .pipe(gulpif(isProduction, imagemin()))
122 | .pipe(dest(distDir))
123 | .on('error', done)
124 | .on('finish', resolve);
125 | });
126 |
127 | if (enableContributions) {
128 | await new Promise(resolve => {
129 | src(
130 | 'node_modules/vueton/components/contribute/assets/*.@(png|webp|svg)',
131 | {encoding: false}
132 | )
133 | .pipe(gulpif(isProduction, imagemin()))
134 | .pipe(dest(path.join(distDir, 'src/contribute/assets')))
135 | .on('error', done)
136 | .on('finish', resolve);
137 | });
138 | }
139 | }
140 |
141 | async function fonts(done) {
142 | await new Promise(resolve => {
143 | src('src/assets/fonts/roboto.css', {base: '.'})
144 | .pipe(postcss())
145 | .pipe(dest(distDir))
146 | .on('error', done)
147 | .on('finish', resolve);
148 | });
149 |
150 | await new Promise(resolve => {
151 | src(
152 | 'node_modules/@fontsource/roboto/files/roboto-latin-@(400|500|700)-normal.woff2',
153 | {encoding: false}
154 | )
155 | .pipe(dest(path.join(distDir, 'src/assets/fonts/files')))
156 | .on('error', done)
157 | .on('finish', resolve);
158 | });
159 | }
160 |
161 | async function locale(done) {
162 | const localesRootDir = path.join(__dirname, 'src/assets/locales');
163 | const localeDirs = (
164 | await Promise.all(
165 | (await readdir(localesRootDir)).map(async function (file) {
166 | if ((await lstat(path.join(localesRootDir, file))).isDirectory()) {
167 | return file;
168 | }
169 | })
170 | )
171 | ).filter(Boolean);
172 |
173 | for (const localeDir of localeDirs) {
174 | const localePath = path.join(localesRootDir, localeDir);
175 | await new Promise(resolve => {
176 | src(
177 | [
178 | path.join(localePath, 'messages.json'),
179 | path.join(localePath, `messages-${targetEnv}.json`)
180 | ],
181 | {allowEmpty: true}
182 | )
183 | .pipe(
184 | jsonMerge({
185 | fileName: 'messages.json',
186 | edit: (parsedJson, file) => {
187 | if (isProduction) {
188 | for (let [key, value] of Object.entries(parsedJson)) {
189 | if (value.hasOwnProperty('description')) {
190 | delete parsedJson[key].description;
191 | }
192 | }
193 | }
194 | return parsedJson;
195 | }
196 | })
197 | )
198 | .pipe(gulpif(isProduction, jsonmin()))
199 | .pipe(dest(path.join(distDir, '_locales', localeDir)))
200 | .on('error', done)
201 | .on('finish', resolve);
202 | });
203 | }
204 | }
205 |
206 | function manifest() {
207 | return src(`src/assets/manifest/${targetEnv}.json`)
208 | .pipe(
209 | jsonMerge({
210 | fileName: 'manifest.json',
211 | edit: (parsedJson, file) => {
212 | parsedJson.version = require('./package.json').version;
213 | return parsedJson;
214 | }
215 | })
216 | )
217 | .pipe(gulpif(isProduction, jsonmin()))
218 | .pipe(dest(distDir));
219 | }
220 |
221 | async function license(done) {
222 | let year = '2017';
223 | const currentYear = new Date().getFullYear().toString();
224 | if (year !== currentYear) {
225 | year = `${year}-${currentYear}`;
226 | }
227 |
228 | let notice = `Web Archives
229 | Copyright (c) ${year} Armin Sebastian
230 | `;
231 |
232 | if (['safari', 'samsung'].includes(targetEnv)) {
233 | await writeFile(path.join(distDir, 'NOTICE'), notice);
234 | } else {
235 | notice = `${notice}
236 | This software is released under the terms of the GNU General Public License v3.0.
237 | See the LICENSE file for further information.
238 | `;
239 | await writeFile(path.join(distDir, 'NOTICE'), notice);
240 |
241 | await new Promise(resolve => {
242 | src('LICENSE')
243 | .pipe(dest(distDir))
244 | .on('error', done)
245 | .on('finish', resolve);
246 | });
247 | }
248 | }
249 |
250 | function zip(done) {
251 | exec(
252 | `web-ext build -s dist/${targetEnv} -a artifacts/${targetEnv} -n "{name}-{version}-${targetEnv}.zip" --overwrite-dest`,
253 | function (err, stdout, stderr) {
254 | console.log(stdout);
255 | console.log(stderr);
256 | done(err);
257 | }
258 | );
259 | }
260 |
261 | function inspect(done) {
262 | initEnv();
263 |
264 | exec(
265 | `npm run build:prod:chrome && \
266 | webpack --profile --json > report.json && \
267 | webpack-bundle-analyzer --mode static report.json dist/chrome/src && \
268 | sleep 3 && rm report.{json,html}`,
269 | function (err, stdout, stderr) {
270 | console.log(stdout);
271 | console.log(stderr);
272 | done(err);
273 | }
274 | );
275 | }
276 |
277 | exports.build = series(
278 | init,
279 | parallel(js, html, images, fonts, locale, manifest, license)
280 | );
281 | exports.zip = zip;
282 | exports.inspect = inspect;
283 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web-archives",
3 | "version": "7.0.1",
4 | "author": "Armin Sebastian",
5 | "license": "GPL-3.0-only",
6 | "homepage": "https://github.com/dessant/web-archives",
7 | "repository": {
8 | "url": "https://github.com/dessant/web-archives.git",
9 | "type": "git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/dessant/web-archives/issues"
13 | },
14 | "scripts": {
15 | "_build": "cross-env NODE_ENV=development gulp build",
16 | "build:chrome": "cross-env TARGET_ENV=chrome npm run _build",
17 | "build:edge": "cross-env TARGET_ENV=edge npm run _build",
18 | "build:firefox": "cross-env TARGET_ENV=firefox npm run _build",
19 | "build:opera": "cross-env TARGET_ENV=opera npm run _build",
20 | "build:safari": "cross-env TARGET_ENV=safari npm run _build",
21 | "build:samsung": "cross-env TARGET_ENV=samsung npm run _build",
22 | "_build:prod": "cross-env NODE_ENV=production gulp build",
23 | "build:prod:chrome": "cross-env TARGET_ENV=chrome npm run _build:prod",
24 | "build:prod:edge": "cross-env TARGET_ENV=edge npm run _build:prod",
25 | "build:prod:firefox": "cross-env TARGET_ENV=firefox npm run _build:prod",
26 | "build:prod:opera": "cross-env TARGET_ENV=opera npm run _build:prod",
27 | "build:prod:safari": "cross-env TARGET_ENV=safari npm run _build:prod",
28 | "build:prod:samsung": "cross-env TARGET_ENV=samsung npm run _build:prod",
29 | "_build:prod:zip": "npm run _build:prod && gulp zip",
30 | "build:prod:zip:chrome": "cross-env TARGET_ENV=chrome npm run _build:prod:zip",
31 | "build:prod:zip:edge": "cross-env TARGET_ENV=edge npm run _build:prod:zip",
32 | "build:prod:zip:firefox": "cross-env TARGET_ENV=firefox npm run _build:prod:zip",
33 | "build:prod:zip:opera": "cross-env TARGET_ENV=opera npm run _build:prod:zip",
34 | "build:prod:zip:safari": "cross-env TARGET_ENV=safari npm run _build:prod:zip",
35 | "build:prod:zip:samsung": "cross-env TARGET_ENV=samsung npm run _build:prod:zip",
36 | "start:chrome": "web-ext run -s dist/chrome -t chromium",
37 | "start:firefox": "web-ext run -s dist/firefox -t firefox-desktop",
38 | "start:android": "web-ext run -s dist/firefox -t firefox-android",
39 | "inspect": "cross-env NODE_ENV=production gulp inspect",
40 | "update": "ncu --dep prod,dev,peer --filterVersion '^*' --upgrade",
41 | "release": "commit-and-tag-version",
42 | "push": "git push --follow-tags origin main"
43 | },
44 | "dependencies": {
45 | "@fontsource/roboto": "^5.1.0",
46 | "buffer": "^6.0.3",
47 | "core-js": "^3.39.0",
48 | "idb-keyval": "^6.2.1",
49 | "lodash-es": "^4.17.21",
50 | "p-queue": "^8.0.1",
51 | "psl": "^1.15.0",
52 | "uuid": "^11.0.3",
53 | "vue": "3.4.23",
54 | "vue-resize": "^2.0.0-alpha.1",
55 | "vuedraggable": "^4.1.0",
56 | "vuetify": "3.3.0",
57 | "vueton": "^0.4.3",
58 | "webextension-polyfill": "^0.12.0",
59 | "wesa": "^0.6.1"
60 | },
61 | "devDependencies": {
62 | "@babel/core": "^7.26.0",
63 | "@babel/preset-env": "^7.26.0",
64 | "babel-loader": "^9.2.1",
65 | "commit-and-tag-version": "^12.5.0",
66 | "cross-env": "^7.0.3",
67 | "css-loader": "^7.1.2",
68 | "cssnano": "^7.0.6",
69 | "fs-extra": "^11.2.0",
70 | "gulp": "^5.0.0",
71 | "gulp-htmlmin": "^5.0.1",
72 | "gulp-if": "^3.0.0",
73 | "gulp-imagemin": "7.1.0",
74 | "gulp-jsonmin": "^1.2.0",
75 | "gulp-merge-json": "^2.2.1",
76 | "gulp-postcss": "^10.0.0",
77 | "mini-css-extract-plugin": "^2.9.2",
78 | "npm-check-updates": "^17.1.11",
79 | "postcss": "^8.4.49",
80 | "postcss-loader": "^8.1.1",
81 | "postcss-preset-env": "^10.1.1",
82 | "prettier": "^3.4.2",
83 | "recursive-readdir": "^2.2.3",
84 | "sass": "^1.83.0",
85 | "sass-loader": "^16.0.4",
86 | "sharp": "^0.33.5",
87 | "vue-loader": "^17.4.2",
88 | "web-ext": "^8.3.0",
89 | "webpack": "^5.97.1",
90 | "webpack-bundle-analyzer": "^4.10.2",
91 | "webpack-cli": "^5.1.4",
92 | "webpack-plugin-vuetify": "^3.0.3"
93 | },
94 | "private": true
95 | }
96 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const postcssPresetEnv = require('postcss-preset-env');
2 | const cssnano = require('cssnano');
3 |
4 | module.exports = function (api) {
5 | const plugins = [postcssPresetEnv()];
6 |
7 | if (api.env === 'production') {
8 | plugins.push(cssnano({zindex: false, discardUnused: false}));
9 | }
10 |
11 | return {plugins};
12 | };
13 |
--------------------------------------------------------------------------------
/src/action/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/action/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue';
2 |
3 | import {configApp, loadFonts} from 'utils/app';
4 | import {configVuetify} from 'utils/vuetify';
5 | import App from './App';
6 |
7 | async function init() {
8 | await loadFonts(['400 14px Roboto', '500 14px Roboto']);
9 |
10 | const app = createApp(App);
11 |
12 | await configApp(app);
13 | await configVuetify(app);
14 |
15 | app.mount('body');
16 | }
17 |
18 | init();
19 |
--------------------------------------------------------------------------------
/src/assets/fonts/roboto.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Roboto';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: url('./files/roboto-latin-400-normal.woff2') format('woff2'),
6 | local('Roboto'), local('Roboto-Regular');
7 | }
8 |
9 | @font-face {
10 | font-family: 'Roboto';
11 | font-style: normal;
12 | font-weight: 500;
13 | src: url('./files/roboto-latin-500-normal.woff2') format('woff2'),
14 | local('Roboto Medium'), local('Roboto-Medium');
15 | }
16 |
17 | @font-face {
18 | font-family: 'Roboto';
19 | font-style: normal;
20 | font-weight: 700;
21 | src: url('./files/roboto-latin-700-normal.woff2') format('woff2'),
22 | local('Roboto Bold'), local('Roboto-Bold');
23 | }
24 |
--------------------------------------------------------------------------------
/src/assets/icons/app/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/allEngines.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/archiveIs-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/archiveIs.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/archiveOrg-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/archiveOrg.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/ghostarchive.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dessant/web-archives/6b76b69f8977d065469aa0f3e7453c00cc4e4b55/src/assets/icons/engines/ghostarchive.png
--------------------------------------------------------------------------------
/src/assets/icons/engines/megalodon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/memento.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/permacc.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/webcite-dark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/webcite.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/assets/icons/engines/yandex.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/favorite-filled.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/favorite-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/help-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/keep-filled-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/keep-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/link-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/more-vert.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/open-in-new-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/open-in-new.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/settings-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/settings.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/spinner.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/icons/misc/tab-light.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/assets/locales/en/messages-firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "engineName_archiveIsAll": {
3 | "message": "Archive.is (All)",
4 | "description": "Name of the search engine."
5 | },
6 |
7 | "engineName_archiveOrgAll": {
8 | "message": "Wayback Machine (All)",
9 | "description": "Name of the search engine."
10 | },
11 |
12 | "menuItemTitle_archiveIsAll": {
13 | "message": "Archive.is (All)",
14 | "description": "Name of the search engine."
15 | },
16 |
17 | "menuItemTitle_archiveOrgAll": {
18 | "message": "Wayback Machine (All)",
19 | "description": "Name of the search engine."
20 | },
21 |
22 | "menuItemTitle_allEngines": {
23 | "message": "All Search Engines",
24 | "description": "Title of the menu item."
25 | },
26 |
27 | "mainMenuItemTitle_allEngines": {
28 | "message": "Search All Engines for Page",
29 | "description": "Title of the menu item."
30 | },
31 |
32 | "mainMenuItemTitle_engine": {
33 | "message": "Search $ENGINE$ for Page",
34 | "description": "Title of the menu item.",
35 | "placeholders": {
36 | "engine": {
37 | "content": "$1",
38 | "example": "Wayback Machine"
39 | }
40 | }
41 | },
42 |
43 | "menuItemTitle_openCurrentDoc": {
44 | "message": "Open Current Page",
45 | "description": "Title of the menu item."
46 | },
47 |
48 | "optionSectionTitle_engines": {
49 | "message": "Search Engines",
50 | "description": "Title of the options section."
51 | },
52 |
53 | "optionTitle_archiveOrgAll": {
54 | "message": "Wayback Machine (All)",
55 | "description": "Title of the option."
56 | },
57 |
58 | "optionTitle_archiveIsAll": {
59 | "message": "Archive.is (All)",
60 | "description": "Title of the option."
61 | },
62 |
63 | "optionSectionTitle_contextmenu": {
64 | "message": "Context Menu",
65 | "description": "Title of the options section."
66 | },
67 |
68 | "optionSectionTitle_toolbar": {
69 | "message": "Browser Toolbar",
70 | "description": "Title of the options section."
71 | },
72 |
73 | "optionSectionTitleMobile_toolbar": {
74 | "message": "Browser Menu",
75 | "description": "Title of the options section."
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/assets/locales/en/messages-safari.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionDescription": {
3 | "message": "View archived and cached versions of web pages.",
4 | "description": "Description of the extension."
5 | },
6 |
7 | "engineName_archiveIsAll": {
8 | "message": "Archive.is (All)",
9 | "description": "Name of the search engine."
10 | },
11 |
12 | "engineName_archiveOrgAll": {
13 | "message": "Wayback Machine (All)",
14 | "description": "Name of the search engine."
15 | },
16 |
17 | "menuItemTitle_archiveIsAll": {
18 | "message": "Archive.is (All)",
19 | "description": "Title of the menu item."
20 | },
21 |
22 | "menuItemTitle_archiveOrgAll": {
23 | "message": "Wayback Machine (All)",
24 | "description": "Title of the menu item."
25 | },
26 |
27 | "menuItemTitle_allEngines": {
28 | "message": "All Search Engines",
29 | "description": "Title of the menu item."
30 | },
31 |
32 | "mainMenuItemTitle_allEngines": {
33 | "message": "Search All Engines for Page",
34 | "description": "Title of the menu item."
35 | },
36 |
37 | "mainMenuItemTitle_engine": {
38 | "message": "Search $ENGINE$ for Page",
39 | "description": "Title of the menu item.",
40 | "placeholders": {
41 | "engine": {
42 | "content": "$1",
43 | "example": "Wayback Machine"
44 | }
45 | }
46 | },
47 |
48 | "menuItemTitle_openCurrentDoc": {
49 | "message": "Open Current Page",
50 | "description": "Title of the menu item."
51 | },
52 |
53 | "optionSectionTitle_engines": {
54 | "message": "Search Engines",
55 | "description": "Title of the options section."
56 | },
57 |
58 | "optionTitle_archiveOrgAll": {
59 | "message": "Wayback Machine (All)",
60 | "description": "Title of the option."
61 | },
62 |
63 | "optionTitle_archiveIsAll": {
64 | "message": "Archive.is (All)",
65 | "description": "Title of the option."
66 | },
67 |
68 | "optionSectionTitle_contextmenu": {
69 | "message": "Context Menu",
70 | "description": "Title of the options section."
71 | },
72 |
73 | "optionSectionTitle_toolbar": {
74 | "message": "Browser Toolbar",
75 | "description": "Title of the options section."
76 | },
77 |
78 | "optionSectionTitleMobile_toolbar": {
79 | "message": "Browser Menu",
80 | "description": "Title of the options section."
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/assets/locales/en/messages-samsung.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionDescription": {
3 | "message": "View archived and cached versions of web pages.",
4 | "description": "Description of the extension."
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/src/assets/locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "extensionName": {
3 | "message": "Web Archives",
4 | "description": "Name of the extension."
5 | },
6 |
7 | "extensionDescription": {
8 | "message": "View archived and cached versions of web pages on various search engines, such as the Wayback Machine and Archive.is.",
9 | "description": "Description of the extension."
10 | },
11 |
12 | "engineName_yandex": {
13 | "message": "Yandex",
14 | "description": "Name of the search engine."
15 | },
16 |
17 | "engineName_archiveOrg": {
18 | "message": "Wayback Machine",
19 | "description": "Name of the search engine."
20 | },
21 |
22 | "engineName_archiveOrgAll": {
23 | "message": "Wayback Machine (all)",
24 | "description": "Name of the search engine."
25 | },
26 |
27 | "engineName_memento": {
28 | "message": "Memento",
29 | "description": "Name of the search engine."
30 | },
31 |
32 | "engineName_archiveIs": {
33 | "message": "Archive.is",
34 | "description": "Name of the search engine."
35 | },
36 |
37 | "engineName_archiveIsAll": {
38 | "message": "Archive.is (all)",
39 | "description": "Name of the search engine."
40 | },
41 |
42 | "engineName_megalodon": {
43 | "message": "Megalodon",
44 | "description": "Name of the search engine."
45 | },
46 |
47 | "engineName_permacc": {
48 | "message": "Perma.cc",
49 | "description": "Name of the search engine."
50 | },
51 |
52 | "engineName_ghostarchive": {
53 | "message": "Ghostarchive",
54 | "description": "Name of the search engine."
55 | },
56 |
57 | "engineName_webcite": {
58 | "message": "WebCite",
59 | "description": "Name of the search engine."
60 | },
61 |
62 | "engineName_allEngines": {
63 | "message": "All search engines",
64 | "description": "Name of the search engine."
65 | },
66 |
67 | "actionTitle_allEngines": {
68 | "message": "Search all engines for page",
69 | "description": "Title of the action."
70 | },
71 |
72 | "actionTitle_engine": {
73 | "message": "Search $ENGINE$ for page",
74 | "description": "Title of the action.",
75 | "placeholders": {
76 | "engine": {
77 | "content": "$1",
78 | "example": "Wayback Machine"
79 | }
80 | }
81 | },
82 |
83 | "menuItemTitle_yandex": {
84 | "message": "Yandex",
85 | "description": "Title of the menu item."
86 | },
87 |
88 | "menuItemTitle_archiveOrg": {
89 | "message": "Wayback Machine",
90 | "description": "Title of the menu item."
91 | },
92 |
93 | "menuItemTitle_archiveOrgAll": {
94 | "message": "Wayback Machine (all)",
95 | "description": "Title of the menu item."
96 | },
97 |
98 | "menuItemTitle_memento": {
99 | "message": "Memento",
100 | "description": "Title of the menu item."
101 | },
102 |
103 | "menuItemTitle_archiveIs": {
104 | "message": "Archive.is",
105 | "description": "Title of the menu item."
106 | },
107 |
108 | "menuItemTitle_archiveIsAll": {
109 | "message": "Archive.is (all)",
110 | "description": "Title of the menu item."
111 | },
112 |
113 | "menuItemTitle_megalodon": {
114 | "message": "Megalodon",
115 | "description": "Title of the menu item."
116 | },
117 |
118 | "menuItemTitle_permacc": {
119 | "message": "Perma.cc",
120 | "description": "Title of the menu item."
121 | },
122 |
123 | "menuItemTitle_ghostarchive": {
124 | "message": "Ghostarchive",
125 | "description": "Title of the menu item."
126 | },
127 |
128 | "menuItemTitle_webcite": {
129 | "message": "WebCite",
130 | "description": "Title of the menu item."
131 | },
132 |
133 | "menuItemTitle_allEngines": {
134 | "message": "All search engines",
135 | "description": "Title of the menu item."
136 | },
137 |
138 | "mainMenuItemTitle_allEngines": {
139 | "message": "Search all engines for page",
140 | "description": "Title of the menu item."
141 | },
142 |
143 | "mainMenuItemTitle_engine": {
144 | "message": "Search $ENGINE$ for page",
145 | "description": "Title of the menu item.",
146 | "placeholders": {
147 | "engine": {
148 | "content": "$1",
149 | "example": "Wayback Machine"
150 | }
151 | }
152 | },
153 |
154 | "menuItemTitle_openCurrentDoc": {
155 | "message": "Open current page",
156 | "description": "Title of the menu item."
157 | },
158 |
159 | "optionSectionTitle_engines": {
160 | "message": "Search engines",
161 | "description": "Title of the options section."
162 | },
163 |
164 | "optionSectionDescription_engines": {
165 | "message": "Toggle search engines and customize their order (drag and drop)",
166 | "description": "Description of the options section."
167 | },
168 |
169 | "optionTitle_yandex": {
170 | "message": "Yandex Cache",
171 | "description": "Title of the option."
172 | },
173 |
174 | "optionTitle_archiveOrg": {
175 | "message": "Wayback Machine",
176 | "description": "Title of the option."
177 | },
178 |
179 | "optionTitle_archiveOrgAll": {
180 | "message": "Wayback Machine (all)",
181 | "description": "Title of the option."
182 | },
183 |
184 | "optionTitle_memento": {
185 | "message": "Memento Time Travel",
186 | "description": "Title of the option."
187 | },
188 |
189 | "optionTitle_archiveIs": {
190 | "message": "Archive.is",
191 | "description": "Title of the option."
192 | },
193 |
194 | "optionTitle_archiveIsAll": {
195 | "message": "Archive.is (all)",
196 | "description": "Title of the option."
197 | },
198 |
199 | "optionTitle_megalodon": {
200 | "message": "Megalodon",
201 | "description": "Title of the option."
202 | },
203 |
204 | "optionTitle_permacc": {
205 | "message": "Perma.cc",
206 | "description": "Title of the option."
207 | },
208 |
209 | "optionTitle_ghostarchive": {
210 | "message": "Ghostarchive",
211 | "description": "Title of the option."
212 | },
213 |
214 | "optionTitle_webcite": {
215 | "message": "WebCite",
216 | "description": "Title of the option."
217 | },
218 |
219 | "optionTitle_searchMode": {
220 | "message": "Search mode",
221 | "description": "Title of the option."
222 | },
223 |
224 | "optionTitle_searchAllEngines": {
225 | "message": "Search all engines",
226 | "description": "Title of the option."
227 | },
228 |
229 | "optionSectionTitle_contextmenu": {
230 | "message": "Context menu",
231 | "description": "Title of the options section."
232 | },
233 |
234 | "optionTitle_showInContextMenu": {
235 | "message": "Visibility",
236 | "description": "Title of the option."
237 | },
238 |
239 | "optionValue_showInContextMenu_all": {
240 | "message": "Show",
241 | "description": "Value of the option."
242 | },
243 |
244 | "optionValue_showInContextMenu_link": {
245 | "message": "Show for links",
246 | "description": "Value of the option."
247 | },
248 |
249 | "optionValue_showInContextMenu_false": {
250 | "message": "Hide",
251 | "description": "Value of the option."
252 | },
253 |
254 | "optionValue_searchAllEnginesContextMenu_main": {
255 | "message": "From context menu",
256 | "description": "Value of the option."
257 | },
258 |
259 | "optionValue_searchAllEnginesContextMenu_sub": {
260 | "message": "From submenu of context menu",
261 | "description": "Value of the option."
262 | },
263 |
264 | "optionValue_searchAllEnginesContextMenu_false": {
265 | "message": "Disable",
266 | "description": "Value of the option."
267 | },
268 |
269 | "optionTitle_openCurrentDocContextMenu": {
270 | "message": "Open current page",
271 | "description": "Title of the option."
272 | },
273 |
274 | "optionSectionTitle_toolbar": {
275 | "message": "Browser toolbar",
276 | "description": "Title of the options section."
277 | },
278 |
279 | "optionSectionTitleMobile_toolbar": {
280 | "message": "Browser menu",
281 | "description": "Title of the options section."
282 | },
283 |
284 | "optionValue_searchAllEnginesAction_main": {
285 | "message": "From browser toolbar",
286 | "description": "Value of the option."
287 | },
288 |
289 | "optionValue_searchAllEnginesAction_sub": {
290 | "message": "From browser toolbar popup",
291 | "description": "Value of the option."
292 | },
293 |
294 | "optionValue_searchAllEnginesAction_false": {
295 | "message": "Disable",
296 | "description": "Value of the option."
297 | },
298 |
299 | "optionValue_searchAllEnginesActionMobile_main": {
300 | "message": "From browser menu",
301 | "description": "Value of the option."
302 | },
303 |
304 | "optionValue_searchAllEnginesActionMobile_sub": {
305 | "message": "From browser menu popup",
306 | "description": "Value of the option."
307 | },
308 |
309 | "optionValue_searchAllEnginesActionMobile_false": {
310 | "message": "Disable",
311 | "description": "Value of the option."
312 | },
313 |
314 | "optionValue_searchModeAction_tab": {
315 | "message": "Tab",
316 | "description": "Value of the option."
317 | },
318 |
319 | "optionValue_searchModeAction_url": {
320 | "message": "URL",
321 | "description": "Value of the option."
322 | },
323 |
324 | "optionValue_action_searchModeAction_tab": {
325 | "message": "Tab",
326 | "description": "Value of the option."
327 | },
328 |
329 | "optionValue_action_searchModeAction_url": {
330 | "message": "URL",
331 | "description": "Value of the option."
332 | },
333 |
334 | "optionTitle_showPageAction": {
335 | "message": "Show in address bar on server error",
336 | "description": "Title of the option."
337 | },
338 |
339 | "optionSectionTitle_misc": {
340 | "message": "Miscellaneous",
341 | "description": "Title of the miscellaneous options section."
342 | },
343 |
344 | "optionTitle_tabInBackgound": {
345 | "message": "Open new tabs in the background",
346 | "description": "Title of the option."
347 | },
348 |
349 | "optionTitle_showEngineIcons": {
350 | "message": "Show search engine icons",
351 | "description": "Title of the option."
352 | },
353 |
354 | "optionTitle_appTheme": {
355 | "message": "Theme",
356 | "description": "Title of the option."
357 | },
358 |
359 | "optionValue_appTheme_auto": {
360 | "message": "System default",
361 | "description": "Value of the option."
362 | },
363 |
364 | "optionValue_appTheme_light": {
365 | "message": "Light",
366 | "description": "Value of the option."
367 | },
368 |
369 | "optionValue_appTheme_dark": {
370 | "message": "Dark",
371 | "description": "Value of the option."
372 | },
373 |
374 | "optionTitle_showContribPage": {
375 | "message": "Show contribution page",
376 | "description": "Title of the option."
377 | },
378 |
379 | "inputPlaceholder_docUrl": {
380 | "message": "Page URL",
381 | "description": "Placeholder of the input."
382 | },
383 |
384 | "buttonLabel_contribute": {
385 | "message": "Contribute",
386 | "description": "Label of the button."
387 | },
388 |
389 | "pageTitle": {
390 | "message": "$PAGETITLE$ - $EXTENSIONNAME$",
391 | "description": "Title of the page.",
392 | "placeholders": {
393 | "pageTitle": {
394 | "content": "$1",
395 | "example": "Options"
396 | },
397 | "extensionName": {
398 | "content": "$2",
399 | "example": "Extension Name"
400 | }
401 | }
402 | },
403 |
404 | "pageTitle_options": {
405 | "message": "Options",
406 | "description": "Title of the page."
407 | },
408 |
409 | "pageTitle_contribute": {
410 | "message": "Contribute",
411 | "description": "Title of the page."
412 | },
413 |
414 | "actionMenu_openCurrentDoc": {
415 | "message": "Open current page",
416 | "description": "Title of the menu item."
417 | },
418 |
419 | "actionMenu_options": {
420 | "message": "Options",
421 | "description": "Title of the menu item."
422 | },
423 |
424 | "actionMenu_contribute": {
425 | "message": "Contribute",
426 | "description": "Title of the menu item."
427 | },
428 |
429 | "actionMenu_support": {
430 | "message": "Support",
431 | "description": "Title of the menu item."
432 | },
433 |
434 | "buttonTooltip_searchMode": {
435 | "message": "Search mode",
436 | "description": "Tooltip of the button."
437 | },
438 |
439 | "buttonTooltip_openCurrentDoc": {
440 | "message": "Open current page",
441 | "description": "Tooltip of the button."
442 | },
443 |
444 | "buttonTooltip_contribute": {
445 | "message": "Contribute",
446 | "description": "Tooltip of the button."
447 | },
448 |
449 | "buttonTooltip_options": {
450 | "message": "Options",
451 | "description": "Tooltip of the button."
452 | },
453 |
454 | "buttonTooltip_menu": {
455 | "message": "Menu",
456 | "description": "Tooltip of the button."
457 | },
458 |
459 | "buttonTooltip_pin": {
460 | "message": "Pin",
461 | "description": "Tooltip of the button."
462 | },
463 |
464 | "buttonTooltip_unpin": {
465 | "message": "Unpin",
466 | "description": "Tooltip of the button."
467 | },
468 |
469 | "error_invalidPageUrl": {
470 | "message": "The page URL is not valid.",
471 | "description": "Error message."
472 | },
473 |
474 | "error_invalidSearchMode_url": {
475 | "message": "Searching for page URLs is only supported from the browser toolbar popup. Visit the extension's options to select a different search mode.",
476 | "description": "Error message."
477 | },
478 |
479 | "error_invalidSearchModeMobile_url": {
480 | "message": "Searching for page URLs is only supported from the browser menu popup. Visit the extension's options to select a different search mode.",
481 | "description": "Error message."
482 | },
483 |
484 | "error_sessionExpired": {
485 | "message": "The session has expired.",
486 | "description": "Error message."
487 | },
488 |
489 | "error_sessionExpiredEngine": {
490 | "message": "The session has expired. Try searching again for the page on $ENGINE$.",
491 | "description": "Error message.",
492 | "placeholders": {
493 | "engine": {
494 | "content": "$1",
495 | "example": "Wayback Machine"
496 | }
497 | }
498 | },
499 |
500 | "error_engine": {
501 | "message": "Something went wrong. Searching on $ENGINE$ has failed.",
502 | "description": "Error message.",
503 | "placeholders": {
504 | "engine": {
505 | "content": "$1",
506 | "example": "Wayback Machine"
507 | }
508 | }
509 | },
510 |
511 | "error_noResults": {
512 | "message": "No results found.",
513 | "description": "Error message."
514 | },
515 |
516 | "error_scriptsNotAllowed": {
517 | "message": "Content scripts are not allowed on this page.",
518 | "description": "Error message."
519 | },
520 |
521 | "error_allEnginesDisabled": {
522 | "message": "All search engines have been disabled. Visit the extensions's options to enable a search engine.",
523 | "description": "Error message."
524 | },
525 |
526 | "error_noSearchEngineAccess": {
527 | "message": "Cannot access search page results. Visit Menu > Extensions and enable the \"Allow access to search page results\" option for the extension.",
528 | "description": "Error message."
529 | },
530 |
531 | "error_optionsNotApplied": {
532 | "message": "Restart the browser for changes to take effect.",
533 | "description": "Error message."
534 | },
535 |
536 | "error_currentDocUrlNotFound": {
537 | "message": "Current page URL not found.",
538 | "description": "Error message."
539 | }
540 | }
541 |
--------------------------------------------------------------------------------
/src/assets/manifest/chrome.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "minimum_chrome_version": "123.0",
11 |
12 | "permissions": [
13 | "alarms",
14 | "contextMenus",
15 | "storage",
16 | "unlimitedStorage",
17 | "tabs",
18 | "activeTab",
19 | "notifications",
20 | "webRequest",
21 | "declarativeNetRequest",
22 | "scripting"
23 | ],
24 |
25 | "host_permissions": [""],
26 |
27 | "content_security_policy": {
28 | "extension_pages": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none';"
29 | },
30 |
31 | "icons": {
32 | "16": "src/assets/icons/app/icon-16.png",
33 | "19": "src/assets/icons/app/icon-19.png",
34 | "24": "src/assets/icons/app/icon-24.png",
35 | "32": "src/assets/icons/app/icon-32.png",
36 | "38": "src/assets/icons/app/icon-38.png",
37 | "48": "src/assets/icons/app/icon-48.png",
38 | "64": "src/assets/icons/app/icon-64.png",
39 | "96": "src/assets/icons/app/icon-96.png",
40 | "128": "src/assets/icons/app/icon-128.png"
41 | },
42 |
43 | "action": {
44 | "default_icon": {
45 | "16": "src/assets/icons/app/icon-16.png",
46 | "19": "src/assets/icons/app/icon-19.png",
47 | "24": "src/assets/icons/app/icon-24.png",
48 | "32": "src/assets/icons/app/icon-32.png",
49 | "38": "src/assets/icons/app/icon-38.png",
50 | "48": "src/assets/icons/app/icon-48.png",
51 | "64": "src/assets/icons/app/icon-64.png",
52 | "96": "src/assets/icons/app/icon-96.png",
53 | "128": "src/assets/icons/app/icon-128.png"
54 | }
55 | },
56 |
57 | "options_ui": {
58 | "page": "src/options/index.html",
59 | "open_in_tab": true
60 | },
61 |
62 | "background": {
63 | "service_worker": "src/background/script.js"
64 | },
65 |
66 | "content_scripts": [
67 | {
68 | "matches": ["http://*/*", "https://*/*"],
69 | "all_frames": false,
70 | "run_at": "document_start",
71 | "js": ["src/base/script.js"]
72 | }
73 | ],
74 |
75 | "incognito": "split"
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/manifest/edge.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "minimum_chrome_version": "123.0",
11 |
12 | "permissions": [
13 | "alarms",
14 | "contextMenus",
15 | "storage",
16 | "unlimitedStorage",
17 | "tabs",
18 | "activeTab",
19 | "notifications",
20 | "webRequest",
21 | "declarativeNetRequest",
22 | "scripting"
23 | ],
24 |
25 | "host_permissions": [""],
26 |
27 | "content_security_policy": {
28 | "extension_pages": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none';"
29 | },
30 |
31 | "icons": {
32 | "16": "src/assets/icons/app/icon-16.png",
33 | "19": "src/assets/icons/app/icon-19.png",
34 | "24": "src/assets/icons/app/icon-24.png",
35 | "32": "src/assets/icons/app/icon-32.png",
36 | "38": "src/assets/icons/app/icon-38.png",
37 | "48": "src/assets/icons/app/icon-48.png",
38 | "64": "src/assets/icons/app/icon-64.png",
39 | "96": "src/assets/icons/app/icon-96.png",
40 | "128": "src/assets/icons/app/icon-128.png"
41 | },
42 |
43 | "action": {
44 | "default_icon": {
45 | "16": "src/assets/icons/app/icon-16.png",
46 | "19": "src/assets/icons/app/icon-19.png",
47 | "24": "src/assets/icons/app/icon-24.png",
48 | "32": "src/assets/icons/app/icon-32.png",
49 | "38": "src/assets/icons/app/icon-38.png",
50 | "48": "src/assets/icons/app/icon-48.png",
51 | "64": "src/assets/icons/app/icon-64.png",
52 | "96": "src/assets/icons/app/icon-96.png",
53 | "128": "src/assets/icons/app/icon-128.png"
54 | }
55 | },
56 |
57 | "options_ui": {
58 | "page": "src/options/index.html",
59 | "open_in_tab": true
60 | },
61 |
62 | "background": {
63 | "service_worker": "src/background/script.js"
64 | },
65 |
66 | "content_scripts": [
67 | {
68 | "matches": ["http://*/*", "https://*/*"],
69 | "all_frames": false,
70 | "run_at": "document_start",
71 | "js": ["src/base/script.js"]
72 | }
73 | ],
74 |
75 | "incognito": "split"
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/manifest/firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "browser_specific_settings": {
11 | "gecko": {
12 | "id": "{d07ccf11-c0cd-4938-a265-2a4d6ad01189}",
13 | "strict_min_version": "115.0"
14 | },
15 | "gecko_android": {
16 | "strict_min_version": "115.0"
17 | }
18 | },
19 |
20 | "permissions": [
21 | "alarms",
22 | "contextMenus",
23 | "storage",
24 | "unlimitedStorage",
25 | "tabs",
26 | "activeTab",
27 | "notifications",
28 | "webRequest",
29 | "webRequestBlocking",
30 | ""
31 | ],
32 |
33 | "content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none';",
34 |
35 | "icons": {
36 | "16": "src/assets/icons/app/icon-16.png",
37 | "19": "src/assets/icons/app/icon-19.png",
38 | "24": "src/assets/icons/app/icon-24.png",
39 | "32": "src/assets/icons/app/icon-32.png",
40 | "38": "src/assets/icons/app/icon-38.png",
41 | "48": "src/assets/icons/app/icon-48.png",
42 | "64": "src/assets/icons/app/icon-64.png",
43 | "96": "src/assets/icons/app/icon-96.png",
44 | "128": "src/assets/icons/app/icon-128.png"
45 | },
46 |
47 | "browser_action": {
48 | "default_icon": {
49 | "16": "src/assets/icons/app/icon-16.png",
50 | "19": "src/assets/icons/app/icon-19.png",
51 | "24": "src/assets/icons/app/icon-24.png",
52 | "32": "src/assets/icons/app/icon-32.png",
53 | "38": "src/assets/icons/app/icon-38.png",
54 | "48": "src/assets/icons/app/icon-48.png",
55 | "64": "src/assets/icons/app/icon-64.png",
56 | "96": "src/assets/icons/app/icon-96.png",
57 | "128": "src/assets/icons/app/icon-128.png"
58 | },
59 | "browser_style": false
60 | },
61 |
62 | "page_action": {
63 | "default_icon": {
64 | "16": "src/assets/icons/app/icon-16.png",
65 | "19": "src/assets/icons/app/icon-19.png",
66 | "24": "src/assets/icons/app/icon-24.png",
67 | "32": "src/assets/icons/app/icon-32.png",
68 | "38": "src/assets/icons/app/icon-38.png",
69 | "48": "src/assets/icons/app/icon-48.png",
70 | "64": "src/assets/icons/app/icon-64.png",
71 | "96": "src/assets/icons/app/icon-96.png",
72 | "128": "src/assets/icons/app/icon-128.png"
73 | },
74 | "browser_style": false
75 | },
76 |
77 | "options_ui": {
78 | "page": "src/options/index.html",
79 | "browser_style": false,
80 | "open_in_tab": true
81 | },
82 |
83 | "background": {
84 | "page": "src/background/index.html",
85 | "persistent": false
86 | },
87 |
88 | "content_scripts": [
89 | {
90 | "matches": ["http://*/*", "https://*/*", "file:///*"],
91 | "all_frames": false,
92 | "run_at": "document_start",
93 | "js": ["src/base/script.js"]
94 | }
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/src/assets/manifest/opera.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "minimum_opera_version": "109.0",
11 |
12 | "permissions": [
13 | "alarms",
14 | "contextMenus",
15 | "storage",
16 | "unlimitedStorage",
17 | "tabs",
18 | "activeTab",
19 | "notifications",
20 | "webRequest",
21 | "declarativeNetRequest",
22 | "scripting"
23 | ],
24 |
25 | "host_permissions": [""],
26 |
27 | "content_security_policy": {
28 | "extension_pages": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none';"
29 | },
30 |
31 | "icons": {
32 | "16": "src/assets/icons/app/icon-16.png",
33 | "19": "src/assets/icons/app/icon-19.png",
34 | "24": "src/assets/icons/app/icon-24.png",
35 | "32": "src/assets/icons/app/icon-32.png",
36 | "38": "src/assets/icons/app/icon-38.png",
37 | "48": "src/assets/icons/app/icon-48.png",
38 | "64": "src/assets/icons/app/icon-64.png",
39 | "96": "src/assets/icons/app/icon-96.png",
40 | "128": "src/assets/icons/app/icon-128.png"
41 | },
42 |
43 | "action": {
44 | "default_icon": {
45 | "16": "src/assets/icons/app/icon-16.png",
46 | "19": "src/assets/icons/app/icon-19.png",
47 | "24": "src/assets/icons/app/icon-24.png",
48 | "32": "src/assets/icons/app/icon-32.png",
49 | "38": "src/assets/icons/app/icon-38.png",
50 | "48": "src/assets/icons/app/icon-48.png",
51 | "64": "src/assets/icons/app/icon-64.png",
52 | "96": "src/assets/icons/app/icon-96.png",
53 | "128": "src/assets/icons/app/icon-128.png"
54 | }
55 | },
56 |
57 | "options_ui": {
58 | "page": "src/options/index.html",
59 | "open_in_tab": true
60 | },
61 |
62 | "background": {
63 | "service_worker": "src/background/script.js"
64 | },
65 |
66 | "content_scripts": [
67 | {
68 | "matches": ["http://*/*", "https://*/*"],
69 | "all_frames": false,
70 | "run_at": "document_start",
71 | "js": ["src/base/script.js"]
72 | }
73 | ],
74 |
75 | "incognito": "split"
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/manifest/safari.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "browser_specific_settings": {
11 | "safari": {
12 | "strict_min_version": "17.0"
13 | }
14 | },
15 |
16 | "permissions": [
17 | "alarms",
18 | "contextMenus",
19 | "storage",
20 | "unlimitedStorage",
21 | "tabs",
22 | "activeTab",
23 | "nativeMessaging",
24 | "webNavigation",
25 | "scripting"
26 | ],
27 |
28 | "host_permissions": [""],
29 |
30 | "icons": {
31 | "16": "src/assets/icons/app/icon-16.png",
32 | "19": "src/assets/icons/app/icon-19.png",
33 | "24": "src/assets/icons/app/icon-24.png",
34 | "32": "src/assets/icons/app/icon-32.png",
35 | "38": "src/assets/icons/app/icon-38.png",
36 | "48": "src/assets/icons/app/icon-48.png",
37 | "64": "src/assets/icons/app/icon-64.png",
38 | "96": "src/assets/icons/app/icon-96.png",
39 | "128": "src/assets/icons/app/icon-128.png",
40 | "256": "src/assets/icons/app/icon-256.png",
41 | "512": "src/assets/icons/app/icon-512.png",
42 | "1024": "src/assets/icons/app/icon-1024.png"
43 | },
44 |
45 | "action": {
46 | "default_icon": {
47 | "16": "src/assets/icons/app/icon-16.png",
48 | "19": "src/assets/icons/app/icon-19.png",
49 | "24": "src/assets/icons/app/icon-24.png",
50 | "32": "src/assets/icons/app/icon-32.png",
51 | "38": "src/assets/icons/app/icon-38.png",
52 | "48": "src/assets/icons/app/icon-48.png",
53 | "64": "src/assets/icons/app/icon-64.png",
54 | "96": "src/assets/icons/app/icon-96.png",
55 | "128": "src/assets/icons/app/icon-128.png"
56 | }
57 | },
58 |
59 | "options_ui": {
60 | "page": "src/options/index.html"
61 | },
62 |
63 | "background": {
64 | "page": "src/background/index.html",
65 | "persistent": false
66 | },
67 |
68 | "content_scripts": [
69 | {
70 | "matches": ["http://*/*", "https://*/*"],
71 | "all_frames": false,
72 | "run_at": "document_start",
73 | "js": ["src/base/script.js"]
74 | }
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/src/assets/manifest/samsung.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "__MSG_extensionName__",
4 | "description": "__MSG_extensionDescription__",
5 | "version": "0.1.0",
6 | "author": "Armin Sebastian",
7 | "homepage_url": "https://github.com/dessant/web-archives",
8 | "default_locale": "en",
9 |
10 | "minimum_chrome_version": "87.0",
11 |
12 | "permissions": [
13 | "alarms",
14 | "contextMenus",
15 | "storage",
16 | "unlimitedStorage",
17 | "tabs",
18 | "activeTab",
19 | "notifications",
20 | "webRequest",
21 | "webRequestBlocking",
22 | "webNavigation",
23 | ""
24 | ],
25 |
26 | "content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data:; connect-src *; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none';",
27 |
28 | "icons": {
29 | "16": "src/assets/icons/app/icon-16.png",
30 | "19": "src/assets/icons/app/icon-19.png",
31 | "24": "src/assets/icons/app/icon-24.png",
32 | "32": "src/assets/icons/app/icon-32.png",
33 | "38": "src/assets/icons/app/icon-38.png",
34 | "48": "src/assets/icons/app/icon-48.png",
35 | "64": "src/assets/icons/app/icon-64.png",
36 | "96": "src/assets/icons/app/icon-96.png",
37 | "128": "src/assets/icons/app/icon-128.png"
38 | },
39 |
40 | "browser_action": {
41 | "default_icon": {
42 | "16": "src/assets/icons/app/icon-16.png",
43 | "19": "src/assets/icons/app/icon-19.png",
44 | "24": "src/assets/icons/app/icon-24.png",
45 | "32": "src/assets/icons/app/icon-32.png",
46 | "38": "src/assets/icons/app/icon-38.png",
47 | "48": "src/assets/icons/app/icon-48.png",
48 | "64": "src/assets/icons/app/icon-64.png",
49 | "96": "src/assets/icons/app/icon-96.png",
50 | "128": "src/assets/icons/app/icon-128.png"
51 | }
52 | },
53 |
54 | "options_ui": {
55 | "page": "src/options/index.html",
56 | "chrome_style": false,
57 | "open_in_tab": true
58 | },
59 |
60 | "background": {
61 | "page": "src/background/index.html"
62 | },
63 |
64 | "content_scripts": [
65 | {
66 | "matches": ["http://*/*", "https://*/*"],
67 | "all_frames": false,
68 | "run_at": "document_start",
69 | "js": ["src/base/script.js"]
70 | }
71 | ],
72 |
73 | "incognito": "split"
74 | }
75 |
--------------------------------------------------------------------------------
/src/background/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/base/main.js:
--------------------------------------------------------------------------------
1 | import storage from 'storage/storage';
2 | import {runOnce} from 'utils/common';
3 |
4 | function main() {
5 | async function checkTask() {
6 | const {taskRegistry} = await storage.get('taskRegistry');
7 | if (Date.now() - taskRegistry.lastTaskStart < 600000) {
8 | await browser.runtime.sendMessage({id: 'taskRequest'});
9 | }
10 | }
11 |
12 | if (window.top === window) {
13 | checkTask();
14 | }
15 | }
16 |
17 | if (runOnce('baseModule')) {
18 | main();
19 | }
20 |
--------------------------------------------------------------------------------
/src/contribute/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
58 |
59 |
69 |
--------------------------------------------------------------------------------
/src/contribute/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/contribute/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue';
2 |
3 | import {configApp, loadFonts} from 'utils/app';
4 | import {configVuetify} from 'utils/vuetify';
5 | import App from './App';
6 |
7 | async function init() {
8 | await loadFonts(['400 14px Roboto', '500 14px Roboto', '700 14px Roboto']);
9 |
10 | const app = createApp(App);
11 |
12 | await configApp(app);
13 | await configVuetify(app);
14 |
15 | app.mount('body');
16 | }
17 |
18 | init();
19 |
--------------------------------------------------------------------------------
/src/engines/ghostarchive.js:
--------------------------------------------------------------------------------
1 | import {validateUrl} from 'utils/app';
2 | import {findNode, runOnce} from 'utils/common';
3 | import {initSearch, sendReceipt} from 'utils/engines';
4 |
5 | const engine = 'ghostarchive';
6 |
7 | async function search({session, search, doc, storageIds}) {
8 | const link = await findNode('#bodyContent table td a', {
9 | throwError: false,
10 | timeout: 10000
11 | });
12 |
13 | await sendReceipt(storageIds);
14 |
15 | if (link) {
16 | const tabUrl = link.href;
17 |
18 | if (validateUrl(tabUrl)) {
19 | window.location.href = tabUrl;
20 | }
21 | }
22 | }
23 |
24 | function init() {
25 | initSearch(search, engine, taskId);
26 | }
27 |
28 | if (runOnce('search')) {
29 | init();
30 | }
31 |
--------------------------------------------------------------------------------
/src/engines/megalodon.js:
--------------------------------------------------------------------------------
1 | import {findNode, runOnce} from 'utils/common';
2 | import {initSearch, sendReceipt} from 'utils/engines';
3 |
4 | const engine = 'megalodon';
5 |
6 | async function search({session, search, doc, storageIds}) {
7 | const node = await findNode('div#bgcontain a[id^="fish"]', {
8 | throwError: false
9 | });
10 |
11 | await sendReceipt(storageIds);
12 |
13 | if (node) {
14 | node.setAttribute('target', '_top');
15 | node.click();
16 | }
17 | }
18 |
19 | function init() {
20 | initSearch(search, engine, taskId);
21 | }
22 |
23 | if (runOnce('search')) {
24 | init();
25 | }
26 |
--------------------------------------------------------------------------------
/src/engines/yandex.js:
--------------------------------------------------------------------------------
1 | import {v4 as uuidv4} from 'uuid';
2 |
3 | import {
4 | findNode,
5 | makeDocumentVisible,
6 | executeScriptMainContext,
7 | runOnce,
8 | sleep
9 | } from 'utils/common';
10 | import {initSearch, sendReceipt, getRankedResults} from 'utils/engines';
11 |
12 | const engine = 'yandex';
13 |
14 | async function handleResults(sourceUrl, results) {
15 | const items = [];
16 |
17 | for (const button of results) {
18 | const data = Object.keys(button.dataset)
19 | .map(item => {
20 | return button.dataset[item];
21 | })
22 | .find(item => item.match(/^{.*variant/g));
23 |
24 | const cacheData = JSON.parse(data.replace(/("\;)/g, '"')).items.find(
25 | item => item.variant === 'copy'
26 | );
27 |
28 | if (cacheData) {
29 | items.push({
30 | button,
31 | url: new URL(cacheData.url).searchParams.get('url')
32 | });
33 | }
34 | }
35 |
36 | const rankedResults = getRankedResults({sourceUrl, results: items});
37 |
38 | if (rankedResults.length) {
39 | rankedResults[0].button.click();
40 |
41 | window.setTimeout(async function () {
42 | const link = await findNode(
43 | '.ExtralinksPopup a.ExtralinksPopup-Item_copy'
44 | );
45 | link.setAttribute('target', '_top');
46 | link.click();
47 | }, 100);
48 | }
49 | }
50 |
51 | async function search({session, search, doc, storageIds}) {
52 | if (!window.location.pathname.startsWith('/search')) {
53 | const input = await findNode('input#text');
54 |
55 | input.value = `url:${doc.docUrl}`;
56 | input.dispatchEvent(new InputEvent('input', {bubbles: true}));
57 |
58 | (await findNode('button.search3__button')).click();
59 |
60 | return;
61 | }
62 |
63 | await findNode('#search-result', {throwError: false});
64 |
65 | // wait for search service to load
66 | await new Promise((resolve, reject) => {
67 | const eventName = uuidv4();
68 |
69 | const onServiceReady = function () {
70 | window.clearTimeout(timeoutId);
71 | resolve();
72 | };
73 |
74 | const timeoutId = window.setTimeout(function () {
75 | document.removeEventListener(eventName, onServiceReady, {
76 | capture: true,
77 | once: true
78 | });
79 |
80 | reject(new Error('Search service is not ready'));
81 | }, 60000); // 1 minute
82 |
83 | document.addEventListener(eventName, onServiceReady, {
84 | capture: true,
85 | once: true
86 | });
87 |
88 | executeScriptMainContext({
89 | func: 'yandexServiceObserver',
90 | args: [eventName]
91 | });
92 | });
93 | await sleep(1000);
94 |
95 | let results = document.querySelectorAll(
96 | '#search-result button.Organic-Extralinks'
97 | );
98 |
99 | if (results.length) {
100 | await sendReceipt(storageIds);
101 |
102 | await handleResults(doc.docUrl, results);
103 | } else {
104 | const input = await findNode(
105 | 'form[role=search] .HeaderForm-InputWrapper input.HeaderForm-Input'
106 | );
107 |
108 | if (input.value.startsWith('url:')) {
109 | input.click();
110 |
111 | input.value = doc.docUrl;
112 | input.dispatchEvent(new InputEvent('input', {bubbles: true}));
113 |
114 | window.setTimeout(async function () {
115 | (await findNode('form[role=search] button.HeaderForm-Submit')).click();
116 | }, 100);
117 |
118 | // the page is not reloaded on desktop
119 | await findNode('#search-result button.Organic-Extralinks', {
120 | throwError: false,
121 | timeout: 30000
122 | });
123 | await sleep(1000);
124 |
125 | await sendReceipt(storageIds);
126 |
127 | results = document.querySelectorAll(
128 | '#search-result button.Organic-Extralinks'
129 | );
130 |
131 | if (results.length) {
132 | await handleResults(doc.docUrl, results);
133 | }
134 | }
135 | }
136 | }
137 |
138 | function init() {
139 | makeDocumentVisible();
140 | if (!window.location.pathname.startsWith('/showcaptcha')) {
141 | initSearch(search, engine, taskId);
142 | }
143 | }
144 |
145 | if (runOnce('search')) {
146 | init();
147 | }
148 |
--------------------------------------------------------------------------------
/src/options/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ getText('optionSectionTitle_engines') }}
6 |
7 |
8 | {{ getText('optionSectionDescription_engines') }}
9 |
10 |
11 |
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
61 |
62 |
99 |
100 |
101 |
102 | {{ getText('optionSectionTitle_misc') }}
103 |
104 |
105 |
106 |
112 |
113 |
114 |
115 |
116 |
120 |
121 |
122 |
126 |
127 |
128 |
132 |
133 |
134 |
140 | {{ getText('buttonLabel_contribute') }}
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
304 |
305 |
402 |
--------------------------------------------------------------------------------
/src/options/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/options/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue';
2 |
3 | import {configApp, loadFonts} from 'utils/app';
4 | import {configVuetify} from 'utils/vuetify';
5 | import App from './App';
6 |
7 | async function init() {
8 | await loadFonts(['400 14px Roboto', '500 14px Roboto']);
9 |
10 | const app = createApp(App);
11 |
12 | await configApp(app);
13 | await configVuetify(app);
14 |
15 | app.mount('body');
16 | }
17 |
18 | init();
19 |
--------------------------------------------------------------------------------
/src/search/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
17 |
18 |
19 |
20 |
21 |
119 |
120 |
182 |
--------------------------------------------------------------------------------
/src/search/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/search/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue';
2 |
3 | import {configApp, loadFonts} from 'utils/app';
4 | import {configVuetify} from 'utils/vuetify';
5 | import App from './App';
6 |
7 | async function init() {
8 | await loadFonts(['400 14px Roboto', '500 14px Roboto']);
9 |
10 | const app = createApp(App);
11 |
12 | await configApp(app);
13 | await configVuetify(app);
14 |
15 | app.mount('body');
16 | }
17 |
18 | init();
19 |
--------------------------------------------------------------------------------
/src/storage/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "revisions": {
3 | "local": [
4 | "SJltHx2rW",
5 | "SkhmnNhMG",
6 | "rJXbW1ZHmM",
7 | "yjRtkzy",
8 | "20211228050445_support_event_pages",
9 | "20220102035029_add_showengineicons",
10 | "20220102051642_add_search_engines",
11 | "20220114113759_add_opencurrentdoc",
12 | "20221220055629_add_theme_support",
13 | "20230201232239_remove_search_engines",
14 | "20230703074609_remove_gigablast",
15 | "20230704125533_remove_opencurrentdocaction",
16 | "20230713165504_add_perma.cc",
17 | "20230715152710_add_ghostarchive",
18 | "20230718120215_add_webcite",
19 | "20240514170322_add_appversion",
20 | "20240619180111_add_menuchangeevent",
21 | "20240928183956_remove_search_engines",
22 | "20241213110403_remove_bing"
23 | ],
24 | "session": [
25 | "20240514122825_initial_version"
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/storage/init.js:
--------------------------------------------------------------------------------
1 | import {migrate} from 'wesa';
2 |
3 | import {isStorageArea} from './storage';
4 |
5 | async function initStorage({area = 'local', data = null, silent = false} = {}) {
6 | const context = {
7 | getAvailableRevisions: async ({area} = {}) =>
8 | (
9 | await import(/* webpackMode: "eager" */ 'storage/config.json', {
10 | with: {type: 'json'}
11 | })
12 | ).revisions[area],
13 | getCurrentRevision: async ({area} = {}) =>
14 | (await browser.storage[area].get('storageVersion')).storageVersion,
15 | getRevision: async ({area, revision} = {}) =>
16 | import(
17 | /* webpackMode: "eager" */ `storage/revisions/${area}/${revision}.js`
18 | )
19 | };
20 |
21 | if (area === 'local') {
22 | await migrateLegacyStorage();
23 | }
24 |
25 | return migrate(context, {area, data, silent});
26 | }
27 |
28 | async function migrateLegacyStorage() {
29 | if (await isStorageArea({area: 'sync'})) {
30 | const {storageVersion: syncVersion} =
31 | await browser.storage.sync.get('storageVersion');
32 | if (syncVersion && syncVersion.length < 14) {
33 | const {storageVersion: localVersion} =
34 | await browser.storage.local.get('storageVersion');
35 |
36 | if (!localVersion || localVersion.length < 14) {
37 | const syncData = await browser.storage.sync.get(null);
38 | await browser.storage.local.clear();
39 | await browser.storage.local.set(syncData);
40 | await browser.storage.sync.clear();
41 | }
42 | }
43 | }
44 | }
45 |
46 | export {initStorage};
47 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20211228050445_support_event_pages.js:
--------------------------------------------------------------------------------
1 | const message = 'Support event pages';
2 |
3 | const revision = '20211228050445_support_event_pages';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 |
8 | const {engines, disabledEngines, searchCount} =
9 | await browser.storage.local.get([
10 | 'engines',
11 | 'disabledEngines',
12 | 'searchCount'
13 | ]);
14 | const removeEngines = ['sogou', 'naver', 'exalead', 'webcite'];
15 |
16 | changes.engines = engines.filter(function (item) {
17 | return !removeEngines.includes(item);
18 | });
19 | changes.disabledEngines = disabledEngines.filter(function (item) {
20 | return !removeEngines.includes(item);
21 | });
22 |
23 | changes.taskRegistry = {lastTaskStart: 0, tabs: {}, tasks: {}};
24 | changes.storageRegistry = {};
25 | changes.lastStorageCleanup = 0;
26 |
27 | changes.lastEngineAccessCheck = 0;
28 |
29 | changes.setContextMenuEvent = 0;
30 |
31 | changes.searchModeContextMenu = 'tab'; // 'tab'
32 | changes.searchModeAction = 'tab'; // 'tab', 'url'
33 |
34 | changes.useCount = searchCount;
35 |
36 | await browser.storage.local.remove(['searchCount', 'openNewTab']);
37 |
38 | changes.storageVersion = revision;
39 | return browser.storage.local.set(changes);
40 | }
41 |
42 | export {message, revision, upgrade};
43 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20220102035029_add_showengineicons.js:
--------------------------------------------------------------------------------
1 | const message = 'Add showEngineIcons';
2 |
3 | const revision = '20220102035029_add_showengineicons';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 |
8 | changes.showEngineIcons = true;
9 |
10 | changes.storageVersion = revision;
11 | return browser.storage.local.set(changes);
12 | }
13 |
14 | export {message, revision, upgrade};
15 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20220102051642_add_search_engines.js:
--------------------------------------------------------------------------------
1 | const message = 'Add search engines';
2 |
3 | const revision = '20220102051642_add_search_engines';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const removeEngines = ['baidu', 'qihoo'];
13 |
14 | changes.engines = engines.filter(function (item) {
15 | return !removeEngines.includes(item);
16 | });
17 | changes.disabledEngines = disabledEngines.filter(function (item) {
18 | return !removeEngines.includes(item);
19 | });
20 |
21 | const newEngines = ['baidu', 'yahoo', 'qihoo', 'mailru'];
22 |
23 | changes.engines = changes.engines.concat(newEngines);
24 | changes.disabledEngines.push('mailru');
25 |
26 | changes.storageVersion = revision;
27 | return browser.storage.local.set(changes);
28 | }
29 |
30 | export {message, revision, upgrade};
31 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20220114113759_add_opencurrentdoc.js:
--------------------------------------------------------------------------------
1 | const message = 'Add openCurrentDoc';
2 |
3 | const revision = '20220114113759_add_opencurrentdoc';
4 |
5 | async function upgrade() {
6 | const changes = {
7 | openCurrentDocAction: true,
8 | openCurrentDocContextMenu: true
9 | };
10 |
11 | changes.storageVersion = revision;
12 | return browser.storage.local.set(changes);
13 | }
14 |
15 | export {message, revision, upgrade};
16 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20221220055629_add_theme_support.js:
--------------------------------------------------------------------------------
1 | import {getDayPrecisionEpoch} from 'utils/common';
2 |
3 | const message = 'Add theme support';
4 |
5 | const revision = '20221220055629_add_theme_support';
6 |
7 | async function upgrade() {
8 | const changes = {
9 | appTheme: 'auto', // auto, light, dark
10 | showContribPage: true,
11 | contribPageLastAutoOpen: 0,
12 | pinActionToolbarOpenCurrentDoc: true,
13 | pinActionToolbarOptions: false,
14 | pinActionToolbarContribute: true
15 | };
16 |
17 | const {installTime} = await browser.storage.local.get('installTime');
18 | changes.installTime = getDayPrecisionEpoch(installTime);
19 |
20 | changes.storageVersion = revision;
21 | return browser.storage.local.set(changes);
22 | }
23 |
24 | export {message, revision, upgrade};
25 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230201232239_remove_search_engines.js:
--------------------------------------------------------------------------------
1 | const message = 'Remove search engines';
2 |
3 | const revision = '20230201232239_remove_search_engines';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const removeEngines = ['baidu', 'qihoo', 'yahooJp', 'mailru'];
13 | const enableEngines = ['gigablast', 'megalodon'];
14 |
15 | changes.engines = engines.filter(function (item) {
16 | return !removeEngines.includes(item);
17 | });
18 | changes.disabledEngines = disabledEngines.filter(function (item) {
19 | return !removeEngines.includes(item) && !enableEngines.includes(item);
20 | });
21 |
22 | changes.storageVersion = revision;
23 | return browser.storage.local.set(changes);
24 | }
25 |
26 | export {message, revision, upgrade};
27 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230703074609_remove_gigablast.js:
--------------------------------------------------------------------------------
1 | const message = 'Remove Gigablast';
2 |
3 | const revision = '20230703074609_remove_gigablast';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const removeEngines = ['gigablast'];
13 |
14 | changes.engines = engines.filter(function (item) {
15 | return !removeEngines.includes(item);
16 | });
17 | changes.disabledEngines = disabledEngines.filter(function (item) {
18 | return !removeEngines.includes(item);
19 | });
20 |
21 | changes.storageVersion = revision;
22 | return browser.storage.local.set(changes);
23 | }
24 |
25 | export {message, revision, upgrade};
26 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230704125533_remove_opencurrentdocaction.js:
--------------------------------------------------------------------------------
1 | const message = 'Remove openCurrentDocAction';
2 |
3 | const revision = '20230704125533_remove_opencurrentdocaction';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 |
8 | await browser.storage.local.remove('openCurrentDocAction');
9 |
10 | changes.storageVersion = revision;
11 | return browser.storage.local.set(changes);
12 | }
13 |
14 | export {message, revision, upgrade};
15 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230713165504_add_perma.cc.js:
--------------------------------------------------------------------------------
1 | const message = 'Add Perma.cc';
2 |
3 | const revision = '20230713165504_add_perma.cc';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines} = await browser.storage.local.get('engines');
8 |
9 | engines.splice(engines.indexOf('megalodon'), 0, 'permacc');
10 | changes.engines = engines;
11 |
12 | changes.storageVersion = revision;
13 | return browser.storage.local.set(changes);
14 | }
15 |
16 | export {message, revision, upgrade};
17 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230715152710_add_ghostarchive.js:
--------------------------------------------------------------------------------
1 | const message = 'Add Ghostarchive';
2 |
3 | const revision = '20230715152710_add_ghostarchive';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const newEngines = ['ghostarchive'];
13 |
14 | changes.engines = engines.concat(newEngines);
15 | changes.disabledEngines = disabledEngines.concat(newEngines);
16 |
17 | changes.storageVersion = revision;
18 | return browser.storage.local.set(changes);
19 | }
20 |
21 | export {message, revision, upgrade};
22 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20230718120215_add_webcite.js:
--------------------------------------------------------------------------------
1 | const message = 'Add WebCite';
2 |
3 | const revision = '20230718120215_add_webcite';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const newEngines = ['webcite'];
13 |
14 | changes.engines = engines.concat(newEngines);
15 | changes.disabledEngines = disabledEngines.concat(newEngines);
16 |
17 | changes.storageVersion = revision;
18 | return browser.storage.local.set(changes);
19 | }
20 |
21 | export {message, revision, upgrade};
22 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20240514170322_add_appversion.js:
--------------------------------------------------------------------------------
1 | const message = 'Add appVersion';
2 |
3 | const revision = '20240514170322_add_appversion';
4 |
5 | async function upgrade() {
6 | const changes = {
7 | appVersion: '',
8 | menuItems: [],
9 | privateMenuItems: []
10 | };
11 |
12 | changes.storageVersion = revision;
13 | return browser.storage.local.set(changes);
14 | }
15 |
16 | export {message, revision, upgrade};
17 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20240619180111_add_menuchangeevent.js:
--------------------------------------------------------------------------------
1 | const message = 'Add menuChangeEvent';
2 |
3 | const revision = '20240619180111_add_menuchangeevent';
4 |
5 | async function upgrade() {
6 | const changes = {
7 | menuChangeEvent: 0,
8 | privateMenuChangeEvent: 0
9 | };
10 |
11 | await browser.storage.local.remove('setContextMenuEvent');
12 |
13 | changes.storageVersion = revision;
14 | return browser.storage.local.set(changes);
15 | }
16 |
17 | export {message, revision, upgrade};
18 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20240928183956_remove_search_engines.js:
--------------------------------------------------------------------------------
1 | const message = 'Remove search engines';
2 |
3 | const revision = '20240928183956_remove_search_engines';
4 |
5 | async function upgrade(context) {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const removeEngines = ['google', 'googleText', 'yahoo'];
13 | const enableEngines = [];
14 |
15 | if (context.install) {
16 | enableEngines.push('ghostarchive', 'webcite');
17 | }
18 |
19 | changes.engines = engines.filter(function (item) {
20 | return !removeEngines.includes(item);
21 | });
22 | changes.disabledEngines = disabledEngines.filter(function (item) {
23 | return !removeEngines.includes(item) && !enableEngines.includes(item);
24 | });
25 |
26 | changes.storageVersion = revision;
27 | return browser.storage.local.set(changes);
28 | }
29 |
30 | export {message, revision, upgrade};
31 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/20241213110403_remove_bing.js:
--------------------------------------------------------------------------------
1 | const message = 'Remove Bing';
2 |
3 | const revision = '20241213110403_remove_bing';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | const {engines, disabledEngines} = await browser.storage.local.get([
8 | 'engines',
9 | 'disabledEngines'
10 | ]);
11 |
12 | const removeEngines = ['bing'];
13 | const enableEngines = ['memento'];
14 |
15 | changes.engines = engines.filter(function (item) {
16 | return !removeEngines.includes(item);
17 | });
18 | changes.disabledEngines = disabledEngines.filter(function (item) {
19 | return !removeEngines.includes(item) && !enableEngines.includes(item);
20 | });
21 |
22 | changes.storageVersion = revision;
23 | return browser.storage.local.set(changes);
24 | }
25 |
26 | export {message, revision, upgrade};
27 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/SJltHx2rW.js:
--------------------------------------------------------------------------------
1 | const message = 'Initial version';
2 |
3 | const revision = 'SJltHx2rW';
4 |
5 | async function upgrade() {
6 | const changes = {
7 | engines: [
8 | 'archiveOrg',
9 | 'google',
10 | 'googleText',
11 | 'bing',
12 | 'yandex',
13 | 'archiveIs',
14 | 'memento',
15 | 'webcite',
16 | 'exalead',
17 | 'gigablast',
18 | 'sogou',
19 | 'qihoo',
20 | 'baidu',
21 | 'naver',
22 | 'yahooJp',
23 | 'megalodon'
24 | ],
25 | disabledEngines: [
26 | 'googleText',
27 | 'memento',
28 | 'webcite',
29 | 'exalead',
30 | 'gigablast',
31 | 'qihoo',
32 | 'baidu',
33 | 'naver',
34 | 'yahooJp',
35 | 'megalodon'
36 | ],
37 | showInContextMenu: 'link', // 'all', 'link', 'false'
38 | searchAllEnginesContextMenu: 'sub', // 'main', 'sub', 'false'
39 | searchAllEnginesAction: 'sub', // 'main', 'sub', 'false'
40 | showPageAction: true,
41 | openNewTab: true,
42 | tabInBackgound: false
43 | };
44 |
45 | changes.storageVersion = revision;
46 | return browser.storage.local.set(changes);
47 | }
48 |
49 | export {message, revision, upgrade};
50 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/SkhmnNhMG.js:
--------------------------------------------------------------------------------
1 | const message = 'Add installTime, searchCount and contribPageLastOpen';
2 |
3 | const revision = 'SkhmnNhMG';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | changes.installTime = new Date().getTime();
8 | changes.searchCount = 0;
9 | changes.contribPageLastOpen = 0;
10 |
11 | changes.storageVersion = revision;
12 | return browser.storage.local.set(changes);
13 | }
14 |
15 | export {message, revision, upgrade};
16 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/rJXbW1ZHmM.js:
--------------------------------------------------------------------------------
1 | const message = 'Set showPageAction to false';
2 |
3 | const revision = 'rJXbW1ZHmM';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 | changes.showPageAction = false;
8 |
9 | changes.storageVersion = revision;
10 | return browser.storage.local.set(changes);
11 | }
12 |
13 | export {message, revision, upgrade};
14 |
--------------------------------------------------------------------------------
/src/storage/revisions/local/yjRtkzy.js:
--------------------------------------------------------------------------------
1 | const message = 'Add archiveOrgAll and archiveIsAll';
2 |
3 | const revision = 'yjRtkzy';
4 |
5 | async function upgrade() {
6 | const changes = {};
7 |
8 | const {engines, disabledEngines} = await browser.storage.local.get([
9 | 'engines',
10 | 'disabledEngines'
11 | ]);
12 |
13 | engines.splice(engines.indexOf('archiveOrg') + 1, 0, 'archiveOrgAll');
14 | engines.splice(engines.indexOf('archiveIs') + 1, 0, 'archiveIsAll');
15 | changes.engines = engines;
16 | changes.disabledEngines = disabledEngines.concat([
17 | 'archiveOrgAll',
18 | 'archiveIsAll'
19 | ]);
20 |
21 | changes.storageVersion = revision;
22 | return browser.storage.local.set(changes);
23 | }
24 |
25 | export {message, revision, upgrade};
26 |
--------------------------------------------------------------------------------
/src/storage/revisions/session/20240514122825_initial_version.js:
--------------------------------------------------------------------------------
1 | const message = 'Initial version';
2 |
3 | const revision = '20240514122825_initial_version';
4 |
5 | async function upgrade() {
6 | const changes = {
7 | platformInfo: null,
8 | menuChangeEvent: 0,
9 | privateMenuChangeEvent: 0,
10 | tabRevisions: []
11 | };
12 |
13 | changes.storageVersion = revision;
14 | return browser.storage.session.set(changes);
15 | }
16 |
17 | export {message, revision, upgrade};
18 |
--------------------------------------------------------------------------------
/src/storage/storage.js:
--------------------------------------------------------------------------------
1 | import {capitalizeFirstLetter, lowercaseFirstLetter} from 'utils/common';
2 | import {storageRevisions} from 'utils/config';
3 |
4 | async function isStorageArea({area = 'local'} = {}) {
5 | try {
6 | await browser.storage[area].get('');
7 | return true;
8 | } catch (err) {
9 | return false;
10 | }
11 | }
12 |
13 | const storageReady = {local: false, session: false, sync: false};
14 | async function isStorageReady({area = 'local'} = {}) {
15 | if (storageReady[area]) {
16 | return true;
17 | } else {
18 | const {storageVersion} = await browser.storage[area].get('storageVersion');
19 | if (storageVersion && storageVersion === storageRevisions[area]) {
20 | storageReady[area] = true;
21 | return true;
22 | }
23 | }
24 |
25 | return false;
26 | }
27 |
28 | async function ensureStorageReady({area = 'local'} = {}) {
29 | if (!storageReady[area]) {
30 | return new Promise((resolve, reject) => {
31 | let stop;
32 |
33 | const checkStorage = async function () {
34 | if (await isStorageReady({area})) {
35 | self.clearTimeout(timeoutId);
36 | resolve();
37 | } else if (stop) {
38 | reject(new Error(`Storage (${area}) is not ready`));
39 | } else {
40 | self.setTimeout(checkStorage, 30);
41 | }
42 | };
43 |
44 | const timeoutId = self.setTimeout(function () {
45 | stop = true;
46 | }, 60000); // 1 minute
47 |
48 | checkStorage();
49 | });
50 | }
51 | }
52 |
53 | function processStorageKey(key, contextName, {encode = true} = {}) {
54 | if (encode) {
55 | return `${contextName}${capitalizeFirstLetter(key)}`;
56 | } else {
57 | return lowercaseFirstLetter(key.replace(new RegExp(`^${contextName}`), ''));
58 | }
59 | }
60 |
61 | function processStorageData(data, contextName, {encode = true} = {}) {
62 | if (typeof data === 'string') {
63 | return processStorageKey(data, contextName, {encode});
64 | } else if (Array.isArray(data)) {
65 | const items = [];
66 |
67 | for (const item of data) {
68 | items.push(processStorageKey(item, contextName, {encode}));
69 | }
70 |
71 | return items;
72 | } else {
73 | const items = {};
74 |
75 | for (const [key, value] of Object.entries(data)) {
76 | items[processStorageKey(key, contextName, {encode})] = value;
77 | }
78 |
79 | return items;
80 | }
81 | }
82 |
83 | function encodeStorageData(data, context) {
84 | if (context?.active) {
85 | return processStorageData(data, context.name, {encode: true});
86 | }
87 |
88 | return data;
89 | }
90 |
91 | function decodeStorageData(data, context) {
92 | if (context?.active) {
93 | return processStorageData(data, context.name, {encode: false});
94 | }
95 |
96 | return data;
97 | }
98 |
99 | async function get(keys = null, {area = 'local', context = null} = {}) {
100 | await ensureStorageReady({area});
101 |
102 | return decodeStorageData(
103 | await browser.storage[area].get(encodeStorageData(keys, context)),
104 | context
105 | );
106 | }
107 |
108 | async function set(obj, {area = 'local', context = null} = {}) {
109 | await ensureStorageReady({area});
110 |
111 | return browser.storage[area].set(encodeStorageData(obj, context));
112 | }
113 |
114 | async function remove(keys, {area = 'local', context = null} = {}) {
115 | await ensureStorageReady({area});
116 |
117 | return browser.storage[area].remove(encodeStorageData(keys, context));
118 | }
119 |
120 | async function clear({area = 'local'} = {}) {
121 | await ensureStorageReady({area});
122 | return browser.storage[area].clear();
123 | }
124 |
125 | export default {get, set, remove, clear};
126 | export {isStorageArea, isStorageReady, encodeStorageData, decodeStorageData};
127 |
--------------------------------------------------------------------------------
/src/tab/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | New Tab
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/tab/main.js:
--------------------------------------------------------------------------------
1 | async function getLocationData() {
2 | const token = new URL(window.location.href).searchParams.get('id');
3 |
4 | return browser.runtime.sendMessage({
5 | id: 'storageRequest',
6 | asyncResponse: true,
7 | saveReceipt: true,
8 | storageId: token
9 | });
10 | }
11 |
12 | function setLocation(tabUrl, keepHistory) {
13 | if (keepHistory) {
14 | window.location.href = tabUrl;
15 | } else {
16 | window.location.replace(tabUrl);
17 | }
18 | }
19 |
20 | async function setupTab(steps) {
21 | return browser.runtime.sendMessage({id: 'setupTab', steps});
22 | }
23 |
24 | async function start() {
25 | const data = await getLocationData();
26 |
27 | if (data.setupSteps) {
28 | await setupTab(data.setupSteps);
29 | }
30 |
31 | setLocation(data.tabUrl, data.keepHistory);
32 | }
33 |
34 | function init() {
35 | start();
36 | }
37 |
38 | init();
39 |
--------------------------------------------------------------------------------
/src/tools/main.js:
--------------------------------------------------------------------------------
1 | import {validateUrl} from 'utils/app';
2 | import {runOnce} from 'utils/common';
3 | import {pageArchiveHosts, linkArchiveUrlRx} from 'utils/data';
4 |
5 | function main() {
6 | self.openCurrentDoc = async function () {
7 | let docUrl;
8 | const hostname = window.location.hostname;
9 |
10 | for (const [engine, hosts] of Object.entries(pageArchiveHosts)) {
11 | if (hosts.includes(hostname)) {
12 | if (engine === 'archiveOrg') {
13 | const baseNode = document.querySelector('#wm-ipp-base');
14 | if (baseNode) {
15 | const shadowRoot =
16 | chrome.dom?.openOrClosedShadowRoot(baseNode) ||
17 | baseNode.openOrClosedShadowRoot ||
18 | baseNode.shadowRoot;
19 |
20 | if (shadowRoot) {
21 | docUrl = shadowRoot.querySelector(
22 | '#wm-toolbar input#wmtbURL'
23 | )?.value;
24 | } else {
25 | docUrl = window.location.href.match(
26 | linkArchiveUrlRx.archiveOrg
27 | )?.[1];
28 | }
29 | }
30 | } else if (engine === 'archiveIs') {
31 | docUrl = document.querySelector(
32 | '#HEADER form[action*="/search/"] input[type=text]'
33 | )?.value;
34 | } else if (engine === 'yandex') {
35 | docUrl = document.querySelector('#yandex-cache-hdr > span > a')?.href;
36 | } else if (engine === 'permacc') {
37 | docUrl = document.querySelector('._livepage a')?.href;
38 | } else if (engine === 'megalodon') {
39 | docUrl = window.location.href.match(linkArchiveUrlRx.megalodon)?.[1];
40 | } else if (engine === 'ghostarchive') {
41 | docUrl = document.querySelector('#searchInput')?.value;
42 | } else if (engine === 'webcite') {
43 | docUrl = document
44 | .querySelector('frame[name="nav"]')
45 | ?.contentDocument.querySelector(
46 | 'tr:first-child td:nth-child(2) a'
47 | )?.href;
48 | }
49 |
50 | break;
51 | }
52 | }
53 |
54 | if (validateUrl(docUrl)) {
55 | await browser.runtime.sendMessage({id: 'showPage', url: docUrl});
56 | } else {
57 | await browser.runtime.sendMessage({
58 | id: 'notification',
59 | messageId: 'error_currentDocUrlNotFound'
60 | });
61 | }
62 | };
63 | }
64 |
65 | if (runOnce('toolsModule')) {
66 | main();
67 | }
68 |
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | import {v4 as uuidv4} from 'uuid';
2 |
3 | import storage from 'storage/storage';
4 | import {getScriptFunction} from 'utils/scripts';
5 | import {targetEnv, mv3} from 'utils/config';
6 |
7 | function getText(messageName, substitutions) {
8 | return browser.i18n.getMessage(messageName, substitutions);
9 | }
10 |
11 | async function executeScript({
12 | files = null,
13 | func = null,
14 | args = null,
15 | tabId = null,
16 | frameIds = [0],
17 | allFrames = false,
18 | world = 'ISOLATED',
19 | injectImmediately = true,
20 | unwrapResults = true,
21 |
22 | code = ''
23 | }) {
24 | if (mv3) {
25 | const params = {target: {tabId}, world};
26 |
27 | // Safari 17: allFrames and frameIds cannot both be specified,
28 | // fixed in Safari 18.
29 | if (allFrames) {
30 | params.target.allFrames = true;
31 | } else {
32 | params.target.frameIds = frameIds;
33 | }
34 |
35 | if (files) {
36 | params.files = files;
37 | } else {
38 | params.func = func;
39 |
40 | if (args) {
41 | params.args = args;
42 | }
43 | }
44 |
45 | if (targetEnv !== 'safari') {
46 | params.injectImmediately = injectImmediately;
47 | }
48 |
49 | const results = await browser.scripting.executeScript(params);
50 |
51 | if (unwrapResults) {
52 | return results.map(item => item.result);
53 | } else {
54 | return results;
55 | }
56 | } else {
57 | const params = {frameId: frameIds[0]};
58 |
59 | if (files) {
60 | params.file = files[0];
61 | } else {
62 | params.code = code;
63 | }
64 |
65 | if (injectImmediately) {
66 | params.runAt = 'document_start';
67 | }
68 |
69 | return browser.tabs.executeScript(tabId, params);
70 | }
71 | }
72 |
73 | function executeScriptMainContext({
74 | files = null,
75 | func = null,
76 | args = null,
77 | allFrames = false,
78 | injectImmediately = true,
79 |
80 | onLoadCallback = null,
81 | setNonce = true
82 | } = {}) {
83 | // Must be called from a content script, `args[0]` must be a trusted string in MV2.
84 | if (mv3) {
85 | return browser.runtime.sendMessage({
86 | id: 'executeScript',
87 | setSenderTabId: true,
88 | setSenderFrameId: true,
89 | params: {files, func, args, allFrames, world: 'MAIN', injectImmediately}
90 | });
91 | } else {
92 | if (allFrames) {
93 | throw new Error('Executing code in all frames is not supported in MV2.');
94 | }
95 |
96 | let nonce;
97 | if (setNonce && ['firefox', 'safari'].includes(targetEnv)) {
98 | const nonceNode = document.querySelector('script[nonce]');
99 | if (nonceNode) {
100 | nonce = nonceNode.nonce;
101 | }
102 | }
103 |
104 | const script = document.createElement('script');
105 | if (nonce) {
106 | script.nonce = nonce;
107 | }
108 |
109 | if (files) {
110 | script.onload = function (ev) {
111 | ev.target.remove();
112 |
113 | if (onLoadCallback) {
114 | onLoadCallback();
115 | }
116 | };
117 |
118 | script.src = files[0];
119 | document.documentElement.appendChild(script);
120 | } else {
121 | const string = `(${getScriptFunction(func).toString()})${args ? `("${args[0]}")` : '()'}`;
122 |
123 | script.textContent = string;
124 | document.documentElement.appendChild(script);
125 |
126 | script.remove();
127 |
128 | if (onLoadCallback) {
129 | onLoadCallback();
130 | }
131 | }
132 | }
133 | }
134 |
135 | async function createTab({
136 | url = '',
137 | token = '',
138 | index = null,
139 | active = true,
140 | openerTabId = null,
141 | getTab = false
142 | } = {}) {
143 | if (!url) {
144 | url = getNewTabUrl(token);
145 | }
146 |
147 | const props = {url, active};
148 |
149 | if (index !== null) {
150 | props.index = index;
151 | }
152 | if (openerTabId !== null) {
153 | props.openerTabId = openerTabId;
154 | }
155 |
156 | let tab = await browser.tabs.create(props);
157 |
158 | if (getTab) {
159 | if (targetEnv === 'samsung') {
160 | // Samsung Internet 13: tabs.create returns previously active tab.
161 | // Samsung Internet 13: tabs.query may not immediately return newly created tabs.
162 | let count = 1;
163 | while (count <= 500 && (!tab || tab.url !== url)) {
164 | [tab] = await browser.tabs.query({lastFocusedWindow: true, url});
165 |
166 | await sleep(20);
167 | count += 1;
168 | }
169 | }
170 |
171 | return tab;
172 | }
173 | }
174 |
175 | function getNewTabUrl(token) {
176 | if (!token) {
177 | token = uuidv4();
178 | }
179 |
180 | return `${browser.runtime.getURL('/src/tab/index.html')}?id=${token}`;
181 | }
182 |
183 | async function getActiveTab() {
184 | const [tab] = await browser.tabs.query({
185 | lastFocusedWindow: true,
186 | active: true
187 | });
188 | return tab;
189 | }
190 |
191 | async function isValidTab({tab, tabId = null} = {}) {
192 | if (!tab && tabId !== null) {
193 | tab = await browser.tabs.get(tabId).catch(err => null);
194 | }
195 |
196 | if (tab && tab.id !== browser.tabs.TAB_ID_NONE) {
197 | return true;
198 | }
199 | }
200 |
201 | let platformInfo;
202 | async function getPlatformInfo() {
203 | if (platformInfo) {
204 | return platformInfo;
205 | }
206 |
207 | if (mv3) {
208 | ({platformInfo} = await storage.get('platformInfo', {area: 'session'}));
209 | } else {
210 | try {
211 | platformInfo = JSON.parse(window.sessionStorage.getItem('platformInfo'));
212 | } catch (err) {}
213 | }
214 |
215 | if (!platformInfo) {
216 | let os, arch;
217 |
218 | if (targetEnv === 'samsung') {
219 | // Samsung Internet 13: runtime.getPlatformInfo fails.
220 | os = 'android';
221 | arch = '';
222 | } else if (targetEnv === 'safari') {
223 | // Safari: runtime.getPlatformInfo returns 'ios' on iPadOS.
224 | ({os, arch} = await browser.runtime.sendNativeMessage('application.id', {
225 | id: 'getPlatformInfo'
226 | }));
227 | } else {
228 | ({os, arch} = await browser.runtime.getPlatformInfo());
229 | }
230 |
231 | platformInfo = {os, arch};
232 |
233 | if (mv3) {
234 | await storage.set({platformInfo}, {area: 'session'});
235 | } else {
236 | try {
237 | window.sessionStorage.setItem(
238 | 'platformInfo',
239 | JSON.stringify(platformInfo)
240 | );
241 | } catch (err) {}
242 | }
243 | }
244 |
245 | return platformInfo;
246 | }
247 |
248 | async function getPlatform() {
249 | if (!isBackgroundPageContext()) {
250 | return browser.runtime.sendMessage({id: 'getPlatform'});
251 | }
252 |
253 | let {os, arch} = await getPlatformInfo();
254 |
255 | if (os === 'win') {
256 | os = 'windows';
257 | } else if (os === 'mac') {
258 | os = 'macos';
259 | }
260 |
261 | if (['x86-32', 'i386'].includes(arch)) {
262 | arch = '386';
263 | } else if (['x86-64', 'x86_64'].includes(arch)) {
264 | arch = 'amd64';
265 | } else if (arch.startsWith('arm')) {
266 | arch = 'arm';
267 | }
268 |
269 | const isWindows = os === 'windows';
270 | const isMacos = os === 'macos';
271 | const isLinux = os === 'linux';
272 | const isAndroid = os === 'android';
273 | const isIos = os === 'ios';
274 | const isIpados = os === 'ipados';
275 |
276 | const isMobile = ['android', 'ios', 'ipados'].includes(os);
277 |
278 | const isChrome = targetEnv === 'chrome';
279 | const isEdge =
280 | ['chrome', 'edge'].includes(targetEnv) &&
281 | /\sedg(?:e|a|ios)?\//i.test(navigator.userAgent);
282 | const isFirefox = targetEnv === 'firefox';
283 | const isOpera =
284 | ['chrome', 'opera'].includes(targetEnv) &&
285 | /\sopr\//i.test(navigator.userAgent);
286 | const isSafari = targetEnv === 'safari';
287 | const isSamsung = targetEnv === 'samsung';
288 |
289 | return {
290 | os,
291 | arch,
292 | targetEnv,
293 | isWindows,
294 | isMacos,
295 | isLinux,
296 | isAndroid,
297 | isIos,
298 | isIpados,
299 | isMobile,
300 | isChrome,
301 | isEdge,
302 | isFirefox,
303 | isOpera,
304 | isSafari,
305 | isSamsung
306 | };
307 | }
308 |
309 | async function isAndroid() {
310 | return (await getPlatform()).isAndroid;
311 | }
312 |
313 | async function isMobile() {
314 | return (await getPlatform()).isMobile;
315 | }
316 |
317 | function getDarkColorSchemeQuery() {
318 | return window.matchMedia('(prefers-color-scheme: dark)');
319 | }
320 |
321 | function getDayPrecisionEpoch(epoch) {
322 | if (!epoch) {
323 | epoch = Date.now();
324 | }
325 |
326 | return epoch - (epoch % 86400000);
327 | }
328 |
329 | function isBackgroundPageContext() {
330 | return self.location.href.startsWith(
331 | browser.runtime.getURL('/src/background/')
332 | );
333 | }
334 |
335 | function getExtensionDomain() {
336 | try {
337 | const {hostname} = new URL(
338 | browser.runtime.getURL('/src/background/script.js')
339 | );
340 |
341 | return hostname;
342 | } catch (err) {}
343 |
344 | return null;
345 | }
346 |
347 | function getRandomInt(min, max) {
348 | return Math.floor(Math.random() * (max - min + 1)) + min;
349 | }
350 |
351 | function capitalizeFirstLetter(string, {locale = 'en-US'} = {}) {
352 | return string.replace(/^\p{CWU}/u, char => char.toLocaleUpperCase(locale));
353 | }
354 |
355 | function lowercaseFirstLetter(string, {locale = 'en-US'} = {}) {
356 | return string.replace(/^\p{CWL}/u, char => char.toLocaleLowerCase(locale));
357 | }
358 |
359 | function getCharCount(string) {
360 | return [...string].length;
361 | }
362 |
363 | function querySelectorXpath(selector, {rootNode = null} = {}) {
364 | rootNode = rootNode || document;
365 |
366 | return document.evaluate(
367 | selector,
368 | rootNode,
369 | null,
370 | XPathResult.FIRST_ORDERED_NODE_TYPE,
371 | null
372 | ).singleNodeValue;
373 | }
374 |
375 | function nodeQuerySelector(
376 | selector,
377 | {rootNode = null, selectorType = 'css'} = {}
378 | ) {
379 | rootNode = rootNode || document;
380 |
381 | return selectorType === 'css'
382 | ? rootNode.querySelector(selector)
383 | : querySelectorXpath(selector, {rootNode});
384 | }
385 |
386 | function findNode(
387 | selector,
388 | {
389 | timeout = 60000,
390 | throwError = true,
391 | observerOptions = null,
392 | rootNode = null,
393 | selectorType = 'css'
394 | } = {}
395 | ) {
396 | return new Promise((resolve, reject) => {
397 | rootNode = rootNode || document;
398 |
399 | const el = nodeQuerySelector(selector, {rootNode, selectorType});
400 | if (el) {
401 | resolve(el);
402 | return;
403 | }
404 |
405 | const observer = new MutationObserver(function (mutations, obs) {
406 | const el = nodeQuerySelector(selector, {rootNode, selectorType});
407 | if (el) {
408 | obs.disconnect();
409 | window.clearTimeout(timeoutId);
410 | resolve(el);
411 | }
412 | });
413 |
414 | const options = {
415 | childList: true,
416 | subtree: true
417 | };
418 | if (observerOptions) {
419 | Object.assign(options, observerOptions);
420 | }
421 |
422 | observer.observe(rootNode, options);
423 |
424 | const timeoutId = window.setTimeout(function () {
425 | observer.disconnect();
426 |
427 | if (throwError) {
428 | reject(new Error(`DOM node not found: ${selector}`));
429 | } else {
430 | resolve();
431 | }
432 | }, timeout);
433 | });
434 | }
435 |
436 | async function processNode(
437 | selector,
438 | actionFn,
439 | {
440 | timeout = 60000,
441 | throwError = true,
442 | observerOptions = null,
443 | rootNode = null,
444 | selectorType = 'css',
445 | reprocess = false
446 | } = {}
447 | ) {
448 | rootNode = rootNode || document;
449 |
450 | let node = await findNode(selector, {
451 | timeout,
452 | throwError,
453 | observerOptions,
454 | rootNode,
455 | selectorType
456 | });
457 |
458 | if (reprocess) {
459 | const observer = new MutationObserver(function (mutations, obs) {
460 | const el = nodeQuerySelector(selector, {rootNode, selectorType});
461 | if (el && !el.isSameNode(node)) {
462 | node = el;
463 | actionFn(node);
464 | }
465 | });
466 |
467 | const options = {
468 | childList: true,
469 | subtree: true
470 | };
471 | if (observerOptions) {
472 | Object.assign(options, observerOptions);
473 | }
474 |
475 | observer.observe(rootNode, options);
476 |
477 | window.setTimeout(function () {
478 | observer.disconnect();
479 | }, timeout);
480 | }
481 |
482 | return actionFn(node);
483 | }
484 |
485 | function waitForDocumentLoad() {
486 | return new Promise(resolve => {
487 | function checkState() {
488 | if (document.readyState === 'complete') {
489 | resolve();
490 | } else {
491 | document.addEventListener('readystatechange', checkState, {once: true});
492 | }
493 | }
494 |
495 | checkState();
496 | });
497 | }
498 |
499 | function makeDocumentVisible() {
500 | const eventName = uuidv4();
501 |
502 | function dispatchVisibilityState() {
503 | document.dispatchEvent(
504 | new CustomEvent(eventName, {detail: document.visibilityState})
505 | );
506 | }
507 |
508 | document.addEventListener('visibilitychange', dispatchVisibilityState, {
509 | capture: true
510 | });
511 |
512 | executeScriptMainContext({func: 'makeDocumentVisible', args: [eventName]});
513 | }
514 |
515 | function getStore(name, {content = null} = {}) {
516 | name = `${name}Store`;
517 |
518 | if (!self[name]) {
519 | self[name] = content || {};
520 | }
521 |
522 | return self[name];
523 | }
524 |
525 | function runOnce(name, func) {
526 | const store = getStore('run');
527 |
528 | if (!store[name]) {
529 | store[name] = true;
530 |
531 | if (!func) {
532 | return true;
533 | }
534 |
535 | return func();
536 | }
537 | }
538 |
539 | async function requestLock(name, func, {timeout = 60000} = {}) {
540 | const params = [name];
541 | if (timeout) {
542 | params.push({signal: AbortSignal.timeout(timeout)});
543 | }
544 |
545 | return navigator.locks.request(...params, func);
546 | }
547 |
548 | function sleep(ms) {
549 | return new Promise(resolve => self.setTimeout(resolve, ms));
550 | }
551 |
552 | export {
553 | getText,
554 | executeScript,
555 | executeScriptMainContext,
556 | createTab,
557 | getNewTabUrl,
558 | getActiveTab,
559 | isValidTab,
560 | getPlatformInfo,
561 | getPlatform,
562 | isAndroid,
563 | isMobile,
564 | getDarkColorSchemeQuery,
565 | getDayPrecisionEpoch,
566 | isBackgroundPageContext,
567 | getExtensionDomain,
568 | getRandomInt,
569 | capitalizeFirstLetter,
570 | lowercaseFirstLetter,
571 | getCharCount,
572 | querySelectorXpath,
573 | nodeQuerySelector,
574 | findNode,
575 | processNode,
576 | waitForDocumentLoad,
577 | makeDocumentVisible,
578 | getStore,
579 | runOnce,
580 | requestLock,
581 | sleep
582 | };
583 |
--------------------------------------------------------------------------------
/src/utils/config.js:
--------------------------------------------------------------------------------
1 | const targetEnv = process.env.TARGET_ENV;
2 |
3 | const enableContributions = process.env.ENABLE_CONTRIBUTIONS === 'true';
4 |
5 | const storageRevisions = {
6 | local: process.env.STORAGE_REVISION_LOCAL,
7 | session: process.env.STORAGE_REVISION_SESSION
8 | };
9 |
10 | const appVersion = process.env.APP_VERSION;
11 |
12 | const mv3 = process.env.MV3 === 'true';
13 |
14 | export {targetEnv, enableContributions, storageRevisions, appVersion, mv3};
15 |
--------------------------------------------------------------------------------
/src/utils/data.js:
--------------------------------------------------------------------------------
1 | const optionKeys = [
2 | 'engines',
3 | 'disabledEngines',
4 | 'showInContextMenu',
5 | 'searchAllEnginesContextMenu',
6 | 'searchAllEnginesAction',
7 | 'showPageAction',
8 | 'tabInBackgound',
9 | 'searchModeAction',
10 | 'searchModeContextMenu',
11 | 'showEngineIcons',
12 | 'openCurrentDocContextMenu',
13 | 'appTheme',
14 | 'showContribPage',
15 | 'pinActionToolbarOpenCurrentDoc',
16 | 'pinActionToolbarOptions',
17 | 'pinActionToolbarContribute'
18 | ];
19 |
20 | const searchUrl = browser.runtime.getURL('/src/search/index.html') + '?id={id}';
21 |
22 | const engines = {
23 | archiveOrg: {
24 | target: 'https://web.archive.org/web/{url}'
25 | },
26 | archiveOrgAll: {
27 | target: 'https://web.archive.org/web/*/{url}'
28 | },
29 | yandex: {
30 | target: 'https://www.yandex.com/',
31 | isExec: true
32 | },
33 | archiveIs: {
34 | target: 'https://archive.is/newest/{url}'
35 | },
36 | archiveIsAll: {
37 | target: 'https://archive.is/{url}'
38 | },
39 | memento: {
40 | target: 'http://timetravel.mementoweb.org/memento/{date}/{url}'
41 | },
42 | megalodon: {
43 | target: 'https://megalodon.jp/?url={url}',
44 | isExec: true
45 | },
46 | permacc: {
47 | target: searchUrl,
48 | isTaskId: true
49 | },
50 | ghostarchive: {
51 | target: 'https://ghostarchive.org/search?term={url}',
52 | isExec: true
53 | },
54 | webcite: {
55 | target: 'https://webcitation.org/query?url={url}&date={date}'
56 | }
57 | };
58 |
59 | const engineIconAlias = {
60 | archiveOrgAll: 'archiveOrg',
61 | archiveIsAll: 'archiveIs'
62 | };
63 |
64 | const engineIconVariants = {
65 | archiveOrg: ['dark'],
66 | archiveIs: ['dark'],
67 | webcite: ['dark']
68 | };
69 |
70 | const rasterEngineIcons = ['ghostarchive'];
71 |
72 | // prettier-ignore
73 | const errorCodes = [
74 | 400,
75 | 403,
76 | 404,
77 | 408,
78 | 410,
79 | 429,
80 | 451,
81 | 500,
82 | 502,
83 | 503,
84 | 504,
85 | // Nonstandard
86 | 444,
87 | 450,
88 | 509,
89 | 530,
90 | 598,
91 | // Cloudflare
92 | 520,
93 | 521,
94 | 522,
95 | 523,
96 | 524,
97 | 525,
98 | 526,
99 | 527
100 | ];
101 |
102 | const pageArchiveHosts = {
103 | archiveOrg: ['web.archive.org'],
104 | archiveIs: [
105 | 'archive.is',
106 | 'archive.today',
107 | 'archive.ph',
108 | 'archive.vn',
109 | 'archive.fo',
110 | 'archive.li',
111 | 'archive.md',
112 | 'archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion'
113 | ],
114 | yandex: ['yandexwebcache.net'],
115 | permacc: ['perma.cc', 'rejouer.perma.cc'],
116 | megalodon: ['megalodon.jp'],
117 | ghostarchive: ['ghostarchive.org'],
118 | webcite: ['webcitation.org']
119 | };
120 |
121 | const linkArchiveHosts = {
122 | archiveOrg: ['web.archive.org'],
123 | archiveIs: [
124 | 'archive.is',
125 | 'archive.today',
126 | 'archive.ph',
127 | 'archive.vn',
128 | 'archive.fo',
129 | 'archive.li',
130 | 'archive.md',
131 | 'archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion'
132 | ],
133 | permacc: ['rejouer.perma.cc'],
134 | megalodon: ['megalodon.jp'],
135 | ghostarchive: ['ghostarchive.org']
136 | };
137 |
138 | const linkArchiveUrlRx = {
139 | archiveOrg: /^https?:\/\/web\.archive\.org\/web\/[0-9]+\/(.*)/i,
140 | archiveIs:
141 | /^https?:\/\/(?:archive\.(?:is|today|ph|vn|fo|li|md)|archiveiya74codqgiixo33q62qlrqtkgmcitqx5u2oeqnmn5bpcbiyd.onion)\/o\/.*?\/(.*)/i,
142 | permacc: /^https:\/\/rejouer\.perma\.cc\/(?:.*)\/mp_\/(.*)/i,
143 | megalodon: /https?:\/\/megalodon\.jp\/(?:\d+-)+\d+\/(.*)/i,
144 | ghostarchive: /^https:\/\/ghostarchive\.org\/(?:.*)\/mp_\/(.*)/i
145 | };
146 |
147 | const chromeDesktopUA =
148 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
149 |
150 | const chromeMobileUA =
151 | 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36';
152 |
153 | const supportUrl = 'https://github.com/dessant/web-archives/issues';
154 |
155 | export {
156 | optionKeys,
157 | engines,
158 | rasterEngineIcons,
159 | engineIconAlias,
160 | engineIconVariants,
161 | errorCodes,
162 | pageArchiveHosts,
163 | linkArchiveHosts,
164 | linkArchiveUrlRx,
165 | chromeDesktopUA,
166 | chromeMobileUA,
167 | supportUrl
168 | };
169 |
--------------------------------------------------------------------------------
/src/utils/engines.js:
--------------------------------------------------------------------------------
1 | import psl from 'psl';
2 |
3 | import {waitForDocumentLoad, getCharCount} from 'utils/common';
4 |
5 | function showEngineError({message, errorId, engine}) {
6 | if (!message) {
7 | message = browser.i18n.getMessage(
8 | errorId,
9 | browser.i18n.getMessage(`engineName_${engine}`)
10 | );
11 | }
12 | browser.runtime.sendMessage({
13 | id: 'notification',
14 | message,
15 | type: `${engine}Error`
16 | });
17 | }
18 |
19 | async function sendReceipt(storageIds) {
20 | if (storageIds.length) {
21 | const keys = [...storageIds];
22 | while (storageIds.length) {
23 | storageIds.pop();
24 | }
25 |
26 | await browser.runtime.sendMessage({
27 | id: 'storageReceipt',
28 | storageIds: keys
29 | });
30 | }
31 | }
32 |
33 | async function initSearch(searchFn, engine, taskId) {
34 | await waitForDocumentLoad();
35 |
36 | const task = await browser.runtime.sendMessage({
37 | id: 'storageRequest',
38 | asyncResponse: true,
39 | storageId: taskId
40 | });
41 |
42 | if (task) {
43 | const storageIds = [taskId, task.docId];
44 |
45 | try {
46 | const doc = await browser.runtime.sendMessage({
47 | id: 'storageRequest',
48 | asyncResponse: true,
49 | storageId: task.docId
50 | });
51 |
52 | if (doc) {
53 | await searchFn({
54 | session: task.session,
55 | search: task.search,
56 | doc,
57 | storageIds
58 | });
59 | } else {
60 | await sendReceipt(storageIds);
61 |
62 | showEngineError({errorId: 'error_sessionExpiredEngine', engine});
63 | }
64 | } catch (err) {
65 | await sendReceipt(storageIds);
66 |
67 | showEngineError({errorId: 'error_engine', engine});
68 |
69 | console.log(err.toString());
70 | throw err;
71 | }
72 | } else {
73 | showEngineError({errorId: 'error_sessionExpiredEngine', engine});
74 | }
75 | }
76 |
77 | function processResult(sourceUrl, resultUrl) {
78 | const source = new URL(sourceUrl);
79 | const result = new URL(resultUrl);
80 |
81 | const data = {
82 | sourceUrl,
83 | resultUrl,
84 | protocolMatch: source.protocol === result.protocol,
85 | hostMatch: source.hostname === result.hostname,
86 | hostWithoutCommonSubdomainsMatch: false,
87 | domainMatch: psl.get(source.hostname) === psl.get(result.hostname),
88 | pathMatch: source.pathname === result.pathname,
89 | sourceStartsWithResultPath: source.pathname.startsWith(result.pathname),
90 | resultStartsWithSourcePath: result.pathname.startsWith(source.pathname),
91 | sourcePathLength: getCharCount(source.pathname) - 1,
92 | resultPathLength: getCharCount(result.pathname) - 1,
93 | searchParamsMatch: source.search === result.search,
94 | sourceStartsWithResultSearchParams: source.search.startsWith(result.search),
95 | resultStartsWithSourceSearchParams: result.search.startsWith(source.search),
96 | sourceSearchParamsLength: getCharCount(source.search),
97 | resultSearchParamsLength: getCharCount(result.search)
98 | };
99 |
100 | if (
101 | source.hostname.replace(/^www\./i, '') ===
102 | result.hostname.replace(/^www\./i, '')
103 | ) {
104 | data.hostWithoutCommonSubdomainsMatch = true;
105 | }
106 |
107 | return data;
108 | }
109 |
110 | function getResultRank(item) {
111 | if (
112 | item.protocolMatch &&
113 | item.hostMatch &&
114 | item.pathMatch &&
115 | item.searchParamsMatch
116 | ) {
117 | return 1;
118 | }
119 |
120 | if (item.hostMatch && item.pathMatch && item.searchParamsMatch) {
121 | return 2;
122 | }
123 |
124 | if (
125 | item.hostWithoutCommonSubdomainsMatch &&
126 | item.pathMatch &&
127 | item.searchParamsMatch
128 | ) {
129 | return 3;
130 | }
131 |
132 | if (
133 | item.hostWithoutCommonSubdomainsMatch &&
134 | item.pathMatch &&
135 | ((item.resultSearchParamsLength &&
136 | item.sourceStartsWithResultSearchParams) ||
137 | (item.sourceSearchParamsLength &&
138 | item.resultStartsWithSourceSearchParams))
139 | ) {
140 | return 4;
141 | }
142 |
143 | if (item.hostWithoutCommonSubdomainsMatch && item.pathMatch) {
144 | return 5;
145 | }
146 |
147 | if (
148 | item.hostWithoutCommonSubdomainsMatch &&
149 | ((item.resultPathLength && item.sourceStartsWithResultPath) ||
150 | (item.sourcePathLength && item.resultStartsWithSourcePath))
151 | ) {
152 | return 6;
153 | }
154 |
155 | if (item.domainMatch && item.pathMatch && item.searchParamsMatch) {
156 | return 7;
157 | }
158 |
159 | if (
160 | item.domainMatch &&
161 | item.pathMatch &&
162 | ((item.resultSearchParamsLength &&
163 | item.sourceStartsWithResultSearchParams) ||
164 | (item.sourceSearchParamsLength &&
165 | item.resultStartsWithSourceSearchParams))
166 | ) {
167 | return 8;
168 | }
169 |
170 | if (item.domainMatch && item.pathMatch) {
171 | return 9;
172 | }
173 |
174 | if (
175 | item.domainMatch &&
176 | ((item.resultPathLength && item.sourceStartsWithResultPath) ||
177 | (item.sourcePathLength && item.resultStartsWithSourcePath))
178 | ) {
179 | return 10;
180 | }
181 |
182 | if (item.hostMatch) {
183 | return 11;
184 | }
185 |
186 | if (item.domainMatch) {
187 | return 12;
188 | }
189 | }
190 |
191 | function getRankedResults({sourceUrl, results} = {}) {
192 | const items = [];
193 |
194 | for (const result of results) {
195 | const resultDetails = processResult(sourceUrl, result.url);
196 | const rank = getResultRank(resultDetails);
197 |
198 | if (rank) {
199 | items.push({result, resultDetails, rank});
200 | }
201 | }
202 |
203 | items.sort(function (a, b) {
204 | if (a.rank < b.rank) {
205 | return -1;
206 | } else if (a.rank > b.rank) {
207 | return 1;
208 | } else if (
209 | a.resultDetails.resultUrl.length < b.resultDetails.resultUrl.length
210 | ) {
211 | return -1;
212 | } else if (
213 | a.resultDetails.resultUrl.length > b.resultDetails.resultUrl.length
214 | ) {
215 | return 1;
216 | } else {
217 | return 0;
218 | }
219 | });
220 |
221 | return items.map(item => item.result);
222 | }
223 |
224 | async function searchPermacc({session, search, doc} = {}) {
225 | const rsp = await fetch(
226 | `https://api.perma.cc/v1/public/archives/?format=json&limit=1&url=${encodeURIComponent(
227 | doc.docUrl
228 | )}`,
229 | {
230 | referrer: '',
231 | mode: 'cors',
232 | method: 'GET',
233 | credentials: 'omit'
234 | }
235 | );
236 |
237 | if (rsp.status !== 200) {
238 | throw new Error(`API response: ${rsp.status}, ${await rsp.text()}`);
239 | }
240 |
241 | const response = await rsp.json();
242 |
243 | const result = response.objects[0];
244 | if (result) {
245 | const tabUrl = `https://perma.cc/${result.guid}`;
246 |
247 | return tabUrl;
248 | }
249 | }
250 |
251 | export {
252 | showEngineError,
253 | sendReceipt,
254 | initSearch,
255 | getRankedResults,
256 | searchPermacc
257 | };
258 |
--------------------------------------------------------------------------------
/src/utils/registry.js:
--------------------------------------------------------------------------------
1 | import {v4 as uuidv4} from 'uuid';
2 | import {
3 | get as getIDB,
4 | set as setIDB,
5 | del as delIDB,
6 | createStore as createIDBStore
7 | } from 'idb-keyval';
8 | import Queue from 'p-queue';
9 |
10 | import storage from 'storage/storage';
11 | import {getTabRevisions} from 'utils/app';
12 | import {targetEnv} from 'utils/config';
13 |
14 | const storageQueue = new Queue({concurrency: 1});
15 | const registryQueue = new Queue({concurrency: 1});
16 |
17 | class RegistryStore {
18 | constructor() {
19 | this.idbStore = null;
20 | this.memoryStore = null;
21 | }
22 |
23 | initStore({area = '', force = false} = {}) {
24 | if (area === 'indexeddb') {
25 | if (!this.idbStore || force) {
26 | this.idbStore = createIDBStore('keyval-store', 'keyval');
27 | }
28 | } else if (area === 'memory') {
29 | if (!this.memoryStore) {
30 | this.memoryStore = {};
31 | }
32 | }
33 | }
34 |
35 | handleError(err, {area = ''} = {}) {
36 | if (
37 | area === 'indexeddb' &&
38 | err?.message?.includes('connection is closing')
39 | ) {
40 | console.log('IndexedDB error:', err.message);
41 |
42 | this.initStore({area, force: true});
43 | } else {
44 | throw err;
45 | }
46 | }
47 |
48 | async get(key, {area = ''} = {}) {
49 | this.initStore({area});
50 |
51 | if (area === 'indexeddb') {
52 | try {
53 | return await getIDB(key, this.idbStore);
54 | } catch (err) {
55 | this.handleError(err, {area});
56 | }
57 | } else if (area === 'local') {
58 | return (await storage.get(key))[key];
59 | } else if (area === 'memory') {
60 | return this.memoryStore[key];
61 | }
62 | }
63 |
64 | async set(key, value, {area = ''} = {}) {
65 | this.initStore({area});
66 |
67 | if (area === 'indexeddb') {
68 | try {
69 | await setIDB(key, value, this.idbStore);
70 | } catch (err) {
71 | this.handleError(err, {area});
72 |
73 | await setIDB(key, value, this.idbStore);
74 | }
75 | } else if (area === 'local') {
76 | await storage.set({[key]: value});
77 | } else if (area === 'memory') {
78 | this.memoryStore[key] = value;
79 | }
80 | }
81 |
82 | async remove(key, {area = ''} = {}) {
83 | this.initStore({area});
84 |
85 | if (area === 'indexeddb') {
86 | try {
87 | await delIDB(key, this.idbStore);
88 | } catch (err) {
89 | this.handleError(err, {area});
90 | }
91 | } else if (area === 'local') {
92 | await storage.remove(key);
93 | } else if (area === 'memory') {
94 | delete this.memoryStore[key];
95 | }
96 | }
97 | }
98 |
99 | const registryStore = new RegistryStore();
100 |
101 | function getStorageItemKeys(storageId) {
102 | return {metadataKey: `metadata_${storageId}`, dataKey: `data_${storageId}`};
103 | }
104 |
105 | async function _getStorageItem({
106 | storageId,
107 | metadata = false,
108 | data = false,
109 | area = 'local'
110 | } = {}) {
111 | const {metadataKey, dataKey} = getStorageItemKeys(storageId);
112 |
113 | if (metadata) {
114 | ({value: metadata} =
115 | (await registryStore.get(metadataKey, {area: 'local'})) || {});
116 | }
117 |
118 | if (data) {
119 | ({value: data} = (await registryStore.get(dataKey, {area})) || {});
120 | }
121 |
122 | return {metadata, data};
123 | }
124 |
125 | async function _setStorageItem({
126 | storageId,
127 | metadata = null,
128 | data = null,
129 | area = 'local'
130 | } = {}) {
131 | const {metadataKey, dataKey} = getStorageItemKeys(storageId);
132 |
133 | if (metadata !== null) {
134 | await registryStore.set(metadataKey, {value: metadata}, {area: 'local'});
135 | }
136 |
137 | if (data !== null) {
138 | await registryStore.set(dataKey, {value: data}, {area});
139 | }
140 | }
141 |
142 | async function _removeStorageItem({
143 | storageId,
144 | metadata = false,
145 | data = false,
146 | area = 'local'
147 | } = {}) {
148 | const {metadataKey, dataKey} = getStorageItemKeys(storageId);
149 |
150 | if (metadata) {
151 | await registryStore.remove(metadataKey, {area: 'local'});
152 | }
153 |
154 | if (data) {
155 | await registryStore.remove(dataKey, {area});
156 | }
157 | }
158 |
159 | async function addStorageItem(
160 | data,
161 | {
162 | token = '',
163 | receipts = null,
164 | expiryTime = 0,
165 | area = 'local',
166 | isTask = false
167 | } = {}
168 | ) {
169 | const storageId = token || uuidv4();
170 | const addTime = Date.now();
171 | const metadata = {area, addTime, receipts, alarms: [], isTask};
172 |
173 | if (expiryTime) {
174 | const alarmName = `delete-storage-item_${storageId}`;
175 | browser.alarms.create(alarmName, {delayInMinutes: expiryTime});
176 |
177 | metadata.alarms.push(alarmName);
178 | }
179 |
180 | await addStorageRegistryItem({storageId, addTime});
181 |
182 | await _setStorageItem({storageId, metadata, data, area});
183 |
184 | return storageId;
185 | }
186 |
187 | async function getStorageItem({storageId, saveReceipt = false} = {}) {
188 | const {metadata} = await _getStorageItem({storageId, metadata: true});
189 |
190 | if (metadata) {
191 | const {data} = await _getStorageItem({
192 | storageId,
193 | data: true,
194 | area: metadata.area
195 | });
196 |
197 | if (data) {
198 | if (saveReceipt) {
199 | await saveStorageItemReceipt({storageId});
200 | }
201 |
202 | return data;
203 | }
204 | }
205 | }
206 |
207 | async function deleteStorageItem({storageId, registry = true} = {}) {
208 | const {metadata} = await _getStorageItem({storageId, metadata: true});
209 |
210 | if (metadata) {
211 | await _removeStorageItem({storageId, data: true, area: metadata.area});
212 |
213 | for (const alarmName of metadata.alarms) {
214 | await browser.alarms.clear(alarmName);
215 | }
216 |
217 | await _removeStorageItem({storageId, metadata: true});
218 |
219 | if (registry) {
220 | if (metadata.isTask) {
221 | await deleteTaskRegistryItem({taskId: storageId});
222 | }
223 |
224 | await deleteStorageRegistryItem({storageId});
225 | }
226 | }
227 | }
228 |
229 | async function saveStorageItemReceipt({storageId} = {}) {
230 | await storageQueue.add(async function () {
231 | const {metadata} = await _getStorageItem({storageId, metadata: true});
232 |
233 | if (metadata && metadata.receipts) {
234 | metadata.receipts.received += 1;
235 |
236 | if (metadata.receipts.received < metadata.receipts.expected) {
237 | await _setStorageItem({storageId, metadata});
238 | } else {
239 | await deleteStorageItem({storageId});
240 | }
241 | }
242 | });
243 | }
244 |
245 | async function addStorageRegistryItem({storageId, addTime} = {}) {
246 | await registryQueue.add(async function () {
247 | const {storageRegistry} = await storage.get('storageRegistry');
248 | storageRegistry[storageId] = {addTime};
249 |
250 | await storage.set({storageRegistry});
251 | });
252 | }
253 |
254 | async function deleteStorageRegistryItem({storageId} = {}) {
255 | await registryQueue.add(async function () {
256 | const {storageRegistry} = await storage.get('storageRegistry');
257 | delete storageRegistry[storageId];
258 |
259 | await storage.set({storageRegistry});
260 | });
261 | }
262 |
263 | async function addTaskRegistryItem({taskId, tabId} = {}) {
264 | await registryQueue.add(async function () {
265 | const {taskRegistry} = await storage.get('taskRegistry');
266 | const addTime = Date.now();
267 |
268 | taskRegistry.lastTaskStart = addTime;
269 | taskRegistry.tabs[tabId] = {taskId};
270 | taskRegistry.tasks[taskId] = {tabId, addTime};
271 |
272 | await storage.set({taskRegistry});
273 | });
274 | }
275 |
276 | async function getTaskRegistryItem({taskId, tabId} = {}) {
277 | const {taskRegistry} = await storage.get('taskRegistry');
278 |
279 | if (tabId) {
280 | let tab = taskRegistry.tabs[tabId];
281 |
282 | if (!tab && ['safari'].includes(targetEnv)) {
283 | const tabRevisions = await getTabRevisions(tabId);
284 |
285 | if (tabRevisions) {
286 | for (const revision of tabRevisions) {
287 | tab = taskRegistry.tabs[revision];
288 |
289 | if (tab) {
290 | break;
291 | }
292 | }
293 | }
294 | }
295 |
296 | if (tab) {
297 | return {
298 | taskId: tab.taskId,
299 | ...taskRegistry.tasks[tab.taskId]
300 | };
301 | }
302 | } else if (taskId) {
303 | const task = taskRegistry.tasks[taskId];
304 |
305 | if (task) {
306 | return {taskId, ...task};
307 | }
308 | }
309 | }
310 |
311 | async function deleteTaskRegistryItem({taskId} = {}) {
312 | await registryQueue.add(async function () {
313 | const {taskRegistry} = await storage.get('taskRegistry');
314 | const taskIndex = taskRegistry.tasks[taskId];
315 |
316 | if (taskIndex) {
317 | const tabIndex = taskRegistry.tabs[taskIndex.tabId];
318 | if (tabIndex && tabIndex.taskId === taskId) {
319 | delete taskRegistry.tabs[taskIndex.tabId];
320 | }
321 | }
322 |
323 | delete taskRegistry.tasks[taskId];
324 |
325 | await storage.set({taskRegistry});
326 | });
327 | }
328 |
329 | async function cleanupRegistry() {
330 | await registryQueue.add(async function () {
331 | const {lastStorageCleanup} = await storage.get('lastStorageCleanup');
332 | // run at most once a day
333 | if (Date.now() - lastStorageCleanup > 86400000) {
334 | const {taskRegistry} = await storage.get('taskRegistry');
335 |
336 | for (const [taskId, taskIndex] of Object.entries(taskRegistry.tasks)) {
337 | // remove tasks older than 1 hour
338 | if (Date.now() - taskIndex.addTime > 3600000) {
339 | const tabIndex = taskRegistry.tabs[taskIndex.tabId];
340 | if (tabIndex && tabIndex.taskId === taskId) {
341 | delete taskRegistry.tabs[taskIndex.tabId];
342 | }
343 |
344 | delete taskRegistry.tasks[taskId];
345 | }
346 | }
347 |
348 | await storage.set({taskRegistry});
349 |
350 | const {storageRegistry} = await storage.get('storageRegistry');
351 |
352 | for (const [storageId, storageIndex] of Object.entries(storageRegistry)) {
353 | // remove storage items older than 1 hour
354 | if (Date.now() - storageIndex.addTime > 3600000) {
355 | await deleteStorageItem({storageId, registry: false});
356 |
357 | delete storageRegistry[storageId];
358 | }
359 | }
360 |
361 | await storage.set({storageRegistry, lastStorageCleanup: Date.now()});
362 | }
363 | });
364 | }
365 |
366 | export default {
367 | addStorageItem,
368 | getStorageItem,
369 | deleteStorageItem,
370 | saveStorageItemReceipt,
371 | addTaskRegistryItem,
372 | getTaskRegistryItem,
373 | cleanupRegistry
374 | };
375 |
--------------------------------------------------------------------------------
/src/utils/scripts.js:
--------------------------------------------------------------------------------
1 | function makeDocumentVisibleScript(eventName) {
2 | let visibilityState = document.visibilityState;
3 |
4 | function updateVisibilityState(ev) {
5 | visibilityState = ev.detail;
6 | }
7 |
8 | document.addEventListener(eventName, updateVisibilityState, {
9 | capture: true
10 | });
11 |
12 | let lastCallTime = 0;
13 | window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, {
14 | apply(target, thisArg, argumentsList) {
15 | if (visibilityState === 'visible') {
16 | return Reflect.apply(target, thisArg, argumentsList);
17 | } else {
18 | const currentTime = Date.now();
19 | const callDelay = Math.max(0, 16 - (currentTime - lastCallTime));
20 |
21 | lastCallTime = currentTime + callDelay;
22 |
23 | const timeoutId = window.setTimeout(function () {
24 | argumentsList[0](performance.now());
25 | }, callDelay);
26 |
27 | return timeoutId;
28 | }
29 | }
30 | });
31 |
32 | window.cancelAnimationFrame = new Proxy(window.cancelAnimationFrame, {
33 | apply(target, thisArg, argumentsList) {
34 | if (visibilityState === 'visible') {
35 | return Reflect.apply(target, thisArg, argumentsList);
36 | } else {
37 | window.clearTimeout(argumentsList[0]);
38 | }
39 | }
40 | });
41 |
42 | Object.defineProperty(document, 'visibilityState', {
43 | get() {
44 | return 'visible';
45 | }
46 | });
47 |
48 | Object.defineProperty(document, 'hidden', {
49 | get() {
50 | return false;
51 | }
52 | });
53 |
54 | Document.prototype.hasFocus = function () {
55 | return true;
56 | };
57 |
58 | function stopEvent(ev) {
59 | ev.preventDefault();
60 | ev.stopImmediatePropagation();
61 | }
62 |
63 | window.addEventListener('pagehide', stopEvent, {capture: true});
64 | window.addEventListener('blur', stopEvent, {capture: true});
65 |
66 | document.dispatchEvent(new Event('visibilitychange'));
67 | window.dispatchEvent(new PageTransitionEvent('pageshow'));
68 | window.dispatchEvent(new FocusEvent('focus'));
69 | }
70 |
71 | function yandexServiceObserverScript(eventName) {
72 | let stop;
73 |
74 | const checkService = function () {
75 | if (window.Ya?.reactBus?.emit) {
76 | window.clearTimeout(timeoutId);
77 | document.dispatchEvent(new Event(eventName));
78 | } else if (!stop) {
79 | window.setTimeout(checkService, 200);
80 | }
81 | };
82 |
83 | const timeoutId = window.setTimeout(function () {
84 | stop = true;
85 | }, 60000); // 1 minute
86 |
87 | checkService();
88 | }
89 |
90 | const scriptFunctions = {
91 | makeDocumentVisible: makeDocumentVisibleScript,
92 | yandexServiceObserver: yandexServiceObserverScript
93 | };
94 |
95 | function getScriptFunction(func) {
96 | return scriptFunctions[func];
97 | }
98 |
99 | export {getScriptFunction};
100 |
--------------------------------------------------------------------------------
/src/utils/vuetify.js:
--------------------------------------------------------------------------------
1 | import {createVuetify} from 'vuetify';
2 |
3 | import {getAppTheme, addThemeListener} from 'utils/app';
4 |
5 | const LightTheme = {
6 | dark: false,
7 | colors: {
8 | background: '#FFFFFF',
9 | surface: '#FFFFFF',
10 | primary: '#6750A4',
11 | secondary: '#625B71'
12 | }
13 | };
14 |
15 | const DarkTheme = {
16 | dark: true,
17 | colors: {
18 | background: '#1C1B1F',
19 | surface: '#1C1B1F',
20 | primary: '#D0BCFF',
21 | secondary: '#CCC2DC'
22 | }
23 | };
24 |
25 | async function configTheme(vuetify, {theme = ''} = {}) {
26 | async function setTheme({theme = '', dispatchChange = true} = {}) {
27 | if (!theme) {
28 | theme = await getAppTheme();
29 | }
30 |
31 | document.documentElement.style.setProperty('color-scheme', theme);
32 | vuetify.theme.global.name.value = theme;
33 |
34 | if (dispatchChange) {
35 | document.dispatchEvent(new CustomEvent('themeChange', {detail: theme}));
36 | }
37 | }
38 |
39 | addThemeListener(setTheme);
40 |
41 | await setTheme({theme, dispatchChange: false});
42 | }
43 |
44 | async function configVuetify(app) {
45 | const theme = await getAppTheme();
46 |
47 | const vuetify = createVuetify({
48 | theme: {
49 | themes: {light: LightTheme, dark: DarkTheme},
50 | defaultTheme: theme
51 | },
52 | defaults: {
53 | VDialog: {
54 | eager: true
55 | },
56 | VSelect: {
57 | eager: true
58 | },
59 | VSnackbar: {
60 | eager: true
61 | },
62 | VMenu: {
63 | eager: true
64 | }
65 | }
66 | });
67 |
68 | await configTheme(vuetify, {theme});
69 |
70 | app.use(vuetify);
71 | }
72 |
73 | export {configTheme, configVuetify};
74 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path');
2 | const {lstat, readdir} = require('node:fs/promises');
3 |
4 | const webpack = require('webpack');
5 | const {VueLoaderPlugin} = require('vue-loader');
6 | const {VuetifyPlugin} = require('webpack-plugin-vuetify');
7 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
8 |
9 | const appVersion = require('./package.json').version;
10 | const storageRevisions = require('./src/storage/config.json').revisions;
11 |
12 | module.exports = async function (env, argv) {
13 | const targetEnv = process.env.TARGET_ENV || 'chrome';
14 | const isProduction = process.env.NODE_ENV === 'production';
15 | const enableContributions =
16 | (process.env.ENABLE_CONTRIBUTIONS || 'true') === 'true';
17 |
18 | const mv3 = env.mv3 === 'true';
19 |
20 | const provideExtApi = !['firefox', 'safari'].includes(targetEnv);
21 |
22 | const provideModules = {Buffer: ['buffer', 'Buffer']};
23 | if (provideExtApi) {
24 | provideModules.browser = 'webextension-polyfill';
25 | }
26 |
27 | const plugins = [
28 | new webpack.DefinePlugin({
29 | 'process.env': {
30 | TARGET_ENV: JSON.stringify(targetEnv),
31 | STORAGE_REVISION_LOCAL: JSON.stringify(storageRevisions.local.at(-1)),
32 | STORAGE_REVISION_SESSION: JSON.stringify(
33 | storageRevisions.session.at(-1)
34 | ),
35 | ENABLE_CONTRIBUTIONS: JSON.stringify(enableContributions.toString()),
36 | APP_VERSION: JSON.stringify(appVersion),
37 | MV3: JSON.stringify(mv3.toString())
38 | },
39 | __VUE_OPTIONS_API__: true,
40 | __VUE_PROD_DEVTOOLS__: false,
41 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false
42 | }),
43 | new webpack.ProvidePlugin(provideModules),
44 | new VueLoaderPlugin(),
45 | new VuetifyPlugin(),
46 | new MiniCssExtractPlugin({
47 | filename: '[name]/style.css',
48 | ignoreOrder: true
49 | })
50 | ];
51 |
52 | const enginesRootDir = path.join(__dirname, 'src/engines');
53 |
54 | const engines = (
55 | await Promise.all(
56 | (await readdir(enginesRootDir)).map(async function (file) {
57 | if ((await lstat(path.join(enginesRootDir, file))).isFile()) {
58 | return file.split('.')[0];
59 | }
60 | })
61 | )
62 | ).filter(Boolean);
63 |
64 | const entries = Object.fromEntries(
65 | engines.map(engine => [engine, `./src/engines/${engine}.js`])
66 | );
67 |
68 | if (enableContributions) {
69 | entries.contribute = './src/contribute/main.js';
70 | }
71 |
72 | return {
73 | mode: isProduction ? 'production' : 'development',
74 | entry: {
75 | background: './src/background/main.js',
76 | options: './src/options/main.js',
77 | action: './src/action/main.js',
78 | search: './src/search/main.js',
79 | base: './src/base/main.js',
80 | tools: './src/tools/main.js',
81 | tab: './src/tab/main.js',
82 | ...entries
83 | },
84 | output: {
85 | path: path.resolve(__dirname, 'dist', targetEnv, 'src'),
86 | filename: pathData => {
87 | return engines.includes(pathData.chunk.name)
88 | ? 'engines/[name]/script.js'
89 | : '[name]/script.js';
90 | },
91 | chunkFilename: '[name]/script.js',
92 | asyncChunks: false
93 | },
94 | optimization: {
95 | splitChunks: {
96 | cacheGroups: {
97 | default: false,
98 | commonsUi: {
99 | name: 'commons-ui',
100 | chunks: chunk => {
101 | return ['options', 'action', 'search', 'contribute'].includes(
102 | chunk.name
103 | );
104 | },
105 | minChunks: 2
106 | },
107 | commonsEngine: {
108 | name: 'commons-engine',
109 | chunks: chunk => engines.includes(chunk.name),
110 | minChunks: 2
111 | }
112 | }
113 | }
114 | },
115 | module: {
116 | rules: [
117 | {
118 | test: /\.js$/,
119 | use: 'babel-loader',
120 | resolve: {
121 | fullySpecified: false
122 | }
123 | },
124 | {
125 | test: /\.vue$/,
126 | use: [
127 | {
128 | loader: 'vue-loader',
129 | options: {
130 | transformAssetUrls: {img: ''},
131 | compilerOptions: {whitespace: 'preserve'}
132 | }
133 | }
134 | ]
135 | },
136 | {
137 | test: /\.(c|sc|sa)ss$/,
138 | use: [
139 | MiniCssExtractPlugin.loader,
140 | 'css-loader',
141 | 'postcss-loader',
142 | {
143 | loader: 'sass-loader',
144 | options: {
145 | api: 'legacy',
146 | sassOptions: {
147 | includePaths: ['node_modules'],
148 | silenceDeprecations: ['legacy-js-api', 'mixed-decls'],
149 | quietDeps: true
150 | },
151 | additionalData: (content, loaderContext) => {
152 | return `
153 | $target-env: "${targetEnv}";
154 | ${content}
155 | `;
156 | }
157 | }
158 | }
159 | ]
160 | }
161 | ]
162 | },
163 | resolve: {
164 | modules: [path.resolve(__dirname, 'src'), 'node_modules'],
165 | extensions: ['.js', '.json', '.css', '.scss', '.vue'],
166 | fallback: {fs: false}
167 | },
168 | performance: {
169 | hints: false
170 | },
171 | devtool: false,
172 | plugins
173 | };
174 | };
175 |
--------------------------------------------------------------------------------