Repository: darkreader/darkreader
Pull request #3131 (2020-07-16 20:17:36)

Myshor wanted this
For any info contact @Myshor 

Myshor: i won't review the code... but functionality works ;)

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 2502f633..1cb29cfd 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,6 +5,10 @@ module.exports = {
     rules: {
         'array-bracket-spacing': ['error', 'never'],
         'block-spacing': ['error', 'always'],
+        'comma-spacing': ['error', {
+            before: false,
+            after: true,
+        }],
         'indent': ['error', 4, {
             'SwitchCase': 1,
         }],
diff --git a/package-lock.json b/package-lock.json
index 228faaf6..442cb1fe 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -901,9 +901,9 @@
       }
     },
     "@types/chrome": {
-      "version": "0.0.120",
-      "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.120.tgz",
-      "integrity": "sha512-dFA5nxeccCWqPUr5y5mSKiwxIwHkySx6IAnJ2wBbnmsVvtKHuiAHpS0Cq8hyv/4aUR0KdhPD0Pp0o99E39rzFA==",
+      "version": "0.0.121",
+      "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.121.tgz",
+      "integrity": "sha512-kt1gZrUnzCARcLIl9rb2+mW15YsijPgNlyAbumB1uZ5XakohAjh8+bD0kfGl462E154Hdn1p2WdKEGizOJ4VgA==",
       "dev": true,
       "requires": {
         "@types/filesystem": "*",
@@ -984,9 +984,9 @@
       }
     },
     "@types/jest": {
-      "version": "26.0.4",
-      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.4.tgz",
-      "integrity": "sha512-4fQNItvelbNA9+sFgU+fhJo8ZFF+AS4Egk3GWwCW2jFtViukXbnztccafAdLhzE/0EiCogljtQQXP8aQ9J7sFg==",
+      "version": "26.0.5",
+      "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.5.tgz",
+      "integrity": "sha512-heU+7w8snfwfjtcj2H458aTx3m5unIToOJhx75ebHilBiiQ39OIdA18WkG4LP08YKeAoWAGvWg8s+22w/PeJ6w==",
       "dev": true,
       "requires": {
         "jest-diff": "^25.2.1",
@@ -1476,9 +1476,9 @@
       }
     },
     "binary-extensions": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
-      "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
+      "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
       "dev": true
     },
     "brace-expansion": {
@@ -1603,9 +1603,9 @@
       "dev": true
     },
     "chokidar": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.0.tgz",
-      "integrity": "sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ==",
+      "version": "3.4.1",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.1.tgz",
+      "integrity": "sha512-TQTJyr2stihpC4Sya9hs2Xh+O2wf+igjL36Y75xx2WdHuiICcn/XJza46Jwt0eT5hVpQOzo3FpY3cj3RVYLX0g==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.1",
@@ -2056,9 +2056,9 @@
       }
     },
     "eslint": {
-      "version": "7.4.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.4.0.tgz",
-      "integrity": "sha512-gU+lxhlPHu45H3JkEGgYhWhkR9wLHHEXC9FbWFnTlEkbKyZKWgWRLgf61E8zWmBuI6g5xKBph9ltg3NtZMVF8g==",
+      "version": "7.5.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.5.0.tgz",
+      "integrity": "sha512-vlUP10xse9sWt9SGRtcr1LAC67BENcQMFeV+w5EvLEoFe3xJ8cF1Skd0msziRx/VMC+72B4DxreCE+OR12OA6Q==",
       "dev": true,
       "requires": {
         "@babel/code-frame": "^7.0.0",
@@ -2069,9 +2069,9 @@
         "doctrine": "^3.0.0",
         "enquirer": "^2.3.5",
         "eslint-scope": "^5.1.0",
-        "eslint-utils": "^2.0.0",
-        "eslint-visitor-keys": "^1.2.0",
-        "espree": "^7.1.0",
+        "eslint-utils": "^2.1.0",
+        "eslint-visitor-keys": "^1.3.0",
+        "espree": "^7.2.0",
         "esquery": "^1.2.0",
         "esutils": "^2.0.2",
         "file-entry-cache": "^5.0.1",
@@ -2085,7 +2085,7 @@
         "js-yaml": "^3.13.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
-        "lodash": "^4.17.14",
+        "lodash": "^4.17.19",
         "minimatch": "^3.0.4",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.1",
@@ -2131,14 +2131,14 @@
       "dev": true
     },
     "espree": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-7.1.0.tgz",
-      "integrity": "sha512-dcorZSyfmm4WTuTnE5Y7MEN1DyoPYy1ZR783QW1FJoenn7RailyWFsq/UL6ZAAA7uXurN9FIpYyUs3OfiIW+Qw==",
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz",
+      "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==",
       "dev": true,
       "requires": {
-        "acorn": "^7.2.0",
+        "acorn": "^7.3.1",
         "acorn-jsx": "^5.2.0",
-        "eslint-visitor-keys": "^1.2.0"
+        "eslint-visitor-keys": "^1.3.0"
       }
     },
     "esprima": {
@@ -4246,9 +4246,9 @@
       "dev": true
     },
     "less": {
-      "version": "3.12.0",
-      "resolved": "https://registry.npmjs.org/less/-/less-3.12.0.tgz",
-      "integrity": "sha512-3mmSHFRP9hGxxQgAKgChfau1LO3ksV/zyZf1qd2ENyBV778NA9Ids99wFRA20jE+5prT7oScKod8PoGlxSe1gg==",
+      "version": "3.12.2",
+      "resolved": "https://registry.npmjs.org/less/-/less-3.12.2.tgz",
+      "integrity": "sha512-+1V2PCMFkL+OIj2/HrtrvZw0BC0sYLMICJfbQjuj/K8CEnlrFX6R5cKKgzzttsZDHyxQNL1jqMREjKN3ja/E3Q==",
       "dev": true,
       "requires": {
         "errno": "^0.1.1",
@@ -4372,9 +4372,9 @@
       }
     },
     "malevic": {
-      "version": "0.18.5",
-      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.5.tgz",
-      "integrity": "sha512-7fzRV8qSlYQ04hw7c77A/JTCHdqO6dbI9PcgL780C9Toad4sRr/aAEGfhjA2VmW+iOmFiKxcYBtDjx6ShbodOQ==",
+      "version": "0.18.6",
+      "resolved": "https://registry.npmjs.org/malevic/-/malevic-0.18.6.tgz",
+      "integrity": "sha512-Um4XRYJpVDhKjRRteiuHdDmcNbI5gX7URsXC6G+5Tk0Dai2W2RB39kg5C/M32IezNPudT+YsgApBh8JG6fIWrA==",
       "dev": true
     },
     "map-cache": {
@@ -4479,10 +4479,13 @@
       }
     },
     "mkdirp": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
-      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
-      "dev": true
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
     },
     "ms": {
       "version": "2.1.2",
@@ -4510,9 +4513,9 @@
       }
     },
     "native-request": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.5.tgz",
-      "integrity": "sha512-7wU3DvBGAJQxWuMR3F62zrhB7hxNj2DdlC/eBVrCgavc6+ZpFZOqS/PsR7QyUPLMkFk0GvvzoeeOAZGLLnObnA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/native-request/-/native-request-1.0.7.tgz",
+      "integrity": "sha512-9nRjinI9bmz+S7dgNtf4A70+/vPhnd+2krGpy4SUlADuOuSa24IDkNaZ+R/QT1wQ6S8jBdi6wE7fLekFZNfUpQ==",
       "dev": true,
       "optional": true
     },
@@ -5141,9 +5144,9 @@
       }
     },
     "rollup": {
-      "version": "2.21.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.21.0.tgz",
-      "integrity": "sha512-BEGgy+wSzux7Ycq58pRiWEOBZaXRXTuvzl1gsm7gqmsAHxkWf9nyA5V2LN9fGSHhhDQd0/C13iRzSh4bbIpWZQ==",
+      "version": "2.22.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.22.1.tgz",
+      "integrity": "sha512-K9AUQUCJkVqC+A7uUDacfhmpEeAjc2uOmSpvGI5xaYsm8pXgy4ZWEM8wHPfKj11xvCwFZppkRDo8a0RESJXCnw==",
       "dev": true,
       "requires": {
         "fsevents": "~2.1.2"
@@ -5838,9 +5841,9 @@
       "dev": true
     },
     "strip-json-comments": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.0.tgz",
-      "integrity": "sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
       "dev": true
     },
     "supports-color": {
@@ -6027,9 +6030,9 @@
       }
     },
     "ts-jest": {
-      "version": "26.1.2",
-      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.2.tgz",
-      "integrity": "sha512-V4SyBDO9gOdEh+AF4KtXJeP+EeI4PkOrxcA8ptl4o8nCXUVM5Gg/8ngGKneS5BsZaR9DXVQNqj9k+iqGAnpGow==",
+      "version": "26.1.3",
+      "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.1.3.tgz",
+      "integrity": "sha512-beUTSvuqR9SmKQEylewqJdnXWMVGJRFqSz2M8wKJe7GBMmLZ5zw6XXKSJckbHNMxn+zdB3guN2eOucSw2gBMnw==",
       "dev": true,
       "requires": {
         "bs-logger": "0.x",
@@ -6042,6 +6045,14 @@
         "mkdirp": "1.x",
         "semver": "7.x",
         "yargs-parser": "18.x"
+      },
+      "dependencies": {
+        "mkdirp": {
+          "version": "1.0.4",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+          "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+          "dev": true
+        }
       }
     },
     "ts-node": {
@@ -6147,9 +6158,9 @@
       }
     },
     "typescript": {
-      "version": "3.9.6",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.6.tgz",
-      "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==",
+      "version": "3.9.7",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz",
+      "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==",
       "dev": true
     },
     "union-value": {
@@ -6395,17 +6406,6 @@
       "dev": true,
       "requires": {
         "mkdirp": "^0.5.1"
-      },
-      "dependencies": {
-        "mkdirp": {
-          "version": "0.5.5",
-          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
-          "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
-          "dev": true,
-          "requires": {
-            "minimist": "^1.2.5"
-          }
-        }
       }
     },
     "write-file-atomic": {
diff --git a/package.json b/package.json
index f9403337..35ec4f46 100644
--- a/package.json
+++ b/package.json
@@ -38,26 +38,26 @@
     "@rollup/plugin-node-resolve": "8.4.0",
     "@rollup/plugin-replace": "2.3.3",
     "@rollup/pluginutils": "3.1.0",
-    "@types/chrome": "0.0.120",
-    "@types/jest": "26.0.4",
+    "@types/chrome": "0.0.121",
+    "@types/jest": "26.0.5",
     "@types/node": "14.0.23",
     "@typescript-eslint/eslint-plugin": "3.6.1",
     "@typescript-eslint/parser": "3.6.1",
-    "chokidar": "3.4.0",
-    "eslint": "7.4.0",
+    "chokidar": "3.4.1",
+    "eslint": "7.5.0",
     "eslint-plugin-local": "1.0.0",
     "fs-extra": "9.0.1",
     "globby": "11.0.1",
     "jest": "26.1.0",
-    "less": "3.12.0",
-    "malevic": "0.18.5",
+    "less": "3.12.2",
+    "malevic": "0.18.6",
     "prettier": "2.0.5",
-    "rollup": "2.21.0",
+    "rollup": "2.22.1",
     "rollup-plugin-typescript2": "0.27.1",
-    "ts-jest": "26.1.2",
+    "ts-jest": "26.1.3",
     "ts-node": "8.10.2",
     "tsconfig-paths": "3.9.0",
-    "typescript": "3.9.6",
+    "typescript": "3.9.7",
     "ws": "7.3.1",
     "yazl": "2.5.1"
   }
