feat(browser-extension): use plasmo framework
4
.gitignore
vendored
@ -1,9 +1,7 @@
|
|||||||
_metadata/
|
_metadata/
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
|
.plasmo/
|
||||||
.pnp.*
|
.pnp.*
|
||||||
.yarn/*
|
|
||||||
build/
|
build/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn commitlint --edit "$1"
|
pnpm commitlint --edit "$1"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
yarn lint-staged
|
pnpm lint-staged
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"packages/**/*.{js,ts}": ["prettier --loglevel=silent --write", "bash -c 'yarn lint'"]
|
"packages/**/*.{js,ts}": ["prettier --loglevel=silent --write", "bash -c 'pnpm lint'"]
|
||||||
}
|
}
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
package.json
|
|
||||||
.yarnrc.yml
|
|
@ -1,4 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.jsonc", ".eslintrc", "tsconfig*.json"],
|
||||||
|
"options": {
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
|
3
.vscode/settings.json
vendored
@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "always"
|
||||||
|
},
|
||||||
"files.associations": {
|
"files.associations": {
|
||||||
".commitlintrc": "json",
|
".commitlintrc": "json",
|
||||||
".lintstagedrc": "json"
|
".lintstagedrc": "json"
|
||||||
|
33
.yarn/plugins/@yarnpkg/plugin-outdated.cjs
vendored
894
.yarn/releases/yarn-4.2.1.cjs
vendored
@ -1,9 +0,0 @@
|
|||||||
enableGlobalCache: true
|
|
||||||
|
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
plugins:
|
|
||||||
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
|
|
||||||
spec: "https://mskelton.dev/yarn-outdated/v2"
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.2.1.cjs
|
|
@ -3,8 +3,9 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "yarn workspaces foreach --all -p run build",
|
"build": "pnpm -r run build",
|
||||||
"lint": "yarn workspaces foreach --all -p run lint",
|
"lint": "pnpm -r run lint",
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -14,12 +15,8 @@
|
|||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"prettier": "^3.2.5"
|
"prettier": "^3.2.5"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20.x"
|
"node": "20.x"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.2.1",
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
@ -2,26 +2,26 @@
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Make sure you have [Node.js](https://nodejs.org/) (version 20.x) and [Yarn](https://yarnpkg.com/) installed.
|
Make sure you have [Node.js](https://nodejs.org/) (version 20.x) and [pnpm](https://pnpm.io/) installed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
## Scripts
|
## Scripts
|
||||||
|
|
||||||
### `yarn build`
|
### `pnpm build`
|
||||||
|
|
||||||
Removes the build directory and compiles TypeScript files.
|
Removes the build directory and compiles TypeScript files.
|
||||||
|
|
||||||
### `yarn dev`
|
### `pnpm dev`
|
||||||
|
|
||||||
Starts the server in development mode with nodemon.
|
Starts the server in development mode with nodemon.
|
||||||
|
|
||||||
### `yarn lint`
|
### `pnpm lint`
|
||||||
|
|
||||||
Lints the codebase using ESLint.
|
Lints the codebase using ESLint.
|
||||||
|
|
||||||
### `yarn start`
|
### `pnpm start`
|
||||||
|
|
||||||
Starts the API server instance in production mode.
|
Starts the API server instance in production mode.
|
||||||
|
@ -33,6 +33,5 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20.x"
|
"node": "20.x"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@4.2.1"
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
||||||
import { formatMessage } from 'services/format';
|
import { formatMessage, formatDomainFromURL } from 'services/format';
|
||||||
import { createIssue, createIssueComment, getIssue, updateIssue } from 'services/git';
|
import { createIssue, createIssueComment, getIssue, updateIssue } from 'services/git';
|
||||||
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
||||||
import { validatorCompiler } from 'services/validation';
|
import { validatorCompiler } from 'services/validation';
|
||||||
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const { reason, url, userAgent, version } = request.body;
|
const { reason, url, userAgent, version } = request.body;
|
||||||
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
|
const domain = formatDomainFromURL(new URL(url));
|
||||||
const issue = await getIssue({ title: hostname });
|
const issue = await getIssue({ title: domain });
|
||||||
const ua = new UAParser(userAgent ?? '').getResult();
|
const ua = new UAParser(userAgent ?? '').getResult();
|
||||||
|
|
||||||
if (issue) {
|
if (issue) {
|
||||||
@ -61,7 +61,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
await createIssue({
|
await createIssue({
|
||||||
description: formatMessage({ reason, ua, url, version }),
|
description: formatMessage({ reason, ua, url, version }),
|
||||||
labels: ['bug'],
|
labels: ['bug'],
|
||||||
title: hostname,
|
title: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.send({
|
reply.send({
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
||||||
import { formatMessage } from 'services/format';
|
import { formatMessage, formatDomainFromURL } from 'services/format';
|
||||||
import { createIssue, createIssueComment, getIssue, updateIssue } from 'services/git';
|
import { createIssue, createIssueComment, getIssue, updateIssue } from 'services/git';
|
||||||
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
||||||
import { validatorCompiler } from 'services/validation';
|
import { validatorCompiler } from 'services/validation';
|
||||||
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const { reason, url, userAgent, version } = request.body;
|
const { reason, url, userAgent, version } = request.body;
|
||||||
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
|
const domain = formatDomainFromURL(new URL(url));
|
||||||
const issue = await getIssue({ title: hostname });
|
const issue = await getIssue({ title: domain });
|
||||||
const ua = new UAParser(userAgent ?? '').getResult();
|
const ua = new UAParser(userAgent ?? '').getResult();
|
||||||
|
|
||||||
if (issue) {
|
if (issue) {
|
||||||
@ -62,7 +62,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
const newIssue = await createIssue({
|
const newIssue = await createIssue({
|
||||||
description: formatMessage({ reason, ua, url, version }),
|
description: formatMessage({ reason, ua, url, version }),
|
||||||
labels: ['bug'],
|
labels: ['bug'],
|
||||||
title: hostname,
|
title: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.send({
|
reply.send({
|
||||||
|
@ -5,14 +5,14 @@ import { validatorCompiler } from 'services/validation';
|
|||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
|
||||||
const GetIssuesParamsSchema = yup.object().shape({
|
const GetIssuesParamsSchema = yup.object().shape({
|
||||||
hostname: yup.string().required(),
|
domain: yup.string().required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GetIssuesParams = yup.InferType<typeof GetIssuesParamsSchema>;
|
type GetIssuesParams = yup.InferType<typeof GetIssuesParamsSchema>;
|
||||||
|
|
||||||
export default (server: FastifyInstance, _options: RouteShorthandOptions, done: () => void) => {
|
export default (server: FastifyInstance, _options: RouteShorthandOptions, done: () => void) => {
|
||||||
server.get<{ Params: GetIssuesParams }>(
|
server.get<{ Params: GetIssuesParams }>(
|
||||||
'/issues/:hostname/',
|
'/issues/:domain/',
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
rateLimit: RATE_LIMIT_10_PER_MIN,
|
rateLimit: RATE_LIMIT_10_PER_MIN,
|
||||||
@ -24,8 +24,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
},
|
},
|
||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const { hostname } = request.params;
|
const { domain } = request.params;
|
||||||
const issue = await getIssue({ title: hostname });
|
const issue = await getIssue({ title: domain });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
issue &&
|
issue &&
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
|
||||||
import { formatMessage } from 'services/format';
|
import { formatMessage, formatDomainFromURL } from 'services/format';
|
||||||
import { createIssue, getIssue, updateIssue } from 'services/git';
|
import { createIssue, getIssue, updateIssue } from 'services/git';
|
||||||
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
|
||||||
import { validatorCompiler } from 'services/validation';
|
import { validatorCompiler } from 'services/validation';
|
||||||
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
async (request, reply) => {
|
async (request, reply) => {
|
||||||
try {
|
try {
|
||||||
const { reason, url, userAgent, version } = request.body;
|
const { reason, url, userAgent, version } = request.body;
|
||||||
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
|
const domain = formatDomainFromURL(new URL(url));
|
||||||
const issue = await getIssue({ title: hostname });
|
const issue = await getIssue({ title: domain });
|
||||||
const ua = new UAParser(userAgent ?? '').getResult();
|
const ua = new UAParser(userAgent ?? '').getResult();
|
||||||
|
|
||||||
if (issue) {
|
if (issue) {
|
||||||
@ -75,7 +75,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
|
|||||||
const newIssue = await createIssue({
|
const newIssue = await createIssue({
|
||||||
description: formatMessage({ reason, ua, url, version }),
|
description: formatMessage({ reason, ua, url, version }),
|
||||||
labels: ['bug'],
|
labels: ['bug'],
|
||||||
title: hostname,
|
title: domain,
|
||||||
});
|
});
|
||||||
|
|
||||||
reply.send({
|
reply.send({
|
||||||
|
@ -20,6 +20,16 @@ export function formatMessage(params: FormatMessageParams): string {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formatDomainFromURL(value: URL): string {
|
||||||
|
let result: string = value.hostname;
|
||||||
|
|
||||||
|
if (result.startsWith('www.')) {
|
||||||
|
result = result.replace('www.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export interface FormatMessageParams {
|
export interface FormatMessageParams {
|
||||||
readonly reason?: string;
|
readonly reason?: string;
|
||||||
readonly ua: UAParserResult;
|
readonly ua: UAParserResult;
|
||||||
|
@ -1,9 +1,41 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
"es6": true
|
"es6": true
|
||||||
},
|
},
|
||||||
"extends": ["plugin:prettier/recommended"],
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:jest-dom/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:react/jsx-runtime",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:react-hooks/recommended",
|
||||||
|
"plugin:testing-library/dom"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest"
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
},
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["import", "simple-import-sort"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"import/first": "error",
|
||||||
|
"import/newline-after-import": "error",
|
||||||
|
"import/no-duplicates": "error",
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"simple-import-sort/imports": "error",
|
||||||
|
"simple-import-sort/exports": "error"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "detect"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BIN
packages/browser-extension/assets/icon.png
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
24
packages/browser-extension/jest.config.mjs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { createRequire } from 'module';
|
||||||
|
import { pathsToModuleNameMapper } from 'ts-jest';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const tsconfig = require('./tsconfig.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('@jest/types').Config.InitialOptions}
|
||||||
|
*/
|
||||||
|
const config = {
|
||||||
|
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||||
|
moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
|
||||||
|
prefix: '<rootDir>/',
|
||||||
|
}),
|
||||||
|
preset: 'ts-jest/presets/default-esm',
|
||||||
|
setupFiles: ['jest-webextension-mock'],
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
testRegex: ['^.+\\.test.tsx?$'],
|
||||||
|
transform: {
|
||||||
|
'^.+.tsx?$': ['ts-jest', { isolatedModules: true }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "مسح القائمة"
|
"message": "مسح القائمة"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "لم يتم العثور على استثناءات"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "قائمة الاستبعاد"
|
"message": "قائمة الاستبعاد"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Liste leeren"
|
"message": "Liste leeren"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Keine Ausschlüsse gefunden"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Ausschlussliste"
|
"message": "Ausschlussliste"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Clear list"
|
"message": "Clear list"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "No exclusions found"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Exclusion list"
|
"message": "Exclusion list"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Borrar lista"
|
"message": "Borrar lista"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "No se encontraron exclusiones"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista de exclusión"
|
"message": "Lista de exclusión"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Vider la liste"
|
"message": "Vider la liste"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Aucune exclusion trouvée"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Liste d'exclusion"
|
"message": "Liste d'exclusion"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "सूची साफ़ करें"
|
"message": "सूची साफ़ करें"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "कोई बहिष्कार नहीं मिला।"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "बहिष्करण सूची"
|
"message": "बहिष्करण सूची"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Bersihkan daftar"
|
"message": "Bersihkan daftar"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Tidak ada pengecualian ditemukan"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Daftar pengecualian"
|
"message": "Daftar pengecualian"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Cancella lista"
|
"message": "Cancella lista"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Nessuna esclusione trovata"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista di esclusione"
|
"message": "Lista di esclusione"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "リストをクリア"
|
"message": "リストをクリア"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "除外項目が見つかりませんでした"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "除外リスト"
|
"message": "除外リスト"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "목록 지우기"
|
"message": "목록 지우기"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "제외 항목을 찾을 수 없습니다"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "제외 목록"
|
"message": "제외 목록"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Wyczyść listę"
|
"message": "Wyczyść listę"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Nie znaleziono wykluczeń"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista wykluczeń"
|
"message": "Lista wykluczeń"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Limpar lista"
|
"message": "Limpar lista"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Nenhuma exclusão encontrada"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista de exclusão"
|
"message": "Lista de exclusão"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Limpar lista"
|
"message": "Limpar lista"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Nenhuma exclusão encontrada"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista de exclusão"
|
"message": "Lista de exclusão"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Golește lista"
|
"message": "Golește lista"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Nu au fost găsite excluderi"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Lista de excludere"
|
"message": "Lista de excludere"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Очистить список"
|
"message": "Очистить список"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Исключения не найдены"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Список исключений"
|
"message": "Список исключений"
|
||||||
},
|
},
|
@ -20,6 +20,9 @@
|
|||||||
"options_clearButton": {
|
"options_clearButton": {
|
||||||
"message": "Listeyi temizle"
|
"message": "Listeyi temizle"
|
||||||
},
|
},
|
||||||
|
"options_empty": {
|
||||||
|
"message": "Hariç tutma bulunamadı"
|
||||||
|
},
|
||||||
"options_exclusionListTitle": {
|
"options_exclusionListTitle": {
|
||||||
"message": "Hariç tutma listesi"
|
"message": "Hariç tutma listesi"
|
||||||
},
|
},
|
@ -2,18 +2,84 @@
|
|||||||
"name": "browser-extension",
|
"name": "browser-extension",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf build; sh scripts/build.sh; sh scripts/pack.sh",
|
"dev": "plasmo dev",
|
||||||
"lint": "eslint --fix"
|
"build": "plasmo build",
|
||||||
|
"lint": "eslint --fix",
|
||||||
|
"package": "plasmo package",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@plasmohq/messaging": "^0.6.2",
|
||||||
|
"@plasmohq/storage": "^1.13.0",
|
||||||
|
"plasmo": "^0.89.4",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/firefox-webext-browser": "^120.0.3",
|
"@jest/globals": "29.7.0",
|
||||||
"eslint": "^8.57.0",
|
"@jest/types": "29.6.3",
|
||||||
|
"@testing-library/react": "^16.0.1",
|
||||||
|
"@types/chrome": "^0.0.283",
|
||||||
|
"@types/node": "^20.11.5",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.14.0",
|
||||||
|
"@typescript-eslint/parser": "^8.14.0",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"rimraf": "^5.0.5"
|
"eslint-plugin-jest-dom": "^5.4.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
|
"eslint-plugin-react": "^7.37.2",
|
||||||
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
|
"eslint-plugin-testing-library": "^6.4.0",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"jest-webextension-mock": "^3.9.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"postcss-modules": "^6.0.1",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"typescript": "^5.6.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20.x"
|
"node": "20.x"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.2.1"
|
"manifest": {
|
||||||
|
"author": "wanhose",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "{77e2c00b-e173-4604-863d-01645d8d2826}",
|
||||||
|
"strict_min_version": "126.0",
|
||||||
|
"update_url": "https://www.cookie-dialog-monster.com/releases/mozilla/updates.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"default_locale": "en",
|
||||||
|
"description": "__MSG_appDesc__",
|
||||||
|
"host_permissions": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
"name": "Cookie Dialog Monster",
|
||||||
|
"permissions": [
|
||||||
|
"contextMenus",
|
||||||
|
"declarativeNetRequest",
|
||||||
|
"storage",
|
||||||
|
"webRequest"
|
||||||
|
],
|
||||||
|
"version": "8.0.5",
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"http://*/*",
|
||||||
|
"https://*/*"
|
||||||
|
],
|
||||||
|
"resources": [
|
||||||
|
"https://fonts.googleapis.com/css?family=Inter"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
input="./src"
|
|
||||||
output="./build"
|
|
||||||
|
|
||||||
mkdir -p "$output"
|
|
||||||
|
|
||||||
for file in $(find "$input" -name "*.css" -o -name "*.html" -o -name "*.js" | sed "s|^$input/||"); do
|
|
||||||
input_file="$input/$file"
|
|
||||||
output_file="$output/$file"
|
|
||||||
|
|
||||||
mkdir -p "${output_file%/*}"
|
|
||||||
cp "$input_file" "$output_file"
|
|
||||||
done
|
|
||||||
|
|
||||||
cp -nR "$input/." "$output"
|
|
@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
path=$(pwd)
|
|
||||||
version=$(jq -r '.version' "$path/build/manifest.json")
|
|
||||||
|
|
||||||
cd "$path/build" || exit
|
|
||||||
zip -r "$path/$version.zip" . -x */\.* *.git* \.* *.md *.sh *.zip
|
|
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 1.8 KiB |
129
packages/browser-extension/src/background/index.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import { sendToContentScript } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DEFAULT_DOMAIN_CONFIG,
|
||||||
|
DEFAULT_EXTENSION_DATA,
|
||||||
|
EXTENSION_MENU_ITEM_ID,
|
||||||
|
REPORT_MENU_ITEM_ID,
|
||||||
|
SETTINGS_MENU_ITEM_ID,
|
||||||
|
} from '~utils/constants';
|
||||||
|
import { formatDomainFromURL, validateSupport } from '~utils/domain';
|
||||||
|
import { noop, suppressLastError } from '~utils/error';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { DomainConfig, ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
import databaseRefreshHandler from './messages/database/refresh';
|
||||||
|
import extensionUpdateAvailableHandler from './messages/extension/updateAvailable';
|
||||||
|
|
||||||
|
chrome.contextMenus.onClicked.addListener((info) => {
|
||||||
|
switch (info.menuItemId) {
|
||||||
|
case REPORT_MENU_ITEM_ID:
|
||||||
|
chrome.action.openPopup();
|
||||||
|
break;
|
||||||
|
case SETTINGS_MENU_ITEM_ID:
|
||||||
|
chrome.runtime.openOptionsPage();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(async () => {
|
||||||
|
chrome.contextMenus.removeAll(() => {
|
||||||
|
const documentUrlPatterns = chrome.runtime.getManifest().content_scripts?.[0].matches;
|
||||||
|
|
||||||
|
chrome.contextMenus.create(
|
||||||
|
{
|
||||||
|
contexts: ['all'],
|
||||||
|
documentUrlPatterns,
|
||||||
|
id: EXTENSION_MENU_ITEM_ID,
|
||||||
|
title: 'Cookie Dialog Monster',
|
||||||
|
},
|
||||||
|
suppressLastError
|
||||||
|
);
|
||||||
|
chrome.contextMenus.create(
|
||||||
|
{
|
||||||
|
contexts: ['all'],
|
||||||
|
documentUrlPatterns,
|
||||||
|
id: SETTINGS_MENU_ITEM_ID,
|
||||||
|
parentId: EXTENSION_MENU_ITEM_ID,
|
||||||
|
title: chrome.i18n.getMessage('contextMenu_settingsOption'),
|
||||||
|
},
|
||||||
|
suppressLastError
|
||||||
|
);
|
||||||
|
chrome.contextMenus.create(
|
||||||
|
{
|
||||||
|
contexts: ['all'],
|
||||||
|
documentUrlPatterns,
|
||||||
|
id: REPORT_MENU_ITEM_ID,
|
||||||
|
parentId: EXTENSION_MENU_ITEM_ID,
|
||||||
|
title: chrome.i18n.getMessage('contextMenu_reportOption'),
|
||||||
|
},
|
||||||
|
suppressLastError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await storage.remove('updateAvailable');
|
||||||
|
await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop });
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(async () => {
|
||||||
|
await storage.remove('updateAvailable');
|
||||||
|
await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop });
|
||||||
|
await extensionUpdateAvailableHandler({ name: 'extension/updateAvailable' }, { send: noop });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Listen to the moment before a request is made to apply the rules
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
chrome.webRequest.onBeforeRequest.addListener(
|
||||||
|
(details) => {
|
||||||
|
const { tabId, type, url } = details;
|
||||||
|
const location = new URL(url);
|
||||||
|
const domain = formatDomainFromURL(location);
|
||||||
|
|
||||||
|
if (tabId > -1 && type === 'main_frame') {
|
||||||
|
storage.get<ExtensionData>('data').then(({ exclusions, rules } = DEFAULT_EXTENSION_DATA) => {
|
||||||
|
if (!validateSupport(location.hostname, exclusions.domains)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.get<DomainConfig>(domain).then((config = DEFAULT_DOMAIN_CONFIG) => {
|
||||||
|
if (rules.length) {
|
||||||
|
const rulesWithTabId = rules.map((rule) => ({
|
||||||
|
...rule,
|
||||||
|
condition: { ...rule.condition, tabIds: [tabId] },
|
||||||
|
}));
|
||||||
|
|
||||||
|
chrome.declarativeNetRequest.updateSessionRules({
|
||||||
|
addRules: config.on ? rulesWithTabId : undefined,
|
||||||
|
removeRuleIds: rules.map((rule) => rule.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Listen for errors on network requests
|
||||||
|
*/
|
||||||
|
chrome.webRequest.onErrorOccurred.addListener(
|
||||||
|
async (details) => {
|
||||||
|
const { error, tabId } = details;
|
||||||
|
|
||||||
|
if (error === 'net::ERR_BLOCKED_BY_CLIENT' && tabId > -1) {
|
||||||
|
await sendToContentScript({
|
||||||
|
body: {
|
||||||
|
value: error,
|
||||||
|
},
|
||||||
|
name: 'INCREASE_LOG_COUNT',
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ urls: ['<all_urls>'] }
|
||||||
|
);
|
@ -0,0 +1,19 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import { DEFAULT_EXTENSION_DATA } from '~utils/constants';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<never, Response> = async (req, res) => {
|
||||||
|
const data = (await storage.get<ExtensionData>('data')) || DEFAULT_EXTENSION_DATA;
|
||||||
|
|
||||||
|
res.send({ data, success: true });
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly data?: ExtensionData;
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
@ -0,0 +1,29 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import { API_URL } from '~utils/constants';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<never, Response> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/data/`);
|
||||||
|
const { data } = await response.json();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
await storage.set('data', data);
|
||||||
|
res.send({ data, success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success: false });
|
||||||
|
} catch {
|
||||||
|
res.send({ success: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly data?: ExtensionData;
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
@ -0,0 +1,63 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import { API_URL, DEFAULT_DOMAIN_CONFIG } from '~utils/constants';
|
||||||
|
import { noop } from '~utils/error';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { DomainConfig } from '~utils/types';
|
||||||
|
|
||||||
|
import updateIconHandler from '../extension/updateIcon';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, Response> = async (req, res) => {
|
||||||
|
const { domain } = req.body ?? {};
|
||||||
|
const { tab } = req.sender || {};
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
let data = (await storage.get<DomainConfig>(domain)) || DEFAULT_DOMAIN_CONFIG;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
if ((data.issue?.expiresAt && now > data.issue.expiresAt) || !data.issue?.expiresAt) {
|
||||||
|
const response = await fetch(`${API_URL}/issues/${domain}/`);
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
res.send({ data, success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const issue = await response.json();
|
||||||
|
|
||||||
|
if (issue.success) {
|
||||||
|
data = { ...data, issue: { ...issue.data, expiresAt: now + 8 * 60 * 60 * 1000 } };
|
||||||
|
await storage.set(domain, data);
|
||||||
|
|
||||||
|
if (tab?.id !== undefined) {
|
||||||
|
await updateIconHandler(
|
||||||
|
{ body: { domain }, name: 'extension/updateIcon', sender: { tab } },
|
||||||
|
{ send: noop }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ data, success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = { ...data, issue: { expiresAt: now + 24 * 60 * 60 * 1000 } };
|
||||||
|
await storage.set(domain, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ data, success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
readonly domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly data?: DomainConfig;
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
@ -0,0 +1,35 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
import { API_URL } from '~utils/constants';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<never, Response> = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/version/`);
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
res.send({ success: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await response.json();
|
||||||
|
const { version } = chrome.runtime.getManifest();
|
||||||
|
|
||||||
|
if (data !== version) {
|
||||||
|
await storage.set('updateAvailable', data);
|
||||||
|
res.send({ success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.remove('updateAvailable');
|
||||||
|
res.send({ success: false });
|
||||||
|
} catch {
|
||||||
|
res.send({ success: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
@ -0,0 +1,25 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, Response> = async (req, res) => {
|
||||||
|
const { value } = req.body || {};
|
||||||
|
const { frameId, tab } = req.sender || {};
|
||||||
|
|
||||||
|
if (frameId === 0 && tab?.id !== undefined) {
|
||||||
|
await chrome.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
||||||
|
await chrome.action.setBadgeText({ tabId: tab.id, text: value ? `${value}` : '' });
|
||||||
|
res.send({ success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
readonly value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
@ -0,0 +1,49 @@
|
|||||||
|
import type { PlasmoMessaging } from '@plasmohq/messaging';
|
||||||
|
import offIcon from 'url:~assets/off.png';
|
||||||
|
import onIcon from 'url:~assets/on.png';
|
||||||
|
import warnIcon from 'url:~assets/warn.png';
|
||||||
|
|
||||||
|
import { DEFAULT_DOMAIN_CONFIG, DEFAULT_EXTENSION_DATA } from '~utils/constants';
|
||||||
|
import { validateSupport } from '~utils/domain';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { DomainConfig, ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
const handler: PlasmoMessaging.MessageHandler<Request, Response> = async (req, res) => {
|
||||||
|
const { domain } = req.body || {};
|
||||||
|
const { frameId, tab } = req.sender || {};
|
||||||
|
|
||||||
|
if (domain && frameId === 0 && tab?.id !== undefined) {
|
||||||
|
const config = (await storage.get<DomainConfig>(domain)) || DEFAULT_DOMAIN_CONFIG;
|
||||||
|
const data = (await storage.get<ExtensionData>('data')) || DEFAULT_EXTENSION_DATA;
|
||||||
|
|
||||||
|
if (
|
||||||
|
config.on &&
|
||||||
|
tab.url &&
|
||||||
|
validateSupport(new URL(tab.url).hostname, data.exclusions.domains)
|
||||||
|
) {
|
||||||
|
if (config.issue?.url) {
|
||||||
|
await chrome.action.setIcon({ path: warnIcon, tabId: tab.id });
|
||||||
|
res.send({ success: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await chrome.action.setIcon({ path: onIcon, tabId: tab.id });
|
||||||
|
res.send({ success: true });
|
||||||
|
} else {
|
||||||
|
await chrome.action.setIcon({ path: offIcon, tabId: tab.id });
|
||||||
|
res.send({ success: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({ success: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
readonly domain: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Response {
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default handler;
|
368
packages/browser-extension/src/contents/index.ts
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import { sendToBackground } from '@plasmohq/messaging';
|
||||||
|
import type { PlasmoCSConfig } from 'plasmo';
|
||||||
|
|
||||||
|
import { DEFAULT_DOMAIN_CONFIG, DEFAULT_EXTENSION_DATA } from '~utils/constants';
|
||||||
|
import { formatDomainFromURL, validateSupport } from '~utils/domain';
|
||||||
|
import type { DomainConfig, ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
export const config: PlasmoCSConfig = {
|
||||||
|
all_frames: true,
|
||||||
|
matches: ['http://*/*', 'https://*/*'],
|
||||||
|
run_at: 'document_start',
|
||||||
|
};
|
||||||
|
|
||||||
|
class NotifiableSet extends Set {
|
||||||
|
constructor(...args: any[]) {
|
||||||
|
super(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: any): this {
|
||||||
|
super.add(value);
|
||||||
|
sendToBackground({ body: { value: super.size }, name: 'extension/updateBadge' });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let { actions, exclusions, keywords, tokens }: ExtensionData = DEFAULT_EXTENSION_DATA;
|
||||||
|
let domainConfig: DomainConfig = DEFAULT_DOMAIN_CONFIG;
|
||||||
|
let initiallyVisible: boolean = false;
|
||||||
|
|
||||||
|
const domain = formatDomainFromURL(new URL(location.href));
|
||||||
|
const log = new NotifiableSet();
|
||||||
|
const observer = new MutationObserver(mutationHandler);
|
||||||
|
const options: MutationObserverInit = { childList: true, subtree: true };
|
||||||
|
const seen = new Set<HTMLElement>();
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', setUpAfterWaitForBody);
|
||||||
|
window.addEventListener('pageshow', setUpAfterWaitForBody);
|
||||||
|
setUpAfterWaitForBody();
|
||||||
|
|
||||||
|
function clean(elements: readonly HTMLElement[], skipMatch?: boolean): void {
|
||||||
|
let index = 0;
|
||||||
|
const size = 50;
|
||||||
|
|
||||||
|
function chunk() {
|
||||||
|
const end = Math.min(index + size, elements.length);
|
||||||
|
|
||||||
|
for (; index < end; index++) {
|
||||||
|
const element = elements[index];
|
||||||
|
|
||||||
|
if (match(element, skipMatch)) {
|
||||||
|
if (element instanceof HTMLDialogElement) element.close();
|
||||||
|
hide(element);
|
||||||
|
log.add(`${Date.now()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < elements.length) {
|
||||||
|
requestAnimationFrame(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceClean(from: HTMLElement): void {
|
||||||
|
const elements = getElements(tokens.selectors, { filterEarly: true, from });
|
||||||
|
|
||||||
|
if (elements.length) {
|
||||||
|
fix();
|
||||||
|
clean(elements, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasKeyword(element: HTMLElement): boolean {
|
||||||
|
return !!keywords.length && !!element.outerHTML.match(new RegExp(keywords.join('|')));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterNodeEarly(node: Node, stopRecursion?: boolean): readonly HTMLElement[] {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof HTMLElement)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasKeyword(node) && !stopRecursion) {
|
||||||
|
return [node, ...[...node.children].flatMap((node) => filterNodeEarly(node, true))];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [node];
|
||||||
|
}
|
||||||
|
|
||||||
|
function fix(): void {
|
||||||
|
for (const action of actions) {
|
||||||
|
const { name, property, selector } = action;
|
||||||
|
|
||||||
|
if (domain.match(action.domain.replaceAll(/\*/g, '[^ ]*'))) {
|
||||||
|
switch (name) {
|
||||||
|
case 'click': {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (element instanceof HTMLElement) {
|
||||||
|
element.click();
|
||||||
|
log.add(name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'remove': {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (element instanceof HTMLElement && property) {
|
||||||
|
element.style.removeProperty(property);
|
||||||
|
log.add(name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reload': {
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'reset': {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
|
||||||
|
if (element instanceof HTMLElement && property) {
|
||||||
|
element.style.setProperty(property, 'initial', 'important');
|
||||||
|
log.add(name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'resetAll': {
|
||||||
|
const elements = getElements(selector);
|
||||||
|
|
||||||
|
if (property) {
|
||||||
|
elements.forEach((e) => e?.style?.setProperty(property, 'initial', 'important'));
|
||||||
|
log.add(name);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const backdrops = getElements(tokens.backdrops);
|
||||||
|
|
||||||
|
for (const backdrop of backdrops) {
|
||||||
|
if (backdrop.children.length === 0 && !seen.has(backdrop)) {
|
||||||
|
log.add(`${Date.now()}`);
|
||||||
|
seen.add(backdrop);
|
||||||
|
hide(backdrop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const skips = exclusions.overflows.map((x) => (x.split('.').length < 3 ? `*${x}` : x));
|
||||||
|
|
||||||
|
if (!skips.some((x) => domain.match(x.replaceAll(/\*/g, '[^ ]*')))) {
|
||||||
|
for (const element of [document.body, document.documentElement]) {
|
||||||
|
element?.classList.remove(...(tokens.classes ?? []));
|
||||||
|
element?.style.setProperty('position', 'initial', 'important');
|
||||||
|
element?.style.setProperty('overflow-y', 'initial', 'important');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ionRouterOutlet = document.getElementsByTagName('ion-router-outlet')[0];
|
||||||
|
|
||||||
|
if (ionRouterOutlet) {
|
||||||
|
// 2024-08-02: fix #644 temporarily
|
||||||
|
ionRouterOutlet.removeAttribute('inert');
|
||||||
|
log.add('ion-router-outlet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const t4Wrapper = document.getElementsByClassName('t4-wrapper')[0];
|
||||||
|
|
||||||
|
if (t4Wrapper) {
|
||||||
|
log.add('t4-wrapper');
|
||||||
|
// 2024-09-12: fix #945 temporarily
|
||||||
|
t4Wrapper.removeAttribute('inert');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElements(selector: Selector, params: GetElementsParams = {}): readonly HTMLElement[] {
|
||||||
|
const { filterEarly, from } = params;
|
||||||
|
let result: HTMLElement[] = [];
|
||||||
|
|
||||||
|
if (selector.length) {
|
||||||
|
result = [
|
||||||
|
...(from ?? document).querySelectorAll(selector as string),
|
||||||
|
] as unknown as HTMLElement[];
|
||||||
|
|
||||||
|
if (filterEarly) {
|
||||||
|
result = result.flatMap((node) => filterNodeEarly(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getElementsWithChildren(
|
||||||
|
selector: Selector,
|
||||||
|
params: GetElementsParams = {}
|
||||||
|
): readonly HTMLElement[] {
|
||||||
|
const elements = getElements(selector, params);
|
||||||
|
const elementsWithChildren = elements.flatMap((element) => [element, ...element.children]);
|
||||||
|
|
||||||
|
return elementsWithChildren as unknown as readonly HTMLElement[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(element: HTMLElement) {
|
||||||
|
element.style.setProperty('clip-path', 'circle(0px)', 'important');
|
||||||
|
element.style.setProperty('display', 'none', 'important');
|
||||||
|
element.style.setProperty('height', '0px', 'important');
|
||||||
|
element.style.setProperty('overflow', 'hidden', 'important');
|
||||||
|
element.style.setProperty('transform', 'scale(0)', 'important');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInViewport(element: HTMLElement): boolean {
|
||||||
|
const styles = window.getComputedStyle(element);
|
||||||
|
const height = window.innerHeight || document.documentElement.clientHeight;
|
||||||
|
const position = element.getBoundingClientRect();
|
||||||
|
const scroll = window.scrollY;
|
||||||
|
|
||||||
|
return (
|
||||||
|
position.bottom === position.top ||
|
||||||
|
(scroll + position.top <= scroll + height && scroll + position.bottom >= scroll) ||
|
||||||
|
styles.animationDuration !== '0s' ||
|
||||||
|
styles.transitionDuration !== '0s'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function match(element: HTMLElement, skipMatch?: boolean): boolean {
|
||||||
|
if (!exclusions.tags.length || !tokens.selectors.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(element instanceof HTMLElement) || !element.tagName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagName = element.tagName.toUpperCase();
|
||||||
|
|
||||||
|
if (exclusions.tags.includes(tagName)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAttributes = !!element.getAttributeNames().filter((x) => x !== 'data-nosnippet').length;
|
||||||
|
|
||||||
|
if (!hasAttributes && !tagName.includes('-')) {
|
||||||
|
forceClean(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2023-06-10: fix #113 temporarily
|
||||||
|
if (element.classList.contains('chat-line__message')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2024-08-03: fix #701 temporarily
|
||||||
|
if (element.classList.contains('sellos')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDialog = tagName === 'DIALOG' && element.getAttribute('open') === 'true';
|
||||||
|
const isFakeDialog = tagName === 'DIV' && element.className.includes('cmp');
|
||||||
|
|
||||||
|
return (
|
||||||
|
(isDialog || isFakeDialog || isInViewport(element)) &&
|
||||||
|
(skipMatch || element.matches(tokens.selectors as unknown as string))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mutationHandler(mutations: readonly MutationRecord[]): void {
|
||||||
|
if (!domainConfig.on || !tokens.selectors.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = mutations.flatMap((mutation) => [...mutation.addedNodes]);
|
||||||
|
const elements = nodes.flatMap((node) => filterNodeEarly(node));
|
||||||
|
|
||||||
|
run({ elements });
|
||||||
|
}
|
||||||
|
|
||||||
|
function run(params: RunParams = {}): void {
|
||||||
|
const { containers, elements, skipMatch } = params;
|
||||||
|
|
||||||
|
if (document.body?.children.length && domainConfig.on && tokens.selectors.length) {
|
||||||
|
fix();
|
||||||
|
|
||||||
|
if (elements?.length) {
|
||||||
|
clean(elements, skipMatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements === undefined && containers?.length) {
|
||||||
|
clean(containers.flatMap((x) => getElementsWithChildren(x, { filterEarly: true })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function runtimeMessageHandler(message: RuntimeMessage): void {
|
||||||
|
switch (message.name) {
|
||||||
|
case 'INCREASE_LOG_COUNT': {
|
||||||
|
log.add(message.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setUp(params: SetUpParams = {}): Promise<void> {
|
||||||
|
const { data } = await sendToBackground({ name: 'database/get' });
|
||||||
|
|
||||||
|
exclusions = data?.exclusions ?? exclusions;
|
||||||
|
sendToBackground({ body: { domain }, name: 'extension/updateIcon' });
|
||||||
|
|
||||||
|
if (!validateSupport(location.hostname, exclusions.domains)) {
|
||||||
|
observer.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
domainConfig = (await sendToBackground({ body: { domain }, name: 'domain/config' }))?.data;
|
||||||
|
|
||||||
|
if (domainConfig.on) {
|
||||||
|
chrome.runtime.onMessage.addListener(runtimeMessageHandler);
|
||||||
|
|
||||||
|
actions = data?.actions ?? actions;
|
||||||
|
keywords = data?.keywords ?? keywords;
|
||||||
|
tokens = data?.tokens ?? tokens;
|
||||||
|
|
||||||
|
observer.observe(document.body ?? document.documentElement, options);
|
||||||
|
if (!params.skipRunFn) run({ containers: tokens.containers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setUpAfterWaitForBody(): Promise<void> {
|
||||||
|
if (document.visibilityState === 'visible' && !initiallyVisible) {
|
||||||
|
if (document.body) {
|
||||||
|
initiallyVisible = true;
|
||||||
|
await setUp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(setUpAfterWaitForBody, 50);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GetElementsParams {
|
||||||
|
readonly filterEarly?: boolean;
|
||||||
|
readonly from?: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RunParams {
|
||||||
|
readonly containers?: readonly string[];
|
||||||
|
readonly elements?: readonly HTMLElement[];
|
||||||
|
readonly skipMatch?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuntimeMessage {
|
||||||
|
readonly name: string;
|
||||||
|
readonly value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Selector = string | readonly string[];
|
||||||
|
|
||||||
|
interface SetUpParams {
|
||||||
|
readonly skipRunFn?: boolean;
|
||||||
|
}
|
17
packages/browser-extension/src/hooks/useBrowser.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export function useBrowser(): UseBrowserResult {
|
||||||
|
const isChromium = navigator.userAgent.indexOf('Chrome') !== -1;
|
||||||
|
const isEdge = navigator.userAgent.indexOf('Edg') !== -1;
|
||||||
|
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isChromium,
|
||||||
|
isEdge,
|
||||||
|
isFirefox,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseBrowserResult {
|
||||||
|
readonly isChromium: boolean;
|
||||||
|
readonly isEdge: boolean;
|
||||||
|
readonly isFirefox: boolean;
|
||||||
|
}
|
41
packages/browser-extension/src/hooks/useData.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { sendToBackground } from '@plasmohq/messaging';
|
||||||
|
import { useStorage } from '@plasmohq/storage/hook';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
export function useData(): UseDataResult {
|
||||||
|
const timeoutId = useRef<number | undefined>(undefined);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const [isRefetched, setIsRefetched] = useState<boolean>(false);
|
||||||
|
const [data] = useStorage<ExtensionData>({ instance: storage, key: 'data' });
|
||||||
|
|
||||||
|
const refetch = useCallback(async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await sendToBackground({ name: 'database/refresh' });
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsRefetched(true);
|
||||||
|
timeoutId.current = window.setTimeout(() => setIsRefetched(false), 60000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeoutId.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
isRefetched,
|
||||||
|
refetch,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseDataResult {
|
||||||
|
readonly data?: ExtensionData;
|
||||||
|
readonly isLoading: boolean;
|
||||||
|
readonly isRefetched: boolean;
|
||||||
|
readonly refetch: () => Promise<void>;
|
||||||
|
}
|
52
packages/browser-extension/src/hooks/useDomain.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useStorage } from '@plasmohq/storage/hook';
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { DEFAULT_DOMAIN_CONFIG } from '~utils/constants';
|
||||||
|
import { formatDomainFromURL, validateSupport } from '~utils/domain';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { DomainConfig, ExtensionData } from '~utils/types';
|
||||||
|
|
||||||
|
import { useTab } from './useTab';
|
||||||
|
|
||||||
|
export function useDomain(): UseDomainResult {
|
||||||
|
const [domain, setDomain] = useState<string>('');
|
||||||
|
const [config, setConfig] = useStorage<DomainConfig>({ instance: storage, key: domain });
|
||||||
|
const [data] = useStorage<ExtensionData>({ instance: storage, key: 'data' });
|
||||||
|
const tab = useTab();
|
||||||
|
const tabId = tab ? (tab.id ?? -1) : -1;
|
||||||
|
|
||||||
|
const isSupported = useMemo(
|
||||||
|
() =>
|
||||||
|
!!data?.exclusions.domains.length &&
|
||||||
|
!!tab?.url &&
|
||||||
|
validateSupport(new URL(tab.url).hostname, data.exclusions.domains),
|
||||||
|
[data, tab]
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggleStatus = useCallback(async () => {
|
||||||
|
if (tabId > -1) {
|
||||||
|
await setConfig((prev = DEFAULT_DOMAIN_CONFIG) => ({ ...prev, on: !prev.on }));
|
||||||
|
await chrome.tabs.reload(tabId, { bypassCache: true });
|
||||||
|
}
|
||||||
|
}, [domain, tabId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tab?.url) {
|
||||||
|
setDomain(formatDomainFromURL(new URL(tab.url)));
|
||||||
|
}
|
||||||
|
}, [tab]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config: config || DEFAULT_DOMAIN_CONFIG,
|
||||||
|
domain,
|
||||||
|
isSupported,
|
||||||
|
toggleStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseDomainResult {
|
||||||
|
readonly config: DomainConfig;
|
||||||
|
readonly domain: string;
|
||||||
|
readonly isSupported: boolean;
|
||||||
|
readonly toggleStatus: () => Promise<void>;
|
||||||
|
}
|
90
packages/browser-extension/src/hooks/useDomainReport.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { API_URL } from '~utils/constants';
|
||||||
|
import type { ReportParams, ReportResult } from '~utils/types';
|
||||||
|
|
||||||
|
import { useExtension } from './useExtension';
|
||||||
|
|
||||||
|
export function useDomainReport(): UseDomainReportResult {
|
||||||
|
const extension = useExtension();
|
||||||
|
const [errors, setErrors] = useState<{ readonly [key: string]: string }>({});
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const report = useCallback(
|
||||||
|
async (params: ReportParams): Promise<ReportResult | undefined> => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const reason = params.reason;
|
||||||
|
const url = params.url;
|
||||||
|
const errors = validateForm(params);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
setErrors(errors);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
const version = extension.version;
|
||||||
|
const body = JSON.stringify({ reason, url, userAgent, version });
|
||||||
|
const headers = { 'Cache-Control': 'no-cache', 'Content-type': 'application/json' };
|
||||||
|
const requestInit = { body, headers, method: 'POST' };
|
||||||
|
const response = await fetch(`${API_URL}/report/`, requestInit);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
throw new Error(chrome.i18n.getMessage('report_rateLimitError'));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(chrome.i18n.getMessage('report_unknownError'));
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
setErrors({ url: error.message });
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[extension.version]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
isSubmitting,
|
||||||
|
report,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm(params: ReportParams): Partial<ReportParams> | undefined {
|
||||||
|
const { reason, url } = params;
|
||||||
|
let errors: Partial<ReportParams> | undefined = undefined;
|
||||||
|
|
||||||
|
if (!reason || reason.length < 10 || reason.length > 1000) {
|
||||||
|
errors = {
|
||||||
|
...(errors ?? {}),
|
||||||
|
reason: chrome.i18n.getMessage('report_reasonInputError'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (/\s/.test(url)) throw new Error();
|
||||||
|
new URL(url);
|
||||||
|
} catch {
|
||||||
|
errors = {
|
||||||
|
...(errors ?? {}),
|
||||||
|
url: chrome.i18n.getMessage('report_urlInputError'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseDomainReportResult {
|
||||||
|
readonly errors: { readonly [key: string]: string };
|
||||||
|
readonly isSubmitting: boolean;
|
||||||
|
readonly report: (params: ReportParams) => Promise<ReportResult | undefined>;
|
||||||
|
}
|
52
packages/browser-extension/src/hooks/useExclusions.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
|
||||||
|
async function getExclusions(): Promise<readonly Exclusion[]> {
|
||||||
|
const result = await storage.rawGetAll();
|
||||||
|
|
||||||
|
return Object.keys(result).flatMap((key) => (result[key]?.on === false ? [{ name: key }] : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExclusions(): readonly Exclusion[] {
|
||||||
|
const [exclusions, setExclusions] = useState<readonly Exclusion[]>([]);
|
||||||
|
|
||||||
|
const handleStorageChange = useCallback(
|
||||||
|
(
|
||||||
|
changes: { [key: string]: chrome.storage.StorageChange },
|
||||||
|
areaName: chrome.storage.AreaName
|
||||||
|
) => {
|
||||||
|
if (areaName === 'local') {
|
||||||
|
for (const key in changes) {
|
||||||
|
if (key === 'data') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
setExclusions((prev) => {
|
||||||
|
if (changes[key].newValue?.on === false) {
|
||||||
|
return [...prev, { name: key }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev.filter((exclusion) => exclusion.name !== key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chrome.storage.onChanged.addListener(handleStorageChange);
|
||||||
|
getExclusions().then(setExclusions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chrome.storage.onChanged.removeListener(handleStorageChange);
|
||||||
|
};
|
||||||
|
}, [handleStorageChange]);
|
||||||
|
|
||||||
|
return exclusions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exclusion {
|
||||||
|
readonly name: string;
|
||||||
|
}
|
18
packages/browser-extension/src/hooks/useExtension.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useStorage } from '@plasmohq/storage/hook';
|
||||||
|
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
|
||||||
|
export function useExtension(): UseExtensionResult {
|
||||||
|
const [updateAvailable] = useStorage({ instance: storage, key: 'updateAvailable' });
|
||||||
|
const { version } = chrome.runtime.getManifest();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateAvailable,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseExtensionResult {
|
||||||
|
readonly updateAvailable?: string;
|
||||||
|
readonly version: string;
|
||||||
|
}
|
11
packages/browser-extension/src/hooks/useTab.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function useTab(): chrome.tabs.Tab | undefined {
|
||||||
|
const [tab, setTab] = useState<chrome.tabs.Tab | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => setTab(tabs[0]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return tab;
|
||||||
|
}
|
@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"manifest_version": 3,
|
|
||||||
"name": "Cookie Dialog Monster",
|
|
||||||
"version": "8.0.5",
|
|
||||||
"default_locale": "en",
|
|
||||||
"description": "__MSG_appDesc__",
|
|
||||||
"icons": {
|
|
||||||
"16": "assets/icons/16.png",
|
|
||||||
"48": "assets/icons/48.png",
|
|
||||||
"128": "assets/icons/128.png"
|
|
||||||
},
|
|
||||||
"action": {
|
|
||||||
"default_icon": "assets/icons/off.png",
|
|
||||||
"default_popup": "popup.html",
|
|
||||||
"default_title": "Cookie Dialog Monster"
|
|
||||||
},
|
|
||||||
"options_page": "options.html",
|
|
||||||
"author": "wanhose",
|
|
||||||
"background": {
|
|
||||||
"scripts": ["scripts/background.js"],
|
|
||||||
"service_worker": "scripts/background.js"
|
|
||||||
},
|
|
||||||
"browser_specific_settings": {
|
|
||||||
"gecko": {
|
|
||||||
"id": "{77e2c00b-e173-4604-863d-01645d8d2826}",
|
|
||||||
"strict_min_version": "126.0",
|
|
||||||
"update_url": "https://www.cookie-dialog-monster.com/releases/mozilla/updates.json"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"content_scripts": [
|
|
||||||
{
|
|
||||||
"all_frames": true,
|
|
||||||
"js": ["scripts/content.js"],
|
|
||||||
"matches": ["http://*/*", "https://*/*"],
|
|
||||||
"run_at": "document_start"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"host_permissions": ["http://*/*", "https://*/*"],
|
|
||||||
"permissions": ["contextMenus", "declarativeNetRequest", "storage", "webRequest"],
|
|
||||||
"web_accessible_resources": [
|
|
||||||
{
|
|
||||||
"matches": ["http://*/*", "https://*/*"],
|
|
||||||
"resources": ["https://fonts.googleapis.com/css?family=Inter"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,127 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Cookie Dialog Monster > Exclusion List</title>
|
|
||||||
<link rel="stylesheet" href="/styles/reset.css" />
|
|
||||||
<link rel="stylesheet" href="/styles/options.css" />
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter" />
|
|
||||||
<script src="/scripts/options.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<div>
|
|
||||||
<h1 class="header-title">
|
|
||||||
Cookie Dialog Monster > <span data-i18n="options_exclusionListTitle"></span>
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<div class="button-group">
|
|
||||||
<button data-variant="large" id="add-button">
|
|
||||||
<span data-i18n="options_addButton"></span>
|
|
||||||
<svg
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
>
|
|
||||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button data-variant="large" id="clear-button">
|
|
||||||
<span data-i18n="options_clearButton"></span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
>
|
|
||||||
<polyline points="3 6 5 6 21 6"></polyline>
|
|
||||||
<path
|
|
||||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
|
||||||
/>
|
|
||||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
||||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button data-variant="large" id="import-button">
|
|
||||||
<span data-i18n="options_importButton"></span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
>
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
||||||
<polyline points="17 8 12 3 7 8"></polyline>
|
|
||||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
|
||||||
</svg>
|
|
||||||
<input accept=".cdm" id="file-input" style="display: none" type="file" />
|
|
||||||
</button>
|
|
||||||
<button data-variant="large" id="export-button">
|
|
||||||
<span data-i18n="options_exportButton"></span>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
>
|
|
||||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
|
||||||
<polyline points="7 10 12 15 17 10"></polyline>
|
|
||||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<input id="filter-input" data-i18n-placeholder="options_filterPlaceholder" />
|
|
||||||
<ul id="exclusion-list">
|
|
||||||
<li id="exclusion-list-item-template" style="display: none">
|
|
||||||
<span></span>
|
|
||||||
<button>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="14"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="14"
|
|
||||||
>
|
|
||||||
<polyline points="3 6 5 6 21 6"></polyline>
|
|
||||||
<path
|
|
||||||
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
|
|
||||||
/>
|
|
||||||
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
||||||
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li id="exclusion-list-item-empty" style="display: none"></li>
|
|
||||||
</ul>
|
|
||||||
</main>
|
|
||||||
<footer></footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
13
packages/browser-extension/src/options/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Cookie Dialog Monster > Exclusion List</title>
|
||||||
|
<link rel="stylesheet" href="~src/styles/reset.css" />
|
||||||
|
<link rel="stylesheet" href="~src/styles/common.css" />
|
||||||
|
<link rel="stylesheet" href="~src/styles/options.css" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter" />
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
227
packages/browser-extension/src/options/index.tsx
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import { type ChangeEvent, type JSX, type MouseEvent, useCallback, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { useExclusions } from '~hooks/useExclusions';
|
||||||
|
import { DOMAIN_REG_EXP } from '~utils/domain';
|
||||||
|
import { storage } from '~utils/storage';
|
||||||
|
import type { DomainConfig } from '~utils/types';
|
||||||
|
|
||||||
|
export default function Options(): JSX.Element {
|
||||||
|
const exclusions = useExclusions();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const [filterValue, setFilterValue] = useState<string>('');
|
||||||
|
|
||||||
|
const handleAddClick = useCallback(async () => {
|
||||||
|
const message = chrome.i18n.getMessage('options_addPrompt');
|
||||||
|
const domain = window.prompt(message)?.trim().replace('www.', '');
|
||||||
|
|
||||||
|
if (domain && (DOMAIN_REG_EXP.test(domain) || domain === 'localhost')) {
|
||||||
|
const prev = await storage.get<DomainConfig>(domain);
|
||||||
|
|
||||||
|
await storage.set(domain, { ...prev, on: false });
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClearClick = useCallback(async () => {
|
||||||
|
for (const domain of exclusions) {
|
||||||
|
const prev = await storage.get<DomainConfig>(domain.name);
|
||||||
|
|
||||||
|
if (prev?.issue?.url) await storage.set(domain.name, { ...prev, on: true });
|
||||||
|
else await storage.remove(domain.name);
|
||||||
|
}
|
||||||
|
}, [exclusions]);
|
||||||
|
|
||||||
|
const handleDeleteClick = useCallback(async (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
const { value: domain } = event.currentTarget.dataset;
|
||||||
|
|
||||||
|
if (domain) {
|
||||||
|
const prev = await storage.get<DomainConfig>(domain);
|
||||||
|
|
||||||
|
if (prev?.issue?.url) await storage.set(domain, { ...prev, on: true });
|
||||||
|
else await storage.remove(domain);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleExportClick = useCallback(() => {
|
||||||
|
const anchor = document.createElement('a');
|
||||||
|
const now = new Date();
|
||||||
|
const day = now.getDate().toString().padStart(2, '0');
|
||||||
|
const month = now.getMonth().toString().padStart(2, '0');
|
||||||
|
const year = now.getUTCFullYear();
|
||||||
|
const text = exclusions.reduce((prev, curr) => `${prev}\n${curr.name}`, '');
|
||||||
|
const defaultTitle = `${year}${month}${day}`;
|
||||||
|
const customTitle = window.prompt('Enter a file name', defaultTitle);
|
||||||
|
|
||||||
|
if (customTitle) {
|
||||||
|
const blob = new Blob([text], { type: 'octet/stream' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = `${customTitle || defaultTitle}.cdm`;
|
||||||
|
anchor.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}, [exclusions]);
|
||||||
|
|
||||||
|
const handleFileChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const file = event.currentTarget.files?.[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.addEventListener('load', async (event) => {
|
||||||
|
const input =
|
||||||
|
event.currentTarget && 'result' in event.currentTarget
|
||||||
|
? (event.currentTarget.result as string)?.split('\n')
|
||||||
|
: [];
|
||||||
|
|
||||||
|
for (const value of input) {
|
||||||
|
const domain = value.replace('www.', '').trim();
|
||||||
|
|
||||||
|
if (domain && (DOMAIN_REG_EXP.test(domain) || domain === 'localhost')) {
|
||||||
|
const prev = await storage.get<DomainConfig>(domain);
|
||||||
|
|
||||||
|
await storage.set(domain, { ...prev, on: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
event.currentTarget.value = '';
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFilterChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilterValue(event.currentTarget.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleImportClick = useCallback(() => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="header">
|
||||||
|
<div>
|
||||||
|
<h1>Cookie Dialog Monster > {chrome.i18n.getMessage('options_exclusionListTitle')}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main className="main">
|
||||||
|
<div className="button-group">
|
||||||
|
<button className="button" data-variant="large" onClick={handleAddClick}>
|
||||||
|
<span>{chrome.i18n.getMessage('options_addButton')}</span>
|
||||||
|
<svg
|
||||||
|
fill="none"
|
||||||
|
height="14"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
>
|
||||||
|
<line x1="12" y1="5" x2="12" y2="19" />
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button className="button" data-variant="large" onClick={handleClearClick}>
|
||||||
|
<span>{chrome.i18n.getMessage('options_clearButton')}</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="14"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17" />
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button className="button" data-variant="large" onClick={handleImportClick}>
|
||||||
|
<span>{chrome.i18n.getMessage('options_importButton')}</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="14"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="17 8 12 3 7 8" />
|
||||||
|
<line x1="12" y1="3" x2="12" y2="15" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
accept=".cdm"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button className="button" data-variant="large" onClick={handleExportClick}>
|
||||||
|
<span>{chrome.i18n.getMessage('options_exportButton')}</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="14"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
>
|
||||||
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||||
|
<polyline points="7 10 12 15 17 10" />
|
||||||
|
<line x1="12" y1="15" x2="12" y2="3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
placeholder={chrome.i18n.getMessage('options_filterPlaceholder')}
|
||||||
|
value={filterValue}
|
||||||
|
/>
|
||||||
|
<ul className="exclusion-list">
|
||||||
|
{exclusions.length ? (
|
||||||
|
exclusions.map((exclusion) => (
|
||||||
|
<li key={exclusion.name}>
|
||||||
|
<span>{exclusion.name}</span>
|
||||||
|
<button className="button" data-value={exclusion.name} onClick={handleDeleteClick}>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="14"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
>
|
||||||
|
<polyline points="3 6 5 6 21 6" />
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17" />
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<li>{chrome.i18n.getMessage('options_empty')}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
<footer className="footer" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,338 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<link rel="stylesheet" href="/styles/reset.css" />
|
|
||||||
<link rel="stylesheet" href="/styles/popup.css" />
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter" />
|
|
||||||
<script src="/scripts/popup.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1 class="header-title">Cookie Dialog Monster</h1>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button
|
|
||||||
class="header-button"
|
|
||||||
data-href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/Help-or-issues%3F"
|
|
||||||
id="help-button"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="18"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="18"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
|
|
||||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button class="header-button" id="settings-button">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="18"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="18"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="3"></circle>
|
|
||||||
<path
|
|
||||||
d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<p aria-hidden="true" class="banner" data-variant="warning" id="issue-banner" role="alert">
|
|
||||||
<span id="issue-banner-text"></span>
|
|
||||||
<a id="issue-banner-url" target="_blank">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="12"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
>
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
||||||
<polyline points="15 3 21 3 21 9"></polyline>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p aria-hidden="true" class="banner" data-variant="error" id="support-banner" role="alert">
|
|
||||||
<span data-i18n="popup_bannerSupport"></span>
|
|
||||||
<a
|
|
||||||
href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/List-of-unsupported-sites"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="12"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
>
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
||||||
<polyline points="15 3 21 3 21 9"></polyline>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<p aria-hidden="true" class="banner" data-variant="notice" id="update-banner" role="alert">
|
|
||||||
<span data-i18n="popup_bannerUpdateAvailable"></span>
|
|
||||||
<a
|
|
||||||
href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/releases"
|
|
||||||
id="update-banner-url"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="12"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
>
|
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
||||||
<polyline points="15 3 21 3 21 9"></polyline>
|
|
||||||
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
<div class="content">
|
|
||||||
<button class="popup-button" disabled id="power-option">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="32"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="32"
|
|
||||||
>
|
|
||||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
|
|
||||||
<line x1="12" y1="2" x2="12" y2="12" />
|
|
||||||
</svg>
|
|
||||||
<span id="host"></span>
|
|
||||||
</button>
|
|
||||||
<button class="popup-button" disabled id="report-option" role="link">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="32"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="32"
|
|
||||||
>
|
|
||||||
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path>
|
|
||||||
<line x1="4" y1="22" x2="4" y2="15"></line>
|
|
||||||
</svg>
|
|
||||||
<span data-i18n="contextMenu_reportOption"></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="popup-button"
|
|
||||||
data-href="https://git.wanhose.dev/wanhose/cookie-dialog-monster"
|
|
||||||
id="contribute-option"
|
|
||||||
role="link"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="32"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="32"
|
|
||||||
>
|
|
||||||
<circle cx="18" cy="18" r="3"></circle>
|
|
||||||
<circle cx="6" cy="6" r="3"></circle>
|
|
||||||
<path d="M13 6h3a2 2 0 0 1 2 2v7"></path>
|
|
||||||
<line x1="6" y1="9" x2="6" y2="21"></line>
|
|
||||||
</svg>
|
|
||||||
<span data-i18n="popup_contributeOption"></span>
|
|
||||||
</button>
|
|
||||||
<button class="popup-button" data-href="#" id="rate-option" role="link">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="32"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="32"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span data-i18n="popup_rateOption"></span>
|
|
||||||
</button>
|
|
||||||
<div class="popup-data-container">
|
|
||||||
<div class="popup-data">
|
|
||||||
<b data-i18n="popup_databaseVersion"></b>
|
|
||||||
<span id="database-version"></span>
|
|
||||||
<button class="popup-data-button" data-animation="flip" id="refresh-database-button">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="12"
|
|
||||||
id="refresh-database-spinner"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
>
|
|
||||||
<polyline points="1 4 1 10 7 10"></polyline>
|
|
||||||
<polyline points="23 20 23 14 17 14"></polyline>
|
|
||||||
<path
|
|
||||||
d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="12"
|
|
||||||
id="refresh-database-check"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="var(--color-success)"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="12"
|
|
||||||
>
|
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="popup-data">
|
|
||||||
<b data-i18n="popup_extensionVersion"></b>
|
|
||||||
<span id="extension-version"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="report" style="display: none">
|
|
||||||
<div class="report-form-view">
|
|
||||||
<div class="report-body-text" data-i18n="report_bodyText"></div>
|
|
||||||
<div class="report-form">
|
|
||||||
<div class="report-input-group">
|
|
||||||
<div class="report-input-label" id="report-label-url">
|
|
||||||
<span data-i18n="report_urlInputLabel"></span>
|
|
||||||
<span class="report-input-label-required">*</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
aria-labelledby="report-label-url"
|
|
||||||
aria-required="true"
|
|
||||||
class="report-input"
|
|
||||||
id="report-input-url"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="report-input-error"
|
|
||||||
data-i18n="report_urlInputError"
|
|
||||||
id="report-input-url-error"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="report-input-group">
|
|
||||||
<div class="report-input-label" id="report-label-reason">
|
|
||||||
<span data-i18n="report_reasonInputLabel"></span>
|
|
||||||
<span class="report-input-label-required">*</span>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
aria-labelledby="report-label-reason"
|
|
||||||
aria-required="true"
|
|
||||||
class="report-input"
|
|
||||||
data-i18n="report_reasonInputPlaceholder"
|
|
||||||
id="report-input-reason"
|
|
||||||
rows="4"
|
|
||||||
></textarea>
|
|
||||||
<div
|
|
||||||
class="report-input-error"
|
|
||||||
data-i18n="report_reasonInputError"
|
|
||||||
id="report-input-reason-error"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
<div class="report-buttons">
|
|
||||||
<button class="report-submit-button" data-i18n="contextMenu_reportOption"></button>
|
|
||||||
<button class="report-cancel-button" data-i18n="report_cancelButtonText"></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="report-submit-error-view" hidden>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="48"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="var(--color-error)"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
||||||
</svg>
|
|
||||||
<div class="report-submit-text" data-i18n="report_submitErrorText"></div>
|
|
||||||
<div class="report-submit-extra-text" data-i18n="report_submitErrorExtraText"></div>
|
|
||||||
<div class="report-issue-button" data-i18n="contextMenu_issueOption"></div>
|
|
||||||
</div>
|
|
||||||
<div class="report-submit-success-view" hidden>
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
fill="none"
|
|
||||||
height="48"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="var(--color-success)"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
width="48"
|
|
||||||
>
|
|
||||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
||||||
<polyline points="22 4 12 14.01 9 11.01" />
|
|
||||||
</svg>
|
|
||||||
<div class="report-submit-text" data-i18n="report_submitSuccessText"></div>
|
|
||||||
<div class="report-submit-extra-text" data-i18n="report_submitSuccessExtraText"></div>
|
|
||||||
<div class="report-issue-button" data-i18n="contextMenu_issueOption"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="loader">
|
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
<footer></footer>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
12
packages/browser-extension/src/popup/index.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="stylesheet" href="~src/styles/reset.css" />
|
||||||
|
<link rel="stylesheet" href="~src/styles/common.css" />
|
||||||
|
<link rel="stylesheet" href="~src/styles/popup.css" />
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Inter" />
|
||||||
|
</head>
|
||||||
|
<body></body>
|
||||||
|
</html>
|
472
packages/browser-extension/src/popup/index.tsx
Normal file
@ -0,0 +1,472 @@
|
|||||||
|
import { type FormEvent, type JSX, type KeyboardEvent, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { useBrowser } from '~hooks/useBrowser';
|
||||||
|
import { useData } from '~hooks/useData';
|
||||||
|
import { useDomain } from '~hooks/useDomain';
|
||||||
|
import { useDomainReport } from '~hooks/useDomainReport';
|
||||||
|
import { useExtension } from '~hooks/useExtension';
|
||||||
|
import { CHROME_STORE_URL, EDGE_STORE_URL, FIREFOX_STORE_URL } from '~utils/constants';
|
||||||
|
import { suppressLastError } from '~utils/error';
|
||||||
|
import type { ReportParams } from '~utils/types';
|
||||||
|
|
||||||
|
export default function Popup(): JSX.Element {
|
||||||
|
const { isChromium, isEdge, isFirefox } = useBrowser();
|
||||||
|
const { data, isLoading, isRefetched, refetch } = useData();
|
||||||
|
const { config, domain, isSupported, toggleStatus } = useDomain();
|
||||||
|
const { errors, isSubmitting, report } = useDomainReport();
|
||||||
|
const extension = useExtension();
|
||||||
|
const [issueExists, setIssueExists] = useState<boolean>(false);
|
||||||
|
const [issueURL, setIssueURL] = useState<string>('');
|
||||||
|
const [view, setView] = useState<'main' | 'report'>('main');
|
||||||
|
|
||||||
|
const handleCancelClick = useCallback(() => {
|
||||||
|
setIssueExists(false);
|
||||||
|
setIssueURL('');
|
||||||
|
setView('main');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.blur();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePowerClick = useCallback(async () => {
|
||||||
|
await toggleStatus();
|
||||||
|
window.close();
|
||||||
|
}, [toggleStatus]);
|
||||||
|
|
||||||
|
const handleRefreshClick = useCallback(async () => {
|
||||||
|
await refetch();
|
||||||
|
}, [refetch]);
|
||||||
|
|
||||||
|
const handleReportClick = useCallback(() => {
|
||||||
|
setView('report');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSettingsClick = useCallback(async () => {
|
||||||
|
chrome.runtime.openOptionsPage(suppressLastError);
|
||||||
|
window.close();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const params = Object.fromEntries(Object.entries(new FormData(event.currentTarget)));
|
||||||
|
const result = await report(params as ReportParams);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
setIssueExists(!result.success);
|
||||||
|
setIssueURL(result.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[report]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="header">
|
||||||
|
<h1>Cookie Dialog Monster</h1>
|
||||||
|
<div className="header-actions">
|
||||||
|
<a
|
||||||
|
className="header-button"
|
||||||
|
href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/Help-or-issues%3F"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="18"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<button className="header-button" onClick={handleSettingsClick}>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="18"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="18"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{config.issue?.url ? (
|
||||||
|
<p className="banner" data-variant="warning" role="alert">
|
||||||
|
<span>
|
||||||
|
{chrome.i18n.getMessage(
|
||||||
|
config.issue.flags?.includes('wontfix')
|
||||||
|
? 'popup_bannerIssueWontFix'
|
||||||
|
: 'popup_bannerIssueOpen'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a href={config.issue.url} target="_blank" rel="noreferrer">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{extension.updateAvailable ? (
|
||||||
|
<p className="banner" data-variant="notice" role="alert">
|
||||||
|
<span>{chrome.i18n.getMessage('popup_bannerUpdateAvailable')}</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`https://git.wanhose.dev/wanhose/cookie-dialog-monster/releases/${extension.updateAvailable}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
{isSupported ? null : (
|
||||||
|
<p className="banner" data-variant="error" role="alert">
|
||||||
|
<span>{chrome.i18n.getMessage('popup_bannerSupport')}</span>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/List-of-unsupported-sites"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
||||||
|
<polyline points="15 3 21 3 21 9" />
|
||||||
|
<line x1="10" y1="14" x2="21" y2="3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{view === 'main' ? (
|
||||||
|
<div className="content">
|
||||||
|
<button
|
||||||
|
className="popup-button power-option"
|
||||||
|
data-value={config.on ? 'on' : 'off'}
|
||||||
|
disabled={!!config.issue || !!extension.updateAvailable || !isSupported}
|
||||||
|
onClick={handlePowerClick}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="32"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="32"
|
||||||
|
>
|
||||||
|
<path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
|
||||||
|
<line x1="12" y1="2" x2="12" y2="12" />
|
||||||
|
</svg>
|
||||||
|
<span>{domain}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="popup-button"
|
||||||
|
disabled={!!config.issue || !!extension.updateAvailable || !isSupported}
|
||||||
|
onClick={handleReportClick}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="32"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="32"
|
||||||
|
>
|
||||||
|
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" />
|
||||||
|
<line x1="4" y1="22" x2="4" y2="15" />
|
||||||
|
</svg>
|
||||||
|
<span>{chrome.i18n.getMessage('contextMenu_reportOption')}</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
className="popup-button"
|
||||||
|
href="https://git.wanhose.dev/wanhose/cookie-dialog-monster"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="32"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="32"
|
||||||
|
>
|
||||||
|
<circle cx="18" cy="18" r="3" />
|
||||||
|
<circle cx="6" cy="6" r="3" />
|
||||||
|
<path d="M13 6h3a2 2 0 0 1 2 2v7" />
|
||||||
|
<line x1="6" y1="9" x2="6" y2="21" />
|
||||||
|
</svg>
|
||||||
|
<span>{chrome.i18n.getMessage('popup_contributeOption')}</span>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="popup-button rate-option"
|
||||||
|
href={
|
||||||
|
isEdge
|
||||||
|
? EDGE_STORE_URL
|
||||||
|
: isChromium
|
||||||
|
? CHROME_STORE_URL
|
||||||
|
: isFirefox
|
||||||
|
? FIREFOX_STORE_URL
|
||||||
|
: '#'
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="32"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="32"
|
||||||
|
>
|
||||||
|
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||||
|
</svg>
|
||||||
|
<span>{chrome.i18n.getMessage('popup_rateOption')}</span>
|
||||||
|
</a>
|
||||||
|
<div className="popup-data-container">
|
||||||
|
<div className="popup-data">
|
||||||
|
<b>{chrome.i18n.getMessage('popup_databaseVersion')}</b>
|
||||||
|
<span>{data?.version || '?'}</span>
|
||||||
|
<button
|
||||||
|
className="popup-data-button"
|
||||||
|
data-animation="flip"
|
||||||
|
data-refreshing={isLoading && !isRefetched}
|
||||||
|
data-refreshed={isRefetched}
|
||||||
|
disabled={isLoading || isRefetched}
|
||||||
|
onClick={handleRefreshClick}
|
||||||
|
>
|
||||||
|
{isRefetched ? (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="var(--color-success)"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="12"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="12"
|
||||||
|
>
|
||||||
|
<polyline points="1 4 1 10 7 10" />
|
||||||
|
<polyline points="23 20 23 14 17 14" />
|
||||||
|
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="popup-data">
|
||||||
|
<b>{chrome.i18n.getMessage('popup_extensionVersion')}</b>
|
||||||
|
<span>{extension.version}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="report">
|
||||||
|
{issueURL ? (
|
||||||
|
<>
|
||||||
|
{issueExists ? (
|
||||||
|
<div className="report-submit-error-view" hidden>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="48"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="var(--color-error)"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="48"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<line x1="12" y1="8" x2="12" y2="12" />
|
||||||
|
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||||
|
</svg>
|
||||||
|
<div className="report-submit-text">
|
||||||
|
{chrome.i18n.getMessage('report_submitErrorText')}
|
||||||
|
</div>
|
||||||
|
<div className="report-submit-extra-text">
|
||||||
|
{chrome.i18n.getMessage('report_submitErrorExtraText')}
|
||||||
|
</div>
|
||||||
|
<div className="report-issue-button">
|
||||||
|
{chrome.i18n.getMessage('contextMenu_issueOption')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="report-submit-success-view" hidden>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
fill="none"
|
||||||
|
height="48"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="var(--color-success)"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="48"
|
||||||
|
>
|
||||||
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
||||||
|
<polyline points="22 4 12 14.01 9 11.01" />
|
||||||
|
</svg>
|
||||||
|
<div className="report-submit-text">
|
||||||
|
{chrome.i18n.getMessage('report_submitSuccessText')}
|
||||||
|
</div>
|
||||||
|
<div className="report-submit-extra-text">
|
||||||
|
{chrome.i18n.getMessage('report_submitSuccessExtraText')}
|
||||||
|
</div>
|
||||||
|
<div className="report-issue-button">
|
||||||
|
{chrome.i18n.getMessage('contextMenu_issueOption')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="report-form-view">
|
||||||
|
<div className="report-body-text">{chrome.i18n.getMessage('report_bodyText')}</div>
|
||||||
|
<form className="report-form" noValidate onSubmit={handleSubmit}>
|
||||||
|
<div className="report-input-group">
|
||||||
|
<div className="report-input-label" id="report-label-url">
|
||||||
|
<span>{chrome.i18n.getMessage('report_urlInputLabel')}</span>
|
||||||
|
<span className="report-input-label-required">*</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
aria-invalid={!!errors['url']}
|
||||||
|
aria-labelledby="report-label-url"
|
||||||
|
aria-required="true"
|
||||||
|
className="report-input"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
name="url"
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
placeholder="https://www.example.com/"
|
||||||
|
/>
|
||||||
|
<div aria-hidden={!errors['url']} className="report-input-error">
|
||||||
|
{chrome.i18n.getMessage('report_urlInputError')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="report-input-group">
|
||||||
|
<div className="report-input-label" id="report-label-reason">
|
||||||
|
<span>{chrome.i18n.getMessage('report_reasonInputLabel')}</span>
|
||||||
|
<span className="report-input-label-required">*</span>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
aria-invalid={!!errors['reason']}
|
||||||
|
aria-labelledby="report-label-reason"
|
||||||
|
aria-required="true"
|
||||||
|
className="report-input"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
name="reason"
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
placeholder={chrome.i18n.getMessage('report_reasonInputPlaceholder')}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<div aria-hidden={!errors['reason']} className="report-input-error">
|
||||||
|
{chrome.i18n.getMessage('report_reasonInputError')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="report-buttons">
|
||||||
|
<button className="report-submit-button" disabled={isSubmitting} type="submit">
|
||||||
|
{chrome.i18n.getMessage('contextMenu_reportOption')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="report-cancel-button"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={handleCancelClick}
|
||||||
|
>
|
||||||
|
{chrome.i18n.getMessage('report_cancelButtonText')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
<footer />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,421 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {Object} ExtensionIssue
|
|
||||||
* @property {string[]} [flags]
|
|
||||||
* @property {string} [url]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} ExtensionState
|
|
||||||
* @property {ExtensionIssue} [issue]
|
|
||||||
* @property {boolean} on
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (typeof browser === 'undefined') {
|
|
||||||
browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Class for request batching
|
|
||||||
*/
|
|
||||||
class RequestManager {
|
|
||||||
constructor() {
|
|
||||||
this.requests = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Fetch wrapper to play with the request map
|
|
||||||
* @param {string} input
|
|
||||||
* @param {RequestInit} [init]
|
|
||||||
* @returns {Promise<any>}
|
|
||||||
*/
|
|
||||||
fetch(input, init) {
|
|
||||||
if (this.requests.has(input)) {
|
|
||||||
return this.requests.get(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promise = fetch(input, init)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.finally(() => this.requests.delete(input));
|
|
||||||
|
|
||||||
this.requests.set(input, promise);
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description API URL
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const apiUrl = 'https://api.cookie-dialog-monster.com/rest/v6';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Context menu identifier
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const extensionMenuItemId = 'CDM-MENU';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Context menu identifier
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const reportMenuItemId = 'CDM-REPORT';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Request manager instance
|
|
||||||
*/
|
|
||||||
const requestManager = new RequestManager();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Context menu identifier
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const settingsMenuItemId = 'CDM-SETTINGS';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Default value for extension state
|
|
||||||
* @type {ExtensionState}
|
|
||||||
*/
|
|
||||||
const stateByDefault = { issue: undefined, on: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description The storage to use
|
|
||||||
* @type {browser.storage.StorageArea}
|
|
||||||
*/
|
|
||||||
const storage = browser.storage.local;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Supress `browser.runtime.lastError`
|
|
||||||
*/
|
|
||||||
const suppressLastError = () => void browser.runtime.lastError;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Enable extension icon
|
|
||||||
* @param {number} tabId
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function enableIcon(hostname, tabId) {
|
|
||||||
const state = await getState(hostname);
|
|
||||||
const path = state.issue?.url ? '/assets/icons/warn.png' : '/assets/icons/on.png';
|
|
||||||
|
|
||||||
await browser.action.setIcon({ path, tabId }, suppressLastError);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Get database
|
|
||||||
* @returns {Promise<Object>}
|
|
||||||
*/
|
|
||||||
async function getData() {
|
|
||||||
const { data } = await storage.get('data');
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return await refreshData();
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Get current hostname
|
|
||||||
* @param {string} url
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getHostname(url) {
|
|
||||||
return new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Get current active tab
|
|
||||||
* @returns {Promise<browser.tabs.Tab>}
|
|
||||||
*/
|
|
||||||
async function getTab() {
|
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
|
||||||
|
|
||||||
return tabs[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Get state for the given hostname
|
|
||||||
* @param {string} hostname
|
|
||||||
* @returns {Promise<ExtensionState>}
|
|
||||||
*/
|
|
||||||
async function getState(hostname) {
|
|
||||||
const { [hostname]: state = stateByDefault } = await storage.get(hostname);
|
|
||||||
|
|
||||||
state.issue = await refreshIssue(hostname);
|
|
||||||
|
|
||||||
return { ...stateByDefault, ...state };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Get latest version available for this extension
|
|
||||||
* @returns {Promise<string>}
|
|
||||||
*/
|
|
||||||
async function getLatestVersion() {
|
|
||||||
try {
|
|
||||||
const { data } = await requestManager.fetch(`${apiUrl}/version/`);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Refresh data
|
|
||||||
* @param {number} [attempt]
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function refreshData(attempt = 1) {
|
|
||||||
if (attempt <= 3) {
|
|
||||||
try {
|
|
||||||
const { data } = await requestManager.fetch(`${apiUrl}/data/`);
|
|
||||||
|
|
||||||
await updateStore('data', data);
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return await refreshData(attempt + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Refresh issues for the given hostname
|
|
||||||
* @param {string} hostname
|
|
||||||
* @param {number} [attempt]
|
|
||||||
* @returns {Promise<ExtensionIssue | undefined>}
|
|
||||||
*/
|
|
||||||
async function refreshIssue(hostname, attempt = 1) {
|
|
||||||
if (attempt <= 3) {
|
|
||||||
try {
|
|
||||||
const { data = {} } = await requestManager.fetch(`${apiUrl}/issues/${hostname}/`);
|
|
||||||
await updateStore(hostname, { issue: { flags: data.flags, url: data.url } });
|
|
||||||
|
|
||||||
return data;
|
|
||||||
} catch {
|
|
||||||
return await refreshIssue(hostname, attempt + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Report given page
|
|
||||||
* @param {any} message
|
|
||||||
* @param {browser.tabs.Tab} tab
|
|
||||||
* @param {void?} callback
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function report(message) {
|
|
||||||
try {
|
|
||||||
const reason = message.reason;
|
|
||||||
const url = message.url;
|
|
||||||
const userAgent = message.userAgent;
|
|
||||||
const version = browser.runtime.getManifest().version;
|
|
||||||
const body = JSON.stringify({ reason, url, userAgent, version });
|
|
||||||
const headers = { 'Cache-Control': 'no-cache', 'Content-type': 'application/json' };
|
|
||||||
const requestInit = { body, headers, method: 'POST' };
|
|
||||||
|
|
||||||
return await requestManager.fetch(`${apiUrl}/report/`, requestInit);
|
|
||||||
} catch {
|
|
||||||
console.error("Can't send report");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Update extension store for a given key
|
|
||||||
* @param {string} [key]
|
|
||||||
* @param {Object} value
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function updateStore(key, value) {
|
|
||||||
if (key) {
|
|
||||||
const { [key]: prev } = await storage.get(key);
|
|
||||||
|
|
||||||
await storage.set({ [key]: { ...prev, ...value } }, suppressLastError);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen to context menus clicked
|
|
||||||
*/
|
|
||||||
browser.contextMenus.onClicked.addListener((info) => {
|
|
||||||
switch (info.menuItemId) {
|
|
||||||
case reportMenuItemId:
|
|
||||||
browser.action.openPopup();
|
|
||||||
break;
|
|
||||||
case settingsMenuItemId:
|
|
||||||
browser.runtime.openOptionsPage();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listens to messages
|
|
||||||
*/
|
|
||||||
browser.runtime.onMessage.addListener((message, sender, callback) => {
|
|
||||||
const hostname = message.hostname;
|
|
||||||
const isPage = sender.frameId === 0;
|
|
||||||
const tabId = sender.tab?.id;
|
|
||||||
|
|
||||||
switch (message.type) {
|
|
||||||
case 'DISABLE_ICON':
|
|
||||||
if (isPage && tabId !== undefined) {
|
|
||||||
browser.action.setIcon({ path: '/assets/icons/off.png', tabId }, suppressLastError);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'ENABLE_ICON':
|
|
||||||
if (isPage && tabId !== undefined) {
|
|
||||||
enableIcon(hostname, tabId).then(callback);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'GET_DATA':
|
|
||||||
getData().then(callback);
|
|
||||||
return true;
|
|
||||||
case 'GET_EXCLUSION_LIST':
|
|
||||||
storage.get(null, (exclusions) => {
|
|
||||||
const exclusionList = Object.entries(exclusions || {}).flatMap((exclusion) => {
|
|
||||||
return exclusion[0] !== 'data' && exclusion[1].on === false ? [exclusion[0]] : [];
|
|
||||||
});
|
|
||||||
callback(exclusionList);
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
case 'GET_LATEST_VERSION':
|
|
||||||
getLatestVersion().then(callback);
|
|
||||||
return true;
|
|
||||||
case 'GET_STATE':
|
|
||||||
if (hostname) {
|
|
||||||
getState(hostname).then(callback);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'GET_TAB':
|
|
||||||
getTab().then(callback);
|
|
||||||
return true;
|
|
||||||
case 'REFRESH_DATA':
|
|
||||||
refreshData().then(callback);
|
|
||||||
return true;
|
|
||||||
case 'REPORT':
|
|
||||||
report(message).then(callback);
|
|
||||||
return true;
|
|
||||||
case 'UPDATE_BADGE':
|
|
||||||
if (isPage && tabId !== undefined) {
|
|
||||||
browser.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
|
||||||
browser.action.setBadgeText({ tabId, text: message.value ? `${message.value}` : null });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'UPDATE_STORE':
|
|
||||||
updateStore(hostname, message.state).then(callback);
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listens to extension installed
|
|
||||||
*/
|
|
||||||
browser.runtime.onInstalled.addListener((details) => {
|
|
||||||
const documentUrlPatterns = browser.runtime.getManifest().content_scripts[0].matches;
|
|
||||||
|
|
||||||
browser.contextMenus.create(
|
|
||||||
{
|
|
||||||
contexts: ['all'],
|
|
||||||
documentUrlPatterns,
|
|
||||||
id: extensionMenuItemId,
|
|
||||||
title: 'Cookie Dialog Monster',
|
|
||||||
},
|
|
||||||
suppressLastError
|
|
||||||
);
|
|
||||||
browser.contextMenus.create(
|
|
||||||
{
|
|
||||||
contexts: ['all'],
|
|
||||||
documentUrlPatterns,
|
|
||||||
id: settingsMenuItemId,
|
|
||||||
parentId: extensionMenuItemId,
|
|
||||||
title: browser.i18n.getMessage('contextMenu_settingsOption'),
|
|
||||||
},
|
|
||||||
suppressLastError
|
|
||||||
);
|
|
||||||
browser.contextMenus.create(
|
|
||||||
{
|
|
||||||
contexts: ['all'],
|
|
||||||
documentUrlPatterns,
|
|
||||||
id: reportMenuItemId,
|
|
||||||
parentId: extensionMenuItemId,
|
|
||||||
title: browser.i18n.getMessage('contextMenu_reportOption'),
|
|
||||||
},
|
|
||||||
suppressLastError
|
|
||||||
);
|
|
||||||
|
|
||||||
if (details.reason === 'update') {
|
|
||||||
refreshData();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen to first start
|
|
||||||
*/
|
|
||||||
browser.runtime.onStartup.addListener(() => {
|
|
||||||
refreshData();
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen to the moment before a request is made to apply the rules
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
browser.webRequest.onBeforeRequest.addListener(
|
|
||||||
async (details) => {
|
|
||||||
const { tabId, type, url } = details;
|
|
||||||
|
|
||||||
if (tabId > -1 && type === 'main_frame') {
|
|
||||||
const { exclusions, rules } = await getData();
|
|
||||||
|
|
||||||
if (exclusions.domains.some((x) => location.hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostname = getHostname(url);
|
|
||||||
const state = await getState(hostname);
|
|
||||||
|
|
||||||
if (rules?.length) {
|
|
||||||
const rulesWithTabId = rules.map((rule) => ({
|
|
||||||
...rule,
|
|
||||||
condition: { ...rule.condition, tabIds: [tabId] },
|
|
||||||
}));
|
|
||||||
|
|
||||||
await browser.declarativeNetRequest.updateSessionRules({
|
|
||||||
addRules: state.on ? rulesWithTabId : undefined,
|
|
||||||
removeRuleIds: rules.map((rule) => rule.id),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ urls: ['<all_urls>'] }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen for errors on network requests
|
|
||||||
*/
|
|
||||||
browser.webRequest.onErrorOccurred.addListener(
|
|
||||||
async (details) => {
|
|
||||||
const { error, tabId } = details;
|
|
||||||
|
|
||||||
if (error === 'net::ERR_BLOCKED_BY_CLIENT' && tabId > -1) {
|
|
||||||
await browser.tabs.sendMessage(tabId, { type: 'INCREASE_ACTIONS_COUNT', value: error });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ urls: ['<all_urls>'] }
|
|
||||||
);
|
|
@ -1,519 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {Object} Action
|
|
||||||
* @property {string} domain
|
|
||||||
* @property {string} name
|
|
||||||
* @property {string} [property]
|
|
||||||
* @property {string} selector
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} ContentState
|
|
||||||
* @property {boolean} on
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} ExclusionMap
|
|
||||||
* @property {string[]} domains
|
|
||||||
* @property {string[]} overflows
|
|
||||||
* @property {string[]} tags
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} ExtensionData
|
|
||||||
* @property {Action[]} actions
|
|
||||||
* @property {ExclusionMap} exclusions
|
|
||||||
* @property {string[]} keywords
|
|
||||||
* @property {TokenMap} tokens
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} GetElementsParams
|
|
||||||
* @property {boolean} [filterEarly]
|
|
||||||
* @property {HTMLElement} [from]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} RunParams
|
|
||||||
* @property {HTMLElement[]} [containers]
|
|
||||||
* @property {HTMLElement[]} [elements]
|
|
||||||
* @property {boolean} [skipMatch]
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} TokenMap
|
|
||||||
* @property {string[]} backdrops
|
|
||||||
* @property {string[]} classes
|
|
||||||
* @property {string[]} containers
|
|
||||||
* @property {string[]} selectors
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} SetUpParams
|
|
||||||
* @property {boolean} [skipRunFn]
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (typeof browser === 'undefined') {
|
|
||||||
browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Class for request batching
|
|
||||||
*/
|
|
||||||
class NotifiableSet extends Set {
|
|
||||||
constructor(...args) {
|
|
||||||
super(...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
add(value) {
|
|
||||||
super.add(value);
|
|
||||||
browser.runtime.sendMessage({ type: 'UPDATE_BADGE', value: super.size });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Data object with all the necessary information
|
|
||||||
* @type {ExtensionData}
|
|
||||||
*/
|
|
||||||
let { actions, exclusions, keywords, tokens } = {
|
|
||||||
actions: [],
|
|
||||||
exclusions: {
|
|
||||||
domains: [],
|
|
||||||
overflows: [],
|
|
||||||
tags: [],
|
|
||||||
},
|
|
||||||
keywords: [],
|
|
||||||
tokens: {
|
|
||||||
backdrops: [],
|
|
||||||
classes: [],
|
|
||||||
selectors: [],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Shortcut to send messages to background script
|
|
||||||
*/
|
|
||||||
const dispatch = browser.runtime.sendMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Current hostname
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const hostname = getHostname();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Initial visibility state
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
let initiallyVisible = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Log of those steps done by the extension
|
|
||||||
* @type {NotifiableSet<string>}
|
|
||||||
*/
|
|
||||||
const log = new NotifiableSet();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Options provided to observer
|
|
||||||
* @type {MutationObserverInit}
|
|
||||||
*/
|
|
||||||
const options = { childList: true, subtree: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Elements that were already matched and are removable
|
|
||||||
* @type {Set<HTMLElement>}
|
|
||||||
*/
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Extension state
|
|
||||||
* @type {ContentState | undefined}
|
|
||||||
*/
|
|
||||||
let state = undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Clean DOM
|
|
||||||
* @param {Element[]} elements
|
|
||||||
* @param {boolean} [skipMatch]
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function clean(elements, skipMatch) {
|
|
||||||
let index = 0;
|
|
||||||
const size = 50;
|
|
||||||
|
|
||||||
function chunk() {
|
|
||||||
const end = Math.min(index + size, elements.length);
|
|
||||||
|
|
||||||
for (; index < end; index++) {
|
|
||||||
const element = elements[index];
|
|
||||||
|
|
||||||
if (match(element, skipMatch)) {
|
|
||||||
if (element instanceof HTMLDialogElement) element.close();
|
|
||||||
hide(element);
|
|
||||||
|
|
||||||
log.add(`${Date.now()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
seen.add(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < elements.length) {
|
|
||||||
requestAnimationFrame(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAnimationFrame(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Check if element contains a keyword
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
*/
|
|
||||||
function hasKeyword(element) {
|
|
||||||
return !!keywords?.length && !!element.outerHTML.match(new RegExp(keywords.join('|')));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Force a DOM clean in the specific element
|
|
||||||
* @param {HTMLElement} from
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function forceClean(from) {
|
|
||||||
const elements = getElements(tokens.selectors, { filterEarly: true, from });
|
|
||||||
|
|
||||||
if (elements.length) {
|
|
||||||
fix();
|
|
||||||
clean(elements, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all elements that match the selector
|
|
||||||
* @param {string | string[]} [selector]
|
|
||||||
* @param {GetElementsParams} [params]
|
|
||||||
* @returns {HTMLElement[]}
|
|
||||||
*/
|
|
||||||
function getElements(selector, params = {}) {
|
|
||||||
const { filterEarly, from } = params;
|
|
||||||
let result = [];
|
|
||||||
|
|
||||||
if (selector?.length) {
|
|
||||||
result = [...(from ?? document).querySelectorAll(selector)];
|
|
||||||
|
|
||||||
if (filterEarly) {
|
|
||||||
result = result.flatMap((node) => filterNodeEarly(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all elements with their children that match the selector
|
|
||||||
* @param {string | string[]} selector
|
|
||||||
* @param {GetElementsParams} [params]
|
|
||||||
* @returns {HTMLElement[]}
|
|
||||||
*/
|
|
||||||
function getElementsWithChildren(selector, params) {
|
|
||||||
return getElements(selector, params).flatMap((element) => [element, ...element.children]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Calculate current hostname
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
function getHostname() {
|
|
||||||
let hostname = document.location.hostname;
|
|
||||||
const referrer = document.referrer;
|
|
||||||
|
|
||||||
if (referrer && window.self !== window.top) {
|
|
||||||
hostname = new URL(referrer).hostname;
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostname.split('.').slice(-3).join('.').replace('www.', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Run if the page wasn't visited yet
|
|
||||||
* @param {Object} message
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
function handleRuntimeMessage(message) {
|
|
||||||
switch (message.type) {
|
|
||||||
case 'INCREASE_ACTIONS_COUNT': {
|
|
||||||
log.add(message.value);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Check if an element is visible in the viewport
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function isInViewport(element) {
|
|
||||||
const styles = window.getComputedStyle(element);
|
|
||||||
const height = window.innerHeight || document.documentElement.clientHeight;
|
|
||||||
const position = element.getBoundingClientRect();
|
|
||||||
const scroll = window.scrollY;
|
|
||||||
|
|
||||||
return (
|
|
||||||
position.bottom === position.top ||
|
|
||||||
(scroll + position.top <= scroll + height && scroll + position.bottom >= scroll) ||
|
|
||||||
styles.animationDuration !== '0s' ||
|
|
||||||
styles.transitionDuration !== '0s'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Check if element element is removable
|
|
||||||
* @param {Element} element
|
|
||||||
* @param {boolean} [skipMatch]
|
|
||||||
* @returns {boolean}
|
|
||||||
*/
|
|
||||||
function match(element, skipMatch) {
|
|
||||||
if (!exclusions.tags.length || !tokens.selectors.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(element instanceof HTMLElement) || !element.tagName) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seen.has(element)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagName = element.tagName.toUpperCase();
|
|
||||||
|
|
||||||
if (exclusions.tags.includes(tagName)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAttributes = !!element.getAttributeNames().filter((x) => x !== 'data-nosnippet').length;
|
|
||||||
|
|
||||||
if (!hasAttributes && !tagName.includes('-')) {
|
|
||||||
forceClean(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2023-06-10: fix #113 temporarily
|
|
||||||
if (element.classList.contains('chat-line__message')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2024-08-03: fix #701 temporarily
|
|
||||||
if (element.classList.contains('sellos')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isDialog = tagName === 'DIALOG' && element.getAttribute('open') === 'true';
|
|
||||||
const isFakeDialog = tagName === 'DIV' && element.className.includes('cmp');
|
|
||||||
|
|
||||||
return (
|
|
||||||
(isDialog || isFakeDialog || isInViewport(element)) &&
|
|
||||||
(skipMatch || element.matches(tokens.selectors))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Filter early nodes
|
|
||||||
* @param {Node} node
|
|
||||||
* @param {boolean} stopRecursion
|
|
||||||
* @returns {HTMLElement[]}
|
|
||||||
*/
|
|
||||||
function filterNodeEarly(node, stopRecursion) {
|
|
||||||
if (node.nodeType !== Node.ELEMENT_NODE || !(node instanceof HTMLElement)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasKeyword(node) && !stopRecursion) {
|
|
||||||
return [node, ...[...node.children].flatMap((node) => filterNodeEarly(node, true))];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [node];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Fix specific cases
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function fix() {
|
|
||||||
for (const action of actions) {
|
|
||||||
const { domain, name, property, selector } = action;
|
|
||||||
|
|
||||||
if (hostname.match(domain.replaceAll(/\*/g, '[^ ]*'))) {
|
|
||||||
switch (name) {
|
|
||||||
case 'click': {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
|
|
||||||
element?.click();
|
|
||||||
log.add(name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'remove': {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
|
|
||||||
element?.style?.removeProperty(property);
|
|
||||||
log.add(name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'reload': {
|
|
||||||
window.location.reload();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'reset': {
|
|
||||||
const element = document.querySelector(selector);
|
|
||||||
|
|
||||||
element?.style?.setProperty(property, 'initial', 'important');
|
|
||||||
log.add(name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'resetAll': {
|
|
||||||
const elements = getElements(selector);
|
|
||||||
|
|
||||||
elements.forEach((e) => e?.style?.setProperty(property, 'initial', 'important'));
|
|
||||||
log.add(name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const backdrops = getElements(tokens.backdrops);
|
|
||||||
|
|
||||||
for (const backdrop of backdrops) {
|
|
||||||
if (backdrop.children.length === 0 && !seen.has(backdrop)) {
|
|
||||||
log.add(`${Date.now()}`);
|
|
||||||
seen.add(backdrop);
|
|
||||||
hide(backdrop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const skips = exclusions.overflows.map((x) => (x.split('.').length < 3 ? `*${x}` : x));
|
|
||||||
|
|
||||||
if (!skips.some((x) => hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) {
|
|
||||||
for (const element of [document.body, document.documentElement]) {
|
|
||||||
element?.classList.remove(...(tokens.classes ?? []));
|
|
||||||
element?.style.setProperty('position', 'initial', 'important');
|
|
||||||
element?.style.setProperty('overflow-y', 'initial', 'important');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ionRouterOutlet = document.getElementsByTagName('ion-router-outlet')[0];
|
|
||||||
|
|
||||||
if (ionRouterOutlet) {
|
|
||||||
// 2024-08-02: fix #644 temporarily
|
|
||||||
ionRouterOutlet.removeAttribute('inert');
|
|
||||||
log.add('ion-router-outlet');
|
|
||||||
}
|
|
||||||
|
|
||||||
const t4Wrapper = document.getElementsByClassName('t4-wrapper')[0];
|
|
||||||
|
|
||||||
if (t4Wrapper) {
|
|
||||||
log.add('t4-wrapper');
|
|
||||||
// 2024-09-12: fix #945 temporarily
|
|
||||||
t4Wrapper.removeAttribute('inert');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Hide DOM element
|
|
||||||
* @param {HTMLElement} element
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function hide(element) {
|
|
||||||
element.style.setProperty('clip-path', 'circle(0px)', 'important');
|
|
||||||
element.style.setProperty('display', 'none', 'important');
|
|
||||||
element.style.setProperty('height', '0px', 'important');
|
|
||||||
element.style.setProperty('overflow', 'hidden', 'important');
|
|
||||||
element.style.setProperty('transform', 'scale(0)', 'important');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Clean DOM when this function is called
|
|
||||||
* @param {RunParams} [params]
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function run(params = {}) {
|
|
||||||
const { containers, elements, skipMatch } = params;
|
|
||||||
|
|
||||||
if (document.body?.children.length && state.on && tokens.selectors.length) {
|
|
||||||
fix();
|
|
||||||
|
|
||||||
if (elements?.length) {
|
|
||||||
clean(elements, skipMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (elements === undefined && containers?.length) {
|
|
||||||
clean(containers.flatMap((x) => getElementsWithChildren(x, { filterEarly: true })));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Set up the extension
|
|
||||||
* @param {SetUpParams} [params]
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function setUp(params = {}) {
|
|
||||||
const data = await dispatch({ hostname, type: 'GET_DATA' });
|
|
||||||
|
|
||||||
exclusions = data?.exclusions ?? exclusions;
|
|
||||||
|
|
||||||
if (exclusions.domains.some((x) => location.hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) {
|
|
||||||
dispatch({ type: 'DISABLE_ICON' });
|
|
||||||
observer.disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
state = await dispatch({ hostname, type: 'GET_STATE' });
|
|
||||||
|
|
||||||
if (state.on) {
|
|
||||||
browser.runtime.onMessage.addListener(handleRuntimeMessage);
|
|
||||||
dispatch({ hostname, type: 'ENABLE_ICON' });
|
|
||||||
|
|
||||||
actions = data?.actions ?? actions;
|
|
||||||
keywords = data?.keywords ?? keywords;
|
|
||||||
tokens = data?.tokens ?? tokens;
|
|
||||||
|
|
||||||
observer.observe(document.body ?? document.documentElement, options);
|
|
||||||
if (!params.skipRunFn) run({ containers: tokens.containers });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Wait for the body to exist
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function setUpAfterWaitForBody() {
|
|
||||||
if (document.visibilityState === 'visible' && !initiallyVisible) {
|
|
||||||
if (document.body) {
|
|
||||||
initiallyVisible = true;
|
|
||||||
await setUp();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(setUpAfterWaitForBody, 50);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Mutation Observer instance
|
|
||||||
* @type {MutationObserver}
|
|
||||||
*/
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
if (!state.on || !tokens.selectors.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = mutations.flatMap((mutation) => [...mutation.addedNodes]);
|
|
||||||
const elements = nodes.flatMap((node) => filterNodeEarly(node));
|
|
||||||
|
|
||||||
run({ elements });
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', setUpAfterWaitForBody);
|
|
||||||
window.addEventListener('pageshow', setUpAfterWaitForBody);
|
|
||||||
setUpAfterWaitForBody();
|
|
@ -1,275 +0,0 @@
|
|||||||
if (typeof browser === 'undefined') {
|
|
||||||
browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Shortcut to send messages to background script
|
|
||||||
*/
|
|
||||||
const dispatch = browser.runtime.sendMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description RegExp for matching domains
|
|
||||||
*/
|
|
||||||
const domainRegExp = /^(?!-)[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z]{2,})+$/;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Exclusion list, URLs where the user prefers to disable the extension
|
|
||||||
* @type {string[]}
|
|
||||||
*/
|
|
||||||
let exclusionList = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Render exclusion items into exclusion list
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function createList() {
|
|
||||||
const emptyItemElement = document.getElementById('exclusion-list-item-empty');
|
|
||||||
const exclusionListElement = document.getElementById('exclusion-list');
|
|
||||||
const exclusionListItemTemplateElement = document.getElementById('exclusion-list-item-template');
|
|
||||||
|
|
||||||
Array.from(exclusionListElement.querySelectorAll('[data-value]')).forEach((exclusionItem) => {
|
|
||||||
exclusionItem.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (exclusionList.length) {
|
|
||||||
for (const exclusionValue of exclusionList) {
|
|
||||||
const ariaLabelOrTitle = `Delete ${exclusionValue}`;
|
|
||||||
const itemElement = exclusionListItemTemplateElement.cloneNode(true);
|
|
||||||
const deleteButtonElement = itemElement.getElementsByTagName('button')[0];
|
|
||||||
|
|
||||||
deleteButtonElement.addEventListener('click', handleDeleteClick);
|
|
||||||
deleteButtonElement.setAttribute('aria-label', ariaLabelOrTitle);
|
|
||||||
deleteButtonElement.setAttribute('title', ariaLabelOrTitle);
|
|
||||||
itemElement.removeAttribute('id');
|
|
||||||
itemElement.getElementsByTagName('span')[0].innerText = exclusionValue;
|
|
||||||
itemElement.setAttribute('data-value', exclusionValue);
|
|
||||||
itemElement.style.removeProperty('display');
|
|
||||||
exclusionListElement.appendChild(itemElement);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emptyItemElement.innerText = "You don't have any exclusions yet";
|
|
||||||
emptyItemElement.style.removeProperty('display');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Add a new item to the exclusion list
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleAddClick() {
|
|
||||||
const message = browser.i18n.getMessage('options_addPrompt');
|
|
||||||
const value = window.prompt(message)?.trim().replace('www.', '');
|
|
||||||
|
|
||||||
if (value && (domainRegExp.test(value) || value === 'localhost')) {
|
|
||||||
const filterInputElement = document.getElementById('filter-input');
|
|
||||||
const state = { on: false };
|
|
||||||
await dispatch({ hostname: value, state, type: 'UPDATE_STORE' });
|
|
||||||
|
|
||||||
exclusionList = [...new Set([...exclusionList, value])].sort();
|
|
||||||
createList();
|
|
||||||
updateList(filterInputElement.value.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Clear all items from the exclusion list
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleClearClick() {
|
|
||||||
const filterInputElement = document.getElementById('filter-input');
|
|
||||||
|
|
||||||
for (const exclusionValue of exclusionList) {
|
|
||||||
const state = { on: true };
|
|
||||||
await dispatch({ hostname: exclusionValue, state, type: 'UPDATE_STORE' });
|
|
||||||
}
|
|
||||||
|
|
||||||
exclusionList = [];
|
|
||||||
createList();
|
|
||||||
updateList(filterInputElement.value.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Setup handlers and items
|
|
||||||
*/
|
|
||||||
async function handleContentLoaded() {
|
|
||||||
exclusionList = await dispatch({ type: 'GET_EXCLUSION_LIST' });
|
|
||||||
createList();
|
|
||||||
|
|
||||||
const addButtonElement = document.getElementById('add-button');
|
|
||||||
addButtonElement.addEventListener('click', handleAddClick);
|
|
||||||
|
|
||||||
const clearButtonElement = document.getElementById('clear-button');
|
|
||||||
clearButtonElement.addEventListener('click', handleClearClick);
|
|
||||||
|
|
||||||
const exportButtonElement = document.getElementById('export-button');
|
|
||||||
exportButtonElement.addEventListener('click', handleExportClick);
|
|
||||||
|
|
||||||
const fileInputElement = document.getElementById('file-input');
|
|
||||||
fileInputElement.addEventListener('change', handleFileChange);
|
|
||||||
|
|
||||||
const filterInputElement = document.getElementById('filter-input');
|
|
||||||
filterInputElement.addEventListener('keydown', handleFilterKeyDown);
|
|
||||||
|
|
||||||
const importButtonElement = document.getElementById('import-button');
|
|
||||||
importButtonElement.addEventListener('click', handleImportClick);
|
|
||||||
|
|
||||||
translate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Delete the clicked element from the exclusion list
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleDeleteClick(event) {
|
|
||||||
const filterInputElement = document.getElementById('filter-input');
|
|
||||||
const { value } = event.currentTarget.parentElement.dataset;
|
|
||||||
const itemElement = document.querySelector(`[data-value="${value}"]`);
|
|
||||||
const state = { on: true };
|
|
||||||
|
|
||||||
await dispatch({ hostname: value, state, type: 'UPDATE_STORE' });
|
|
||||||
exclusionList = exclusionList.filter((exclusionValue) => exclusionValue !== value);
|
|
||||||
itemElement?.remove();
|
|
||||||
updateList(filterInputElement.value.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Export a file with the current exclusion list
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleExportClick() {
|
|
||||||
const anchor = document.createElement('a');
|
|
||||||
const now = new Date();
|
|
||||||
const day = now.getDate().toString().padStart(2, '0');
|
|
||||||
const month = now.getMonth().toString().padStart(2, '0');
|
|
||||||
const year = now.getUTCFullYear();
|
|
||||||
const text = exclusionList.join('\n');
|
|
||||||
const defaultTitle = `${year}${month}${day}`;
|
|
||||||
const customTitle = window.prompt('Enter a file name', defaultTitle);
|
|
||||||
const blob = new Blob([text], { type: 'octet/stream' });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
anchor.href = url;
|
|
||||||
anchor.download = `${customTitle || defaultTitle}.cdm`;
|
|
||||||
anchor.click();
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Process a file and send the updates
|
|
||||||
* @param {InputEvent} event
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleFileChange(event) {
|
|
||||||
const file = event.currentTarget.files[0];
|
|
||||||
const filterInputElement = document.getElementById('filter-input');
|
|
||||||
const reader = new FileReader();
|
|
||||||
|
|
||||||
reader.addEventListener('load', async (event) => {
|
|
||||||
const input = event.currentTarget.result.split('\n');
|
|
||||||
const exclusions = [];
|
|
||||||
|
|
||||||
for (let value of input) {
|
|
||||||
value = value.replace('www.', '');
|
|
||||||
|
|
||||||
if (value && (domainRegExp.test(value) || value === 'localhost')) {
|
|
||||||
const state = { on: false };
|
|
||||||
|
|
||||||
await dispatch({ hostname: value, state, type: 'UPDATE_STORE' });
|
|
||||||
exclusions.push(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
exclusionList = [...new Set([...exclusionList, ...exclusions])].sort();
|
|
||||||
createList();
|
|
||||||
updateList(filterInputElement.value.trim());
|
|
||||||
});
|
|
||||||
|
|
||||||
event.currentTarget.value = '';
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Apply filter to the exclusion list when the user presses ENTER key
|
|
||||||
* @param {KeyboardEvent} event
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleFilterKeyDown(event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
const filterValue = event.currentTarget.value.trim();
|
|
||||||
updateList(filterValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Shallow click an hidden input to open the file explorer
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleImportClick() {
|
|
||||||
const fileInputElement = document.getElementById('file-input');
|
|
||||||
fileInputElement.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Apply translations to tags with i18n data attribute
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function translate() {
|
|
||||||
const nodes = document.querySelectorAll('[data-i18n], [data-i18n-placeholder]');
|
|
||||||
|
|
||||||
for (let i = nodes.length; i--; ) {
|
|
||||||
const node = nodes[i];
|
|
||||||
const { i18n, i18nPlaceholder } = node.dataset;
|
|
||||||
|
|
||||||
if (i18n) {
|
|
||||||
node.innerHTML = browser.i18n.getMessage(i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i18nPlaceholder) {
|
|
||||||
node.setAttribute('placeholder', browser.i18n.getMessage(i18nPlaceholder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Update exclusion items in DOM
|
|
||||||
* @param {string | undefined} filterValue
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function updateList(filterValue) {
|
|
||||||
const emptyItemElement = document.getElementById('exclusion-list-item-empty');
|
|
||||||
const exclusionListElement = document.getElementById('exclusion-list');
|
|
||||||
const exclusionListElements = exclusionListElement.querySelectorAll(`[data-value]`);
|
|
||||||
|
|
||||||
if (exclusionListElements.length) {
|
|
||||||
let isEmpty = true;
|
|
||||||
emptyItemElement.style.setProperty('display', 'none');
|
|
||||||
|
|
||||||
for (const exclusionItemElement of Array.from(exclusionListElements)) {
|
|
||||||
if (exclusionItemElement.matches(`[data-value*="${filterValue}"]`) || !filterValue) {
|
|
||||||
exclusionItemElement.style.removeProperty('display');
|
|
||||||
isEmpty = false;
|
|
||||||
} else {
|
|
||||||
exclusionItemElement.style.setProperty('display', 'none');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEmpty) {
|
|
||||||
emptyItemElement.innerText = 'No exclusions found';
|
|
||||||
emptyItemElement.style.removeProperty('display');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emptyItemElement.innerText = "You don't have any exclusions yet";
|
|
||||||
emptyItemElement.style.removeProperty('display');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen to document ready
|
|
||||||
* @listens document#DOMContentLoaded
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', handleContentLoaded);
|
|
@ -1,425 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {Object} ExtensionState
|
|
||||||
* @property {ExtensionIssue} [issue]
|
|
||||||
* @property {boolean} on
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @typedef {Object} PopupState
|
|
||||||
* @extends {ExtensionState}
|
|
||||||
* @property {number} [tabId]
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (typeof browser === 'undefined') {
|
|
||||||
browser = chrome;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Chrome Web Store link
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const chromeUrl = 'https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Shortcut to send messages to background script
|
|
||||||
*/
|
|
||||||
const dispatch = browser.runtime.sendMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Edge Add-ons link
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const edgeUrl =
|
|
||||||
'https://microsoftedge.microsoft.com/addons/detail/hbogodfciblakeneadpcolhmfckmjcii';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Firefox Add-ons link
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
const firefoxUrl = 'https://addons.mozilla.org/firefox/addon/cookie-dialog-monster';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Current hostname
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
let hostname = '?';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Is current browser an instance of Chromium?
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
const isChromium = navigator.userAgent.indexOf('Chrome') !== -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Is current browser an instance of Edge?
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
const isEdge = navigator.userAgent.indexOf('Edg') !== -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Is current browser an instance of Firefox?
|
|
||||||
* @type {boolean}
|
|
||||||
*/
|
|
||||||
const isFirefox = navigator.userAgent.indexOf('Firefox') !== -1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Popup state
|
|
||||||
* @type {PopupState}
|
|
||||||
*/
|
|
||||||
let state = { on: true };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Close report form
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleCancelClick() {
|
|
||||||
const content = document.getElementsByClassName('content')[0];
|
|
||||||
const report = document.getElementsByClassName('report')[0];
|
|
||||||
|
|
||||||
if (content instanceof HTMLElement && report instanceof HTMLElement) {
|
|
||||||
content.style.removeProperty('display');
|
|
||||||
report.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Setup stars handlers and result message links
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleContentLoaded() {
|
|
||||||
const tab = await dispatch({ type: 'GET_TAB' });
|
|
||||||
const url = tab?.url ? new URL(tab.url) : undefined;
|
|
||||||
|
|
||||||
hostname = url?.hostname.split('.').slice(-3).join('.').replace('www.', '');
|
|
||||||
state = { ...((await dispatch({ hostname, type: 'GET_STATE' })) ?? state), tabId: tab?.id };
|
|
||||||
|
|
||||||
const hostTextElement = document.getElementById('host');
|
|
||||||
hostTextElement.innerText = hostname ?? 'unknown';
|
|
||||||
|
|
||||||
const contributeButtonElement = document.getElementById('contribute-option');
|
|
||||||
contributeButtonElement?.addEventListener('click', handleLinkRedirect);
|
|
||||||
|
|
||||||
const databaseRefreshButtonElement = document.getElementById('refresh-database-button');
|
|
||||||
databaseRefreshButtonElement?.addEventListener('click', handleDatabaseRefresh);
|
|
||||||
|
|
||||||
const extensionVersionElement = document.getElementById('extension-version');
|
|
||||||
extensionVersionElement.innerText = browser.runtime.getManifest().version;
|
|
||||||
|
|
||||||
const helpButtonElement = document.getElementById('help-button');
|
|
||||||
helpButtonElement?.addEventListener('click', handleLinkRedirect);
|
|
||||||
|
|
||||||
const rateButtonElement = document.getElementById('rate-option');
|
|
||||||
rateButtonElement?.addEventListener('click', handleLinkRedirect);
|
|
||||||
if (isEdge) rateButtonElement?.setAttribute('data-href', edgeUrl);
|
|
||||||
else if (isChromium) rateButtonElement?.setAttribute('data-href', chromeUrl);
|
|
||||||
else if (isFirefox) rateButtonElement?.setAttribute('data-href', firefoxUrl);
|
|
||||||
|
|
||||||
const settingsButtonElement = document.getElementById('settings-button');
|
|
||||||
settingsButtonElement?.addEventListener('click', handleSettingsClick);
|
|
||||||
|
|
||||||
translate();
|
|
||||||
await updateDatabaseVersion();
|
|
||||||
|
|
||||||
const { exclusions } = (await dispatch({ hostname, type: 'GET_DATA' })) ?? {};
|
|
||||||
const currentVersion = browser.runtime.getManifest().version;
|
|
||||||
const latestVersion = await dispatch({ type: 'GET_LATEST_VERSION' });
|
|
||||||
const updateAvailable = latestVersion && currentVersion !== latestVersion;
|
|
||||||
|
|
||||||
if (updateAvailable) {
|
|
||||||
const updateBanner = document.getElementById('update-banner');
|
|
||||||
updateBanner.removeAttribute('aria-hidden');
|
|
||||||
|
|
||||||
const updateBannerUrl = document.getElementById('update-banner-url');
|
|
||||||
updateBannerUrl.href += `/tag/${latestVersion}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loader = document.getElementById('loader');
|
|
||||||
loader.style.setProperty('display', 'none');
|
|
||||||
|
|
||||||
if (exclusions?.domains.some((x) => url.hostname.match(x.replaceAll(/\*/g, '[^ ]*')))) {
|
|
||||||
const supportBanner = document.getElementById('support-banner');
|
|
||||||
supportBanner.removeAttribute('aria-hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const powerButtonElement = document.getElementById('power-option');
|
|
||||||
powerButtonElement?.addEventListener('click', handlePowerToggle);
|
|
||||||
powerButtonElement?.removeAttribute('disabled');
|
|
||||||
if (state.on) powerButtonElement?.setAttribute('data-value', 'on');
|
|
||||||
else powerButtonElement?.setAttribute('data-value', 'off');
|
|
||||||
|
|
||||||
if (state.issue?.url) {
|
|
||||||
const issueBanner = document.getElementById('issue-banner');
|
|
||||||
issueBanner.removeAttribute('aria-hidden');
|
|
||||||
|
|
||||||
const issueBannerText = document.getElementById('issue-banner-text');
|
|
||||||
if (state.issue.flags.includes('wontfix'))
|
|
||||||
issueBannerText.innerText = browser.i18n.getMessage('popup_bannerIssueWontFix');
|
|
||||||
else issueBannerText.innerText = browser.i18n.getMessage('popup_bannerIssueOpen');
|
|
||||||
|
|
||||||
const issueBannerUrl = document.getElementById('issue-banner-url');
|
|
||||||
issueBannerUrl.setAttribute('href', state.issue.url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelButtonElement = document.getElementsByClassName('report-cancel-button')[0];
|
|
||||||
cancelButtonElement?.addEventListener('click', handleCancelClick);
|
|
||||||
|
|
||||||
const reasonInputElement = document.getElementById('report-input-reason');
|
|
||||||
reasonInputElement?.addEventListener('input', handleInputChange);
|
|
||||||
reasonInputElement?.addEventListener('keydown', handleInputKeyDown);
|
|
||||||
|
|
||||||
const reportButtonElement = document.getElementById('report-option');
|
|
||||||
reportButtonElement?.addEventListener('click', handleReportClick);
|
|
||||||
reportButtonElement?.removeAttribute('disabled');
|
|
||||||
|
|
||||||
const submitButtonElement = document.getElementsByClassName('report-submit-button')[0];
|
|
||||||
submitButtonElement?.addEventListener('click', handleSubmitButtonClick);
|
|
||||||
|
|
||||||
const urlInputElement = document.getElementById('report-input-url');
|
|
||||||
urlInputElement?.addEventListener('input', handleInputChange);
|
|
||||||
urlInputElement?.addEventListener('keydown', handleInputKeyDown);
|
|
||||||
if (url) urlInputElement?.setAttribute('value', `${url.origin}${url.pathname}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Refresh the database
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
*/
|
|
||||||
async function handleDatabaseRefresh(event) {
|
|
||||||
const target = event.currentTarget;
|
|
||||||
|
|
||||||
if (target.getAttribute('aria-disabled') === 'true') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkIcon = target.querySelector('#refresh-database-check');
|
|
||||||
const spinnerIcon = target.querySelector('#refresh-database-spinner');
|
|
||||||
|
|
||||||
target.setAttribute('data-refreshing', 'true');
|
|
||||||
target.setAttribute('aria-disabled', 'true');
|
|
||||||
await dispatch({ type: 'REFRESH_DATA' });
|
|
||||||
checkIcon.style.setProperty('display', 'block');
|
|
||||||
spinnerIcon.style.setProperty('display', 'none');
|
|
||||||
target.removeAttribute('data-animation');
|
|
||||||
target.removeAttribute('data-refreshing');
|
|
||||||
await updateDatabaseVersion();
|
|
||||||
|
|
||||||
window.setTimeout(() => {
|
|
||||||
checkIcon.style.setProperty('display', 'none');
|
|
||||||
spinnerIcon.style.setProperty('display', 'block');
|
|
||||||
target.removeAttribute('aria-disabled');
|
|
||||||
target.setAttribute('data-animation', 'flip');
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Input change handler
|
|
||||||
* @param {InputEvent} event
|
|
||||||
*/
|
|
||||||
function handleInputChange(event) {
|
|
||||||
event.currentTarget.removeAttribute('aria-invalid');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Input key down handler
|
|
||||||
* @param {KeyboardEvent} event
|
|
||||||
*/
|
|
||||||
function handleInputKeyDown(event) {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
event.preventDefault();
|
|
||||||
event.currentTarget.blur();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Open a new tab
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleLinkRedirect(event) {
|
|
||||||
const { href } = event.currentTarget.dataset;
|
|
||||||
|
|
||||||
if (href) {
|
|
||||||
await browser.tabs.create({ url: href });
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Disable or enable extension on current page
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
async function handlePowerToggle() {
|
|
||||||
const next = { ...state, on: !state.on };
|
|
||||||
|
|
||||||
await dispatch({ hostname, state: next, type: 'UPDATE_STORE' });
|
|
||||||
await browser.tabs.reload(state.tabId, { bypassCache: true });
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Show report form
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function handleReportClick() {
|
|
||||||
const content = document.getElementsByClassName('content')[0];
|
|
||||||
const report = document.getElementsByClassName('report')[0];
|
|
||||||
|
|
||||||
if (content instanceof HTMLElement && report instanceof HTMLElement) {
|
|
||||||
content.style.display = 'none';
|
|
||||||
report.style.removeProperty('display');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Open options page
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function handleSettingsClick() {
|
|
||||||
await browser.runtime.openOptionsPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Report submit button click handler
|
|
||||||
* @param {MouseEvent} event
|
|
||||||
*/
|
|
||||||
async function handleSubmitButtonClick(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (event.currentTarget.getAttribute('aria-disabled') === 'true') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.currentTarget.setAttribute('aria-disabled', 'true');
|
|
||||||
|
|
||||||
const reasonInput = document.getElementById('report-input-reason');
|
|
||||||
const reasonText = reasonInput?.value.trim();
|
|
||||||
const urlInput = document.getElementById('report-input-url');
|
|
||||||
const urlText = urlInput?.value.trim();
|
|
||||||
const errors = validateForm({ reason: reasonText, url: urlText });
|
|
||||||
|
|
||||||
if (errors) {
|
|
||||||
if (errors.reason) {
|
|
||||||
reasonInput?.setAttribute('aria-invalid', 'true');
|
|
||||||
reasonInput?.setAttribute('aria-errormessage', 'report-input-reason-error');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.url) {
|
|
||||||
urlInput?.setAttribute('aria-invalid', 'true');
|
|
||||||
urlInput?.setAttribute('aria-errormessage', 'report-input-url-error');
|
|
||||||
}
|
|
||||||
|
|
||||||
event.currentTarget.setAttribute('aria-disabled', 'false');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const issueButtons = document.getElementsByClassName('report-issue-button');
|
|
||||||
const formView = document.getElementsByClassName('report-form-view')[0];
|
|
||||||
const userAgent = window.navigator.userAgent;
|
|
||||||
const response = await dispatch({ userAgent, reason: reasonText, url: urlText, type: 'REPORT' });
|
|
||||||
const hostname = new URL(urlText).hostname.split('.').slice(-3).join('.').replace('www.', '');
|
|
||||||
const issue = { expiresIn: Date.now() + 8 * 60 * 60 * 1000, flags: ['bug'], url: response.data };
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
const successView = document.getElementsByClassName('report-submit-success-view')[0];
|
|
||||||
|
|
||||||
await dispatch({ hostname, state: { issue }, type: 'UPDATE_STORE' });
|
|
||||||
await dispatch({ hostname, type: 'ENABLE_ICON' });
|
|
||||||
formView?.setAttribute('hidden', 'true');
|
|
||||||
issueButtons[1]?.addEventListener('click', () => window.open(response.data, '_blank'));
|
|
||||||
successView?.removeAttribute('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data) {
|
|
||||||
const errorView = document.getElementsByClassName('report-submit-error-view')[0];
|
|
||||||
|
|
||||||
if (response.errors?.some((error) => error.includes('wontfix'))) {
|
|
||||||
issue.flags.push('wontfix');
|
|
||||||
}
|
|
||||||
|
|
||||||
await dispatch({ hostname, state: { issue }, type: 'UPDATE_STORE' });
|
|
||||||
errorView?.removeAttribute('hidden');
|
|
||||||
formView?.setAttribute('hidden', 'true');
|
|
||||||
issueButtons[0]?.addEventListener('click', () => window.open(response.data, '_blank'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Apply translations to tags with i18n data attribute
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
function translate() {
|
|
||||||
const nodes = document.querySelectorAll('[data-i18n], [data-i18n-placeholder]');
|
|
||||||
|
|
||||||
for (let i = nodes.length; i--; ) {
|
|
||||||
const node = nodes[i];
|
|
||||||
const { i18n, i18nPlaceholder } = node.dataset;
|
|
||||||
|
|
||||||
if (i18n) {
|
|
||||||
node.innerHTML = browser.i18n.getMessage(i18n);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i18nPlaceholder) {
|
|
||||||
node.setAttribute('placeholder', browser.i18n.getMessage(i18nPlaceholder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @async
|
|
||||||
* @description Update the database version element
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async function updateDatabaseVersion() {
|
|
||||||
const data = await dispatch({ hostname, type: 'GET_DATA' });
|
|
||||||
const databaseVersionElement = document.getElementById('database-version');
|
|
||||||
|
|
||||||
if (data.version) databaseVersionElement.innerText = data.version;
|
|
||||||
else databaseVersionElement.style.setProperty('display', 'none');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validate form
|
|
||||||
* @param {{ reason: string | undefined | undefined, url: string | undefined }} params
|
|
||||||
* @returns {{ reason: string | undefined, url: string | undefined } | undefined}
|
|
||||||
*/
|
|
||||||
function validateForm(params) {
|
|
||||||
const { reason, url } = params;
|
|
||||||
let errors = undefined;
|
|
||||||
|
|
||||||
if (!reason || reason.length < 10 || reason.length > 1000) {
|
|
||||||
errors = {
|
|
||||||
...(errors ?? {}),
|
|
||||||
reason: browser.i18n.getMessage('report_reasonInputError'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (/\s/.test(url)) throw new Error();
|
|
||||||
new URL(url);
|
|
||||||
} catch {
|
|
||||||
errors = {
|
|
||||||
...(errors ?? {}),
|
|
||||||
url: browser.i18n.getMessage('report_urlInputError'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Listen to document ready
|
|
||||||
* @listens document#DOMContentLoaded
|
|
||||||
*/
|
|
||||||
document.addEventListener('DOMContentLoaded', handleContentLoaded);
|
|
15
packages/browser-extension/src/styles/common.css
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
:root {
|
||||||
|
--color-error: #cc0000;
|
||||||
|
--color-primary: #3dd9eb;
|
||||||
|
--color-secondary: #34495e;
|
||||||
|
--color-success: #5cb85c;
|
||||||
|
--color-tertiary: #6b7280;
|
||||||
|
--color-transparent: transparent;
|
||||||
|
--color-warning: #ffdf00;
|
||||||
|
--color-white: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body * {
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: Inter, Arial, Helvetica, sans-serif;
|
||||||
|
}
|
@ -1,29 +1,4 @@
|
|||||||
:root {
|
.button {
|
||||||
--color-error: #cc0000;
|
|
||||||
--color-primary: #3dd9eb;
|
|
||||||
--color-secondary: #34495e;
|
|
||||||
--color-success: #5cb85c;
|
|
||||||
--color-tertiary: #6b7280;
|
|
||||||
--color-transparent: transparent;
|
|
||||||
--color-warning: #ffdf00;
|
|
||||||
--color-white: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: var(--color-tertiary);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
font-family: Inter, Arial, Helvetica, sans-serif;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
body * {
|
|
||||||
box-sizing: border-box;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
border: none;
|
border: none;
|
||||||
@ -35,24 +10,24 @@ button {
|
|||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[data-variant='large'] {
|
.button:focus,
|
||||||
|
.button:hover {
|
||||||
|
background-color: var(--color-secondary);
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button[data-variant='large'] {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-device-width: 768px) {
|
@media only screen and (max-device-width: 768px) {
|
||||||
button[data-variant='large'] {
|
.button[data-variant='large'] {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button:focus,
|
.footer {
|
||||||
button:hover {
|
|
||||||
background-color: var(--color-secondary);
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
@ -60,14 +35,14 @@ footer {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
.header {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
header > div {
|
.header > div {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -75,7 +50,7 @@ header > div {
|
|||||||
margin: auto 0px;
|
margin: auto 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main input {
|
.input {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
@ -90,18 +65,18 @@ main input {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
main input::placeholder {
|
.input::placeholder {
|
||||||
color: var(--color-tertiary);
|
color: var(--color-tertiary);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
main input:focus,
|
.input:focus,
|
||||||
main input:hover {
|
.input:hover {
|
||||||
border-bottom: 1px solid var(--color-primary);
|
border-bottom: 1px solid var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
header > div,
|
.header > div,
|
||||||
main {
|
.main {
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
max-width: 768px;
|
max-width: 768px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@ -122,13 +97,13 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#exclusion-list {
|
.exclusion-list {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#exclusion-list > li {
|
.exclusion-list > li {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -137,14 +112,23 @@ main {
|
|||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#exclusion-list > li:focus-within,
|
.exclusion-list > li:focus-within,
|
||||||
#exclusion-list > li:hover {
|
.exclusion-list > li:hover {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
#exclusion-list > li > button {
|
.exclusion-list > button {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#__plasmo {
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: var(--color-tertiary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-family: Inter, Arial, Helvetica, sans-serif;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
@ -1,17 +1,3 @@
|
|||||||
:root {
|
|
||||||
--color-error: #cc0000;
|
|
||||||
--color-primary: #3dd9eb;
|
|
||||||
--color-secondary: #34495e;
|
|
||||||
--color-success: #5cb85c;
|
|
||||||
--color-tertiary: #6b7280;
|
|
||||||
--color-warning: #ffdf00;
|
|
||||||
--color-white: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
b {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var(--color-tertiary);
|
color: var(--color-tertiary);
|
||||||
@ -25,12 +11,42 @@ body {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body * {
|
.banner {
|
||||||
box-sizing: border-box;
|
font-size: 12px;
|
||||||
font-family: Inter, Arial, Helvetica, sans-serif;
|
line-height: 16px;
|
||||||
|
margin: 0px;
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
.banner[data-variant='error'] {
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner[data-variant='notice'] {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: var(--color-white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner[data-variant='warning'] {
|
||||||
|
background-color: #f39c12;
|
||||||
|
color: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner a {
|
||||||
|
color: inherit;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@ -39,7 +55,7 @@ footer {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
header {
|
.header {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
@ -49,58 +65,11 @@ header {
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
& .header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-device-width: 768px) {
|
|
||||||
main {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 320px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 18px;
|
|
||||||
margin: 0px;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
&[aria-hidden='true'] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-variant='error'] {
|
|
||||||
background-color: #e74c3c;
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-variant='notice'] {
|
|
||||||
background-color: #2196f3;
|
|
||||||
color: var(--color-white);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-variant='warning'] {
|
|
||||||
background-color: #f39c12;
|
|
||||||
color: #c0392b;
|
|
||||||
}
|
|
||||||
|
|
||||||
& a {
|
|
||||||
color: inherit;
|
|
||||||
display: inline-block;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-button {
|
.header-button {
|
||||||
@ -114,11 +83,19 @@ header {
|
|||||||
outline: none;
|
outline: none;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus:not(:disabled),
|
.header-button:focus,
|
||||||
&:hover:not(:disabled) {
|
.header-button:hover {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-device-width: 768px) {
|
||||||
|
.main {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 320px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,32 +113,33 @@ header {
|
|||||||
outline: none;
|
outline: none;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus,
|
.popup-button:focus,
|
||||||
&:hover {
|
.popup-button:hover {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
|
||||||
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
.popup-button:disabled {
|
||||||
background-color: var(--color-tertiary);
|
background-color: var(--color-tertiary);
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
& span {
|
.popup-button span {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
& svg {
|
.popup-button svg {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-data {
|
.popup-data {
|
||||||
@ -170,10 +148,10 @@ header {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
.popup-data:not(:first-child) {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup-data-button {
|
.popup-data-button {
|
||||||
@ -182,19 +160,20 @@ header {
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: 0.4s;
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
&[aria-disabled='true'] {
|
.popup-data-button:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-animation='flip']:focus,
|
.popup-data-button[data-animation='flip']:focus:not([data-refreshed='true']),
|
||||||
&[data-animation='flip']:hover {
|
.popup-data-button[data-animation='flip']:hover:not([data-refreshed='true']) {
|
||||||
transform: rotate(-180deg);
|
transform: rotate(-180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-refreshing='true'] {
|
.popup-data-button[data-refreshing='true'] {
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 1s linear infinite;
|
||||||
}
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
@ -211,106 +190,44 @@ header {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.power-option {
|
||||||
|
color: var(--color-white);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-option[data-value='off']:not(:disabled) {
|
||||||
|
background-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.power-option[data-value='on']:not(:disabled) {
|
||||||
|
background-color: var(--color-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-option > svg {
|
||||||
|
transition: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-option:focus > svg,
|
||||||
|
.rate-option:hover > svg {
|
||||||
|
color: var(--color-warning);
|
||||||
|
fill: var(--color-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-database-check {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.report {
|
.report {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
& .report-buttons {
|
.report-buttons {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-form {
|
.report-cancel-button {
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-form-view {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-form-view[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input {
|
|
||||||
all: unset;
|
|
||||||
border: 1px solid var(--color-tertiary);
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--color-secondary);
|
|
||||||
cursor: text;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1;
|
|
||||||
outline: none;
|
|
||||||
padding: 12px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input:hover {
|
|
||||||
border-color: var(--color-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input:focus {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input:focus-visible {
|
|
||||||
box-shadow: initial;
|
|
||||||
transition: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input[aria-invalid='true'] {
|
|
||||||
border-color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input[aria-invalid='true'] + .report-input-error {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input[aria-multiline='false'] {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
display: flex;
|
|
||||||
height: 40px;
|
|
||||||
overflow-x: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input[aria-multiline='true'] {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input-error {
|
|
||||||
color: var(--color-error);
|
|
||||||
display: none;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input-group {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input-label {
|
|
||||||
color: var(--color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-input-label-required {
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-cancel-button {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
@ -325,15 +242,91 @@ header {
|
|||||||
outline: none;
|
outline: none;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-cancel-button:focus,
|
.report-cancel-button:focus,
|
||||||
& .report-cancel-button:hover {
|
.report-cancel-button:hover {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-issue-button,
|
.report-form {
|
||||||
& .report-submit-button {
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-form-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input {
|
||||||
|
all: unset;
|
||||||
|
border: 1px solid var(--color-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
cursor: text;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
outline: none;
|
||||||
|
padding: 12px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input[aria-invalid='true']:not(:disabled) {
|
||||||
|
border-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input[aria-invalid='true']:not(:disabled) + .report-input-error {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input:hover {
|
||||||
|
border-color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input:focus {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input:focus-visible {
|
||||||
|
box-shadow: initial;
|
||||||
|
transition: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input:disabled {
|
||||||
|
border-color: var(--color-tertiary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input-error {
|
||||||
|
color: var(--color-error);
|
||||||
|
display: none;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input-group {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input-label {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-input-label-required {
|
||||||
|
color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.report-issue-button,
|
||||||
|
.report-submit-button {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--color-secondary);
|
background-color: var(--color-secondary);
|
||||||
border: 1px solid var(--color-secondary);
|
border: 1px solid var(--color-secondary);
|
||||||
@ -350,115 +343,50 @@ header {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-issue-button:focus,
|
.report-issue-button:focus,
|
||||||
& .report-issue-button:hover,
|
.report-submit-button:focus,
|
||||||
& .report-submit-button:focus,
|
.report-issue-button:hover,
|
||||||
& .report-submit-button:hover {
|
.report-submit-button:hover {
|
||||||
background-color: var(--color-white);
|
background-color: var(--color-white);
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-issue-button:focus-visible,
|
.report-issue-button:focus-visible,
|
||||||
& .report-submit-button:focus-visible {
|
.report-submit-button:focus-visible {
|
||||||
box-shadow: initial;
|
box-shadow: initial;
|
||||||
transition: initial;
|
transition: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-issue-button[aria-disabled='true'],
|
.report-issue-button:disabled,
|
||||||
& .report-submit-button[aria-disabled='true'] {
|
.report-submit-button:disabled {
|
||||||
background-color: var(--color-tertiary);
|
background-color: var(--color-tertiary);
|
||||||
border: 1px solid var(--color-tertiary);
|
border: 1px solid var(--color-tertiary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .report-submit-extra-text {
|
.report-submit-error-view,
|
||||||
font-size: 14px;
|
.report-submit-success-view {
|
||||||
line-height: 18px;
|
|
||||||
margin: 0px;
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-submit-text {
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 22px;
|
|
||||||
margin: 0px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .report-submit-error-view,
|
|
||||||
& .report-submit-success-view {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
|
||||||
|
|
||||||
& .report-submit-error-view[hidden],
|
|
||||||
& .report-submit-success-view[hidden] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#loader {
|
.report-submit-extra-text {
|
||||||
align-items: center;
|
font-size: 14px;
|
||||||
background-color: var(--color-white);
|
line-height: 18px;
|
||||||
bottom: 0px;
|
margin: 0px;
|
||||||
display: flex;
|
text-align: justify;
|
||||||
justify-content: center;
|
|
||||||
left: 0px;
|
|
||||||
position: fixed;
|
|
||||||
right: 0px;
|
|
||||||
top: 0px;
|
|
||||||
|
|
||||||
& > span {
|
|
||||||
animation: rotation 1s linear infinite;
|
|
||||||
border-bottom-color: var(--color-primary) !important;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 6px solid var(--color-secondary);
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-block;
|
|
||||||
height: 48px;
|
|
||||||
width: 48px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rotation {
|
.report-submit-text {
|
||||||
0% {
|
font-size: 18px;
|
||||||
transform: rotate(0deg);
|
line-height: 22px;
|
||||||
}
|
margin: 0px;
|
||||||
100% {
|
text-align: center;
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#power-option {
|
|
||||||
color: var(--color-white);
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
&[data-value='off'] {
|
|
||||||
background-color: var(--color-error);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-value='on'] {
|
|
||||||
background-color: var(--color-success);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#rate-option > svg {
|
|
||||||
transition: 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#rate-option:focus > svg,
|
|
||||||
#rate-option:hover > svg {
|
|
||||||
color: var(--color-warning);
|
|
||||||
fill: var(--color-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
#refresh-database-check {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
|
40
packages/browser-extension/src/utils/constants.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import type { DomainConfig, ExtensionData } from './types';
|
||||||
|
|
||||||
|
export const API_URL = 'https://api.cookie-dialog-monster.com/rest/v6';
|
||||||
|
|
||||||
|
export const CHROME_STORE_URL =
|
||||||
|
'https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg';
|
||||||
|
|
||||||
|
export const DEFAULT_DOMAIN_CONFIG: DomainConfig = {
|
||||||
|
issue: undefined,
|
||||||
|
on: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_EXTENSION_DATA: ExtensionData = {
|
||||||
|
actions: [],
|
||||||
|
exclusions: {
|
||||||
|
domains: [],
|
||||||
|
overflows: [],
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
keywords: [],
|
||||||
|
rules: [],
|
||||||
|
tokens: {
|
||||||
|
backdrops: [],
|
||||||
|
classes: [],
|
||||||
|
containers: [],
|
||||||
|
selectors: [],
|
||||||
|
},
|
||||||
|
version: '?.?.?',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EDGE_STORE_URL =
|
||||||
|
'https://microsoftedge.microsoft.com/addons/detail/hbogodfciblakeneadpcolhmfckmjcii';
|
||||||
|
|
||||||
|
export const EXTENSION_MENU_ITEM_ID = 'CDM-MENU';
|
||||||
|
|
||||||
|
export const FIREFOX_STORE_URL = 'https://addons.mozilla.org/firefox/addon/cookie-dialog-monster';
|
||||||
|
|
||||||
|
export const REPORT_MENU_ITEM_ID = 'CDM-REPORT';
|
||||||
|
|
||||||
|
export const SETTINGS_MENU_ITEM_ID = 'CDM-SETTINGS';
|
15
packages/browser-extension/src/utils/domain.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
export const DOMAIN_REG_EXP = /^(?!-)[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z]{2,})+$/;
|
||||||
|
|
||||||
|
export function formatDomainFromURL(value: URL): string {
|
||||||
|
let result: string = value.hostname;
|
||||||
|
|
||||||
|
if (result.startsWith('www.')) {
|
||||||
|
result = result.replace('www.', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateSupport(hostname: string, exclusions: readonly string[]): boolean {
|
||||||
|
return !exclusions.some((x) => hostname.match(x.replaceAll(/\*/g, '[^ ]*')));
|
||||||
|
}
|
3
packages/browser-extension/src/utils/error.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const noop = () => undefined;
|
||||||
|
|
||||||
|
export const suppressLastError = () => void chrome.runtime.lastError;
|
3
packages/browser-extension/src/utils/storage.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { Storage } from '@plasmohq/storage';
|
||||||
|
|
||||||
|
export const storage = new Storage({ area: 'local' });
|
49
packages/browser-extension/src/utils/types.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
export interface Action {
|
||||||
|
readonly domain: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly property?: string;
|
||||||
|
readonly selector: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainConfig {
|
||||||
|
readonly issue?: DomainIssue;
|
||||||
|
readonly on: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DomainIssue {
|
||||||
|
readonly expiresAt?: number;
|
||||||
|
readonly flags?: readonly string[];
|
||||||
|
readonly url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExclusionMap {
|
||||||
|
readonly domains: readonly string[];
|
||||||
|
readonly overflows: readonly string[];
|
||||||
|
readonly tags: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionData {
|
||||||
|
readonly actions: readonly Action[];
|
||||||
|
readonly exclusions: ExclusionMap;
|
||||||
|
readonly keywords: readonly string[];
|
||||||
|
readonly rules: readonly chrome.declarativeNetRequest.Rule[];
|
||||||
|
readonly tokens: TokenMap;
|
||||||
|
readonly version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportParams {
|
||||||
|
readonly reason: string;
|
||||||
|
readonly url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportResult {
|
||||||
|
readonly data: string;
|
||||||
|
readonly success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TokenMap {
|
||||||
|
readonly backdrops: readonly string[];
|
||||||
|
readonly classes: readonly string[];
|
||||||
|
readonly containers: readonly string[];
|
||||||
|
readonly selectors: readonly string[];
|
||||||
|
}
|
3
packages/browser-extension/tests/contents/index.test.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { describe } from '@jest/globals';
|
||||||
|
|
||||||
|
describe('contents/index.ts', () => {});
|
13
packages/browser-extension/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "plasmo/templates/tsconfig.base",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"noPropertyAccessFromIndexSignature": true,
|
||||||
|
"paths": {
|
||||||
|
"~*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": [".plasmo/index.d.ts", "./**/*.ts", "./**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
@ -14,6 +14,5 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20.x"
|
"node": "20.x"
|
||||||
},
|
}
|
||||||
"packageManager": "yarn@4.2.1"
|
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,11 @@ for file in $(find $input -name "*.css" -o -name "*.html" -o -name "*.js" | sed
|
|||||||
output_file="$output/$file"
|
output_file="$output/$file"
|
||||||
|
|
||||||
mkdir -p "${output_file%/*}" && touch "$output_file"
|
mkdir -p "${output_file%/*}" && touch "$output_file"
|
||||||
yarn minify $input_file > $output_file
|
pnpm minify $input_file > $output_file
|
||||||
done
|
done
|
||||||
|
|
||||||
yarn tailwindcss -i "$input/index.css" -o "$output/index.css"
|
pnpm tailwindcss -i "$input/index.css" -o "$output/index.css"
|
||||||
yarn minify "$output/index.css" > "$output/index-1.css"
|
pnpm minify "$output/index.css" > "$output/index-1.css"
|
||||||
rm -rf "$output/index.css"
|
rm -rf "$output/index.css"
|
||||||
mv "$output/index-1.css" "$output/index.css"
|
mv "$output/index-1.css" "$output/index.css"
|
||||||
cp -nR "$input/." $output
|
cp -nR "$input/." $output
|
||||||
|
12833
pnpm-lock.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
- "packages/**"
|