Repository: darkreader/darkreader
Pull request #2369 (2020-04-24 19:39:43)

google apps icon invert fix
fix excluding classes and independent from language
![image](https://user-images.githubusercontent.com/56877029/80250403-10898e00-8674-11ea-8d7e-1f7f3d356d86.png)


Myshor: Checked for PL and EN.
@Gusted, @alexanderby can you check if it works in your languages too? ;)
Gusted: NL works

Patch:
diff --git a/src/background/user-storage.ts b/src/background/user-storage.ts
index 17c3fd9d..e2fdfacb 100644
--- a/src/background/user-storage.ts
+++ b/src/background/user-storage.ts
@@ -40,6 +40,7 @@ export default class UserStorage {
                 longitude: null,
             },
             previewNewDesign: false,
+            enableForPDF: true,
         };
         this.settings = null;
     }
diff --git a/src/background/utils/extension-api.ts b/src/background/utils/extension-api.ts
index e97a0d5f..10e0dc75 100644
--- a/src/background/utils/extension-api.ts
+++ b/src/background/utils/extension-api.ts
@@ -13,6 +13,7 @@ export function canInjectScript(url: string) {
             && !url.startsWith('moz')
             && !url.startsWith('view-source:')
             && !url.startsWith('https://addons.mozilla.org')
+            && !url.endsWith('.pdf')
         );
     }
     if (isEdge()) {
diff --git a/src/config/dynamic-theme-fixes.config b/src/config/dynamic-theme-fixes.config
index 36556ea1..14d9e017 100644
--- a/src/config/dynamic-theme-fixes.config
+++ b/src/config/dynamic-theme-fixes.config
@@ -2,6 +2,7 @@
 
 INVERT
 .jfk-bubble
+embed[type="application/pdf"]
 
 CSS
 .vimvixen-hint {
@@ -1223,7 +1224,7 @@ a.gb_xc
 .i4ewOd-LQLjdd li::before
 .un1lmc-j4gsHd
 .maps-sprite-settings-languages
-.gb_D.gb_tc
+a[href*="about/products"]
 
 ================================
 
@@ -2211,7 +2212,7 @@ INVERT
 .bUWb7c
 .WF1WQd
 img[src="https://www.gstatic.com/android/market_images/web/play_prism_hlock_2x.png"]
-.gb_D.gb_tc
+a[href*="about/products"]
 
 ================================
 
@@ -2230,7 +2231,7 @@ plus.google.com
 
 INVERT
 a.gb_b > div
-.gb_D.gb_tc
+a[href*="about/products"]
 
 ================================
 
@@ -3470,7 +3471,7 @@ INVERT
 .gb_0b
 #dictionary-modules img[src*="png"]
 a.gb_b > div
-.gb_D.gb_tc
+a[href*="about/products"]
 
 CSS
 .RNNXgb {
diff --git a/src/definitions.d.ts b/src/definitions.d.ts
index 27abaa5d..2a630812 100644
--- a/src/definitions.d.ts
+++ b/src/definitions.d.ts
@@ -70,6 +70,7 @@ export interface UserSettings {
     time: TimeSettings;
     location: LocationSettings;
     previewNewDesign: boolean;
+    enableForPDF: boolean;
 }
 
 export interface TimeSettings {
diff --git a/src/ui/controls/dropdown/index.tsx b/src/ui/controls/dropdown/index.tsx
index d4919476..eae08b11 100644
--- a/src/ui/controls/dropdown/index.tsx
+++ b/src/ui/controls/dropdown/index.tsx
@@ -2,6 +2,7 @@ import {m} from 'malevic';
 import {getContext} from 'malevic/dom';
 
 interface DropDownProps {
+    class?: string;
     selected: string;
     values: string[];
     onChange: (value: string) => void;
@@ -54,6 +55,7 @@ export default function DropDown(props: DropDownProps) {
                 class={{
                     'dropdown__list__item': true,
                     'dropdown__list__item--selected': value === props.selected,
+                    [props.class]: props.class != null,
                 }}
                 onclick={() => {
                     store.isOpen = false;
@@ -71,6 +73,7 @@ export default function DropDown(props: DropDownProps) {
             class={{
                 'dropdown': true,
                 'dropdown--open': store.isOpen,
+                [props.class]: Boolean(props.class),
             }}
         >
             <span
diff --git a/src/ui/popup/body/index.tsx b/src/ui/popup/body/index.tsx
index 856ffd89..78f965d0 100644
--- a/src/ui/popup/body/index.tsx
+++ b/src/ui/popup/body/index.tsx
@@ -6,6 +6,7 @@ import {isMobile} from '../../../utils/platform';
 import MainPage from '../main-page';
 import {Page, PageViewer} from '../page-viewer';
 import SettingsPage from '../settings-page';
+import ThemePage from '../theme/page';
 import {ViewProps} from '../types';
 
 function Logo() {
@@ -24,18 +25,23 @@ function Logo() {
 function Pages(props: ViewProps) {
     const context = getContext();
     const store = context.store as {
-        activePage: 'main' | 'settings';
+        activePage: 'main' | 'theme' | 'settings';
     };
     if (store.activePage == null) {
         store.activePage = 'main';
     }
 
+    function onThemeNavClick() {
+        store.activePage = 'theme';
+        context.refresh();
+    }
+
     function onSettingsNavClick() {
         store.activePage = 'settings';
         context.refresh();
     }
 
-    function onSettingsBackClick() {
+    function onBackClick() {
         store.activePage = 'main';
         context.refresh();
     }
@@ -43,16 +49,20 @@ function Pages(props: ViewProps) {
     return (
         <PageViewer
             activePage={store.activePage}
-            onBackButtonClick={onSettingsBackClick}
+            onBackButtonClick={onBackClick}
         >
             <Page id="main">
                 <MainPage
                     {...props}
+                    onThemeNavClick={onThemeNavClick}
                     onSettingsNavClick={onSettingsNavClick}
                 />
             </Page>
+            <Page id="theme">
+                <ThemePage {...props} />
+            </Page>
             <Page id="settings">
-                <SettingsPage {...props} onBackClick={onSettingsBackClick} />
+                <SettingsPage {...props} />
             </Page>
         </PageViewer>
     );
diff --git a/src/ui/popup/body/style.less b/src/ui/popup/body/style.less
index 3011ba7e..0c9b5742 100644
--- a/src/ui/popup/body/style.less
+++ b/src/ui/popup/body/style.less
@@ -23,7 +23,7 @@
     }
 
     .pages-section {
-        height: 20.5rem;
+        height: 22.5rem;
     }
 
     .m-logo {
@@ -39,77 +39,6 @@
         width: @popup-content-width;
     }
 
-    .m-simple-control {
-        align-items: stretch;
-        display: flex;
-        flex-direction: column;
-        width: 100%;
-
-        &__description {
-            font-size: @size-text-small;
-            line-height: @size-text-small-height;
-            text-align: center;
-            -moz-user-select: none;
-            user-select: none;
-        }
-    }
-
-    .m-theme-control {
-        align-items: center;
-        display: flex;
-        flex-direction: row;
-        justify-content: center;
-        min-height: @size-control-inner;
-        width: 100%;
-
-        &__label {
-            font-size: @size-text-normal;
-            line-height: @size-text-normal;
-            text-align: left;
-            -moz-user-select: none;
-            user-select: none;
-            width: 50%;
-        }
-    }
-
-    .m-theme-group {
-        &__presets-wrapper {
-            background-color: @color-control-active;
-            border: @size-border solid @color-border;
-            height: @size-control-inner;
-
-            .dropdown {
-                &__selected {
-                    border-radius: unset;
-                    height: @size-control-inner;
-                    line-height: @size-control-inner;
-                    margin-top: 0;
-                }
-
-                &__list {
-                    border-radius: unset;
-                }
-            }
-        }
-
-        &__controls-wrapper {
-            border-bottom: @size-border solid @color-border;
-            border-left: @size-border solid @color-border;
-            border-right: @size-border solid @color-border;
-            padding: @indent-small;
-        }
-
-        &__description {
-            display: inline-block;
-            font-size: @size-text-small;
-            line-height: @size-text-small-height;
-            text-align: center;
-            -moz-user-select: none;
-            user-select: none;
-            width: 100%;
-        }
-    }
-
     .m-settings-button {
         background-image: @icon-right;
         background-position: center right 0.25rem;
@@ -140,42 +69,6 @@
         }
     }
 
-    .m-check-button {
-        display: inline-block;
-
-        .checkbox {
-            width: calc(100% - 2 * @size-border);
-
-            &__checkmark {
-                background-color: @color-control-active;
-            }
-
-            &__content {
-                align-items: center;
-                display: flex;
-                flex: auto;
-                height: @size-control-inner;
-                justify-content: center;
-                -moz-user-select: none;
-                user-select: none;
-            }
-
-            &__input:checked ~ * {
-                color: @color-control-fore;
-            }
-        }
-
-        &__description {
-            display: inline-block;
-            font-size: @size-text-small;
-            line-height: @size-text-small-height;
-            text-align: center;
-            -moz-user-select: none;
-            user-select: none;
-            width: 100%;
-        }
-    }
-
     .m-help-button {
         align-items: center;
         background-image: @icon-right;
@@ -242,8 +135,6 @@
         }
     }
 
-    .m-devtools-description,
-    .m-help-description,
     .m-donate-description {
         display: inline-block;
         font-size: @size-text-small;
@@ -281,7 +172,11 @@
 .preview {
     @import "../control-group/style.less";
     @import "../check-button/style.less";
+    @import "../main-page/style.less";
     @import "../page-viewer/style.less";
+    @import "../theme/controls/theme-control.less";
+    @import "../theme/page/style.less";
+    @import "../theme/preset-picker/style.less";
 }
 
 .mobile.firefox {
diff --git a/src/ui/popup/main-page/app-switch.tsx b/src/ui/popup/main-page/app-switch.tsx
new file mode 100644
index 00000000..91e7cb85
--- /dev/null
+++ b/src/ui/popup/main-page/app-switch.tsx
@@ -0,0 +1,60 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../utils/locales';
+import {MultiSwitch} from '../../controls';
+import ControlGroup from '../control-group';
+import {ViewProps} from '../types';
+
+export default function AppSwitch(props: ViewProps) {
+    const isOn = props.data.settings.enabled === true && !props.data.settings.automation;
+    const isOff = props.data.settings.enabled === false && !props.data.settings.automation;
+
+    // TODO: Replace messages with some IDs.
+    const values = [
+        getLocalMessage('on'),
+        'Auto',
+        getLocalMessage('off'),
+    ];
+    const value = isOn ? values[0] : isOff ? values[2] : values[1];
+
+    function onSwitchChange(v: string) {
+        const index = values.indexOf(v);
+        if (index === 0) {
+            props.actions.changeSettings({
+                enabled: true,
+                automation: '',
+            });
+        } else if (index === 2) {
+            props.actions.changeSettings({
+                enabled: false,
+                automation: '',
+            });
+        } else if (index === 1) {
+            props.actions.changeSettings({
+                automation: 'system',
+            });
+        }
+    }
+
+    const description = isOn ?
+        'Extension is enabled' :
+        isOff ?
+            'Extension is disabled' :
+            // TODO: More messages (location, time etc).
+            'Switches according to system dark mode';
+
+    return (
+        <ControlGroup class="app-switch">
+            <ControlGroup.Control>
+                <MultiSwitch
+                    class="app-switch__control"
+                    options={values}
+                    value={value}
+                    onChange={onSwitchChange}
+                />
+            </ControlGroup.Control>
+            <ControlGroup.Description>
+                {description}
+            </ControlGroup.Description>
+        </ControlGroup>
+    );
+}
diff --git a/src/ui/popup/main-page/index.tsx b/src/ui/popup/main-page/index.tsx
index b81a257b..99d602d9 100644
--- a/src/ui/popup/main-page/index.tsx
+++ b/src/ui/popup/main-page/index.tsx
@@ -1,278 +1,19 @@
 import {m} from 'malevic';
-import {Theme} from '../../../definitions';
-import ThemeEngines from '../../../generators/theme-engines';
-import {getLocalMessage} from '../../../utils/locales';
-import {isURLEnabled, isURLInList, getURLHost} from '../../../utils/url';
-import {Button, DropDown, MultiSwitch, Slider} from '../../controls';
-import SiteToggle from '../components/site-toggle';
+import {Button} from '../../controls';
 import {ViewProps} from '../types';
-
-function SimpleControl(props: {class?: string}, control: Malevic.Spec, description: string) {
-    return (
-        <div class={['m-simple-control', props.class]}>
-            {control}
-            <label class="m-simple-control__description">
-                {description}
-            </label>
-        </div>
-    );
-}
-
-function ThemeControl(props: {class?: string; label: string}, control: Malevic.Spec) {
-    return (
-        <div class={['m-theme-control', props.class]}>
-            <label class="m-theme-control__label">
-                {props.label}
-            </label>
-            {control}
-        </div>
-    );
-}
-
-interface AppSwitchOptions {
-    isOn: boolean;
-    isOff: boolean;
-    isAuto: boolean;
-}
-
-function AppSwitch(props: AppSwitchOptions & {onChange: (opt: AppSwitchOptions) => void}) {
-    const values = [
-        getLocalMessage('on'),
-        'Auto',
-        getLocalMessage('off'),
-    ];
-    const value = props.isOn ? values[0] : props.isOff ? values[2] : values[1];
-
-    function onSwitchChange(v: string) {
-        const index = values.indexOf(v);
-        props.onChange({
-            isOn: index === 0,
-            isOff: index === 2,
-            isAuto: index === 1,
-        });
-    }
-
-    return (
-        <SimpleControl>
-            <MultiSwitch
-                class="m-app-switch__control"
-                options={values}
-                value={value}
-                onChange={onSwitchChange}
-            />
-            {props.isOn ?
-                'Extension is enabled' :
-                props.isOff ?
-                    'Extension is disabled' :
-                    'Switches according to system dark mode'}
-        </SimpleControl>
-    );
-}
-
-function SiteToggleGroup(props: ViewProps) {
-    const isSiteEnabled = isURLEnabled(props.tab.url, props.data.settings, props.tab);
-    return (
-        <SimpleControl>
-            <SiteToggle
-                {...props}
-            />
-            {isSiteEnabled ?
-                'Enabled for current website' :
-                'Disabled for current website'}
-        </SimpleControl>
-    );
-}
+import AppSwitch from './app-switch';
+import SiteToggleGroup from './site-toggle';
+import ThemeGroup from './theme-group';
 
 function SwitchGroup(props: ViewProps) {
-    function onAppSwitchChange(opt: AppSwitchOptions) {
-        if (opt.isOn) {
-            props.actions.changeSettings({
-                enabled: true,
-                automation: '',
-            });
-        } else if (opt.isOff) {
-            props.actions.changeSettings({
-                enabled: false,
-                automation: '',
-            });
-        } else if (opt.isAuto) {
-            props.actions.changeSettings({
-                automation: 'system',
-            });
-        }
-    }
-
     return (
         <Array>
-            <AppSwitch
-                isOn={props.data.settings.enabled === true && !props.data.settings.automation}
-                isOff={props.data.settings.enabled === false && !props.data.settings.automation}
-                isAuto={Boolean(props.data.settings.automation)}
-                onChange={onAppSwitchChange}
-            />
-            <SiteToggleGroup
-                {...props}
-            />
+            <AppSwitch {...props} />
+            <SiteToggleGroup {...props} />
         </Array>
     );
 }
 
-function formatPercent(v: number) {
-    return `${v}%`;
-}
-
-function Brightness(props: {value: number; onChange: (v: number) => void}) {
-    return (
-        <ThemeControl label={getLocalMessage('brightness')}>
-            <Slider
-                value={props.value}
-                min={50}
-                max={150}
-                step={1}
-                formatValue={formatPercent}
-                onChange={props.onChange}
-            />
-        </ThemeControl>
-    );
-}
-
-function Contrast(props: {value: number; onChange: (v: number) => void}) {
-    return (
-        <ThemeControl label={getLocalMessage('contrast')}>
-            <Slider
-                value={props.value}
-                min={50}
-                max={150}
-                step={1}
-                formatValue={formatPercent}
-                onChange={props.onChange}
-            />
-        </ThemeControl>
-    );
-}
-
-function Scheme(props: {isDark: boolean; onChange: (dark: boolean) => void}) {
-    const valDark = getLocalMessage('Dark');
-    const valLight = getLocalMessage('Light');
-    return (
-        <ThemeControl label="Scheme">
-            <DropDown
-                selected={props.isDark ? valDark : valLight}
-                values={[
-                    valDark,
-                    valLight,
-                ]}
-                onChange={(v) => props.onChange(v === valDark)}
-            />
-        </ThemeControl>
-    );
-}
-
-function Mode(props: {mode: string; onChange: (mode: string) => void}) {
-    const modes = [
-        [ThemeEngines.dynamicTheme, getLocalMessage('engine_dynamic')],
-        [ThemeEngines.cssFilter, getLocalMessage('engine_filter')],
-        [ThemeEngines.staticTheme, getLocalMessage('engine_static')],
-    ];
-    return (
-        <ThemeControl label="Mode">
-            <DropDown
-                selected={modes.find((m) => m[0] === props.mode)[1]}
-                values={modes.map((m) => m[1])}
-                onChange={(v) => {
-                    const mode = modes.find((m) => m[1] === v)[0];
-                    props.onChange(mode);
-                }}
-            />
-        </ThemeControl>
-    );
-}
-
-function ThemeControls(props: {theme: Theme; onChange: (theme: Partial<Theme>) => void}) {
-    const {theme, onChange} = props;
-    return (
-        <section class="m-section m-theme-controls">
-            <Brightness
-                value={theme.brightness}
-                onChange={(v) => onChange({brightness: v})}
-            />
-            <Contrast
-                value={theme.contrast}
-                onChange={(v) => onChange({contrast: v})}
-            />
-            <Scheme
-                isDark={theme.mode === 1}
-                onChange={(isDark) => onChange({mode: isDark ? 1 : 0})}
-            />
-            <Mode
-                mode={theme.engine}
-                onChange={(mode) => onChange({engine: mode})}
-            />
-        </section>
-    );
-}
-
-function ThemeGroup(props: ViewProps) {
-    const host = getURLHost(props.tab.url || '');
-    const custom = props.data.settings.customThemes.find(
-        ({url}) => isURLInList(props.tab.url, url)
-    );
-    const theme = custom ?
-        custom.theme :
-        props.data.settings.theme;
-
-    function setTheme(config: Partial<Theme>) {
-        if (custom) {
-            custom.theme = {...custom.theme, ...config};
-            props.actions.changeSettings({
-                customThemes: props.data.settings.customThemes,
-            });
-        } else {
-            props.actions.setTheme(config);
-        }
-    }
-
-    const defaultPresetName = 'Default theme';
-    const customPresetName = `Custom for ${host}`;
-
-    function onPresetChange(name: string) {
-        const filteredCustomThemes = props.data.settings.customThemes.filter(({url}) => !isURLInList(props.tab.url, url));
-        if (name === defaultPresetName) {
-            props.actions.changeSettings({customThemes: filteredCustomThemes});
-        } else if (name === customPresetName) {
-            const extended = filteredCustomThemes.concat({
-                url: [host],
-                theme: {...props.data.settings.theme},
-            });
-            props.actions.changeSettings({customThemes: extended});
-        }
-    }
-
-    return (
-        <div class="m-theme-group">
-            <div class="m-theme-group__presets-wrapper">
-                <DropDown
-                    selected={custom ? customPresetName : defaultPresetName}
-                    values={[
-                        defaultPresetName,
-                        customPresetName,
-                    ]}
-                    onChange={onPresetChange}
-                />
-            </div>
-            <div class="m-theme-group__controls-wrapper">
-                <ThemeControls
-                    theme={theme}
-                    onChange={setTheme}
-                />
-            </div>
-            <label class="m-theme-group__description">
-                Configure theme
-            </label>
-        </div>
-    );
-}
-
 function SettingsNavButton(props: {onClick: () => void}) {
     return (
         <Button class="m-settings-button" onclick={props.onClick}>
@@ -284,7 +25,12 @@ function SettingsNavButton(props: {onClick: () => void}) {
     );
 }
 
-export default function MainPage(props: ViewProps & {onSettingsNavClick: () => void}) {
+type MainPageProps = ViewProps & {
+    onThemeNavClick: () => void;
+    onSettingsNavClick: () => void;
+};
+
+export default function MainPage(props: MainPageProps) {
     return (
         <Array>
             <section class="m-section">
diff --git a/src/ui/popup/main-page/site-toggle.tsx b/src/ui/popup/main-page/site-toggle.tsx
new file mode 100644
index 00000000..3ad81577
--- /dev/null
+++ b/src/ui/popup/main-page/site-toggle.tsx
@@ -0,0 +1,21 @@
+import {m} from 'malevic';
+import {isURLEnabled} from '../../../utils/url';
+import SiteToggle from '../components/site-toggle';
+import ControlGroup from '../control-group';
+import {ViewProps} from '../types';
+
+export default function SiteToggleGroup(props: ViewProps) {
+    const isSiteEnabled = isURLEnabled(props.tab.url, props.data.settings, props.tab);
+    return (
+        <ControlGroup class="site-toggle-group">
+            <ControlGroup.Control class="site-toggle-group__control">
+                <SiteToggle {...props} />
+            </ControlGroup.Control>
+            <ControlGroup.Description>
+                {isSiteEnabled ?
+                    'Enabled for current website' :
+                    'Disabled for current website'}
+            </ControlGroup.Description>
+        </ControlGroup>
+    );
+}
diff --git a/src/ui/popup/main-page/style.less b/src/ui/popup/main-page/style.less
new file mode 100644
index 00000000..e9cfba89
--- /dev/null
+++ b/src/ui/popup/main-page/style.less
@@ -0,0 +1,52 @@
+@import "../../theme.less";
+
+.app-switch {
+    &__control {
+        width: calc(100% - @size-border * 2);
+    }
+}
+
+.site-toggle-group {
+    .site-toggle {
+        width: calc(100% - @size-border * 2);
+    }
+}
+
+.theme-group {
+    &__presets-wrapper {
+        background-color: @color-control-active;
+        border: @size-border solid @color-border;
+        height: @size-control-inner;
+    }
+
+    &__controls-wrapper {
+        border-bottom: @size-border solid @color-border;
+        border-left: @size-border solid @color-border;
+        border-right: @size-border solid @color-border;
+        padding: @indent-small;
+    }
+
+    &__more-button {
+        background-color: @color-control-active;
+        background-image: @icon-right;
+        background-position: center right 0.25rem;
+        background-repeat: no-repeat;
+        background-size: 1rem;
+        border: none;
+        border-radius: @size-control-inner / 2;
+        height: @size-control-inner - 0.25rem;
+        margin-bottom: 0.125rem;
+        margin-top: @indent-small + 0.125rem;
+        width: 100%;
+    }
+
+    &__description {
+        display: inline-block;
+        font-size: @size-text-small;
+        line-height: @size-text-small-height;
+        text-align: center;
+        -moz-user-select: none;
+        user-select: none;
+        width: 100%;
+    }
+}
diff --git a/src/ui/popup/main-page/theme-group.tsx b/src/ui/popup/main-page/theme-group.tsx
new file mode 100644
index 00000000..a2cc59c4
--- /dev/null
+++ b/src/ui/popup/main-page/theme-group.tsx
@@ -0,0 +1,57 @@
+import {m} from 'malevic';
+import {Theme} from '../../../definitions';
+import {Button} from '../../controls';
+import {Brightness, Contrast, Scheme, Mode} from '../theme/controls';
+import ThemePresetPicker from '../theme/preset-picker';
+import {getCurrentThemePreset} from '../theme/utils';
+import {ViewProps} from '../types';
+
+function ThemeControls(props: {theme: Theme; onChange: (theme: Partial<Theme>) => void}) {
+    const {theme, onChange} = props;
+    return (
+        <section class="m-section m-theme-controls">
+            <Brightness
+                value={theme.brightness}
+                onChange={(v) => onChange({brightness: v})}
+            />
+            <Contrast
+                value={theme.contrast}
+                onChange={(v) => onChange({contrast: v})}
+            />
+            <Scheme
+                isDark={theme.mode === 1}
+                onChange={(isDark) => onChange({mode: isDark ? 1 : 0})}
+            />
+            <Mode
+                mode={theme.engine}
+                onChange={(mode) => onChange({engine: mode})}
+            />
+        </section>
+    );
+}
+
+export default function ThemeGroup(props: ViewProps & {onThemeNavClick: () => void}) {
+    const preset = getCurrentThemePreset(props);
+
+    return (
+        <div class="theme-group">
+            <div class="theme-group__presets-wrapper">
+                <ThemePresetPicker {...props} />
+            </div>
+            <div class="theme-group__controls-wrapper">
+                <ThemeControls
+                    theme={preset.theme}
+                    onChange={preset.change}
+                />
+                <Button class="theme-group__more-button" onclick={props.onThemeNavClick}>
+                    See all options
+                </Button>
+            </div>
+            <label class="theme-group__description">
+                Configure theme
+            </label>
+        </div>
+    );
+}
+
+
diff --git a/src/ui/popup/settings-page/devtools.tsx b/src/ui/popup/settings-page/devtools.tsx
index b3fe56ce..0b3d9c9a 100644
--- a/src/ui/popup/settings-page/devtools.tsx
+++ b/src/ui/popup/settings-page/devtools.tsx
@@ -5,16 +5,38 @@ import {Button} from '../../controls';
 import ControlGroup from '../control-group';
 import {ViewProps} from '../types';
 
-function openDevTools() {
-    // TODO: Switch to already opened window.
-    chrome.windows.create({
-        type: 'panel',
-        url: 'ui/devtools/index.html',
-        width: 600,
-        height: 600,
+
+function getExistingDevToolsWindow() {
+    return new Promise<chrome.windows.Window>((resolve) => {
+        chrome.windows.getAll({
+            populate: true,
+            windowTypes: ['popup']
+        }, (w) => {
+            for (const window of w) {
+                if (window.tabs[0].url.endsWith('ui/devtools/index.html')) {
+                    resolve(window);
+                    return;
+                }
+            }
+            resolve(null);
+        });
     });
 }
 
+async function openDevTools() {
+    const devToolsWindow = await getExistingDevToolsWindow();
+    if (devToolsWindow) {
+        chrome.windows.update(devToolsWindow.id, {'focused': true});
+    } else {
+        chrome.windows.create({
+            type: 'popup',
+            url: 'ui/devtools/index.html',
+            width: 600,
+            height: 600,
+        });
+    }
+}
+
 export default function DevToolsGroup(props: ViewProps) {
     const globalThemeEngine = props.data.settings.theme.engine;
     const devtoolsData = props.data.devtools;
diff --git a/src/ui/popup/settings-page/index.tsx b/src/ui/popup/settings-page/index.tsx
index 7951caf4..7c53bd49 100644
--- a/src/ui/popup/settings-page/index.tsx
+++ b/src/ui/popup/settings-page/index.tsx
@@ -3,11 +3,13 @@ import {ViewProps} from '../types';
 import DevToolsGroup from './devtools';
 import EnabledByDefaultGroup from './enabled-by-default';
 import HelpGroup from './help';
+import InvertPDF from './invert-pdf';
 
-export default function SettingsPage(props: ViewProps & {onBackClick: () => void}) {
+export default function SettingsPage(props: ViewProps) {
     return (
         <section class="m-section">
             <EnabledByDefaultGroup {...props} />
+            <InvertPDF {...props} />
             <DevToolsGroup {...props} />
             <HelpGroup />
         </section>
diff --git a/src/ui/popup/settings-page/invert-pdf.tsx b/src/ui/popup/settings-page/invert-pdf.tsx
new file mode 100644
index 00000000..66b3407f
--- /dev/null
+++ b/src/ui/popup/settings-page/invert-pdf.tsx
@@ -0,0 +1,20 @@
+import {m} from 'malevic';
+import {ViewProps} from '../types';
+import CheckButton from '../check-button';
+
+export default function InvertPDF(props: ViewProps) {
+    function onInvertPDFChange(checked: boolean) {
+        props.actions.changeSettings({enableForPDF: checked});
+    }
+
+    return (
+        <CheckButton
+            checked={props.data.settings.enableForPDF}
+            label='Enable for PDF files'
+            description={props.data.settings.enableForPDF ?
+                'Enabled for PDF documents' :
+                'Disabled for PDF documents'}
+            onChange={onInvertPDFChange}
+        />
+    );
+}
diff --git a/src/ui/popup/theme/controls/brightness.tsx b/src/ui/popup/theme/controls/brightness.tsx
new file mode 100644
index 00000000..d5642118
--- /dev/null
+++ b/src/ui/popup/theme/controls/brightness.tsx
@@ -0,0 +1,20 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../../utils/locales';
+import {Slider} from '../../../controls';
+import {formatPercent} from './format';
+import ThemeControl from './theme-control';
+
+export default function Brightness(props: {value: number; onChange: (v: number) => void}) {
+    return (
+        <ThemeControl label={getLocalMessage('brightness')}>
+            <Slider
+                value={props.value}
+                min={50}
+                max={150}
+                step={1}
+                formatValue={formatPercent}
+                onChange={props.onChange}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/contrast.tsx b/src/ui/popup/theme/controls/contrast.tsx
new file mode 100644
index 00000000..b03e83b0
--- /dev/null
+++ b/src/ui/popup/theme/controls/contrast.tsx
@@ -0,0 +1,20 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../../utils/locales';
+import {Slider} from '../../../controls';
+import {formatPercent} from './format';
+import ThemeControl from './theme-control';
+
+export default function Contrast(props: {value: number; onChange: (v: number) => void}) {
+    return (
+        <ThemeControl label={getLocalMessage('contrast')}>
+            <Slider
+                value={props.value}
+                min={50}
+                max={150}
+                step={1}
+                formatValue={formatPercent}
+                onChange={props.onChange}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/format.ts b/src/ui/popup/theme/controls/format.ts
new file mode 100644
index 00000000..c676f2b3
--- /dev/null
+++ b/src/ui/popup/theme/controls/format.ts
@@ -0,0 +1,3 @@
+export function formatPercent(v: number) {
+    return `${v}%`;
+}
diff --git a/src/ui/popup/theme/controls/grayscale.tsx b/src/ui/popup/theme/controls/grayscale.tsx
new file mode 100644
index 00000000..5b7bd28c
--- /dev/null
+++ b/src/ui/popup/theme/controls/grayscale.tsx
@@ -0,0 +1,20 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../../utils/locales';
+import {Slider} from '../../../controls';
+import {formatPercent} from './format';
+import ThemeControl from './theme-control';
+
+export default function Grayscale(props: {value: number; onChange: (v: number) => void}) {
+    return (
+        <ThemeControl label={getLocalMessage('grayscale')}>
+            <Slider
+                value={props.value}
+                min={0}
+                max={100}
+                step={1}
+                formatValue={formatPercent}
+                onChange={props.onChange}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/index.tsx b/src/ui/popup/theme/controls/index.tsx
new file mode 100644
index 00000000..10fb8318
--- /dev/null
+++ b/src/ui/popup/theme/controls/index.tsx
@@ -0,0 +1,15 @@
+import Brightness from './brightness';
+import Contrast from './contrast';
+import Grayscale from './grayscale';
+import Mode from './mode';
+import Scheme from './scheme';
+import Sepia from './sepia';
+
+export {
+    Brightness,
+    Contrast,
+    Grayscale,
+    Mode,
+    Scheme,
+    Sepia,
+};
diff --git a/src/ui/popup/theme/controls/mode.tsx b/src/ui/popup/theme/controls/mode.tsx
new file mode 100644
index 00000000..55fe7b0f
--- /dev/null
+++ b/src/ui/popup/theme/controls/mode.tsx
@@ -0,0 +1,25 @@
+import {m} from 'malevic';
+import ThemeEngines from '../../../../generators/theme-engines';
+import {getLocalMessage} from '../../../../utils/locales';
+import {DropDown} from '../../../controls';
+import ThemeControl from './theme-control';
+
+export default function Mode(props: {mode: string; onChange: (mode: string) => void}) {
+    const modes = [
+        [ThemeEngines.dynamicTheme, getLocalMessage('engine_dynamic')],
+        [ThemeEngines.cssFilter, getLocalMessage('engine_filter')],
+        [ThemeEngines.staticTheme, getLocalMessage('engine_static')],
+    ];
+    return (
+        <ThemeControl label="Mode">
+            <DropDown
+                selected={modes.find((m) => m[0] === props.mode)[1]}
+                values={modes.map((m) => m[1])}
+                onChange={(v) => {
+                    const mode = modes.find((m) => m[1] === v)[0];
+                    props.onChange(mode);
+                }}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/scheme.tsx b/src/ui/popup/theme/controls/scheme.tsx
new file mode 100644
index 00000000..be06dd0e
--- /dev/null
+++ b/src/ui/popup/theme/controls/scheme.tsx
@@ -0,0 +1,21 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../../utils/locales';
+import {DropDown} from '../../../controls';
+import ThemeControl from './theme-control';
+
+export default function Scheme(props: {isDark: boolean; onChange: (dark: boolean) => void}) {
+    const valDark = getLocalMessage('dark');
+    const valLight = getLocalMessage('light');
+    return (
+        <ThemeControl label="Scheme">
+            <DropDown
+                selected={props.isDark ? valDark : valLight}
+                values={[
+                    valDark,
+                    valLight,
+                ]}
+                onChange={(v) => props.onChange(v === valDark)}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/sepia.tsx b/src/ui/popup/theme/controls/sepia.tsx
new file mode 100644
index 00000000..9fa1406f
--- /dev/null
+++ b/src/ui/popup/theme/controls/sepia.tsx
@@ -0,0 +1,20 @@
+import {m} from 'malevic';
+import {getLocalMessage} from '../../../../utils/locales';
+import {Slider} from '../../../controls';
+import {formatPercent} from './format';
+import ThemeControl from './theme-control';
+
+export default function Sepia(props: {value: number; onChange: (v: number) => void}) {
+    return (
+        <ThemeControl label={getLocalMessage('sepia')}>
+            <Slider
+                value={props.value}
+                min={0}
+                max={100}
+                step={1}
+                formatValue={formatPercent}
+                onChange={props.onChange}
+            />
+        </ThemeControl>
+    );
+}
diff --git a/src/ui/popup/theme/controls/theme-control.less b/src/ui/popup/theme/controls/theme-control.less
new file mode 100644
index 00000000..581f49c0
--- /dev/null
+++ b/src/ui/popup/theme/controls/theme-control.less
@@ -0,0 +1,17 @@
+.theme-control {
+    align-items: center;
+    display: flex;
+    flex-direction: row;
+    justify-content: center;
+    min-height: @size-control-inner;
+    width: 100%;
+
+    &__label {
+        font-size: @size-text-normal;
+        line-height: @size-text-normal;
+        text-align: left;
+        -moz-user-select: none;
+        user-select: none;
+        width: 50%;
+    }
+}
diff --git a/src/ui/popup/theme/controls/theme-control.tsx b/src/ui/popup/theme/controls/theme-control.tsx
new file mode 100644
index 00000000..64f5a0d7
--- /dev/null
+++ b/src/ui/popup/theme/controls/theme-control.tsx
@@ -0,0 +1,12 @@
+import {m} from 'malevic';
+
+export default function ThemeControl(props: {label: Malevic.Child}, control: Malevic.Child) {
+    return (
+        <span class="theme-control">
+            <label class="theme-control__label">
+                {props.label}
+            </label>
+            {control}
+        </span>
+    );
+}
diff --git a/src/ui/popup/theme/page/index.tsx b/src/ui/popup/theme/page/index.tsx
new file mode 100644
index 00000000..94f80ca5
--- /dev/null
+++ b/src/ui/popup/theme/page/index.tsx
@@ -0,0 +1,38 @@
+import {m} from 'malevic';
+import {ViewProps} from '../../types';
+import {Brightness, Contrast, Grayscale, Mode, Scheme, Sepia} from '../controls';
+import ThemePresetPicker from '../preset-picker';
+import {getCurrentThemePreset} from '../utils';
+
+export default function ThemePage(props: ViewProps) {
+    const {theme, change} = getCurrentThemePreset(props);
+
+    return (
+        <section class="m-section theme-page">
+            <ThemePresetPicker {...props} /><Brightness
+                value={theme.brightness}
+                onChange={(v) => change({brightness: v})}
+            />
+            <Contrast
+                value={theme.contrast}
+                onChange={(v) => change({contrast: v})}
+            />
+            <Sepia
+                value={theme.sepia}
+                onChange={(v) => change({sepia: v})}
+            />
+            <Grayscale
+                value={theme.grayscale}
+                onChange={(v) => change({grayscale: v})}
+            />
+            <Scheme
+                isDark={theme.mode === 1}
+                onChange={(isDark) => change({mode: isDark ? 1 : 0})}
+            />
+            <Mode
+                mode={theme.engine}
+                onChange={(mode) => change({engine: mode})}
+            />
+        </section>
+    );
+}
diff --git a/src/ui/popup/theme/page/style.less b/src/ui/popup/theme/page/style.less
new file mode 100644
index 00000000..f0d75890
--- /dev/null
+++ b/src/ui/popup/theme/page/style.less
@@ -0,0 +1,25 @@
+@import "../../../theme.less";
+
+.theme-page {
+    .theme-preset-picker.dropdown {
+        border-radius: unset;
+        height: @size-control-inner + 2 * @size-border;
+    }
+
+    .theme-preset-picker {
+        .dropdown {
+            &__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;
+                }
+            }
+        }
+    }
+}
diff --git a/src/ui/popup/theme/preset-picker/index.tsx b/src/ui/popup/theme/preset-picker/index.tsx
new file mode 100644
index 00000000..cd2f75eb
--- /dev/null
+++ b/src/ui/popup/theme/preset-picker/index.tsx
@@ -0,0 +1,38 @@
+import {m} from 'malevic';
+import {isURLInList, getURLHost} from '../../../../utils/url';
+import {DropDown} from '../../../controls';
+import {ViewProps} from '../../types';
+
+export default function PresetPicker(props: ViewProps) {
+    const host = getURLHost(props.tab.url || '');
+    const custom = props.data.settings.customThemes.find(
+        ({url}) => isURLInList(props.tab.url, url)
+    );
+
+    const defaultPresetName = 'Default theme';
+    const customPresetName = `Custom for ${host}`;
+
+    function onPresetChange(name: string) {
+        const filteredCustomThemes = props.data.settings.customThemes.filter(({url}) => !isURLInList(props.tab.url, url));
+        if (name === defaultPresetName) {
+            props.actions.changeSettings({customThemes: filteredCustomThemes});
+        } else if (name === customPresetName) {
+            const extended = filteredCustomThemes.concat({
+                url: [host],
+                theme: {...props.data.settings.theme},
+            });
+            props.actions.changeSettings({customThemes: extended});
+        }
+    }
+    return (
+        <DropDown
+            class="theme-preset-picker"
+            selected={custom ? customPresetName : defaultPresetName}
+            values={[
+                defaultPresetName,
+                customPresetName,
+            ]}
+            onChange={onPresetChange}
+        />
+    );
+}
diff --git a/src/ui/popup/theme/preset-picker/style.less b/src/ui/popup/theme/preset-picker/style.less
new file mode 100644
index 00000000..ac0d25f1
--- /dev/null
+++ b/src/ui/popup/theme/preset-picker/style.less
@@ -0,0 +1,13 @@
+.theme-preset-picker {
+    .dropdown {
+        &__selected {
+            border-radius: unset;
+            height: @size-control-inner;
+            line-height: @size-control-inner;
+            margin-top: 0;
+        }
+        &__list {
+            border-radius: unset;
+        }
+    }
+}
diff --git a/src/ui/popup/theme/utils.ts b/src/ui/popup/theme/utils.ts
new file mode 100644
index 00000000..e81a6c23
--- /dev/null
+++ b/src/ui/popup/theme/utils.ts
@@ -0,0 +1,28 @@
+import {Theme} from '../../../definitions';
+import {isURLInList} from '../../../utils/url';
+import {ViewProps} from '../types';
+
+export function getCurrentThemePreset(props: ViewProps) {
+    const custom = props.data.settings.customThemes.find(
+        ({url}) => isURLInList(props.tab.url, url)
+    );
+    const theme = custom ?
+        custom.theme :
+        props.data.settings.theme;
+
+    function setTheme(config: Partial<Theme>) {
+        if (custom) {
+            custom.theme = {...custom.theme, ...config};
+            props.actions.changeSettings({
+                customThemes: props.data.settings.customThemes,
+            });
+        } else {
+            props.actions.setTheme(config);
+        }
+    }
+
+    return {
+        theme,
+        change: setTheme,
+    };
+}
\ No newline at end of file
diff --git a/src/utils/url.ts b/src/utils/url.ts
index 5395384a..6a295aac 100644
--- a/src/utils/url.ts
+++ b/src/utils/url.ts
@@ -96,7 +96,36 @@ function createUrlRegex(urlTemplate: string): RegExp {
     return new RegExp(result, 'i');
 }
 
+export function isPDF(url: string) {
+    if (url.includes('.pdf')) {
+        if (url.includes('?')) {
+            url = url.substring(0, url.lastIndexOf('?'));
+        }
+        if (url.includes('#')) {
+            url = url.substring(0, url.lastIndexOf('#'));
+        }
+        if (url.endsWith('.pdf')) {
+            for (let i = url.length; 0 < i; i--) {
+                if (url[i] === '=') {
+                    return false;
+                } else if (url[i] === '/') {
+                    return true;
+                }
+            }
+        } else {
+            return false;
+        }
+    }
+    return false;
+}
+
 export function isURLEnabled(url: string, userSettings: UserSettings, {isProtected, isInDarkList}) {
+    if (isPDF(url) && userSettings.enableForPDF) {
+        return true;
+    }
+    if (isPDF(url) && !userSettings.enableForPDF) {
+        return false;
+    }
     if (isProtected) {
         return false;
     }


Test patch:
diff --git a/tests/url.tests.ts b/tests/url.tests.ts
index ac925681..a0b78348 100644
--- a/tests/url.tests.ts
+++ b/tests/url.tests.ts
@@ -1,4 +1,4 @@
-import {isURLEnabled} from '../src/utils/url';
+import {isURLEnabled, isPDF} from '../src/utils/url';
 import {UserSettings} from '../src/definitions';
 
 test('URL is enabled', () => {
@@ -67,12 +67,12 @@ test('URL is enabled', () => {
         {siteList: ['chrome.google.com'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
         {isProtected: true, isInDarkList: false},
     )).toBe(false);
-        expect(isURLEnabled(
+    expect(isURLEnabled(
         'https://microsoftedge.microsoft.com/addons',
         {siteList: ['microsoftedge.microsoft.com'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
         {isProtected: true, isInDarkList: false},
     )).toBe(false);
-        expect(isURLEnabled(
+    expect(isURLEnabled(
         'https://microsoftedge.microsoft.com/addons',
         {siteList: ['microsoftedge.microsoft.com'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
         {isProtected: true, isInDarkList: false},
@@ -87,6 +87,43 @@ test('URL is enabled', () => {
         {siteList: ['darkreader.org'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
         {isProtected: false, isInDarkList: true},
     )).toBe(true);
+    expect(isURLEnabled(
+        'https://www.google.com/file.pdf',
+        {enableForPDF: true, siteList: ['darkreader.org'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toBe(true);
+    expect(isURLEnabled(
+        'https://www.google.com/file.pdf/resource',
+        {enableForPDF: true, siteList: ['darkreader.org'], siteListEnabled: [], applyToListedOnly: true} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toBe(false);
+    expect(isURLEnabled(
+        'https://www.google.com/file.pdf/resource',
+        {enableForPDF: true, siteList: ['darkreader.org'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toBe(true);
+    expect(isURLEnabled(
+        'https://www.google.com/very/good/hidden/folder/pdf#file.pdf',
+        {enableForPDF: true, siteList: ['https://www.google.com/very/good/hidden/folder/pdf#file.pdf'], siteListEnabled: [], applyToListedOnly: false} as UserSettings,
+        {isProtected: false, isInDarkList: false},
+    )).toBe(false);
+
+    // Test for PDF enabling
+    expect(isPDF(
+        'https://www.google.com/file.pdf'
+    )).toBe(true);
+    expect(isPDF(
+        'https://www.google.com/file.pdf?id=2'
+    )).toBe(true);
+    expect(isPDF(
+        'https://www.google.com/file.pdf/resource'
+    )).toBe(false);
+    expect(isPDF(
+        'https://www.google.com/resource?file=file.pdf'
+    )).toBe(false);
+    expect(isPDF(
+        'https://www.google.com/very/good/hidden/folder/pdf#file.pdf'
+    )).toBe(false);
 
     // Temporary Dark Sites list fix
     expect(isURLEnabled(