diff --git a/src/background/extension.ts b/src/background/extension.ts
index 7c790a17..c4b33604 100644
--- a/src/background/extension.ts
+++ b/src/background/extension.ts
@@ -372,28 +372,29 @@ export class Extension {
         const urlInfo = this.getURLInfo(url);
         if (this.isEnabled() && isURLEnabled(url, this.user.settings, urlInfo)) {
             const custom = this.user.settings.customThemes.find(({url: urlList}) => isURLInList(url, urlList));
-            const filterConfig = custom ? custom.theme : this.user.settings.theme;
+            const preset = custom ? null : this.user.settings.presets.find(({urls}) => isURLInList(url, urls));
+            const theme = custom ? custom.theme : preset ? preset.theme : this.user.settings.theme;
 
             console.log(`Creating CSS for url: ${url}`);
-            switch (filterConfig.engine) {
+            switch (theme.engine) {
                 case ThemeEngines.cssFilter: {
                     return {
                         type: 'add-css-filter',
-                        data: createCSSFilterStylesheet(filterConfig, url, frameURL, this.config.INVERSION_FIXES),
+                        data: createCSSFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES),
                     };
                 }
                 case ThemeEngines.svgFilter: {
                     if (isFirefox()) {
                         return {
                             type: 'add-css-filter',
-                            data: createSVGFilterStylesheet(filterConfig, url, frameURL, this.config.INVERSION_FIXES),
+                            data: createSVGFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES),
                         };
                     }
                     return {
                         type: 'add-svg-filter',
                         data: {
-                            css: createSVGFilterStylesheet(filterConfig, url, frameURL, this.config.INVERSION_FIXES),
-                            svgMatrix: getSVGFilterMatrixValue(filterConfig),
+                            css: createSVGFilterStylesheet(theme, url, frameURL, this.config.INVERSION_FIXES),
+                            svgMatrix: getSVGFilterMatrixValue(theme),
                             svgReverseMatrix: getSVGReverseFilterMatrixValue(),
                         },
                     };
@@ -401,13 +402,13 @@ export class Extension {
                 case ThemeEngines.staticTheme: {
                     return {
                         type: 'add-static-theme',
-                        data: filterConfig.stylesheet && filterConfig.stylesheet.trim() ?
-                            filterConfig.stylesheet :
-                            createStaticStylesheet(filterConfig, url, frameURL, this.config.STATIC_THEMES),
+                        data: theme.stylesheet && theme.stylesheet.trim() ?
+                            theme.stylesheet :
+                            createStaticStylesheet(theme, url, frameURL, this.config.STATIC_THEMES),
                     };
                 }
                 case ThemeEngines.dynamicTheme: {
-                    const filter = {...filterConfig};
+                    const filter = {...theme};
                     delete filter.engine;
                     const fixes = getDynamicThemeFixesFor(url, frameURL, this.config.DYNAMIC_THEME_FIXES, this.user.settings.enableForPDF);
                     const isIFrame = frameURL != null;
@@ -417,7 +418,7 @@ export class Extension {
                     };
                 }
                 default: {
-                    throw new Error(`Unknown engine ${filterConfig.engine}`);
+                    throw new Error(`Unknown engine ${theme.engine}`);
                 }
             }
         } else {
diff --git a/src/background/tab-manager.ts b/src/background/tab-manager.ts
index 996885d0..036f16e0 100644
--- a/src/background/tab-manager.ts
+++ b/src/background/tab-manager.ts
@@ -79,6 +79,13 @@ export default class TabManager {
                 a.download = name;
                 a.click();
             }
+            if (type === 'request-export-css') {
+                const activeTab = await this.getActiveTab();
+                this.ports
+                    .get(activeTab.id)
+                    .get(0).port
+                    .postMessage({type: 'export-css'});
+            }
         });
     }
 
