Compare commits

..

88 Commits
8.0.2 ... main

Author SHA1 Message Date
886d31bdea feat(browser-extension): add background script tests 2024-11-20 13:26:48 +01:00
edf5eda7dc feat(browser-extension): use plasmo framework 2024-11-19 12:57:32 +01:00
6434dd6c1c fix(api): rate limit bug 2024-11-19 12:19:21 +01:00
d16f86cd32 feat: enough github 2024-11-19 01:39:57 +01:00
12a5d68e3c feat(web): add new mobile release 2024-11-11 20:37:24 +01:00
59b4c4334e refactor(browser-extension): drop scripting permission 2024-11-11 20:35:40 +01:00
781511be3c feat(web): add new desktop release 2024-11-11 20:22:25 +01:00
657e6955a5 Merge pull request '8.0.5' (#228) from v8.0.5 into main
Reviewed-on: #228
2024-11-11 19:12:17 +00:00
c360ea2d70 Merge branch 'main' into v8.0.5 2024-11-11 20:10:41 +01:00
5fa4b9566c fix(browser-extension): issue #229 2024-11-11 20:00:59 +01:00
debc9d3b05 feat(web): add new mobile release 2024-11-11 14:33:24 +01:00
9559fc761b refactor(web): improve releases structure 2024-11-11 14:33:03 +01:00
9bece711a2 refactor(browser-extension): rename locale folder jp to ja 2024-11-11 14:20:56 +01:00
56929d0bcd feat(web): add new mobile release 2024-11-11 14:19:14 +01:00
fd242502ab feat(web): update texts 2024-11-11 14:07:02 +01:00
cc42141293 feat(web): add new desktop release 2024-11-11 14:06:40 +01:00
4dd405f0d2 Merge pull request '8.0.4' (#226) from v8.0.4 into main
Reviewed-on: #226
2024-11-11 12:44:06 +00:00
31dc8389f0 fix(browser-extension): tab creation not closing extension menu 2024-11-11 13:43:26 +01:00
b1f09ce11f feat(browser-extension): add loader now that we have more queries 2024-11-11 13:34:48 +01:00
3e539e9e1c fix(browser-extension): issue #166 2024-11-11 12:35:54 +01:00
adc8ccc391 feat(api): add version endpoint 2024-11-10 23:13:32 +01:00
2390960d21 fix(browser-extension): report context menu item sometimes disabled 2024-11-10 22:45:57 +01:00
effe22da79 fix(browser-extension): issue #199 2024-11-10 22:38:50 +01:00
40b29fdd87 feat(browser-extension): add new translations ar, hi, id, jp, ko and tr 2024-11-10 22:29:13 +01:00
f8578b0940 feat(browser-extension): add new popup_bannerSupport message 2024-11-10 22:28:50 +01:00
dc44a6f8da feat(browser-extension): improve ux/ui 2024-11-10 22:28:18 +01:00
62b6a21b95 fix(browser-extension): issue #197 2024-11-10 22:28:03 +01:00
5dc35344a1 feat(web): add new mobile release 2024-10-28 14:01:52 +01:00
f6aaf9f600 feat(web): add new desktop release 2024-10-28 13:32:04 +01:00
d781f756f6 Merge pull request '8.0.3' (#162) from v8.0.3 into main
Reviewed-on: #162
2024-10-28 12:20:34 +00:00
0c99d86b96 fix(database): issue #102 2024-10-28 12:30:49 +01:00
031f7d0b34 fix(browser-extension): issue #166 2024-10-28 12:21:50 +01:00
9293bd84ac fix(browser-extension): some broken styles in firefox 2024-10-28 12:21:38 +01:00
5c82a0e8f9 fix(database): issue #109 2024-10-25 10:37:44 +02:00
d4430a72dc fix(database): issue #108 2024-10-25 10:28:54 +02:00
9e4384a08d fix(database): issue #107 2024-10-25 10:27:12 +02:00
ff2b18256a fix(database): issue #106 2024-10-25 10:25:35 +02:00
e7ff26f1f7 fix(database): issue #103 2024-10-25 10:19:03 +02:00
85f052c3a5 fix(database): issue #100 2024-10-25 10:11:16 +02:00
8dd3f4d3c9 fix(database): issue #99 2024-10-25 10:08:50 +02:00
7a42f91532 fix(database): issue #95 2024-10-25 10:03:44 +02:00
7e2b4b3e08 fix(database): issue #94 2024-10-25 10:02:08 +02:00
1f37fa0607 fix(database): issue #93 2024-10-25 09:58:49 +02:00
fa8b3833d8 fix(database): issue #91 2024-10-23 12:10:29 +02:00
a6e187e350 fix(database): issue #90 2024-10-23 12:08:51 +02:00
49baa47eaa fix(database): issue #86 2024-10-23 12:05:06 +02:00
92d5d5507f fix(database): issue #83 2024-10-23 11:58:06 +02:00
04edca2ec5 fix(database): issue #82 2024-10-23 11:53:10 +02:00
9aaf27868a fix(database): issue #80 2024-10-23 11:46:47 +02:00
1862793a9c fix(database): issue #78 2024-10-23 11:39:57 +02:00
0a4442aa19 fix(database): issue #76 2024-10-23 11:35:48 +02:00
fc3db66cfd fix(database): issue #74 2024-10-23 11:33:07 +02:00
c4f428591e fix(database): issue #68 2024-10-23 11:23:48 +02:00
7800ed86b9 fix(database): issue #67 2024-10-21 11:15:39 +02:00
c118551dc7 fix(database): issue #66 2024-10-21 11:10:33 +02:00
8fed2f0444 fix(database): issue #60 2024-10-21 11:06:27 +02:00
cf56b67eb7 fix(database): issue #62 2024-10-21 11:04:01 +02:00
95e6cfbe84 fix(database): issue #60 2024-10-21 11:01:19 +02:00
b88a81e347 fix(database): issue #53 2024-10-21 10:51:51 +02:00
1873c4c426 fix(database): issue #49 2024-10-21 10:50:03 +02:00
8510d50ab9 fix(database): issue #47 2024-10-21 10:39:59 +02:00
362da4a7ef fix(database): issue #46 2024-10-21 10:35:17 +02:00
daa774a3da fix(database): issue #44 2024-10-21 10:31:19 +02:00
fd19ba3242 refactor(browser-extension): drop unnecessary line break 2024-10-19 18:29:03 +02:00
6b788abe28 refactor(browser-extension): drop old github references 2024-10-19 18:23:47 +02:00
1ac4812ebf chore(browser-extension): bump extension version 2024-10-19 18:23:25 +02:00
b0e56c87eb refactor(web): drop old references to GitHub, also improve alt for images 2024-10-19 18:19:24 +02:00
f8f2e64aef fix(database): issue #118 2024-10-19 11:48:24 +02:00
59229f7eeb fix(database): issue #116 2024-10-19 11:26:20 +02:00
4394695d15 fix(database): issue #117 2024-10-19 11:24:45 +02:00
a6f72cd90d fix(database): issue #118 2024-10-19 11:23:29 +02:00
35f3622e00 fix(database): issue #119 2024-10-19 11:21:25 +02:00
b7f04383e8 fix(database): issue #120 2024-10-19 11:18:23 +02:00
dfb220ce90 fix(database): issue #124 2024-10-19 11:08:38 +02:00
6827951d6d fix(database): issue #128 2024-10-19 10:55:21 +02:00
0460306fbf fix(database): issue #129 2024-10-19 10:53:03 +02:00
21112ffcbc fix(database): issue #133 2024-10-19 10:48:54 +02:00
42d5dd6885 fix(database): issue #135 2024-10-19 10:40:26 +02:00
08efbe3e0f fix(database): issue #141 2024-10-19 10:30:51 +02:00
c99a18d1a5 fix(database): issue #140 2024-10-19 10:29:02 +02:00
32bb4ce5e6 fix(database): issue #142 2024-10-19 10:27:23 +02:00
3046799d10 fix(database): issue #145 2024-10-19 10:25:32 +02:00
7b14777d1c fix(database): issue #146 2024-10-19 10:22:56 +02:00
d015c4a993 fix(database): issue #147 2024-10-19 10:21:14 +02:00
c2a80cd97c fix(database): issue #148 2024-10-19 10:19:04 +02:00
a7b267c0eb fix(database): issue #153 2024-10-19 10:14:15 +02:00
105a73b17c fix(database): issue #156 2024-10-19 10:12:05 +02:00
51d86c36f2 fix(database): issue #158 2024-10-19 10:08:05 +02:00
174 changed files with 17243 additions and 8893 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

@ -1,25 +1,7 @@
> AN IMPORTANT UPDATE
> <br><br>
> On the morning of October 11, 2024, GitHub unexpectedly hid our repository without any prior notification. This sudden action immediately disrupted all of our API services, as they relied on files from the repository that were no longer accessible. Our team reached out to GitHub support to understand the situation, but we have not yet received any response. Given the critical impact on our services and the lack of communication from GitHub, we decided to migrate the entire repository to an alternative platform to ensure continuity and reliability.
> <br><br>
> We initially migrated the repository, releases, and open issues (excluding discussions) to GitLab. However, during the migration of issues, GitLab's own spam detection mechanism mistakenly identified its bot activity as spam, leading to our issues being hidden. Despite reaching out to GitLab support, we have not yet received a resolution for this incident. Faced with these ongoing platform limitations and communication delays, we have opted to move forward with self-hosting our own Git system using Gitea to ensure full control over our repository and services.
> <br><br>
> We will no longer support platforms that overlook the contributions of our users and the significant work invested over the last five years. Once our GitHub account is reinstated, we will set up a redirect to guide users to our new, self-hosted repository.
> <br><br>
> Thank you to our community for your patience. Your contributions remain vital to this project, and we are committed to ensuring its stability and growth in a more secure environment.
# Cookie Dialog Monster
Cookie Dialog Monster is a browser extension that hides cookie consent dialogs without changing user preferences. By default, we do NOT accept cookies (except in [a few cases](https://git.wanhose.dev/wanhose/cookie-dialog-monster/src/branch/main/database.json) where the pages do not function without accepting them). You can report broken sites with a single click, which will create an issue in this repository to be fixed promptly.
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
[MIT](https://choosealicense.com/licenses/mit/)
## Repositories
- [API](/wanhose/cookie-dialog-monster/src/branch/main/packages/api)
@ -34,3 +16,11 @@ Pull requests are welcome. For major changes, please open an issue first to disc
- [Pull requests](https://git.wanhose.dev/wanhose/cookie-dialog-monster/pulls)
- [Releases](https://git.wanhose.dev/wanhose/cookie-dialog-monster/releases)
- [Wiki](https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/Help-or-issues%3F)
## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
## License
[MIT](https://choosealicense.com/licenses/mit/)

View File

@ -5,6 +5,12 @@
"name": "click",
"selector": "#CybotCookiebotDialogBodyLevelButtonLevelOptinDeclineAll"
},
{
"domain": "ai.meta.com",
"name": "reset",
"property": "position",
"selector": "._31e"
},
{
"domain": "automobielmanagement.nl",
"name": "resetAll",
@ -134,6 +140,12 @@
"name": "click",
"selector": "[role=\"dialog\"] > [style=\"display: flex; flex-direction: column; height: 100%; max-width: 100%;\"] > button"
},
{
"domain": "larousse.fr",
"name": "reset",
"property": "filter",
"selector": ".wrapper"
},
{
"domain": "motorolasound.com",
"name": "click",
@ -247,6 +259,7 @@
"*.facebook.com",
"*.googleapis.com",
"*.olympics.com",
"azure.microsoft.com",
"translate.google.*",
"www.youtube-nocookie.com",
"www.youtube.com"
@ -925,19 +938,26 @@
"||b2brouter.net/de/wp-content/plugins/cookies-and-content-security-policy^",
"||benno-gymnasium.de/plugins/system/gdpr^",
"||c.evidon.com^",
"||cache.consentframework.com^",
"||cdn-cookieyes.com^",
"||cdn.consentmanager.net^",
"||cdn.cookielaw.org^",
"||cdn.optable.co^",
"||cdn.trustcommander.net/privacy^",
"||cdntranscend.eventbrite.com^",
"||choices.consentframework.com^",
"||chollometro.com/assets/js/CookiesMessage*.js^",
"||city-bahn.de/wp-content/plugins/borlabs-cookie^",
"||cmp.actiview.de^",
"||cmp.computerbild.de^",
"||cmp.huffingtonpost.fr^",
"||cmp.inmobi.com^",
"||cmp.meteored.com^",
"||cmp.prisa.com^",
"||cmp.uniconsent.com^",
"||cmp2.zdf.de^",
"||cms.static-bahn.de/cms/consent-layer^",
"||composants-pe-communs.pole-emploi.fr/*/pe-cookies.js^",
"||consent.cookiebot.com^",
"||consent.cookiefirst.com^",
"||consent.pdf24.org^",
@ -945,11 +965,14 @@
"||consent.up.welt.de^",
"||consentbar.interencheres.com^",
"||consentcdn.cookiebot.com^",
"||cookie.porsche.com^",
"||cookiehub.net^",
"||cp.as.com^",
"||cp.elektroauto-news.net^",
"||cp.inside-digital.de^",
"||cp.winfuture.de^",
"||download.amd.com/OneTrust^",
"||driftingchef.com/static/*/Sticky2,ConsentManager",
"||driftingchef.com^",
"||emp.bbci.co.uk^",
"||eu.fastcmp.com^",
"||forum.vorondesign.com/js/xf/notice.min.js^",
@ -962,15 +985,20 @@
"||media.bzcompany.cz/scookies^",
"||myprivacy-static.dpgmedia.net/consent.js^",
"||myprivacy.dpgmedia.*/consent^",
"||nojazz.eu/nl/cmp/consentui^",
"||optanon.blob.core.windows.net^",
"||privacy.wetransfer.com/fides.js^",
"||prod.widgets.burgerprofiel.vlaanderen.be/*/js.cookie.js^",
"||redditstatic.com/onetrust.*.js",
"||release.jobylon.com/jbl-cookie-consent-js^",
"||s.nitropay.com/gpp-*.min.js",
"||s.p7s1.io/cmp^",
"||schumacher-elektro.de/typo3conf/ext/we_cookie_consent^",
"||sdk.privacy-center.org^",
"||static.axept.io^",
"||static.fastcmp.com^",
"||transcend-cdn.com^",
"||uct.eu.usercentrics.eu^",
"||wcpstatic.microsoft.com^",
"||webtools.europa.eu/js/webtools.cck.js^",
"||widgets.marketcat.net^"
@ -980,6 +1008,8 @@
".cc-policy-overlay",
".cdk-overlay-backdrop:has(+ .cdk-global-overlay-wrapper)",
".js--overlay",
".js-lcc-backdrop",
".lcc-backdrop",
".modal-backdrop",
".modals-overlay",
".offcanvas-backdrop",
@ -989,6 +1019,7 @@
"[data-aria-hidden=\"true\"][data-state=\"open\"][style=\"pointer-events: auto;\"]",
"#fancybox-overlay",
"#gdprMask",
"#id-modal-backdrop",
"#overlay:not([class*=\"ytd\"])",
"overlay-block"
],
@ -998,6 +1029,7 @@
"backdrop-no-scroll",
"bb-con-loaded",
"blocked",
"blur",
"blur-background",
"bodyBlocked",
"c24-cc-visible",
@ -1054,6 +1086,7 @@
".dw-content",
".footer",
".layout",
".lv-header",
".page",
".row",
".rp-container",
@ -1066,12 +1099,16 @@
"#content",
"#layout > [data-fetch-key]",
"#main",
"#modal-root",
"#root",
"#wrapperDivisions",
"#wt-modal-container",
"body > *",
"div:has(#main)",
"html > *"
"footer > *",
"header > *",
"html > *",
"main > *"
],
"selectors": [
".___eucookiePopup",
@ -1459,6 +1496,7 @@
".banner-message-cpopup",
".banner-notification",
".banner-policy-wrap",
".banner-pop:has([href=\"/sp-confidentialite/\"])",
".banner[data-banner=\"cookies\"]",
".bannerCookie",
".bannerCookies",
@ -1596,6 +1634,7 @@
".box_cookie",
".box_Cookie",
".box_cookies",
".box_right_bottom:has(.allowed_ookie)",
".box--cookies",
".box-accept-cookies",
".box-alert-cookie-inside",
@ -3126,6 +3165,7 @@
".cookieConsentBar",
".cookieConsentBarContainer",
".CookieConsentContainer",
".cookieconsentdialog",
".CookieConsentMemo",
".CookieConsentOverlay",
".cookieConsentPanel",
@ -4052,6 +4092,7 @@
".dgp-consent",
".dgpr-drop-down",
".dhl-cgk",
".dialog-content-wrapper:has([data-testid=\"cookie-dialog\"])",
".dialog-cookie-settings",
".dialog[aria-label=\"cookieconsent\"]",
".dialogBanner__inner[data-qa=\"cookie-banner\"]",
@ -4380,6 +4421,7 @@
".fixed-cookie-prl",
".fixed-cookie",
".fixed-msg--cookies",
".fixed.bottom-6.left-6:has([href=\"/privacy\"])",
".fixed.bottom-sm.left-sm.right-sm.z-50.max-w-md.rounded-lg.border.p-md.shadow-sm.md\\:left-auto.border-borderMain\\/50.ring-borderMain\\/50.divide-borderMain\\/50.dark\\:divide-borderMainDark\\/50.dark\\:ring-borderMainDark\\/50.dark\\:border-borderMainDark\\/50.bg-background.dark\\:bg-backgroundDark",
".fixed.md\\:flex.items-center.justify-between.p-2.md\\:py-4.md\\:px-6.bottom-0.left-0.right-0.bg-midnight-lighter.text-white",
".fixed.right-2.bottom-2.z-\\[60\\].max-h-\\[calc\\(100vh-1rem\\)\\].max-w-\\[calc\\(100vw-1rem\\)\\].sm\\:max-w-md.rounded-3xl.font-styrene.bg-bg-500.p-4.sm\\:p-8.overflow-auto",
@ -4659,6 +4701,7 @@
".GdprCookieConsent-ConsentContainer",
".GdprCookieConsent-ConsentContainerOverlay",
".gdprCookieLaw",
".gdprCookiePolicyBannerContainer",
".gdprCookieWrapper",
".gdprcp-modal",
".gdprcp-overlay",
@ -4831,6 +4874,7 @@
".identity-noticebar",
".idgcp__section",
".idrPageRow[style^=\"min-height:0;height:auto;padding:0;position:relative;z-index:1;zoom:1;border:none;\"]",
".if6_eprivacy",
".iframe_cookies",
".ilikecookies",
".ilsa-cookies-cta",
@ -5294,6 +5338,7 @@
".mensajecokies",
".mentionCookie",
".menu-con-cookies",
".mercusys-cookie-eu",
".message_cookie",
".message--cnil",
".message--cookie-warning",
@ -5705,6 +5750,7 @@
".otcn-modal",
".otcn",
".otCookiesNotification",
".otPlaceholder",
".ou-cookies-bar",
".outer-privacy-top-navigationBar",
".outerCookieBar",
@ -5809,6 +5855,9 @@
".pnl-cookie",
".pnlCookiemelding",
".pnotify-cookies",
".pointer-events-auto.bottom-0.w-full.flex.justify-center.items-center.bg-background-primary.z-20.py-4.gap-3.px-5:has([href=\"https://changelly.com/privacy-policy\"])",
".polaris-consent-widget",
".polaris-consent-widgets",
".polcookies",
".policy_footer",
".policy_notification",
@ -6281,6 +6330,7 @@
".sm-cookie-consent",
".small_gdpr_popup_holder",
".smcc_bottom_cookieaccept_container",
".smile-cookie-notification-container",
".sml_cookiepolicy",
".smoothie-consent",
".smrt-ncookies-alert",
@ -6288,6 +6338,7 @@
".smt-cookie-note",
".sncmp-app_gdpr",
".snoop-cc",
".so--modal:has(.so-cookie-banner)",
".so-cookie-wrapper",
".so-gdpr__cookie-notification",
".social-link-container",
@ -6514,6 +6565,7 @@
".uc-cookie-block",
".uc-cookie",
".ucgCookieBar",
".ucm-bl-mainwrap + #divAlert.alertPopup",
".ucn-block",
".ucp-cookiebanner-wrapper",
".ud-component--eu-cookie-message--app",
@ -6581,6 +6633,7 @@
".v-app-PortletCookiesLaw",
".v-cookie-bar",
".v-cookieconsent",
".v-snack:has([href*=\"accounts.hunterco.com.br/privacy\"])",
".v-window-cookieDialogWindow",
".v2_cookies_container",
".v2-cookie-disclaimer",
@ -6674,6 +6727,7 @@
".widget-gdpr-banner",
".widget-gdpr-cookie-banner",
".widget-GdprCookieBanner",
".widget.HTML:has([href=\"https://policies.google.com/technologies/cookies\"])",
".widilo-cookie-rules",
".win_cookies",
".with-cookie",
@ -6783,6 +6837,7 @@
".zone-cookies",
".zpconsentinfo",
".zpWinThemeCookie",
".zsl-footer + #divAlert.alertPopup",
"[aria-describedby=\"cookie-information\"]",
"[aria-describedby=\"cookieconsent:desc\"]",
"[aria-describedby=\"cookieConsentDesc\"]",
@ -6915,6 +6970,7 @@
"[data-analytics-category=\"Cookies Consent\"]",
"[data-assembly-source=\"cookie/banner\"]",
"[data-at-selector=\"cookie-banner\"]",
"[data-automation-id=\"legalNotice\"]",
"[data-automation=\"cookies-banner\"]",
"[data-automation=\"privacy-banner-wrapper\"]",
"[data-banner=\"cookies\"]",
@ -6939,6 +6995,7 @@
"[data-cookie-bar-target=\"modal\"]",
"[data-cookie-consent-overlay]",
"[data-cookie-id]",
"[data-cookie-key=\"__cookie_consent\"][role=\"dialog\"]",
"[data-cookie-law-banner-selector]",
"[data-cookie-message]",
"[data-cookie-name=\"header_cookie_policy\"]",
@ -6947,6 +7004,10 @@
"[data-cookie-permission=\"true\"]",
"[data-cookie-policy-modal]",
"[data-cookie-validity-days]",
"[data-cookie-value-analytics=\"2\"][role=\"dialog\"]",
"[data-cookie-value-both=\"true\"][role=\"dialog\"]",
"[data-cookie-value-marketing=\"3\"][role=\"dialog\"]",
"[data-cookie-value-none=\"false\"][role=\"dialog\"]",
"[data-cookie-warning]",
"[data-cookie]",
"[data-cookie^=\"cookie.disclaimer\"]",
@ -6963,6 +7024,7 @@
"[data-etsy-promo-cookie-expires]",
"[data-feature=\"Web.Core.Feature.PrivacyCategory.Dialog\"]",
"[data-gdpr-consent-prompt]",
"[data-gtm-event=\"cookie_refresh\"][role=\"dialog\"]",
"[data-headlessui-portal]:has([alt=\"cookie\"])",
"[data-headlessui-portal]:has([aria-label=\"Cookie Policy\"])",
"[data-hook=\"consent-banner-root\"]",
@ -6973,6 +7035,7 @@
"[data-js=\"cookie-disclaimer\"]",
"[data-key=\"cookies-warning\"]",
"[data-module=\"cookie_banner\"]",
"[data-module=\"cookie-manager-dialog\"]",
"[data-module=\"cookie-notice\"]",
"[data-module=\"cookies\"]",
"[data-name=\"cookie-policy\"]",
@ -7032,11 +7095,13 @@
"[data-testid=\"cookie-policy-dialog\"]",
"[data-testid=\"cookie-policy-manage-dialog\"]",
"[data-testid=\"cookie-wall-modal\"]",
"[data-testid=\"cookie-widget\"]",
"[data-testid=\"cookienotice-container\"]",
"[data-testid=\"cookies-dialog\"]",
"[data-testid=\"dl-cookieBanner\"]",
"[data-testid=\"gateway-container\"][aria-hidden=\"true\"]",
"[data-testid=\"main-cookies-banner-container\"]",
"[data-testid=\"Modal\"]:has([class*=\"CookieBanner-\"])",
"[data-testid=\"uc-default-banner\"]",
"[data-testid=\"wa_cookies_banner_modal\"]",
"[data-tk-teaser-entity=\"g-consentmanager\"]",
@ -7084,6 +7149,7 @@
"[src=\"https://www.tumblr.com/dashboard/iframe/consent\"]",
"[style*=\"bg_cookies.jpg\"]",
"[style*=\"cookie-consent-zindex\"]",
"[style=\"inset: 0px; width: 100%; height: 100%; box-sizing: border-box; position: fixed; touch-action: none; padding: 20px; z-index: 10; display: flex; flex-direction: row; gap: 20px; justify-content: center; pointer-events: none;\"]:has([href=\"/cookie-policy\"])",
"[style=\"inset: 0px; width: 100%; height: 100%; box-sizing: border-box; position: fixed; touch-action: none; padding: 20px; z-index: 10; display: flex; flex-direction: row; gap: 20px; justify-content: center; pointer-events: none;\"]:has([href=\"/privacy-policy\"])",
"[style=\"opacity: 1; display: flex;\"]:has(#ac)",
"[style=\"z-index: 4;\"]:has([role=\"dialog\"] [data-anchor-id=\"cookie-policy\"])",
@ -7330,7 +7396,6 @@
"#alert-cookies",
"#alert-eu-cookies",
"#alert-privacy-update",
"#alert",
"#alerta_cookies",
"#alerta-cookies",
"#AlertaCookies",
@ -7348,7 +7413,6 @@
"#alertMentionCookiesMessage",
"#alertpop-privacypolicy",
"#alerts-policy",
"#alertTop",
"#all4gn-cookie-policy-notification",
"#all4nav-cookie-component",
"#all4nav-cookie-policy-notification",
@ -7824,6 +7888,7 @@
"#catapult-cookie-bar",
"#cb_alert",
"#cb_content",
"#cb-cookie_consent",
"#cb-cookieoverlay",
"#cbcookies-bottom",
"#cbcookies-top",
@ -8647,6 +8712,7 @@
"#cookie-banner--popup-wrapper",
"#cookie-banner-bespoke",
"#cookie-banner-container",
"#cookie-banner-host",
"#cookie-banner-message",
"#cookie-banner-panel",
"#cookie-banner-root",
@ -9059,6 +9125,7 @@
"#cookie-settings-container",
"#cookie-settings-dlg",
"#cookie-settings-layer",
"#cookie-settings-popup",
"#cookie-settings.cookie-settings",
"#cookie-slide",
"#cookie-slider",
@ -10639,6 +10706,7 @@
"#cookieWarningTable",
"#cookieWarningWrapper",
"#cookiewarnung",
"#cookieWarnung",
"#cookiewet-balk",
"#cookiewet-hoek",
"#cookiewet",
@ -12308,6 +12376,7 @@
"#ma > .ckch",
"#ma-cmp-container",
"#mab-cookie-notice-wrap",
"#macaron_cookie_box",
"#macchiato_cookies_wrapper",
"#machete_cookie_container",
"#macookie",
@ -12368,6 +12437,7 @@
"#mention_cookies",
"#mention-cookies",
"#mentions-cookies",
"#mercusys-cookie",
"#message_cookie_info_accept",
"#message_cookie",
"#message_cookies",
@ -12457,6 +12527,7 @@
"#modalCook.mostrar-modal",
"#modalCookie",
"#modalCookieComp",
"#modalCookieConsentDialogContainer",
"#modalCookieContainer",
"#modalcookies",
"#modalCookies",
@ -12874,6 +12945,7 @@
"#pol_cookies",
"#pol-cookies",
"#poland_cookie_cont",
"#polaris-consent-widgets",
"#polCockie",
"#polCookies",
"#policiyAlert",
@ -12930,6 +13002,7 @@
"#PolitykaPrywatnosciCookie",
"#politykaPrywatnosciHolder",
"#poolcookiebadge",
"#poool-widget",
"#pop_cookies",
"#pop_site_cookie",
"#pop-client",
@ -13975,6 +14048,7 @@
"correos-cdk-cookies-module",
"dialog.consent.js-consent",
"dile-cookies-consent",
"div:has([data-testid=\"close-button\"]):has([href*=\"policy.medium.com/medium-privacy-policy\"]):has([role=\"alert\"])",
"div:has(+ * a[href=\"https://www.viagogo.com/help/cookies\"])",
"div:not(#app):has([href=\"https://www.viagogo.com/help/cookies\"])",
"div[jsmodel]:not([jscontroller]):has([data-cookie-path])",
@ -14008,5 +14082,5 @@
"ytm-consent-bump-v2-renderer"
]
},
"version": "1729252494919"
"version": "1730114957636"
}

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

@ -15,7 +15,9 @@ import v5ReportRoutes from 'routes/v5/report';
import v6DataRoutes from 'routes/v6/data';
import v6IssuesRoutes from 'routes/v6/issues';
import v6ReportRoutes from 'routes/v6/report';
import v6VersionRoutes from 'routes/v6/version';
import environment from 'services/environment';
import { keyGenerator } from 'services/rateLimit';
const server = fastify({ logger: true });
@ -29,6 +31,7 @@ server.register(cors, {
server.register(rateLimit, {
global: false,
keyGenerator,
});
server.register(v1EntriesRoutes, { prefix: '/rest/v1' });
@ -45,6 +48,7 @@ server.register(v5ReportRoutes, { prefix: '/rest/v5' });
server.register(v6DataRoutes, { prefix: '/rest/v6' });
server.register(v6IssuesRoutes, { prefix: '/rest/v6' });
server.register(v6ReportRoutes, { prefix: '/rest/v6' });
server.register(v6VersionRoutes, { prefix: '/rest/v6' });
server.listen({ host: '0.0.0.0', port: environment.port }, (error, address) => {
if (error) {

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

@ -0,0 +1,35 @@
import { FastifyInstance, RouteShorthandOptions } from 'fastify';
import fetch from 'node-fetch';
import environment from 'services/environment';
import { RATE_LIMIT_10_PER_MIN } from 'services/rateLimit';
export default (server: FastifyInstance, _options: RouteShorthandOptions, done: () => void) => {
server.get(
'/version/',
{
config: {
rateLimit: RATE_LIMIT_10_PER_MIN,
},
},
async (_request, reply) => {
try {
const manifest = `${environment.gitea.raw}/packages/browser-extension/src/manifest.json`;
const options = { headers: { 'Cache-Control': 'no-cache' } };
const response = await fetch(manifest, options);
const { version } = await response.json();
reply.send({
data: version,
success: true,
});
} catch (error) {
reply.send({
errors: [error.message],
success: false,
});
}
}
);
done();
};

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,3 +1,5 @@
import type { FastifyRequest } from 'fastify';
export const RATE_LIMIT_1_PER_HOUR = {
max: 1,
timeWindow: '1 hour',
@ -17,3 +19,9 @@ export const RATE_LIMIT_3_PER_MIN = {
max: 3,
timeWindow: '1 minute',
};
export function keyGenerator(req: FastifyRequest): string {
const userIdentifier = req.headers['x-forwarded-for'] || req.headers['x-real-ip'] || req.ip;
return `${userIdentifier}:${req.routerPath}`;
}

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

View File

@ -12,7 +12,8 @@
- [Chrome Web Store](https://chrome.google.com/webstore/detail/djcbfpkdhdkaflcigibkbpboflaplabg)
- [Edge Add-ons](https://microsoftedge.microsoft.com/addons/detail/hbogodfciblakeneadpcolhmfckmjcii)
- [Mozilla Firefox (.xpi)](https://www.cookie-dialog-monster.com/releases/latest.xpi)
- [Mozilla Firefox](https://www.cookie-dialog-monster.com/releases/mozilla/latest.xpi)
- [Mozilla Firefox Mobile](https://www.cookie-dialog-monster.com/releases/mozilla-mobile/latest.xpi)
## Installation

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,25 @@
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>/' }),
'^url:~assets/(.+).png$': '<rootDir>/mocks/assets/$1.mock.ts',
},
preset: 'ts-jest/presets/default-esm',
setupFiles: ['./jest.setup.ts'],
testEnvironment: 'jsdom',
testRegex: ['^.+\\.test.tsx?$'],
transform: {
'^.+.tsx?$': ['ts-jest', { isolatedModules: true, useESM: true }],
},
};
export default config;

View File

@ -0,0 +1,5 @@
import 'jest-webextension-mock';
import './mocks/chrome.mock';
import './mocks/plasmo.mock';
import './mocks/utils/domain.mock';
import './mocks/utils/storage.mock';

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "هل ذكر أحد حوارات موافقة ملفات تعريف الارتباط؟ 😋"
},
"contextMenu_issueOption": {
"message": "متابعة هذه المشكلة"
},
"contextMenu_reportOption": {
"message": "الإبلاغ عن هذا الموقع"
},
"contextMenu_settingsOption": {
"message": "إدارة قائمة الاستبعاد"
},
"options_addButton": {
"message": "إضافة استبعاد"
},
"options_addPrompt": {
"message": "أدخل النطاق"
},
"options_clearButton": {
"message": "مسح القائمة"
},
"options_empty": {
"message": "لم يتم العثور على استثناءات"
},
"options_exclusionListTitle": {
"message": "قائمة الاستبعاد"
},
"options_exportButton": {
"message": "تصدير القائمة"
},
"options_filterPlaceholder": {
"message": "اضغط ENTER للتصفية بعد الكتابة"
},
"options_importButton": {
"message": "استيراد القائمة"
},
"popup_bannerIssueOpen": {
"message": "تم الإبلاغ عن مشكلة في هذه الصفحة. قد لا تعمل الإضافة بشكل متوقع مؤقتًا. تابع المشكلة أو أضف المزيد من المعلومات"
},
"popup_bannerIssueWontFix": {
"message": "تم الإبلاغ عن مشكلة في هذه الصفحة ولكن لن يتم إصلاحها. يُرجى العلم أن الإضافة قد لا تعمل كما هو متوقع في هذه الصفحة. نوصي بشدة بإيقاف تشغيل الإضافة باستخدام الزر أدناه"
},
"popup_bannerSupport": {
"message": "لم يعد هذا الموقع مدعومًا. لمزيد من المعلومات، يرجى المتابعة هنا"
},
"popup_bannerUpdateAvailable": {
"message": "التحديث متاح. يرجى تثبيت أحدث إصدار لحل المشكلات المعروفة قبل الإبلاغ عن مشكلات جديدة"
},
"popup_contributeOption": {
"message": "ساهم في هذا المشروع"
},
"popup_databaseVersion": {
"message": "إصدار قاعدة البيانات"
},
"popup_extensionVersion": {
"message": "إصدار الإضافة"
},
"popup_helpOption": {
"message": "المساعدة أو المشاكل؟"
},
"popup_rateOption": {
"message": "قيّم هذه الإضافة"
},
"report_bodyText": {
"message": "يرجى عدم مشاركة أي معلومات شخصية في هذا التقرير. إذا كنت تريد إضافة المزيد من التفاصيل، انتقل إلى المشكلة في الخطوة التالية وأضف تعليقًا."
},
"report_cancelButtonText": {
"message": "إلغاء"
},
"report_reasonInputError": {
"message": "يرجى إدخال سبب يتكون من 10 أحرف على الأقل ولا يزيد عن 1000 حرف"
},
"report_reasonInputLabel": {
"message": "السبب"
},
"report_reasonInputPlaceholder": {
"message": "ظهر إشعار"
},
"report_submitErrorExtraText": {
"message": "يبدو أن هناك مشكلة مفتوحة بالفعل أو تم تحديدها بأنها لن يتم إصلاحها. يرجى الدخول إلى المشكلة إذا كنت ترغب في إضافة المزيد من المعلومات باستخدام الزر التالي."
},
"report_submitErrorText": {
"message": "لم يتم إرسال التقرير؟"
},
"report_submitSuccessExtraText": {
"message": "بينما نعمل على هذه المسألة، يمكنك إضافة هذا الموقع إلى قائمة الاستبعاد في إعدادات الإضافة أو تعطيل الإضافة لهذا الموقع بالنقر فوق أيقونة الإضافة في شريط الأدوات الخاص بالمتصفح"
},
"report_submitSuccessText": {
"message": "تم إرسال التقرير!"
},
"report_urlInputError": {
"message": "يرجى إدخال عنوان URL صالح لا يزيد عن 1000 حرف"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -3,7 +3,7 @@
"message": "Hat jemand Cookie-Einwilligungsdialoge gesagt? 😋"
},
"contextMenu_issueOption": {
"message": "Folgen Sie diesem Problem auf GitHub"
"message": "Folgen Sie diesem Problem"
},
"contextMenu_reportOption": {
"message": "Diese Website melden"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Liste leeren"
},
"options_empty": {
"message": "Keine Ausschlüsse gefunden"
},
"options_exclusionListTitle": {
"message": "Ausschlussliste"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Ein Problem wurde für diese Seite gemeldet, wird jedoch nicht behoben. Bitte beachten Sie, dass die Erweiterung auf dieser Seite möglicherweise nicht wie erwartet funktioniert. Wir empfehlen dringend, die Erweiterung über die Schaltfläche unten zu deaktivieren."
},
"popup_bannerSupport": {
"message": "Diese Seite wird nicht mehr unterstützt. Für weitere Informationen lesen Sie bitte hier weiter"
},
"popup_bannerUpdateAvailable": {
"message": "Update verfügbar. Bitte installieren Sie die neueste Version, um bekannte Probleme zu beheben, bevor Sie neue melden"
},
@ -57,7 +63,7 @@
"message": "Bewerten Sie diese Erweiterung"
},
"report_bodyText": {
"message": "Bitte geben Sie in diesem Bericht keine persönlichen Informationen weiter. Wenn Sie weitere Details hinzufügen möchten, öffnen Sie das GitHub-Problem im nächsten Schritt und fügen Sie einen Kommentar hinzu."
"message": "Bitte teilen Sie in diesem Bericht keine persönlichen Informationen. Wenn Sie weitere Details hinzufügen möchten, gehen Sie im nächsten Schritt zum Problem und fügen Sie einen Kommentar hinzu."
},
"report_cancelButtonText": {
"message": "Abbrechen"

View File

@ -3,7 +3,7 @@
"message": "Did someone say cookie consent dialogs? 😋"
},
"contextMenu_issueOption": {
"message": "Follow this issue in GitHub"
"message": "Follow this issue"
},
"contextMenu_reportOption": {
"message": "Report this website"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Clear list"
},
"options_empty": {
"message": "No exclusions found"
},
"options_exclusionListTitle": {
"message": "Exclusion list"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "An issue has been reported for this page, but it won't be fixed. Please be aware that the extension may not function as expected on this page. We highly recommend turning off the extension using the button below"
},
"popup_bannerSupport": {
"message": "This site is no longer supported. For more information, please keep reading here"
},
"popup_bannerUpdateAvailable": {
"message": "Update available. Please install the latest version to address known issues before reporting new ones"
},
@ -57,7 +63,7 @@
"message": "Rate this extension"
},
"report_bodyText": {
"message": "Please, do not share any personal information in this report. If you want to add more details, open the GitHub issue in the next step and add a comment."
"message": "Please, do not share any personal information in this report. If you want to add more details, go to the issue in the next step and add a comment."
},
"report_cancelButtonText": {
"message": "Cancel"

View File

@ -3,7 +3,7 @@
"message": "¿Alguien mencionó los diálogos de consentimiento de cookies? 😋"
},
"contextMenu_issueOption": {
"message": "Sigue este problema en GitHub"
"message": "Sigue este problema"
},
"contextMenu_reportOption": {
"message": "Reportar este sitio web"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Borrar lista"
},
"options_empty": {
"message": "No se encontraron exclusiones"
},
"options_exclusionListTitle": {
"message": "Lista de exclusión"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Se ha informado de un problema en esta página, pero no se solucionará. Tenga en cuenta que la extensión puede no funcionar como se espera en esta página. Recomendamos encarecidamente desactivar la extensión utilizando el botón a continuación."
},
"popup_bannerSupport": {
"message": "Este sitio ya no es compatible. Para más información, sigue leyendo aquí"
},
"popup_bannerUpdateAvailable": {
"message": "Actualización disponible. Por favor, instale la última versión para solucionar problemas conocidos antes de informar nuevos"
},
@ -57,7 +63,7 @@
"message": "Califica esta extensión"
},
"report_bodyText": {
"message": "Por favor, no compartas ninguna información personal en este informe. Si deseas agregar más detalles, abre el problema de GitHub en el siguiente paso y agrega un comentario."
"message": "Por favor, no compartas información personal en este informe. Si deseas agregar más detalles, ve al problema en el siguiente paso y añade un comentario."
},
"report_cancelButtonText": {
"message": "Cancelar"

View File

@ -3,7 +3,7 @@
"message": "Quelqu'un a parlé de boîtes de dialogue de consentement aux cookies ? 😋"
},
"contextMenu_issueOption": {
"message": "Suivre ce problème sur GitHub"
"message": "Suivre ce problème"
},
"contextMenu_reportOption": {
"message": "Signaler ce site web"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Vider la liste"
},
"options_empty": {
"message": "Aucune exclusion trouvée"
},
"options_exclusionListTitle": {
"message": "Liste d'exclusion"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Un problème a été signalé pour cette page, mais il ne sera pas résolu. Veuillez noter que l'extension pourrait ne pas fonctionner comme prévu sur cette page. Nous vous recommandons vivement de désactiver l'extension en utilisant le bouton ci-dessous."
},
"popup_bannerSupport": {
"message": "Ce site n'est plus pris en charge. Pour plus d'informations, veuillez continuer à lire ici"
},
"popup_bannerUpdateAvailable": {
"message": "Mise à jour disponible. Veuillez installer la dernière version pour résoudre les problèmes connus avant d'en signaler de nouveaux"
},
@ -57,7 +63,7 @@
"message": "Évaluer cette extension"
},
"report_bodyText": {
"message": "Veuillez ne pas partager d'informations personnelles dans ce rapport. Si vous souhaitez ajouter plus de détails, ouvrez le problème GitHub à l'étape suivante et ajoutez un commentaire."
"message": "Veuillez ne partager aucune information personnelle dans ce rapport. Si vous souhaitez ajouter plus de détails, allez à l'issue à l'étape suivante et ajoutez un commentaire."
},
"report_cancelButtonText": {
"message": "Annuler"

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "क्या किसी ने कुकी सहमति संवाद का उल्लेख किया? 😋"
},
"contextMenu_issueOption": {
"message": "इस समस्या का पालन करें"
},
"contextMenu_reportOption": {
"message": "इस वेबसाइट की रिपोर्ट करें"
},
"contextMenu_settingsOption": {
"message": "बहिष्करण सूची प्रबंधित करें"
},
"options_addButton": {
"message": "बहिष्करण जोड़ें"
},
"options_addPrompt": {
"message": "डोमेन दर्ज करें"
},
"options_clearButton": {
"message": "सूची साफ़ करें"
},
"options_empty": {
"message": "कोई बहिष्कार नहीं मिला।"
},
"options_exclusionListTitle": {
"message": "बहिष्करण सूची"
},
"options_exportButton": {
"message": "सूची निर्यात करें"
},
"options_filterPlaceholder": {
"message": "टाइप करने के बाद ENTER दबाएं फ़िल्टर करने के लिए"
},
"options_importButton": {
"message": "सूची आयात करें"
},
"popup_bannerIssueOpen": {
"message": "इस पृष्ठ के लिए एक समस्या दर्ज की गई है। अस्थायी रूप से एक्सटेंशन ठीक से काम नहीं कर सकता है। समस्या का पालन करें या अधिक जानकारी जोड़ें"
},
"popup_bannerIssueWontFix": {
"message": "इस पृष्ठ के लिए एक समस्या दर्ज की गई है, लेकिन इसे ठीक नहीं किया जाएगा। कृपया ध्यान दें कि एक्सटेंशन इस पृष्ठ पर अपेक्षित रूप से कार्य नहीं कर सकता है। हम नीचे दिए गए बटन से एक्सटेंशन को बंद करने की सलाह देते हैं"
},
"popup_bannerSupport": {
"message": "इस साइट को अब समर्थन प्राप्त नहीं है। अधिक जानकारी के लिए कृपया यहाँ पढ़ें"
},
"popup_bannerUpdateAvailable": {
"message": "अपडेट उपलब्ध है। कृपया नई समस्याओं की रिपोर्ट करने से पहले ज्ञात मुद्दों को हल करने के लिए नवीनतम संस्करण इंस्टॉल करें"
},
"popup_contributeOption": {
"message": "इस प्रोजेक्ट में योगदान करें"
},
"popup_databaseVersion": {
"message": "डेटाबेस संस्करण"
},
"popup_extensionVersion": {
"message": "एक्सटेंशन संस्करण"
},
"popup_helpOption": {
"message": "मदद या समस्याएँ?"
},
"popup_rateOption": {
"message": "इस एक्सटेंशन को रेट करें"
},
"report_bodyText": {
"message": "कृपया इस रिपोर्ट में कोई भी व्यक्तिगत जानकारी साझा न करें। यदि आप अधिक विवरण जोड़ना चाहते हैं, तो अगले चरण में समस्या पर जाएं और एक टिप्पणी जोड़ें।"
},
"report_cancelButtonText": {
"message": "रद्द करें"
},
"report_reasonInputError": {
"message": "कृपया कम से कम 10 अक्षरों और 1000 से अधिक नहीं अक्षरों के साथ एक कारण दर्ज करें"
},
"report_reasonInputLabel": {
"message": "कारण"
},
"report_reasonInputPlaceholder": {
"message": "पॉपअप दिखाई दिया"
},
"report_submitErrorExtraText": {
"message": "ऐसा लगता है कि पहले से ही एक मुद्दा खुला है, या इसे 'फिक्स नहीं होगा' के रूप में चिह्नित किया गया है। कृपया अधिक जानकारी जोड़ने के लिए अगले बटन का उपयोग करके समस्या में जाएं।"
},
"report_submitErrorText": {
"message": "रिपोर्ट नहीं भेजी गई?"
},
"report_submitSuccessExtraText": {
"message": "जब तक हम इस पर काम कर रहे हैं, तब तक आप इस वेबसाइट को एक्सटेंशन सेटिंग में बहिष्करण सूची में जोड़ सकते हैं या अपने ब्राउज़र टूलबार में एक्सटेंशन आइकन पर क्लिक करके इस वेबसाइट के लिए एक्सटेंशन को अक्षम कर सकते हैं"
},
"report_submitSuccessText": {
"message": "रिपोर्ट भेज दी गई!"
},
"report_urlInputError": {
"message": "कृपया अधिकतम 1000 अक्षरों के साथ एक मान्य URL दर्ज करें"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "Ada yang bilang dialog persetujuan cookie? 😋"
},
"contextMenu_issueOption": {
"message": "Ikuti masalah ini"
},
"contextMenu_reportOption": {
"message": "Laporkan situs ini"
},
"contextMenu_settingsOption": {
"message": "Kelola daftar pengecualian"
},
"options_addButton": {
"message": "Tambahkan pengecualian"
},
"options_addPrompt": {
"message": "Masukkan domain"
},
"options_clearButton": {
"message": "Bersihkan daftar"
},
"options_empty": {
"message": "Tidak ada pengecualian ditemukan"
},
"options_exclusionListTitle": {
"message": "Daftar pengecualian"
},
"options_exportButton": {
"message": "Ekspor daftar"
},
"options_filterPlaceholder": {
"message": "Tekan ENTER untuk memfilter setelah mengetik"
},
"options_importButton": {
"message": "Impor daftar"
},
"popup_bannerIssueOpen": {
"message": "Ada masalah yang dilaporkan untuk halaman ini. Ekstensi mungkin tidak berfungsi seperti yang diharapkan untuk sementara. Ikuti masalahnya atau tambahkan informasi lainnya"
},
"popup_bannerIssueWontFix": {
"message": "Ada masalah yang dilaporkan untuk halaman ini, tetapi tidak akan diperbaiki. Harap diperhatikan bahwa ekstensi mungkin tidak berfungsi seperti yang diharapkan di halaman ini. Kami sangat menyarankan untuk mematikan ekstensi menggunakan tombol di bawah ini"
},
"popup_bannerSupport": {
"message": "Situs ini tidak lagi didukung. Untuk informasi lebih lanjut, silakan baca di sini"
},
"popup_bannerUpdateAvailable": {
"message": "Pembaruan tersedia. Harap instal versi terbaru untuk menyelesaikan masalah yang diketahui sebelum melaporkan masalah baru"
},
"popup_contributeOption": {
"message": "Berkontribusi pada proyek ini"
},
"popup_databaseVersion": {
"message": "Versi basis data"
},
"popup_extensionVersion": {
"message": "Versi ekstensi"
},
"popup_helpOption": {
"message": "Butuh bantuan atau ada masalah?"
},
"popup_rateOption": {
"message": "Beri peringkat pada ekstensi ini"
},
"report_bodyText": {
"message": "Jangan membagikan informasi pribadi apa pun dalam laporan ini. Jika Anda ingin menambahkan lebih banyak detail, buka masalah pada langkah berikutnya dan tambahkan komentar."
},
"report_cancelButtonText": {
"message": "Batal"
},
"report_reasonInputError": {
"message": "Harap masukkan alasan yang terdiri dari minimal 10 karakter dan maksimal 1000 karakter"
},
"report_reasonInputLabel": {
"message": "Alasan"
},
"report_reasonInputPlaceholder": {
"message": "Muncul pop-up"
},
"report_submitErrorExtraText": {
"message": "Tampaknya sudah ada masalah yang terbuka atau ditandai sebagai 'tidak akan diperbaiki'. Silakan buka masalah menggunakan tombol di bawah ini untuk menambahkan lebih banyak informasi."
},
"report_submitErrorText": {
"message": "Laporan tidak terkirim?"
},
"report_submitSuccessExtraText": {
"message": "Sambil kami menangani masalah ini, Anda dapat menambahkan situs web ini ke daftar pengecualian di pengaturan ekstensi atau menonaktifkan ekstensi untuk situs ini dengan mengklik ikon ekstensi di bilah alat peramban Anda"
},
"report_submitSuccessText": {
"message": "Laporan terkirim!"
},
"report_urlInputError": {
"message": "Masukkan URL yang valid dengan maksimal 1000 karakter"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -3,7 +3,7 @@
"message": "Qualcuno ha parlato di dialoghi di consenso sui cookie? 😋"
},
"contextMenu_issueOption": {
"message": "Segui questo problema su GitHub"
"message": "Segui questo problema"
},
"contextMenu_reportOption": {
"message": "Segnala questo sito web"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Cancella lista"
},
"options_empty": {
"message": "Nessuna esclusione trovata"
},
"options_exclusionListTitle": {
"message": "Lista di esclusione"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "È stato segnalato un problema per questa pagina, ma non verrà risolto. Si prega di notare che l'estensione potrebbe non funzionare come previsto su questa pagina. Si consiglia vivamente di disattivare l'estensione utilizzando il pulsante qui sotto."
},
"popup_bannerSupport": {
"message": "Questo sito non è più supportato. Per ulteriori informazioni, continua a leggere qui"
},
"popup_bannerUpdateAvailable": {
"message": "Aggiornamento disponibile. Si prega di installare l'ultima versione per risolvere i problemi noti prima di segnalarne di nuovi"
},
@ -57,7 +63,7 @@
"message": "Valuta questa estensione"
},
"report_bodyText": {
"message": "Per favore, non condividere informazioni personali in questo rapporto. Se vuoi aggiungere più dettagli, apri il problema su GitHub nel passaggio successivo e aggiungi un commento."
"message": "Per favore, non condividere informazioni personali in questo rapporto. Se desideri aggiungere ulteriori dettagli, vai al problema nel prossimo passaggio e aggiungi un commento."
},
"report_cancelButtonText": {
"message": "Annulla"

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "クッキー同意ダイアログの話をしている? 😋"
},
"contextMenu_issueOption": {
"message": "この問題をフォローする"
},
"contextMenu_reportOption": {
"message": "このウェブサイトを報告する"
},
"contextMenu_settingsOption": {
"message": "除外リストを管理する"
},
"options_addButton": {
"message": "除外を追加"
},
"options_addPrompt": {
"message": "ドメインを入力してください"
},
"options_clearButton": {
"message": "リストをクリア"
},
"options_empty": {
"message": "除外項目が見つかりませんでした"
},
"options_exclusionListTitle": {
"message": "除外リスト"
},
"options_exportButton": {
"message": "リストをエクスポート"
},
"options_filterPlaceholder": {
"message": "入力後にENTERキーを押してフィルタリング"
},
"options_importButton": {
"message": "リストをインポート"
},
"popup_bannerIssueOpen": {
"message": "このページで問題が報告されました。拡張機能が一時的に正常に動作しない可能性があります。問題をフォローするか、追加情報を提供してください"
},
"popup_bannerIssueWontFix": {
"message": "このページで問題が報告されましたが、修正されません。このページで拡張機能が期待通りに機能しない可能性があります。以下のボタンで拡張機能をオフにすることをお勧めします"
},
"popup_bannerSupport": {
"message": "このサイトはもうサポートされていません。詳しくは、ここをお読みください"
},
"popup_bannerUpdateAvailable": {
"message": "アップデートが利用可能です。新しい問題を報告する前に、既知の問題を解決するため最新バージョンをインストールしてください"
},
"popup_contributeOption": {
"message": "このプロジェクトに貢献する"
},
"popup_databaseVersion": {
"message": "データベースバージョン"
},
"popup_extensionVersion": {
"message": "拡張機能のバージョン"
},
"popup_helpOption": {
"message": "ヘルプまたは問題?"
},
"popup_rateOption": {
"message": "この拡張機能を評価する"
},
"report_bodyText": {
"message": "このレポートには個人情報を含めないでください。追加の詳細を提供する場合は、次のステップで問題に移動し、コメントを追加してください。"
},
"report_cancelButtonText": {
"message": "キャンセル"
},
"report_reasonInputError": {
"message": "10文字以上、1000文字以下の理由を入力してください"
},
"report_reasonInputLabel": {
"message": "理由"
},
"report_reasonInputPlaceholder": {
"message": "ポップアップが表示されました"
},
"report_submitErrorExtraText": {
"message": "既に問題が開かれているか、「修正しない」とマークされています。追加情報を提供する場合は、次のボタンから問題に移動してください。"
},
"report_submitErrorText": {
"message": "レポートが送信されませんでしたか?"
},
"report_submitSuccessExtraText": {
"message": "処理が完了するまでの間、このウェブサイトを拡張機能の設定で除外リストに追加するか、ブラウザツールバーの拡張アイコンをクリックしてこのウェブサイトで拡張機能を無効にしてください"
},
"report_submitSuccessText": {
"message": "レポートが送信されました!"
},
"report_urlInputError": {
"message": "1000文字以下の有効なURLを入力してください"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "쿠키 동의 대화 상자를 말한 사람이 있나요? 😋"
},
"contextMenu_issueOption": {
"message": "이 문제를 팔로우하기"
},
"contextMenu_reportOption": {
"message": "이 웹사이트 신고하기"
},
"contextMenu_settingsOption": {
"message": "제외 목록 관리하기"
},
"options_addButton": {
"message": "제외 추가"
},
"options_addPrompt": {
"message": "도메인 입력"
},
"options_clearButton": {
"message": "목록 지우기"
},
"options_empty": {
"message": "제외 항목을 찾을 수 없습니다"
},
"options_exclusionListTitle": {
"message": "제외 목록"
},
"options_exportButton": {
"message": "목록 내보내기"
},
"options_filterPlaceholder": {
"message": "입력 후 ENTER를 눌러 필터링"
},
"options_importButton": {
"message": "목록 가져오기"
},
"popup_bannerIssueOpen": {
"message": "이 페이지에 대한 문제가 보고되었습니다. 확장이 일시적으로 예상대로 작동하지 않을 수 있습니다. 문제를 팔로우하거나 추가 정보를 제공하십시오"
},
"popup_bannerIssueWontFix": {
"message": "이 페이지에 대한 문제가 보고되었지만 수정되지 않습니다. 이 페이지에서 확장이 예상대로 작동하지 않을 수 있습니다. 아래 버튼을 사용하여 확장을 끄는 것이 좋습니다"
},
"popup_bannerSupport": {
"message": "이 사이트는 더 이상 지원되지 않습니다. 자세한 내용은 여기를 계속 읽어 주세요"
},
"popup_bannerUpdateAvailable": {
"message": "업데이트 가능. 새 문제를 보고하기 전에 알려진 문제를 해결하려면 최신 버전을 설치하십시오"
},
"popup_contributeOption": {
"message": "이 프로젝트에 기여하기"
},
"popup_databaseVersion": {
"message": "데이터베이스 버전"
},
"popup_extensionVersion": {
"message": "확장 버전"
},
"popup_helpOption": {
"message": "도움말 또는 문제?"
},
"popup_rateOption": {
"message": "이 확장 평가하기"
},
"report_bodyText": {
"message": "이 보고서에 개인 정보를 공유하지 마십시오. 더 자세한 내용을 추가하려면 다음 단계에서 문제로 이동하여 댓글을 추가하십시오."
},
"report_cancelButtonText": {
"message": "취소"
},
"report_reasonInputError": {
"message": "10자 이상, 1000자 이하의 이유를 입력하십시오"
},
"report_reasonInputLabel": {
"message": "이유"
},
"report_reasonInputPlaceholder": {
"message": "팝업이 나타났습니다"
},
"report_submitErrorExtraText": {
"message": "이미 열린 문제가 있거나 '수정하지 않음'으로 표시된 것 같습니다. 추가 정보를 추가하려면 다음 버튼을 사용하여 문제로 이동하십시오."
},
"report_submitErrorText": {
"message": "보고서가 전송되지 않았습니까?"
},
"report_submitSuccessExtraText": {
"message": "이 문제를 처리하는 동안 확장 설정에서 제외 목록에 이 웹사이트를 추가하거나 브라우저 툴바의 확장 아이콘을 클릭하여 이 웹사이트에서 확장을 비활성화할 수 있습니다"
},
"report_submitSuccessText": {
"message": "보고서가 전송되었습니다!"
},
"report_urlInputError": {
"message": "유효한 URL을 1000자 이하로 입력하십시오"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -3,7 +3,7 @@
"message": "Czy ktoś wspomniał o dialogach zgody na pliki cookie? 😋"
},
"contextMenu_issueOption": {
"message": "Śledź ten problem na GitHub"
"message": "Śledź ten problem"
},
"contextMenu_reportOption": {
"message": "Zgłoś tę stronę"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Wyczyść listę"
},
"options_empty": {
"message": "Nie znaleziono wykluczeń"
},
"options_exclusionListTitle": {
"message": "Lista wykluczeń"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Zgłoszono problem z tą stroną, ale nie zostanie on naprawiony. Należy pamiętać, że rozszerzenie może nie działać zgodnie z oczekiwaniami na tej stronie. Zdecydowanie zalecamy wyłączenie rozszerzenia za pomocą poniższego przycisku."
},
"popup_bannerSupport": {
"message": "Ta strona nie jest już obsługiwana. Aby uzyskać więcej informacji, proszę czytać dalej tutaj"
},
"popup_bannerUpdateAvailable": {
"message": "Dostępna aktualizacja. Zainstaluj najnowszą wersję, aby rozwiązać znane problemy przed zgłoszeniem nowych"
},
@ -57,7 +63,7 @@
"message": "Oceń to rozszerzenie"
},
"report_bodyText": {
"message": "Prosimy, nie udostępniaj żadnych danych osobowych w tym raporcie. Jeśli chcesz dodać więcej szczegółów, otwórz zgłoszenie na GitHub w następnym kroku i dodaj komentarz."
"message": "Pros, nie udostępniaj żadnych danych osobowych w tym raporcie. Jeśli chcesz dodać więcej szczegółów, przejdź do zgłoszenia w kolejnym kroku i dodaj komentarz."
},
"report_cancelButtonText": {
"message": "Anuluj"

View File

@ -3,7 +3,7 @@
"message": "Alguém falou sobre diálogos de consentimento de cookies? 😋"
},
"contextMenu_issueOption": {
"message": "Siga este problema no GitHub"
"message": "Siga este problema"
},
"contextMenu_reportOption": {
"message": "Denunciar este site"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Limpar lista"
},
"options_empty": {
"message": "Nenhuma exclusão encontrada"
},
"options_exclusionListTitle": {
"message": "Lista de exclusão"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Um problema foi relatado para esta página, mas não será corrigido. Esteja ciente de que a extensão pode não funcionar conforme o esperado nesta página. Recomendamos desativar a extensão usando o botão abaixo."
},
"popup_bannerSupport": {
"message": "Este site não é mais compatível. Para mais informações, continue lendo aqui"
},
"popup_bannerUpdateAvailable": {
"message": "Atualização disponível. Por favor, instale a versão mais recente para corrigir problemas conhecidos antes de relatar novos"
},
@ -57,7 +63,7 @@
"message": "Avalie esta extensão"
},
"report_bodyText": {
"message": "Por favor, não compartilhe nenhuma informação pessoal neste relatório. Se você quiser adicionar mais detalhes, abra o problema no GitHub na próxima etapa e adicione um comentário."
"message": "Por favor, não compartilhe informações pessoais neste relatório. Se quiser adicionar mais detalhes, vá até o problema no próximo passo e adicione um comentário."
},
"report_cancelButtonText": {
"message": "Cancelar"

View File

@ -3,7 +3,7 @@
"message": "Alguém mencionou diálogos de consentimento de cookies? 😋"
},
"contextMenu_issueOption": {
"message": "Siga este problema no GitHub"
"message": "Siga este problema"
},
"contextMenu_reportOption": {
"message": "Reportar este site"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Limpar lista"
},
"options_empty": {
"message": "Nenhuma exclusão encontrada"
},
"options_exclusionListTitle": {
"message": "Lista de exclusão"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Foi relatado um problema para esta página, mas não será resolvido. Esteja ciente de que a extensão pode não funcionar conforme o esperado nesta página. Recomendamos desativar a extensão usando o botão abaixo."
},
"popup_bannerSupport": {
"message": "Este site já não é suportado. Para mais informações, continue a ler aqui"
},
"popup_bannerUpdateAvailable": {
"message": "Atualização disponível. Por favor, instale a versão mais recente para resolver problemas conhecidos antes de relatar novos"
},
@ -57,7 +63,7 @@
"message": "Avalie esta extensão"
},
"report_bodyText": {
"message": "Por favor, não partilhe nenhuma informação pessoal neste relatório. Se quiser adicionar mais detalhes, abra o problema no GitHub na próxima etapa e adicione um comentário."
"message": "Por favor, não partilhe informações pessoais neste relatório. Se quiser adicionar mais detalhes, vá até ao problema no próximo passo e adicione um comentário."
},
"report_cancelButtonText": {
"message": "Cancelar"

View File

@ -3,7 +3,7 @@
"message": "A menționat cineva dialogurile de consimțământ pentru cookie-uri? 😋"
},
"contextMenu_issueOption": {
"message": "Urmărește această problemă pe GitHub"
"message": "Urmărește această problemă"
},
"contextMenu_reportOption": {
"message": "Raportează acest site web"
@ -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"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "A fost raportată o problemă pentru această pagină, dar nu va fi rezolvată. Rețineți că extensia poate să nu funcționeze conform așteptărilor pe această pagină. Vă recomandăm cu insistență să dezactivați extensia folosind butonul de mai jos."
},
"popup_bannerSupport": {
"message": "Acest site nu mai este acceptat. Pentru mai multe informații, continuați să citiți aici"
},
"popup_bannerUpdateAvailable": {
"message": "Actualizare disponibilă. Vă rugăm să instalați cea mai recentă versiune pentru a rezolva problemele cunoscute înainte de a raporta altele noi."
},
@ -57,7 +63,7 @@
"message": "Evaluează această extensie"
},
"report_bodyText": {
"message": "Te rugăm să nu împărtășești informații personale în acest raport. Dacă dorești să adaugi mai multe detalii, deschide problema GitHub în pasul următor și adaugă un comentariu."
"message": "Vă rugăm să nu împărtășiți informații personale în acest raport. Dacă doriți să adăugați mai multe detalii, accesați problema în pasul următor și adăugați un comentariu."
},
"report_cancelButtonText": {
"message": "Anulează"

View File

@ -3,7 +3,7 @@
"message": "Кто-то сказал диалоги согласия на использование cookie? 😋"
},
"contextMenu_issueOption": {
"message": "Следить за этим вопросом на GitHub"
"message": "Следить за этой проблемой"
},
"contextMenu_reportOption": {
"message": "Сообщить об этом сайте"
@ -20,6 +20,9 @@
"options_clearButton": {
"message": "Очистить список"
},
"options_empty": {
"message": "Исключения не найдены"
},
"options_exclusionListTitle": {
"message": "Список исключений"
},
@ -38,6 +41,9 @@
"popup_bannerIssueWontFix": {
"message": "Для этой страницы было сообщено о проблеме, но она не будет исправлена. Обратите внимание, что расширение может работать некорректно на этой странице. Мы настоятельно рекомендуем отключить расширение, используя кнопку ниже."
},
"popup_bannerSupport": {
"message": "Этот сайт больше не поддерживается. Для получения дополнительной информации продолжайте читать здесь"
},
"popup_bannerUpdateAvailable": {
"message": "Доступно обновление. Установите последнюю версию, чтобы устранить известные проблемы перед отправкой новых отчётов."
},
@ -57,7 +63,7 @@
"message": "Оцените это расширение"
},
"report_bodyText": {
"message": "Пожалуйста, не делитесь личной информацией в этом отчете. Если вы хотите добавить больше деталей, откройте вопрос на GitHub на следующем шаге и добавьте комментарий."
"message": "Пожалуйста, не делитесь личной информацией в этом отчете. Если вы хотите добавить больше деталей, перейдите к проблеме на следующем этапе и добавьте комментарий."
},
"report_cancelButtonText": {
"message": "Отмена"

View File

@ -0,0 +1,98 @@
{
"appDesc": {
"message": "Biri çerez onayı diyaloglarını mı söyledi? 😋"
},
"contextMenu_issueOption": {
"message": "Bu sorunu takip et"
},
"contextMenu_reportOption": {
"message": "Bu web sitesini bildir"
},
"contextMenu_settingsOption": {
"message": "Hariç tutma listesini yönet"
},
"options_addButton": {
"message": "Hariç tutma ekle"
},
"options_addPrompt": {
"message": "Alan adı girin"
},
"options_clearButton": {
"message": "Listeyi temizle"
},
"options_empty": {
"message": "Hariç tutma bulunamadı"
},
"options_exclusionListTitle": {
"message": "Hariç tutma listesi"
},
"options_exportButton": {
"message": "Listeyi dışa aktar"
},
"options_filterPlaceholder": {
"message": "Yazdıktan sonra ENTER tuşuna basarak filtreleyin"
},
"options_importButton": {
"message": "Listeyi içe aktar"
},
"popup_bannerIssueOpen": {
"message": "Bu sayfa için bir sorun bildirildi. Uzantı geçici olarak beklenildiği gibi çalışmayabilir. Sorunu takip edin veya daha fazla bilgi ekleyin"
},
"popup_bannerIssueWontFix": {
"message": "Bu sayfa için bir sorun bildirildi ancak düzeltilmeyecek. Bu sayfada uzantının beklenildiği gibi çalışmayabileceğini unutmayın. Aşağıdaki düğmeyi kullanarak uzantıyı kapatmanızı öneririz"
},
"popup_bannerSupport": {
"message": "Bu site artık desteklenmiyor. Daha fazla bilgi için lütfen buradan okumaya devam edin"
},
"popup_bannerUpdateAvailable": {
"message": "Güncelleme mevcut. Yeni sorunları bildirmeden önce bilinen sorunları gidermek için lütfen en son sürümü yükleyin"
},
"popup_contributeOption": {
"message": "Bu projeye katkıda bulun"
},
"popup_databaseVersion": {
"message": "Veritabanı sürümü"
},
"popup_extensionVersion": {
"message": "Uzantı sürümü"
},
"popup_helpOption": {
"message": "Yardım veya sorunlar?"
},
"popup_rateOption": {
"message": "Bu uzantıyı değerlendir"
},
"report_bodyText": {
"message": "Bu raporda kişisel bilgi paylaşmayın. Daha fazla ayrıntı eklemek istiyorsanız, bir sonraki adımda soruna gidin ve yorum ekleyin."
},
"report_cancelButtonText": {
"message": "İptal"
},
"report_reasonInputError": {
"message": "Lütfen en az 10 ve en fazla 1000 karakterlik bir sebep girin"
},
"report_reasonInputLabel": {
"message": "Sebep"
},
"report_reasonInputPlaceholder": {
"message": "Açılır pencere göründü"
},
"report_submitErrorExtraText": {
"message": "Zaten açık bir sorun var gibi görünüyor veya 'düzeltmeyecek' olarak işaretlenmiş. Aşağıdaki düğmeyi kullanarak sorun sayfasına giderek daha fazla bilgi ekleyin."
},
"report_submitErrorText": {
"message": "Rapor gönderilmedi mi?"
},
"report_submitSuccessExtraText": {
"message": "Bu sorun üzerinde çalışırken, uzantı ayarlarında bu web sitesini hariç tutma listesine ekleyebilir veya tarayıcı araç çubuğundaki uzantı simgesine tıklayarak bu web sitesi için uzantıyı devre dışı bırakabilirsiniz"
},
"report_submitSuccessText": {
"message": "Rapor gönderildi!"
},
"report_urlInputError": {
"message": "Lütfen en fazla 1000 karakterle geçerli bir URL girin"
},
"report_urlInputLabel": {
"message": "URL"
}
}

View File

@ -0,0 +1 @@
export default 'off-icon';

View File

@ -0,0 +1 @@
export default 'on-icon';

View File

@ -0,0 +1 @@
export default 'warn-icon';

View File

@ -0,0 +1,56 @@
import { jest } from '@jest/globals';
chrome.action = {
...chrome.action,
openPopup: jest.fn(),
setBadgeBackgroundColor: jest.fn(),
setBadgeText: jest.fn(),
setIcon: jest.fn(),
} as typeof chrome.action;
chrome.contextMenus = {
...chrome.contextMenus,
create: jest.fn(),
onClicked: {
...(chrome.contextMenus ?? {}).onClicked,
addListener: jest.fn(),
},
removeAll: jest.fn(),
} as typeof chrome.contextMenus;
chrome.declarativeNetRequest = {
...chrome.declarativeNetRequest,
updateSessionRules: jest.fn(),
} as typeof chrome.declarativeNetRequest;
chrome.runtime = {
...chrome.runtime,
getManifest: jest.fn(
() =>
({
content_scripts: [{ matches: ['https://example.com/*'] }],
version: '1.0.0',
}) as chrome.runtime.ManifestV3
),
onInstalled: {
...(chrome.runtime ?? {}).onInstalled,
addListener: jest.fn(),
},
onStartup: {
...(chrome.runtime ?? {}).onStartup,
addListener: jest.fn(),
},
openOptionsPage: jest.fn(),
} as typeof chrome.runtime;
chrome.webRequest = {
...chrome.webRequest,
onBeforeRequest: {
...(chrome.webRequest ?? {}).onBeforeRequest,
addListener: jest.fn(),
},
onErrorOccurred: {
...(chrome.webRequest ?? {}).onErrorOccurred,
addListener: jest.fn(),
},
} as typeof chrome.webRequest;

View File

@ -0,0 +1,5 @@
import { jest } from '@jest/globals';
jest.mock('@plasmohq/messaging', () => ({
sendToContentScript: jest.fn(),
}));

View File

@ -0,0 +1,6 @@
import { jest } from '@jest/globals';
jest.mock('~utils/domain', () => ({
formatDomainFromURL: jest.fn(() => 'example.com'),
validateSupport: jest.fn(),
}));

View File

@ -0,0 +1,9 @@
import { jest } from '@jest/globals';
jest.mock('~utils/storage', () => ({
storage: {
get: async (key: string) => (await chrome.storage.local.get(key))?.[key],
remove: (key: string) => chrome.storage.local.remove(key),
set: (key: string, value: any) => chrome.storage.local.set({ [key]: value }),
},
}));

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,11 @@
import onClicked from './utils/contextMenus/onClicked';
import onInstalled from './utils/runtime/onInstalled';
import onStartup from './utils/runtime/onStartup';
import onBeforeRequest from './utils/webRequest/onBeforeRequest';
import onErrorOccurred from './utils/webRequest/onErrorOccurred';
chrome.contextMenus.onClicked.addListener(onClicked);
chrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onStartup.addListener(onStartup);
chrome.webRequest.onBeforeRequest.addListener(onBeforeRequest as any, { urls: ['<all_urls>'] });
chrome.webRequest.onErrorOccurred.addListener(onErrorOccurred, { 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,15 @@
import { REPORT_MENU_ITEM_ID, SETTINGS_MENU_ITEM_ID } from '~utils/constants';
import { suppressLastError } from '~utils/error';
export default function onClicked(data: chrome.contextMenus.OnClickData) {
switch (data.menuItemId) {
case REPORT_MENU_ITEM_ID:
chrome.action.openPopup(suppressLastError);
break;
case SETTINGS_MENU_ITEM_ID:
chrome.runtime.openOptionsPage(suppressLastError);
break;
default:
break;
}
}

View File

@ -0,0 +1,38 @@
import databaseRefreshHandler from '~background/messages/database/refresh';
import {
EXTENSION_MENU_ITEM_ID,
REPORT_MENU_ITEM_ID,
SETTINGS_MENU_ITEM_ID,
} from '~utils/constants';
import { noop } from '~utils/error';
import { storage } from '~utils/storage';
export default async function onInstalled() {
await chrome.contextMenus.removeAll();
const documentUrlPatterns = chrome.runtime.getManifest().content_scripts?.[0].matches;
await chrome.contextMenus.create({
contexts: ['all'],
documentUrlPatterns,
id: EXTENSION_MENU_ITEM_ID,
title: 'Cookie Dialog Monster',
});
await chrome.contextMenus.create({
contexts: ['all'],
documentUrlPatterns,
id: SETTINGS_MENU_ITEM_ID,
parentId: EXTENSION_MENU_ITEM_ID,
title: chrome.i18n.getMessage('contextMenu_settingsOption'),
});
await chrome.contextMenus.create({
contexts: ['all'],
documentUrlPatterns,
id: REPORT_MENU_ITEM_ID,
parentId: EXTENSION_MENU_ITEM_ID,
title: chrome.i18n.getMessage('contextMenu_reportOption'),
});
await storage.remove('updateAvailable');
await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop });
}

View File

@ -0,0 +1,10 @@
import databaseRefreshHandler from '~background/messages/database/refresh';
import extensionUpdateAvailableHandler from '~background/messages/extension/updateAvailable';
import { noop } from '~utils/error';
import { storage } from '~utils/storage';
export default async function onStartup() {
await storage.remove('updateAvailable');
await databaseRefreshHandler({ name: 'database/refresh' }, { send: noop });
await extensionUpdateAvailableHandler({ name: 'extension/updateAvailable' }, { send: noop });
}

View File

@ -0,0 +1,32 @@
import { DEFAULT_DOMAIN_CONFIG, DEFAULT_EXTENSION_DATA } from '~utils/constants';
import { formatDomainFromURL, validateSupport } from '~utils/domain';
import { storage } from '~utils/storage';
import type { DomainConfig, ExtensionData } from '~utils/types';
export default async function onBeforeRequest(details: chrome.webRequest.WebRequestBodyDetails) {
const { tabId, type, url } = details;
const location = new URL(url);
const domain = formatDomainFromURL(location);
if (tabId > -1 && type === 'main_frame') {
const data = (await storage.get<ExtensionData>('data')) || DEFAULT_EXTENSION_DATA;
if (!validateSupport(location.hostname, data.exclusions.domains)) {
return;
}
const config = (await storage.get<DomainConfig>(domain)) || DEFAULT_DOMAIN_CONFIG;
if (data.rules.length) {
const rulesWithTabId = data.rules.map((rule) => ({
...rule,
condition: { ...rule.condition, tabIds: [tabId] },
}));
chrome.declarativeNetRequest.updateSessionRules({
addRules: config.on ? rulesWithTabId : undefined,
removeRuleIds: data.rules.map((rule) => rule.id),
});
}
}
}

View File

@ -0,0 +1,15 @@
import { sendToContentScript } from '@plasmohq/messaging';
export default async function onErrorOccurred(details: chrome.webRequest.WebResponseErrorDetails) {
const { error, tabId } = details;
if (error === 'net::ERR_BLOCKED_BY_CLIENT' && tabId > -1) {
await sendToContentScript({
body: {
value: error,
},
name: 'INCREASE_LOG_COUNT',
tabId,
});
}
}

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,45 +0,0 @@
{
"manifest_version": 3,
"name": "Cookie Dialog Monster",
"version": "8.0.2",
"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_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/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", "scripting", "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,328 +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 disabled id="report-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"
>
<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>
</button>
<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="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">
<popup-button id="power-option" role="button" tabindex="0">
<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>
</popup-button>
<popup-button
data-href="https://git.wanhose.dev/wanhose/cookie-dialog-monster/wiki/Help-or-issues%3F"
id="help-option"
role="link"
tabindex="0"
>
<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="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
/>
</svg>
<span data-i18n="popup_helpOption"></span>
</popup-button>
<popup-button
data-href="https://git.wanhose.dev/wanhose/cookie-dialog-monster"
id="contribute-option"
role="link"
tabindex="0"
>
<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>
</popup-button>
<popup-button data-href="#" id="rate-option" role="link" tabindex="0">
<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>
</popup-button>
<popup-data-container>
<popup-data>
<strong data-i18n="popup_databaseVersion"></strong>
<span id="database-version"></span>
<popup-data-button
data-animation="flip"
id="refresh-database-button"
role="button"
tabindex="0"
>
<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>
</popup-data-button>
</popup-data>
<popup-data>
<strong data-i18n="popup_extensionVersion"></strong>
<span id="extension-version"></span>
</popup-data>
</popup-data-container>
</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"
role="button"
tabindex="0"
></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"
role="button"
tabindex="0"
></div>
</div>
</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,466 +0,0 @@
/**
* @typedef {Object} ExtensionIssue
* @property {number} [expiresIn]
* @property {string[]} [flags]
* @property {string} [url]
*/
/**
* @typedef {Object} ExtensionState
* @property {ExtensionIssue} [issue]
* @property {boolean} on
* @property {string} [updateAvailable]
*/
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 A shortcut for browser.scripting
* @type {browser.scripting}
*/
const script = browser.scripting;
/**
* @description Default value for extension state
* @type {ExtensionState}
*/
const stateByDefault = { issue: { expiresIn: 0 }, 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;
}
/**
* @async
* @description Disable report context menu option
* @returns {Promise<void>}
*/
async function disableReport() {
return browser.contextMenus.update(reportMenuItemId, { enabled: false });
}
/**
* @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 keys = [hostname, 'updateAvailable'];
const { [hostname]: state = stateByDefault, updateAvailable } = await storage.get(keys);
if ((state.issue && Date.now() > state.issue.expiresIn) || !state.issue?.expiresIn) {
state.issue = await refreshIssue(hostname);
}
return { ...stateByDefault, ...state, updateAvailable };
}
/**
* @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}/`);
if (Object.keys(data).length === 0) {
await updateStore(hostname, { issue: { expiresIn: Date.now() + 8 * 60 * 60 * 1000 } });
return undefined;
}
const issue = {
expiresIn: Date.now() + 4 * 60 * 60 * 1000,
flags: data.flags,
url: data.url,
};
await updateStore(hostname, { issue });
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_REPORT':
if (isPage && tabId !== undefined) disableReport();
break;
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 'ENABLE_POPUP':
if (isPage && tabId !== undefined) {
browser.action.setPopup({ popup: '/popup.html', tabId }, suppressLastError);
}
break;
case 'ENABLE_REPORT':
if (isPage && tabId !== undefined) {
browser.contextMenus.update(reportMenuItemId, { enabled: 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_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,
enabled: false,
id: reportMenuItemId,
parentId: extensionMenuItemId,
title: browser.i18n.getMessage('contextMenu_reportOption'),
},
suppressLastError
);
if (details.reason === 'update') {
refreshData();
storage.remove('updateAvailable');
}
});
/**
* @description Listen to available updates
*/
browser.runtime.onUpdateAvailable.addListener((details) => {
storage.set({ updateAvailable: details.version }, suppressLastError);
});
/**
* @description Listen to first start
*/
browser.runtime.onStartup.addListener(() => {
refreshData();
});
/**
* @description Listen to tab changes
*/
browser.tabs.onActivated.addListener(() => {
disableReport();
});
/**
* @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,521 +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' });
dispatch({ type: 'ENABLE_POPUP' });
dispatch({ type: 'ENABLE_REPORT' });
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,412 +0,0 @@
/**
* @typedef {Object} ExtensionState
* @property {ExtensionIssue} [issue]
* @property {boolean} on
* @property {string} [updateAvailable]
*/
/**
* @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 tabUrl = tab?.url ? new URL(tab.url) : undefined;
hostname = tabUrl?.hostname.split('.').slice(-3).join('.').replace('www.', '');
const next = await dispatch({ hostname, type: 'GET_STATE' });
state = { ...(next ?? state), tabId: tab?.id };
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);
} else {
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);
if (!state.updateAvailable) {
const reportButtonElement = document.getElementById('report-button');
reportButtonElement?.addEventListener('click', handleReportClick);
reportButtonElement?.removeAttribute('disabled');
}
const urlInputElement = document.getElementById('report-input-url');
urlInputElement?.addEventListener('input', handleInputChange);
urlInputElement?.addEventListener('keydown', handleInputKeyDown);
if (tabUrl) urlInputElement?.setAttribute('value', `${tabUrl.origin}${tabUrl.pathname}`);
const submitButtonElement = document.getElementsByClassName('report-submit-button')[0];
submitButtonElement?.addEventListener('click', handleSubmitButtonClick);
}
if (state.updateAvailable) {
const updateBanner = document.getElementById('update-banner');
updateBanner.removeAttribute('aria-hidden');
const updateBannerUrl = document.getElementById('update-banner-url');
updateBannerUrl.href += `/tag/${state.updateAvailable}`;
}
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-option');
helpButtonElement?.addEventListener('click', handleLinkRedirect);
const powerButtonElement = document.getElementById('power-option');
powerButtonElement?.addEventListener('click', handlePowerToggle);
if (state.on) powerButtonElement?.setAttribute('data-value', 'on');
else powerButtonElement?.setAttribute('data-value', 'off');
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();
}
/**
* @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 });
}
}
/**
* @async
* @description Disable or enable extension on current page
* @returns {void}
*/
async function handlePowerToggle() {
const next = { 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 {
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,13 +1,3 @@
:root {
--color-error: #cc0000;
--color-primary: #3dd9eb;
--color-secondary: #34495e;
--color-success: #5cb85c;
--color-tertiary: #6b7280;
--color-warning: #ffdf00;
--color-white: #ffffff;
}
body {
box-sizing: border-box;
color: var(--color-tertiary);
@ -21,11 +11,68 @@ body {
}
}
body * {
box-sizing: border-box;
.banner {
font-size: 12px;
line-height: 16px;
margin: 0px;
padding: 16px;
}
button {
.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;
height: 4px;
margin-top: auto;
text-align: center;
}
.header {
align-items: center;
background-color: var(--color-secondary);
color: var(--color-white);
display: flex;
flex-shrink: 0;
font-size: 16px !important;
height: 48px;
justify-content: space-between;
padding: 0 16px;
}
.header-actions {
display: flex;
gap: 8px;
}
.header-button {
align-items: center;
background-color: var(--color-secondary);
border: none;
@ -36,96 +83,29 @@ button {
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);
}
&:disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
footer {
background-color: var(--color-secondary);
flex-shrink: 0;
font-size: 12px;
height: 4px;
margin-top: auto;
text-align: center;
}
header {
align-items: center;
background-color: var(--color-secondary);
color: var(--color-white);
display: flex;
flex-shrink: 0;
font-size: 16px !important;
height: 48px;
justify-content: space-between;
padding: 0 16px;
& .header-actions {
display: flex;
gap: 8px;
& #report-button:focus:not(:disabled) > svg,
& #report-button:hover:not(:disabled) > svg {
color: var(--color-error);
}
}
}
@media only screen and (max-device-width: 768px) {
main {
.main {
margin: 0 auto;
max-width: 320px;
width: 100%;
}
}
main > .banner {
margin: 0px;
padding: 16px;
&[aria-hidden='true'] {
display: none;
}
&[data-variant='notice'] {
background-color: #2196f3;
color: var(--color-white);
}
&[data-variant='warning'] {
background-color: #f39c12;
color: #c0392b;
}
& #issue-banner-url,
& #update-banner-url {
color: inherit;
display: inline-block;
vertical-align: middle;
}
}
main > .content {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
padding: 16px;
}
popup-button {
.popup-button {
border-radius: 4px;
color: var(--color-secondary);
cursor: pointer;
display: grid;
font-size: 14px;
line-height: 18px;
gap: 16px;
grid-template-rows: repeat(2, 1fr);
height: 136px;
@ -133,27 +113,36 @@ popup-button {
outline: none;
padding: 8px;
text-align: center;
text-decoration: none;
transition: 0.4s;
width: 100%;
word-break: break-word;
}
popup-button:focus,
popup-button: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;
}
popup-button > span {
.popup-button:disabled {
background-color: var(--color-tertiary);
box-shadow: none;
color: var(--color-white);
cursor: not-allowed;
opacity: 0.5;
}
.popup-button span {
align-self: flex-start;
}
popup-button > svg {
.popup-button svg {
align-self: flex-end;
}
popup-data {
.popup-data {
align-items: center;
display: flex;
gap: 4px;
@ -161,27 +150,30 @@ popup-data {
outline: none;
}
popup-data:not(:first-child) {
.popup-data:not(:first-child) {
margin-top: 4px;
}
popup-data-button {
.popup-data-button {
color: var(--color-tertiary);
cursor: pointer;
line-height: 0;
outline: none;
transition: 0.4s;
}
popup-data-button[aria-disabled='true'] {
.popup-data-button:disabled {
cursor: not-allowed;
}
popup-data-button[data-animation='flip']:focus,
popup-data-button[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);
}
popup-data-button[data-refreshing='true'] {
.popup-data-button[data-refreshing='true'] {
animation: spin 1s linear infinite;
transform: none;
}
@keyframes spin {
@ -190,126 +182,60 @@ popup-data-button[data-refreshing='true'] {
}
}
popup-data-container {
.popup-data-container {
font-size: 12px;
grid-column: 1 / -1;
justify-self: center;
line-height: 16px;
text-align: center;
}
.report {
font-family: inherit;
font-size: 14px;
line-height: 1.2;
padding: 16px;
.power-option {
color: var(--color-white);
word-break: break-all;
}
& .report-buttons {
.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 {
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-family: Inter, Arial, Helvetica, sans-serif;
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-family: Inter, Arial, Helvetica, sans-serif;
font-size: 10px;
line-height: 1.2;
}
& .report-input-group {
display: grid;
gap: 4px;
}
& .report-input-label {
color: var(--color-secondary);
font-family: Inter, Arial, Helvetica, sans-serif;
font-size: 12px;
line-height: 1.2;
}
& .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);
cursor: pointer;
display: flex;
font-family: Inter, Arial, Helvetica, sans-serif;
font-size: 14px;
justify-content: center;
line-height: 1.2;
line-height: 18px;
margin-left: auto;
margin-right: auto;
margin-top: 16px;
@ -318,13 +244,89 @@ popup-data-container {
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);
@ -332,11 +334,10 @@ popup-data-container {
color: var(--color-white);
cursor: pointer;
display: flex;
font-family: Inter, Arial, Helvetica, sans-serif;
font-size: 14px;
height: 42px;
justify-content: center;
line-height: 1.2;
line-height: 18px;
margin-top: 8px;
outline: none;
padding: 8px 16px;
@ -344,88 +345,48 @@ popup-data-container {
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-family: inherit;
font-size: 14px;
line-height: 1.2;
margin: 0px;
text-align: justify;
}
& .report-submit-text {
font-family: inherit;
font-size: 18px;
line-height: 1.2;
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;
font-family: Inter, Arial, Helvetica, sans-serif;
gap: 24px;
justify-content: center;
margin-top: 16px;
}
& .report-submit-error-view[hidden],
& .report-submit-success-view[hidden] {
display: none;
}
.report-submit-extra-text {
font-size: 14px;
line-height: 18px;
margin: 0px;
text-align: justify;
}
strong {
font-weight: bold;
}
#refresh-database-check {
display: none;
}
#power-option {
color: var(--color-white);
word-break: break-all;
}
#power-option[data-value='off'] {
background-color: var(--color-error);
}
#power-option[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);
.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,31 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import onClicked from '~background/utils/contextMenus/onClicked';
import onInstalled from '~background/utils/runtime/onInstalled';
import onStartup from '~background/utils/runtime/onStartup';
import onBeforeRequest from '~background/utils/webRequest/onBeforeRequest';
import onErrorOccurred from '~background/utils/webRequest/onErrorOccurred';
describe('background/index.ts', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should set up all listeners correctly', async () => {
const filter: chrome.webRequest.RequestFilter = { urls: ['<all_urls>'] };
await import('~background');
expect(chrome.contextMenus.onClicked.addListener).toHaveBeenCalledWith(onClicked);
expect(chrome.runtime.onInstalled.addListener).toHaveBeenCalledWith(onInstalled);
expect(chrome.runtime.onStartup.addListener).toHaveBeenCalledWith(onStartup);
expect(chrome.webRequest.onBeforeRequest.addListener).toHaveBeenCalledWith(
onBeforeRequest,
filter
);
expect(chrome.webRequest.onErrorOccurred.addListener).toHaveBeenCalledWith(
onErrorOccurred,
filter
);
});
});

Some files were not shown because too many files have changed in this diff Show More