feat: migrate to Vuetify

BREAKING CHANGE: browser versions older than Chrome 92, Edge 92,
Firefox 91, and Opera 78 are no longer supported
main
dessant 1 year ago
parent 64b4ce7d46
commit c921f6509e

@ -0,0 +1,14 @@
[chrome]
Chrome >= 92
Edge >= 92
Opera >= 78
[edge]
Edge >= 92
[firefox]
Firefox >= 91
FirefoxAndroid >= 91
[opera]
Opera >= 78

@ -1,9 +0,0 @@
{
"parserOptions": {
"ecmaVersion": 8,
"sourceType": "module"
},
"rules": {
"semi": 2
}
}

@ -5,30 +5,34 @@ on: push
jobs:
build:
name: Build
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Clone repository
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
persist-credentials: 'false'
- name: Get Node.js version
id: nvm
run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc)
- name: Setup Node.js
uses: actions/setup-node@v1
uses: actions/setup-node@v3
with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: yarn
run: npm install --legacy-peer-deps
- name: Build artifacts
run: yarn build:prod:zip:all
run: |
npm run build:prod:zip:chrome
npm run build:prod:zip:edge
npm run build:prod:zip:firefox
npm run build:prod:zip:opera
env:
BUSTER_SECRETS: ${{ secrets.BUSTER_SECRETS }}
- name: Hash artifacts
run: sha256sum artifacts/*/*
if: startsWith(github.ref, 'refs/tags/v')
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
if: startsWith(github.ref, 'refs/tags/v')
with:
name: artifacts
@ -36,68 +40,28 @@ jobs:
retention-days: 1
release:
name: Release on GitHub
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
needs: [build]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Get release information
id: release_info
run: |
echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//}
echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: artifacts
path: artifacts/
- name: Hash artifacts
run: sha256sum artifacts/*/*
- name: Create GitHub release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ github.token }}
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.release_info.outputs.TAG }}
release_name: ${{ steps.release_info.outputs.TAG }}
tag_name: ${{ github.ref_name }}
name: ${{ github.ref_name }}
body: |
Download and install the extension from the [extension store](https://github.com/dessant/buster#readme) of your browser.
Learn more about this release from the [changelog](https://github.com/dessant/buster/blob/master/CHANGELOG.md#changelog).
files: artifacts/*/*
fail_on_unmatched_files: true
draft: true
- name: Upload GitHub release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/chrome/buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-chrome.zip
asset_name: buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-chrome.zip
asset_content_type: application/octet-stream
- name: Upload GitHub release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/edge/buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-edge.zip
asset_name: buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-edge.zip
asset_content_type: application/octet-stream
- name: Upload GitHub release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/firefox/buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-firefox.zip
asset_name: buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-firefox.zip
asset_content_type: application/octet-stream
- name: Upload GitHub release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: artifacts/opera/buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-opera.zip
asset_name: buster_captcha_solver_for_humans-${{ steps.release_info.outputs.VERSION }}-opera.zip
asset_content_type: application/octet-stream

20
.gitignore vendored

@ -1,15 +1,15 @@
.assets/
secrets.json
/.assets/
/secrets.json
node_modules/
artifacts/
dist/
/artifacts/
/dist/
web-ext-config.js
/.vscode
npm-debug.log
yarn-debug.log
yarn-error.log
.yarn-integrity
/web-ext-config.js
report.json
/npm-debug.log
/report.json
/report.html

@ -1 +1 @@
12.18.4
18.12.1

@ -1,2 +1,2 @@
package.json
.travis.yml
*.md

@ -0,0 +1,34 @@
const path = require('node:path');
const corejsVersion = require(path.join(
path.dirname(require.resolve('core-js')),
'package.json'
)).version;
module.exports = function (api) {
const presets = [
[
'@babel/env',
{
modules: false,
bugfixes: true,
useBuiltIns: 'usage',
corejs: {version: corejsVersion}
}
]
];
const plugins = [];
const ignore = [
new RegExp(`node_modules\\${path.sep}(?!(vueton|wesa)\\${path.sep}).*`)
];
const parserOpts = {plugins: ['importAssertions']};
if (api.env('production')) {
plugins.push('lodash');
}
return {presets, plugins, ignore, parserOpts};
};

@ -1,75 +1,80 @@
const path = require('path');
const {exec} = require('child_process');
const {lstatSync, readdirSync, readFileSync, writeFileSync} = require('fs');
const path = require('node:path');
const {exec} = require('node:child_process');
const {
lstatSync,
readdirSync,
readFileSync,
writeFileSync,
rmSync
} = require('node:fs');
const {series, parallel, src, dest} = require('gulp');
const babel = require('gulp-babel');
const postcss = require('gulp-postcss');
const gulpif = require('gulp-if');
const jsonMerge = require('gulp-merge-json');
const jsonmin = require('gulp-jsonmin');
const htmlmin = require('gulp-htmlmin');
const imagemin = require('gulp-imagemin');
const del = require('del');
const {ensureDirSync, readJsonSync} = require('fs-extra');
const sharp = require('sharp');
const CryptoJS = require('crypto-js');
const targetEnv = process.env.TARGET_ENV || 'firefox';
const targetEnv = process.env.TARGET_ENV || 'chrome';
const isProduction = process.env.NODE_ENV === 'production';
const distDir = path.join('dist', targetEnv);
const enableContributions =
(process.env.ENABLE_CONTRIBUTIONS || 'true') === 'true';
const distDir = path.join(__dirname, 'dist', targetEnv);
function clean() {
return del([distDir]);
function initEnv() {
process.env.BROWSERSLIST_ENV = targetEnv;
}
function jsWebpack(done) {
exec('webpack-cli --display-error-details --bail --colors', function (
err,
stdout,
stderr
) {
function init(done) {
initEnv();
rmSync(distDir, {recursive: true, force: true});
ensureDirSync(distDir);
done();
}
function js(done) {
exec('webpack-cli build --color', function (err, stdout, stderr) {
console.log(stdout);
console.log(stderr);
done(err);
});
}
function jsBabel() {
return src(['src/content/**/*.js'], {base: '.'})
.pipe(babel())
.pipe(dest(distDir));
}
const js = parallel(jsWebpack, jsBabel);
function html() {
return src('src/**/*.html', {base: '.'})
return src(
enableContributions
? 'src/**/*.html'
: ['src/**/*.html', '!src/contribute/*.html'],
{base: '.'}
)
.pipe(gulpif(isProduction, htmlmin({collapseWhitespace: true})))
.pipe(dest(distDir));
}
function css() {
return src(['src/solve/*.css'], {
base: '.'
})
return src('src/solve/*.css', {base: '.'})
.pipe(postcss())
.pipe(dest(distDir));
}
async function images(done) {
ensureDirSync(path.join(distDir, 'src/icons/app'));
const appIconSvg = readFileSync('src/icons/app/icon.svg');
ensureDirSync(path.join(distDir, 'src/assets/icons/app'));
const appIconSvg = readFileSync('src/assets/icons/app/icon.svg');
const appIconSizes = [16, 19, 24, 32, 38, 48, 64, 96, 128];
for (const size of appIconSizes) {
await sharp(appIconSvg, {density: (72 * size) / 24})
.resize(size)
.toFile(path.join(distDir, `src/icons/app/icon-${size}.png`));
.toFile(path.join(distDir, `src/assets/icons/app/icon-${size}.png`));
}
// Chrome Web Store does not correctly display optimized icons
if (isProduction && targetEnv !== 'chrome') {
await new Promise(resolve => {
src(path.join(distDir, 'src/icons/app/*.png'), {base: '.'})
src(path.join(distDir, 'src/assets/icons/app/*.png'), {base: '.'})
.pipe(imagemin())
.pipe(dest('.'))
.on('error', done)
@ -77,18 +82,20 @@ async function images(done) {
});
}
await new Promise(resolve => {
src('node_modules/ext-contribute/src/assets/*.@(jpg|png|svg)')
.pipe(gulpif(isProduction, imagemin()))
.pipe(dest(path.join(distDir, 'src/contribute/assets')))
.on('error', done)
.on('finish', resolve);
});
if (enableContributions) {
await new Promise(resolve => {
src('node_modules/vueton/components/contribute/assets/*.@(png|svg)')
.pipe(gulpif(isProduction, imagemin()))
.pipe(dest(path.join(distDir, 'src/contribute/assets')))
.on('error', done)
.on('finish', resolve);
});
}
}
async function fonts(done) {
await new Promise(resolve => {
src('src/fonts/roboto.css', {base: '.'})
src('src/assets/fonts/roboto.css', {base: '.'})
.pipe(postcss())
.pipe(dest(distDir))
.on('error', done)
@ -97,16 +104,16 @@ async function fonts(done) {
await new Promise(resolve => {
src(
'node_modules/fontsource-roboto/files/roboto-latin-@(400|500|700)-normal.woff2'
'node_modules/@fontsource/roboto/files/roboto-latin-@(400|500|700)-normal.woff2'
)
.pipe(dest(path.join(distDir, 'src/fonts/files')))
.pipe(dest(path.join(distDir, 'src/assets/fonts/files')))
.on('error', done)
.on('finish', resolve);
});
}
async function locale(done) {
const localesRootDir = path.join(__dirname, 'src/_locales');
const localesRootDir = path.join(__dirname, 'src/assets/locales');
const localeDirs = readdirSync(localesRootDir).filter(function (file) {
return lstatSync(path.join(localesRootDir, file)).isDirectory();
});
@ -144,29 +151,11 @@ async function locale(done) {
}
function manifest() {
return src('src/manifest.json')
return src(`src/assets/manifest/${targetEnv}.json`)
.pipe(
jsonMerge({
fileName: 'manifest.json',
edit: (parsedJson, file) => {
if (['chrome', 'edge', 'opera'].includes(targetEnv)) {
delete parsedJson.browser_specific_settings;
delete parsedJson.options_ui.browser_style;
}
if (['chrome', 'edge', 'firefox'].includes(targetEnv)) {
delete parsedJson.minimum_opera_version;
}
if (['firefox', 'opera'].includes(targetEnv)) {
delete parsedJson.minimum_chrome_version;
}
if (targetEnv === 'firefox') {
delete parsedJson.options_ui.chrome_style;
delete parsedJson.incognito;
}
parsedJson.version = require('./package.json').version;
return parsedJson;
}
@ -191,7 +180,7 @@ See the LICENSE file for further information.
`;
writeFileSync(path.join(distDir, 'NOTICE'), notice);
return src(['LICENSE']).pipe(dest(distDir));
return src('LICENSE').pipe(dest(distDir));
}
function secrets(done) {
@ -223,7 +212,7 @@ function secrets(done) {
function zip(done) {
exec(
`web-ext build -s dist/${targetEnv} -a artifacts/${targetEnv} -n '{name}-{version}-${targetEnv}.zip' --overwrite-dest`,
`web-ext build -s dist/${targetEnv} -a artifacts/${targetEnv} -n "{name}-{version}-${targetEnv}.zip" --overwrite-dest`,
function (err, stdout, stderr) {
console.log(stdout);
console.log(stderr);
@ -233,8 +222,13 @@ function zip(done) {
}
function inspect(done) {
initEnv();
exec(
`webpack --profile --json > report.json && webpack-bundle-analyzer report.json dist/firefox/src && sleep 10 && rm report.{json,html}`,
`npm run build:prod:chrome && \
webpack --profile --json > report.json && \
webpack-bundle-analyzer --mode static report.json dist/chrome/src && \
sleep 3 && rm report.{json,html}`,
function (err, stdout, stderr) {
console.log(stdout);
console.log(stderr);
@ -244,7 +238,7 @@ function inspect(done) {
}
exports.build = series(
clean,
init,
parallel(js, html, css, images, fonts, locale, manifest, license),
secrets
);

43745
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -2,92 +2,89 @@
"name": "buster",
"version": "1.3.2",
"author": "Armin Sebastian",
"repository": "https://github.com/dessant/buster",
"license": "GPL-3.0-only",
"homepage": "https://github.com/dessant/buster",
"repository": {
"url": "https://github.com/dessant/buster.git",
"type": "git"
},
"bugs": {
"url": "https://github.com/dessant/buster/issues"
},
"scripts": {
"_build": "cross-env NODE_ENV=development gulp build",
"build:chrome": "cross-env TARGET_ENV=chrome yarn _build",
"build:edge": "cross-env TARGET_ENV=edge yarn _build",
"build:firefox": "cross-env TARGET_ENV=firefox yarn _build",
"build:opera": "cross-env TARGET_ENV=opera yarn _build",
"build:all": "run-s 'build:@(chrome|edge|firefox|opera)'",
"build:chrome": "cross-env TARGET_ENV=chrome npm run _build",
"build:edge": "cross-env TARGET_ENV=edge npm run _build",
"build:firefox": "cross-env TARGET_ENV=firefox npm run _build",
"build:opera": "cross-env TARGET_ENV=opera npm run _build",
"_build:prod": "cross-env NODE_ENV=production gulp build",
"build:prod:chrome": "cross-env TARGET_ENV=chrome yarn _build:prod",
"build:prod:edge": "cross-env TARGET_ENV=edge yarn _build:prod",
"build:prod:firefox": "cross-env TARGET_ENV=firefox yarn _build:prod",
"build:prod:opera": "cross-env TARGET_ENV=opera yarn _build:prod",
"build:prod:all": "run-s 'build:prod:@(chrome|edge|firefox|opera)'",
"_build:prod:zip": "yarn _build:prod && gulp zip",
"build:prod:zip:chrome": "cross-env TARGET_ENV=chrome yarn _build:prod:zip",
"build:prod:zip:edge": "cross-env TARGET_ENV=edge yarn _build:prod:zip",
"build:prod:zip:firefox": "cross-env TARGET_ENV=firefox yarn _build:prod:zip",
"build:prod:zip:opera": "cross-env TARGET_ENV=opera yarn _build:prod:zip",
"build:prod:zip:all": "run-s 'build:prod:zip:@(chrome|edge|firefox|opera)'",
"build:prod:chrome": "cross-env TARGET_ENV=chrome npm run _build:prod",
"build:prod:edge": "cross-env TARGET_ENV=edge npm run _build:prod",
"build:prod:firefox": "cross-env TARGET_ENV=firefox npm run _build:prod",
"build:prod:opera": "cross-env TARGET_ENV=opera npm run _build:prod",
"_build:prod:zip": "npm run _build:prod && gulp zip",
"build:prod:zip:chrome": "cross-env TARGET_ENV=chrome npm run _build:prod:zip",
"build:prod:zip:edge": "cross-env TARGET_ENV=edge npm run _build:prod:zip",
"build:prod:zip:firefox": "cross-env TARGET_ENV=firefox npm run _build:prod:zip",
"build:prod:zip:opera": "cross-env TARGET_ENV=opera npm run _build:prod:zip",
"start:chrome": "web-ext run -s dist/chrome -t chromium",
"start:firefox": "web-ext run -s dist/firefox -t firefox-desktop",
"start:android": "web-ext run -s dist/firefox -t firefox-android",
"inspect": "cross-env NODE_ENV=production gulp inspect",
"update": "ncu --upgrade",
"push": "git push --follow-tags origin master",
"release": "standard-version"
"release": "standard-version",
"push": "git push --follow-tags origin master"
},
"browserslist": [
"Chrome >= 76",
"Firefox >= 68",
"FirefoxAndroid >= 68",
"Opera >= 63"
],
"dependencies": {
"@material/theme": "^4.0.0",
"@material/typography": "^4.0.0",
"@fontsource/roboto": "^4.5.8",
"audiobuffer-to-wav": "^1.0.0",
"bowser": "^2.11.0",
"core-js": "^3.6.5",
"crypto-js": "^4.0.0",
"ext-components": "dessant/ext-components#^0.4.0",
"ext-contribute": "dessant/ext-contribute#^0.3.3",
"fontsource-roboto": "^3.0.3",
"storage-versions": "dessant/storage-versions#^0.2.6",
"uuid": "^8.3.1",
"vue": "^2.6.12",
"webextension-polyfill": "^0.6.0"
"core-js": "^3.26.1",
"crypto-js": "^4.1.1",
"p-queue": "^7.3.0",
"uuid": "^9.0.0",
"vue": "^3.2.45",
"vuetify": "^3.0.5",
"vueton": "^0.1.3",
"webextension-polyfill": "^0.10.0",
"wesa": "^0.3.0"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/preset-env": "^7.11.5",
"autoprefixer": "^9.8.6",
"babel-loader": "^8.1.0",
"@babel/core": "^7.20.5",
"@babel/preset-env": "^7.20.2",
"babel-loader": "^9.1.0",
"babel-plugin-lodash": "^3.3.4",
"babel-preset-minify": "^0.5.1",
"cross-env": "^7.0.2",
"css-loader": "^4.3.0",
"cssnano": "^4.1.10",
"del": "^6.0.0",
"fs-extra": "^9.0.1",
"cross-env": "^7.0.3",
"css-loader": "^6.7.2",
"cssnano": "^5.1.14",
"fs-extra": "^11.1.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-htmlmin": "^5.0.1",
"gulp-if": "^3.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-jsonmin": "^1.2.0",
"gulp-merge-json": "^2.1.1",
"gulp-postcss": "^8.0.0",
"lodash-webpack-plugin": "^0.11.5",
"mini-css-extract-plugin": "^0.12.0",
"npm-check-updates": "^9.0.4",
"npm-run-all": "^4.1.5",
"postcss-loader": "^3.0.0",
"prettier": "^2.1.2",
"sass": "^1.26.12",
"sass-loader": "^10.0.2",
"sharp": "^0.26.1",
"standard-version": "^9.0.0",
"vue-loader": "^15.9.3",
"vue-template-compiler": "^2.6.12",
"web-ext": "^5.2.0",
"webpack": "^4.44.2",
"webpack-bundle-analyzer": "^3.9.0",
"webpack-cli": "^3.3.12"
"gulp-merge-json": "^2.1.2",
"gulp-postcss": "^9.0.1",
"lodash-webpack-plugin": "^0.11.6",
"mini-css-extract-plugin": "^2.7.2",
"npm-check-updates": "^16.5.6",
"postcss": "^8.4.20",
"postcss-loader": "^7.0.2",
"postcss-preset-env": "^7.8.3",
"prettier": "^2.8.1",
"sass": "^1.56.2",
"sass-loader": "^13.2.0",
"sharp": "^0.31.2",
"standard-version": "^9.5.0",
"vue-loader": "^17.0.1",
"web-ext": "^7.4.0",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.7.0",
"webpack-cli": "^5.0.1",
"webpack-plugin-vuetify": "^2.0.0"
},
"overrides": {
"glob": "7.2.3"
},
"private": true
}

@ -1,9 +1,12 @@
module.exports = function(ctx) {
return {
plugins: {
autoprefixer: {},
cssnano:
ctx.env === 'production' ? {zindex: false, discardUnused: false} : false
}
};
const postcssPresetEnv = require('postcss-preset-env');
const cssnano = require('cssnano');
module.exports = function (api) {
const plugins = [postcssPresetEnv()];
if (api.env === 'production') {
plugins.push(cssnano({zindex: false, discardUnused: false}));
}
return {plugins};
};

@ -0,0 +1,23 @@
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('./files/roboto-latin-400-normal.woff2') format('woff2'),
local('Roboto'), local('Roboto-Regular');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('./files/roboto-latin-500-normal.woff2') format('woff2'),
local('Roboto Medium'), local('Roboto-Medium');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url('./files/roboto-latin-700-normal.woff2') format('woff2'),
local('Roboto Bold'), local('Roboto-Bold');
}

Before

Width:  |  Height:  |  Size: 493 B

After

Width:  |  Height:  |  Size: 493 B

@ -15,7 +15,7 @@
},
"optionTitle_speechService": {
"message": "Speech service",
"message": "Speech recognition",
"description": "Title of the option."
},
@ -239,6 +239,31 @@
"description": "Title of the option."
},
"optionTitle_appTheme": {
"message": "Theme",
"description": "Title of the option."
},
"optionValue_appTheme_auto": {
"message": "System default",
"description": "Value of the option."
},
"optionValue_appTheme_light": {
"message": "Light",
"description": "Value of the option."
},
"optionValue_appTheme_dark": {
"message": "Dark",
"description": "Value of the option."
},
"optionTitle_showContribPage": {
"message": "Show contribution page",
"description": "Title of the option."
},
"optionTitle_witSpeechApiLang": {
"message": "API language",
"description": "Title of the option."
@ -420,36 +445,41 @@
"description": "Label of the input."
},
"buttonText_addApi": {
"buttonLabel_addApi": {
"message": "Add API",
"description": "Text of the button."
},
"buttonText_solve": {
"buttonLabel_solve": {
"message": "Solve the challenge",
"description": "Text of the button."
},
"buttonText_reset": {
"buttonLabel_reset": {
"message": "Reset the challenge",
"description": "Text of the button."
},
"buttonText_downloadApp": {
"buttonLabel_downloadApp": {
"message": "Download app",
"description": "Text of the button."
},
"buttonText_installApp": {
"buttonLabel_installApp": {
"message": "Install app",
"description": "Text of the button."
},
"buttonText_goBack": {
"buttonLabel_goBack": {
"message": "Go back",
"description": "Text of the button."
},
"buttonLabel_contribute": {
"message": "Contribute",
"description": "Label of the button."
},
"linkText_installGuide": {
"message": "Installation guide",
"description": "Text of the link."

@ -0,0 +1,91 @@
{
"manifest_version": 2,
"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__",
"version": "0.1.0",
"author": "Armin Sebastian",
"homepage_url": "https://github.com/dessant/buster",
"default_locale": "en",
"minimum_chrome_version": "92.0",
"permissions": [
"storage",
"notifications",
"webRequest",
"webRequestBlocking",
"webNavigation",
"nativeMessaging",
"<all_urls>"
],
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors http://127.0.0.1:*;",
"icons": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
},
"browser_action": {
"default_icon": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
}
},
"options_ui": {
"page": "src/options/index.html",
"chrome_style": false,
"open_in_tab": true
},
"background": {
"page": "src/background/index.html"
},
"content_scripts": [
{
"matches": [
"https://google.com/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/api2/bframe*",
"https://google.com/recaptcha/enterprise/bframe*",
"https://www.google.com/recaptcha/enterprise/bframe*",
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*"
],
"all_frames": true,
"run_at": "document_idle",
"css": ["src/solve/style.css"],
"js": ["src/solve/script.js"]
},
{
"matches": ["http://127.0.0.1/buster/setup?session=*"],
"run_at": "document_idle",
"js": ["src/scripts/init-setup.js"]
}
],
"web_accessible_resources": [
"src/setup/index.html",
"src/scripts/reset.js",
"src/solve/solver-button.css"
],
"incognito": "split"
}

@ -0,0 +1,91 @@
{
"manifest_version": 2,
"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__",
"version": "0.1.0",
"author": "Armin Sebastian",
"homepage_url": "https://github.com/dessant/buster",
"default_locale": "en",
"minimum_chrome_version": "92.0",
"permissions": [
"storage",
"notifications",
"webRequest",
"webRequestBlocking",
"webNavigation",
"nativeMessaging",
"<all_urls>"
],
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors http://127.0.0.1:*;",
"icons": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
},
"browser_action": {
"default_icon": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
}
},
"content_scripts": [
{
"matches": [
"https://google.com/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/api2/bframe*",
"https://google.com/recaptcha/enterprise/bframe*",
"https://www.google.com/recaptcha/enterprise/bframe*",
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*"
],
"all_frames": true,
"run_at": "document_idle",
"css": ["src/solve/style.css"],
"js": ["src/solve/script.js"]
},
{
"matches": ["http://127.0.0.1/buster/setup?session=*"],
"run_at": "document_idle",
"js": ["src/scripts/init-setup.js"]
}
],
"options_ui": {
"page": "src/options/index.html",
"chrome_style": false,
"open_in_tab": true
},
"background": {
"page": "src/background/index.html"
},
"web_accessible_resources": [
"src/setup/index.html",
"src/scripts/reset.js",
"src/solve/solver-button.css"
],
"incognito": "split"
}

@ -10,11 +10,9 @@
"browser_specific_settings": {
"gecko": {
"id": "{e58d3966-3d76-4cd9-8552-1582fbc800c1}",
"strict_min_version": "68.0"
"strict_min_version": "91.0"
}
},
"minimum_chrome_version": "76.0",
"minimum_opera_version": "63.0",
"permissions": [
"storage",
@ -29,43 +27,58 @@
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors http://127.0.0.1:*;",
"icons": {
"16": "src/icons/app/icon-16.png",
"19": "src/icons/app/icon-19.png",
"24": "src/icons/app/icon-24.png",
"32": "src/icons/app/icon-32.png",
"38": "src/icons/app/icon-38.png",
"48": "src/icons/app/icon-48.png",
"64": "src/icons/app/icon-64.png",
"96": "src/icons/app/icon-96.png",
"128": "src/icons/app/icon-128.png"
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
},
"browser_action": {
"default_icon": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
}
},
"content_scripts": [
{
"matches": [
"https://google.com/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://google.com/recaptcha/enterprise/bframe*",
"https://www.google.com/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*"
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*"
],
"all_frames": true,
"run_at": "document_idle",
"css": ["src/solve/reset-button.css"],
"js": ["src/manifest.js", "src/solve/script.js"]
"css": ["src/solve/style.css"],
"js": ["src/solve/script.js"]
},
{
"matches": ["http://127.0.0.1/buster/setup?session=*"],
"run_at": "document_idle",
"js": ["src/content/setup.js"]
"js": ["src/scripts/init-setup.js"]
}
],
"options_ui": {
"page": "src/options/index.html",
"browser_style": false,
"chrome_style": false,
"open_in_tab": true
},
@ -75,9 +88,7 @@
"web_accessible_resources": [
"src/setup/index.html",
"src/content/reset.js",
"src/scripts/reset.js",
"src/solve/solver-button.css"
],
"incognito": "split"
]
}

@ -0,0 +1,91 @@
{
"manifest_version": 2,
"name": "__MSG_extensionName__",
"description": "__MSG_extensionDescription__",
"version": "0.1.0",
"author": "Armin Sebastian",
"homepage_url": "https://github.com/dessant/buster",
"default_locale": "en",
"minimum_opera_version": "78.0",
"permissions": [
"storage",
"notifications",
"webRequest",
"webRequestBlocking",
"webNavigation",
"nativeMessaging",
"<all_urls>"
],
"content_security_policy": "default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors http://127.0.0.1:*;",
"icons": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
},
"browser_action": {
"default_icon": {
"16": "src/assets/icons/app/icon-16.png",
"19": "src/assets/icons/app/icon-19.png",
"24": "src/assets/icons/app/icon-24.png",
"32": "src/assets/icons/app/icon-32.png",
"38": "src/assets/icons/app/icon-38.png",
"48": "src/assets/icons/app/icon-48.png",
"64": "src/assets/icons/app/icon-64.png",
"96": "src/assets/icons/app/icon-96.png",
"128": "src/assets/icons/app/icon-128.png"
}
},
"content_scripts": [
{
"matches": [
"https://google.com/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/api2/bframe*",
"https://google.com/recaptcha/enterprise/bframe*",
"https://www.google.com/recaptcha/enterprise/bframe*",
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*"
],
"all_frames": true,
"run_at": "document_idle",
"css": ["src/solve/style.css"],
"js": ["src/solve/script.js"]
},
{
"matches": ["http://127.0.0.1/buster/setup?session=*"],
"run_at": "document_idle",
"js": ["src/scripts/init-setup.js"]
}
],
"options_ui": {
"page": "src/options/index.html",
"chrome_style": false,
"open_in_tab": true
},
"background": {
"page": "src/background/index.html"
},
"web_accessible_resources": [
"src/setup/index.html",
"src/scripts/reset.js",
"src/solve/solver-button.css"
],
"incognito": "split"
}

@ -2,9 +2,9 @@
<html>
<head>
<meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
</head>
<body>
<script src="/src/manifest.js"></script>
<script src="script.js"></script>
</body>
</html>

@ -1,21 +1,20 @@
import browser from 'webextension-polyfill';
import audioBufferToWav from 'audiobuffer-to-wav';
import aes from 'crypto-js/aes';
import sha256 from 'crypto-js/sha256';
import utf8 from 'crypto-js/enc-utf8';
import {initStorage} from 'storage/init';
import {initStorage, migrateLegacyStorage} from 'storage/init';
import {isStorageReady} from 'storage/storage';
import storage from 'storage/storage';
import {
showNotification,
showContributePage,
sendNativeMessage
sendNativeMessage,
processMessageResponse,
processAppUse
} from 'utils/app';
import {
executeCode,
executeFile,
scriptsAllowed,
functionInContext,
getBrowser,
getPlatform,
getRandomInt,
@ -89,23 +88,46 @@ async function getFramePos(tabId, frameId, frameIndex) {
return {x, y};
}
function initResetCaptcha() {
const initReset = function (challengeUrl) {
const script = document.createElement('script');
script.onload = function (ev) {
ev.target.remove();
document.dispatchEvent(
new CustomEvent('___resetCaptcha', {detail: challengeUrl})
);
};
script.src = chrome.runtime.getURL('/src/scripts/reset.js');
document.documentElement.appendChild(script);
};
const onMessage = function (request) {
if (request.id === 'resetCaptcha') {
removeCallbacks();
initReset(request.challengeUrl);
}
};
const removeCallbacks = function () {
window.clearTimeout(timeoutId);
chrome.runtime.onMessage.removeListener(onMessage);
};
const timeoutId = window.setTimeout(removeCallbacks, 10000); // 10 seconds
chrome.runtime.onMessage.addListener(onMessage);
}
async function resetCaptcha(tabId, frameId, challengeUrl) {
frameId = (
await browser.webNavigation.getFrame({
tabId,
frameId: frameId
})
).parentFrameId;
frameId = (await browser.webNavigation.getFrame({tabId, frameId}))
.parentFrameId;
if (!(await scriptsAllowed(tabId, frameId))) {
await showNotification({messageId: 'error_scriptsNotAllowed'});
return;
}
if (!(await functionInContext('addListener', tabId, frameId))) {
await executeFile('/src/content/initReset.js', tabId, frameId);
}
await executeCode('addListener()', tabId, frameId);
await executeCode(`(${initResetCaptcha.toString()})()`, tabId, frameId);
await browser.tabs.sendMessage(
tabId,
@ -126,10 +148,10 @@ function challengeRequestCallback(details) {
}
async function setChallengeLocale() {
const {loadEnglishChallenge, simulateUserInput} = await storage.get(
['loadEnglishChallenge', 'simulateUserInput'],
'sync'
);
const {loadEnglishChallenge, simulateUserInput} = await storage.get([
'loadEnglishChallenge',
'simulateUserInput'
]);
if (loadEnglishChallenge || simulateUserInput) {
if (
@ -139,18 +161,22 @@ async function setChallengeLocale() {
challengeRequestCallback,
{
urls: [
'https://google.com/recaptcha/api2/anchor*',
'https://google.com/recaptcha/api2/bframe*',
'https://www.google.com/recaptcha/api2/anchor*',
'https://www.google.com/recaptcha/api2/bframe*',
'https://www.recaptcha.net/recaptcha/api2/anchor*',
'https://www.recaptcha.net/recaptcha/api2/bframe*',
'https://recaptcha.net/recaptcha/api2/anchor*',
'https://recaptcha.net/recaptcha/api2/bframe*',
'https://google.com/recaptcha/enterprise/anchor*',
'https://google.com/recaptcha/enterprise/bframe*',
'https://www.google.com/recaptcha/enterprise/anchor*',
'https://www.google.com/recaptcha/enterprise/bframe*',
'https://www.recaptcha.net/recaptcha/enterprise/anchor*',
'https://www.recaptcha.net/recaptcha/enterprise/bframe*',
'https://recaptcha.net/recaptcha/api2/anchor*',
'https://recaptcha.net/recaptcha/api2/bframe*',
'https://www.recaptcha.net/recaptcha/api2/anchor*',
'https://www.recaptcha.net/recaptcha/api2/bframe*',
'https://recaptcha.net/recaptcha/enterprise/anchor*',
'https://recaptcha.net/recaptcha/enterprise/bframe*'
'https://recaptcha.net/recaptcha/enterprise/bframe*',
'https://www.recaptcha.net/recaptcha/enterprise/anchor*',
'https://www.recaptcha.net/recaptcha/enterprise/bframe*'
],
types: ['sub_frame']
},
@ -182,9 +208,10 @@ function addBackgroundRequestListener() {
!browser.webRequest.onBeforeSendHeaders.hasListener(removeRequestOrigin)
) {
const urls = [
'https://google.com/*',
'https://www.google.com/*',
'https://www.recaptcha.net/*',
'https://recaptcha.net/*',
'https://www.recaptcha.net/*',
'https://api.wit.ai/*',
'https://speech.googleapis.com/*',
'https://*.speech-to-text.watson.cloud.ibm.com/*',
@ -242,9 +269,9 @@ async function loadSecrets() {
secrets = JSON.parse(aes.decrypt(ciphertext, key).toString(utf8));
} catch (err) {
secrets = {};
const {speechService} = await storage.get('speechService', 'sync');
const {speechService} = await storage.get('speechService');
if (speechService === 'witSpeechApiDemo') {
await storage.set({speechService: 'witSpeechApi'}, 'sync');
await storage.set({speechService: 'witSpeechApi'});
}
}
}
@ -263,10 +290,7 @@ async function getWitSpeechApiKey(speechService, language) {
return apiKey;
}
} else {
const {witSpeechApiKeys: apiKeys} = await storage.get(
'witSpeechApiKeys',
'sync'
);
const {witSpeechApiKeys: apiKeys} = await storage.get('witSpeechApiKeys');
return apiKeys[language];
}
}
@ -362,10 +386,10 @@ async function transcribeAudio(audioUrl, lang) {
const audioRsp = await fetch(audioUrl, {referrer: ''});
const audioContent = await prepareAudio(await audioRsp.arrayBuffer());
const {speechService, tryEnglishSpeechModel} = await storage.get(
['speechService', 'tryEnglishSpeechModel'],
'sync'
);
const {speechService, tryEnglishSpeechModel} = await storage.get([
'speechService',
'tryEnglishSpeechModel'
]);
if (['witSpeechApiDemo', 'witSpeechApi'].includes(speechService)) {
const language = captchaWitSpeechApiLangCodes[lang] || 'english';
@ -404,8 +428,7 @@ async function transcribeAudio(audioUrl, lang) {
}
} else if (speechService === 'googleSpeechApi') {
const {googleSpeechApiKey: apiKey} = await storage.get(
'googleSpeechApiKey',
'sync'
'googleSpeechApiKey'
);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
@ -447,10 +470,8 @@ async function transcribeAudio(audioUrl, lang) {
solution = results[0].alternatives[0].transcript.trim();
}
} else if (speechService === 'ibmSpeechApi') {
const {
ibmSpeechApiLoc: apiLoc,
ibmSpeechApiKey: apiKey
} = await storage.get(['ibmSpeechApiLoc', 'ibmSpeechApiKey'], 'sync');
const {ibmSpeechApiLoc: apiLoc, ibmSpeechApiKey: apiKey} =
await storage.get(['ibmSpeechApiLoc', 'ibmSpeechApiKey']);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
@ -478,13 +499,8 @@ async function transcribeAudio(audioUrl, lang) {
);
}
} else if (speechService === 'microsoftSpeechApi') {
const {
microsoftSpeechApiLoc: apiLoc,
microsoftSpeechApiKey: apiKey
} = await storage.get(
['microsoftSpeechApiLoc', 'microsoftSpeechApiKey'],
'sync'
);
const {microsoftSpeechApiLoc: apiLoc, microsoftSpeechApiKey: apiKey} =
await storage.get(['microsoftSpeechApiLoc', 'microsoftSpeechApiKey']);
if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'});
return;
@ -519,7 +535,7 @@ async function transcribeAudio(audioUrl, lang) {
}
}
async function onMessage(request, sender) {
async function processMessage(request, sender) {
if (request.id === 'notification') {
showNotification({
message: request.message,
@ -529,12 +545,7 @@ async function onMessage(request, sender) {
timeout: request.timeout
});
} else if (request.id === 'captchaSolved') {
let {useCount} = await storage.get('useCount', 'sync');
useCount += 1;
await storage.set({useCount}, 'sync');
if ([30, 100].includes(useCount)) {
await showContributePage('use');
}
await processAppUse();
} else if (request.id === 'transcribeAudio') {
addBackgroundRequestListener();
try {
@ -606,22 +617,26 @@ async function onMessage(request, sender) {
} else if (request.id === 'openOptions') {
browser.runtime.openOptionsPage();
} else if (request.id === 'getPlatform') {
return getPlatform();
return getPlatform({fallback: false});
} else if (request.id === 'getBrowser') {
return getBrowser();
} else if (request.id === 'optionChange') {
await onOptionChange();
}
}
async function onStorageChange(changes, area) {
await setChallengeLocale();
function onMessage(request, sender, sendResponse) {
const response = processMessage(request, sender);
return processMessageResponse(response, sendResponse);
}
function addStorageListener() {
browser.storage.onChanged.addListener(onStorageChange);
async function onOptionChange() {
await setChallengeLocale();
}
function addMessageListener() {
browser.runtime.onMessage.addListener(onMessage);
async function onActionButtonClick(tab) {
await browser.runtime.openOptionsPage();
}
async function onInstall(details) {
@ -645,14 +660,9 @@ async function onInstall(details) {
await browser.tabs.insertCSS(tabId, {
frameId,
runAt: 'document_idle',
file: '/src/solve/reset-button.css'
file: '/src/solve/style.css'
});
await browser.tabs.executeScript(tabId, {
frameId,
runAt: 'document_idle',
file: '/src/manifest.js'
});
await browser.tabs.executeScript(tabId, {
frameId,
runAt: 'document_idle',
@ -673,13 +683,33 @@ async function onInstall(details) {
}
}
async function onLoad() {
await initStorage('sync');
function addBrowserActionListener() {
browser.browserAction.onClicked.addListener(onActionButtonClick);
}
function addMessageListener() {
browser.runtime.onMessage.addListener(onMessage);
}
function addInstallListener() {
browser.runtime.onInstalled.addListener(onInstall);
}
async function setup() {
if (!(await isStorageReady())) {
await migrateLegacyStorage();
await initStorage();
}
await setChallengeLocale();
addStorageListener();
addMessageListener();
}
browser.runtime.onInstalled.addListener(onInstall);
function init() {
addBrowserActionListener();
addMessageListener();
addInstallListener();
setup();
}
document.addEventListener('DOMContentLoaded', onLoad);
init();

@ -1,8 +0,0 @@
{
"presets": ["@babel/env"],
"env": {
"production": {
"presets": [["minify", {"deadcode": false}]]
}
}
}

@ -1,28 +0,0 @@
function initReset(challengeUrl) {
const script = document.createElement('script');
script.onload = function (ev) {
ev.target.remove();
document.dispatchEvent(
new CustomEvent('___resetCaptcha', {detail: challengeUrl})
);
};
script.src = chrome.runtime.getURL('/src/content/reset.js');
document.documentElement.appendChild(script);
}
function addListener() {
const onMessage = function (request) {
if (request.id === 'resetCaptcha') {
removeCallbacks();
initReset(request.challengeUrl);
}
};
const removeCallbacks = function () {
window.clearTimeout(timeoutId);
chrome.runtime.onMessage.removeListener(onMessage);
};
const timeoutId = window.setTimeout(removeCallbacks, 10000); // 10 seconds
chrome.runtime.onMessage.addListener(onMessage);
}

@ -1,14 +1,19 @@
<template>
<div id="app">
<v-contribute :extName="extName" :extSlug="extSlug" :notice="notice">
</v-contribute>
</div>
<v-app id="app">
<vn-contribute
:extName="extName"
:extSlug="extSlug"
:notice="notice"
@open="contribute"
>
</vn-contribute>
</v-app>
</template>
<script>
import {Contribute} from 'ext-contribute';
import {Contribute} from 'vueton/components/contribute';
import {getText} from 'utils/common';
import {getText, getActiveTab} from 'utils/common';
export default {
components: {
@ -23,6 +28,14 @@ export default {
};
},
methods: {
contribute: async function ({url} = {}) {
const activeTab = await getActiveTab();
await browser.tabs.create({url, index: activeTab.index + 1});
}
},
created: function () {
document.title = getText('pageTitle', [
getText('pageTitle_contribute'),
@ -30,24 +43,20 @@ export default {
]);
const query = new URL(window.location.href).searchParams;
if (query.get('action') === 'use') {
this.notice = `This page is shown during your 30th and 100th use
of the extension.`;
if (query.get('action') === 'auto') {
this.notice = 'This page is shown once a year while using the extension.';
}
}
};
</script>
<style lang="scss">
@import '@material/typography/mixins';
@use 'vueton/styles' as vueton;
@include vueton.theme-base;
body {
.v-application__wrap {
display: flex;
align-items: center;
justify-content: center;
margin: 0;
@include mdc-typography-base;
font-size: 100%;
background-color: #ffffff;
}
</style>

@ -3,16 +3,19 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/icons/app/icon-32.png" type="image/png" />
<link href="/src/fonts/roboto.css" rel="stylesheet" />
<meta name="referrer" content="no-referrer" />
<link
rel="icon"
href="/src/assets/icons/app/icon-64.png"
type="image/png"
/>
<link href="/src/assets/fonts/roboto.css" rel="stylesheet" />
<link href="/src/commons-ui/style.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<script src="/src/manifest.js"></script>
<script src="/src/commons-ui/script.js"></script>
<script src="script.js"></script>
</body>
<script src="script.js" defer></script>
</head>
<body></body>
</html>

@ -1,8 +1,16 @@
import Vue from 'vue';
import {createApp} from 'vue';
import {configApp} from 'utils/app';
import {configVuetify} from 'utils/vuetify';
import App from './App';
new Vue({
el: '#app',
render: h => h(App)
});
async function init() {
const app = createApp(App);
await configApp(app);
await configVuetify(app);
app.mount('body');
}
init();

@ -1,23 +0,0 @@
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'),
url('./files/roboto-latin-400-normal.woff2') format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'),
url('./files/roboto-latin-500-normal.woff2') format('woff2');
}
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'),
url('./files/roboto-latin-700-normal.woff2') format('woff2');
}

@ -1,28 +1,28 @@
<template>
<div id="app" v-if="dataLoaded">
<v-app id="app" v-if="dataLoaded">
<div class="section">
<div class="section-title" v-once>
{{ getText('optionSectionTitle_services') }}
</div>
<div class="option-wrap">
<div class="option select">
<v-select
<vn-select
:label="getText('optionTitle_speechService')"
:items="listItems.speechService"
v-model="options.speechService"
:options="selectOptions.speechService"
>
</v-select>
</vn-select>
</div>
<div
class="option text-field"
v-if="options.speechService === 'googleSpeechApi'"
>
<v-textfield
v-model.trim="options.googleSpeechApiKey"
<vn-text-field
:label="getText('inputLabel_apiKey')"
v-model.trim="options.googleSpeechApiKey"
>
</v-textfield>
</vn-text-field>
</div>
<a
@ -39,22 +39,22 @@
class="option select"
v-if="options.speechService === 'ibmSpeechApi'"
>
<v-select
<vn-select
:label="getText('optionTitle_ibmSpeechApiLoc')"
:items="listItems.ibmSpeechApiLoc"
v-model="options.ibmSpeechApiLoc"
:options="selectOptions.ibmSpeechApiLoc"
>
</v-select>
</vn-select>
</div>
<div
class="option text-field"
v-if="options.speechService === 'ibmSpeechApi'"
>
<v-textfield
<vn-text-field
v-model.trim="options.ibmSpeechApiKey"
:label="getText('inputLabel_apiKey')"
>
</v-textfield>
</vn-text-field>
</div>
<a
@ -71,22 +71,22 @@
class="option select"
v-if="options.speechService === 'microsoftSpeechApi'"
>
<v-select
<vn-select
:label="getText('optionTitle_microsoftSpeechApiLoc')"
:items="listItems.microsoftSpeechApiLoc"
v-model="options.microsoftSpeechApiLoc"
:options="selectOptions.microsoftSpeechApiLoc"
>
</v-select>
</vn-select>
</div>
<div
class="option text-field"
v-if="options.speechService === 'microsoftSpeechApi'"
>
<v-textfield
<vn-text-field
v-model.trim="options.microsoftSpeechApiKey"
:label="getText('inputLabel_apiKey')"
>
</v-textfield>
</vn-text-field>
</div>
<a
@ -99,36 +99,34 @@
{{ getText('linkText_apiGuide') }}
</a>
<v-textfield
<vn-text-field
class="text-field"
v-if="options.speechService === 'witSpeechApi'"
v-for="item in witSpeechApis"
:key="item.id"
:value="options.witSpeechApiKeys[item] || ''"
:model-value="options.witSpeechApiKeys[item] || ''"
:label="
getText('inputLabel_apiKeyType', [
getText(`optionValue_witSpeechApiLang_${item}`)
])
"
@input="saveWitSpeechApiKey($event.trim(), item)"
@update:modelValue="saveWitSpeechApiKey($event.trim(), item)"
>
</v-textfield>
</vn-text-field>
<div
class="wit-add-api"
v-if="options.speechService === 'witSpeechApi'"
>
<v-select
v-model="witSpeechApiLang"
:options="selectOptions.witSpeechApiLang"
<vn-select
:label="getText('optionTitle_witSpeechApiLang')"
:items="listItems.witSpeechApiLang"
v-model="witSpeechApiLang"
>
</v-select>
<v-button
:outlined="true"
:disabled="!witSpeechApiLang"
:label="getText('buttonText_addApi')"
@click="addWitSpeechApi"
>
</v-button>
</vn-select>
<vn-button :disabled="!witSpeechApiLang" @click="addWitSpeechApi">
{{ getText('buttonLabel_addApi') }}
</vn-button>
</div>
<a
@ -143,7 +141,7 @@
</div>
</div>
<div class="section">
<div class="section section-client">
<div class="section-title" v-once>
{{ getText('optionSectionTitle_client') }}
</div>
@ -158,33 +156,27 @@
(clientAppVerified && options.simulateUserInput)
"
>
<v-form-field
input-id="si"
<vn-switch
:label="getText('optionTitle_simulateUserInput')"
>
<v-switch id="si" v-model="options.simulateUserInput"></v-switch>
</v-form-field>
v-model="options.simulateUserInput"
></vn-switch>
</div>
<div
class="option"
v-if="clientAppVerified && options.simulateUserInput"
>
<v-form-field
input-id="nc"
<vn-switch
:label="getText('optionTitle_navigateWithKeyboard')"
>
<v-switch id="nc" v-model="options.navigateWithKeyboard"></v-switch>
</v-form-field>
v-model="options.navigateWithKeyboard"
></vn-switch>
</div>
<div class="option" v-if="clientAppInstalled">
<v-form-field
input-id="auc"
<vn-switch
:label="getText('optionTitle_autoUpdateClientApp')"
>
<v-switch id="auc" v-model="options.autoUpdateClientApp"></v-switch>
</v-form-field>
v-model="options.autoUpdateClientApp"
></vn-switch>
</div>
<div
@ -205,14 +197,14 @@
{{ getText('pageContent_optionClientAppOSError') }}
</div>
<v-button
<vn-button
class="download-button"
:unelevated="true"
:disabled="!clientAppDownloadUrl"
:label="getText('buttonText_downloadApp')"
@click="$refs.dlLink.click()"
variant="elevated"
>
</v-button>
{{ getText('buttonLabel_downloadApp') }}
</vn-button>
<a
ref="dlLink"
class="download-link"
@ -229,42 +221,53 @@
{{ getText('optionSectionTitle_misc') }}
</div>
<div class="option-wrap">
<div class="option select">
<vn-select
:label="getText('optionTitle_appTheme')"
:items="listItems.appTheme"
v-model="options.appTheme"
>
</vn-select>
</div>
<div class="option">
<v-form-field
input-id="lec"
<vn-switch
:label="getText('optionTitle_loadEnglishChallenge')"
>
<v-switch
id="lec"
v-model="options.loadEnglishChallenge"
></v-switch>
</v-form-field>
v-model="options.loadEnglishChallenge"
></vn-switch>
</div>
<div class="option" v-if="!options.loadEnglishChallenge">
<v-form-field
input-id="esm"
<vn-switch
:label="getText('optionTitle_tryEnglishSpeechModel')"
>
<v-switch
id="esm"
v-model="options.tryEnglishSpeechModel"
></v-switch>
</v-form-field>
v-model="options.tryEnglishSpeechModel"
></vn-switch>
</div>
<div class="option" v-if="enableContributions">
<vn-switch
:label="getText('optionTitle_showContribPage')"
v-model="options.showContribPage"
></vn-switch>
</div>
<div class="option button" v-if="enableContributions">
<vn-button
class="contribute-button vn-icon--start"
@click="showContribute"
><vn-icon src="/src/contribute/assets/heart.svg"></vn-icon>
{{ getText('buttonLabel_contribute') }}
</vn-button>
</div>
</div>
</div>
</div>
</v-app>
</template>
<script>
import browser from 'webextension-polyfill';
import {Button, Select, Switch, FormField, TextField} from 'ext-components';
import {toRaw} from 'vue';
import {Button, Icon, Select, Switch, TextField} from 'vueton';
import storage from 'storage/storage';
import {getOptionLabels, pingClientApp} from 'utils/app';
import {getText, getPlatform} from 'utils/common';
import {clientAppVersion} from 'utils/config';
import {getListItems, showContributePage, pingClientApp} from 'utils/app';
import {getText} from 'utils/common';
import {enableContributions, clientAppVersion} from 'utils/config';
import {
optionKeys,
clientAppPlatforms,
@ -274,9 +277,9 @@ import {
export default {
components: {
[Button.name]: Button,
[Icon.name]: Icon,
[Select.name]: Select,
[Switch.name]: Switch,
[FormField.name]: FormField,
[TextField.name]: TextField
},
@ -284,55 +287,81 @@ export default {
return {
dataLoaded: false,
selectOptions: getOptionLabels({
speechService: [
'witSpeechApiDemo',
'googleSpeechApi',
'witSpeechApi',
'ibmSpeechApi',
'microsoftSpeechApi'
],
ibmSpeechApiLoc: [
'seoul',
'london',
'frankfurt',
'dallas',
'washington',
'sydney',
'tokyo'
],
microsoftSpeechApiLoc: [
'eastAu',
'centralCa',
'centralUs',
'centralFr',
'centralIn',
'eastJp',
'westJp',
'southBr',
'centralKr',
'northCh',
'northCentralUs',
'southCentralUs',
'westCentralUs',
'southUk',
'eastUs',
'eastUs2',
'westUs',
'westUs2',
'eastAsia',
'southeastAsia',
'westEu',
'northEu'
],
witSpeechApiLang: [
...new Set(
Object.values(captchaWitSpeechApiLangCodes).filter(Boolean)
)
].sort()
}),
witSpeechApiLang: '',
listItems: {
...getListItems(
{
speechService: [
'witSpeechApiDemo',
'googleSpeechApi',
'witSpeechApi',
'ibmSpeechApi',
'microsoftSpeechApi'
]
},
{scope: 'optionValue_speechService'}
),
...getListItems(
{
ibmSpeechApiLoc: [
'seoul',
'london',
'frankfurt',
'dallas',
'washington',
'sydney',
'tokyo'
]
},
{scope: 'optionValue_ibmSpeechApiLoc'}
),
...getListItems(
{
microsoftSpeechApiLoc: [
'eastAu',
'centralCa',
'centralUs',
'centralFr',
'centralIn',
'eastJp',
'westJp',
'southBr',
'centralKr',
'northCh',
'northCentralUs',
'southCentralUs',
'westCentralUs',
'southUk',
'eastUs',
'eastUs2',
'westUs',
'westUs2',
'eastAsia',
'southeastAsia',
'westEu',
'northEu'
]
},
{scope: 'optionValue_microsoftSpeechApiLoc'}
),
...getListItems(
{
witSpeechApiLang: [
...new Set(
Object.values(captchaWitSpeechApiLangCodes).filter(Boolean)
)
].sort()
},
{scope: 'optionValue_witSpeechApiLang'}
),
...getListItems(
{appTheme: ['auto', 'light', 'dark']},
{scope: 'optionValue_appTheme'}
)
},
enableContributions,
witSpeechApiLang: null,
witSpeechApis: [],
clientAppVerified: false,
@ -352,7 +381,9 @@ export default {
tryEnglishSpeechModel: false,
simulateUserInput: false,
autoUpdateClientApp: false,
navigateWithKeyboard: false
navigateWithKeyboard: false,
appTheme: '',
showContribPage: false
}
};
},
@ -368,7 +399,7 @@ export default {
if (!this.installGuideUrl) {
this.installGuideUrl =
'https://github.com/dessant/buster/wiki/Installing-the-client-app';
const {os, arch} = await getPlatform();
const {os, arch} = this.$env;
if (clientAppPlatforms.includes(`${os}/${arch}`)) {
this.installGuideUrl += `#${os}`;
this.clientAppDownloadUrl = `https://github.com/dessant/buster-client/releases/download/v${clientAppVersion}/buster-client-setup-v${clientAppVersion}-${os}-${arch}`;
@ -385,14 +416,14 @@ export default {
},
setWitSpeechApiLangOptions: function () {
this.selectOptions.witSpeechApiLang = this.selectOptions.witSpeechApiLang.filter(
item => !this.witSpeechApis.includes(item.id)
this.listItems.witSpeechApiLang = this.listItems.witSpeechApiLang.filter(
item => !this.witSpeechApis.includes(item.value)
);
},
addWitSpeechApi: function () {
this.witSpeechApis.push(this.witSpeechApiLang);
this.witSpeechApiLang = '';
this.witSpeechApiLang = null;
this.setWitSpeechApiLangOptions();
},
@ -406,17 +437,27 @@ export default {
delete apiKeys[lang];
this.options.witSpeechApiKeys = Object.assign({}, apiKeys);
}
},
showContribute: async function () {
await showContributePage();
}
},
created: async function () {
const options = await storage.get(optionKeys, 'sync');
const options = await storage.get(optionKeys);
for (const option of Object.keys(this.options)) {
this.options[option] = options[option];
this.$watch(`options.${option}`, async function (value) {
await storage.set({[option]: value}, 'sync');
});
this.$watch(
`options.${option}`,
async function (value) {
await storage.set({[option]: toRaw(value)});
await browser.runtime.sendMessage({id: 'optionChange'});
},
{deep: true}
);
}
this.witSpeechApis = Object.keys(options.witSpeechApiKeys);
@ -435,42 +476,32 @@ export default {
</script>
<style lang="scss">
$mdc-theme-primary: #1abc9c;
@import '@material/select/mdc-select';
@import '@material/theme/mixins';
@import '@material/typography/mixins';
@import '@material/button/mixins';
body {
margin: 0;
@include mdc-typography-base;
font-size: 100%;
background-color: #ffffff;
overflow: visible !important;
}
@use 'vueton/styles' as vueton;
@include vueton.theme-base;
#app {
.v-application__wrap {
display: grid;
grid-row-gap: 32px;
grid-column-gap: 48px;
padding: 24px;
}
.mdc-switch {
margin-right: 16px;
}
.section-title,
.section-desc {
@include mdc-theme-prop(color, text-primary-on-light);
grid-auto-rows: min-content;
grid-auto-columns: min-content;
}
.section-title {
@include mdc-typography(headline6);
font-size: 20px;
font-weight: 500;
letter-spacing: 0.25px;
line-height: 32px;
}
.section-desc {
@include mdc-typography(body2);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.25px;
line-height: 20px;
padding-top: 8px;
max-width: 380px;
}
@ -479,73 +510,80 @@ body {
display: grid;
grid-row-gap: 24px;
padding-top: 24px;
grid-auto-columns: min-content;
}
.option {
display: flex;
align-items: center;
height: 24px;
& .mdc-form-field {
max-width: calc(100vw - 48px);
height: 20px;
& label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&.button {
height: 40px;
}
}
.option {
&.select,
&.text-field {
height: 56px;
}
}
.option.select {
align-items: start;
& .contribute-button {
@include vueton.theme-prop(color, primary);
& .mdc-select__anchor,
& .mdc-select__menu {
max-width: calc(100vw - 48px);
& .vn-icon {
@include vueton.theme-prop(background-color, cta);
}
}
}
& .mdc-select__selected-text {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.text-field .v-input__control {
width: 326px;
}
.section-client .section-desc {
width: 272px;
}
.wit-add-api {
display: flex;
align-items: center;
}
.wit-add-api > button {
margin-left: 24px;
white-space: nowrap;
& .vn-select {
& .v-input__control,
& .v-input__details {
max-width: calc(100vw - 48px - 124px) !important;
}
}
& .vn-button {
margin-left: 24px;
@include vueton.theme-prop(color, primary);
}
}
.service-guide {
@include mdc-typography(body1);
font-size: 16px;
font-weight: 400;
letter-spacing: 0.5px;
line-height: 24px;
}
.client-download {
min-width: 300px;
width: 272px;
}
.download-desc,
.download-error {
@include mdc-theme-prop(color, text-primary-on-light);
@include mdc-typography(body2);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.25px;
line-height: 20px;
}
.download-desc a {
@include mdc-typography(body2);
font-size: 14px;
font-weight: 400;
letter-spacing: 0.25px;
line-height: 20px;
}
.download-desc {
@ -554,7 +592,7 @@ body {
.download-error {
margin-top: 12px;
color: #e74c3c;
@include vueton.theme-prop(color, error);
}
.download-link {
@ -562,9 +600,11 @@ body {
}
.download-button {
@include mdc-button-ink-color(#fff);
width: 200px;
height: 48px;
margin-top: 24px;
@include vueton.theme-prop(background-color, primary);
& .v-btn__content {
@include vueton.theme-prop(color, on-primary);
}
}
</style>

@ -3,16 +3,19 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/icons/app/icon-32.png" type="image/png" />
<link href="/src/fonts/roboto.css" rel="stylesheet" />
<meta name="referrer" content="no-referrer" />
<link
rel="icon"
href="/src/assets/icons/app/icon-64.png"
type="image/png"
/>
<link href="/src/assets/fonts/roboto.css" rel="stylesheet" />
<link href="/src/commons-ui/style.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<script src="/src/manifest.js"></script>
<script src="/src/commons-ui/script.js"></script>
<script src="script.js"></script>
</body>
<script src="script.js" defer></script>
</head>
<body></body>
</html>

@ -1,17 +1,18 @@
import Vue from 'vue';
import {createApp} from 'vue';
import {configApp, loadFonts} from 'utils/app';
import {configVuetify} from 'utils/vuetify';
import App from './App';
async function init() {
try {
await document.fonts.load('400 14px Roboto');
await document.fonts.load('500 14px Roboto');
} catch (err) {}
await loadFonts(['400 14px Roboto', '500 14px Roboto']);
new Vue({
el: '#app',
render: h => h(App)
});
const app = createApp(App);
await configApp(app);
await configVuetify(app);
app.mount('body');
}
init();

@ -1,5 +1,5 @@
function setup() {
const url = new URL(chrome.runtime.getURL('/src/setup/index.html'));
const url = new URL(browser.runtime.getURL('/src/setup/index.html'));
url.searchParams.set(
'session',
new URL(window.location.href).searchParams.get('session')

@ -16,17 +16,19 @@
const onMessage = function (ev) {
ev.stopImmediatePropagation();
window.clearTimeout(timeoutId);
removeCallbacks();
reset(ev.detail);
};
const timeoutId = window.setTimeout(function () {
const removeCallbacks = function () {
window.clearTimeout(timeoutId);
document.removeEventListener('___resetCaptcha', onMessage, {
capture: true,
once: true
});
}, 10000); // 10 seconds
};
const timeoutId = window.setTimeout(removeCallbacks, 10000); // 10 seconds
document.addEventListener('___resetCaptcha', onMessage, {
capture: true,

@ -1,5 +1,5 @@
<template>
<div id="app" v-if="dataLoaded">
<v-app id="app" v-if="dataLoaded">
<div class="wrap" v-if="!isInstallSuccess && !isInstallError">
<div class="title">
{{ getText('pageContent_installTitle') }}
@ -8,33 +8,34 @@
{{ getText('pageContent_installDesc') }}
</div>
<v-textfield
v-model.trim="appDir"
<vn-text-field
:label="getText('inputLabel_appLocation')"
v-model.trim="appDir"
>
</v-textfield>
</vn-text-field>
<div class="manifest-desc" v-if="manifestDirEditable">
{{ getText('pageContent_manifestLocationDesc') }}
</div>
<v-textfield
<vn-text-field
class="manifest-location"
v-if="manifestDirEditable"
v-model.trim="manifestDir"
:label="getText('inputLabel_manifestLocation')"
v-model.trim="manifestDir"
>
</v-textfield>
</vn-text-field>
<v-button
<div class="manifest-desc" v-if="manifestDirEditable">
{{ getText('pageContent_manifestLocationDesc') }}
</div>
<vn-button
class="button install-button"
:unelevated="true"
:disabled="
isInstalling || !appDir || (manifestDirEditable && !manifestDir)
"
:label="getText('buttonText_installApp')"
@click="runInstall"
variant="elevated"
>
</v-button>
{{ getText('buttonLabel_installApp') }}
</vn-button>
</div>
<div class="wrap" v-if="isInstallSuccess">
@ -50,25 +51,23 @@
</div>
<div class="desc">{{ getText('pageContent_installErrorDesc') }}</div>
<v-button
<vn-button
class="button error-button"
:unelevated="true"
:label="getText('buttonText_goBack')"
@click="isInstallError = false"
variant="elevated"
>
</v-button>
{{ getText('buttonLabel_goBack') }}
</vn-button>
</div>
</div>
</v-app>
</template>
<script>
import browser from 'webextension-polyfill';
import {Button, TextField} from 'ext-components';
import {Button, TextField} from 'vueton';
import storage from 'storage/storage';
import {pingClientApp} from 'utils/app';
import {getText} from 'utils/common';
import {targetEnv} from 'utils/config';
export default {
components: {
@ -102,7 +101,7 @@ export default {
getExtensionId: function () {
let id = browser.runtime.id;
if (targetEnv !== 'firefox') {
if (!this.$env.isFirefox) {
const scheme = window.location.protocol;
id = `${scheme}//${id}/`;
}
@ -148,7 +147,7 @@ export default {
const data = new FormData();
data.append('session', this.session);
data.append('browser', this.browser);
data.append('targetEnv', targetEnv);
data.append('targetEnv', this.$env.targetEnv);
const rsp = await fetch(`${this.apiUrl}/setup/location`, {
referrer: '',
@ -173,7 +172,7 @@ export default {
data.append('appDir', this.appDir);
data.append('manifestDir', this.manifestDir);
data.append('browser', this.browser);
data.append('targetEnv', targetEnv);
data.append('targetEnv', this.$env.targetEnv);
data.append('extension', this.getExtensionId());
const rsp = await fetch(`${this.apiUrl}/setup/install`, {
@ -185,7 +184,7 @@ export default {
if (rsp.status === 200) {
await pingClientApp();
await storage.set({simulateUserInput: true}, 'sync');
await storage.set({simulateUserInput: true});
this.isInstallSuccess = true;
} else {
@ -199,8 +198,7 @@ export default {
await this.setLocation();
const {os} = await browser.runtime.sendMessage({id: 'getPlatform'});
if (os !== 'windows') {
if (!this.$env.isWindows) {
this.manifestDirEditable = true;
}
@ -210,23 +208,14 @@ export default {
</script>
<style lang="scss">
$mdc-theme-primary: #1abc9c;
@import '@material/theme/mixins';
@import '@material/typography/mixins';
@import '@material/button/mixins';
@use 'vueton/styles' as vueton;
body {
@include mdc-typography-base;
font-size: 100%;
background-color: #ecf0f1;
margin: 0;
}
@include vueton.theme-base;
#app {
.v-application__wrap {
display: flex;
justify-content: center;
padding: 12px;
align-items: center;
padding: 24px;
}
.wrap {
@ -235,49 +224,64 @@ body {
max-width: 400px;
}
.title,
.desc,
.manifest-desc {
@include mdc-theme-prop(color, text-primary-on-light);
}
.title {
@include mdc-typography(headline6);
font-size: 20px;
font-weight: 500;
letter-spacing: 0.25px;
line-height: 32px;
margin-top: 48px;
align-self: center;
}
.error-title {
color: #e74c3c;
@include vueton.theme-prop(color, error);
}
.desc {
@include mdc-typography(body2);
margin-top: 24px;
font-size: 14px;
font-weight: 400;
letter-spacing: 0.25px;
line-height: 20px;
margin-top: 32px;
margin-bottom: 24px;
}
.manifest-location {
margin-top: 24px;
}
.manifest-desc {
@include mdc-typography(caption);
font-size: 12px;
font-weight: 400;
letter-spacing: 0.4px;
line-height: 20px;
margin-top: 12px;
margin-bottom: 4px;
}
.button {
@include mdc-button-ink-color(#fff);
width: 200px;
height: 48px;
@include vueton.theme-prop(background-color, primary);
& .v-btn__content {
@include vueton.theme-prop(color, on-primary);
}
}
.install-button {
margin-top: 36px;
margin-top: 32px;
align-self: center;
}
.error-button {
margin-top: 12px;
margin-top: 8px;
align-self: center;
}
.success-icon {
font-size: 72px;
margin-top: 36px;
margin-top: 48px;
align-self: center;
}
</style>

@ -3,16 +3,19 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/icons/app/icon-32.png" type="image/png" />
<link href="/src/fonts/roboto.css" rel="stylesheet" />
<meta name="referrer" content="no-referrer" />
<link
rel="icon"
href="/src/assets/icons/app/icon-64.png"
type="image/png"
/>
<link href="/src/assets/fonts/roboto.css" rel="stylesheet" />
<link href="/src/commons-ui/style.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<script src="/src/manifest.js"></script>
<script src="/src/commons-ui/script.js"></script>
<script src="script.js"></script>
</body>
<script src="script.js" defer></script>
</head>
<body></body>
</html>

@ -1,17 +1,18 @@
import Vue from 'vue';
import {createApp} from 'vue';
import {configApp, loadFonts} from 'utils/app';
import {configVuetify} from 'utils/vuetify';
import App from './App';
async function init() {
try {
await document.fonts.load('400 14px Roboto');
await document.fonts.load('500 14px Roboto');
} catch (err) {}
await loadFonts(['400 14px Roboto', '500 14px Roboto']);
new Vue({
el: '#app',
render: h => h(App)
});
const app = createApp(App);
await configApp(app);
await configVuetify(app);
app.mount('body');
}
// only run in a frame

@ -1,10 +1,8 @@
import browser from 'webextension-polyfill';
import storage from 'storage/storage';
import {meanSleep, pingClientApp} from 'utils/app';
import {
getText,
waitForElement,
findNode,
getRandomFloat,
sleep,
getBrowser
@ -41,7 +39,7 @@ function syncUI() {
const button = document.createElement('button');
button.classList.add('rc-button');
button.setAttribute('tabindex', '0');
button.setAttribute('title', getText('buttonText_reset'));
button.setAttribute('title', getText('buttonLabel_reset'));
button.id = 'reset-button';
button.addEventListener('click', resetCaptcha);
@ -76,7 +74,7 @@ function syncUI() {
solverButton = document.createElement('button');
solverButton.setAttribute('tabindex', '0');
solverButton.setAttribute('title', getText('buttonText_solve'));
solverButton.setAttribute('title', getText('buttonLabel_solve'));
solverButton.id = 'solver-button';
if (solverWorking) {
solverButton.classList.add('working');
@ -92,7 +90,7 @@ function isBlocked({timeout = 0} = {}) {
const selector = '.rc-doscaptcha-body';
if (timeout) {
return new Promise(resolve => {
waitForElement(selector, {timeout}).then(result =>
findNode(selector, {timeout, throwError: false}).then(result =>
resolve(Boolean(result))
);
});
@ -258,10 +256,7 @@ async function solve(simulateUserInput, clickEvent) {
return;
}
const {navigateWithKeyboard} = await storage.get(
'navigateWithKeyboard',
'sync'
);
const {navigateWithKeyboard} = await storage.get('navigateWithKeyboard');
let browserBorder;
let osScale;
@ -292,9 +287,11 @@ async function solve(simulateUserInput, clickEvent) {
const result = await Promise.race([
new Promise(resolve => {
waitForElement(audioElSelector, {timeout: 10000}).then(el => {
meanSleep(500).then(() => resolve({audioEl: el}));
});
findNode(audioElSelector, {timeout: 10000, throwError: false}).then(
el => {
meanSleep(500).then(() => resolve({audioEl: el}));
}
);
}),
new Promise(resolve => {
isBlocked({timeout: 10000}).then(blocked => resolve({blocked}));
@ -415,10 +412,10 @@ function solveChallenge(ev) {
}
async function runSolver(ev) {
const {simulateUserInput, autoUpdateClientApp} = await storage.get(
['simulateUserInput', 'autoUpdateClientApp'],
'sync'
);
const {simulateUserInput, autoUpdateClientApp} = await storage.get([
'simulateUserInput',
'autoUpdateClientApp'
]);
if (simulateUserInput) {
try {
@ -497,7 +494,7 @@ async function runSolver(ev) {
function init() {
const observer = new MutationObserver(syncUI);
observer.observe(document.body, {
observer.observe(document, {
childList: true,
subtree: true
});

@ -0,0 +1,17 @@
{
"revisions": {
"local": [
"UoT3kGyBH",
"ONiJBs00o",
"UidMDYaYA",
"nOedd0Txqd",
"ZtLMLoh1ag",
"t335iRDhZ8",
"X3djS8vZC",
"DlgF14Chrh",
"Lj3MYlSr4L",
"20221211221603_add_theme_support"
],
"sync": []
}
}

@ -1,11 +1,44 @@
import {migrate} from 'storage-versions';
import {migrate} from 'wesa';
import {getSupportedArea} from './storage';
import {isStorageArea} from './storage';
async function initStorage({area = 'local'} = {}) {
const context = {
getAvailableRevisions: async ({area} = {}) =>
(
await import(/* webpackMode: "eager" */ 'storage/config.json', {
assert: {type: 'json'}
})
).revisions[area],
getCurrentRevision: async ({area} = {}) =>
(await browser.storage[area].get('storageVersion')).storageVersion,
getRevision: async ({area, revision} = {}) =>
import(
/* webpackMode: "eager" */ `storage/revisions/${area}/${revision}.js`
)
};
async function initStorage(area = 'local') {
area = await getSupportedArea(area);
const context = require.context('storage/versions', true, /\.(?:js|json)$/i);
return migrate(context, {area});
}
export {initStorage};
async function migrateLegacyStorage() {
if (await isStorageArea({area: 'sync'})) {
const {storageVersion: syncVersion} = await browser.storage.sync.get(
'storageVersion'
);
if (syncVersion && syncVersion.length < 14) {
const {storageVersion: localVersion} = await browser.storage.local.get(
'storageVersion'
);
if (!localVersion || localVersion.length < 14) {
const syncData = await browser.storage.sync.get(null);
await browser.storage.local.clear();
await browser.storage.local.set(syncData);
await browser.storage.sync.clear();
}
}
}
}
export {initStorage, migrateLegacyStorage};

@ -0,0 +1,22 @@
import {getDayPrecisionEpoch} from 'utils/common';
const message = 'Add theme support';
const revision = '20221211221603_add_theme_support';
async function upgrade() {
const changes = {
appTheme: 'auto', // auto, light, dark
showContribPage: true,
contribPageLastOpen: 0,
contribPageLastAutoOpen: 0
};
const {installTime} = await browser.storage.local.get('installTime');
changes.installTime = getDayPrecisionEpoch(installTime);
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,16 @@
const message = 'Revision description';
const revision = 'DlgF14Chrh';
async function upgrade() {
const changes = {};
const {speechService} = await browser.storage.local.get('speechService');
if (speechService === 'googleSpeechApiDemo') {
changes.speechService = 'witSpeechApiDemo';
}
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,14 @@
const message = 'Add navigateWithKeyboard';
const revision = 'Lj3MYlSr4L';
async function upgrade() {
const changes = {
navigateWithKeyboard: false
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,15 @@
const message = 'Add IBM Watson Speech to Text API';
const revision = 'ONiJBs00o';
async function upgrade() {
const changes = {
ibmSpeechApiLoc: 'frankfurt', // 'frankfurt', 'dallas', 'washington', 'sydney', 'tokyo'
ibmSpeechApiKey: ''
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,15 @@
const message = 'Add Microsoft Azure Speech to Text API';
const revision = 'UidMDYaYA';
async function upgrade() {
const changes = {
microsoftSpeechApiLoc: 'eastUs', // 'eastUs', 'eastUs2', 'westUs', 'westUs2', 'eastAsia', 'southeastAsia', 'westEu', 'northEu'
microsoftSpeechApiKey: ''
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -1,11 +1,6 @@
import browser from 'webextension-polyfill';
const message = 'Initial version';
const revision = 'UoT3kGyBH';
const downRevision = null;
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
@ -16,11 +11,7 @@ async function upgrade() {
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
return storage.clear();
return browser.storage.local.set(changes);
}
export {message, revision, upgrade, downgrade};
export {message, revision, upgrade};

@ -0,0 +1,14 @@
const message = 'Add autoUpdateClientApp option';
const revision = 'X3djS8vZC';
async function upgrade() {
const changes = {
autoUpdateClientApp: true
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,14 @@
const message = 'Add loadEnglishChallenge option';
const revision = 'ZtLMLoh1ag';
async function upgrade() {
const changes = {
loadEnglishChallenge: true
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,15 @@
const message = 'Add Wit Speech API and tryEnglishSpeechModel option';
const revision = 'nOedd0Txqd';
async function upgrade() {
const changes = {
witSpeechApiKeys: {},
tryEnglishSpeechModel: true
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -0,0 +1,14 @@
const message = 'Add simulateUserInput option';
const revision = 't335iRDhZ8';
async function upgrade() {
const changes = {
simulateUserInput: false
};
changes.storageVersion = revision;
return browser.storage.local.set(changes);
}
export {message, revision, upgrade};

@ -1,38 +1,73 @@
import browser from 'webextension-polyfill';
let syncArea;
async function getSupportedArea(requestedArea) {
if (typeof syncArea === 'undefined') {
try {
await browser.storage.sync.get('');
syncArea = true;
} catch (e) {
syncArea = false;
import {storageRevisions} from 'utils/config';
async function isStorageArea({area = 'local'} = {}) {
try {
await browser.storage[area].get('');
return true;
} catch (err) {
return false;
}
}
const storageReady = {};
async function isStorageReady({area = 'local'} = {}) {
if (storageReady[area]) {
return true;
} else {
const {storageVersion} = await browser.storage[area].get('storageVersion');
if (storageVersion && storageVersion === storageRevisions[area]) {
storageReady[area] = true;
return true;
}
}
return syncArea ? requestedArea : 'local';
return false;
}
async function ensureStorageReady({area = 'local'} = {}) {
if (!storageReady[area]) {
return new Promise((resolve, reject) => {
let stop;
const checkStorage = async function () {
if (await isStorageReady({area})) {
window.clearTimeout(timeoutId);
resolve();
} else if (stop) {
reject(new Error(`Storage (${area}) is not ready`));
} else {
window.setTimeout(checkStorage, 30);
}
};
const timeoutId = window.setTimeout(function () {
stop = true;
}, 60000); // 1 minute
checkStorage();
});
}
}
async function get(keys = null, area = 'local') {
area = await getSupportedArea(area);
async function get(keys = null, {area = 'local'} = {}) {
await ensureStorageReady({area});
return browser.storage[area].get(keys);
}
async function set(obj, area = 'local') {
area = await getSupportedArea(area);
async function set(obj, {area = 'local'} = {}) {
await ensureStorageReady({area});
return browser.storage[area].set(obj);
}
async function remove(keys, area = 'local') {
area = await getSupportedArea(area);
async function remove(keys, {area = 'local'} = {}) {
await ensureStorageReady({area});
return browser.storage[area].remove(keys);
}
async function clear(area = 'local') {
area = await getSupportedArea(area);
async function clear({area = 'local'} = {}) {
await ensureStorageReady({area});
return browser.storage[area].clear();
}
export default {get, set, remove, clear};
export {getSupportedArea};
export {isStorageArea, isStorageReady, ensureStorageReady};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Revision description';
const revision = 'DlgF14Chrh';
const downRevision = 'X3djS8vZC';
const storage = browser.storage.local;
async function upgrade() {
const changes = {};
const {speechService} = await storage.get('speechService');
if (speechService === 'googleSpeechApiDemo') {
changes.speechService = 'witSpeechApiDemo';
}
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add navigateWithKeyboard';
const revision = 'Lj3MYlSr4L';
const downRevision = 'DlgF14Chrh';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
navigateWithKeyboard: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove('navigateWithKeyboard');
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add IBM Watson Speech to Text API';
const revision = 'ONiJBs00o';
const downRevision = 'UoT3kGyBH';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
ibmSpeechApiLoc: 'frankfurt', // 'frankfurt', 'dallas', 'washington', 'sydney', 'tokyo'
ibmSpeechApiKey: ''
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['ibmSpeechApiLoc', 'ibmSpeechApiKey']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add Microsoft Azure Speech to Text API';
const revision = 'UidMDYaYA';
const downRevision = 'ONiJBs00o';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
microsoftSpeechApiLoc: 'eastUs', // 'eastUs', 'eastUs2', 'westUs', 'westUs2', 'eastAsia', 'southeastAsia', 'westEu', 'northEu'
microsoftSpeechApiKey: ''
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['microsoftSpeechApiLoc', 'microsoftSpeechApiKey']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,26 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Initial version';
const revision = 'UoT3kGyBH';
const downRevision = null;
const storage = browser.storage.local;
async function upgrade() {
const changes = {
speechService: 'googleSpeechApiDemo', // 'googleSpeechApiDemo', 'witSpeechApiDemo', 'googleSpeechApi', 'witSpeechApi', 'ibmSpeechApi', 'microsoftSpeechApi'
googleSpeechApiKey: '',
installTime: new Date().getTime(),
useCount: 0
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
return storage.clear();
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add autoUpdateClientApp option';
const revision = 'X3djS8vZC';
const downRevision = 't335iRDhZ8';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
autoUpdateClientApp: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['autoUpdateClientApp']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add loadEnglishChallenge option';
const revision = 'ZtLMLoh1ag';
const downRevision = 'nOedd0Txqd';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
loadEnglishChallenge: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['loadEnglishChallenge']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add Wit Speech API and tryEnglishSpeechModel option';
const revision = 'nOedd0Txqd';
const downRevision = 'UidMDYaYA';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
witSpeechApiKeys: {},
tryEnglishSpeechModel: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['witSpeechApiKeys', 'tryEnglishSpeechModel']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add simulateUserInput option';
const revision = 't335iRDhZ8';
const downRevision = 'ZtLMLoh1ag';
const storage = browser.storage.local;
async function upgrade() {
const changes = {
simulateUserInput: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['simulateUserInput']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,13 +0,0 @@
{
"versions": [
"UoT3kGyBH",
"ONiJBs00o",
"UidMDYaYA",
"nOedd0Txqd",
"ZtLMLoh1ag",
"t335iRDhZ8",
"X3djS8vZC",
"DlgF14Chrh",
"Lj3MYlSr4L"
]
}

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Revision description';
const revision = 'DlgF14Chrh';
const downRevision = 'X3djS8vZC';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {};
const {speechService} = await storage.get('speechService');
if (speechService === 'googleSpeechApiDemo') {
changes.speechService = 'witSpeechApiDemo';
}
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add navigateWithKeyboard';
const revision = 'Lj3MYlSr4L';
const downRevision = 'DlgF14Chrh';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
navigateWithKeyboard: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove('navigateWithKeyboard');
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add IBM Watson Speech to Text API';
const revision = 'ONiJBs00o';
const downRevision = 'UoT3kGyBH';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
ibmSpeechApiLoc: 'frankfurt', // 'frankfurt', 'dallas', 'washington', 'sydney', 'tokyo'
ibmSpeechApiKey: ''
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['ibmSpeechApiLoc', 'ibmSpeechApiKey']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add Microsoft Azure Speech to Text API';
const revision = 'UidMDYaYA';
const downRevision = 'ONiJBs00o';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
microsoftSpeechApiLoc: 'eastUs', // 'eastUs', 'eastUs2', 'westUs', 'westUs2', 'eastAsia', 'southeastAsia', 'westEu', 'northEu'
microsoftSpeechApiKey: ''
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['microsoftSpeechApiLoc', 'microsoftSpeechApiKey']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add autoUpdateClientApp option';
const revision = 'X3djS8vZC';
const downRevision = 't335iRDhZ8';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
autoUpdateClientApp: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['autoUpdateClientApp']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add loadEnglishChallenge option';
const revision = 'ZtLMLoh1ag';
const downRevision = 'nOedd0Txqd';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
loadEnglishChallenge: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['loadEnglishChallenge']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,28 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add Wit Speech API and tryEnglishSpeechModel option';
const revision = 'nOedd0Txqd';
const downRevision = 'UidMDYaYA';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
witSpeechApiKeys: {},
tryEnglishSpeechModel: true
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['witSpeechApiKeys', 'tryEnglishSpeechModel']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,27 +0,0 @@
import browser from 'webextension-polyfill';
const message = 'Add simulateUserInput option';
const revision = 't335iRDhZ8';
const downRevision = 'ZtLMLoh1ag';
const storage = browser.storage.sync;
async function upgrade() {
const changes = {
simulateUserInput: false
};
changes.storageVersion = revision;
return storage.set(changes);
}
async function downgrade() {
const changes = {};
await storage.remove(['simulateUserInput']);
changes.storageVersion = downRevision;
return storage.set(changes);
}
export {message, revision, upgrade, downgrade};

@ -1,13 +0,0 @@
{
"versions": [
"UoT3kGyBH",
"ONiJBs00o",
"UidMDYaYA",
"nOedd0Txqd",
"ZtLMLoh1ag",
"t335iRDhZ8",
"X3djS8vZC",
"DlgF14Chrh",
"Lj3MYlSr4L"
]
}

@ -1,13 +1,15 @@
import browser from 'webextension-polyfill';
import {v4 as uuidv4} from 'uuid';
import storage from 'storage/storage';
import {
getText,
createTab,
getActiveTab,
getPlatform,
getDayPrecisionEpoch,
getRandomInt,
sleep
} from 'utils/common';
import {targetEnv, enableContributions} from 'utils/config';
async function showNotification({
message,
@ -28,7 +30,7 @@ async function showNotification({
type: 'basic',
title,
message,
iconUrl: '/src/icons/app/icon-64.png'
iconUrl: '/src/assets/icons/app/icon-64.png'
}
);
@ -41,27 +43,163 @@ async function showNotification({
return notification;
}
function getOptionLabels(data, scope = 'optionValue') {
function getListItems(data, {scope = ''} = {}) {
const labels = {};
for (const [group, items] of Object.entries(data)) {
labels[group] = [];
items.forEach(function (value) {
labels[group].push({
id: value,
label: getText(`${scope}_${group}_${value}`)
});
const item = {
value,
title: getText(`${scope ? scope + '_' : ''}${value}`)
};
labels[group].push(item);
});
}
return labels;
}
async function showContributePage(action = false) {
const activeTab = await getActiveTab();
async function configApp(app) {
const platform = await getPlatform();
document.documentElement.classList.add(platform.targetEnv, platform.os);
if (app) {
app.config.globalProperties.$env = platform;
}
}
async function loadFonts(fonts) {
await Promise.allSettled(fonts.map(font => document.fonts.load(font)));
}
function processMessageResponse(response, sendResponse) {
if (targetEnv === 'safari') {
response.then(function (result) {
// Safari 15: undefined response will cause sendMessage to never resolve.
if (result === undefined) {
result = null;
}
sendResponse(result);
});
return true;
} else {
return response;
}
}
async function showContributePage({
action = '',
activeTab = null,
setOpenerTab = true,
updateStats = true
} = {}) {
if (updateStats) {
await storage.set({contribPageLastOpen: getDayPrecisionEpoch()});
}
if (!activeTab) {
activeTab = await getActiveTab();
}
let url = browser.runtime.getURL('/src/contribute/index.html');
if (action) {
url = `${url}?action=${action}`;
}
await createTab(url, {index: activeTab.index + 1});
const props = {url, index: activeTab.index + 1, active: true};
if (
setOpenerTab &&
activeTab.id !== browser.tabs.TAB_ID_NONE &&
(await getPlatform()).os !== 'android'
) {
props.openerTabId = activeTab.id;
}
return browser.tabs.create(props);
}
async function autoShowContributePage({
minUseCount = 0, // 0-1000
minInstallDays = 0,
minLastOpenDays = 0,
minLastAutoOpenDays = 0,
activeTab = null,
setOpenerTab = true,
action = 'auto'
} = {}) {
if (enableContributions) {
const options = await storage.get([
'showContribPage',
'useCount',
'installTime',
'contribPageLastOpen',
'contribPageLastAutoOpen'
]);
const epoch = getDayPrecisionEpoch();
if (
options.showContribPage &&
options.useCount >= minUseCount &&
epoch - options.installTime >= minInstallDays * 86400000 &&
epoch - options.contribPageLastOpen >= minLastOpenDays * 86400000 &&
epoch - options.contribPageLastAutoOpen >= minLastAutoOpenDays * 86400000
) {
await storage.set({
contribPageLastOpen: epoch,
contribPageLastAutoOpen: epoch
});
return showContributePage({
action,
activeTab,
setOpenerTab,
updateStats: false
});
}
}
}
let useCountLastUpdate = 0;
async function updateUseCount({
valueChange = 1,
maxUseCount = Infinity,
minInterval = 0
} = {}) {
if (Date.now() - useCountLastUpdate >= minInterval) {
useCountLastUpdate = Date.now();
const {useCount} = await storage.get('useCount');
if (useCount < maxUseCount) {
await storage.set({useCount: useCount + valueChange});
} else if (useCount > maxUseCount) {
await storage.set({useCount: maxUseCount});
}
}
}
async function processAppUse({
activeTab = null,
setOpenerTab = true,
action = 'auto'
} = {}) {
await updateUseCount({
valueChange: 1,
maxUseCount: 1000
});
return autoShowContributePage({
minUseCount: 10,
minInstallDays: 14,
minLastOpenDays: 14,
minLastAutoOpenDays: 365,
activeTab,
setOpenerTab,
action
});
}
function meanSleep(ms) {
@ -129,8 +267,14 @@ async function pingClientApp({
export {
showNotification,
getOptionLabels,
getListItems,
configApp,
loadFonts,
processMessageResponse,
showContributePage,
autoShowContributePage,
updateUseCount,
processAppUse,
meanSleep,
sendNativeMessage,
pingClientApp

@ -1,44 +1,92 @@
import browser from 'webextension-polyfill';
import Bowser from 'bowser';
import {targetEnv} from 'utils/config';
const getText = browser.i18n.getMessage;
function createTab(
url,
{index = null, active = true, openerTabId = null} = {}
) {
const props = {url, active};
if (index !== null) {
props.index = index;
}
if (openerTabId !== null && ['chrome', 'edge', 'opera'].includes(targetEnv)) {
props.openerTabId = openerTabId;
}
return browser.tabs.create(props);
function getText(messageName, substitutions) {
return browser.i18n.getMessage(messageName, substitutions);
}
async function isAndroid() {
const {os} = await browser.runtime.getPlatformInfo();
return os === 'android';
}
async function getPlatform({fallback = true} = {}) {
let os, arch;
if (targetEnv === 'samsung') {
// Samsung Internet 13: runtime.getPlatformInfo fails.
os = 'android';
arch = '';
} else {
try {
({os, arch} = await browser.runtime.getPlatformInfo());
} catch (err) {
if (fallback) {
({os, arch} = await browser.runtime.sendMessage({id: 'getPlatform'}));
} else {
throw err;
}
}
}
async function getPlatform() {
let {os, arch} = await browser.runtime.getPlatformInfo();
if (os === 'win') {
os = 'windows';
} else if (os === 'mac') {
os = 'macos';
}
if (
navigator.platform === 'MacIntel' &&
(os === 'ios' || typeof navigator.standalone !== 'undefined')
) {
os = 'ipados';
}
if (arch === 'x86-32') {
arch = '386';
} else if (arch === 'x86-64' || (arch.startsWith('arm') && os === 'macos')) {
} else if (arch === 'x86-64') {
arch = 'amd64';
} else if (arch.startsWith('arm')) {
arch = 'arm';
}
return {os, arch};
// Client app supports ARM with Rosetta 2
if (arch === 'arm' && os === 'macos') {
arch = 'amd64';
}
const isWindows = os === 'windows';
const isMacos = os === 'macos';
const isLinux = os === 'linux';
const isAndroid = os === 'android';
const isIos = os === 'ios';
const isIpados = os === 'ipados';
const isMobile = ['android', 'ios', 'ipados'].includes(os);
const isChrome = targetEnv === 'chrome';
const isEdge = targetEnv === 'edge';
const isFirefox = targetEnv === 'firefox';
const isOpera =
['chrome', 'opera'].includes(targetEnv) &&
/ opr\//i.test(navigator.userAgent);
const isSafari = targetEnv === 'safari';
const isSamsung = targetEnv === 'samsung';
return {
os,
arch,
targetEnv,
isWindows,
isMacos,
isLinux,
isAndroid,
isIos,
isIpados,
isMobile,
isChrome,
isEdge,
isFirefox,
isOpera,
isSafari,
isSamsung
};
}
async function getBrowser() {
@ -58,6 +106,11 @@ async function getBrowser() {
return {name, version};
}
async function isAndroid() {
const {os} = await getPlatform();
return os === 'android';
}
async function getActiveTab() {
const [tab] = await browser.tabs.query({
lastFocusedWindow: true,
@ -66,16 +119,26 @@ async function getActiveTab() {
return tab;
}
function waitForElement(selector, {timeout = 10000} = {}) {
return new Promise(resolve => {
const el = document.querySelector(selector);
function findNode(
selector,
{
timeout = 60000,
throwError = true,
observerOptions = null,
rootNode = null
} = {}
) {
return new Promise((resolve, reject) => {
rootNode = rootNode || document;
const el = rootNode.querySelector(selector);
if (el) {
resolve(el);
return;
}
const observer = new MutationObserver(function (mutations, obs) {
const el = document.querySelector(selector);
const el = rootNode.querySelector(selector);
if (el) {
obs.disconnect();
window.clearTimeout(timeoutId);
@ -83,14 +146,24 @@ function waitForElement(selector, {timeout = 10000} = {}) {
}
});
observer.observe(document, {
const options = {
childList: true,
subtree: true
});
};
if (observerOptions) {
Object.assign(options, observerOptions);
}
observer.observe(rootNode, options);
const timeoutId = window.setTimeout(function () {
observer.disconnect();
resolve();
if (throwError) {
reject(new Error(`DOM node not found: ${selector}`));
} else {
resolve();
}
}, timeout);
});
}
@ -148,6 +221,18 @@ async function functionInContext(
return isFunction;
}
function getDarkColorSchemeQuery() {
return window.matchMedia('(prefers-color-scheme: dark)');
}
function getDayPrecisionEpoch(epoch) {
if (!epoch) {
epoch = Date.now();
}
return epoch - (epoch % 86400000);
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
@ -201,17 +286,18 @@ async function sliceAudio({audioBuffer, start, end}) {
export {
getText,
createTab,
isAndroid,
getPlatform,
getBrowser,
getActiveTab,
waitForElement,
findNode,
arrayBufferToBase64,
executeCode,
executeFile,
scriptsAllowed,
functionInContext,
getDarkColorSchemeQuery,
getDayPrecisionEpoch,
getRandomInt,
getRandomFloat,
sleep,

@ -1,5 +1,9 @@
const targetEnv = process.env.TARGET_ENV;
const enableContributions = process.env.ENABLE_CONTRIBUTIONS === 'true';
const storageRevisions = {local: process.env.STORAGE_REVISION_LOCAL};
const clientAppVersion = '0.3.0';
export {targetEnv, clientAppVersion};
export {targetEnv, enableContributions, storageRevisions, clientAppVersion};

@ -10,7 +10,9 @@ const optionKeys = [
'tryEnglishSpeechModel',
'simulateUserInput',
'autoUpdateClientApp',
'navigateWithKeyboard'
'navigateWithKeyboard',
'appTheme',
'showContribPage'
];
const clientAppPlatforms = [
@ -20,7 +22,8 @@ const clientAppPlatforms = [
'macos/amd64'
];
const recaptchaChallengeUrlRx = /^https:\/\/www\.(?:google\.com|recaptcha\.net)\/recaptcha\/(?:api2|enterprise)\/bframe.*/;
const recaptchaChallengeUrlRx =
/^https:\/\/(?:www\.)?(?:google\.com|recaptcha\.net)\/recaptcha\/(?:api2|enterprise)\/bframe.*/;
// https://developers.google.com/recaptcha/docs/language
// https://cloud.google.com/speech-to-text/docs/languages
@ -245,6 +248,7 @@ const captchaMicrosoftSpeechApiLangCodes = {
zu: '' // Zulu
};
// https://wit.ai/faq
const captchaWitSpeechApiLangCodes = {
ar: 'arabic', // Arabic
af: '', // Afrikaans

@ -0,0 +1,64 @@
import {createVuetify} from 'vuetify';
import storage from 'storage/storage';
import {getDarkColorSchemeQuery} from 'utils/common';
const LightTheme = {
dark: false,
colors: {
background: '#FFFFFF',
surface: '#FFFFFF',
primary: '#6750A4',
secondary: '#625B71'
}
};
const DarkTheme = {
dark: true,
colors: {
background: '#1C1B1F',
surface: '#1C1B1F',
primary: '#D0BCFF',
secondary: '#CCC2DC'
}
};
async function configTheme(vuetify) {
async function setTheme(theme) {
if (!theme) {
({appTheme: theme} = await storage.get('appTheme'));
}
if (theme === 'auto') {
theme = getDarkColorSchemeQuery().matches ? 'dark' : 'light';
}
vuetify.theme.global.name.value = theme;
}
getDarkColorSchemeQuery().addEventListener('change', function () {
setTheme();
});
browser.storage.onChanged.addListener(function (changes, area) {
if (area === 'local' && changes.appTheme) {
setTheme(changes.appTheme.newValue);
}
});
await setTheme();
}
async function configVuetify(app) {
const vuetify = createVuetify({
theme: {
themes: {light: LightTheme, dark: DarkTheme}
}
});
await configTheme(vuetify);
app.use(vuetify);
}
export {configTheme, configVuetify};

@ -1,27 +1,58 @@
const path = require('path');
const path = require('node:path');
const {lstatSync, readdirSync} = require('node:fs');
const webpack = require('webpack');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const {VueLoaderPlugin} = require('vue-loader');
const {VuetifyPlugin} = require('webpack-plugin-vuetify');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const targetEnv = process.env.TARGET_ENV || 'firefox';
const storageRevisions = require('./src/storage/config.json').revisions;
const targetEnv = process.env.TARGET_ENV || 'chrome';
const isProduction = process.env.NODE_ENV === 'production';
const enableContributions =
(process.env.ENABLE_CONTRIBUTIONS || 'true') === 'true';
const provideExtApi = !['firefox'].includes(targetEnv);
const provideModules = {};
if (provideExtApi) {
provideModules.browser = 'webextension-polyfill';
}
let plugins = [
const plugins = [
new webpack.DefinePlugin({
'process.env': {
TARGET_ENV: JSON.stringify(targetEnv)
TARGET_ENV: JSON.stringify(targetEnv),
STORAGE_REVISION_LOCAL: JSON.stringify(storageRevisions.local.at(-1)),
ENABLE_CONTRIBUTIONS: JSON.stringify(enableContributions.toString())
},
global: {}
__VUE_OPTIONS_API__: true,
__VUE_PROD_DEVTOOLS__: false
}),
new webpack.ProvidePlugin(provideModules),
new VueLoaderPlugin(),
new VuetifyPlugin(),
new MiniCssExtractPlugin({
filename: '[name]/style.css'
filename: '[name]/style.css',
ignoreOrder: true
}),
isProduction ? new LodashModuleReplacementPlugin({shorthands: true}) : null
];
plugins = plugins.filter(Boolean);
].filter(Boolean);
const scriptsRootDir = path.join(__dirname, 'src/scripts');
const scripts = readdirSync(scriptsRootDir)
.filter(file => lstatSync(path.join(scriptsRootDir, file)).isFile())
.map(file => file.split('.')[0]);
const entries = Object.fromEntries(
scripts.map(script => [script, `./src/scripts/${script}.js`])
);
if (enableContributions) {
entries.contribute = './src/contribute/main.js';
}
module.exports = {
mode: isProduction ? 'production' : 'development',
@ -30,16 +61,19 @@ module.exports = {
options: './src/options/main.js',
contribute: './src/contribute/main.js',
solve: './src/solve/main.js',
setup: './src/setup/main.js'
setup: './src/setup/main.js',
...entries
},
output: {
path: path.resolve(__dirname, 'dist', targetEnv, 'src'),
filename: pathData => {
return scripts.includes(pathData.chunk.name)
? 'scripts/[name].js'
: '[name]/script.js';
},
chunkFilename: '[name]/script.js'
},
optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: {
cacheGroups: {
default: false,
@ -57,7 +91,10 @@ module.exports = {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
use: 'babel-loader',
resolve: {
fullySpecified: false
}
},
{
test: /\.vue$/,
@ -65,7 +102,8 @@ module.exports = {
{
loader: 'vue-loader',
options: {
transformAssetUrls: {img: ''}
transformAssetUrls: {img: ''},
compilerOptions: {whitespace: 'preserve'}
}
}
]
@ -80,7 +118,8 @@ module.exports = {
loader: 'sass-loader',
options: {
sassOptions: {
includePaths: ['node_modules']
includePaths: ['node_modules'],
quietDeps: true
}
}
}
@ -90,8 +129,9 @@ module.exports = {
},
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.js', '.json', '.css', '.scss', '.vue']
extensions: ['.js', '.json', '.css', '.scss', '.vue'],
fallback: {fs: false}
},
devtool: isProduction && targetEnv !== 'opera' ? 'source-map' : false,
devtool: false,
plugins
};

12640
yarn.lock

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save