feat(browser-extension): use plasmo framework

This commit is contained in:
wanhose 2024-11-19 12:57:32 +01:00
parent 6434dd6c1c
commit edf5eda7dc
87 changed files with 15235 additions and 8773 deletions

4
.gitignore vendored
View File

@ -1,9 +1,7 @@
_metadata/
!.yarn/plugins
!.yarn/releases
.DS_Store
.env
.plasmo/
.pnp.*
.yarn/*
build/
node_modules/

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit "$1"
pnpm commitlint --edit "$1"

View File

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
pnpm lint-staged

View File

@ -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'"]
}

View File

@ -1,2 +0,0 @@
package.json
.yarnrc.yml

View File

@ -1,4 +1,12 @@
{
"overrides": [
{
"files": ["*.jsonc", ".eslintrc", "tsconfig*.json"],
"options": {
"trailingComma": "none"
}
}
],
"printWidth": 100,
"semi": true,
"singleQuote": true,

View File

@ -1,4 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "always"
},
"files.associations": {
".commitlintrc": "json",
".lintstagedrc": "json"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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

View File

@ -3,8 +3,9 @@
"private": true,
"version": "1.0.0",
"scripts": {
"build": "yarn workspaces foreach --all -p run build",
"lint": "yarn workspaces foreach --all -p run lint",
"build": "pnpm -r run build",
"lint": "pnpm -r run lint",
"preinstall": "npx only-allow pnpm",
"prepare": "husky"
},
"devDependencies": {
@ -14,12 +15,8 @@
"lint-staged": "^15.2.2",
"prettier": "^3.2.5"
},
"workspaces": [
"packages/*"
],
"engines": {
"node": "20.x"
},
"packageManager": "yarn@4.2.1",
"license": "MIT"
}

View File

@ -2,26 +2,26 @@
## 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
yarn install
pnpm i
```
## Scripts
### `yarn build`
### `pnpm build`
Removes the build directory and compiles TypeScript files.
### `yarn dev`
### `pnpm dev`
Starts the server in development mode with nodemon.
### `yarn lint`
### `pnpm lint`
Lints the codebase using ESLint.
### `yarn start`
### `pnpm start`
Starts the API server instance in production mode.

View File

@ -33,6 +33,5 @@
},
"engines": {
"node": "20.x"
},
"packageManager": "yarn@4.2.1"
}
}

View File

@ -1,5 +1,5 @@
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 { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
import { validatorCompiler } from 'services/validation';
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
async (request, reply) => {
try {
const { reason, url, userAgent, version } = request.body;
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
const issue = await getIssue({ title: hostname });
const domain = formatDomainFromURL(new URL(url));
const issue = await getIssue({ title: domain });
const ua = new UAParser(userAgent ?? '').getResult();
if (issue) {
@ -61,7 +61,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
await createIssue({
description: formatMessage({ reason, ua, url, version }),
labels: ['bug'],
title: hostname,
title: domain,
});
reply.send({

View File

@ -1,5 +1,5 @@
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 { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
import { validatorCompiler } from 'services/validation';
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
async (request, reply) => {
try {
const { reason, url, userAgent, version } = request.body;
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
const issue = await getIssue({ title: hostname });
const domain = formatDomainFromURL(new URL(url));
const issue = await getIssue({ title: domain });
const ua = new UAParser(userAgent ?? '').getResult();
if (issue) {
@ -62,7 +62,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
const newIssue = await createIssue({
description: formatMessage({ reason, ua, url, version }),
labels: ['bug'],
title: hostname,
title: domain,
});
reply.send({

View File

@ -5,14 +5,14 @@ import { validatorCompiler } from 'services/validation';
import * as yup from 'yup';
const GetIssuesParamsSchema = yup.object().shape({
hostname: yup.string().required(),
domain: yup.string().required(),
});
type GetIssuesParams = yup.InferType<typeof GetIssuesParamsSchema>;
export default (server: FastifyInstance, _options: RouteShorthandOptions, done: () => void) => {
server.get<{ Params: GetIssuesParams }>(
'/issues/:hostname/',
'/issues/:domain/',
{
config: {
rateLimit: RATE_LIMIT_10_PER_MIN,
@ -24,8 +24,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
},
async (request, reply) => {
try {
const { hostname } = request.params;
const issue = await getIssue({ title: hostname });
const { domain } = request.params;
const issue = await getIssue({ title: domain });
if (
issue &&

View File

@ -1,5 +1,5 @@
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
import { formatMessage } from 'services/format';
import { formatMessage, formatDomainFromURL } from 'services/format';
import { createIssue, getIssue, updateIssue } from 'services/git';
import { RATE_LIMIT_1_PER_MIN } from 'services/rateLimit';
import { validatorCompiler } from 'services/validation';
@ -34,8 +34,8 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
async (request, reply) => {
try {
const { reason, url, userAgent, version } = request.body;
const hostname = new URL(url).hostname.split('.').slice(-3).join('.').replace('www.', '');
const issue = await getIssue({ title: hostname });
const domain = formatDomainFromURL(new URL(url));
const issue = await getIssue({ title: domain });
const ua = new UAParser(userAgent ?? '').getResult();
if (issue) {
@ -75,7 +75,7 @@ export default (server: FastifyInstance, _options: RouteShorthandOptions, done:
const newIssue = await createIssue({
description: formatMessage({ reason, ua, url, version }),
labels: ['bug'],
title: hostname,
title: domain,
});
reply.send({

View File

@ -20,6 +20,16 @@ export function formatMessage(params: FormatMessageParams): string {
].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 {
readonly reason?: string;
readonly ua: UAParserResult;

View File

@ -1,9 +1,41 @@
{
"env": {
"browser": true,
"node": 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": {
"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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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;

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "مسح القائمة"
},
"options_empty": {
"message": "لم يتم العثور على استثناءات"
},
"options_exclusionListTitle": {
"message": "قائمة الاستبعاد"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Liste leeren"
},
"options_empty": {
"message": "Keine Ausschlüsse gefunden"
},
"options_exclusionListTitle": {
"message": "Ausschlussliste"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Clear list"
},
"options_empty": {
"message": "No exclusions found"
},
"options_exclusionListTitle": {
"message": "Exclusion list"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Borrar lista"
},
"options_empty": {
"message": "No se encontraron exclusiones"
},
"options_exclusionListTitle": {
"message": "Lista de exclusión"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Vider la liste"
},
"options_empty": {
"message": "Aucune exclusion trouvée"
},
"options_exclusionListTitle": {
"message": "Liste d'exclusion"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "सूची साफ़ करें"
},
"options_empty": {
"message": "कोई बहिष्कार नहीं मिला।"
},
"options_exclusionListTitle": {
"message": "बहिष्करण सूची"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Bersihkan daftar"
},
"options_empty": {
"message": "Tidak ada pengecualian ditemukan"
},
"options_exclusionListTitle": {
"message": "Daftar pengecualian"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Cancella lista"
},
"options_empty": {
"message": "Nessuna esclusione trovata"
},
"options_exclusionListTitle": {
"message": "Lista di esclusione"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "リストをクリア"
},
"options_empty": {
"message": "除外項目が見つかりませんでした"
},
"options_exclusionListTitle": {
"message": "除外リスト"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "목록 지우기"
},
"options_empty": {
"message": "제외 항목을 찾을 수 없습니다"
},
"options_exclusionListTitle": {
"message": "제외 목록"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Wyczyść listę"
},
"options_empty": {
"message": "Nie znaleziono wykluczeń"
},
"options_exclusionListTitle": {
"message": "Lista wykluczeń"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Limpar lista"
},
"options_empty": {
"message": "Nenhuma exclusão encontrada"
},
"options_exclusionListTitle": {
"message": "Lista de exclusão"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Limpar lista"
},
"options_empty": {
"message": "Nenhuma exclusão encontrada"
},
"options_exclusionListTitle": {
"message": "Lista de exclusão"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Golește lista"
},
"options_empty": {
"message": "Nu au fost găsite excluderi"
},
"options_exclusionListTitle": {
"message": "Lista de excludere"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Очистить список"
},
"options_empty": {
"message": "Исключения не найдены"
},
"options_exclusionListTitle": {
"message": "Список исключений"
},

View File

@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Listeyi temizle"
},
"options_empty": {
"message": "Hariç tutma bulunamadı"
},
"options_exclusionListTitle": {
"message": "Hariç tutma listesi"
},

View File

@ -2,18 +2,84 @@
"name": "browser-extension",
"version": "1.0.0",
"scripts": {
"build": "rimraf build; sh scripts/build.sh; sh scripts/pack.sh",
"lint": "eslint --fix"
"dev": "plasmo dev",
"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": {
"@types/firefox-webext-browser": "^120.0.3",
"eslint": "^8.57.0",
"@jest/globals": "29.7.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-plugin-prettier": "^5.1.3",
"rimraf": "^5.0.5"
"eslint-plugin-import": "^2.31.0",
"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": {
"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"
]
}
]
}
}

View File

@ -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"

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View 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>'] }
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View 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;
}

View 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;
}

View 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>;
}

View 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>;
}

View 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>;
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -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"]
}
]
}

View File

@ -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>

View 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>

View 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 &gt; {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" />
</>
);
}

View File

@ -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>

View 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>

View 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>
&nbsp;
<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>
&nbsp;
<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>
&nbsp;
<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 />
</>
);
}

View File

@ -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>'] }
);

View File

@ -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();

View File

@ -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);

View File

@ -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);

View 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;
}

View File

@ -1,29 +1,4 @@
: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;
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 {
.button {
align-items: center;
background-color: var(--color-white);
border: none;
@ -35,24 +10,24 @@ button {
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;
padding: 8px;
}
@media only screen and (max-device-width: 768px) {
button[data-variant='large'] {
.button[data-variant='large'] {
justify-content: flex-end;
}
}
button:focus,
button:hover {
background-color: var(--color-secondary);
color: var(--color-white);
}
footer {
.footer {
background-color: var(--color-secondary);
font-size: 12px;
height: 4px;
@ -60,14 +35,14 @@ footer {
text-align: center;
}
header {
.header {
background-color: var(--color-secondary);
color: var(--color-white);
font-size: 16px !important;
height: 48px;
}
header > div {
.header > div {
align-items: center;
display: flex;
height: 100%;
@ -75,7 +50,7 @@ header > div {
margin: auto 0px;
}
main input {
.input {
-webkit-appearance: none;
appearance: none;
background-color: var(--color-white);
@ -90,18 +65,18 @@ main input {
width: 100%;
}
main input::placeholder {
.input::placeholder {
color: var(--color-tertiary);
opacity: 1;
}
main input:focus,
main input:hover {
.input:focus,
.input:hover {
border-bottom: 1px solid var(--color-primary);
}
header > div,
main {
.header > div,
.main {
margin: 0px auto;
max-width: 768px;
padding: 16px;
@ -122,13 +97,13 @@ main {
}
}
#exclusion-list {
.exclusion-list {
font-size: 14px;
list-style: none;
padding: 0px;
}
#exclusion-list > li {
.exclusion-list > li {
align-items: center;
border-radius: 4px;
display: flex;
@ -137,14 +112,23 @@ main {
transition: 0.4s;
}
#exclusion-list > li:focus-within,
#exclusion-list > li:hover {
.exclusion-list > li:focus-within,
.exclusion-list > li:hover {
background-color: var(--color-secondary);
color: var(--color-white);
}
#exclusion-list > li > button {
.exclusion-list > button {
background-color: var(--color-white);
color: var(--color-error);
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;
}

View File

@ -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 {
box-sizing: border-box;
color: var(--color-tertiary);
@ -25,12 +11,42 @@ body {
}
}
body * {
box-sizing: border-box;
font-family: Inter, Arial, Helvetica, sans-serif;
.banner {
font-size: 12px;
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);
flex-shrink: 0;
font-size: 12px;
@ -39,7 +55,7 @@ footer {
text-align: center;
}
header {
.header {
align-items: center;
background-color: var(--color-secondary);
color: var(--color-white);
@ -49,58 +65,11 @@ header {
height: 48px;
justify-content: space-between;
padding: 0 16px;
}
& .header-actions {
.header-actions {
display: flex;
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 {
@ -114,11 +83,19 @@ header {
outline: none;
padding: 2px;
transition: 0.4s;
}
&:focus:not(:disabled),
&:hover:not(:disabled) {
.header-button:focus,
.header-button:hover {
background-color: var(--color-white);
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;
padding: 8px;
text-align: center;
text-decoration: none;
transition: 0.4s;
width: 100%;
word-break: break-word;
}
&:focus,
&:hover {
.popup-button:focus,
.popup-button:hover {
box-shadow:
rgba(50, 50, 93, 0.25) 0px 2px 5px -1px,
rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;
}
}
&:disabled {
.popup-button:disabled {
background-color: var(--color-tertiary);
box-shadow: none;
color: var(--color-white);
cursor: not-allowed;
opacity: 0.5;
}
}
& span {
.popup-button span {
align-self: flex-start;
}
}
& svg {
.popup-button svg {
align-self: flex-end;
}
}
.popup-data {
@ -170,10 +148,10 @@ header {
gap: 4px;
justify-content: center;
outline: none;
}
&:not(:first-child) {
.popup-data:not(:first-child) {
margin-top: 4px;
}
}
.popup-data-button {
@ -182,19 +160,20 @@ header {
line-height: 0;
outline: none;
transition: 0.4s;
}
&[aria-disabled='true'] {
.popup-data-button:disabled {
cursor: not-allowed;
}
}
&[data-animation='flip']:focus,
&[data-animation='flip']:hover {
.popup-data-button[data-animation='flip']:focus:not([data-refreshed='true']),
.popup-data-button[data-animation='flip']:hover:not([data-refreshed='true']) {
transform: rotate(-180deg);
}
}
&[data-refreshing='true'] {
.popup-data-button[data-refreshing='true'] {
animation: spin 1s linear infinite;
}
transform: none;
}
@keyframes spin {
@ -211,106 +190,44 @@ header {
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 {
font-size: 14px;
line-height: 18px;
padding: 16px;
}
& .report-buttons {
.report-buttons {
margin-top: auto;
}
}
& .report-form {
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 {
.report-cancel-button {
align-items: center;
background-color: var(--color-white);
color: var(--color-secondary);
@ -325,15 +242,91 @@ header {
outline: none;
padding: 0px;
text-align: center;
}
}
& .report-cancel-button:focus,
& .report-cancel-button:hover {
.report-cancel-button:focus,
.report-cancel-button:hover {
color: var(--color-error);
}
}
& .report-issue-button,
& .report-submit-button {
.report-form {
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;
background-color: var(--color-secondary);
border: 1px solid var(--color-secondary);
@ -350,115 +343,50 @@ header {
padding: 8px 16px;
text-align: center;
width: 100%;
}
}
& .report-issue-button:focus,
& .report-issue-button:hover,
& .report-submit-button:focus,
& .report-submit-button:hover {
.report-issue-button:focus,
.report-submit-button:focus,
.report-issue-button:hover,
.report-submit-button:hover {
background-color: var(--color-white);
color: var(--color-secondary);
}
}
& .report-issue-button:focus-visible,
& .report-submit-button:focus-visible {
.report-issue-button:focus-visible,
.report-submit-button:focus-visible {
box-shadow: initial;
transition: initial;
}
}
& .report-issue-button[aria-disabled='true'],
& .report-submit-button[aria-disabled='true'] {
.report-issue-button:disabled,
.report-submit-button:disabled {
background-color: var(--color-tertiary);
border: 1px solid var(--color-tertiary);
color: var(--color-white);
cursor: not-allowed;
}
}
& .report-submit-extra-text {
font-size: 14px;
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 {
.report-submit-error-view,
.report-submit-success-view {
align-items: center;
display: flex;
flex-direction: column;
gap: 24px;
justify-content: center;
margin-top: 16px;
}
& .report-submit-error-view[hidden],
& .report-submit-success-view[hidden] {
display: none;
}
}
#loader {
align-items: center;
background-color: var(--color-white);
bottom: 0px;
display: flex;
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;
}
.report-submit-extra-text {
font-size: 14px;
line-height: 18px;
margin: 0px;
text-align: justify;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
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;
.report-submit-text {
font-size: 18px;
line-height: 22px;
margin: 0px;
text-align: center;
}

View 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';

View 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, '[^ ]*')));
}

View File

@ -0,0 +1,3 @@
export const noop = () => undefined;
export const suppressLastError = () => void chrome.runtime.lastError;

View File

@ -0,0 +1,3 @@
import { Storage } from '@plasmohq/storage';
export const storage = new Storage({ area: 'local' });

View 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[];
}

View File

@ -0,0 +1,3 @@
import { describe } from '@jest/globals';
describe('contents/index.ts', () => {});

View 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"]
}

View File

@ -14,6 +14,5 @@
},
"engines": {
"node": "20.x"
},
"packageManager": "yarn@4.2.1"
}
}

View File

@ -10,11 +10,11 @@ for file in $(find $input -name "*.css" -o -name "*.html" -o -name "*.js" | sed
output_file="$output/$file"
mkdir -p "${output_file%/*}" && touch "$output_file"
yarn minify $input_file > $output_file
pnpm minify $input_file > $output_file
done
yarn tailwindcss -i "$input/index.css" -o "$output/index.css"
yarn minify "$output/index.css" > "$output/index-1.css"
pnpm tailwindcss -i "$input/index.css" -o "$output/index.css"
pnpm minify "$output/index.css" > "$output/index-1.css"
rm -rf "$output/index.css"
mv "$output/index-1.css" "$output/index.css"
cp -nR "$input/." $output

12833
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
packages:
- "packages/**"

5220
yarn.lock

File diff suppressed because it is too large Load Diff