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: jobs:
build: build:
name: Build name: Build
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
permissions:
contents: read
steps: steps:
- name: Clone repository - name: Clone repository
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
persist-credentials: 'false' persist-credentials: 'false'
- name: Get Node.js version
id: nvm
run: echo ::set-output name=NODE_VERSION::$(cat .nvmrc)
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v3
with: with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }} node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies - name: Install dependencies
run: yarn run: npm install --legacy-peer-deps
- name: Build artifacts - 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: env:
BUSTER_SECRETS: ${{ secrets.BUSTER_SECRETS }} BUSTER_SECRETS: ${{ secrets.BUSTER_SECRETS }}
- name: Hash artifacts - name: Hash artifacts
run: sha256sum artifacts/*/* run: sha256sum artifacts/*/*
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
name: artifacts name: artifacts
@ -36,68 +40,28 @@ jobs:
retention-days: 1 retention-days: 1
release: release:
name: Release on GitHub name: Release on GitHub
runs-on: ubuntu-18.04 runs-on: ubuntu-22.04
needs: [build] needs: [build]
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps: 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 - name: Download artifacts
uses: actions/download-artifact@v2 uses: actions/download-artifact@v3
with: with:
name: artifacts name: artifacts
path: artifacts/ path: artifacts/
- name: Hash artifacts - name: Hash artifacts
run: sha256sum artifacts/*/* run: sha256sum artifacts/*/*
- name: Create GitHub release - name: Create GitHub release
id: create_release uses: softprops/action-gh-release@v1
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ github.token }}
with: with:
tag_name: ${{ steps.release_info.outputs.TAG }} tag_name: ${{ github.ref_name }}
release_name: ${{ steps.release_info.outputs.TAG }} name: ${{ github.ref_name }}
body: | body: |
Download and install the extension from the [extension store](https://github.com/dessant/buster#readme) of your browser. 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). 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 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/ /.assets/
secrets.json /secrets.json
node_modules/ node_modules/
artifacts/ /artifacts/
dist/ /dist/
web-ext-config.js /.vscode
npm-debug.log /web-ext-config.js
yarn-debug.log
yarn-error.log
.yarn-integrity
report.json /npm-debug.log
/report.json
/report.html

@ -1 +1 @@
12.18.4 18.12.1

@ -1,2 +1,2 @@
package.json 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 path = require('node:path');
const {exec} = require('child_process'); const {exec} = require('node:child_process');
const {lstatSync, readdirSync, readFileSync, writeFileSync} = require('fs'); const {
lstatSync,
readdirSync,
readFileSync,
writeFileSync,
rmSync
} = require('node:fs');
const {series, parallel, src, dest} = require('gulp'); const {series, parallel, src, dest} = require('gulp');
const babel = require('gulp-babel');
const postcss = require('gulp-postcss'); const postcss = require('gulp-postcss');
const gulpif = require('gulp-if'); const gulpif = require('gulp-if');
const jsonMerge = require('gulp-merge-json'); const jsonMerge = require('gulp-merge-json');
const jsonmin = require('gulp-jsonmin'); const jsonmin = require('gulp-jsonmin');
const htmlmin = require('gulp-htmlmin'); const htmlmin = require('gulp-htmlmin');
const imagemin = require('gulp-imagemin'); const imagemin = require('gulp-imagemin');
const del = require('del');
const {ensureDirSync, readJsonSync} = require('fs-extra'); const {ensureDirSync, readJsonSync} = require('fs-extra');
const sharp = require('sharp'); const sharp = require('sharp');
const CryptoJS = require('crypto-js'); 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 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() { function initEnv() {
return del([distDir]); process.env.BROWSERSLIST_ENV = targetEnv;
} }
function jsWebpack(done) { function init(done) {
exec('webpack-cli --display-error-details --bail --colors', function ( initEnv();
err,
stdout, rmSync(distDir, {recursive: true, force: true});
stderr ensureDirSync(distDir);
) { done();
}
function js(done) {
exec('webpack-cli build --color', function (err, stdout, stderr) {
console.log(stdout); console.log(stdout);
console.log(stderr); console.log(stderr);
done(err); done(err);
}); });
} }
function jsBabel() {
return src(['src/content/**/*.js'], {base: '.'})
.pipe(babel())
.pipe(dest(distDir));
}
const js = parallel(jsWebpack, jsBabel);
function html() { 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(gulpif(isProduction, htmlmin({collapseWhitespace: true})))
.pipe(dest(distDir)); .pipe(dest(distDir));
} }
function css() { function css() {
return src(['src/solve/*.css'], { return src('src/solve/*.css', {base: '.'})
base: '.'
})
.pipe(postcss()) .pipe(postcss())
.pipe(dest(distDir)); .pipe(dest(distDir));
} }
async function images(done) { async function images(done) {
ensureDirSync(path.join(distDir, 'src/icons/app')); ensureDirSync(path.join(distDir, 'src/assets/icons/app'));
const appIconSvg = readFileSync('src/icons/app/icon.svg'); const appIconSvg = readFileSync('src/assets/icons/app/icon.svg');
const appIconSizes = [16, 19, 24, 32, 38, 48, 64, 96, 128]; const appIconSizes = [16, 19, 24, 32, 38, 48, 64, 96, 128];
for (const size of appIconSizes) { for (const size of appIconSizes) {
await sharp(appIconSvg, {density: (72 * size) / 24}) await sharp(appIconSvg, {density: (72 * size) / 24})
.resize(size) .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 // Chrome Web Store does not correctly display optimized icons
if (isProduction && targetEnv !== 'chrome') { if (isProduction && targetEnv !== 'chrome') {
await new Promise(resolve => { 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(imagemin())
.pipe(dest('.')) .pipe(dest('.'))
.on('error', done) .on('error', done)
@ -77,18 +82,20 @@ async function images(done) {
}); });
} }
if (enableContributions) {
await new Promise(resolve => { await new Promise(resolve => {
src('node_modules/ext-contribute/src/assets/*.@(jpg|png|svg)') src('node_modules/vueton/components/contribute/assets/*.@(png|svg)')
.pipe(gulpif(isProduction, imagemin())) .pipe(gulpif(isProduction, imagemin()))
.pipe(dest(path.join(distDir, 'src/contribute/assets'))) .pipe(dest(path.join(distDir, 'src/contribute/assets')))
.on('error', done) .on('error', done)
.on('finish', resolve); .on('finish', resolve);
}); });
} }
}
async function fonts(done) { async function fonts(done) {
await new Promise(resolve => { await new Promise(resolve => {
src('src/fonts/roboto.css', {base: '.'}) src('src/assets/fonts/roboto.css', {base: '.'})
.pipe(postcss()) .pipe(postcss())
.pipe(dest(distDir)) .pipe(dest(distDir))
.on('error', done) .on('error', done)
@ -97,16 +104,16 @@ async function fonts(done) {
await new Promise(resolve => { await new Promise(resolve => {
src( 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('error', done)
.on('finish', resolve); .on('finish', resolve);
}); });
} }
async function locale(done) { 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) { const localeDirs = readdirSync(localesRootDir).filter(function (file) {
return lstatSync(path.join(localesRootDir, file)).isDirectory(); return lstatSync(path.join(localesRootDir, file)).isDirectory();
}); });
@ -144,29 +151,11 @@ async function locale(done) {
} }
function manifest() { function manifest() {
return src('src/manifest.json') return src(`src/assets/manifest/${targetEnv}.json`)
.pipe( .pipe(
jsonMerge({ jsonMerge({
fileName: 'manifest.json', fileName: 'manifest.json',
edit: (parsedJson, file) => { 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; parsedJson.version = require('./package.json').version;
return parsedJson; return parsedJson;
} }
@ -191,7 +180,7 @@ See the LICENSE file for further information.
`; `;
writeFileSync(path.join(distDir, 'NOTICE'), notice); writeFileSync(path.join(distDir, 'NOTICE'), notice);
return src(['LICENSE']).pipe(dest(distDir)); return src('LICENSE').pipe(dest(distDir));
} }
function secrets(done) { function secrets(done) {
@ -223,7 +212,7 @@ function secrets(done) {
function zip(done) { function zip(done) {
exec( 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) { function (err, stdout, stderr) {
console.log(stdout); console.log(stdout);
console.log(stderr); console.log(stderr);
@ -233,8 +222,13 @@ function zip(done) {
} }
function inspect(done) { function inspect(done) {
initEnv();
exec( 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) { function (err, stdout, stderr) {
console.log(stdout); console.log(stdout);
console.log(stderr); console.log(stderr);
@ -244,7 +238,7 @@ function inspect(done) {
} }
exports.build = series( exports.build = series(
clean, init,
parallel(js, html, css, images, fonts, locale, manifest, license), parallel(js, html, css, images, fonts, locale, manifest, license),
secrets secrets
); );

43745
package-lock.json generated

File diff suppressed because it is too large Load Diff

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

@ -1,9 +1,12 @@
module.exports = function(ctx) { const postcssPresetEnv = require('postcss-preset-env');
return { const cssnano = require('cssnano');
plugins: {
autoprefixer: {}, module.exports = function (api) {
cssnano: const plugins = [postcssPresetEnv()];
ctx.env === 'production' ? {zindex: false, discardUnused: false} : false
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": { "optionTitle_speechService": {
"message": "Speech service", "message": "Speech recognition",
"description": "Title of the option." "description": "Title of the option."
}, },
@ -239,6 +239,31 @@
"description": "Title of the option." "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": { "optionTitle_witSpeechApiLang": {
"message": "API language", "message": "API language",
"description": "Title of the option." "description": "Title of the option."
@ -420,36 +445,41 @@
"description": "Label of the input." "description": "Label of the input."
}, },
"buttonText_addApi": { "buttonLabel_addApi": {
"message": "Add API", "message": "Add API",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonText_solve": { "buttonLabel_solve": {
"message": "Solve the challenge", "message": "Solve the challenge",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonText_reset": { "buttonLabel_reset": {
"message": "Reset the challenge", "message": "Reset the challenge",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonText_downloadApp": { "buttonLabel_downloadApp": {
"message": "Download app", "message": "Download app",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonText_installApp": { "buttonLabel_installApp": {
"message": "Install app", "message": "Install app",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonText_goBack": { "buttonLabel_goBack": {
"message": "Go back", "message": "Go back",
"description": "Text of the button." "description": "Text of the button."
}, },
"buttonLabel_contribute": {
"message": "Contribute",
"description": "Label of the button."
},
"linkText_installGuide": { "linkText_installGuide": {
"message": "Installation guide", "message": "Installation guide",
"description": "Text of the link." "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": { "browser_specific_settings": {
"gecko": { "gecko": {
"id": "{e58d3966-3d76-4cd9-8552-1582fbc800c1}", "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": [ "permissions": [
"storage", "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:*;", "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": { "icons": {
"16": "src/icons/app/icon-16.png", "16": "src/assets/icons/app/icon-16.png",
"19": "src/icons/app/icon-19.png", "19": "src/assets/icons/app/icon-19.png",
"24": "src/icons/app/icon-24.png", "24": "src/assets/icons/app/icon-24.png",
"32": "src/icons/app/icon-32.png", "32": "src/assets/icons/app/icon-32.png",
"38": "src/icons/app/icon-38.png", "38": "src/assets/icons/app/icon-38.png",
"48": "src/icons/app/icon-48.png", "48": "src/assets/icons/app/icon-48.png",
"64": "src/icons/app/icon-64.png", "64": "src/assets/icons/app/icon-64.png",
"96": "src/icons/app/icon-96.png", "96": "src/assets/icons/app/icon-96.png",
"128": "src/icons/app/icon-128.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": [ "content_scripts": [
{ {
"matches": [ "matches": [
"https://google.com/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/api2/bframe*", "https://www.google.com/recaptcha/api2/bframe*",
"https://www.recaptcha.net/recaptcha/api2/bframe*", "https://google.com/recaptcha/enterprise/bframe*",
"https://recaptcha.net/recaptcha/api2/bframe*",
"https://www.google.com/recaptcha/enterprise/bframe*", "https://www.google.com/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*", "https://recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*" "https://www.recaptcha.net/recaptcha/api2/bframe*",
"https://recaptcha.net/recaptcha/enterprise/bframe*",
"https://www.recaptcha.net/recaptcha/enterprise/bframe*"
], ],
"all_frames": true, "all_frames": true,
"run_at": "document_idle", "run_at": "document_idle",
"css": ["src/solve/reset-button.css"], "css": ["src/solve/style.css"],
"js": ["src/manifest.js", "src/solve/script.js"] "js": ["src/solve/script.js"]
}, },
{ {
"matches": ["http://127.0.0.1/buster/setup?session=*"], "matches": ["http://127.0.0.1/buster/setup?session=*"],
"run_at": "document_idle", "run_at": "document_idle",
"js": ["src/content/setup.js"] "js": ["src/scripts/init-setup.js"]
} }
], ],
"options_ui": { "options_ui": {
"page": "src/options/index.html", "page": "src/options/index.html",
"browser_style": false, "browser_style": false,
"chrome_style": false,
"open_in_tab": true "open_in_tab": true
}, },
@ -75,9 +88,7 @@
"web_accessible_resources": [ "web_accessible_resources": [
"src/setup/index.html", "src/setup/index.html",
"src/content/reset.js", "src/scripts/reset.js",
"src/solve/solver-button.css" "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> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="referrer" content="no-referrer" />
</head> </head>
<body> <body>
<script src="/src/manifest.js"></script>
<script src="script.js"></script> <script src="script.js"></script>
</body> </body>
</html> </html>

@ -1,21 +1,20 @@
import browser from 'webextension-polyfill';
import audioBufferToWav from 'audiobuffer-to-wav'; import audioBufferToWav from 'audiobuffer-to-wav';
import aes from 'crypto-js/aes'; import aes from 'crypto-js/aes';
import sha256 from 'crypto-js/sha256'; import sha256 from 'crypto-js/sha256';
import utf8 from 'crypto-js/enc-utf8'; 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 storage from 'storage/storage';
import { import {
showNotification, showNotification,
showContributePage, sendNativeMessage,
sendNativeMessage processMessageResponse,
processAppUse
} from 'utils/app'; } from 'utils/app';
import { import {
executeCode, executeCode,
executeFile,
scriptsAllowed, scriptsAllowed,
functionInContext,
getBrowser, getBrowser,
getPlatform, getPlatform,
getRandomInt, getRandomInt,
@ -89,23 +88,46 @@ async function getFramePos(tabId, frameId, frameIndex) {
return {x, y}; 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) { async function resetCaptcha(tabId, frameId, challengeUrl) {
frameId = ( frameId = (await browser.webNavigation.getFrame({tabId, frameId}))
await browser.webNavigation.getFrame({ .parentFrameId;
tabId,
frameId: frameId
})
).parentFrameId;
if (!(await scriptsAllowed(tabId, frameId))) { if (!(await scriptsAllowed(tabId, frameId))) {
await showNotification({messageId: 'error_scriptsNotAllowed'}); await showNotification({messageId: 'error_scriptsNotAllowed'});
return; return;
} }
if (!(await functionInContext('addListener', tabId, frameId))) { await executeCode(`(${initResetCaptcha.toString()})()`, tabId, frameId);
await executeFile('/src/content/initReset.js', tabId, frameId);
}
await executeCode('addListener()', tabId, frameId);
await browser.tabs.sendMessage( await browser.tabs.sendMessage(
tabId, tabId,
@ -126,10 +148,10 @@ function challengeRequestCallback(details) {
} }
async function setChallengeLocale() { async function setChallengeLocale() {
const {loadEnglishChallenge, simulateUserInput} = await storage.get( const {loadEnglishChallenge, simulateUserInput} = await storage.get([
['loadEnglishChallenge', 'simulateUserInput'], 'loadEnglishChallenge',
'sync' 'simulateUserInput'
); ]);
if (loadEnglishChallenge || simulateUserInput) { if (loadEnglishChallenge || simulateUserInput) {
if ( if (
@ -139,18 +161,22 @@ async function setChallengeLocale() {
challengeRequestCallback, challengeRequestCallback,
{ {
urls: [ 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/anchor*',
'https://www.google.com/recaptcha/api2/bframe*', 'https://www.google.com/recaptcha/api2/bframe*',
'https://www.recaptcha.net/recaptcha/api2/anchor*', 'https://google.com/recaptcha/enterprise/anchor*',
'https://www.recaptcha.net/recaptcha/api2/bframe*', 'https://google.com/recaptcha/enterprise/bframe*',
'https://recaptcha.net/recaptcha/api2/anchor*',
'https://recaptcha.net/recaptcha/api2/bframe*',
'https://www.google.com/recaptcha/enterprise/anchor*', 'https://www.google.com/recaptcha/enterprise/anchor*',
'https://www.google.com/recaptcha/enterprise/bframe*', 'https://www.google.com/recaptcha/enterprise/bframe*',
'https://www.recaptcha.net/recaptcha/enterprise/anchor*', 'https://recaptcha.net/recaptcha/api2/anchor*',
'https://www.recaptcha.net/recaptcha/enterprise/bframe*', '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/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'] types: ['sub_frame']
}, },
@ -182,9 +208,10 @@ function addBackgroundRequestListener() {
!browser.webRequest.onBeforeSendHeaders.hasListener(removeRequestOrigin) !browser.webRequest.onBeforeSendHeaders.hasListener(removeRequestOrigin)
) { ) {
const urls = [ const urls = [
'https://google.com/*',
'https://www.google.com/*', 'https://www.google.com/*',
'https://www.recaptcha.net/*',
'https://recaptcha.net/*', 'https://recaptcha.net/*',
'https://www.recaptcha.net/*',
'https://api.wit.ai/*', 'https://api.wit.ai/*',
'https://speech.googleapis.com/*', 'https://speech.googleapis.com/*',
'https://*.speech-to-text.watson.cloud.ibm.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)); secrets = JSON.parse(aes.decrypt(ciphertext, key).toString(utf8));
} catch (err) { } catch (err) {
secrets = {}; secrets = {};
const {speechService} = await storage.get('speechService', 'sync'); const {speechService} = await storage.get('speechService');
if (speechService === 'witSpeechApiDemo') { 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; return apiKey;
} }
} else { } else {
const {witSpeechApiKeys: apiKeys} = await storage.get( const {witSpeechApiKeys: apiKeys} = await storage.get('witSpeechApiKeys');
'witSpeechApiKeys',
'sync'
);
return apiKeys[language]; return apiKeys[language];
} }
} }
@ -362,10 +386,10 @@ async function transcribeAudio(audioUrl, lang) {
const audioRsp = await fetch(audioUrl, {referrer: ''}); const audioRsp = await fetch(audioUrl, {referrer: ''});
const audioContent = await prepareAudio(await audioRsp.arrayBuffer()); const audioContent = await prepareAudio(await audioRsp.arrayBuffer());
const {speechService, tryEnglishSpeechModel} = await storage.get( const {speechService, tryEnglishSpeechModel} = await storage.get([
['speechService', 'tryEnglishSpeechModel'], 'speechService',
'sync' 'tryEnglishSpeechModel'
); ]);
if (['witSpeechApiDemo', 'witSpeechApi'].includes(speechService)) { if (['witSpeechApiDemo', 'witSpeechApi'].includes(speechService)) {
const language = captchaWitSpeechApiLangCodes[lang] || 'english'; const language = captchaWitSpeechApiLangCodes[lang] || 'english';
@ -404,8 +428,7 @@ async function transcribeAudio(audioUrl, lang) {
} }
} else if (speechService === 'googleSpeechApi') { } else if (speechService === 'googleSpeechApi') {
const {googleSpeechApiKey: apiKey} = await storage.get( const {googleSpeechApiKey: apiKey} = await storage.get(
'googleSpeechApiKey', 'googleSpeechApiKey'
'sync'
); );
if (!apiKey) { if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'}); showNotification({messageId: 'error_missingApiKey'});
@ -447,10 +470,8 @@ async function transcribeAudio(audioUrl, lang) {
solution = results[0].alternatives[0].transcript.trim(); solution = results[0].alternatives[0].transcript.trim();
} }
} else if (speechService === 'ibmSpeechApi') { } else if (speechService === 'ibmSpeechApi') {
const { const {ibmSpeechApiLoc: apiLoc, ibmSpeechApiKey: apiKey} =
ibmSpeechApiLoc: apiLoc, await storage.get(['ibmSpeechApiLoc', 'ibmSpeechApiKey']);
ibmSpeechApiKey: apiKey
} = await storage.get(['ibmSpeechApiLoc', 'ibmSpeechApiKey'], 'sync');
if (!apiKey) { if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'}); showNotification({messageId: 'error_missingApiKey'});
return; return;
@ -478,13 +499,8 @@ async function transcribeAudio(audioUrl, lang) {
); );
} }
} else if (speechService === 'microsoftSpeechApi') { } else if (speechService === 'microsoftSpeechApi') {
const { const {microsoftSpeechApiLoc: apiLoc, microsoftSpeechApiKey: apiKey} =
microsoftSpeechApiLoc: apiLoc, await storage.get(['microsoftSpeechApiLoc', 'microsoftSpeechApiKey']);
microsoftSpeechApiKey: apiKey
} = await storage.get(
['microsoftSpeechApiLoc', 'microsoftSpeechApiKey'],
'sync'
);
if (!apiKey) { if (!apiKey) {
showNotification({messageId: 'error_missingApiKey'}); showNotification({messageId: 'error_missingApiKey'});
return; return;
@ -519,7 +535,7 @@ async function transcribeAudio(audioUrl, lang) {
} }
} }
async function onMessage(request, sender) { async function processMessage(request, sender) {
if (request.id === 'notification') { if (request.id === 'notification') {
showNotification({ showNotification({
message: request.message, message: request.message,
@ -529,12 +545,7 @@ async function onMessage(request, sender) {
timeout: request.timeout timeout: request.timeout
}); });
} else if (request.id === 'captchaSolved') { } else if (request.id === 'captchaSolved') {
let {useCount} = await storage.get('useCount', 'sync'); await processAppUse();
useCount += 1;
await storage.set({useCount}, 'sync');
if ([30, 100].includes(useCount)) {
await showContributePage('use');
}
} else if (request.id === 'transcribeAudio') { } else if (request.id === 'transcribeAudio') {
addBackgroundRequestListener(); addBackgroundRequestListener();
try { try {
@ -606,22 +617,26 @@ async function onMessage(request, sender) {
} else if (request.id === 'openOptions') { } else if (request.id === 'openOptions') {
browser.runtime.openOptionsPage(); browser.runtime.openOptionsPage();
} else if (request.id === 'getPlatform') { } else if (request.id === 'getPlatform') {
return getPlatform(); return getPlatform({fallback: false});
} else if (request.id === 'getBrowser') { } else if (request.id === 'getBrowser') {
return getBrowser(); return getBrowser();
} else if (request.id === 'optionChange') {
await onOptionChange();
} }
} }
async function onStorageChange(changes, area) { function onMessage(request, sender, sendResponse) {
await setChallengeLocale(); const response = processMessage(request, sender);
return processMessageResponse(response, sendResponse);
} }
function addStorageListener() { async function onOptionChange() {
browser.storage.onChanged.addListener(onStorageChange); await setChallengeLocale();
} }
function addMessageListener() { async function onActionButtonClick(tab) {
browser.runtime.onMessage.addListener(onMessage); await browser.runtime.openOptionsPage();
} }
async function onInstall(details) { async function onInstall(details) {
@ -645,14 +660,9 @@ async function onInstall(details) {
await browser.tabs.insertCSS(tabId, { await browser.tabs.insertCSS(tabId, {
frameId, frameId,
runAt: 'document_idle', 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, { await browser.tabs.executeScript(tabId, {
frameId, frameId,
runAt: 'document_idle', runAt: 'document_idle',
@ -673,13 +683,33 @@ async function onInstall(details) {
} }
} }
async function onLoad() { function addBrowserActionListener() {
await initStorage('sync'); browser.browserAction.onClicked.addListener(onActionButtonClick);
await setChallengeLocale(); }
addStorageListener();
addMessageListener(); function addMessageListener() {
browser.runtime.onMessage.addListener(onMessage);
} }
function addInstallListener() {
browser.runtime.onInstalled.addListener(onInstall); browser.runtime.onInstalled.addListener(onInstall);
}
async function setup() {
if (!(await isStorageReady())) {
await migrateLegacyStorage();
await initStorage();
}
await setChallengeLocale();
}
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> <template>
<div id="app"> <v-app id="app">
<v-contribute :extName="extName" :extSlug="extSlug" :notice="notice"> <vn-contribute
</v-contribute> :extName="extName"
</div> :extSlug="extSlug"
:notice="notice"
@open="contribute"
>
</vn-contribute>
</v-app>
</template> </template>
<script> <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 { export default {
components: { 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 () { created: function () {
document.title = getText('pageTitle', [ document.title = getText('pageTitle', [
getText('pageTitle_contribute'), getText('pageTitle_contribute'),
@ -30,24 +43,20 @@ export default {
]); ]);
const query = new URL(window.location.href).searchParams; const query = new URL(window.location.href).searchParams;
if (query.get('action') === 'use') { if (query.get('action') === 'auto') {
this.notice = `This page is shown during your 30th and 100th use this.notice = 'This page is shown once a year while using the extension.';
of the extension.`;
} }
} }
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
@import '@material/typography/mixins'; @use 'vueton/styles' as vueton;
@include vueton.theme-base;
body { .v-application__wrap {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
margin: 0;
@include mdc-typography-base;
font-size: 100%;
background-color: #ffffff;
} }
</style> </style>

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

@ -3,16 +3,19 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/icons/app/icon-32.png" type="image/png" /> <meta name="referrer" content="no-referrer" />
<link href="/src/fonts/roboto.css" rel="stylesheet" /> <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="/src/commons-ui/style.css" rel="stylesheet" />
<link href="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="/src/commons-ui/script.js"></script>
<script src="script.js"></script> <script src="script.js" defer></script>
</body> </head>
<body></body>
</html> </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'; import App from './App';
async function init() { async function init() {
try { await loadFonts(['400 14px Roboto', '500 14px Roboto']);
await document.fonts.load('400 14px Roboto');
await document.fonts.load('500 14px Roboto');
} catch (err) {}
new Vue({ const app = createApp(App);
el: '#app',
render: h => h(App) await configApp(app);
}); await configVuetify(app);
app.mount('body');
} }
init(); init();

@ -1,5 +1,5 @@
function setup() { 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( url.searchParams.set(
'session', 'session',
new URL(window.location.href).searchParams.get('session') new URL(window.location.href).searchParams.get('session')

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

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

@ -3,16 +3,19 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/src/icons/app/icon-32.png" type="image/png" /> <meta name="referrer" content="no-referrer" />
<link href="/src/fonts/roboto.css" rel="stylesheet" /> <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="/src/commons-ui/style.css" rel="stylesheet" />
<link href="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="/src/commons-ui/script.js"></script>
<script src="script.js"></script> <script src="script.js" defer></script>
</body> </head>
<body></body>
</html> </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'; import App from './App';
async function init() { async function init() {
try { await loadFonts(['400 14px Roboto', '500 14px Roboto']);
await document.fonts.load('400 14px Roboto');
await document.fonts.load('500 14px Roboto');
} catch (err) {}
new Vue({ const app = createApp(App);
el: '#app',
render: h => h(App) await configApp(app);
}); await configVuetify(app);
app.mount('body');
} }
// only run in a frame // only run in a frame

@ -1,10 +1,8 @@
import browser from 'webextension-polyfill';
import storage from 'storage/storage'; import storage from 'storage/storage';
import {meanSleep, pingClientApp} from 'utils/app'; import {meanSleep, pingClientApp} from 'utils/app';
import { import {
getText, getText,
waitForElement, findNode,
getRandomFloat, getRandomFloat,
sleep, sleep,
getBrowser getBrowser
@ -41,7 +39,7 @@ function syncUI() {
const button = document.createElement('button'); const button = document.createElement('button');
button.classList.add('rc-button'); button.classList.add('rc-button');
button.setAttribute('tabindex', '0'); button.setAttribute('tabindex', '0');
button.setAttribute('title', getText('buttonText_reset')); button.setAttribute('title', getText('buttonLabel_reset'));
button.id = 'reset-button'; button.id = 'reset-button';
button.addEventListener('click', resetCaptcha); button.addEventListener('click', resetCaptcha);
@ -76,7 +74,7 @@ function syncUI() {
solverButton = document.createElement('button'); solverButton = document.createElement('button');
solverButton.setAttribute('tabindex', '0'); solverButton.setAttribute('tabindex', '0');
solverButton.setAttribute('title', getText('buttonText_solve')); solverButton.setAttribute('title', getText('buttonLabel_solve'));
solverButton.id = 'solver-button'; solverButton.id = 'solver-button';
if (solverWorking) { if (solverWorking) {
solverButton.classList.add('working'); solverButton.classList.add('working');
@ -92,7 +90,7 @@ function isBlocked({timeout = 0} = {}) {
const selector = '.rc-doscaptcha-body'; const selector = '.rc-doscaptcha-body';
if (timeout) { if (timeout) {
return new Promise(resolve => { return new Promise(resolve => {
waitForElement(selector, {timeout}).then(result => findNode(selector, {timeout, throwError: false}).then(result =>
resolve(Boolean(result)) resolve(Boolean(result))
); );
}); });
@ -258,10 +256,7 @@ async function solve(simulateUserInput, clickEvent) {
return; return;
} }
const {navigateWithKeyboard} = await storage.get( const {navigateWithKeyboard} = await storage.get('navigateWithKeyboard');
'navigateWithKeyboard',
'sync'
);
let browserBorder; let browserBorder;
let osScale; let osScale;
@ -292,9 +287,11 @@ async function solve(simulateUserInput, clickEvent) {
const result = await Promise.race([ const result = await Promise.race([
new Promise(resolve => { new Promise(resolve => {
waitForElement(audioElSelector, {timeout: 10000}).then(el => { findNode(audioElSelector, {timeout: 10000, throwError: false}).then(
el => {
meanSleep(500).then(() => resolve({audioEl: el})); meanSleep(500).then(() => resolve({audioEl: el}));
}); }
);
}), }),
new Promise(resolve => { new Promise(resolve => {
isBlocked({timeout: 10000}).then(blocked => resolve({blocked})); isBlocked({timeout: 10000}).then(blocked => resolve({blocked}));
@ -415,10 +412,10 @@ function solveChallenge(ev) {
} }
async function runSolver(ev) { async function runSolver(ev) {
const {simulateUserInput, autoUpdateClientApp} = await storage.get( const {simulateUserInput, autoUpdateClientApp} = await storage.get([
['simulateUserInput', 'autoUpdateClientApp'], 'simulateUserInput',
'sync' 'autoUpdateClientApp'
); ]);
if (simulateUserInput) { if (simulateUserInput) {
try { try {
@ -497,7 +494,7 @@ async function runSolver(ev) {
function init() { function init() {
const observer = new MutationObserver(syncUI); const observer = new MutationObserver(syncUI);
observer.observe(document.body, { observer.observe(document, {
childList: true, childList: true,
subtree: 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}); 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 message = 'Initial version';
const revision = 'UoT3kGyBH'; const revision = 'UoT3kGyBH';
const downRevision = null;
const storage = browser.storage.sync;
async function upgrade() { async function upgrade() {
const changes = { const changes = {
@ -16,11 +11,7 @@ async function upgrade() {
}; };
changes.storageVersion = revision; changes.storageVersion = revision;
return storage.set(changes); return browser.storage.local.set(changes);
}
async function downgrade() {
return storage.clear();
} }
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'; import {storageRevisions} from 'utils/config';
let syncArea; async function isStorageArea({area = 'local'} = {}) {
async function getSupportedArea(requestedArea) {
if (typeof syncArea === 'undefined') {
try { try {
await browser.storage.sync.get(''); await browser.storage[area].get('');
syncArea = true; return true;
} catch (e) { } catch (err) {
syncArea = false; return false;
} }
} }
return syncArea ? requestedArea : 'local'; 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 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') { async function get(keys = null, {area = 'local'} = {}) {
area = await getSupportedArea(area); await ensureStorageReady({area});
return browser.storage[area].get(keys); return browser.storage[area].get(keys);
} }
async function set(obj, area = 'local') { async function set(obj, {area = 'local'} = {}) {
area = await getSupportedArea(area); await ensureStorageReady({area});
return browser.storage[area].set(obj); return browser.storage[area].set(obj);
} }
async function remove(keys, area = 'local') { async function remove(keys, {area = 'local'} = {}) {
area = await getSupportedArea(area); await ensureStorageReady({area});
return browser.storage[area].remove(keys); return browser.storage[area].remove(keys);
} }
async function clear(area = 'local') { async function clear({area = 'local'} = {}) {
area = await getSupportedArea(area); await ensureStorageReady({area});
return browser.storage[area].clear(); return browser.storage[area].clear();
} }
export default {get, set, remove, 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 {v4 as uuidv4} from 'uuid';
import storage from 'storage/storage';
import { import {
getText, getText,
createTab,
getActiveTab, getActiveTab,
getPlatform,
getDayPrecisionEpoch,
getRandomInt, getRandomInt,
sleep sleep
} from 'utils/common'; } from 'utils/common';
import {targetEnv, enableContributions} from 'utils/config';
async function showNotification({ async function showNotification({
message, message,
@ -28,7 +30,7 @@ async function showNotification({
type: 'basic', type: 'basic',
title, title,
message, 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; return notification;
} }
function getOptionLabels(data, scope = 'optionValue') { function getListItems(data, {scope = ''} = {}) {
const labels = {}; const labels = {};
for (const [group, items] of Object.entries(data)) { for (const [group, items] of Object.entries(data)) {
labels[group] = []; labels[group] = [];
items.forEach(function (value) { items.forEach(function (value) {
labels[group].push({ const item = {
id: value, value,
label: getText(`${scope}_${group}_${value}`) title: getText(`${scope ? scope + '_' : ''}${value}`)
}); };
labels[group].push(item);
}); });
} }
return labels; return labels;
} }
async function showContributePage(action = false) { async function configApp(app) {
const activeTab = await getActiveTab(); 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'); let url = browser.runtime.getURL('/src/contribute/index.html');
if (action) { if (action) {
url = `${url}?action=${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) { function meanSleep(ms) {
@ -129,8 +267,14 @@ async function pingClientApp({
export { export {
showNotification, showNotification,
getOptionLabels, getListItems,
configApp,
loadFonts,
processMessageResponse,
showContributePage, showContributePage,
autoShowContributePage,
updateUseCount,
processAppUse,
meanSleep, meanSleep,
sendNativeMessage, sendNativeMessage,
pingClientApp pingClientApp

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

@ -1,5 +1,9 @@
const targetEnv = process.env.TARGET_ENV; 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'; const clientAppVersion = '0.3.0';
export {targetEnv, clientAppVersion}; export {targetEnv, enableContributions, storageRevisions, clientAppVersion};

@ -10,7 +10,9 @@ const optionKeys = [
'tryEnglishSpeechModel', 'tryEnglishSpeechModel',
'simulateUserInput', 'simulateUserInput',
'autoUpdateClientApp', 'autoUpdateClientApp',
'navigateWithKeyboard' 'navigateWithKeyboard',
'appTheme',
'showContribPage'
]; ];
const clientAppPlatforms = [ const clientAppPlatforms = [
@ -20,7 +22,8 @@ const clientAppPlatforms = [
'macos/amd64' '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://developers.google.com/recaptcha/docs/language
// https://cloud.google.com/speech-to-text/docs/languages // https://cloud.google.com/speech-to-text/docs/languages
@ -245,6 +248,7 @@ const captchaMicrosoftSpeechApiLangCodes = {
zu: '' // Zulu zu: '' // Zulu
}; };
// https://wit.ai/faq
const captchaWitSpeechApiLangCodes = { const captchaWitSpeechApiLangCodes = {
ar: 'arabic', // Arabic ar: 'arabic', // Arabic
af: '', // Afrikaans 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 webpack = require('webpack');
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin'); 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 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 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({ new webpack.DefinePlugin({
'process.env': { '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 VueLoaderPlugin(),
new VuetifyPlugin(),
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: '[name]/style.css' filename: '[name]/style.css',
ignoreOrder: true
}), }),
isProduction ? new LodashModuleReplacementPlugin({shorthands: true}) : null isProduction ? new LodashModuleReplacementPlugin({shorthands: true}) : null
]; ].filter(Boolean);
plugins = plugins.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 = { module.exports = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
@ -30,16 +61,19 @@ module.exports = {
options: './src/options/main.js', options: './src/options/main.js',
contribute: './src/contribute/main.js', contribute: './src/contribute/main.js',
solve: './src/solve/main.js', solve: './src/solve/main.js',
setup: './src/setup/main.js' setup: './src/setup/main.js',
...entries
}, },
output: { output: {
path: path.resolve(__dirname, 'dist', targetEnv, 'src'), 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' chunkFilename: '[name]/script.js'
}, },
optimization: { optimization: {
runtimeChunk: {
name: 'manifest'
},
splitChunks: { splitChunks: {
cacheGroups: { cacheGroups: {
default: false, default: false,
@ -57,7 +91,10 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.js$/, test: /\.js$/,
use: 'babel-loader' use: 'babel-loader',
resolve: {
fullySpecified: false
}
}, },
{ {
test: /\.vue$/, test: /\.vue$/,
@ -65,7 +102,8 @@ module.exports = {
{ {
loader: 'vue-loader', loader: 'vue-loader',
options: { options: {
transformAssetUrls: {img: ''} transformAssetUrls: {img: ''},
compilerOptions: {whitespace: 'preserve'}
} }
} }
] ]
@ -80,7 +118,8 @@ module.exports = {
loader: 'sass-loader', loader: 'sass-loader',
options: { options: {
sassOptions: { sassOptions: {
includePaths: ['node_modules'] includePaths: ['node_modules'],
quietDeps: true
} }
} }
} }
@ -90,8 +129,9 @@ module.exports = {
}, },
resolve: { resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'], 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 plugins
}; };

12640
yarn.lock

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