├── .prettierignore ├── .prettierrc.json ├── tsconfig.build.json ├── src ├── utils │ ├── index.ts │ ├── config.ts │ ├── types.ts │ ├── helpers.ts │ └── browser-helpers.ts ├── assets │ ├── icons │ │ ├── abf-enabled-128.png │ │ └── abf-disabled-128.png │ ├── css │ │ ├── popup.css │ │ └── dashboard.css │ ├── html │ │ ├── popup.html │ │ └── dashboard.html │ └── _locales │ │ ├── en │ │ └── messages.json │ │ ├── es │ │ └── messages.json │ │ ├── fr │ │ └── messages.json │ │ └── de │ │ └── messages.json ├── background │ └── index.ts ├── content │ └── index.ts └── gui │ └── index.ts ├── .husky └── pre-commit ├── engines ├── chromium │ ├── manifest.json │ └── webpack.config.js ├── default-copy-plugin-patterns.json ├── common │ └── manifest │ │ ├── base.json │ │ └── content-scripts.json └── gecko │ ├── webpack.config.js │ └── manifest.json ├── jest.config.js ├── .gitignore ├── tests └── helpers.test.ts ├── .eslintrc.json ├── webpack.config.js ├── LICENSE ├── scripts ├── post-build-update-manifest-gecko.js └── post-build-update-manifest-chromium.js ├── tsconfig.json ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": ["src"] 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "utils/config"; 2 | export * from "utils/helpers"; 3 | export * from "utils/types"; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | yarn lint 5 | yarn lint-staged 6 | yarn test 7 | -------------------------------------------------------------------------------- /src/assets/icons/abf-enabled-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-mosley/AmazonBrandFilter/HEAD/src/assets/icons/abf-enabled-128.png -------------------------------------------------------------------------------- /engines/chromium/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "service_worker": "background.js", 4 | "type": "module" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/assets/icons/abf-disabled-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chris-mosley/AmazonBrandFilter/HEAD/src/assets/icons/abf-disabled-128.png -------------------------------------------------------------------------------- /engines/default-copy-plugin-patterns.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "from": "src/assets/css", "to": "./" }, 3 | { "from": "src/assets/html", "to": "./" }, 4 | { "from": "src/assets/icons", "to": "icons" }, 5 | { "from": "src/assets/_locales", "to": "_locales" } 6 | ] 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { compilerOptions } = require('./tsconfig.json'); 2 | 3 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 4 | module.exports = { 5 | preset: 'ts-jest', 6 | testEnvironment: 'node', 7 | roots: [''], 8 | modulePaths: [compilerOptions.baseUrl], 9 | }; 10 | -------------------------------------------------------------------------------- /engines/common/manifest/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "default_locale": "en", 4 | "icons": { 5 | "128": "icons/abf-disabled-128.png" 6 | }, 7 | "permissions": ["storage", "activeTab"], 8 | "action": { 9 | "default_title": "Amazon Brand Filter", 10 | "default_area": "navbar", 11 | "default_popup": "popup.html" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | report.html -------------------------------------------------------------------------------- /tests/helpers.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "@jest/globals"; 2 | 3 | import { defaultLocalStorageValue, defaultSyncStorageValue } from "utils/config"; 4 | import { extractSyncStorageSettingsObject } from "utils/helpers"; 5 | 6 | describe("extractSyncStorageSettingsObject", () => { 7 | test("object with local keys should be returned with sync keys only", () => { 8 | const sampleLocalValue = defaultLocalStorageValue; 9 | const sampleSyncValue = extractSyncStorageSettingsObject(sampleLocalValue); 10 | expect(sampleSyncValue).toMatchObject(defaultSyncStorageValue); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /engines/common/manifest/content-scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "content_scripts": [ 3 | { 4 | "matches": [ 5 | "*://*.amazon.com/*", 6 | "*://*.amazon.ca/*", 7 | "*://*.amazon.cn/*", 8 | "*://*.amazon.co.jp/*", 9 | "*://*.amazon.com/*", 10 | "*://*.amazon.com.au/*", 11 | "*://*.amazon.com.mx/*", 12 | "*://*.amazon.co.uk/*", 13 | "*://*.amazon.de/*", 14 | "*://*.amazon.es/*", 15 | "*://*.amazon.fr/*", 16 | "*://*.amazon.in/*", 17 | "*://*.amazon.it/*", 18 | "*://*.amazon.nl/*" 19 | ], 20 | "js": ["content.js"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "rules": { 10 | "max-len": ["error", { "code": 120 }], 11 | "no-unused-vars": "off", 12 | "@typescript-eslint/no-unused-vars": [ 13 | "error", 14 | { 15 | "argsIgnorePattern": "^_", 16 | "varsIgnorePattern": "^_", 17 | "caughtErrorsIgnorePattern": "^_" 18 | } 19 | ] 20 | }, 21 | "ignorePatterns": ["node_modules/", "dist/", "*.config.js", "scripts/"], 22 | "env": { 23 | "browser": true, 24 | "webextensions": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /engines/gecko/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | 5 | const rootPath = path.resolve('./'); 6 | const baseConfig = require(`${rootPath}/webpack.config.js`); 7 | const defaultCopyPluginPatterns = require(`${rootPath}/engines/default-copy-plugin-patterns.json`); 8 | 9 | module.exports = (env, argv) => { 10 | return { 11 | ...baseConfig(env, argv), 12 | plugins: [ 13 | new CleanWebpackPlugin(), 14 | new CopyPlugin({ 15 | patterns: [ 16 | ...defaultCopyPluginPatterns, 17 | { from: "engines/gecko/manifest.json", to: "./" }, 18 | ], 19 | }), 20 | ], 21 | output: { 22 | filename: "[name].js", 23 | path: path.resolve(rootPath, "dist","gecko"), 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /engines/chromium/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | const { CleanWebpackPlugin } = require("clean-webpack-plugin"); 4 | 5 | const rootPath = path.resolve('./'); 6 | const baseConfig = require(`${rootPath}/webpack.config.js`); 7 | const defaultCopyPluginPatterns = require(`${rootPath}/engines/default-copy-plugin-patterns.json`); 8 | 9 | module.exports = (env, argv) => { 10 | return { 11 | ...baseConfig(env, argv), 12 | plugins: [ 13 | new CleanWebpackPlugin(), 14 | new CopyPlugin({ 15 | patterns: [ 16 | ...defaultCopyPluginPatterns, 17 | { from: "engines/chromium/manifest.json", to: "./" }, 18 | ], 19 | }), 20 | ], 21 | output: { 22 | filename: "[name].js", 23 | path: path.resolve(rootPath, "dist","chromium"), 24 | }, 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin"); 3 | 4 | module.exports = (env, argv) => { 5 | return { 6 | mode: argv.mode === "development" ? "development" : "production", 7 | devtool: argv.mode === "development" ? "cheap-module-source-map" : "source-map", 8 | entry: { 9 | background: "./src/background/index.ts", 10 | content: "./src/content/index.ts", 11 | gui: "./src/gui/index.ts", 12 | utils: "./src/utils/index.ts", 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: "ts-loader", 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | resolve: { 24 | extensions: [".tsx", ".ts", ".js"], 25 | alias: { 26 | "~/": "./src", 27 | }, 28 | plugins: [new TsconfigPathsPlugin()], 29 | } 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /engines/gecko/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_specific_settings": { 3 | "gecko": { 4 | "id": "abf@mosley.xyz", 5 | "strict_min_version": "113.0" 6 | }, 7 | "gecko_android": { 8 | "strict_min_version": "113.0" 9 | } 10 | }, 11 | "background": { 12 | "scripts": ["background.js"], 13 | "type": "module" 14 | }, 15 | "content_security_policy": { 16 | "extension_pages": "default-src 'self'; script-src 'self'; object-src 'none'; connect-src https://raw.githubusercontent.com/chris-mosley/AmazonBrandFilterList/main/brands.txt https://api.github.com/repos/chris-mosley/AmazonBrandFilterList/releases/latest;" 17 | }, 18 | "web_accessible_resources": [ 19 | { 20 | "resources": ["brands.txt"], 21 | "matches": [ 22 | "https://raw.githubusercontent.com/chris-mosley/AmazonBrandFilterList/main/brands.txt", 23 | "https://api.github.com/repos/chris-mosley/AmazonBrandFilterList/releases/latest" 24 | ] 25 | }, 26 | { 27 | "resources": ["brands.txt"], 28 | "matches": ["*://*/*"] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Chris Mosley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/post-build-update-manifest-gecko.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const rootPath = path.resolve("./"); 5 | const packageJson = require(`${rootPath}/package.json`); 6 | const baseManifestJson = require(`${rootPath}/engines/common/manifest/base.json`); 7 | const contentScriptsJson = require(`${rootPath}/engines/common/manifest/content-scripts.json`); 8 | const manifestFilePath = path.resolve(__dirname, `${rootPath}/dist/gecko/manifest.json`); 9 | 10 | try { 11 | // read the manifest file 12 | let manifest = JSON.parse(fs.readFileSync(manifestFilePath)); 13 | 14 | // update the relevant keys 15 | manifest.version = packageJson.version; 16 | manifest.name = packageJson.name; 17 | manifest.description = packageJson.description; 18 | manifest = { ...manifest, ...baseManifestJson, ...contentScriptsJson }; 19 | 20 | // write the updated manifest file 21 | fs.writeFileSync(manifestFilePath, JSON.stringify(manifest, null, 2)); 22 | 23 | console.log(`Version updated to ${packageJson.version} in manifest.json.`); 24 | } catch (error) { 25 | console.error(`Error updating manifest.json: ${error.message}`); 26 | } 27 | -------------------------------------------------------------------------------- /scripts/post-build-update-manifest-chromium.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | const rootPath = path.resolve("./"); 5 | const packageJson = require(`${rootPath}/package.json`); 6 | const baseManifestJson = require(`${rootPath}/engines/common/manifest/base.json`); 7 | const contentScriptsJson = require(`${rootPath}/engines/common/manifest/content-scripts.json`); 8 | const manifestFilePath = path.resolve(__dirname, `${rootPath}/dist/chromium/manifest.json`); 9 | 10 | try { 11 | // read the manifest file 12 | let manifest = JSON.parse(fs.readFileSync(manifestFilePath)); 13 | 14 | // update the relevant keys 15 | manifest.version = packageJson.version; 16 | manifest.name = packageJson.name; 17 | manifest.description = packageJson.description; 18 | manifest = { ...manifest, ...baseManifestJson, ...contentScriptsJson }; 19 | 20 | // write the updated manifest file 21 | fs.writeFileSync(manifestFilePath, JSON.stringify(manifest, null, 2)); 22 | 23 | console.log(`Version updated to ${packageJson.version} in manifest.json.`); 24 | } catch (error) { 25 | console.error(`Error updating manifest.json: ${error.message}`); 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "ES2015"], 4 | "target": "es6", 5 | "module": "commonjs", 6 | "baseUrl": "./src", 7 | "paths": { 8 | "~/*": ["./src"] 9 | }, 10 | "types": ["web", "chrome", "node", "webextension-polyfill-ts"], 11 | "esModuleInterop": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strictBindCallApply": true, 18 | "strictPropertyInitialization": true, 19 | "noImplicitThis": true, 20 | "useUnknownInCatchVariables": true, 21 | "alwaysStrict": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "exactOptionalPropertyTypes": true, 25 | "noImplicitReturns": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noUncheckedIndexedAccess": true, 28 | "noImplicitOverride": true, 29 | "noPropertyAccessFromIndexSignature": true, 30 | "allowUnusedLabels": true, 31 | "allowUnreachableCode": true, 32 | "skipDefaultLibCheck": true, 33 | "skipLibCheck": true 34 | }, 35 | "include": ["src", "tests"] 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import { StorageSettings, SyncStorageSettings } from "utils/types"; 2 | 3 | export const latestReleaseUrl: string = 4 | "https://api.github.com/repos/chris-mosley/AmazonBrandFilterList/releases/latest"; 5 | export const brandsUrl: string = "https://raw.githubusercontent.com/chris-mosley/AmazonBrandFilterList/main/brands.txt"; 6 | 7 | // don't copy brandsMap for sync storage 8 | export const defaultSyncStorageValue: SyncStorageSettings = { 9 | isFirstRun: false, 10 | deptMap: {}, 11 | enabled: true, 12 | knownDepts: {}, 13 | deptCount: 0, 14 | seenBrands: {}, 15 | seenBrandCount: 0, 16 | searchDepth: 7, 17 | showAllDepts: false, 18 | filterRefiner: false, 19 | filterWithRefiner: false, 20 | refinerMode: "grey", 21 | refinerBypass: false, 22 | usePersonalBlock: false, 23 | useDebugMode: false, 24 | personalBlockMap: {}, 25 | lastMapRun: null, 26 | deptFilter: false, 27 | showKnownBrands: false, 28 | showSeenBrands: false, 29 | }; 30 | 31 | export const defaultLocalStorageValue: StorageSettings = { 32 | ...defaultSyncStorageValue, 33 | currentDepts: { Unknown: true }, 34 | brandsMap: {}, 35 | brandsVersion: 0, 36 | brandsCount: 0, 37 | maxWordCount: 0, 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type Engine = "gecko" | "chromium"; 2 | export type GuiLocation = "popup" | "dashboard"; 3 | export type StorageArea = "local" | "sync"; 4 | export type StorageMode = "normal" | "overwrite"; 5 | 6 | export interface StorageSettings { 7 | isFirstRun: boolean; 8 | brandsVersion: number | null; 9 | brandsCount: number | null; 10 | brandsMap: Record; 11 | searchDepth: number; 12 | seenBrands: Record; 13 | seenBrandCount: number | null; 14 | deptMap: Record; 15 | currentDepts: Record; 16 | knownDepts: Record; 17 | deptCount: number | null; 18 | showAllDepts: boolean; 19 | deptFilter: boolean; 20 | maxWordCount: number; 21 | enabled: boolean; 22 | filterRefiner: boolean; 23 | refinerMode: "grey" | "hide"; 24 | refinerBypass: boolean; 25 | usePersonalBlock: boolean; 26 | personalBlockMap: Record; 27 | useDebugMode: boolean; 28 | lastMapRun: number | null; 29 | filterWithRefiner: boolean; 30 | showKnownBrands: boolean; 31 | showSeenBrands: boolean; 32 | } 33 | 34 | export type SeenBrand = { 35 | hide: boolean; 36 | }; 37 | 38 | export type SyncStorageSettings = Omit< 39 | StorageSettings, 40 | "brandsMap" | "brandsCount" | "brandsVersion" | "currentDepts" | "maxWordCount" 41 | >; 42 | 43 | export type PopupMessageType = keyof StorageSettings; 44 | 45 | export interface PopupMessage { 46 | type: PopupMessageType; 47 | isChecked: boolean; 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/css/popup.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | width: 25rem; 9 | font-family: "Open Sans Light", sans-serif; 10 | font-size: 16px; 11 | font-weight: normal; 12 | padding: 0.6rem; 13 | } 14 | 15 | label, 16 | input[type="checkbox"], 17 | input[type="radio"], 18 | input[type="button"] { 19 | cursor: pointer; 20 | } 21 | #abf-current-depts > div > input[type="checkbox"]{ 22 | margin-right: 0.3em; 23 | } 24 | label { 25 | user-select: none; 26 | padding: 0.2rem 0; 27 | } 28 | 29 | input[type="button"] { 30 | padding: 0 0.6rem; 31 | } 32 | 33 | /* label:hover { 34 | background-color: #eaeff2; 35 | } */ 36 | 37 | /* Version span style */ 38 | .abf-version { 39 | position: absolute; 40 | inset: 0 0 auto auto; 41 | width: fit-content; 42 | 43 | font-size: 0.8rem; 44 | font-weight: bold; 45 | color: grey; 46 | } 47 | 48 | /* Last Run span style */ 49 | .last-run { 50 | position: absolute; 51 | inset: auto 0 0 auto; 52 | width: fit-content; 53 | } 54 | 55 | /* Options Style */ 56 | .options { 57 | position: relative; 58 | width: 100%; 59 | 60 | display: flex; 61 | flex-flow: column nowrap; 62 | gap: 0.2rem 0; 63 | } 64 | 65 | .options-links > a { 66 | display: block; 67 | } 68 | 69 | .indent { 70 | margin-left: 1.3rem; 71 | } 72 | 73 | /* Instead of creating a spacer element, use this in 74 | a div to add spacings above that div */ 75 | .spaced { 76 | margin-top: 0.1rem; 77 | } 78 | 79 | /* Other */ 80 | section.clear-options { 81 | padding: 0.5em 0; 82 | margin: 1em 0; 83 | } 84 | 85 | .title { 86 | font-size: 1.2em; 87 | margin-bottom: 0.5em; 88 | } 89 | 90 | #clear-button { 91 | margin: 0 1.3em 1em 0; 92 | } 93 | 94 | section.clear-options input, 95 | section.clear-options > select, 96 | #clear-button { 97 | float: right; 98 | } 99 | 100 | #abf-personal-block-textbox { 101 | resize: vertical; 102 | width: 100%; 103 | display: none; 104 | } 105 | 106 | #abf-personal-block-button { 107 | position: right; 108 | float: right; 109 | display: none; 110 | } 111 | 112 | .abf-personal-block-saved-confirm { 113 | display: none; 114 | float: right; 115 | margin-right: .5em; 116 | } 117 | 118 | .resizable-textarea { 119 | resize: vertical; 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmazonBrandFilter 2 | 3 | **Table of content:** 4 | 5 | - [What is it?](#what) 6 | 7 | - [How does it work?](#how) 8 | - [Why?](#why) 9 | - [Missing a Brand?](#missing-brand) 10 | - [What's Coming?](#upcoming) 11 | 12 | ## What is it? 13 | 14 | ### AmazonBrandFilter is a Firefox and Chrome add-on that filters out all of the drop shipping brands that are randomly generated and clog up your search. 15 | 16 | ## Where do I get it? 17 | ### [Firefox](https://addons.mozilla.org/en-US/firefox/addon/amazonbrandfilter/) 18 | ### [Chrome](https://chromewebstore.google.com/detail/amazonbrandfilter/mhfjchmiaocbleapojmgnmjfcmanihio) 19 | 20 | ## How does it work? 21 | 22 | ### It uses an Allow List over [here](https://github.com/chris-mosley/AmazonBrandFilterList). 23 | 24 | The allow list can be updated any time. By default the add-on will update this list at initial startup and then once a day after that. 25 | 26 | ### Why not a block list instead of an allow list? 27 | 28 | Because while it will certainly be a challenge and a very _very_ long road, an allow list has a hypothetical end while a block list would be impossible to "complete." They will generate brands faster than we could ever update a block list. 29 | 30 | ### Why? 31 | 32 | Because we're all sick of crappy fake brands clogging up our search! 33 | 34 | ### Missing a Brand? 35 | 36 | Please refer to the Submission Criteria in the [AmazonBrandFilterList](https://github.com/chris-mosley/AmazonBrandFilterList#submission-criteria) repo. 37 | 38 | ### Which Brands are allowed? 39 | 40 | #### The main concern here is that this list is not a measure of "quality." Generic brands, bad brands, evil brands. The lot, they are all allowed on this list. The only thing meant to be filtered out here are the drop-shipping brands that sell random garbage. 41 | 42 | If they have existed for more than a few years they likely belong on this list. 43 | 44 | A good (but not foolproof) rule of thumb is that if a brand has a dedicated website, it probably belongs on this list. 45 | 46 | ### Help translate! 47 | 48 | I didn't expect this to make it outside my home country of the USA but I'm thrilled that others are interested! To that ends I'm hoping to accomadate as many as possible. 49 | Unfortunately I'm monolingual and the best I could do would be to use internet translation to give crappy translations for an unknown number of languages. 50 | 51 | So if you would like to see your language represented. Please help translate all of the terms found in messages.json for your locale [here](src/assets/_locales). 52 | 53 | 54 | ### Roadmap 55 | 56 | In no particular order these are the things I would like to add eventually 57 | 58 | - Per department disabling of the filter 59 | - Caret or something on the page to indicate that things have been hidden 60 | - A way for users to report a missing brand from within the add-on popup 61 | - Along with localiztion I'm considering having different lists based on locale. I'm not sure it will be necessary yet. 62 | -------------------------------------------------------------------------------- /src/assets/css/dashboard.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | width: 25rem; 9 | font-family: "Open Sans Light", sans-serif; 10 | font-size: 16px; 11 | font-weight: normal; 12 | padding: 0.6rem; 13 | } 14 | 15 | label, 16 | input[type="checkbox"], 17 | input[type="radio"], 18 | input[type="button"] { 19 | cursor: pointer; 20 | } 21 | 22 | label { 23 | user-select: none; 24 | padding: 0.2rem 0; 25 | } 26 | 27 | #abf-dashboard-depts > div{ 28 | user-select: none; 29 | padding: 0.1rem 0; 30 | margin-left: 1.3rem; 31 | } 32 | #abf-dashboard-depts > div > input[type="checkbox"]{ 33 | margin-right: 0.3em; 34 | } 35 | 36 | input[type="button"]{ 37 | padding: 0 0.6rem; 38 | } 39 | 40 | /* label:hover { 41 | background-color: #eaeff2; 42 | } */ 43 | 44 | /* Version span style */ 45 | .abf-version { 46 | position: absolute; 47 | inset: 0 0 auto auto; 48 | width: fit-content; 49 | 50 | font-size: 0.8rem; 51 | font-weight: bold; 52 | color: grey; 53 | } 54 | 55 | /* Last Run span style */ 56 | .last-run { 57 | position: absolute; 58 | inset: auto 0 0 auto; 59 | width: fit-content; 60 | } 61 | 62 | /* Options Style */ 63 | .options { 64 | position: relative; 65 | width: 100%; 66 | 67 | display: flex; 68 | flex-flow: column nowrap; 69 | gap: 0.2rem 0; 70 | } 71 | 72 | .options-links > a { 73 | display: block; 74 | } 75 | 76 | .indent { 77 | margin-left: 1.3rem; 78 | } 79 | 80 | 81 | /* Instead of creating a spacer element, use this in 82 | a div to add spacings above that div */ 83 | .spaced { 84 | margin-top: 0.1rem; 85 | } 86 | 87 | .double-spaced { 88 | margin-top: 0.2rem; 89 | } 90 | 91 | 92 | /* Other */ 93 | section.clear-options { 94 | padding: 0.5em 0; 95 | margin: 1em 0; 96 | } 97 | 98 | .title { 99 | font-size: 1.2em; 100 | margin-bottom: 0.5em; 101 | } 102 | 103 | #clear-button { 104 | margin: 0 1.3em 1em 0; 105 | } 106 | 107 | section.clear-options input, 108 | section.clear-options > select, 109 | #clear-button { 110 | float: right; 111 | } 112 | 113 | #abf-personal-block-textbox { 114 | resize: vertical; 115 | width: 100%; 116 | display: none; 117 | } 118 | 119 | #abf-personal-block-button { 120 | float: right; 121 | display: none; 122 | } 123 | 124 | #abf-save-search-depth-button { 125 | float:right; 126 | } 127 | 128 | .abf-search-depth-saved-confirm { 129 | display: none; 130 | float: right; 131 | margin-right: .5em; 132 | } 133 | 134 | 135 | #abf-search-depth{ 136 | width: 4em; 137 | } 138 | 139 | #abf-dept-view-control { 140 | position: right; 141 | float: right; 142 | } 143 | 144 | #abf-known-brand-view-control { 145 | position: right; 146 | float: right; 147 | } 148 | 149 | 150 | #abf-seen-brand-view-control { 151 | position: right; 152 | float: right; 153 | } 154 | 155 | 156 | .abf-personal-block-saved-confirm { 157 | display: none; 158 | float: right; 159 | margin-right: .5em; 160 | } 161 | 162 | .resizable-textarea { 163 | resize: vertical; 164 | } 165 | -------------------------------------------------------------------------------- /src/assets/html/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 |
37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 |
45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 | 58 |
59 | 60 | 61 | 62 | | 63 | 64 | 65 | 66 |
67 | 68 | 75 | 76 | 77 |
78 | 79 | 80 |
81 |
82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AmazonBrandFilter", 3 | "description": "Filters out all unknown brands from Amazon search results.", 4 | "version": "0.8.0", 5 | "keywords": [], 6 | "author": "", 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@jest/globals": "^29.7.0", 10 | "@types/chrome": "^0.0.237", 11 | "@types/jest": "^29.5.11", 12 | "@types/lodash": "^4.14.202", 13 | "@types/web": "^0.0.130", 14 | "@typescript-eslint/eslint-plugin": "^6.13.2", 15 | "@typescript-eslint/parser": "^6.13.2", 16 | "clean-webpack-plugin": "^4.0.0", 17 | "copy-webpack-plugin": "^11.0.0", 18 | "html-webpack-plugin": "^5.5.2", 19 | "husky": "^8.0.3", 20 | "jest": "^29.7.0", 21 | "lint-staged": "^13.2.2", 22 | "prettier": "2.8.8", 23 | "run-script-os": "^1.1.6", 24 | "ts-jest": "^29.1.1", 25 | "ts-loader": "^9.4.3", 26 | "tsconfig-paths-webpack-plugin": "^4.1.0", 27 | "typescript": "^5.1.3", 28 | "web-ext": "^7.6.2", 29 | "webextension-polyfill-ts": "^0.26.0", 30 | "webpack": "^5.86.0", 31 | "webpack-cli": "^5.1.4" 32 | }, 33 | "dependencies": { 34 | "lodash": "^4.17.21" 35 | }, 36 | "lint-staged": { 37 | "src/**/*.ts": [ 38 | "prettier --write \"src/**/*.ts\"" 39 | ], 40 | "src/**/*.json": [ 41 | "prettier --write \"src/**/*.json\"" 42 | ], 43 | "engines/**/*.json": [ 44 | "prettier --write \"engines/**/*.json\"" 45 | ], 46 | "scripts/**/*.js": [ 47 | "prettier --write \"scripts/**/*.js\"" 48 | ], 49 | "tests/**/*.ts": [ 50 | "prettier --write \"tests/**/*.ts\"" 51 | ] 52 | }, 53 | "scripts": { 54 | "package": "web-ext build", 55 | "prepare": "husky install", 56 | "lint": "yarn eslint src/**/*.ts", 57 | "test": "jest", 58 | "win-build-gecko": "Remove-Item dist/gecko -Recurse -Force -ErrorAction SilentlyContinue; webpack --mode development --config engines/gecko/webpack.config.js; node scripts\\post-build-update-manifest-gecko.js; Get-ChildItem dist/gecko | Compress-Archive -DestinationPath dist/gecko/abf.zip -Force", 59 | "win-build-chromium": "Remove-Item dist/chromium -Recurse -Force -ErrorAction SilentlyContinue; webpack --mode development --config engines/chromium/webpack.config.js; node scripts\\post-build-update-manifest-chromium.js", 60 | "nix-build-gecko": "rm -rf dist/gecko && webpack --mode development --config engines/gecko/webpack.config.js && node scripts/post-build-update-manifest-gecko.js", 61 | "nix-build-chromium": "rm -rf dist/chromium && webpack --mode development --config engines/chromium/webpack.config.js && node scripts/post-build-update-manifest-chromium.js", 62 | "win-build-gecko-prod": "Remove-Item dist/gecko -Recurse -Force -ErrorAction SilentlyContinue; webpack --mode production --config engines/gecko/webpack.config.js; node scripts\\post-build-update-manifest-gecko.js; Get-ChildItem dist/gecko | Compress-Archive -DestinationPath dist/gecko/abf.zip -Force", 63 | "win-build-chromium-prod": "Remove-Item dist/chromium -Recurse -Force -ErrorAction SilentlyContinue; webpack --mode production --config engines/chromium/webpack.config.js; node scripts\\post-build-update-manifest-chromium.js", 64 | "nix-build-gecko-prod": "rm -rf dist/gecko && webpack --mode production --config engines/gecko/webpack.config.js && node scripts/post-build-update-manifest-gecko.js", 65 | "nix-build-chromium-prod": "rm -rf dist/chromium && webpack --mode production --config engines/chromium/webpack.config.js && node scripts/post-build-update-manifest-chromium.js" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | 3 | // import { 4 | // ensureSettingsExist, 5 | // getSettings, 6 | // getStorageValue, 7 | // setIcon, 8 | // setStorageValue, 9 | // } from "utils/browser-helpers"; 10 | import { defaultLocalStorageValue, defaultSyncStorageValue } from "utils/config"; 11 | import { StorageSettings, SyncStorageSettings } from "utils/types"; 12 | 13 | export const sleep = (ms: number) => { 14 | return new Promise((resolve) => setTimeout(resolve, ms)); 15 | }; 16 | 17 | export const getItemDivs = (): HTMLCollectionOf => { 18 | const divs = document.getElementsByClassName("s-result-item"); 19 | return divs as HTMLCollectionOf; 20 | }; 21 | 22 | export const getRefinerBrands = (): string[] => { 23 | // i hate this. 24 | const visibleRefinerDivs = document 25 | .getElementById("brandsRefinements") 26 | ?.getElementsByClassName("a-unordered-list a-nostyle a-vertical a-spacing-medium")[0]; 27 | 28 | const hiddenRefinerDivs = document 29 | .getElementById("brandsRefinements") 30 | ?.getElementsByClassName("a-unordered-list a-nostyle a-vertical a-spacing-none")[0]; 31 | if (visibleRefinerDivs === null && hiddenRefinerDivs === null) { 32 | return []; 33 | } 34 | 35 | const refinerBrands: string[] = []; 36 | if (visibleRefinerDivs?.children[0] !== undefined) { 37 | for (const div of visibleRefinerDivs?.children as HTMLCollectionOf) { 38 | console.debug("AmazonBrandFilter: getRefinerBrands - visible - found div: " + div.innerText); 39 | if (div.innerText === "Brand" || div.innerText === "Brands" || div.innerText === "See more") { 40 | continue; 41 | } 42 | refinerBrands.push(div.innerText.trimStart().trimEnd()); 43 | console.debug("AmazonBrandFilter: getRefinerBrands pushing brand: " + div.innerText.trimStart().trimEnd()); 44 | } 45 | } 46 | for (const div of hiddenRefinerDivs?.children as HTMLCollectionOf) { 47 | if (div.innerText === "Brand" || div.innerText === "Brands" || div.innerText === "See more") { 48 | continue; 49 | } 50 | refinerBrands.push(div.innerText.trimStart().trimEnd()); 51 | console.debug("AmazonBrandFilter: getRefinerBrands pushing brand: " + div.innerText.trimStart().trimEnd()); 52 | } 53 | return refinerBrands; 54 | }; 55 | 56 | export const unHideDivs = () => { 57 | const divs = getItemDivs(); 58 | for (const div of divs) { 59 | div.style.display = "block"; 60 | } 61 | }; 62 | 63 | /** 64 | * use regular expression to split the input string based on the delimiters 65 | * delimiterPattern matches one or more commas, spaces, new lines, or return characters 66 | * 67 | * @returns 68 | */ 69 | export const getSanitizedUserInput = (userInput: string) => { 70 | const delimiterPattern = /[,\s\n\r]+/; 71 | const wordsArray = userInput.split(delimiterPattern); 72 | const upperCaseWordsArray = wordsArray.map((word) => word.toUpperCase()); 73 | // remove empty strings from the array (e.g., if there are multiple consecutive delimiters) 74 | const filteredArray = upperCaseWordsArray.filter((word) => word.trim() !== ""); 75 | return filteredArray; 76 | }; 77 | 78 | /** 79 | * extracts a subset of settings suitable for sync storage 80 | * 81 | * @param settings 82 | * @returns 83 | */ 84 | export const extractSyncStorageSettingsObject = (settings: StorageSettings): SyncStorageSettings => { 85 | const keysDefaultSettings = _.keys(defaultLocalStorageValue); 86 | const keysSyncSettings = _.keys(defaultSyncStorageValue); 87 | const exclusiveKeys = _.difference(keysDefaultSettings, keysSyncSettings); 88 | return _.omit(settings, exclusiveKeys) as SyncStorageSettings; 89 | }; 90 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import { brandsUrl, defaultLocalStorageValue, defaultSyncStorageValue, latestReleaseUrl } from "utils/config"; 2 | import { getSettings, getStorageValue, setIcon, setStorageValue } from "utils/browser-helpers"; 3 | 4 | const getBrandsListVersion = async () => { 5 | console.log("AmazonBrandFilter: %cChecking latest brands list version!", "color: yellow"); 6 | const latestRelease = await fetch(latestReleaseUrl, { mode: "cors" }) 7 | .then((response) => response.json()) 8 | .catch((error) => { 9 | console.error(error, "AmazonBrandFilter: %cFailed fetching latest release!", "color: lightcoral"); 10 | return defaultLocalStorageValue.brandsVersion; 11 | }); 12 | 13 | const latestVersion = parseInt(latestRelease.tag_name.slice(1)); 14 | return latestVersion; 15 | }; 16 | 17 | const getBrandsListMap = async () => { 18 | console.log("AmazonBrandFilter: %cChecking brands list!", "color: lightgreen"); 19 | const brandsListFetch: string[] = await fetch(brandsUrl) 20 | .then((res) => res.text()) 21 | .then((text) => text.toUpperCase()) 22 | .then((text) => text.split("\n")) 23 | .catch((err) => { 24 | console.error(err, "AmazonBrandFilter: %cFailed downloading brands list!", "color: lightcoral"); 25 | return []; 26 | }); 27 | 28 | let maxWordCount = 0; 29 | const brandsMap: Record = {}; 30 | const brandsCount = brandsListFetch.length; 31 | 32 | for (const brandName of brandsListFetch) { 33 | brandsMap[brandName] = true; 34 | const wordCount = brandName.split(" ").length; 35 | if (wordCount > maxWordCount) { 36 | maxWordCount = wordCount; 37 | } 38 | } 39 | 40 | console.log(`AmazonBrandFilter: Brands count is ${brandsCount}!`); 41 | console.log(`AmazonBrandFilter: Max brand word count is ${maxWordCount}!`); 42 | console.log(`AmazonBrandFilter: Showing brands list!`, brandsMap); 43 | return { brandsCount, maxWordCount, brandsMap }; 44 | }; 45 | 46 | const checkForBrandListUpdates = async () => { 47 | console.log("AmazonBrandFilter: %cChecking for updates!", "color: yellow"); 48 | const { brandsVersion: currentVersion } = await getStorageValue("brandsVersion"); 49 | const latestVersion = await getBrandsListVersion(); 50 | if (currentVersion !== latestVersion) { 51 | const { brandsCount, maxWordCount, brandsMap } = await getBrandsListMap(); 52 | // set local storage values only 53 | await setStorageValue({ brandsVersion: latestVersion, brandsCount, maxWordCount, brandsMap }); 54 | } 55 | }; 56 | 57 | const setStorageSettings = async () => { 58 | let syncValue: typeof defaultSyncStorageValue; 59 | let localValue: typeof defaultLocalStorageValue; 60 | 61 | const { isFirstRun } = await getStorageValue("isFirstRun"); 62 | if (isFirstRun) { 63 | console.log("AmazonBrandFilter: %cFirst run, setting defaults!", "color: yellow"); 64 | syncValue = defaultSyncStorageValue; 65 | localValue = defaultLocalStorageValue; 66 | } else { 67 | // handle case where no default values exist when !isFirstRun 68 | const { settings, syncSettings } = await getSettings(); 69 | 70 | // defaults destructured first to ensure that settings that have been set are not overwritten 71 | syncValue = { ...defaultSyncStorageValue, ...syncSettings }; 72 | localValue = { ...defaultLocalStorageValue, ...settings }; 73 | } 74 | 75 | const { brandsVersion: currentVersion } = await getStorageValue("brandsVersion"); 76 | const latestVersion = await getBrandsListVersion(); 77 | if (currentVersion !== latestVersion) { 78 | const { brandsCount, maxWordCount, brandsMap } = await getBrandsListMap(); 79 | // set local storage values only 80 | localValue = { ...localValue, brandsVersion: latestVersion, brandsCount, maxWordCount, brandsMap }; 81 | } 82 | 83 | await setStorageValue(syncValue, "sync", "overwrite"); 84 | await setStorageValue(localValue, "local", "overwrite"); 85 | }; 86 | 87 | (async () => { 88 | await setStorageSettings(); 89 | setIcon(); 90 | setInterval(checkForBrandListUpdates, 86_400_000); // check for updates once everyday 91 | })(); 92 | -------------------------------------------------------------------------------- /src/assets/html/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 |
34 | 35 |
36 | 37 | 38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 |
56 | 57 |
58 | 59 | 60 | 61 | 62 | 63 |
64 |
65 | 66 |
67 | 68 | 69 | 70 | 71 | 72 |
73 |
74 | 75 |
76 |
77 |
78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 |
94 | 95 |
96 |
97 | 98 | 103 | 104 | 105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/assets/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "popup_enabled": { 3 | "message": "Enable Amazon Brand Filter", 4 | "description": "Enable Amazon Brand Filter" 5 | }, 6 | "popup_enabled_tooltip": { 7 | "message": "If checked, the extension will filter out brands you don't want to see", 8 | "description": "enabled checkbox tooltip" 9 | }, 10 | "popup_filter_sidebar": { 11 | "message": "Filter Sidebar", 12 | "description": "Also hide brands in the sidebar" 13 | }, 14 | "popup_filter_sidebar_tooltip": { 15 | "message": "If checked this will hide (or grey out) brands in the sidebar that are not on the list", 16 | "description": "filter sidebar tooltip" 17 | }, 18 | "popup_sidebar_hide": { 19 | "message": "Hide", 20 | "description": "Hide filtered items in the sidebar" 21 | }, 22 | "popup_sidebar_hide_tooltip": { 23 | "message": "If selected this will hide brands in the sidebar that are not on the list", 24 | "description": "sidebar hide tooltip" 25 | }, 26 | "popup_sidebar_grey": { 27 | "message": "Grey Out", 28 | "description": "Grey out filtered items in the sidebar" 29 | }, 30 | "popup_sidebar_grey_tooltip": { 31 | "message": "If selected this will set the text of unknown brands in the sidebar to grey", 32 | "description": "sidebar grey tooltip" 33 | }, 34 | "popup_allow_refine_bypass": { 35 | "message": "Allow Sidebar Bypass", 36 | "description": "Disable filtering when you select a brand in the sidebar" 37 | }, 38 | "popup_allow_refine_bypass_tooltip": { 39 | "message": "If checked then selecting a brand in the sidebar will bypass the filter and show results regardless of if the brand is on the list", 40 | "description": "Refiner bypass tooltip" 41 | }, 42 | "popup_debug": { 43 | "message": "Debug Mode", 44 | "description": "Enable or disable debug mode" 45 | }, 46 | "popup_debug_tooltip": { 47 | "message": "Will highlight search results in red/green to show what would be filtered without hiding them. Also logs much more to the console.", 48 | "description": "debug mode tooltip" 49 | }, 50 | "popup_personal_blocklist": { 51 | "message": "Personal Blocklist", 52 | "description": "Enable or disable personal blocklist" 53 | }, 54 | "popup_personal_blocklist_tooltip": { 55 | "message": "Brands added to this list will be blocked regardless of if they are on the known list or not.", 56 | "description": "personal blocklist tooltip" 57 | }, 58 | "save_button": { 59 | "message": "Save", 60 | "description": "Save button" 61 | }, 62 | "save_confirm": { 63 | "message": "Saved!", 64 | "description": "Personal list saved confirmation" 65 | }, 66 | "department_header": { 67 | "message": "Departments: ", 68 | "description": "Current Departments, uncheck to disable filtering for a department" 69 | }, 70 | "department_header_tooltip": { 71 | "message": "All departments ever seen. Unchecking a department will disable filtering any time that department is seen.", 72 | "description": "Current Departments, uncheck to disable filtering for a department" 73 | }, 74 | "brand_list_version": { 75 | "message": "List Version: ", 76 | "description": "Show what version of the list we have" 77 | }, 78 | "brand_list_count": { 79 | "message": "Brands: ", 80 | "description": "How many brands we know about" 81 | }, 82 | "popup_feedback_link": { 83 | "message": "Feedback", 84 | "description": "Link to github to report issues or request features" 85 | }, 86 | "popup_dashboard": { 87 | "message": "Dashboard", 88 | "description": "Amazon Brand Filter Dashboard" 89 | }, 90 | "popup_missing_brand": { 91 | "message": "Missing a brand?", 92 | "description": "Link to AmazonBrandFilter to request a brand be added" 93 | }, 94 | "popup_last_run": { 95 | "message": "Last Run", 96 | "description": "How long the last run took" 97 | }, 98 | "popup_help_translate": { 99 | "message": "Help translate", 100 | "description": "Link to help translate the extension" 101 | }, 102 | "show_all": { 103 | "message": "Show All", 104 | "description": "For show/hide button for known departments on the dashboard" 105 | }, 106 | "hide_all": { 107 | "message": "Hide All", 108 | "description": "For show/hide button for known departments on the dashboard" 109 | }, 110 | "dept_count": { 111 | "message": "Departments: ", 112 | "description": "How many departments we know about" 113 | }, 114 | "dept_unknown": { 115 | "message": "Unknown", 116 | "description": "Unknown department" 117 | }, 118 | "current_departments": { 119 | "message": "Current Departments", 120 | "description": "Current Departments" 121 | }, 122 | "current_departments_tooltip": { 123 | "message": "Departments found on page. Unchecking a department will disable filtering any time that department is seen.", 124 | "description": "Current Departments" 125 | }, 126 | "use_filter_with_refiner": { 127 | "message": "Filter with Refiner", 128 | "description": "label for Filter with Refiner checkbox" 129 | }, 130 | "use_filter_with_refiner_tooltip": { 131 | "message": "Base brand filter on the brands showing in the refiner on the left side of the page. This will reduce false positives but might also increase false negatives if Amazon doesn't show all the brands on the page.", 132 | "description": "tooltip for use_filter_with_refiner" 133 | }, 134 | "experimental_features": { 135 | "message": "Experimental Features", 136 | "description": "label for Experimental Features header" 137 | }, 138 | "experimental_features_tooltip": { 139 | "message": "These features are experimental and may not work as expected. Please report any issues you encounter.", 140 | "description": "tooltip for experimental features" 141 | }, 142 | "dashboard_notice": { 143 | "message": "(New features here!)", 144 | "description": "Notice for the dashboard" 145 | }, 146 | "known_brands_list_text": { 147 | "message": "Known Brands: ", 148 | "description": "Known Brands header" 149 | }, 150 | "known_brands_list_text_tooltip": { 151 | "message": "Known Brands: ", 152 | "description": "All brands known by AmazonBrandFilter" 153 | }, 154 | "search_depth": { 155 | "message": "Search Depth: ", 156 | "description": "Searth Depth label" 157 | }, 158 | "search_depth_tooltip": { 159 | "message": "Controls how many words in the product title are searched, a higher number will catch more brands but may also increase false positives while a low number may miss some brands.", 160 | "description": "Searth Depth tooltip" 161 | }, 162 | "seen_brands_list_text": { 163 | "message": "Seen Brands: ", 164 | "description": "Seen Brands header" 165 | }, 166 | "seen_brands_list_text_tooltip": { 167 | "message": "Every brand ever seen. While searching.", 168 | "description": "Seen Brands tooltip" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/assets/_locales/es/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "popup_enabled": { 3 | "message": "Activar Amazon Brand Filter", 4 | "description": "Activar el filtro de marcas de Amazon" 5 | }, 6 | "popup_enabled_tooltip": { 7 | "message": "Si está marcado, la extensión filtrará las marcas que no deseas ver. ", 8 | "description": "enabled checkbox tooltip" 9 | }, 10 | "popup_filter_sidebar": { 11 | "message": "Filtrar barra lateral", 12 | "description": "También ocultar marcas en la barra lateral" 13 | }, 14 | "popup_filter_sidebar_tooltip": { 15 | "message": "Si está marcado, esto ocultará (o atenuará) las marcas en la barra lateral que no están en la lista. ", 16 | "description": "filter sidebar tooltip" 17 | }, 18 | "popup_sidebar_hide": { 19 | "message": "Ocultar", 20 | "description": "Ocultar los elementos filtrados en la barra lateral" 21 | }, 22 | "popup_sidebar_hide_tooltip": { 23 | "message": "Si se selecciona, esto ocultará las marcas en la barra lateral que no están en la lista. ", 24 | "description": "sidebar hide tooltip" 25 | }, 26 | "popup_sidebar_grey": { 27 | "message": "Gris Fuera", 28 | "description": "Filtrar los elementos de la barra lateral" 29 | }, 30 | "popup_sidebar_grey_tooltip": { 31 | "message": "Si se selecciona, esto establecerá el texto de marcas desconocidas en la barra lateral a gris. ", 32 | "description": "sidebar grey tooltip" 33 | }, 34 | "popup_allow_refine_bypass": { 35 | "message": "Permitir el desvío de la barra lateral", 36 | "description": "Desactivar el filtrado al seleccionar una marca en la barra lateral" 37 | }, 38 | "popup_allow_refine_bypass_tooltip": { 39 | "message": "Si está marcado, al seleccionar una marca en la barra lateral se omitirá el filtro y se mostrarán los resultados independientemente de si la marca está en la lista.", 40 | "description": "Refiner bypass tooltip" 41 | }, 42 | "popup_debug": { 43 | "message": "Modo depuración", 44 | "description": "Activar o desactivar el modo de depuración" 45 | }, 46 | "popup_debug_tooltip": { 47 | "message": "Resaltará los resultados de búsqueda en rojo/verde para mostrar lo que sería filtrado sin ocultarlos. También registra mucho más en la consola. ", 48 | "description": "debug mode tooltip" 49 | }, 50 | "popup_personal_blocklist": { 51 | "message": "Lista de bloqueo personal", 52 | "description": "Activar o desactivar la lista de bloqueo personal" 53 | }, 54 | "popup_personal_blocklist_tooltip": { 55 | "message": "Las marcas añadidas a esta lista serán bloqueadas independientemente de si están en la lista conocida o no. ", 56 | "description": "personal blocklist tooltip" 57 | }, 58 | "popup_save_button": { 59 | "message": "Ahorrar", 60 | "description": "botón guardar" 61 | }, 62 | "department_header": { 63 | "message": "Departments", 64 | "description": "Current Departments, uncheck to disable filtering for a department" 65 | }, 66 | "department_header_tooltip": { 67 | "message": "Todos los departamentos vistos alguna vez. Desmarcar un departamento deshabilitará el filtrado cada vez que se vea ese departamento. ", 68 | "description": "Current Departments, uncheck to disable filtering for a department" 69 | }, 70 | "popup_save_confirm": { 71 | "message": "Salvado!", 72 | "description": "Confirmación de lista personal guardada" 73 | }, 74 | "popup_list_version": { 75 | "message": "Versión de lista: ", 76 | "description": "Mostrar qué versión de la lista tenemos" 77 | }, 78 | "popup_list_count": { 79 | "message": "Marcas conocidas: ", 80 | "description": "Cuántas marcas conocemos" 81 | }, 82 | "popup_feedback_link": { 83 | "message": "Comentarios (inglés)", 84 | "description": "Enlace a github para informar problemas o solicitar funciones" 85 | }, 86 | "popup_dashboard": { 87 | "message": "Panel", 88 | "description": "Panel de filtro de marca de Amazon" 89 | }, 90 | "popup_missing_brand": { 91 | "message": "¿Falta una marca?", 92 | "description": "Enlace a AmazonBrandFilter para solicitar que se agregue una marca" 93 | }, 94 | "popup_last_run": { 95 | "message": "Ejecución anterior", 96 | "description": "Cuánto tiempo tomó la última carrera" 97 | }, 98 | "popup_help_translate": { 99 | "message": "Help translate", 100 | "description": "Link to help translate the extension" 101 | }, 102 | "show_all": { 103 | "message": "Mostrar todo", 104 | "description": "Para mostrar/ocultar el botón para departamentos conocidos en el panel" 105 | }, 106 | "hide_all": { 107 | "message": "Ocultar todo", 108 | "description": "Para mostrar/ocultar el botón para departamentos conocidos en el panel" 109 | }, 110 | "dept_count": { 111 | "message": "Departamentos: ", 112 | "description": "Cuántos departamentos conocemos" 113 | }, 114 | "dept_unknown": { 115 | "message": "Desconocido", 116 | "description": "Departamento desconocido" 117 | }, 118 | "current_departments": { 119 | "message": "Departamentos actuales", 120 | "description": "Departamentos actuales" 121 | }, 122 | "current_departments_tooltip": { 123 | "message": "Departamentos encontrados en la página. Desmarcar un departamento deshabilitará el filtrado cada vez que se vea ese departamento. ", 124 | "description": "Current Departments" 125 | }, 126 | "use_filter_with_refiner": { 127 | "message": "Filtrar con refinador", 128 | "description": "label for Filter with Refiner checkbox" 129 | }, 130 | "use_filter_with_refiner_tooltip": { 131 | "message": "Filtro de marca base en las marcas que se muestran en el refinador en el lado izquierdo de la página. Esto reducirá los falsos positivos, pero también podría aumentar los falsos negativos si Amazon no muestra todas las marcas en la página.", 132 | "description": "tooltip for use_filter_with_refiner" 133 | }, 134 | "experimental_features": { 135 | "message": "Funciones experimentales", 136 | "description": "label for Experimental Features header" 137 | }, 138 | "experimental_features_tooltip": { 139 | "message": "Estas funciones son experimentales y es posible que no funcionen como se esperaba. Informe cualquier problema que encuentre.", 140 | "description": "tooltip for experimental features" 141 | }, 142 | "dashboard_notice": { 143 | "message": "(¡Nuevas funciones aquí!)", 144 | "description": "Notice for the dashboard" 145 | }, 146 | "known_brands_list_text": { 147 | "message": "Marcas Conocidas: ", 148 | "description": "Known Brands header" 149 | }, 150 | "known_brands_list_text_tooltip": { 151 | "message": "Marcas Conocidas: ", 152 | "description": "All brands known by AmazonBrandFilter" 153 | }, 154 | "search_depth": { 155 | "message": "profundidad de búsqueda: ", 156 | "description": "Searth Depth label" 157 | }, 158 | "search_depth_tooltip": { 159 | "message": "Controla cuántas palabras del título del producto se buscan; un número más alto detectará más marcas, pero también puede aumentar los falsos positivos, mientras que un número bajo puede pasar por alto algunas marcas.", 160 | "description": "Searth Depth tooltip" 161 | }, 162 | "seen_brands_list_text": { 163 | "message": "Marcas vistas: ", 164 | "description": "Seen Brands header" 165 | }, 166 | "seen_brands_list_text_tooltip": { 167 | "message": "Cada marca jamás vista. Mientras buscaba.", 168 | "description": "Seen Brands tooltip" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/assets/_locales/fr/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "popup_enabled": { 3 | "message": "Activer le filtre de marque Amazon", 4 | "description": "Enable Amazon Brand Filter" 5 | }, 6 | "popup_enabled_tooltip": { 7 | "message": "Si cette option est cochée, l'extension filtrera les marques que vous ne souhaitez pas voir. ", 8 | "description": "enabled checkbox tooltip" 9 | }, 10 | "popup_filter_sidebar": { 11 | "message": "Filtrer la barre latérale", 12 | "description": "Also hide brands in the sidebar" 13 | }, 14 | "popup_filter_sidebar_tooltip": { 15 | "message": "Si cette option est cochée, cela masquera (ou grisonnera) les marques dans la barre latérale qui ne figurent pas sur la liste. ", 16 | "description": "filter sidebar tooltip" 17 | }, 18 | "popup_sidebar_hide": { 19 | "message": "Cacher", 20 | "description": "Hide filtered items in the sidebar" 21 | }, 22 | "popup_sidebar_hide_tooltip": { 23 | "message": "Si cette option est sélectionnée, cela masquera les marques dans la barre latérale qui ne figurent pas sur la liste. ", 24 | "description": "sidebar hide tooltip" 25 | }, 26 | "popup_sidebar_grey": { 27 | "message": "surligner", 28 | "description": "Grey out filtered items in the sidebar" 29 | }, 30 | "popup_sidebar_grey_tooltip": { 31 | "message": "Si cette option est sélectionnée, cela définira le texte des marques inconnues dans la barre latérale en gris. ", 32 | "description": "sidebar grey tooltip" 33 | }, 34 | "popup_allow_refine_bypass": { 35 | "message": "Autoriser le contournement de la barre latérale", 36 | "description": "Disable filtering when you select a brand in the sidebar" 37 | }, 38 | "popup_allow_refine_bypass_tooltip": { 39 | "message": "Si coché, la sélection d'une marque dans la barre latérale contournera le filtre et affichera les résultats, que la marque figure ou non sur la liste. ", 40 | "description": "Refiner bypass tooltip" 41 | }, 42 | "popup_debug": { 43 | "message": "Mode débogage", 44 | "description": "Enable or disable debug mode" 45 | }, 46 | "popup_debug_tooltip": { 47 | "message": "Mettra en surbrillance les résultats de recherche en rouge/vert pour montrer ce qui serait filtré sans les masquer. Enregistre également beaucoup plus dans la console. ", 48 | "description": "debug mode tooltip" 49 | }, 50 | "popup_personal_blocklist": { 51 | "message": "Liste de marques filtrées", 52 | "description": "Enable or disable personal blocklist" 53 | }, 54 | "popup_personal_blocklist_tooltip": { 55 | "message": "Les marques ajoutées à cette liste seront bloquées, qu'elles figurent ou non sur la liste connue. ", 56 | "description": "personal blocklist tooltip" 57 | }, 58 | "save_button": { 59 | "message": "Sauvegarder", 60 | "description": "Save button" 61 | }, 62 | "save_confirm": { 63 | "message": "Sauvé!", 64 | "description": "Personal list saved confirmation" 65 | }, 66 | "department_header": { 67 | "message": "Départements: ", 68 | "description": "Current Departments, uncheck to disable filtering for a department" 69 | }, 70 | "department_header_tooltip": { 71 | "message": "Tous les départements jamais vus. Désélectionner un département désactivera le filtrage chaque fois que ce département est vu. ", 72 | "description": "Current Departments, uncheck to disable filtering for a department" 73 | }, 74 | "brand_list_version": { 75 | "message": "Version de la liste: ", 76 | "description": "Show what version of the list we have" 77 | }, 78 | "brand_list_count": { 79 | "message": "Marques: ", 80 | "description": "How many brands we know about" 81 | }, 82 | "popup_feedback_link": { 83 | "message": "Commentaires (anglais)", 84 | "description": "Link to github to report issues or request features" 85 | }, 86 | "popup_dashboard": { 87 | "message": "Tableau de bord", 88 | "description": "Amazon Brand Filter Dashboard" 89 | }, 90 | "popup_missing_brand": { 91 | "message": "Il vous manque une marque?", 92 | "description": "Link to AmazonBrandFilter to request a brand be added" 93 | }, 94 | "popup_last_run": { 95 | "message": "Last exécution", 96 | "description": "How long the last run took" 97 | }, 98 | "popup_help_translate": { 99 | "message": "Aidez à traduire", 100 | "description": "Link to help translate the extension" 101 | }, 102 | "show_all": { 103 | "message": "Afficher tout", 104 | "description": "For show/hide button for known departments on the dashboard" 105 | }, 106 | "hide_all": { 107 | "message": "Masquer tout", 108 | "description": "For show/hide button for known departments on the dashboard" 109 | }, 110 | "dept_count": { 111 | "message": "Départements: ", 112 | "description": "How many departments we know about" 113 | }, 114 | "dept_unknown": { 115 | "message": "Inconnu", 116 | "description": "Unknown department" 117 | }, 118 | "current_departments": { 119 | "message": "Départements actuels", 120 | "description": "Current Departments" 121 | }, 122 | "current_departments_tooltip": { 123 | "message": "Départements trouvés sur la page. Désélectionner un département désactivera le filtrage chaque fois que ce département est détecté. ", 124 | "description": "Current Departments" 125 | }, 126 | "use_filter_with_refiner": { 127 | "message": "Filtrer avec raffineur", 128 | "description": "label for Filter with Refiner checkbox" 129 | }, 130 | "use_filter_with_refiner_tooltip": { 131 | "message": "Filtrez les marques de base sur les marques affichées dans l'affineur sur le côté gauche de la page. Cela réduira les faux positifs mais pourrait également augmenter les faux négatifs si Amazon n'affiche pas toutes les marques sur la page.", 132 | "description": "tooltip for use_filter_with_refiner" 133 | }, 134 | "experimental_features": { 135 | "message": "Fonctionnalités expérimentales", 136 | "description": "label for Experimental Features header" 137 | }, 138 | "experimental_features_tooltip": { 139 | "message": "Ces fonctionnalités sont expérimentales et peuvent ne pas fonctionner comme prévu. Veuillez signaler tout problème que vous rencontrez.", 140 | "description": "tooltip for experimental features" 141 | }, 142 | "dashboard_notice": { 143 | "message": "(Nouvelles fonctionnalités ici !)", 144 | "description": "Notice for the dashboard" 145 | }, 146 | "known_brands_list_text": { 147 | "message": "Marques connues: ", 148 | "description": "Known Brands header" 149 | }, 150 | "known_brands_list_text_tooltip": { 151 | "message": "Marques connues: ", 152 | "description": "All brands known by AmazonBrandFilter" 153 | }, 154 | "search_depth": { 155 | "message": "Profondeur de recherche: ", 156 | "description": "Searth Depth label" 157 | }, 158 | "search_depth_tooltip": { 159 | "message": "Contrôle le nombre de mots du titre du produit qui sont recherchés. Un nombre plus élevé permettra de détecter plus de marques, mais peut également augmenter les faux positifs, tandis qu'un nombre faible peut manquer certaines marques.", 160 | "description": "Searth Depth tooltip" 161 | }, 162 | "seen_brands_list_text": { 163 | "message": "Marques Vues: ", 164 | "description": "Seen Brands header" 165 | }, 166 | "seen_brands_list_text_tooltip": { 167 | "message": "Toutes les marques jamais vues. Pendant la recherche.", 168 | "description": "Seen Brands tooltip" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/assets/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "popup_enabled": { 3 | "message": "Aktivieren Sie Amazon Brand Filter", 4 | "description": "Aktivieren Sie Amazon Brand Filter" 5 | }, 6 | "popup_enabled_tooltip": { 7 | "message": "Wenn aktiviert, filtert die Erweiterung Marken heraus, die Sie nicht sehen möchten. ", 8 | "description": "enabled checkbox tooltip" 9 | }, 10 | "popup_filter_sidebar": { 11 | "message": "Seitenleiste filtern", 12 | "description": "Auch Marken in der Seitenleiste ausblenden" 13 | }, 14 | "popup_filter_sidebar_tooltip": { 15 | "message": "Wenn aktiviert, werden Marken in der Seitenleiste, die nicht auf der Liste stehen, ausgeblendet (oder ausgegraut). ", 16 | "description": "filter sidebar tooltip" 17 | }, 18 | "popup_sidebar_hide": { 19 | "message": "Ausblenden", 20 | "description": "Gefilterte Elemente in der Seitenleiste ausblenden" 21 | }, 22 | "popup_sidebar_hide_tooltip": { 23 | "message": "Wenn ausgewählt, werden Marken in der Seitenleiste ausgeblendet, die nicht auf der Liste stehen. ", 24 | "description": "sidebar hide tooltip" 25 | }, 26 | "popup_sidebar_grey": { 27 | "message": "Grey Out", 28 | "description": "Gefilterte Elemente in der Seitenleiste ausgrauen" 29 | }, 30 | "popup_sidebar_grey_tooltip": { 31 | "message": "Wenn ausgewählt, wird der Text unbekannter Marken in der Seitenleiste auf Grau gesetzt. ", 32 | "description": "sidebar grey tooltip" 33 | }, 34 | "popup_allow_refine_bypass": { 35 | "message": "Umgehung der Seitenleiste zulassen", 36 | "description": "Filterung bei Auswahl einer Marke in der Seitenleiste deaktivieren" 37 | }, 38 | "popup_allow_refine_bypass_tooltip": { 39 | "message": "Wenn aktiviert, wird durch die Auswahl einer Marke in der Seitenleiste der Filter umgangen und Ergebnisse werden angezeigt, unabhängig davon, ob die Marke auf der Liste steht. ", 40 | "description": "Refiner bypass tooltip" 41 | }, 42 | "popup_debug": { 43 | "message": "Debug_Modus", 44 | "description": "Aktivieren oder Deaktivieren des Debug_Modus" 45 | }, 46 | "popup_debug_tooltip": { 47 | "message": "Hebt Suchergebnisse in Rot/Grün hervor, um anzuzeigen, was gefiltert würde, ohne sie zu verbergen. Protokolliert außerdem viel mehr in der Konsole.", 48 | "description": "debug mode tooltip" 49 | }, 50 | "popup_personal_blocklist": { 51 | "message": "Persönliche Sperrliste", 52 | "description": "Aktivieren oder Deaktivieren der persönlichen Blockliste" 53 | }, 54 | "popup_personal_blocklist_tooltip": { 55 | "message": "Marken, die zu dieser Liste hinzugefügt werden, werden blockiert, unabhängig davon, ob sie auf der bekannten Liste stehen oder nicht. ", 56 | "description": "personal blocklist tooltip" 57 | }, 58 | "popup_save_button": { 59 | "message": "Speichern Sie", 60 | "description": "Schaltfläche Speichern" 61 | }, 62 | "popup_save_confirm": { 63 | "message": "Gerettet!", 64 | "description": "Persönliche Liste gespeicherte Bestätigung" 65 | }, 66 | "popup_department_header": { 67 | "message": "Departments", 68 | "description": "Current Departments, uncheck to disable filtering for a department" 69 | }, 70 | "department_header_tooltip": { 71 | "message": "Alle jemals gesehenen Abteilungen. Das Deaktivieren einer Abteilung wird das Filtern jedes Mal deaktivieren, wenn diese Abteilung gesehen wird. ", 72 | "description": "Current Departments, uncheck to disable filtering for a department" 73 | }, 74 | "popup_list_version": { 75 | "message": "Liste Version: ", 76 | "description": "Zeigen, welche Version der Liste wir haben" 77 | }, 78 | "popup_list_count": { 79 | "message": "Bekannte Marken: ", 80 | "description": "Wie viele Marken wir kennen" 81 | }, 82 | "popup_feedback_link": { 83 | "message": "Feedback (Englisch)", 84 | "description": "Link zu Github, um Probleme zu melden oder Funktionen anzufordern" 85 | }, 86 | "popup_dashboard": { 87 | "message": "Armaturenbrett", 88 | "description": "Amazon-Markenfilter-Dashboard" 89 | }, 90 | "popup_missing_brand": { 91 | "message": "Fehlt eine Marke?", 92 | "description": "Link zu AmazonBrandFilter, um das Hinzufügen einer Marke zu beantragen" 93 | }, 94 | "popup_last_run": { 95 | "message": "Letzter Lauf", 96 | "description": "Wie lange der letzte Lauf gedauert hat" 97 | }, 98 | "popup_help_translate": { 99 | "message": "Help translate", 100 | "description": "Link to help translate the extension" 101 | }, 102 | "show_all": { 103 | "message": "Alles anzeigen", 104 | "description": "Zum Ein-/Ausblenden der Schaltfläche für bekannte Abteilungen im Dashboard" 105 | }, 106 | "hide_all": { 107 | "message": "Alles ausblenden", 108 | "description": "Zum Ein-/Ausblenden der Schaltfläche für bekannte Abteilungen im Dashboard" 109 | }, 110 | "dept_count": { 111 | "message": "Abteilungen: ", 112 | "description": "How many departments we know about" 113 | }, 114 | "dept_unknown": { 115 | "message": "Unbekannt", 116 | "description": "Unbekannte Abteilung" 117 | }, 118 | "current_departments": { 119 | "message": "Aktuelle Abteilungen", 120 | "description": "Aktuelle Abteilungen" 121 | }, 122 | "current_departments_tooltip": { 123 | "message": "Abteilungen auf der Seite gefunden. Das Deaktivieren einer Abteilung wird das Filtern deaktivieren, sobald diese Abteilung angezeigt wird. ", 124 | "description": "Current Departments" 125 | }, 126 | "use_filter_with_refiner": { 127 | "message": "Mit Refiner filtern", 128 | "description": "label for Filter with Refiner checkbox" 129 | }, 130 | "use_filter_with_refiner_tooltip": { 131 | "message": "Basieren Sie den Markenfilter auf den Marken, die im Refiner auf der linken Seite der Seite angezeigt werden. Dadurch wird die Zahl der falsch-positiven Ergebnisse reduziert, es kann aber auch zu einer Zunahme der falsch-negativen Ergebnisse kommen, wenn Amazon nicht alle Marken auf der Seite anzeigt.", 132 | "description": "tooltip for use_filter_with_refiner" 133 | }, 134 | "experimental_features": { 135 | "message": "Experimentelle Funktionen", 136 | "description": "label for Experimental Features header" 137 | }, 138 | "experimental_features_tooltip": { 139 | "message": "Diese Funktionen sind experimentell und funktionieren möglicherweise nicht wie erwartet. Bitte melden Sie alle auftretenden Probleme.", 140 | "description": "tooltip for experimental features" 141 | }, 142 | "dashboard_notice": { 143 | "message": "(Neue Funktionen hier!)", 144 | "description": "Notice for the dashboard" 145 | }, 146 | "known_brands_list_text": { 147 | "message": "Bekannte Marken: ", 148 | "description": "Known Brands header" 149 | }, 150 | "known_brands_list_text_tooltip": { 151 | "message": "Bekannte Marken: ", 152 | "description": "All brands known by AmazonBrandFilter" 153 | }, 154 | 155 | "search_depth": { 156 | "message": "Suchtiefe: ", 157 | "description": "Searth Depth label" 158 | }, 159 | "search_depth_tooltip": { 160 | "message": "Steuert, wie viele Wörter im Produkttitel durchsucht werden. Eine höhere Zahl erfasst mehr Marken, kann aber auch die Anzahl der Fehlalarme erhöhen, während eine niedrige Zahl einige Marken übersehen kann.", 161 | "description": "Searth Depth tooltip" 162 | }, 163 | "seen_brands_list_text": { 164 | "message": "Gesehene Marken: ", 165 | "description": "Known Brands header" 166 | }, 167 | "seen_brands_list_text_tooltip": { 168 | "message": "Jede Marke, die je gesehen wurde. Während der Suche.", 169 | "description": "Known Brands header" 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/utils/browser-helpers.ts: -------------------------------------------------------------------------------- 1 | import { browser } from "webextension-polyfill-ts"; 2 | 3 | import { extractSyncStorageSettingsObject, sleep } from "utils/helpers"; 4 | import { Engine, StorageMode, StorageArea, StorageSettings, SyncStorageSettings } from "utils/types"; 5 | 6 | /** 7 | * retrieves the name of the browser engine based on the runtime environment. 8 | * 9 | * @returns 10 | */ 11 | export const getEngine = (): Engine => { 12 | if (typeof chrome !== "undefined") { 13 | return "chromium"; 14 | } else if (typeof browser !== "undefined") { 15 | return "gecko"; 16 | } else { 17 | throw new Error("Unsupported engine."); 18 | } 19 | }; 20 | 21 | /** 22 | * retrieves the API object for the current browser environment. 23 | * 24 | * @returns 25 | */ 26 | export const getEngineApi = () => { 27 | const engine = getEngine(); 28 | if (engine === "chromium") { 29 | return chrome; 30 | } else if (engine === "gecko") { 31 | return browser; 32 | } else { 33 | throw new Error("Unsupported engine."); 34 | } 35 | }; 36 | 37 | /** 38 | * Retrieves a value from storage based on the current browser environment. 39 | * watch out for QUOTA_BYTES_PER_ITEM error when using "sync" param (chrome) 40 | * 41 | * @param keys - The key/keys to look up in storage. Use null if undefined, which will return all keys. 42 | * @param storageArea - The storage area to use. Defaults to "local". 43 | * @returns 44 | */ 45 | export async function getStorageValue(storageArea?: Exclude): Promise; 46 | export async function getStorageValue(storageArea: "sync"): Promise; 47 | export async function getStorageValue( 48 | keys: T | T[], 49 | storageArea?: Exclude 50 | ): Promise>; 51 | export async function getStorageValue( 52 | keys: T | T[], 53 | storageArea: "sync" 54 | ): Promise>; 55 | export async function getStorageValue( 56 | keys?: T | T[], 57 | storageArea: StorageArea = "local" 58 | ): Promise> { 59 | const engine = getEngine(); 60 | if (engine === "chromium" && chrome.storage && chrome.storage[storageArea]) { 61 | return await new Promise((resolve) => { 62 | chrome.storage[storageArea].get(keys ?? null, (result) => { 63 | resolve(result as Record); 64 | }); 65 | }); 66 | } else if (engine === "gecko" && browser.storage && browser.storage[storageArea]) { 67 | const result = await browser.storage[storageArea].get(keys ?? null); 68 | return result as Record; 69 | } else { 70 | throw new Error("Storage API not found."); 71 | } 72 | } 73 | 74 | /** 75 | * set value in storage 76 | * 77 | * @param data - The data to store. 78 | * @param storageArea - The storage area to use. Defaults to "local". 79 | * @param mode - Determines whether to overwrite the existing data or merge it with the new data. Defaults to "normal". 80 | * @returns 81 | */ 82 | export async function setStorageValue( 83 | data: Partial, 84 | storageArea?: Exclude 85 | ): Promise; 86 | export async function setStorageValue(data: Partial, storageArea: "sync"): Promise; 87 | export async function setStorageValue( 88 | data: Partial, 89 | storageArea?: Exclude, 90 | mode?: StorageMode 91 | ): Promise; 92 | export async function setStorageValue( 93 | data: Partial, 94 | storageArea: "sync", 95 | mode?: StorageMode 96 | ): Promise; 97 | export async function setStorageValue( 98 | data: Partial, 99 | storageArea: StorageArea = "local", 100 | mode: StorageMode = "normal" 101 | ): Promise { 102 | const engine = getEngine(); 103 | if (engine === "chromium" && chrome.storage && chrome.storage[storageArea]) { 104 | if (mode === "overwrite") { 105 | await new Promise((resolve) => { 106 | chrome.storage[storageArea].clear(() => { 107 | resolve(); 108 | }); 109 | }); 110 | } 111 | return new Promise((resolve) => { 112 | chrome.storage[storageArea].set(data, () => { 113 | resolve(); 114 | }); 115 | }); 116 | } else if (engine === "gecko" && browser.storage && browser.storage[storageArea]) { 117 | if (mode === "overwrite") { 118 | await browser.storage[storageArea].clear(); 119 | } 120 | return browser.storage[storageArea].set(data); 121 | } else { 122 | throw new Error("Storage API not found."); 123 | } 124 | } 125 | 126 | /** 127 | * attempt to get the sync settings first, then fall back to local 128 | * sync settings may not exist in some cases, so we just return the local settings instead 129 | * 130 | * @returns 131 | */ 132 | export const getSettings = async () => { 133 | let syncSettings = await getStorageValue("sync"); 134 | const settings = await getStorageValue(); 135 | if (Object.keys(syncSettings).length === 0) { 136 | syncSettings = { ...extractSyncStorageSettingsObject(settings) }; 137 | } 138 | return { settings, syncSettings }; 139 | }; 140 | 141 | export const getMessage = async (message: string): Promise => { 142 | const engine = getEngine(); 143 | if (engine == "gecko" && browser.i18n) { 144 | browser.i18n.getMessage(message); 145 | return browser.i18n.getMessage(message); 146 | } else if (engine == "chromium" && chrome.i18n) { 147 | return chrome.i18n.getMessage(message); 148 | } else { 149 | throw new Error("Unsupported engine."); 150 | } 151 | }; 152 | 153 | export const setIcon = async () => { 154 | const result = await getStorageValue("enabled"); 155 | if (result.enabled) { 156 | getEngineApi().action.setIcon({ 157 | path: { 158 | 48: "icons/abf-enabled-128.png", 159 | }, 160 | }); 161 | } else { 162 | getEngineApi().action.setIcon({ 163 | path: { 164 | 48: "icons/abf-disabled-128.png", 165 | }, 166 | }); 167 | } 168 | }; 169 | 170 | /** 171 | * Retrieves the manifest data for the current extension based on the runtime engine. 172 | * 173 | * @returns 174 | */ 175 | export const getManifest = () => { 176 | const engine = getEngine(); 177 | if (engine === "chromium") { 178 | return chrome.runtime.getManifest(); 179 | } else if (engine === "gecko") { 180 | return browser.runtime.getManifest(); 181 | } else { 182 | throw new Error("Unsupported engine."); 183 | } 184 | }; 185 | 186 | /** 187 | * Retrieves information about the currently active tab based on the runtime engine. 188 | * 189 | * @returns 190 | */ 191 | export const getCurrentTab = () => { 192 | const engine = getEngine(); 193 | if (engine === "chromium") { 194 | return new Promise((resolve) => { 195 | chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { 196 | resolve(tabs[0]); 197 | }); 198 | }); 199 | } else if (engine === "gecko") { 200 | return browser.tabs.query({ active: true, currentWindow: true }); 201 | } else { 202 | throw new Error("Unsupported engine."); 203 | } 204 | }; 205 | 206 | /** 207 | * waits for settings to be set in storage from the background script 208 | * required as the background script may not be ready before the popup/content scripts are loaded 209 | * always check local storage, never sync storage as sync storage is not always available 210 | * 211 | * @returns 212 | */ 213 | export const ensureSettingsExist = async (): Promise => { 214 | const settings = await getStorageValue(); 215 | 216 | // if there are no settings, wait for a period and try again 217 | if (Object.keys(settings).length === 0) { 218 | console.log("no settings found, waiting briefly and trying again"); 219 | await sleep(1500); 220 | return ensureSettingsExist(); 221 | } 222 | console.log("AmazonBrandFilter: %cSettings exist!", "color: lightgreen"); 223 | return true; 224 | }; 225 | -------------------------------------------------------------------------------- /src/content/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ensureSettingsExist, 3 | getEngineApi, 4 | getSettings, 5 | getStorageValue, 6 | setStorageValue, 7 | } from "utils/browser-helpers"; 8 | import { getItemDivs, unHideDivs, getRefinerBrands } from "utils/helpers"; 9 | import { PopupMessage, SeenBrand, StorageSettings } from "utils/types"; 10 | 11 | const debugRed = "#ffbbbb"; 12 | const debugGreen = "#bbffbb"; 13 | const debugYellow = "#ffffbb"; 14 | 15 | const descriptionSearch = async (settings: StorageSettings, div: HTMLDivElement) => { 16 | const { syncSettings } = await getSettings(); 17 | var shortText = div.getElementsByClassName("a-color-base a-text-normal") as HTMLCollectionOf; 18 | if (shortText.length === 0) { 19 | return; 20 | } 21 | 22 | // check to see if each word is in the map. if we dont stop then we hide it. 23 | const searchDepth = syncSettings.searchDepth; 24 | const fullText = shortText[0]?.innerText.toUpperCase() ?? ""; 25 | const wordList = fullText.replace(", ", " ").split(" ").slice(0, searchDepth); 26 | for (let w = 0; w < settings.maxWordCount + 3; w++) { 27 | for (let x = 0; x < wordList.length; x++) { 28 | const searchTerm = wordList.slice(x, w).join(" "); 29 | if (searchTerm.length === 0) { 30 | continue; 31 | } 32 | 33 | if (settings.brandsMap[searchTerm]) { 34 | if (syncSettings.usePersonalBlock) { 35 | // block if found in personal block list 36 | if (syncSettings.personalBlockMap && syncSettings.personalBlockMap[searchTerm]) { 37 | if (settings.useDebugMode) { 38 | div.style.display = "block"; 39 | div.style.backgroundColor = debugYellow; 40 | addDebugLabel(div, searchTerm); 41 | } else { 42 | div.style.display = "none"; 43 | div.style.backgroundColor = "white"; 44 | } 45 | return; 46 | } else { 47 | // if personal block is not enabled then we want to show the item again 48 | div.style.display = "block"; 49 | if (settings.useDebugMode) { 50 | div.style.backgroundColor = debugGreen; 51 | addDebugLabel(div, searchTerm); 52 | } else { 53 | removeDebugLabel(div); 54 | } 55 | return; 56 | } 57 | } else { 58 | // if personal block is not enabled then we want to show the item again 59 | div.style.display = "block"; 60 | if (settings.useDebugMode) { 61 | div.style.backgroundColor = debugGreen; 62 | addDebugLabel(div, searchTerm); 63 | } else { 64 | removeDebugLabel(div); 65 | div.style.backgroundColor = "white"; 66 | } 67 | return; 68 | } 69 | 70 | if (settings.useDebugMode) { 71 | addDebugLabel(div, searchTerm); 72 | } else { 73 | removeDebugLabel(div); 74 | div.style.backgroundColor = "white"; 75 | } 76 | return; 77 | } 78 | } 79 | } 80 | 81 | if (settings.useDebugMode) { 82 | div.style.display = "block"; 83 | div.style.backgroundColor = debugRed; 84 | } else { 85 | div.style.display = "none"; 86 | div.style.backgroundColor = "white"; 87 | } 88 | }; 89 | 90 | const addDebugLabel = (div: HTMLDivElement, searchTerm: string) => { 91 | if (div.getElementsByClassName("ABF-DebugLabel " + searchTerm).length === 0) { 92 | var abflabel = document.createElement("span"); 93 | abflabel.innerText = "ABF: " + searchTerm; 94 | abflabel.className = "ABF-DebugLabel " + searchTerm; 95 | abflabel.style.backgroundColor = "darkgreen"; 96 | abflabel.style.color = "white"; 97 | div.insertAdjacentElement("afterbegin", abflabel); 98 | } 99 | }; 100 | const removeDebugLabel = (div: HTMLDivElement) => { 101 | if (div.getElementsByClassName("ABF-DebugLabel").length > 0) { 102 | div.getElementsByClassName("ABF-DebugLabel")[0]!.remove(); 103 | } 104 | }; 105 | const runFilterRefiner = async (settings: StorageSettings) => { 106 | if (!settings.enabled || !settings.filterRefiner) { 107 | return; 108 | } 109 | 110 | const { syncSettings } = await getSettings(); 111 | 112 | const refiner = document.getElementById("brandsRefinements"); 113 | if (!refiner) { 114 | return; 115 | } 116 | 117 | const divs = refiner.getElementsByClassName("a-list-item") as HTMLCollectionOf; 118 | for (const div of divs) { 119 | const brand = 120 | ( 121 | div.getElementsByClassName("a-size-base a-color-base") as HTMLCollectionOf 122 | )[0]?.innerText.toUpperCase() ?? ""; 123 | if (brand.length === 0) { 124 | continue; 125 | } 126 | 127 | if (!settings.brandsMap[brand] || (syncSettings.usePersonalBlock && syncSettings.personalBlockMap[brand])) { 128 | if (settings.refinerMode === "grey") { 129 | div.style.display = "block"; 130 | div 131 | .getElementsByClassName("a-size-base a-color-base")[0] 132 | ?.setAttribute("style", "display: inlne-block; color: grey !important;"); 133 | } else { 134 | div.style.display = "none"; 135 | div 136 | .getElementsByClassName("a-size-base a-color-base")[0] 137 | ?.setAttribute("style", "display: inline-block; color: black !important;"); 138 | } 139 | 140 | if (settings.useDebugMode) { 141 | div.style.display = "inline-block"; 142 | div.style.backgroundColor = debugRed; 143 | } else { 144 | div.style.backgroundColor = "white"; 145 | } 146 | } 147 | } 148 | }; 149 | 150 | const updateSeenBrands = async () => { 151 | const refinerBrands = getRefinerBrands(); 152 | if (refinerBrands.length === 0) { 153 | return; 154 | } 155 | const seenBrands = await (await getStorageValue("seenBrands", "sync")).seenBrands; 156 | var updateSeenBrandsList = false; 157 | for (const brand of refinerBrands) { 158 | if (brand === "") { 159 | continue; 160 | } 161 | if (seenBrands[brand] === undefined) { 162 | // eventually I want to be able to add individual filters and the ability to create new brands tickets with these 163 | const newBrand: SeenBrand = { 164 | hide: false, 165 | }; 166 | console.log("AmazonBrandFilter: Adding brand to seenBrands - " + brand); 167 | seenBrands[brand] = newBrand; 168 | updateSeenBrandsList = true; 169 | } 170 | } 171 | if (updateSeenBrandsList) { 172 | const seenBrandCount = Object.keys(seenBrands).length; 173 | console.debug(`AmazonBrandFilter: Updated seenBrands count: ${seenBrandCount}`); 174 | await setStorageValue({ seenBrands: seenBrands }, "local"); 175 | await setStorageValue({ seenBrands: seenBrands }, "sync"); 176 | await setStorageValue({ seenBrandCount: seenBrandCount }, "local"); 177 | await setStorageValue({ seenBrandCount: seenBrandCount }, "sync"); 178 | } 179 | }; 180 | 181 | const filterBrands = async (settings: StorageSettings) => { 182 | if (!settings.enabled) { 183 | return; 184 | } 185 | 186 | const { syncSettings } = await getSettings(); 187 | 188 | var brands = settings.brandsMap; 189 | if (syncSettings.filterWithRefiner) { 190 | if (settings.useDebugMode) { 191 | console.debug("AmazonBrandFilter: Using refiner to get brands"); 192 | } 193 | const refinerBrands = document.getElementById("brandsRefinements")?.getElementsByClassName("a-spacing-micro"); 194 | if (refinerBrands) { 195 | var refinerKnownBrands: Record = {}; 196 | for (const div of refinerBrands) { 197 | const brand = 198 | ( 199 | div.getElementsByClassName("a-size-base a-color-base") as HTMLCollectionOf 200 | )[0]?.innerText.toUpperCase() ?? ""; 201 | if (brand && brands[brand]) { 202 | refinerKnownBrands[brand] = true; 203 | } 204 | } 205 | if (refinerKnownBrands != null) { 206 | brands = refinerKnownBrands; 207 | if (settings.useDebugMode) { 208 | console.debug("AmazonBrandFilter: Found brands in refiner: " + Object.keys(refinerKnownBrands).join(",")); 209 | } 210 | } 211 | } 212 | } 213 | 214 | if (Object.keys(brands).length === 0) { 215 | console.debug("AmazonBrandFilter: No brands found"); 216 | return; 217 | } 218 | if (settings.useDebugMode) { 219 | console.debug("AmazonBrandFilter: Brands found"); 220 | } 221 | 222 | if (settings.refinerBypass) { 223 | const refiner = document.getElementById("brandsRefinements")?.getElementsByTagName("input"); 224 | if (refiner) { 225 | for (const input of refiner) { 226 | if (input.checked) { 227 | return; 228 | } 229 | } 230 | } 231 | } 232 | // if any of the departments are set not to filter we just cancel for now. 233 | const currentDepts = await (await getStorageValue("currentDepts")).currentDepts; 234 | for (const dept in currentDepts) { 235 | if (syncSettings.knownDepts[dept] != null && syncSettings.knownDepts[dept] != true) { 236 | console.log("AmazonBrandFilter: knownDepts[dept] is: " + syncSettings.knownDepts[dept]); 237 | resetBrands(); 238 | return; 239 | } 240 | } 241 | 242 | const divs = getItemDivs(); 243 | for (const div of divs) { 244 | const itemHeader = div.getElementsByClassName("s-line-clamp-1") as HTMLCollectionOf; 245 | if (itemHeader.length !== 0) { 246 | const searchTerm = itemHeader[0]?.innerText.toUpperCase(); 247 | if (searchTerm && settings.brandsMap[searchTerm]) { 248 | if (syncSettings.usePersonalBlock) { 249 | if (syncSettings.personalBlockMap && syncSettings.personalBlockMap[searchTerm]) { 250 | if (settings.useDebugMode) { 251 | div.style.display = "block"; 252 | div.style.backgroundColor = "yellow"; 253 | } else { 254 | div.style.display = "none"; 255 | } 256 | continue; 257 | } else { 258 | div.style.display = "block"; 259 | if (settings.useDebugMode) { 260 | div.style.backgroundColor = debugGreen; 261 | } else { 262 | div.style.backgroundColor = "white"; 263 | } 264 | continue; 265 | } 266 | } else { 267 | div.style.display = "block"; 268 | if (settings.useDebugMode) { 269 | div.style.backgroundColor = debugGreen; 270 | } else { 271 | div.style.backgroundColor = "white"; 272 | } 273 | continue; 274 | } 275 | } else { 276 | if (settings.useDebugMode) { 277 | div.style.display = "block"; 278 | div.style.backgroundColor = debugRed; 279 | } else { 280 | div.style.display = "none"; 281 | div.style.backgroundColor = "white"; 282 | } 283 | continue; 284 | } 285 | } 286 | 287 | const shortText = div.getElementsByClassName("a-color-base a-text-normal") as HTMLCollectionOf; 288 | if (shortText.length === 0) { 289 | continue; 290 | } 291 | await descriptionSearch(settings, div); 292 | } 293 | 294 | if (settings.filterRefiner) { 295 | runFilterRefiner(settings); 296 | } 297 | }; 298 | 299 | const resetBrandsRefiner = () => { 300 | const divs = [ 301 | ...(document.getElementById("brandsRefinements")?.getElementsByClassName("a-spacing-micro") ?? []), 302 | ] as HTMLDivElement[]; 303 | divs.forEach((div) => { 304 | div.style.backgroundColor = "white"; 305 | div 306 | .getElementsByClassName("a-size-base a-color-base")[0] 307 | ?.setAttribute("style", "display: block; color: black !important;"); 308 | div.style.display = "block"; 309 | }); 310 | }; 311 | 312 | const resetBrandsSearchResults = () => { 313 | const divs = [...getItemDivs()] as HTMLDivElement[]; 314 | divs.forEach((div) => { 315 | removeDebugLabel(div); 316 | div.style.backgroundColor = "white"; 317 | div.style.display = "block"; 318 | }); 319 | }; 320 | 321 | /** 322 | * resets the brands filter to the default Amazon settings (colors and display) 323 | */ 324 | const resetBrands = () => { 325 | resetBrandsRefiner(); 326 | resetBrandsSearchResults(); 327 | }; 328 | 329 | const listenForMessages = () => { 330 | getEngineApi().runtime.onMessage.addListener(async (message: PopupMessage) => { 331 | console.log({ type: message.type, isChecked: message.isChecked }); 332 | const settings = await getStorageValue(); 333 | switch (message.type) { 334 | case "enabled": 335 | if (message.isChecked) { 336 | filterBrands(settings); 337 | } else { 338 | resetBrands(); 339 | // previously hidden elements should be shown 340 | unHideDivs(); 341 | } 342 | break; 343 | case "refinerBypass": 344 | if (message.isChecked) { 345 | resetBrands(); 346 | // previously hidden elements should be shown 347 | unHideDivs(); 348 | } else { 349 | filterBrands(settings); 350 | } 351 | break; 352 | case "useDebugMode": 353 | filterBrands(settings); 354 | break; 355 | case "filterRefiner": 356 | resetBrandsRefiner(); 357 | runFilterRefiner(settings); 358 | break; 359 | case "refinerMode": 360 | runFilterRefiner(settings); 361 | break; 362 | case "filterWithRefiner": 363 | runFilterRefiner(settings); 364 | break; 365 | case "usePersonalBlock": 366 | case "personalBlockMap": 367 | filterBrands(settings); 368 | break; 369 | case "deptFilter": 370 | filterBrands(settings); 371 | break; 372 | default: 373 | break; 374 | } 375 | }); 376 | }; 377 | 378 | const runFilter = async () => { 379 | const settings = await getStorageValue(); 380 | if (!settings.enabled) { 381 | return; 382 | } 383 | 384 | const timerStart = performance.now(); 385 | filterBrands(settings); 386 | const timerEnd = performance.now(); 387 | setStorageValue({ lastMapRun: timerEnd - timerStart }); 388 | }; 389 | 390 | const startObserver = async () => { 391 | const settings = await getStorageValue(); 392 | console.log("AmazonBrandFilter: Starting observer!"); 393 | const observer = new MutationObserver(async (mutations) => { 394 | // check if the mutation is invalid 395 | let mutationInvalid = false; 396 | for (const mutation of mutations) { 397 | if (mutation.type !== "childList") { 398 | continue; 399 | } 400 | mutationInvalid = Array.from(mutation.addedNodes).some( 401 | (node) => 402 | // check if the node is a carousel card class (these are the revolving ads) 403 | (node as HTMLElement).classList?.contains("a-carousel-card") || 404 | // check if the node contains the text "ends in" (lowercase) 405 | (node.nodeType === 3 && (node as Text).textContent?.toLowerCase().includes("ends in")) 406 | ); 407 | } 408 | 409 | if (mutationInvalid) { 410 | return; 411 | } 412 | if (settings.useDebugMode) { 413 | console.debug("AmazonBrandFilter: Mutation detected!"); 414 | } 415 | runFilter(); 416 | }); 417 | observer.observe(document, { 418 | subtree: true, 419 | childList: true, 420 | }); 421 | }; 422 | 423 | (async () => { 424 | updateSeenBrands(); 425 | unHideDivs(); 426 | await ensureSettingsExist(); 427 | runFilter(); 428 | listenForMessages(); 429 | startObserver(); 430 | 431 | console.log("AmazonBrandFilter: %cContent script loaded!", "color: lightgreen"); 432 | })(); 433 | -------------------------------------------------------------------------------- /src/gui/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ensureSettingsExist, 3 | getEngineApi, 4 | getManifest, 5 | getMessage, 6 | getSettings, 7 | getStorageValue, 8 | setIcon, 9 | setStorageValue, 10 | } from "utils/browser-helpers"; 11 | import { getSanitizedUserInput } from "utils/helpers"; 12 | import { PopupMessage, GuiLocation } from "utils/types"; 13 | 14 | var guiLocation: GuiLocation = "popup"; 15 | if (location.pathname === "/dashboard.html") { 16 | guiLocation = "dashboard"; 17 | } 18 | 19 | // checkboxes 20 | const abfEnabled = document.getElementById("abf-enabled")! as HTMLInputElement; 21 | const abfFilterRefiner = document.getElementById("abf-filter-refiner")! as HTMLInputElement; 22 | const abfFilterRefinerHide = document.getElementById("abf-filter-refiner-hide")! as HTMLInputElement; 23 | const abfFilterRefinerGrey = document.getElementById("abf-filter-refiner-grey")! as HTMLInputElement; 24 | const abfAllowRefineBypass = document.getElementById("abf-allow-refine-bypass")! as HTMLInputElement; 25 | const abfDebugMode = document.getElementById("abf-debug-mode")! as HTMLInputElement; 26 | const abfPersonalBlockEnabled = document.getElementById("abf-personal-block-enabled")! as HTMLInputElement; 27 | const abfPersonalBlockTextBox = document.getElementById("abf-personal-block-textbox")! as HTMLTextAreaElement; 28 | const abfPersonalBlockButton = document.getElementById("abf-personal-block-button")! as HTMLButtonElement; 29 | const abfVersion = document.getElementById("abf-version")! as HTMLSpanElement; 30 | const abfPersonalBlockSavedConfirm = document.getElementById("abf-personal-block-saved-confirm")! as HTMLSpanElement; 31 | const abfSearchDepthSavedConfirm = document.getElementById("abf-search-depth-saved-confirm")! as HTMLSpanElement; 32 | const abfFilterWithRefiner = document.getElementById("abf-use-filter-with-refiner")! as HTMLInputElement; 33 | 34 | // numbers 35 | const versionNumber = document.getElementById("version-number")! as HTMLSpanElement; 36 | const brandCount = document.getElementById("brand-count")! as HTMLSpanElement; 37 | const seenBrandCount = document.getElementById("seen-brand-count")! as HTMLSpanElement; 38 | const lastRun = document.getElementById("last-run")! as HTMLSpanElement; 39 | const abfSearchDepth = document.getElementById("abf-search-depth")! as HTMLInputElement; 40 | // buttons 41 | 42 | const knownBrandViewControlButton = document.getElementById("abf-known-brand-view-control")! as HTMLButtonElement; 43 | const seenBrandViewControlButton = document.getElementById("abf-seen-brand-view-control")! as HTMLButtonElement; 44 | const abfSaveSearchDepthButton = document.getElementById("abf-save-search-depth-button")! as HTMLButtonElement; 45 | // text 46 | const abfKnownBrandsListDiv = document.getElementById("abf-dashboard-known-brands")! as HTMLTextAreaElement; 47 | const abfSeenBrandsListDiv = document.getElementById("abf-dashboard-seen-brands")! as HTMLTextAreaElement; 48 | const abfSearchDepthText = document.getElementById("abf-search-depth-text")! as HTMLTextAreaElement; 49 | 50 | const abfEnabledText = document.getElementById("abf-enabled-text")! as HTMLInputElement; 51 | const abfFilterRefinerText = document.getElementById("abf-filter-refiner-text")! as HTMLInputElement; 52 | const abfFilterRefinerHideText = document.getElementById("abf-filter-refiner-hide-text")! as HTMLInputElement; 53 | const abfFilterRefinerGreyText = document.getElementById("abf-filter-refiner-grey-text")! as HTMLInputElement; 54 | const abfAllowRefineBypassText = document.getElementById("abf-allow-refine-bypass-text")! as HTMLInputElement; 55 | const abfDebugModeText = document.getElementById("abf-debug-mode-text")! as HTMLInputElement; 56 | const abfPersonalBlockEnabledText = document.getElementById("abf-personal-block-enabled-text")! as HTMLInputElement; 57 | 58 | const abfPersonalBlockText = document.getElementById("abf-personal-block-saved-confirm")! as HTMLSpanElement; 59 | const brandListVersionText = document.getElementById("brand-version-text")! as HTMLSpanElement; 60 | const brandCountText = document.getElementById("brand-count-text")! as HTMLSpanElement; 61 | const feedbackText = document.getElementById("popup-feedback-text")! as HTMLSpanElement; 62 | const missingBrandText = document.getElementById("popup-missing-brand-text")! as HTMLSpanElement; 63 | const lastRunText = document.getElementById("last-run")! as HTMLSpanElement; 64 | const helptranslate = document.getElementById("popup-help-translate")! as HTMLSpanElement; 65 | const dashboard = document.getElementById("popup-dashboard")! as HTMLSpanElement; 66 | 67 | const abfFilterWithRefinerText = document.getElementById("abf-use-filter-with-refiner-text")! as HTMLInputElement; 68 | const abfExperimentalFeatures = document.getElementById("abf-experimental-features")! as HTMLSpanElement; 69 | const dashboardNotice = document.getElementById("dashboard-notice")! as HTMLInputElement; 70 | const dashboardKnownBrandListText = document.getElementById("known-brands-list-text")! as HTMLInputElement; 71 | const dashboardSeenBrandListText = document.getElementById("seen-brands-list-text")! as HTMLInputElement; 72 | const setText = async (locationPath: GuiLocation) => { 73 | const { settings, syncSettings } = await getSettings(); 74 | // these have to be snake_case because chrome doesnt support hyphens in i18n 75 | abfEnabledText.innerText = await getMessage("popup_enabled"); 76 | abfEnabledText.title = await getMessage("popup_enabled_tooltip"); 77 | abfFilterRefinerText.innerText = await getMessage("popup_filter_sidebar"); 78 | abfFilterRefinerText.title = await getMessage("popup_filter_sidebar_tooltip"); 79 | abfFilterRefinerHideText.innerText = await getMessage("popup_sidebar_hide"); 80 | abfFilterRefinerHideText.title = await getMessage("popup_sidebar_hide_tooltip"); 81 | abfFilterRefinerGreyText.innerText = await getMessage("popup_sidebar_grey"); 82 | abfFilterRefinerGreyText.title = await getMessage("popup_sidebar_grey_tooltip"); 83 | abfAllowRefineBypassText.innerText = await getMessage("popup_allow_refine_bypass"); 84 | abfAllowRefineBypassText.title = await getMessage("popup_allow_refine_bypass_tooltip"); 85 | 86 | abfDebugModeText.innerText = await getMessage("popup_debug"); 87 | abfDebugModeText.title = await getMessage("popup_debug_tooltip"); 88 | abfPersonalBlockEnabledText.innerText = await getMessage("popup_personal_blocklist"); 89 | abfPersonalBlockEnabledText.title = await getMessage("popup_personal_blocklist_tooltip"); 90 | abfPersonalBlockButton.value = await getMessage("save_button"); 91 | abfPersonalBlockText.innerText = await getMessage("save_confirm"); 92 | 93 | brandListVersionText.innerText = await getMessage("brand_list_version"); 94 | 95 | feedbackText.innerText = await getMessage("popup_feedback_link"); 96 | missingBrandText.innerText = await getMessage("popup_missing_brand"); 97 | lastRunText.innerText = await getMessage("popup_last_run"); 98 | helptranslate.innerText = await getMessage("popup_help_translate"); 99 | 100 | if (locationPath === "dashboard") { 101 | abfFilterWithRefinerText.innerText = await getMessage("use_filter_with_refiner"); 102 | abfFilterWithRefinerText.title = await getMessage("use_filter_with_refiner_tooltip"); 103 | abfExperimentalFeatures.innerText = await getMessage("experimental_features"); 104 | abfExperimentalFeatures.title = await getMessage("experimental_features_tooltip"); 105 | dashboardKnownBrandListText.innerText = await getMessage("known_brands_list_text"); 106 | abfSearchDepthText.innerText = await getMessage("search_depth"); 107 | abfSearchDepthText.title = await getMessage("search_depth_tooltip"); 108 | dashboardSeenBrandListText.innerText = await getMessage("seen_brands_list_text"); 109 | seenBrandCount.innerText = settings.seenBrandCount?.toString() ?? ""; 110 | abfSearchDepth.value = syncSettings.searchDepth.toString(); 111 | abfSearchDepthSavedConfirm.innerText = await getMessage("save_confirm"); 112 | abfSearchDepthSavedConfirm.style.display = "none"; 113 | 114 | if (syncSettings.showKnownBrands === null) { 115 | if (settings.showKnownBrands) { 116 | knownBrandViewControlButton.value = await getMessage("hide_all"); 117 | abfKnownBrandsListDiv.style.display = "block"; 118 | } else { 119 | knownBrandViewControlButton.value = await getMessage("show_all"); 120 | abfKnownBrandsListDiv.style.display = "none"; 121 | } 122 | } else { 123 | if (syncSettings.showKnownBrands) { 124 | knownBrandViewControlButton.value = await getMessage("hide_all"); 125 | abfKnownBrandsListDiv.style.display = "block"; 126 | } else { 127 | knownBrandViewControlButton.value = await getMessage("show_all"); 128 | abfKnownBrandsListDiv.style.display = "none"; 129 | } 130 | } 131 | 132 | if (syncSettings.showSeenBrands === null) { 133 | if (settings.showSeenBrands) { 134 | seenBrandViewControlButton.value = await getMessage("hide_all"); 135 | abfSeenBrandsListDiv.style.display = "block"; 136 | } else { 137 | abfSeenBrandsListDiv.style.display = "none"; 138 | } 139 | } else { 140 | if (syncSettings.showSeenBrands) { 141 | seenBrandViewControlButton.value = await getMessage("hide_all"); 142 | abfSeenBrandsListDiv.style.display = "block"; 143 | } else { 144 | seenBrandViewControlButton.value = await getMessage("show_all"); 145 | abfSeenBrandsListDiv.style.display = "none"; 146 | } 147 | } 148 | } else { 149 | brandCountText.innerText = await getMessage("brand_list_count"); 150 | dashboard.innerText = await getMessage("popup_dashboard"); 151 | dashboardNotice.innerText = await getMessage("dashboard_notice"); 152 | } 153 | }; 154 | 155 | const setCheckBoxStates = async (locationPath: GuiLocation) => { 156 | const { settings, syncSettings } = await getSettings(); 157 | 158 | if (syncSettings.enabled) { 159 | abfEnabled.checked = true; 160 | } else { 161 | abfEnabled.checked = false; 162 | } 163 | 164 | if (syncSettings.filterRefiner) { 165 | abfFilterRefiner.checked = true; 166 | } else { 167 | abfFilterRefiner.checked = false; 168 | } 169 | 170 | if (syncSettings.refinerBypass) { 171 | abfAllowRefineBypass.checked = true; 172 | } else { 173 | abfAllowRefineBypass.checked = false; 174 | } 175 | 176 | if (syncSettings.refinerMode === "grey") { 177 | abfFilterRefinerGrey.checked = true; 178 | abfFilterRefinerHide.checked = false; 179 | } else { 180 | abfFilterRefinerHide.checked = true; 181 | abfFilterRefinerGrey.checked = false; 182 | } 183 | if (locationPath === "dashboard") { 184 | if (syncSettings.filterWithRefiner) { 185 | abfFilterWithRefiner.checked = true; 186 | } else { 187 | abfFilterWithRefiner.checked = false; 188 | } 189 | } 190 | 191 | versionNumber.innerText = settings.brandsVersion?.toString() ?? ""; 192 | brandCount.innerText = settings.brandsCount?.toString() ?? ""; 193 | 194 | if (syncSettings.lastMapRun) { 195 | lastRun.innerText = `${syncSettings.lastMapRun}ms`; 196 | } else { 197 | lastRun.innerText = "N/A"; 198 | } 199 | 200 | if (syncSettings.useDebugMode) { 201 | abfDebugMode.checked = true; 202 | } 203 | 204 | setIcon(); 205 | }; 206 | 207 | const setAddonVersion = () => { 208 | const manifest = getManifest(); 209 | abfVersion.innerText = `v${manifest.version}`; 210 | }; 211 | 212 | const setTextBoxStates = async () => { 213 | const { syncSettings } = await getSettings(); 214 | 215 | if (syncSettings.usePersonalBlock === true) { 216 | abfPersonalBlockEnabled.checked = true; 217 | abfPersonalBlockTextBox.style.display = "block"; 218 | abfPersonalBlockButton.style.display = "block"; 219 | } 220 | }; 221 | 222 | const enableDisable = async (_event: Event) => { 223 | await setStorageValue({ enabled: abfEnabled.checked }, "sync"); 224 | await setStorageValue({ enabled: abfEnabled.checked }); 225 | await setIcon(); 226 | sendMessageToContentScriptPostClick({ type: "enabled", isChecked: abfEnabled.checked }); 227 | }; 228 | 229 | const setFilterRefiner = async (_event: Event) => { 230 | await setStorageValue({ filterRefiner: abfFilterRefiner.checked }, "sync"); 231 | await setStorageValue({ filterRefiner: abfFilterRefiner.checked }); 232 | sendMessageToContentScriptPostClick({ type: "filterRefiner", isChecked: abfFilterRefiner.checked }); 233 | }; 234 | 235 | const setRefinerHide = async (_event: Event) => { 236 | abfFilterRefinerGrey.checked = !abfFilterRefinerHide.checked; 237 | await setStorageValue({ refinerMode: abfFilterRefinerHide.checked ? "hide" : "grey" }, "sync"); 238 | await setStorageValue({ refinerMode: abfFilterRefinerHide.checked ? "hide" : "grey" }); 239 | sendMessageToContentScriptPostClick({ type: "refinerMode", isChecked: abfFilterRefinerHide.checked }); 240 | }; 241 | 242 | const setRefinerGrey = async (_event: Event) => { 243 | abfFilterRefinerHide.checked = !abfFilterRefinerGrey.checked; 244 | await setStorageValue({ refinerMode: abfFilterRefinerGrey.checked ? "grey" : "hide" }, "sync"); 245 | await setStorageValue({ refinerMode: abfFilterRefinerGrey.checked ? "grey" : "hide" }); 246 | sendMessageToContentScriptPostClick({ type: "refinerMode", isChecked: abfFilterRefinerGrey.checked }); 247 | }; 248 | 249 | const setRefinerBypass = async (_event: Event) => { 250 | await setStorageValue({ refinerBypass: abfAllowRefineBypass.checked }, "sync"); 251 | await setStorageValue({ refinerBypass: abfAllowRefineBypass.checked }); 252 | sendMessageToContentScriptPostClick({ type: "refinerBypass", isChecked: abfAllowRefineBypass.checked }); 253 | }; 254 | const setFilterWithRefiner = async (_event: Event) => { 255 | await setStorageValue({ filterWithRefiner: abfFilterWithRefiner.checked }, "sync"); 256 | await setStorageValue({ filterWithRefiner: abfFilterWithRefiner.checked }); 257 | sendMessageToContentScriptPostClick({ type: "filterWithRefiner", isChecked: abfFilterWithRefiner.checked }); 258 | }; 259 | 260 | const setDebugMode = async (_event: Event) => { 261 | await setStorageValue({ useDebugMode: abfDebugMode.checked }, "sync"); 262 | await setStorageValue({ useDebugMode: abfDebugMode.checked }); 263 | sendMessageToContentScriptPostClick({ type: "useDebugMode", isChecked: abfDebugMode.checked }); 264 | }; 265 | 266 | const setPersonalBlockEnabled = async (_event: Event) => { 267 | abfPersonalBlockTextBox.style.display = abfPersonalBlockEnabled.checked ? "block" : "none"; 268 | abfPersonalBlockButton.style.display = abfPersonalBlockEnabled.checked ? "block" : "none"; 269 | await setStorageValue({ usePersonalBlock: abfPersonalBlockEnabled.checked }, "sync"); 270 | await setStorageValue({ usePersonalBlock: abfPersonalBlockEnabled.checked }); 271 | sendMessageToContentScriptPostClick({ type: "usePersonalBlock", isChecked: abfPersonalBlockEnabled.checked }); 272 | }; 273 | 274 | const savePersonalBlock = async () => { 275 | const userInput = getSanitizedUserInput(abfPersonalBlockTextBox.value); 276 | const personalBlockMap: Record = {}; 277 | for (const brand of userInput) { 278 | personalBlockMap[brand] = true; 279 | } 280 | await setStorageValue({ personalBlockMap }, "sync"); 281 | await setStorageValue({ personalBlockMap }); 282 | abfPersonalBlockSavedConfirm.style.display = "block"; 283 | // use the same isChecked value as the personalBlockEnabled checkbox 284 | sendMessageToContentScriptPostClick({ type: "personalBlockMap", isChecked: abfPersonalBlockEnabled.checked }); 285 | }; 286 | 287 | const setPersonalList = async () => { 288 | let result = await getStorageValue("personalBlockMap", "sync"); 289 | if (Object.keys(result).length === 0) { 290 | result = await getStorageValue("personalBlockMap"); 291 | } 292 | 293 | const personalBlockMap = result.personalBlockMap; 294 | if (!personalBlockMap) { 295 | return; 296 | } 297 | 298 | const textValue = Object.keys(personalBlockMap); 299 | let textHeight = Object.keys(personalBlockMap).length; 300 | if (textHeight > 10) { 301 | textHeight = 10; 302 | abfPersonalBlockTextBox.style.overflow = "scroll"; 303 | } 304 | 305 | abfPersonalBlockTextBox.value = textValue.join("\n"); 306 | abfPersonalBlockTextBox.rows = textHeight; 307 | }; 308 | 309 | const saveSearchDepth = async (_event: Event) => { 310 | const inputValue = abfSearchDepth.value; 311 | 312 | const newDepth = Number(inputValue); 313 | 314 | if (Number.isInteger(newDepth) && newDepth > -1) { 315 | await setStorageValue({ searchDepth: newDepth }, "sync"); 316 | await setStorageValue({ searchDepth: newDepth }); 317 | abfSearchDepthSavedConfirm.style.display = "block"; 318 | // use the same isChecked value as the personalBlockEnabled checkbox 319 | sendMessageToContentScriptPostClick({ type: "personalBlockMap", isChecked: abfPersonalBlockEnabled.checked }); 320 | } 321 | }; 322 | 323 | const showKnownBrands = async (_event: Event) => { 324 | if (abfKnownBrandsListDiv.style.display === "none") { 325 | abfKnownBrandsListDiv.style.display = "block"; 326 | knownBrandViewControlButton.value = await getMessage("hide_all"); 327 | setStorageValue({ showKnownBrands: true }, "local"); 328 | setStorageValue({ showKnownBrands: true }, "sync"); 329 | } else { 330 | abfKnownBrandsListDiv.style.display = "none"; 331 | knownBrandViewControlButton.value = await getMessage("show_all"); 332 | setStorageValue({ showKnownBrands: false }, "local"); 333 | setStorageValue({ showKnownBrands: false }, "sync"); 334 | } 335 | }; 336 | 337 | const showSeenBrands = async (_event: Event) => { 338 | if (abfSeenBrandsListDiv.style.display === "none") { 339 | abfSeenBrandsListDiv.style.display = "block"; 340 | seenBrandViewControlButton.value = await getMessage("hide_all"); 341 | setStorageValue({ showSeenBrands: true }, "local"); 342 | setStorageValue({ showSeenBrands: true }, "sync"); 343 | } else { 344 | abfSeenBrandsListDiv.style.display = "none"; 345 | seenBrandViewControlButton.value = await getMessage("show_all"); 346 | setStorageValue({ showSeenBrands: false }, "local"); 347 | setStorageValue({ showSeenBrands: false }, "sync"); 348 | } 349 | }; 350 | 351 | // i think i want to create a unified function here to display the different lists with checkboxes but i want to know all the actions i want to perform before i do that 352 | const createKnownBrandList = async () => { 353 | console.log("AmazonBrandFilter: %cCreateKnownBrandList", "color: yellow"); 354 | let result = await getStorageValue("brandsMap"); 355 | 356 | if (Object.keys(result.brandsMap).length === 0) { 357 | console.log("createKnownBrandList: no knownDepts found in sync storage"); 358 | return; 359 | } 360 | if (!result.brandsMap) { 361 | console.log("createKnownBrandList: brandMap is empty"); 362 | return; 363 | } 364 | console.debug(`createKnownBrandList: ${Object.keys(result.brandsMap).length} brands found in brandMap storage`); 365 | const textValue = Object.keys(result.brandsMap).sort(); 366 | 367 | for (const key of textValue) { 368 | const brandDiv = document.createElement("div"); 369 | brandDiv.innerText = key; 370 | // const deptEntryLabel = document.createElement("label"); 371 | // deptEntryLabel.htmlFor = deptCheckbox.id; 372 | // deptEntryLabel.innerText = key; 373 | abfKnownBrandsListDiv.appendChild(brandDiv); 374 | } 375 | }; 376 | 377 | const createSeenBrandList = async () => { 378 | console.log("AmazonBrandFilter: %cCreateSeenBrandList", "color: yellow"); 379 | let result = await getStorageValue("seenBrands"); 380 | 381 | if (Object.keys(result.seenBrands).length === 0) { 382 | console.log("createSeenBrandList: no seenBrands found in sync storage"); 383 | return; 384 | } 385 | if (!result.seenBrands) { 386 | console.log("createSeenBrandList: seenBrands is empty"); 387 | return; 388 | } 389 | console.debug(`createSeenBrandList: ${Object.keys(result.seenBrands).length} brands found in seenBrands storage`); 390 | const textValue = Object.keys(result.seenBrands).sort(); 391 | 392 | for (const key of textValue) { 393 | const brandDiv = document.createElement("div"); 394 | brandDiv.innerText = key; 395 | abfSeenBrandsListDiv.appendChild(brandDiv); 396 | } 397 | }; 398 | 399 | const sendMessageToContentScriptPostClick = (message: PopupMessage) => { 400 | getEngineApi().tabs.query({ active: true, currentWindow: true }, (tabs) => { 401 | const activeTab = tabs[0]; 402 | if (!activeTab || !activeTab.id || !activeTab.url?.includes(".amazon.")) { 403 | return; 404 | } 405 | getEngineApi().tabs.sendMessage(activeTab.id, message); 406 | }); 407 | }; 408 | 409 | abfEnabled.addEventListener("click", enableDisable); 410 | abfFilterRefiner.addEventListener("click", setFilterRefiner); 411 | abfFilterRefinerHide.addEventListener("click", setRefinerHide); 412 | abfFilterRefinerGrey.addEventListener("click", setRefinerGrey); 413 | abfAllowRefineBypass.addEventListener("click", setRefinerBypass); 414 | abfDebugMode.addEventListener("click", setDebugMode); 415 | abfPersonalBlockEnabled.addEventListener("click", setPersonalBlockEnabled); 416 | abfPersonalBlockButton.addEventListener("click", savePersonalBlock); 417 | 418 | // these are only on the dashboard 419 | if (guiLocation === "dashboard") { 420 | abfFilterWithRefiner.addEventListener("click", setFilterWithRefiner); 421 | knownBrandViewControlButton.addEventListener("click", showKnownBrands); 422 | seenBrandViewControlButton.addEventListener("click", showSeenBrands); 423 | createKnownBrandList(); 424 | createSeenBrandList(); 425 | abfSaveSearchDepthButton.addEventListener("click", saveSearchDepth); 426 | } 427 | // abfHideAll.addEventListener("click", hideAll) 428 | 429 | (async () => { 430 | await ensureSettingsExist(); 431 | setText(guiLocation); 432 | setAddonVersion(); 433 | setCheckBoxStates(guiLocation); 434 | setTextBoxStates(); 435 | setPersonalList(); 436 | 437 | console.log("AmazonBrandFilter: %cgui script loaded!", "color: lightgreen"); 438 | })(); 439 | --------------------------------------------------------------------------------