@@ -111,6 +118,9 @@ export default class TabManager {
     }
 
     async getActiveTabURL() {
+        return (await this.getActiveTab()).url;
+    }
+    async getActiveTab() {
         let tab = (await queryTabs({
             active: true,
             lastFocusedWindow: true
@@ -121,6 +131,6 @@ export default class TabManager {
             const tabs = (await queryTabs({active: true}));
             tab = tabs.find((t) => !isExtensionPage(t.url)) || tab;
         }
-        return tab.url;
+        return tab;
     }
 }
diff --git a/src/background/user-storage.ts b/src/background/user-storage.ts
index e28b5c2c..224ecda2 100644
--- a/src/background/user-storage.ts
+++ b/src/background/user-storage.ts
@@ -43,6 +43,9 @@ export default class UserStorage {
                     }
                     sync.theme = {...DEFAULT_SETTINGS.theme, ...sync.theme};
                     sync.time = {...DEFAULT_SETTINGS.time, ...sync.time};
+                    sync.presets.forEach((preset) => {
+                        preset.theme = {...DEFAULT_SETTINGS.theme, ...preset.theme};
+                    });
                     sync.customThemes.forEach((site) => {
                         site.theme = {...DEFAULT_SETTINGS.theme, ...site.theme};
                     });
diff --git a/src/config/dark-sites.config b/src/config/dark-sites.config
index e1934260..898e808f 100644
--- a/src/config/dark-sites.config
+++ b/src/config/dark-sites.config
@@ -121,6 +121,7 @@ dark.diatr.us
 darkmodelist.com
 darkreader.org
 dcards.snaz.in
+deepweblinks.net
 defanor.uberspace.net
 deltarune.com
 demonforums.net
@@ -260,7 +261,6 @@ hdencode.com
 heapmedia.com
 heavybit.com
 hellofromhe.re
-henrypp.org
 hidive.com
 himovies.to
 hoarding.me
@@ -280,6 +280,7 @@ illicoweb.videotron.com
 imgbox.com
 imgops.com
 imgur.com
+indexmania.org
 indiexpo.net
 infinitezoom.net
 infocon.org
@@ -339,6 +340,7 @@ merklex.io
 metronom.us
 metropool.nl
 miaou.drycat.fr
+minehut.com
 miniroyale2.io
 mixer.com
 mixxx.org
@@ -381,6 +383,7 @@ obsproject.com
 offshorecorptalk.com
 ogusers.com
 ohmyz.sh
+oldergeeks.com
 omgwtfnzbs.me
 onionplay.co
 open.spotify.com
@@ -418,6 +421,7 @@ playcode.io/$
 pleroma.site
 pleroma.social
 plexguide.com
+pluto.tv
 poal.co
 poe-racing.com
 poe.trade
@@ -554,6 +558,7 @@ training.azeria-labs.com
 trellobot.xyz
 tripleaughtdesign.com
 trovo.live
+truckersmp.com
 trustreaming.tv
 ttf.tf
 tube.kdy.ch
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index b64eafae..5b527e5b 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -136,6 +136,14 @@ CSS
 
 ================================
 
+abiturma.de
+
+INVERT
+div.formula
+amp-img.latex-inline
+
+================================
+
 ableton.com
 
 INVERT
@@ -528,6 +536,54 @@ a[href="/product"]
 
 ================================
 
+bitwit.tech
+
+CSS
+.svg-primary {
+    fill: #375D69 !important;
+}
+.svg-primary-light {
+    fill: #4A7F8F !important;
+}
+.svg-primary-dark {
+    fill: #28444D !important;
+}
+.svg-secondary {
+    fill: #B88399 !important;
+}
+.svg-secondary-light {
+    fill: #E8A9C4 !important;
+}
+.svg-secondary-dark {
+    fill: #825F6E !important;
+}
+.svg-light {
+    fill: #ADCED9 !important;
+}
+.svg-outline {
+    fill: none !important;
+    stroke: #000000 !important;
+}
+.svg-primary-outline {
+    fill: none !important;
+    stroke: #ADCED9 !important;
+}
+.svg-secondary-outline {
+    fill: none !important;
+    stroke: #375D69 !important;
+}
+.theme-light {
+    display: none !important;
+}
+.theme-dark {
+    display: none !important;
+}
+.theme-darkreader {
+    display: block !important;
+}
+
+================================
+
 blog.cloudflare.com
 
 CSS
@@ -647,6 +703,16 @@ CSS
 
 ================================
 
+chilkatsoft.com
+
+CSS
+body {
+    background-image: none !important;
+    background-color: var(--darkreader-neutral-background) !important;   
+}
+
+================================
+
 chtoes.li
 
 INVERT
@@ -798,16 +864,8 @@ coolblue.be
 coolblue.nl
 hotorangemedia.nl
 
-CSS
-path[fill="#F60"] {
-    --darkreader-inline-fill: #F60 !important
-}
-path[fill="#FFF"] {
-    --darkreader-inline-fill: #FFF !important
-}
-path[fill="#285DAB"] {
-    --darkreader-inline-fill: #285DAB !important
-}
+IGNORE INLINE STYLE
+.header__logo-image > path
 
 ================================
 
@@ -827,6 +885,16 @@ img[title="Switch"]
 
 ================================
 
+copitosystem.com
+
+CSS
+body {
+    background-image: none !important;
+    background-color: var(--darkreader-neutral-background) !important;
+}
+
+================================
+
 courses.fit.cvut.cz
 
 CSS
@@ -1474,6 +1542,42 @@ img[src*="map"], [style*="map"],
 .sp_58eJ3V9_er3, .sp_tUIA9AtJipl {
     background-image: url(/rsrc.php/v3/yd/r/jUiHkRoni5c.png) !important;
 }
+.sp_gITciRsg_Pe {
+    background-image: url(/rsrc.php/v3/yg/r/Ol5WjEjhlbW.png) !important;
+}
+.sp_0t0rHlmAULF {
+    background-image: url(/rsrc.php/v3/yl/r/JetT-c8Ev5N.png) !important;
+}
+.sp_E2Ht8u3xQ2D {
+    background-image: url(/rsrc.php/v3/yW/r/ydlqmQJZJmy.png) !important;
+}
+.sp_v8yz2528JQj {
+    background-image: url(/rsrc.php/v3/yG/r/phm3a7Fdsm7.png) !important;
+}
+.sp_TCVUlcfh1wT {
+    background-image: url(https://static.xx.fbcdn.net/rsrc.php/v3/yp/r/p6ilSAsuyC-.png) !important;
+}
+.sp_SAXe3VMcdiu {
+    background-image: url(https://static.xx.fbcdn.net/rsrc.php/v3/yp/r/-tZa63I21SG.png) !important;
+}
+.sp_qmDkVJtJmp3 {
+    background-image: url(https://static.xx.fbcdn.net/rsrc.php/v3/y5/r/6qH63PqkI6h.png) !important;
+}
+.sp_M4ukGb_suKT {
+    background-image: url(/rsrc.php/v3/yA/r/wFJuszm5vxY.png) !important;
+}
+.sp_PSWLq4kgoAU {
+    background-image: url(https://static.xx.fbcdn.net/rsrc.php/v3/y-/r/f0k1WJGEgri.png) !important;;
+}
+.sp_TbPEy0vJCwv {
+    background-image: url(/rsrc.php/v3/yi/r/6CkpTuf5j8w.png) !important;
+}
+.sp_0Oe4oGgXSQb {
+    background-image: url(/rsrc.php/v3/yD/r/Nt7s6Wvp3Df.png) !important;
+}
+.sp_5vRJx47G9C5 {
+    background-image: url(/rsrc.php/v3/yi/r/rU9AJ4lp7mV.png) !important;
+}
 
 IGNORE INLINE STYLE
 [role="button"] svg
@@ -1819,21 +1923,21 @@ CSS
     fill: ${#ebedf0} !important;
     background-color: ${#ebedf0} !important;
 }
-.day[fill="#c6e48b"], .legend li[style*="background-color: rgb(198, 228, 139)"] {
-    fill: ${#c6d4cb} !important;
-    background-color: ${#c6d4cb} !important;
+.day[fill="#9be9a8"], .legend li[style*="background-color: rgb(155, 233, 168)"] {
+    fill: ${#9be9a8} !important;
+    background-color: ${#9be9a8} !important;
 }
-.day[fill="#7bc96f"], .legend li[style*="background-color: rgb(123, 201, 111)"] {
-    fill: ${#7bb97f} !important;
-    background-color: ${#7bb97f} !important;
+.day[fill="#40c463"], .legend li[style*="background-color: rgb(64, 196, 99)"] {
+    fill: ${#40c463} !important;
+    background-color: ${#40c463} !important;
 }
-.day[fill="#239a3b"], .legend li[style*="background-color: rgb(35, 154, 59)"] {
-    fill: ${#238a3b} !important;
-    background-color: ${#238a3b} !important;
+.day[fill="#30a14e"], .legend li[style*="background-color: rgb(48, 161, 78)"] {
+    fill: ${#30a14e} !important;
+    background-color: ${#30a14e} !important;
 }
-.day[fill="#196127"], .legend li[style*="background-color: rgb(25, 97, 39)"] {
-    fill: ${#196127} !important;
-    background-color: ${#196127} !important;
+.day[fill="#216e39"], .legend li[style*="background-color: rgb(33, 110, 57)"] {
+    fill: ${#216e39} !important;
+    background-color: ${#216e39} !important;
 }
 .blob-num:not(.cc-coverage-covered-border):not(.cc-coverage-missed-border) {
     border-right: 0 !important;
@@ -2373,6 +2477,13 @@ INVERT
 
 ================================
 
+jsdelivr.com
+
+INVERT
+.navbar-brand > img
+
+================================
+
 kaggle.com
 kaggleusercontent.com
 
@@ -2403,6 +2514,15 @@ INVERT
 
 ================================
 
+krita.org
+
+CSS
+.container {
+    background: none !important;
+}
+
+================================
+
 laptopmag.com
 
 INVERT
@@ -2661,6 +2781,13 @@ INVERT
 
 ================================
 
+maps.metager.de
+
+INVERT
+.map canvas
+
+================================
+
 marginalrevolution.com
 
 INVERT
@@ -3166,6 +3293,18 @@ CSS
 
 ================================
 
+nvidia.com
+
+INVERT
+.global-footer__logo
+
+CSS
+.brand-link.pull-left {
+    filter: invert(1) hue-rotate(170deg) brightness(4);
+}
+
+================================
+
 nvidia.com
 nvidia.in
 
@@ -3259,6 +3398,18 @@ CSS
 
 ================================
 
+olx.pl
+
+CSS
+.cat-icon-628 {
+    background-image: url(https://static.olx.pl/static/olxpl/packed/img/2f828dab38aaebec334f341d5246c125a2.png) !important;
+}
+.cat-icon-87 {
+    background-image: url(https://static.olx.pl/static/olxpl/packed/img/2fe929a474b25c02ed8d46f6b4191f56c2.png) !important;
+}
+
+================================
+
 omnicalculator.com
 
 INVERT
@@ -3354,7 +3505,7 @@ INVERT
 openai.com
 
 INVERT
-.release-cover-title img
+figure.release-cover>div:first-of-type>img
 
 CSS
 body {
@@ -3451,6 +3602,16 @@ CSS
 
 ================================
 
+peonaviveu.blogspot.com
+
+CSS
+body {
+    background-image: none !important;    
+    background-color: var(--darkreader-neutral-background) !important;                                                                                                                                                                                                                                                                     
+}
+
+================================
+
 pgatour.com
 
 CSS
@@ -3649,6 +3810,18 @@ CSS
 [style^="--pseudo-before-background"] {
     --pseudo-before-background: ${#DAE0E6} !important;
 }
+.md p>a[href="#s"]::after, a[href="#s"]::after {
+    color: #000;
+}
+
+================================
+
+redgamingtech.com
+
+CSS
+body {
+    background-image: none !important;                                                                                                                                                                                                                                                                    
+}
 
 ================================
 
@@ -3954,6 +4127,13 @@ CSS
 
 ================================
 
+sony.*
+
+IGNORE INLINE STYLE
+.brand-logo-svg > g
+
+================================
+
 soundcloud.com
 
 INVERT
@@ -4013,6 +4193,15 @@ CSS
 
 ================================
 
+stardewvalleywiki.com
+
+CSS
+html {
+    background: none !important;
+}
+
+================================
+
 store.ubi.com
 
 INVERT
@@ -4063,6 +4252,16 @@ CSS
 
 ================================
 
+streamable.com
+
+INVERT
+.landing .features-pane .feature > span > svg
+.landing .logo
+.landing .start-pane .start-form .try-arrow > img
+#player.container .actions-section .logo
+
+================================
+
 studio.youtube.com
 
 INVERT
@@ -4168,7 +4367,7 @@ body {
 
 ================================
 
-support.discordapp.com
+support.discord.com
 
 INVERT
 .logo img
@@ -4391,6 +4590,20 @@ h1.site-logo
 
 ================================
 
+thronemaster.net
+
+INVERT
+.head-info-wild
+
+CSS
+table.inneractionwindow,
+table.actionwindow.overlay_shadow.dlg_battle,
+table tbody tr td small i {
+    color: #000 !important;
+}
+
+================================
+
 thunderbird.net
 
 INVERT
@@ -4728,6 +4941,19 @@ CSS
 
 unsplash.com
 
+INVERT
+._1Jlgk
+._1Ig-9
+
+CSS
+._2AYJu._1PdBr {
+    background: ${white} !important;
+}
+
+================================
+
+unsplash.com
+
 CSS
 ._2AYJu._1PdBr {
     background: ${white} !important;
diff --git a/src/config/inversion-fixes.config b/src/config/inversion-fixes.config
index 7433ec82..e25914b8 100644
--- a/src/config/inversion-fixes.config
+++ b/src/config/inversion-fixes.config
@@ -295,6 +295,14 @@ NO INVERT
 
 ================================
 
+dash.cloudflare.com
+
+NO INVERT
+.c_bt > div:nth-child(1) > svg:nth-child(1)
+.c_k
+
+================================
+
 dedication.pl
 
 INVERT
diff --git a/src/defaults.ts b/src/defaults.ts
index 0bae2345..0bd3351f 100644
--- a/src/defaults.ts
+++ b/src/defaults.ts
@@ -35,6 +35,7 @@ export const DEFAULT_THEME: Theme = {
 export const DEFAULT_SETTINGS: UserSettings = {
     enabled: true,
     theme: DEFAULT_THEME,
+    presets: [],
     customThemes: [],
     siteList: [],
     siteListEnabled: [],
diff --git a/src/definitions.d.ts b/src/definitions.d.ts
index 8654f0e5..5911c88d 100644
--- a/src/definitions.d.ts
+++ b/src/definitions.d.ts
@@ -36,7 +36,7 @@ export interface ExtWrapper {
     actions: ExtensionActions;
 }
 
-export interface FilterConfig {
+export interface Theme {
     mode: FilterMode;
     brightness: number;
     contrast: number;
@@ -55,16 +55,24 @@ export interface FilterConfig {
     selectionColor: '' | 'auto' | string;
 }
 
-export type Theme = FilterConfig;
+export type FilterConfig = Theme;
 
 export interface CustomSiteConfig {
     url: string[];
     theme: FilterConfig;
 }
 
+export interface ThemePreset {
+    id: string;
+    name: string;
+    urls: string[];
+    theme: Theme;
+}
+
 export interface UserSettings {
     enabled: boolean;
     theme: FilterConfig;
+    presets: ThemePreset[];
     customThemes: CustomSiteConfig[];
     siteList: string[];
     siteListEnabled: string[];
@@ -108,9 +116,10 @@ export interface Shortcuts {
 
 export interface DynamicThemeFix {
     url: string[];
-    ignoreInlineStyle: string[];
     invert: string[];
     css: string;
+    ignoreInlineStyle: string[];
+    ignoreImageAnalysis: string[];
 }
 
 export interface InversionFix {
diff --git a/src/generators/dynamic-theme.ts b/src/generators/dynamic-theme.ts
index ce68d56d..49a4e0a5 100644
--- a/src/generators/dynamic-theme.ts
+++ b/src/generators/dynamic-theme.ts
@@ -8,6 +8,7 @@ const dynamicThemeFixesCommands = {
     'INVERT': 'invert',
     'CSS': 'css',
     'IGNORE INLINE STYLE': 'ignoreInlineStyle',
+    'IGNORE IMAGE ANALYSIS': 'ignoreImageAnalysis',
 };
 
 export function parseDynamicThemeFixes(text: string) {
@@ -54,6 +55,7 @@ export function getDynamicThemeFixesFor(url: string, frameURL: string, fixes: Dy
         invert: fixes[0].invert || [],
         css: fixes[0].css || [],
         ignoreInlineStyle: fixes[0].ignoreInlineStyle || [],
+        ignoreImageAnalysis: fixes[0].ignoreImageAnalysis || [],
     };
     if (enabledForPDF) {
         common.invert = common.invert.concat('embed[type="application/pdf"]');
@@ -80,5 +82,6 @@ export function getDynamicThemeFixesFor(url: string, frameURL: string, fixes: Dy
         invert: common.invert.concat(match.invert || []),
         css: [common.css, match.css].filter((s) => s).join('\n'),
         ignoreInlineStyle: common.ignoreInlineStyle.concat(match.ignoreInlineStyle || []),
+        ignoreImageAnalysis: common.ignoreImageAnalysis.concat(match.ignoreImageAnalysis || []),
     };
 }
diff --git a/src/inject/dynamic-theme/adopted-style-manger.ts b/src/inject/dynamic-theme/adopted-style-manger.ts
index dde7186b..4326b6d8 100644
--- a/src/inject/dynamic-theme/adopted-style-manger.ts
+++ b/src/inject/dynamic-theme/adopted-style-manger.ts
@@ -6,7 +6,7 @@ const adoptedStyleOverrides = new WeakMap<CSSStyleSheet, CSSStyleSheet>();
 const overrideList = new WeakSet<CSSStyleSheet>();
 
 export interface AdoptedStyleSheetManager {
-    render(theme: Theme, variables: Map<string, string>): void;
+    render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]): void;
     destroy(): void;
 }
 
@@ -44,7 +44,7 @@ export function createAdoptedStyleSheetOverride(node: Document | ShadowRoot): Ad
         node.adoptedStyleSheets = newSheets;
     }
 
-    function render(theme: Theme, globalVariables: Map<string, string>) {
+    function render(theme: Theme, globalVariables: Map<string, string>, ignoreImageAnalysis: string[]) {
         node.adoptedStyleSheets.forEach((sheet) => {
             if (overrideList.has(sheet)) {
                 return;
@@ -72,6 +72,7 @@ export function createAdoptedStyleSheetOverride(node: Document | ShadowRoot): Ad
                 sourceCSSRules: rules,
                 theme,
                 variables,
+                ignoreImageAnalysis,
                 force: false,
                 isAsyncCancelled: () => cancelAsyncOperations,
             });
diff --git a/src/inject/dynamic-theme/css-collection.ts b/src/inject/dynamic-theme/css-collection.ts
new file mode 100644
index 00000000..b373d448
--- /dev/null
+++ b/src/inject/dynamic-theme/css-collection.ts
@@ -0,0 +1,51 @@
+import {forEach} from '../../utils/array';
+import {loadAsDataURL} from '../../utils/network';
+import {getMatches, formatCSS} from '../../utils/text';
+
+const blobRegex = /url\(\"(blob\:.*?)\"\)/g;
+
+async function replaceBlobs(text: string) {
+    const promises = [];
+    getMatches(blobRegex, text, 1).forEach((url) => {
+        const promise = loadAsDataURL(url);
+        promises.push(promise);
+    });
+    const data = await Promise.all(promises);
+    return text.replace(blobRegex, () => `url("${data.shift()}")`);
+}
+
+export async function collectCSS() {
+    const css = [];
+
+    function addStaticCSS(selector: string, comment: string) {
+        const staticStyle = document.head.querySelector(selector);
+        if (staticStyle && staticStyle.textContent) {
+            css.push(`/* ${comment} */`);
+            css.push(staticStyle.textContent);
+            css.push('');
+        }
+    }
+
+    addStaticCSS('.darkreader--fallback', 'Fallback Style');
+    addStaticCSS('.darkreader--user-agent', 'User-Agent Style');
+    addStaticCSS('.darkreader--text', 'Text Style');
+    addStaticCSS('.darkreader--invert', 'Invert Style');
+    addStaticCSS('.darkreader--override', 'Override Style');
+    addStaticCSS('.darkreader--variables', 'Variables Style');
+
+    const modifiedCSS = [];
+    document.querySelectorAll('.darkreader--sync').forEach((element: HTMLStyleElement) => {
+        forEach(element.sheet.cssRules, (rule) => {
+            rule && rule.cssText && modifiedCSS.push(rule.cssText);
+        });
+    });
+
+    if (modifiedCSS.length != 0) {
+        const formattedCSS = formatCSS(modifiedCSS.join('\n'));
+        css.push('/* Modified CSS */');
+        css.push(await replaceBlobs(formattedCSS));
+        css.push('');
+    }
+
+    return css.join('\n');
+}
diff --git a/src/inject/dynamic-theme/index.ts b/src/inject/dynamic-theme/index.ts
index 5d5eaeb7..8a03cd16 100644
--- a/src/inject/dynamic-theme/index.ts
+++ b/src/inject/dynamic-theme/index.ts
@@ -14,11 +14,11 @@ import {getCSSFilterValue} from '../../generators/css-filter';
 import {modifyColor} from '../../generators/modify-colors';
 import {createTextStyle} from '../../generators/text-style';
 import {FilterConfig, DynamicThemeFix} from '../../definitions';
-import {generateInstanceId} from './uuid';
+import {generateUID} from '../../utils/uid';
 import {createAdoptedStyleSheetOverride, AdoptedStyleSheetManager} from './adopted-style-manger';
 
 const variables = new Map<string, string>();
-const INSTANCE_ID = generateInstanceId();
+const INSTANCE_ID = generateUID();
 const styleManagers = new Map<StyleElement, StyleManager>();
 const adoptedStyleManagers = [] as Array<AdoptedStyleSheetManager>;
 let filter: FilterConfig = null;
@@ -137,6 +137,10 @@ function cleanFallbackStyle() {
     }
 }
 
+function getIgnoreImageAnalysisSelectors() {
+    return fixes && Array.isArray(fixes.ignoreImageAnalysis) ? fixes.ignoreImageAnalysis : [];
+}
+
 function createDynamicStyleOverrides() {
     cancelRendering();
 
@@ -152,7 +156,7 @@ function createDynamicStyleOverrides() {
         .filter((details) => details && details.variables.size > 0)
         .map(({variables}) => variables);
     if (newVariables.length === 0) {
-        styleManagers.forEach((manager) => manager.render(filter, variables));
+        styleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
         if (loadingStyles.size === 0) {
             cleanFallbackStyle();
         }
@@ -174,8 +178,8 @@ function createDynamicStyleOverrides() {
             push(inlineStyleElements, elements);
         }
     });
-    const ignoredSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
-    inlineStyleElements.forEach((el) => overrideInlineStyle(el as HTMLElement, filter, ignoredSelectors));
+    const ignoredInlineSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
+    inlineStyleElements.forEach((el) => overrideInlineStyle(el as HTMLElement, filter, getIgnoreImageAnalysisSelectors(), ignoredInlineSelectors));
     handleAdoptedStyleSheets(document);
 }
 
@@ -209,7 +213,7 @@ function createManager(element: StyleElement) {
             return;
         }
         if (details.variables.size === 0) {
-            manager.render(filter, variables);
+            manager.render(filter, variables, getIgnoreImageAnalysisSelectors());
         } else {
             updateVariables(details.variables);
             throttledRenderAllStyles();
@@ -243,8 +247,8 @@ function removeManager(element: StyleElement) {
 }
 
 const throttledRenderAllStyles = throttle((callback?: () => void) => {
-    styleManagers.forEach((manager) => manager.render(filter, variables));
-    adoptedStyleManagers.forEach((manager) => manager.render(filter, variables));
+    styleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
+    adoptedStyleManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
     callback && callback();
 });
 
@@ -301,8 +305,9 @@ function handleAdoptedStyleSheets(node: ShadowRoot | Document) {
     if (Array.isArray(node.adoptedStyleSheets)) {
         if (node.adoptedStyleSheets.length > 0) {
             const newManger = createAdoptedStyleSheetOverride(node);
+
             adoptedStyleManagers.push(newManger);
-            newManger.render(filter, variables);
+            newManger.render(filter, variables, getIgnoreImageAnalysisSelectors());
         }
     }
 }
@@ -323,7 +328,7 @@ function watchForUpdates() {
             .filter((details) => details && details.variables.size > 0)
             .map(({variables}) => variables);
         if (newVariables.length === 0) {
-            newManagers.forEach((manager) => manager.render(filter, variables));
+            newManagers.forEach((manager) => manager.render(filter, variables, getIgnoreImageAnalysisSelectors()));
         } else {
             newVariables.forEach((variables) => updateVariables(variables));
             throttledRenderAllStyles();
@@ -334,9 +339,9 @@ function watchForUpdates() {
         handleAdoptedStyleSheets(shadowRoot);
     });
 
-    const ignoredSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
+    const ignoredInlineSelectors = fixes && Array.isArray(fixes.ignoreInlineStyle) ? fixes.ignoreInlineStyle : [];
     watchForInlineStyles((element) => {
-        overrideInlineStyle(element, filter, ignoredSelectors);
+        overrideInlineStyle(element, filter, ignoredInlineSelectors, getIgnoreImageAnalysisSelectors());
         if (element === document.documentElement) {
             const rootVariables = getElementCSSVariables(document.documentElement);
             if (rootVariables.size > 0) {
@@ -348,7 +353,7 @@ function watchForUpdates() {
         const inlineStyleElements = root.querySelectorAll(INLINE_STYLE_SELECTOR);
         if (inlineStyleElements.length > 0) {
             createShadowStaticStyleOverrides(root);
-            forEach(inlineStyleElements, (el) => overrideInlineStyle(el as HTMLElement, filter, ignoredSelectors));
+            forEach(inlineStyleElements, (el) => overrideInlineStyle(el as HTMLElement, filter, getIgnoreImageAnalysisSelectors(), ignoredInlineSelectors));
         }
     });
 
diff --git a/src/inject/dynamic-theme/inline-style.ts b/src/inject/dynamic-theme/inline-style.ts
index 32ca68b4..024371fb 100644
--- a/src/inject/dynamic-theme/inline-style.ts
+++ b/src/inject/dynamic-theme/inline-style.ts
@@ -213,7 +213,7 @@ function shouldIgnoreInlineStyle(element: HTMLElement, selectors: string[]) {
     return false;
 }
 
-export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, ignoreSelectors: string[]) {
+export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, ignoreInlineSelectors: string[], ignoreImageSelectors: string[]) {
     const cacheKey = getInlineStyleCacheKey(element, theme);
     if (cacheKey === inlineStyleCache.get(element)) {
         return;
@@ -224,7 +224,7 @@ export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, i
     function setCustomProp(targetCSSProp: string, modifierCSSProp: string, cssVal: string) {
         const {customProp, dataAttr} = overrides[targetCSSProp];
 
-        const mod = getModifiableCSSDeclaration(modifierCSSProp, cssVal, null, null);
+        const mod = getModifiableCSSDeclaration(modifierCSSProp, cssVal, null, ignoreImageSelectors, null);
         if (!mod) {
             return;
         }
@@ -239,8 +239,8 @@ export function overrideInlineStyle(element: HTMLElement, theme: FilterConfig, i
         unsetProps.delete(targetCSSProp);
     }
 
-    if (ignoreSelectors.length > 0) {
-        if (shouldIgnoreInlineStyle(element, ignoreSelectors)) {
+    if (ignoreInlineSelectors.length > 0) {
+        if (shouldIgnoreInlineStyle(element, ignoreInlineSelectors)) {
             unsetProps.forEach((cssProp) => {
                 const {store, dataAttr} = overrides[cssProp];
                 store.delete(element);
diff --git a/src/inject/dynamic-theme/modify-css.ts b/src/inject/dynamic-theme/modify-css.ts
index 589c294e..0a32a7df 100644
--- a/src/inject/dynamic-theme/modify-css.ts
+++ b/src/inject/dynamic-theme/modify-css.ts
@@ -23,7 +23,7 @@ export interface ModifiableCSSRule {
     declarations: ModifiableCSSDeclaration[];
 }
 
-export function getModifiableCSSDeclaration(property: string, value: string, rule: CSSStyleRule, isCancelled: () => boolean): ModifiableCSSDeclaration {
+export function getModifiableCSSDeclaration(property: string, value: string, rule: CSSStyleRule, ignoreImageSelectors: string[], isCancelled: () => boolean): ModifiableCSSDeclaration {
     const important = Boolean(rule && rule.style && rule.style.getPropertyPriority(property));
     const sourceValue = value;
     if (property.startsWith('--')) {
@@ -38,7 +38,7 @@ export function getModifiableCSSDeclaration(property: string, value: string, rul
             return {property, value: modifier, important, sourceValue};
         }
     } else if (property === 'background-image' || property === 'list-style-image') {
-        const modifier = getBgImageModifier(property, value, rule, isCancelled);
+        const modifier = getBgImageModifier(value, rule, ignoreImageSelectors, isCancelled);
         if (modifier) {
             return {property, value: modifier, important, sourceValue};
         }
@@ -233,7 +233,20 @@ const gradientRegex = /[\-a-z]+gradient\(([^\(\)]*(\(([^\(\)]*(\(.*?\)))*[^\(\)]
 const imageDetailsCache = new Map<string, ImageDetails>();
 const awaitingForImageLoading = new Map<string, ((imageDetails: ImageDetails) => void)[]>();
 
-function getBgImageModifier(prop: string, value: string, rule: CSSStyleRule, isCancelled: () => boolean): string | CSSValueModifier {
+function shouldIgnoreImage(element: CSSStyleRule, selectors: string[]) {
+    if (!element) {
+        return false;
+    }
+    for (let i = 0; i < selectors.length; i++) {
+        const ingnoredSelector = selectors[i];
+        if (element.selectorText.match(ingnoredSelector)) {
+            return true;
+        }
+    }
+    return false;
+}
+
+function getBgImageModifier(value: string, rule: CSSStyleRule, ignoreImageSelectors: string[], isCancelled: () => boolean): string | CSSValueModifier {
     try {
         const gradients = getMatches(gradientRegex, value);
         const urls = getMatches(cssURLRegex, value);
@@ -311,6 +324,9 @@ function getBgImageModifier(prop: string, value: string, rule: CSSStyleRule, isC
                     imageDetails = imageDetailsCache.get(url);
                 } else {
                     try {
+                        if (shouldIgnoreImage(rule, ignoreImageSelectors)) {
+                            return null;
+                        }
                         if (awaitingForImageLoading.has(url)) {
                             const awaiters = awaitingForImageLoading.get(url);
                             imageDetails = await new Promise<ImageDetails>((resolve) => awaiters.push(resolve));
diff --git a/src/inject/dynamic-theme/style-manager.ts b/src/inject/dynamic-theme/style-manager.ts
index c81f9b51..28fdc9f0 100644
--- a/src/inject/dynamic-theme/style-manager.ts
+++ b/src/inject/dynamic-theme/style-manager.ts
@@ -31,7 +31,7 @@ export type StyleElement = HTMLLinkElement | HTMLStyleElement;
 
 export interface StyleManager {
     details(): {variables: Map<string, string>};
-    render(theme: Theme, variables: Map<string, string>): void;
+    render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]): void;
     pause(): void;
     destroy(): void;
     watch(): void;
@@ -226,7 +226,7 @@ export function manageStyle(element: StyleElement, {update, loadingStart, loadin
 
     let forceRenderStyle = false;
 
-    function render(theme: Theme, variables: Map<string, string>) {
+    function render(theme: Theme, variables: Map<string, string>, ignoreImageAnalysis: string[]) {
         const rules = getRulesSync();
         if (!rules) {
             return;
@@ -276,6 +276,7 @@ export function manageStyle(element: StyleElement, {update, loadingStart, loadin
                 sourceCSSRules: rules,
                 theme,
                 variables,
+                ignoreImageAnalysis,
                 force,
                 isAsyncCancelled: () => cancelAsyncOperations,
             });
diff --git a/src/inject/dynamic-theme/stylesheet-modifier.ts b/src/inject/dynamic-theme/stylesheet-modifier.ts
index 5c845075..d33e872b 100644
--- a/src/inject/dynamic-theme/stylesheet-modifier.ts
+++ b/src/inject/dynamic-theme/stylesheet-modifier.ts
@@ -44,6 +44,7 @@ export function createStyleSheetModifier() {
         sourceCSSRules: CSSRuleList;
         theme: Theme;
         variables: Map<string, string>;
+        ignoreImageAnalysis: string[]
         force: boolean;
         prepareSheet: () => CSSStyleSheet;
         isAsyncCancelled: () => boolean;
@@ -51,7 +52,7 @@ export function createStyleSheetModifier() {
 
     function modifySheet(options: ModifySheetOptions): void {
         const rules = options.sourceCSSRules;
-        const {theme, variables, force, prepareSheet, isAsyncCancelled} = options;
+        const {theme, variables, ignoreImageAnalysis, force, prepareSheet, isAsyncCancelled} = options;
 
         let rulesChanged = (rulesModCache.size === 0);
         const notFoundCacheKeys = new Set(rulesModCache.keys());
@@ -94,7 +95,7 @@ export function createStyleSheetModifier() {
             const modDecs: ModifiableCSSDeclaration[] = [];
             const targetRule = varsRule || rule;
             targetRule && targetRule.style && iterateCSSDeclarations(targetRule.style, (property, value) => {
-                const mod = getModifiableCSSDeclaration(property, value, rule, isAsyncCancelled);
+                const mod = getModifiableCSSDeclaration(property, value, rule, ignoreImageAnalysis, isAsyncCancelled);
                 if (mod) {
                     modDecs.push(mod);
                 }
diff --git a/src/inject/index.ts b/src/inject/index.ts
index 470c9357..c7d9bfc8 100644
--- a/src/inject/index.ts
+++ b/src/inject/index.ts
@@ -3,6 +3,7 @@ import {createOrUpdateSVGFilter, removeSVGFilter} from './svg-filter';
 import {createOrUpdateDynamicTheme, removeDynamicTheme, cleanDynamicThemeCache} from './dynamic-theme';
 import {logInfo, logWarn} from './utils/log';
 import {watchForColorSchemeChange} from './utils/watch-color-scheme';
+import {collectCSS} from './dynamic-theme/css-collection';
 
 function onMessage({type, data}) {
     switch (type) {
@@ -26,6 +27,10 @@ function onMessage({type, data}) {
             createOrUpdateDynamicTheme(filter, fixes, isIFrame);
             break;
         }
+        case 'export-css': {
+            collectCSS().then((collectedCSS) => chrome.runtime.sendMessage({type: 'export-css-response', data: collectedCSS}));
+            break;
+        }
         case 'clean-up': {
             removeStyle();
             removeSVGFilter();
diff --git a/src/ui/controls/color-dropdown/index.tsx b/src/ui/controls/color-dropdown/index.tsx
index 9531de4d..31486565 100644
--- a/src/ui/controls/color-dropdown/index.tsx
+++ b/src/ui/controls/color-dropdown/index.tsx
@@ -29,9 +29,9 @@ export default function ColorDropDown(props: ColorDropDownProps) {
     };
 
     const dropDownOptions = [
-        props.hasDefaultOption ? {id: 'default', label: labels.DEFAULT} : null,
-        props.hasAutoOption ? {id: 'auto', label: labels.AUTO} : null,
-        {id: 'custom', label: labels.CUSTOM},
+        props.hasDefaultOption ? {id: 'default', content: labels.DEFAULT} : null,
+        props.hasAutoOption ? {id: 'auto', content: labels.AUTO} : null,
+        {id: 'custom', content: labels.CUSTOM},
     ].filter((v) => v);
 
     const selectedDropDownValue = (
diff --git a/src/ui/controls/dropdown/index.tsx b/src/ui/controls/dropdown/index.tsx
index 91ff4653..c3c33d83 100644
--- a/src/ui/controls/dropdown/index.tsx
+++ b/src/ui/controls/dropdown/index.tsx
@@ -1,7 +1,7 @@
 import {m} from 'malevic';
 import {getContext} from 'malevic/dom';
 
-type DropDownOption<T> = {id: T; label: string};
+type DropDownOption<T> = {id: T; content: Malevic.Child};
 
 interface DropDownProps<T> {
     class?: string;
@@ -18,6 +18,14 @@ export default function DropDown<T>(props: DropDownProps<T>) {
         selectedNode: HTMLElement;
     };
 
+    if (context.prev) {
+        const currOptions = props.options.map((o) => o.id);
+        const prevOptions = (context.prev.props.options as DropDownOption<T>[]).map((o) => o.id);
+        if (currOptions.length !== prevOptions.length || currOptions.some((o, i) => o !== prevOptions[i])) {
+            store.isOpen = false;
+        }
+    }
+
     function saveListNode(el: HTMLElement) {
         store.listNode = el;
     }
@@ -65,12 +73,12 @@ export default function DropDown<T>(props: DropDownProps<T>) {
                     props.onChange(value.id);
                 }}
             >
-                {value.label}
+                {value.content}
             </span>
         );
     }
 
-    const selectedLabel = props.options.find((value) => value.id === props.selected).label;
+    const selectedContent = props.options.find((value) => value.id === props.selected).content;
 
     return (
         <span
@@ -95,7 +103,7 @@ export default function DropDown<T>(props: DropDownProps<T>) {
                 onclick={onSelectedClick}
             >
                 <span class="dropdown__selected__text">
-                    {selectedLabel}
+                    {selectedContent}
                 </span>
             </span>
         </span >
diff --git a/src/ui/controls/dropdown/style.less b/src/ui/controls/dropdown/style.less
index adc509d7..82deea12 100644
--- a/src/ui/controls/dropdown/style.less
+++ b/src/ui/controls/dropdown/style.less
@@ -11,6 +11,7 @@
     width: 100%;
 
     &__selected {
+        align-items: center;
         background-color: @color-control-active;
         background-image: @icon-down;
         background-position: center right 0.25rem;
@@ -18,12 +19,13 @@
         background-size: 1rem;
         border-radius: @dropdown-item-height / 2;
         color: @color-control-fore;
-        display: inline-block;
+        display: inline-flex;
         height: @dropdown-item-height;
+        justify-content: center;
         line-height: @dropdown-item-height;
         margin-top: (@dropdown-height - @dropdown-item-height) / 2;
-        text-align: center;
         position: absolute;
+        text-align: center;
         width: 100%;
 
         &__text {
@@ -36,7 +38,7 @@
     }
 
     &__list {
-        background-color: @color-control-hover;
+        background-color: @color-control-back;
         border-radius: @dropdown-item-height / 2;
         box-shadow: 0 0 0.25rem black;
         display: none;
@@ -45,10 +47,12 @@
         width: 100%;
 
         &__item {
+            align-items: center;
+            background-color: @color-control-back;
             color: @color-control-fore;
-            display: inline-block;
-            height: @dropdown-item-height;
-            line-height: @dropdown-item-height;
+            display: inline-flex;
+            justify-content: center;
+            min-height: @dropdown-item-height;
             text-align: center;
             width: 100%;
             
@@ -72,15 +76,13 @@
         z-index: 1;
 
         &__item {
-            height: @dropdown-item-active-height;
-            line-height: @dropdown-item-active-height;
+            min-height: @dropdown-item-active-height;
         }
     }
 
     &--open &__selected {
         border-radius: @dropdown-item-active-height / 2;
         height: @dropdown-item-active-height;
-        line-height: @dropdown-item-active-height;
         margin-top: (@dropdown-height - @dropdown-item-active-height) / 2;
         z-index: 2;
     }
diff --git a/src/ui/controls/overlay/style.less b/src/ui/controls/overlay/style.less
index 0c1c003f..5bea3b2d 100644
--- a/src/ui/controls/overlay/style.less
+++ b/src/ui/controls/overlay/style.less
@@ -11,6 +11,7 @@
     position: fixed;
     top: 0;
     width: 100%;
+    z-index: 2;
 
     &:empty {
         display: none;
diff --git a/src/ui/popup/manage-settings-page/export-theme.tsx b/src/ui/popup/manage-settings-page/export-theme.tsx
new file mode 100644
index 00000000..737cb16f
--- /dev/null
+++ b/src/ui/popup/manage-settings-page/export-theme.tsx
@@ -0,0 +1,35 @@
+import {m} from 'malevic';
+import {Button} from '../../controls';
+import {saveFile} from '../../utils';
+import ControlGroup from '../control-group';
+import {getURLHost} from '../../../utils/url';
+
+export default function ExportTheme() {
+    const listener = ({type, data}, sender: chrome.runtime.MessageSender) => {
+        if (type === 'export-css-response') {
+            const url = getURLHost(sender.tab.url).replace(/[^a-z0-1\-]/g, '-');
+            saveFile(`DarkReader-${url}.css`, data);
+            chrome.runtime.onMessage.removeListener(listener);
+        }
+    };
+
+    function exportCSS() {
+        chrome.runtime.onMessage.addListener(listener);
+        chrome.runtime.sendMessage({type: 'request-export-css'});
+    }
+    return (
+        <ControlGroup>
+            <ControlGroup.Control>
+                <Button
+                    onclick={exportCSS}
+                    class="settings-button"
+                >
+                    Export Dynamic Theme
+                </Button>
+            </ControlGroup.Control>
+            <ControlGroup.Description>
+                Save generated CSS to a file
+            </ControlGroup.Description>
+        </ControlGroup>
+    );
+}
diff --git a/src/ui/popup/manage-settings-page/index.tsx b/src/ui/popup/manage-settings-page/index.tsx
index 092eb483..9c7159f1 100644
--- a/src/ui/popup/manage-settings-page/index.tsx
+++ b/src/ui/popup/manage-settings-page/index.tsx
@@ -4,14 +4,24 @@ import ResetButtonGroup from './reset-settings-button';
 import ImportButton from './import-settings';
 import ExportButton from './export-settings';
 import SyncSettings from './sync-settings';
+import ExportTheme from './export-theme';
+import {isURLInList} from '../../../utils/url';
+import themeEngines from '../../../generators/theme-engines';
 
 export default function ManageSettingsPage(props: ViewProps) {
+    const custom = props.data.settings.customThemes.find(
+        ({url}) => isURLInList(props.tab.url, url)
+    );
+    const engine = custom ?
+        custom.theme.engine :
+        props.data.settings.theme.engine;
 
     return (
         <section class="m-section">
             <SyncSettings {...props} />
             <ImportButton {...props} />
             <ExportButton {...props} />
+            {engine === themeEngines.dynamicTheme ? <ExportTheme /> : null}
             <ResetButtonGroup {...props} />
         </section>
     );
diff --git a/src/ui/popup/theme/controls/mode.tsx b/src/ui/popup/theme/controls/mode.tsx
index b98c4263..ae60be3c 100644
--- a/src/ui/popup/theme/controls/mode.tsx
+++ b/src/ui/popup/theme/controls/mode.tsx
@@ -17,10 +17,10 @@ export default function Mode(props: {mode: string; onChange: (mode: string) => v
     }
 
     const modes = [
-        {id: ThemeEngines.dynamicTheme, label: getLocalMessage('engine_dynamic')},
-        {id: ThemeEngines.cssFilter, label: getLocalMessage('engine_filter')},
-        {id: ThemeEngines.svgFilter, label: getLocalMessage('engine_filter_plus')},
-        {id: ThemeEngines.staticTheme, label: getLocalMessage('engine_static')},
+        {id: ThemeEngines.dynamicTheme, content: getLocalMessage('engine_dynamic')},
+        {id: ThemeEngines.cssFilter, content: getLocalMessage('engine_filter')},
+        {id: ThemeEngines.svgFilter, content: getLocalMessage('engine_filter_plus')},
+        {id: ThemeEngines.staticTheme, content: getLocalMessage('engine_static')},
     ];
     return (
         <ThemeControl label="Mode">
diff --git a/src/ui/popup/theme/controls/scheme.tsx b/src/ui/popup/theme/controls/scheme.tsx
index d5828b6f..61a57f11 100644
--- a/src/ui/popup/theme/controls/scheme.tsx
+++ b/src/ui/popup/theme/controls/scheme.tsx
@@ -9,8 +9,8 @@ export default function Scheme(props: {isDark: boolean; onChange: (dark: boolean
             <DropDown
                 selected={props.isDark}
                 options={[
-                    {id: true, label: getLocalMessage('dark')},
-                    {id: false, label: getLocalMessage('light')},
+                    {id: true, content: getLocalMessage('dark')},
+                    {id: false, content: getLocalMessage('light')},
                 ]}
                 onChange={props.onChange}
             />
diff --git a/src/ui/popup/theme/page/style.less b/src/ui/popup/theme/page/style.less
index 91cc86e9..086d7fe3 100644
--- a/src/ui/popup/theme/page/style.less
+++ b/src/ui/popup/theme/page/style.less
@@ -12,13 +12,11 @@
             &__selected {
                 border-radius: unset;
                 height: @size-control-inner + 2 * @size-border;
-                line-height: @size-control-inner + 2 * @size-border;
             }
         
             &__list {
                 &__item {
-                    height: @size-control-inner + 2 * @size-border;
-                    line-height: @size-control-inner + 2 * @size-border;
+                    min-height: @size-control-inner + 2 * @size-border;
                 }
             }
         }
diff --git a/src/ui/popup/theme/preset-picker/index.tsx b/src/ui/popup/theme/preset-picker/index.tsx
index fe4d60d0..2283c98a 100644
--- a/src/ui/popup/theme/preset-picker/index.tsx
+++ b/src/ui/popup/theme/preset-picker/index.tsx
@@ -1,35 +1,149 @@
 import {m} from 'malevic';
-import {isURLInList, getURLHost} from '../../../../utils/url';
-import {DropDown} from '../../../controls';
+import {getContext} from 'malevic/dom';
+import {ThemePreset} from '../../../../definitions';
+import {isURLInList, isURLMatched, getURLHost} from '../../../../utils/url';
+import {DropDown, MessageBox} from '../../../controls';
 import {ViewProps} from '../../types';
+import {generateUID} from '../../../../utils/uid';
+
+function PresetItem(props: ViewProps & {preset: ThemePreset}) {
+    const context = getContext();
+    const store = context.store as {isConfirmationVisible: boolean};
+
+    function onRemoveClick(e: MouseEvent) {
+        e.stopPropagation();
+        store.isConfirmationVisible = true;
+        context.refresh();
+    }
+
+    function onConfirmRemoveClick() {
+        const filtered = props.data.settings.presets.filter((p) => p.id !== props.preset.id);
+        props.actions.changeSettings({presets: filtered});
+    }
+
+    function onCancelRemoveClick() {
+        store.isConfirmationVisible = false;
+        context.refresh();
+    }
+
+    const confirmation = store.isConfirmationVisible ? (
+        <MessageBox
+            caption={`Are you sure you want to remove ${props.preset.name}?`}
+            onOK={onConfirmRemoveClick}
+            onCancel={onCancelRemoveClick}
+        />
+    ) : null;
+
+    return (
+        <span class="theme-preset-picker__preset">
+            <span class="theme-preset-picker__preset__name">{props.preset.name}</span>
+            <span class="theme-preset-picker__preset__remove-button" onclick={onRemoveClick}></span>
+            {confirmation}
+        </span>
+    );
+}
+
+const MAX_ALLOWED_PRESETS = 3;
 
 export default function PresetPicker(props: ViewProps) {
     const host = getURLHost(props.tab.url || '');
+    const preset = props.data.settings.presets.find(
+        ({urls}) => isURLInList(props.tab.url, urls)
+    );
     const custom = props.data.settings.customThemes.find(
         ({url}) => isURLInList(props.tab.url, url)
     );
 
+    const selectedPresetId = custom ? 'custom' : preset ? preset.id : 'default';
+
+    const defaultOption = {id: 'default', content: 'Default theme'};
+    const addNewPresetOption = props.data.settings.presets.length < MAX_ALLOWED_PRESETS ?
+        {id: 'add-preset', content: '\uff0b Create new theme'} :
+        null;
+    const userPresetsOptions = props.data.settings.presets.map((preset) => {
+        if (preset.id === selectedPresetId) {
+            return {id: preset.id, content: preset.name};
+        }
+        return {
+            id: preset.id,
+            content: <PresetItem {...props} preset={preset} />
+        };
+    });
+    const customSitePresetOption = {
+        id: 'custom',
+        content: `${selectedPresetId === 'custom' ? '\u2605' : '\u2606'} Custom for ${host}`,
+    };
+
     const dropdownOptions = [
-        {id: 'default', label: 'Default theme'},
-        {id: 'custom', label: `Custom for ${host}`},
-    ];
+        defaultOption,
+        ...userPresetsOptions,
+        addNewPresetOption,
+        customSitePresetOption,
+    ].filter(Boolean);
 
     function onPresetChange(id: string) {
         const filteredCustomThemes = props.data.settings.customThemes.filter(({url}) => !isURLInList(props.tab.url, url));
+        const filteredPresets = props.data.settings.presets.map((preset) => {
+            return {
+                ...preset,
+                urls: preset.urls.filter((template) => !isURLMatched(props.tab.url, template)),
+            };
+        });
         if (id === 'default') {
-            props.actions.changeSettings({customThemes: filteredCustomThemes});
+            props.actions.changeSettings({
+                customThemes: filteredCustomThemes,
+                presets: filteredPresets,
+            });
         } else if (id === 'custom') {
             const extended = filteredCustomThemes.concat({
                 url: [host],
                 theme: {...props.data.settings.theme},
             });
-            props.actions.changeSettings({customThemes: extended});
+            props.actions.changeSettings({
+                customThemes: extended,
+                presets: filteredPresets,
+            });
+        } else if (id === 'add-preset') {
+            let newPresetName: string;
+            for (let i = 0; i <= props.data.settings.presets.length; i++) {
+                newPresetName = `Theme ${i + 1}`;
+                if (props.data.settings.presets.every((p) => p.name !== newPresetName)) {
+                    break;
+                }
+            }
+
+            const extended = filteredPresets.concat({
+                id: `preset-${generateUID()}`,
+                name: newPresetName,
+                urls: [host],
+                theme: {...props.data.settings.theme},
+            });
+            props.actions.changeSettings({
+                customThemes: filteredCustomThemes,
+                presets: extended,
+            });
+        } else {
+            const chosenPresetId = id;
+            const extended = filteredPresets.map((preset) => {
+                if (preset.id === chosenPresetId) {
+                    return {
+                        ...preset,
+                        urls: preset.urls.concat(host)
+                    };
+                }
+                return preset;
+            });
+            props.actions.changeSettings({
+                customThemes: filteredCustomThemes,
+                presets: extended,
+            });
         }
     }
+
     return (
         <DropDown
             class="theme-preset-picker"
-            selected={custom ? 'custom' : 'default'}
+            selected={selectedPresetId}
             options={dropdownOptions}
             onChange={onPresetChange}
         />
diff --git a/src/ui/popup/theme/preset-picker/style.less b/src/ui/popup/theme/preset-picker/style.less
index ac0d25f1..a44e8c43 100644
--- a/src/ui/popup/theme/preset-picker/style.less
+++ b/src/ui/popup/theme/preset-picker/style.less
@@ -1,13 +1,51 @@
+@import "../../../theme";
+
+@theme-preset-height: 1.5rem;
+@theme-preset-picker-button-size: 1.5rem;
+
 .theme-preset-picker {
     .dropdown {
         &__selected {
             border-radius: unset;
             height: @size-control-inner;
-            line-height: @size-control-inner;
             margin-top: 0;
         }
+
         &__list {
             border-radius: unset;
+            box-shadow: 0 0 0 @size-border @color-border, 0 0 0.25rem 0.0625rem + @size-border black;
+        }
+    }
+
+    &__preset {
+        align-items: center;
+        display: inline-flex;
+        justify-content: center;
+        min-height: @theme-preset-height;
+        position: relative;
+        width: 100%;
+
+        &__remove-button {
+            border-radius: 50%;
+            cursor: pointer;
+            display: inline-block;
+            height: @theme-preset-picker-button-size;
+            position: absolute;
+            right: 0;
+            top: 0;
+            width: @theme-preset-picker-button-size;
+
+            &::after {
+                content: "\2715";
+                display: inline-block;
+                line-height: @theme-preset-picker-button-size;
+                text-align: center;
+            }
+
+            &:hover {
+                background-color: @color-control-active;
+                color: @color-control-fore;
+            }
         }
     }
 }
diff --git a/src/ui/popup/theme/utils.ts b/src/ui/popup/theme/utils.ts
index 676666cf..908441be 100644
--- a/src/ui/popup/theme/utils.ts
+++ b/src/ui/popup/theme/utils.ts
@@ -6,9 +6,14 @@ export function getCurrentThemePreset(props: ViewProps) {
     const custom = props.data.settings.customThemes.find(
         ({url}) => isURLInList(props.tab.url, url)
     );
+    const preset = custom ? null : props.data.settings.presets.find(
+        ({urls}) => isURLInList(props.tab.url, urls)
+    );
     const theme = custom ?
         custom.theme :
-        props.data.settings.theme;
+        preset ?
+            preset.theme :
+            props.data.settings.theme;
 
     function setTheme(config: Partial<Theme>) {
         if (custom) {
@@ -16,6 +21,11 @@ export function getCurrentThemePreset(props: ViewProps) {
             props.actions.changeSettings({
                 customThemes: props.data.settings.customThemes,
             });
+        } else if (preset) {
+            preset.theme = {...preset.theme, ...config};
+            props.actions.changeSettings({
+                presets: props.data.settings.presets,
+            });
         } else {
             props.actions.setTheme(config);
         }
diff --git a/src/utils/text.ts b/src/utils/text.ts
index 5c39a41d..1c063cac 100644
--- a/src/utils/text.ts
+++ b/src/utils/text.ts
@@ -60,3 +60,47 @@ export function getMatches(regex: RegExp, input: string, group = 0) {
 export function getStringSize(value: string) {
     return value.length * 2;
 }
+
+export function formatCSS(text: string) {
+
+    function trimLeft(text: string) {
+        return text.replace(/^\s+/, '');
+    }
+
+    function getIndent(depth: number) {
+        if (depth === 0) {
+            return '';
+        }
+        return ' '.repeat(4 * depth);
+    }
+
+    const emptyRuleRegexp = /[^{}]+{\s*}/g;
+    while (emptyRuleRegexp.test(text)) {
+        text = text.replace(emptyRuleRegexp, '');
+    }
+
+    const css = (text
+        .replace(/\s{2,}/g, ' ') // Replacing multiple spaces to one
+        .replace(/\{/g, '{\n') // {
+        .replace(/\}/g, '\n}\n') // }
+        .replace(/\;(?![^(\(|\")]*(\)|\"))/g, ';\n') // ; and do not target between () and ""
+        .replace(/\,(?![^(\(|\")]*(\)|\"))/g, ',\n') // , and do not target between () and ""
+        .replace(/\n\s*\n/g, '\n') // Remove \n Without any characters between it to the next \n
+        .split('\n'));
+
+    let depth = 0;
+    const formatted = [];
+
+    for (let x = 0, len = css.length; x < len; x++) {
+        const line = css[x] + '\n';
+        if (line.match(/\{/)) { // {
+            formatted.push(getIndent(depth++) + trimLeft(line));
+        } else if (line.match(/\}/)) { // }
+            formatted.push(getIndent(--depth) + trimLeft(line));
+        } else { // CSS line
+            formatted.push(getIndent(depth) + trimLeft(line));
+        }
+    }
+
+    return formatted.join('').trim();
+}
diff --git a/src/inject/dynamic-theme/uuid.ts b/src/utils/uid.ts
similarity index 83%
rename from src/inject/dynamic-theme/uuid.ts
rename to src/utils/uid.ts
index 32289063..56c2aadc 100644
--- a/src/inject/dynamic-theme/uuid.ts
+++ b/src/utils/uid.ts
@@ -2,6 +2,6 @@ function hexify(number: number) {
     return ((number < 16 ? '0' : '') + number.toString(16));
 }
 
-export function generateInstanceId() {
+export function generateUID() {
     return Array.from(crypto.getRandomValues(new Uint8Array(16))).map((x) => hexify(x)).join('');
 }


Test patch:
diff --git a/tests/config/config.tests.ts b/tests/config/config.tests.ts
index 0d4aba2e..12c1bf26 100644
--- a/tests/config/config.tests.ts
+++ b/tests/config/config.tests.ts
@@ -62,7 +62,7 @@ test('Dynamic Theme Fixes config', async () => {
     expect(fixes.map(({url}) => url[0])).toEqual(fixes.map(({url}) => url[0]).sort(compareURLPatterns));
 
     // selectors should have no comma
-    expect(fixes.every(({invert, ignoreInlineStyle}) => (invert || []).concat(ignoreInlineStyle || []).every((s) => s.indexOf(',') < 0))).toBe(true);
+    expect(fixes.every(({invert, ignoreInlineStyle, ignoreImageAnalysis}) => (invert || []).concat(ignoreInlineStyle || []).concat(ignoreImageAnalysis || []).every((s) => s.indexOf(',') < 0))).toBe(true);
 
     // fixes are properly formatted
     expect(throwIfDifferent(file, formatDynamicThemeFixes(fixes), 'Dynamic fixes format error')).not.toThrow();
@@ -84,10 +84,14 @@ test('Dynamic Theme Fixes config', async () => {
         '========',
         'wikipedia.org',
         'IGNORE INLINE STYLE', 'a', 'b',
+        '========',
+        'duckduckgo.com',
+        'IGNORE IMAGE ANALYSIS', 'img[alt="Logo"]', 'canvas',
     ].join('\r\n'))).toEqual([
         {url: ['inbox.google.com', 'mail.google.com'], invert: ['a', 'b'], css: '.x { color: white !important; }'},
         {url: ['twitter.com'], invert: ['c', 'd']},
         {url: ['wikipedia.org'], ignoreInlineStyle: ['a', 'b']},
+        {url: ['duckduckgo.com'], ignoreImageAnalysis: ['img[alt="Logo"]', 'canvas']},
     ]);
 });
 
diff --git a/tests/utils/text.tests.ts b/tests/utils/text.tests.ts
new file mode 100644
index 00000000..d51cfb34
--- /dev/null
+++ b/tests/utils/text.tests.ts
@@ -0,0 +1,101 @@
+import {formatCSS} from '../../src/utils/text';
+
+test('CSS formatting', () => {
+    expect(formatCSS('div { color: red; }'))
+        .toEqual([
+            'div {',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div { color: red; } .list-item { background: rgb(0, 0, 0); color: white; }'))
+        .toEqual([
+            'div {',
+            '    color: red;',
+            '}',
+            '.list-item {',
+            '    background: rgb(0, 0, 0);',
+            '    color: white;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media screen { div { color: red; } span { color: red; } } @media all { p { color: green; } }'))
+        .toEqual([
+            '@media screen {',
+            '    div {',
+            '        color: red;',
+            '    }',
+            '    span {',
+            '        color: red;',
+            '    }',
+            '}',
+            '@media all {',
+            '    p {',
+            '        color: green;',
+            '    }',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div, span { background: green; color: red; }'))
+        .toEqual([
+            'div,',
+            'span {',
+            '    background: green;',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media print, screen and (min-width: 20rem) { div, span { background: green; color: red; } }'))
+        .toEqual([
+            '@media print,',
+            'screen and (min-width: 20rem) {',
+            '    div,',
+            '    span {',
+            '        background: green;',
+            '        color: red;',
+            '    }',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('.icon { background-image: url(data:image/gif;base64,XYZ); }'))
+        .toEqual([
+            '.icon {',
+            '    background-image: url(data:image/gif;base64,XYZ);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('img[src*="a,b;c"] { filter: invert(1); }'))
+        .toEqual([
+            'img[src*="a,b;c"] {',
+            '    filter: invert(1);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('img[src*="a,b;c"] {\n    filter: invert(1); \n}'))
+        .toEqual([
+            'img[src*="a,b;c"] {',
+            '    filter: invert(1);',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('div { } span { color: red; } button { }'))
+        .toEqual([
+            'span {',
+            '    color: red;',
+            '}',
+        ].join('\n'));
+
+    expect(formatCSS('@media all { div { } span { color: red; } } @media all { div { } } @media all { button { color: green; } }'))
+        .toEqual([
+            '@media all {',
+            '    span {',
+            '        color: red;',
+            '    }',
+            '}',
+            '@media all {',
+            '    button {',
+            '        color: green;',
+            '    }',
+            '}',
+        ].join('\n'));
+});