├── .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 | Chrome Web Store 10 | 11 | 12 | 13 | Firefox add-ons 14 | 15 | 16 | 17 | Microsoft Store 18 | 19 | 20 | 21 | Opera add-ons 22 |

23 |

24 |

25 | 26 | 27 | 28 | Mac App Store 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 | 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 | 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 | 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 | --------------------------------------------------------------------------------