├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── _locales │ ├── be.config │ ├── cs.config │ ├── de.config │ ├── el.config │ ├── en.config │ ├── en_GB.config │ ├── en_US.config │ ├── es.config │ ├── fr.config │ ├── hi.config │ ├── id.config │ ├── it.config │ ├── ja.config │ ├── ko.config │ ├── nl.config │ ├── no.config │ ├── pt_BR.config │ ├── pt_PT.config │ ├── ro.config │ ├── ru.config │ ├── th.config │ ├── zh_CN.config │ └── zh_TW.config ├── background │ ├── config-manager.ts │ ├── devtools.ts │ ├── extension.ts │ ├── icon-manager.ts │ ├── index.html │ ├── index.ts │ ├── messenger.ts │ ├── newsmaker.ts │ ├── tab-manager.ts │ ├── user-storage.ts │ ├── utils │ │ ├── extension-api.ts │ │ └── network.ts │ └── window-theme.ts ├── config │ ├── dark-sites.config │ ├── dark_sites.json │ ├── dynamic-theme-fixes.config │ ├── fix_inversion.json │ ├── inversion-fixes.config │ └── static-themes.config ├── definitions.d.ts ├── generators │ ├── css-filter.ts │ ├── dynamic-theme.ts │ ├── modify-colors.ts │ ├── static-theme.ts │ ├── svg-filter.ts │ ├── text-style.ts │ ├── theme-engines.ts │ └── utils │ │ ├── format.ts │ │ ├── matrix.ts │ │ └── parse.ts ├── icons │ ├── dr_128.png │ ├── dr_16.png │ ├── dr_48.png │ ├── dr_active_19.png │ ├── dr_active_38.png │ ├── dr_inactive_19.png │ ├── dr_inactive_38.png │ ├── pumpkin_19.png │ └── pumpkin_38.png ├── inject │ ├── dynamic-theme │ │ ├── css-rules.ts │ │ ├── image.ts │ │ ├── index.ts │ │ ├── inline-style.ts │ │ ├── meta-theme-color.ts │ │ ├── modify-css.ts │ │ ├── network.ts │ │ ├── style-manager.ts │ │ ├── url.ts │ │ └── watch.ts │ ├── index.ts │ ├── style.ts │ ├── svg-filter.ts │ └── utils │ │ ├── dom.ts │ │ ├── log.ts │ │ └── throttle.ts ├── manifest-firefox.json ├── manifest.json ├── tsconfig.json ├── ui │ ├── assets │ │ ├── fonts │ │ │ ├── LICENSE.txt │ │ │ ├── OpenSans-Light.ttf │ │ │ ├── OpenSans-Regular.ttf │ │ │ └── OpenSans-SemiBold.ttf │ │ └── images │ │ │ ├── darkreader-icon-256x256.png │ │ │ ├── darkreader-type.svg │ │ │ ├── ladybug-32.svg │ │ │ ├── mode-dark-32.svg │ │ │ └── mode-light-32.svg │ ├── connect │ │ ├── connector.ts │ │ ├── index.ts │ │ └── mock.ts │ ├── controls │ │ ├── button │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── checkbox │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── index.ts │ │ ├── multi-switch │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── select │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── shortcut │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── style.less │ │ ├── tab-panel │ │ │ ├── index.tsx │ │ │ ├── style.less │ │ │ └── tab.tsx │ │ ├── text-list │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── textbox │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── time-range-picker │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── toggle │ │ │ ├── index.tsx │ │ │ └── style.less │ │ ├── updown │ │ │ ├── index.tsx │ │ │ ├── style.less │ │ │ └── track.tsx │ │ ├── utils.ts │ │ └── virtual-scroll │ │ │ └── index.tsx │ ├── devtools │ │ ├── components │ │ │ └── body.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ └── style.less │ ├── popup │ │ ├── compatibility.js │ │ ├── components │ │ │ ├── body.tsx │ │ │ ├── custom-settings-toggle │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── engine-switch │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── filter-settings │ │ │ │ ├── index.tsx │ │ │ │ ├── mode-toggle.tsx │ │ │ │ └── style.less │ │ │ ├── font-settings │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── header │ │ │ │ ├── index.tsx │ │ │ │ ├── more-toggle-settings.tsx │ │ │ │ ├── style.less │ │ │ │ └── watch-icon.tsx │ │ │ ├── loader │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── more-settings │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── news │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ ├── site-list-settings │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ │ └── site-toggle │ │ │ │ ├── checkmark-icon.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── style.less │ │ ├── index.html │ │ ├── index.tsx │ │ ├── style.less │ │ └── utils │ │ │ ├── issues.ts │ │ │ └── markdown.tsx │ ├── shared.less │ ├── stylesheet-editor │ │ ├── components │ │ │ └── body.tsx │ │ ├── index.html │ │ ├── index.tsx │ │ └── style.less │ └── theme.less └── utils │ ├── color.ts │ ├── links.ts │ ├── locales.ts │ ├── math.ts │ ├── network.ts │ ├── platform.ts │ ├── text.ts │ ├── time.ts │ └── url.ts ├── tasks ├── bundle-css.js ├── bundle-html.js ├── bundle-js.js ├── bundle-locales.js ├── clean.js ├── copy.js ├── debug.js ├── foxify.js ├── paths.js ├── release.js ├── reload.js ├── utils.js ├── watch.js └── zip.js └── tests ├── color.tests.ts ├── config.tests.ts ├── jest.config.js ├── locales.tests.ts ├── time.tests.ts ├── tsconfig.json └── url.tests.ts /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.* text eol=lf 4 | src/config/*.json text eol=crlf 5 | 6 | *.png binary 7 | *.ttf binary 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: darkreader 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with a single custom sponsorship URL 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | #----------------------------------- 3 | build 4 | debug 5 | build.zip 6 | build-firefox 7 | debug-firefox 8 | build-firefox.xpi 9 | 10 | # Node.js modules 11 | #----------------------------------- 12 | node_modules 13 | 14 | # IDE and editor configs 15 | #----------------------------------- 16 | .idea 17 | .vscode 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "lts/*" 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Shutov 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkreader", 3 | "version": "4.7.12", 4 | "description": "Dark theme for every website", 5 | "scripts": { 6 | "debug": "node tasks/debug.js", 7 | "release": "npm test && node tasks/release.js", 8 | "test": "jest --config=tests/jest.config.js" 9 | }, 10 | "private": true, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/alexanderby/darkreader.git" 14 | }, 15 | "author": "Alexander Shutov (http://shutov.by/)", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/alexanderby/darkreader/issues" 19 | }, 20 | "homepage": "https://darkreader.org/", 21 | "dependencies": { 22 | "malevic": "0.12.2" 23 | }, 24 | "devDependencies": { 25 | "@types/chrome": "0.0.81", 26 | "@types/jest": "24.0.9", 27 | "@types/node": "11.10.5", 28 | "chokidar": "2.1.2", 29 | "fs-extra": "7.0.1", 30 | "globby": "9.1.0", 31 | "jest": "24.3.1", 32 | "less": "3.9.0", 33 | "rollup": "1.6.0", 34 | "rollup-plugin-commonjs": "9.2.1", 35 | "rollup-plugin-node-resolve": "4.0.1", 36 | "rollup-plugin-replace": "2.1.0", 37 | "rollup-plugin-typescript2": "0.19.3", 38 | "ts-jest": "24.0.0", 39 | "ts-node": "8.0.3", 40 | "typescript": "3.3.3333", 41 | "yazl": "2.5.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/_locales/be.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Цёмная Тэма для кожнага сайта. Беражыце свае вочы, карыстайцеся Дарк Рыдарам для начнога ці штодзённага прагляду вэб-старонак. 3 | 4 | @loading_please_wait 5 | Ідзе загрузка, пачакайце 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | Укл. 12 | 13 | @off 14 | Выкл. 15 | 16 | @toggle_current_site 17 | Пераключыць гэты сайт 18 | 19 | @setup_hotkey_toggle_site 20 | Усталюйце спалучэнне 21 | клавіш 22 | 23 | @toggle_extension 24 | Укл/выкл Дарк Рыдар 25 | 26 | @setup_hotkey_toggle_extension 27 | Уст. спалучэнне 28 | клавіш 29 | 30 | @time_settings 31 | Налады часу 32 | 33 | @set_active_hours 34 | Усталюйце перыяд актыўнасці 35 | 36 | @page_protected 37 | Гэтая старонка ахоўваецца 38 | браўзэрам 39 | 40 | @page_in_dark_list 41 | Гэтая старонка ў глаб. 42 | спісе Цёмных Сайтаў 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Фільтр 49 | 50 | @mode 51 | Рэжым 52 | 53 | @dark 54 | Цёмны 55 | 56 | @light 57 | Светлы 58 | 59 | @brightness 60 | Яркасць 61 | 62 | @contrast 63 | Кантраснасць 64 | 65 | @grayscale 66 | Адценні шэрага 67 | 68 | @sepia 69 | Сэпія 70 | 71 | @only_for 72 | Толькі для 73 | 74 | @only_for_description 75 | Прымяніць налады толькі да гэтага сайта 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Спіс 82 | 83 | @invert_listed_only 84 | Інверт. толькі гэтыя 85 | 86 | @not_invert_listed 87 | Ня інвертаваць 88 | 89 | @add_site_to_list 90 | Дадаць сайт у спіс 91 | 92 | @setup_add_site_hotkey 93 | Уст. спалуч. клавіш для даб. сайта ў спіс 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Яшчэ 100 | 101 | @select_font 102 | Выберыце шрыфт 103 | 104 | @text_stroke 105 | Абводка тэксту 106 | 107 | @try_experimental_theme_engines 108 | Апрабуйце **эксперыментальныя** рэжымы: 109 | **Фільтр+** зах. яркасць колераў, выкарыстоўвае GPU 110 | **Статычны рэжым** стварае простую тэму 111 | **Дынамічны** аналізуе колеры і карцінкі 112 | 113 | @engine_filter 114 | Фільтр 115 | 116 | @engine_filter_plus 117 | Фільтр+ 118 | 119 | @engine_static 120 | Стат. 121 | 122 | @engine_dynamic 123 | Дынам. 124 | 125 | @theme_generation_mode 126 | Рэжым генерацыi тэмы 127 | 128 | @custom_browser_theme_on 129 | Асаблiвая 130 | 131 | @custom_browser_theme_off 132 | Агаданая 133 | 134 | @change_browser_theme 135 | Змяніць тэму браўзэра 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Пагад. 142 | 143 | @help 144 | Даведка 145 | 146 | @donate 147 | Падтрымка 148 | 149 | @news 150 | Навіны 151 | 152 | @read_more 153 | Чытаць яшчэ 154 | 155 | @open_dev_tools 156 | Прылады 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Гэта пашырэнне пераводзіць браўзэр у начны рэжым. Дарк Рыдар замяняе светлы фон цёмным, што зніжае стомленасць вачэй пры доўгай працы за кампутарам альбо пры праглядзе вэб-старонак ноччу. 163 | 164 | Маецца магчымасць наладжваць яркасць, кантраснасць, шрыфт, рэжым інверсіі, рэжым накладання жоўтага фільтра (сэпія). 165 | 166 | Dark Reader ня ўбудоўвае рэкламу і не збірае дадзеныя карыстальніка, увесь зыходны код адкрыты https://github.com/darkreader/darkreader 167 | 168 | Перад усталёўкай адключыце падобныя пашырэнні. Прыемнага прагляду! 169 | -------------------------------------------------------------------------------- /src/_locales/cs.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Tmavý motiv pro každou stránku. Používejte Dark Reader pro celodenní prohlížení webu a šetřete tak své oči. 3 | 4 | @loading_please_wait 5 | Načítání, prosím čekejte 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | Zap 12 | 13 | @off 14 | Vyp 15 | 16 | @toggle_current_site 17 | Přepnout současnou stránku 18 | 19 | @setup_hotkey_toggle_site 20 | Nastavit kláv. zkratku 21 | pro přep. současné stránky 22 | 23 | @toggle_extension 24 | Přepnout rozšíření 25 | 26 | @setup_hotkey_toggle_extension 27 | Nastavit klávesovou 28 | pro přepínání rozšíř. 29 | 30 | @time_settings 31 | Nastavení času 32 | 33 | @set_active_hours 34 | Změňte doby aktivního používání 35 | 36 | @page_protected 37 | Tato stránka je chráněna 38 | prohlížečem 39 | 40 | @page_in_dark_list 41 | Tato stránka je v seznamu 42 | stránek s tmavým motivem 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filtr 49 | 50 | @mode 51 | Režim 52 | 53 | @dark 54 | Tmavý 55 | 56 | @light 57 | Světlý 58 | 59 | @brightness 60 | Jas 61 | 62 | @contrast 63 | Kontrast 64 | 65 | @grayscale 66 | Odstíny šedé 67 | 68 | @sepia 69 | Sépie 70 | 71 | @only_for 72 | Pouze pro 73 | 74 | @only_for_description 75 | Aplikovat nastavení pouze pro současnou stránku 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Seznam 82 | 83 | @invert_listed_only 84 | Invertovat stránky 85 | 86 | @not_invert_listed 87 | Neinvertovat 88 | 89 | @add_site_to_list 90 | Přidat stránku do seznamu 91 | 92 | @setup_add_site_hotkey 93 | Nastavit klávesovou zkratku pro přidání stránky 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Více 100 | 101 | @select_font 102 | Vybrat písmo 103 | 104 | @text_stroke 105 | Tloušťka písma 106 | 107 | @try_experimental_theme_engines 108 | Vyzkoušejte **experimentální** generátory motivů: 109 | **Filter+** zachovává sytost barev, používá GPU 110 | **Statický motiv** generuje rychlý a jednoduchý motiv 111 | **Dynamický motiv** analyzuje barvy a obrázky 112 | 113 | @engine_filter 114 | Filtr 115 | 116 | @engine_filter_plus 117 | Filtr+ 118 | 119 | @engine_static 120 | Statický 121 | 122 | @engine_dynamic 123 | Dynamic. 124 | 125 | @theme_generation_mode 126 | Generátor motivů 127 | 128 | @custom_browser_theme_on 129 | Vlastní nastavení 130 | 131 | @custom_browser_theme_off 132 | Původní nastavení 133 | 134 | @change_browser_theme 135 | Změnit motiv prohlížeče 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Soukromí 142 | 143 | @help 144 | Nápověda 145 | 146 | @donate 147 | Přispějte 148 | 149 | @news 150 | Novinky 151 | 152 | @read_more 153 | Více... 154 | 155 | @open_dev_tools 156 | Nástroje 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Toto k očím šetrné rozšíření přepíná stránky do nočního režimu tím, že je samo převádí do tmavého motivu. Dark Reader invertuje jasné barvy tak, aby byly kontrastní a lehce čitelné v noci. 163 | 164 | Můžete upravit jas, kontrast, sépiový filtr, tmavý mód, nastavení písma a seznam ignorovaných stránek. 165 | 166 | Dark Reader neobsahuje reklamy a nikam neodesílá data uživatele. Projekt je plně open-source a repositář je možné nalézt na https://github.com/darkreader/darkreader 167 | 168 | Před instalací vypněte všechna podobná rozšíření. Přejeme příjemné surfování! 169 | -------------------------------------------------------------------------------- /src/_locales/en.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Dark mode for every website. Take care of your eyes, use dark theme for night and daily browsing. 3 | 4 | @loading_please_wait 5 | Loading, please wait 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | On 12 | 13 | @off 14 | Off 15 | 16 | @toggle_current_site 17 | Toggle current site 18 | 19 | @setup_hotkey_toggle_site 20 | Setup current site 21 | toggle hotkey 22 | 23 | @toggle_extension 24 | Toggle extension 25 | 26 | @setup_hotkey_toggle_extension 27 | Setup extension 28 | toggle hotkey 29 | 30 | @time_settings 31 | Time settings 32 | 33 | @set_active_hours 34 | Set active hours 35 | 36 | @page_protected 37 | This page is protected 38 | by browser 39 | 40 | @page_in_dark_list 41 | This site is in global 42 | Dark List 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filter 49 | 50 | @mode 51 | Mode 52 | 53 | @dark 54 | Dark 55 | 56 | @light 57 | Light 58 | 59 | @brightness 60 | Brightness 61 | 62 | @contrast 63 | Contrast 64 | 65 | @grayscale 66 | Grayscale 67 | 68 | @sepia 69 | Sepia 70 | 71 | @only_for 72 | Only for 73 | 74 | @only_for_description 75 | Apply settings to current website only 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Site list 82 | 83 | @invert_listed_only 84 | Invert listed only 85 | 86 | @not_invert_listed 87 | Not invert listed 88 | 89 | @add_site_to_list 90 | Add site to list 91 | 92 | @setup_add_site_hotkey 93 | Setup a hotkey for adding site 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | More 100 | 101 | @select_font 102 | Select a font 103 | 104 | @text_stroke 105 | Text stroke 106 | 107 | @try_experimental_theme_engines 108 | Try out **experimental** theme engines: 109 | **Filter+** preserves colors saturation, uses GPU 110 | **Static theme** generates a simple fast theme 111 | **Dynamic theme** analyzes colors and images 112 | 113 | @engine_filter 114 | Filter 115 | 116 | @engine_filter_plus 117 | Filter+ 118 | 119 | @engine_static 120 | Static 121 | 122 | @engine_dynamic 123 | Dynamic 124 | 125 | @theme_generation_mode 126 | Theme generation mode 127 | 128 | @custom_browser_theme_on 129 | Custom 130 | 131 | @custom_browser_theme_off 132 | Default 133 | 134 | @change_browser_theme 135 | Change the browser theme 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privacy 142 | 143 | @help 144 | Help 145 | 146 | @donate 147 | Donate 148 | 149 | @news 150 | News 151 | 152 | @read_more 153 | Read more 154 | 155 | @open_dev_tools 156 | Dev tools 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | This eye-care extension enables night mode creating dark themes for websites on the fly. Dark Reader inverts bright colors making them high contrast and easy to read at night. 163 | 164 | You can adjust brightness, contrast, sepia filter, dark mode, font settings and ignore-list. 165 | 166 | Dark Reader doesn't show ads and doesn't send user's data anywhere. It is fully open-source https://github.com/darkreader/darkreader 167 | 168 | Before you install disable similar extensions. Enjoy watching! 169 | -------------------------------------------------------------------------------- /src/_locales/en_GB.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Dark mode for every website. Take care of your eyes, use dark theme for night and daily browsing. 3 | 4 | @loading_please_wait 5 | Loading, please wait 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | On 12 | 13 | @off 14 | Off 15 | 16 | @toggle_current_site 17 | Toggle current site 18 | 19 | @setup_hotkey_toggle_site 20 | Setup current site 21 | toggle hotkey 22 | 23 | @toggle_extension 24 | Toggle extension 25 | 26 | @setup_hotkey_toggle_extension 27 | Setup extension 28 | toggle hotkey 29 | 30 | @time_settings 31 | Time settings 32 | 33 | @set_active_hours 34 | Set active hours 35 | 36 | @page_protected 37 | This page is protected 38 | by browser 39 | 40 | @page_in_dark_list 41 | This site is in global 42 | Dark List 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filter 49 | 50 | @mode 51 | Mode 52 | 53 | @dark 54 | Dark 55 | 56 | @light 57 | Light 58 | 59 | @brightness 60 | Brightness 61 | 62 | @contrast 63 | Contrast 64 | 65 | @grayscale 66 | Grayscale 67 | 68 | @sepia 69 | Sepia 70 | 71 | @only_for 72 | Only for 73 | 74 | @only_for_description 75 | Apply settings to current website only 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Site list 82 | 83 | @invert_listed_only 84 | Invert listed only 85 | 86 | @not_invert_listed 87 | Not invert listed 88 | 89 | @add_site_to_list 90 | Add site to list 91 | 92 | @setup_add_site_hotkey 93 | Setup a hotkey for adding site 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | More 100 | 101 | @select_font 102 | Select a font 103 | 104 | @text_stroke 105 | Text stroke 106 | 107 | @try_experimental_theme_engines 108 | Try out **experimental** theme engines: 109 | **Filter+** preserves colours saturation, uses GPU 110 | **Static theme** generates a simple fast theme 111 | **Dynamic theme** analyzes colours and images 112 | 113 | @engine_filter 114 | Filter 115 | 116 | @engine_filter_plus 117 | Filter+ 118 | 119 | @engine_static 120 | Static 121 | 122 | @engine_dynamic 123 | Dynamic 124 | 125 | @theme_generation_mode 126 | Theme generation mode 127 | 128 | @custom_browser_theme_on 129 | Custom 130 | 131 | @custom_browser_theme_off 132 | Default 133 | 134 | @change_browser_theme 135 | Change the browser theme 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privacy 142 | 143 | @help 144 | Help 145 | 146 | @donate 147 | Donate 148 | 149 | @news 150 | News 151 | 152 | @read_more 153 | Read more 154 | 155 | @open_dev_tools 156 | Dev tools 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | This eye-care extension enables night mode creating dark themes for websites on the fly. Dark Reader inverts bright colours making them high contrast and easy to read at night. 163 | 164 | You can adjust brightness, contrast, sepia filter, dark mode, font settings and ignore-list. 165 | 166 | Dark Reader doesn't show ads and doesn't send user's data anywhere. It is fully open-source https://github.com/darkreader/darkreader 167 | 168 | Before you install disable similar extensions. Enjoy watching! 169 | -------------------------------------------------------------------------------- /src/_locales/en_US.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Dark mode for every website. Take care of your eyes, use dark theme for night and daily browsing. 3 | 4 | @loading_please_wait 5 | Loading, please wait 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | On 12 | 13 | @off 14 | Off 15 | 16 | @toggle_current_site 17 | Toggle current site 18 | 19 | @setup_hotkey_toggle_site 20 | Setup current site 21 | toggle hotkey 22 | 23 | @toggle_extension 24 | Toggle extension 25 | 26 | @setup_hotkey_toggle_extension 27 | Setup extension 28 | toggle hotkey 29 | 30 | @time_settings 31 | Time settings 32 | 33 | @set_active_hours 34 | Set active hours 35 | 36 | @page_protected 37 | This page is protected 38 | by browser 39 | 40 | @page_in_dark_list 41 | This site is in global 42 | Dark List 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filter 49 | 50 | @mode 51 | Mode 52 | 53 | @dark 54 | Dark 55 | 56 | @light 57 | Light 58 | 59 | @brightness 60 | Brightness 61 | 62 | @contrast 63 | Contrast 64 | 65 | @grayscale 66 | Grayscale 67 | 68 | @sepia 69 | Sepia 70 | 71 | @only_for 72 | Only for 73 | 74 | @only_for_description 75 | Apply settings to current website only 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Site list 82 | 83 | @invert_listed_only 84 | Invert listed only 85 | 86 | @not_invert_listed 87 | Not invert listed 88 | 89 | @add_site_to_list 90 | Add site to list 91 | 92 | @setup_add_site_hotkey 93 | Setup a hotkey for adding site 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | More 100 | 101 | @select_font 102 | Select a font 103 | 104 | @text_stroke 105 | Text stroke 106 | 107 | @try_experimental_theme_engines 108 | Try out **experimental** theme engines: 109 | **Filter+** preserves colors saturation, uses GPU 110 | **Static theme** generates a simple fast theme 111 | **Dynamic theme** analyzes colors and images 112 | 113 | @engine_filter 114 | Filter 115 | 116 | @engine_filter_plus 117 | Filter+ 118 | 119 | @engine_static 120 | Static 121 | 122 | @engine_dynamic 123 | Dynamic 124 | 125 | @theme_generation_mode 126 | Theme generation mode 127 | 128 | @custom_browser_theme_on 129 | Custom 130 | 131 | @custom_browser_theme_off 132 | Default 133 | 134 | @change_browser_theme 135 | Change the browser theme 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privacy 142 | 143 | @help 144 | Help 145 | 146 | @donate 147 | Donate 148 | 149 | @news 150 | News 151 | 152 | @read_more 153 | Read more 154 | 155 | @open_dev_tools 156 | Dev tools 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | This eye-care extension enables night mode creating dark themes for websites on the fly. Dark Reader inverts bright colors making them high contrast and easy to read at night. 163 | 164 | You can adjust brightness, contrast, sepia filter, dark mode, font settings and ignore-list. 165 | 166 | Dark Reader doesn't show ads and doesn't send user's data anywhere. It is fully open-source https://github.com/darkreader/darkreader 167 | 168 | Before you install disable similar extensions. Enjoy watching! 169 | -------------------------------------------------------------------------------- /src/_locales/hi.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | हर वेबसाइट के लिए डार्क थीम। अपनी आंखों की देखभाल करें, रात और दैनिक ब्राउज़िंग के लिए डार्क रीडर का उपयोग करें। 3 | 4 | @loading_please_wait 5 | लोड हो रहा है, कृपया प्रतीक्षा करें 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | चालू करो 12 | 13 | @off 14 | बंद करें 15 | 16 | @toggle_current_site 17 | वर्तमान साइट टॉगल करें 18 | 19 | @setup_hotkey_toggle_site 20 | मौजूदा साइट टॉगल हॉटकी 21 | सेटअप करें 22 | 23 | @toggle_extension 24 | एक्सटेंशन टॉगल करें 25 | 26 | @setup_hotkey_toggle_extension 27 | सेटअप एक्सटेंशन 28 | टॉगल हॉटकी 29 | 30 | @time_settings 31 | समय सैट करना 32 | 33 | @set_active_hours 34 | गतिविधि का समय बदलें 35 | 36 | @page_protected 37 | यह पृष्ठ ब्राउज़र 38 | द्वारा संरक्षित है 39 | 40 | @page_in_dark_list 41 | यह साइट वैश्विक 42 | डार्क लिस्ट में है 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | फ़िल्टर 49 | 50 | @mode 51 | मोड 52 | 53 | @dark 54 | अंधेरा 55 | 56 | @light 57 | रोशनी 58 | 59 | @brightness 60 | चमक 61 | 62 | @contrast 63 | विरोध 64 | 65 | @grayscale 66 | ग्रेस्केल 67 | 68 | @sepia 69 | गरम 70 | 71 | @only_for 72 | केवल के लिए 73 | 74 | @only_for_description 75 | केवल मौजूदा वेबसाइट पर सेटिंग्स लागू करें 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | साइट सूची 82 | 83 | @invert_listed_only 84 | इन्वर्टर केवल सूचीबद्ध है 85 | 86 | @not_invert_listed 87 | सूचीबद्ध उलटा नहीं है 88 | 89 | @add_site_to_list 90 | सूची में साइट जोड़ें 91 | 92 | @setup_add_site_hotkey 93 | साइट जोड़ने के लिए एक हॉटकी सेटअप करें 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | अधिक 100 | 101 | @select_font 102 | एक फ़ॉन्ट का चयन करें 103 | 104 | @text_stroke 105 | पाठ स्ट्रोक 106 | 107 | @try_experimental_theme_engines 108 | **प्रयोगात्मक** थीम इंजन आज़माएं: 109 | **फ़िल्टर+** रंग संतृप्ति को संरक्षित करता है, जीपीयू का उपयोग करता है 110 | **स्टेटिक थीम** एक साधारण तेज़ विषय उत्पन्न करता है 111 | **गतिशील विषय** रंग और छवियों का विश्लेषण करता है 112 | 113 | @engine_filter 114 | फ़िल्टर 115 | 116 | @engine_filter_plus 117 | फ़िल्टर+ 118 | 119 | @engine_static 120 | स्थिर 121 | 122 | @engine_dynamic 123 | गतिशील 124 | 125 | @theme_generation_mode 126 | थीम पीढ़ी मोड 127 | 128 | @custom_browser_theme_on 129 | बदला हुआ 130 | 131 | @custom_browser_theme_off 132 | डिफ़ॉल्ट द्वारा 133 | 134 | @change_browser_theme 135 | ब्राउज़र विषय बदलें 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | गोपनीयता नीति 142 | 143 | @help 144 | सहायता 145 | 146 | @donate 147 | दान करना 148 | 149 | @news 150 | समाचार 151 | 152 | @read_more 153 | और पढो 154 | 155 | @open_dev_tools 156 | डेवलपर टूल 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | यह आंख-देखभाल विस्तार रात के मोड को वेबसाइटों के लिए अंधेरे विषयों को बनाने में सक्षम बनाता है। डार्क रीडर चमकदार रंगों को इन्हें उच्च विपरीत बना देता है और रात में पढ़ने में आसान होता है। 163 | 164 | आप चमक, कंट्रास्ट, सेपिया फ़िल्टर, डार्क मोड, फ़ॉन्ट सेटिंग्स और अनदेखा-सूची समायोजित कर सकते हैं। 165 | 166 | डार्क रीडर विज्ञापन नहीं दिखाता है और कहीं भी उपयोगकर्ता का डेटा नहीं भेजता है। यह पूरी तरह से खुला स्रोत है https://github.com/darkreader/darkreader 167 | 168 | डार्क रीडर स्थापित करने से पहले इसी तरह की एक्सटेंशन अक्षम करें। देखने का मज़ा लें! 169 | -------------------------------------------------------------------------------- /src/_locales/id.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Mode Malam untuk semua website. Jaga kesehatan mata and dengan menggunakan Dark Reader untuk aktivitas anda berselancar di dunia maya. 3 | 4 | @loading_please_wait 5 | Sedang Memuat, harap menunggu 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | On 12 | 13 | @off 14 | Off 15 | 16 | @toggle_current_site 17 | Saklar situs yang terbuka 18 | 19 | @setup_hotkey_toggle_site 20 | Setelan situs yang terbuka 21 | Saklar Pintasan 22 | 23 | @toggle_extension 24 | Saklar ekstensi 25 | 26 | @setup_hotkey_toggle_extension 27 | Setelan ekstensi 28 | Saklar Pintasan 29 | 30 | @time_settings 31 | Pengaturan waktu 32 | 33 | @set_active_hours 34 | Ubah waktu aktivitas 35 | 36 | @page_protected 37 | Halaman ini dilindungi 38 | oleh browser 39 | 40 | @page_in_dark_list 41 | Situs ini berada dalam daftar 42 | Mode Malam 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filter 49 | 50 | @mode 51 | Mode 52 | 53 | @dark 54 | Malam 55 | 56 | @light 57 | Siang 58 | 59 | @brightness 60 | Kecerahan 61 | 62 | @contrast 63 | Kontras 64 | 65 | @grayscale 66 | Abu-abu 67 | 68 | @sepia 69 | Sepia 70 | 71 | @only_for 72 | Hanya Untuk 73 | 74 | @only_for_description 75 | Terapkan setelan hanya untuk site yang terbuka 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Daftar Situs 82 | 83 | @invert_listed_only 84 | Pengecualian hanya 85 | 86 | @not_invert_listed 87 | Batalkan didaftarkan 88 | 89 | @add_site_to_list 90 | Tambahkan situs dalam daftar 91 | 92 | @setup_add_site_hotkey 93 | Setelan Pintasan untuk menambahkan situs 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Lanjutan 100 | 101 | @select_font 102 | Pilih Font 103 | 104 | @text_stroke 105 | Tebal Text 106 | 107 | @try_experimental_theme_engines 108 | Uji Coba tema engines **Eksperimental** : 109 | **Filter+** jaga saturasi warna, mengggunakan GPU 110 | **Tema Statis** hasilkan tema cepat & sederhana 111 | **Tema Dinamis** analisa warna dan gambar 112 | 113 | @engine_filter 114 | Filter 115 | 116 | @engine_filter_plus 117 | Filter+ 118 | 119 | @engine_static 120 | Statis 121 | 122 | @engine_dynamic 123 | Dinamis 124 | 125 | @theme_generation_mode 126 | Mode penghasil tema 127 | 128 | @custom_browser_theme_on 129 | Tema Ubahan 130 | 131 | @custom_browser_theme_off 132 | Tema Awal 133 | 134 | @change_browser_theme 135 | Ubah tema browser 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privasi 142 | 143 | @help 144 | Bantuan 145 | 146 | @donate 147 | Donasi 148 | 149 | @news 150 | Berita 151 | 152 | @read_more 153 | Bacaan Lanjutan 154 | 155 | @open_dev_tools 156 | Peralatan 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Ekstensi ini memberikan "Mode Malam" bagi setiap website secara langsung. Dark Reader bekerja dengan membalik warna cerah dengan warna kontras sehingga nyaman di mata. 163 | 164 | Anda bisa mengatur kecerahan, kontras warna, Filter Sepia, Modem Malam, setelan font dan membuat daftar atas situs yg dikecualikan dari daftar mode malam. 165 | 166 | Dark Reader tidak beriklan dan mengumpulkan data penggunan ke manapun. Aplikasi in sepenuhnya open-source https://github.com/darkreader/darkreader 167 | 168 | Sebelum instalasi, disable terlebih dahulu ekstensi serupa. Selamat Berselancar! 169 | -------------------------------------------------------------------------------- /src/_locales/ja.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | すべてのウェブサイトにダークテーマを適用します。夜間や毎日のブラウジングにDark Readerを使用し、あなたの目を気遣います。 3 | 4 | @loading_please_wait 5 | 読み込み中…しばらくお待ちください 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | オン 12 | 13 | @off 14 | オフ 15 | 16 | @toggle_current_site 17 | 現在のサイトを切り替える 18 | 19 | @setup_hotkey_toggle_site 20 | 現在のサイトトグル 21 | ホットキーを設定する 22 | 23 | @toggle_extension 24 | 拡張機能の切り替え 25 | 26 | @setup_hotkey_toggle_extension 27 | セットアップ拡張 28 | トグルホットキー 29 | 30 | @time_settings 31 | 時間設定 32 | 33 | @set_active_hours 34 | アクティブ時間を設定する 35 | 36 | @page_protected 37 | このページはブラウザに 38 | よって保護されています 39 | 40 | @page_in_dark_list 41 | このサイトはグローバル 42 | ダークリストに含まれています 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | フィルタ 49 | 50 | @mode 51 | モード 52 | 53 | @dark 54 | ダーク 55 | 56 | @light 57 | ライト 58 | 59 | @brightness 60 | 輝度 61 | 62 | @contrast 63 | コントラスト 64 | 65 | @grayscale 66 | グレースケール 67 | 68 | @sepia 69 | セピア 70 | 71 | @only_for 72 | このサイトでのみ適用 73 | 74 | @only_for_description 75 | 現在のウェブサイトのみに設定を適用する 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | サイトリスト 82 | 83 | @invert_listed_only 84 | ホワイトリスト 85 | 86 | @not_invert_listed 87 | ブラックリスト 88 | 89 | @add_site_to_list 90 | サイトをリストに追加する 91 | 92 | @setup_add_site_hotkey 93 | サイトを追加するためのホットキーを設定する 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | その他 100 | 101 | @select_font 102 | フォントを選択 103 | 104 | @text_stroke 105 | テキストストローク 106 | 107 | @try_experimental_theme_engines 108 | **実験的**テーマエンジンを試してみてください: 109 | **フィルタ+**は色の彩度を維持します(GPUを使用) 110 | **静的テーマ**は簡単な高速テーマを生成します 111 | **動的テーマ**は色と画像を分析します 112 | 113 | @engine_filter 114 | フィルタ 115 | 116 | @engine_filter_plus 117 | フィルタ+ 118 | 119 | @engine_static 120 | 静的 121 | 122 | @engine_dynamic 123 | 動的 124 | 125 | @theme_generation_mode 126 | テーマ生成モード 127 | 128 | @custom_browser_theme_on 129 | カスタム 130 | 131 | @custom_browser_theme_off 132 | デフォルト 133 | 134 | @change_browser_theme 135 | ブラウザのテーマを変更する 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | 個人情報 142 | 143 | @help 144 | ヘルプ 145 | 146 | @donate 147 | 寄付する 148 | 149 | @news 150 | ニュース 151 | 152 | @read_more 153 | 続きを読む 154 | 155 | @open_dev_tools 156 | 開発者ツール 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | このアイ・ケアの拡張機能は、オンザフライでウェブサイトのためのダークテーマを作成し、ナイトモードを有効にします。 Dark Readerは鮮やかな色を反転させてコントラストを高め、夜間に読みやすくします。 163 | 164 | 明るさ、コントラスト、セピアフィルター、ダークモード、フォント設定、無視リストを調整することができます。 165 | 166 | Dark Readerは広告を表示せず、ユーザーのデータをどこにも送信しません。 それは完全にオープンソースです https://github.com/darkreader/darkreader 167 | 168 | インストールする前に、同様の拡張機能を無効にしてください。 ブラウジングをお楽しみください! 169 | -------------------------------------------------------------------------------- /src/_locales/ko.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | 모든 웹사이트에 다크 모드를 적용합니다. 밤이나 일상적인 웹 브라우징을 할 때에 어두운 테마를 사용하여 눈을 보호하십시오. 3 | 4 | @loading_please_wait 5 | 로딩 중... 잠시만 기다려 주십시오. 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | 킴 12 | 13 | @off 14 | 끔 15 | 16 | @toggle_current_site 17 | 현재 웹 사이트에 적용 18 | 19 | @setup_hotkey_toggle_site 20 | 현재 웹 사이트 설정 21 | 단축키 전환 22 | 23 | @toggle_extension 24 | 확장 기능 적용 25 | 26 | @setup_hotkey_toggle_extension 27 | 확장 기능 설정하기 28 | 바로 가기 전환 29 | 30 | @time_settings 31 | 시간 설정 32 | 33 | @set_active_hours 34 | 사용 시간 설정 35 | 36 | @page_protected 37 | 이 페이지는 38 | 브라우저에 의해 보호되어 있습니다. 39 | 40 | @page_in_dark_list 41 | 이 사이트는 이미 42 | 전체 목록에 있습니다 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | 필터 49 | 50 | @mode 51 | 모드 52 | 53 | @dark 54 | 어두운 테마 55 | 56 | @light 57 | 밝은 테마 58 | 59 | @brightness 60 | 밝기 61 | 62 | @contrast 63 | 대비 64 | 65 | @grayscale 66 | 흑백 필터 67 | 68 | @sepia 69 | 세피아 필터 70 | 71 | @only_for 72 | 해당 웹사이트에만 적용 73 | 74 | @only_for_description 75 | 현재 웹사이트에만 설정을 적용함 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | 사이트 목록 82 | 83 | @invert_listed_only 84 | 목록에 있는 사이트만 반전 85 | 86 | @not_invert_listed 87 | 목록에 있는 사이트만 제외 88 | 89 | @add_site_to_list 90 | 목록에 웹사이트 추가 91 | 92 | @setup_add_site_hotkey 93 | 웹사이트를 추가하는 단축키 설정 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | 기타 100 | 101 | @select_font 102 | 글꼴 선택 103 | 104 | @text_stroke 105 | 텍스트 획 굵기 106 | 107 | @try_experimental_theme_engines 108 | **실험용** 테마 엔진들 109 | **필터+** 는 GPU를 사용하여 채도를 유지합니다 110 | **정적 테마** 는 간단하고 빠른 테마를 생성합니다 111 | **동적 테마** 는 색상 및 이미지를 분석합니다 112 | 113 | @engine_filter 114 | 필터 115 | 116 | @engine_filter_plus 117 | 필터+ 118 | 119 | @engine_static 120 | 정적 121 | 122 | @engine_dynamic 123 | 동적 124 | 125 | @theme_generation_mode 126 | 테마 생성 모드 127 | 128 | @custom_browser_theme_on 129 | 맞춤 테마 130 | 131 | @custom_browser_theme_off 132 | 기본값 133 | 134 | @change_browser_theme 135 | 브라우저 테마 변경 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | 개인 정보 142 | 143 | @help 144 | 도움말 145 | 146 | @donate 147 | 기부 148 | 149 | @news 150 | 뉴스 151 | 152 | @read_more 153 | 더 읽기 154 | 155 | @open_dev_tools 156 | 개발자 도구 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | 이 눈 건강을 위한 확장 기능은 실시간으로 각 웹사이트에 어두운 테마를 적용시켜 야간 모드를 가능케 합니다. 다크 리더는 밝은 색상을 반전시켜 고대비로 만들어 밤에 읽기가 쉽도록 만듭니다. 163 | 164 | 밝기, 대비, 세피아 필터, 어두운 모드, 폰트와 예외 목록을 설정할 수 있습니다. 165 | 166 | 다크 리더는 광고를 보여주지 않으며 사용자의 데이터를 어디에도 보내지 않습니다. 완전히 오픈 소스입니다. https://github.com/darkreader/darkreader 167 | 168 | 설치하기 전에 비슷한 기능을 가진 확장 기능을 사용 해제하시기 바랍니다. 편하게 읽으십시오! 169 | -------------------------------------------------------------------------------- /src/_locales/no.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Mørkt tema for alle nettsteder. Ta vare på øynene, bruk Dark Reader for nettleseren, dag og kveld. 3 | 4 | @loading_please_wait 5 | Laster, vennligst vent 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | På 12 | 13 | @off 14 | Av 15 | 16 | @toggle_current_site 17 | Inverter denne siden 18 | 19 | @setup_hotkey_toggle_site 20 | Sett opp hurtigtast 21 | for å invertere sider 22 | 23 | @toggle_extension 24 | Slå utvidelsen på/av 25 | 26 | @setup_hotkey_toggle_extension 27 | Sett opp hurtigtast for 28 | å slå utvidelsen på/av 29 | 30 | @time_settings 31 | Tidsinnstillinger 32 | 33 | @set_active_hours 34 | Angi aktivitetsperiode 35 | 36 | @page_protected 37 | Denne siden er beskyttet 38 | av nettleseren 39 | 40 | @page_in_dark_list 41 | Siden er i den globale 42 | mørk-modus listen 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filter 49 | 50 | @mode 51 | Modus 52 | 53 | @dark 54 | Mørk 55 | 56 | @light 57 | Lys 58 | 59 | @brightness 60 | Lysstyrke 61 | 62 | @contrast 63 | Kontrast 64 | 65 | @grayscale 66 | Gråtone 67 | 68 | @sepia 69 | Bruntone 70 | 71 | @only_for 72 | Bare for 73 | 74 | @only_for_description 75 | Benytt instillingene bare for denne nettsiden 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Side liste 82 | 83 | @invert_listed_only 84 | Inverter bare 85 | 86 | @not_invert_listed 87 | Ikke inverter 88 | 89 | @add_site_to_list 90 | Legg til en side 91 | 92 | @setup_add_site_hotkey 93 | Legg til en hurtigtast for å legge til sider 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Mer 100 | 101 | @select_font 102 | Velg en skrifttype 103 | 104 | @text_stroke 105 | Tekst-tykkelse 106 | 107 | @try_experimental_theme_engines 108 | Prøv ut **eksperimentell** tema motor: 109 | **Filter+** bevarer fargers metning og benytter GPUen 110 | **Statisk tema** genererer et enkelt, raskt tema 111 | **Dynamisk tema** analyserer farger og bilder 112 | 113 | @engine_filter 114 | Filter 115 | 116 | @engine_filter_plus 117 | Filter+ 118 | 119 | @engine_static 120 | Statisk 121 | 122 | @engine_dynamic 123 | Dynamisk 124 | 125 | @theme_generation_mode 126 | Tema generering modus 127 | 128 | @custom_browser_theme_on 129 | Tilpasset 130 | 131 | @custom_browser_theme_off 132 | Standard 133 | 134 | @change_browser_theme 135 | Bytt nettleser tema 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Personvern 142 | 143 | @help 144 | Hjelp 145 | 146 | @donate 147 | Donere 148 | 149 | @news 150 | Nyheter 151 | 152 | @read_more 153 | Les mer 154 | 155 | @open_dev_tools 156 | Utvikler 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Denne øye-beskyttelse utvidelsen aktiverer nattmodus og lager mørke temaer for nettsider. Dark Reader bytter ut lyse farger med mørke høy-kontrast farger og gjør lesing om kvelden enkelt. 163 | 164 | Du kan regulere lysstyrken, kontrasten, bruntone, nattmodus, skrifttype-instillinger og ignorer-liste. 165 | 166 | Dark Reader viser ikke reklame og sender ikke ut brukerens data. Utvidelsen er åpen-kilde tilgjengelig på https://github.com/darkreader/darkreader 167 | 168 | Deaktiver lignende utvidelser før du installerer. Nyt opplevelsen! 169 | -------------------------------------------------------------------------------- /src/_locales/pt_BR.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Tema escuro para todos os sites. Cuide dos seus olhos, use o Dark Reader para navegação noturna e diária. 3 | 4 | @loading_please_wait 5 | Carregando, por favor espere 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | Ligar 12 | 13 | @off 14 | Des. 15 | 16 | @toggle_current_site 17 | Alternar o site atual 18 | 19 | @setup_hotkey_toggle_site 20 | Configurar o atalho de 21 | alternância do site atual 22 | 23 | @toggle_extension 24 | Alternar extensão 25 | 26 | @setup_hotkey_toggle_extension 27 | Tecla de atalho de alternância 28 | da extensão de configuração 29 | 30 | @time_settings 31 | Configurações de tempo 32 | 33 | @set_active_hours 34 | Defina o horário ativo 35 | 36 | @page_protected 37 | Esta página é protegida 38 | pelo navegador 39 | 40 | @page_in_dark_list 41 | Este site está na lista 42 | negra global 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filtro 49 | 50 | @mode 51 | Modo 52 | 53 | @dark 54 | Escuro 55 | 56 | @light 57 | Claro 58 | 59 | @brightness 60 | Brilho 61 | 62 | @contrast 63 | Contraste 64 | 65 | @grayscale 66 | Em tons de cinza 67 | 68 | @sepia 69 | Sépia 70 | 71 | @only_for 72 | Somente para 73 | 74 | @only_for_description 75 | Aplicar configurações somente ao site atual 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Lista de sites 82 | 83 | @invert_listed_only 84 | Inverter listado 85 | 86 | @not_invert_listed 87 | Não invertido listado 88 | 89 | @add_site_to_list 90 | Adicionar site à lista 91 | 92 | @setup_add_site_hotkey 93 | Configurar uma tecla de atalho para adicionar site 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Mais 100 | 101 | @select_font 102 | Selecione uma fonte 103 | 104 | @text_stroke 105 | Traço de texto 106 | 107 | @try_experimental_theme_engines 108 | Experimente os motores de temas **experimental**: 109 | **Filtro+** preserva a saturação de cores, usa GPU 110 | **Tema estático** gera um tema rápido simples 111 | **Tema dinâmico** analisa cores e imagens 112 | 113 | @engine_filter 114 | Filtro 115 | 116 | @engine_filter_plus 117 | Filtro+ 118 | 119 | @engine_static 120 | Estático 121 | 122 | @engine_dynamic 123 | Dinâmico 124 | 125 | @theme_generation_mode 126 | Modo de geração de tema 127 | 128 | @custom_browser_theme_on 129 | Mudou 130 | 131 | @custom_browser_theme_off 132 | Predefinido 133 | 134 | @change_browser_theme 135 | Alterar o tema do navegador 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privac. 142 | 143 | @help 144 | Ajuda 145 | 146 | @donate 147 | Doar 148 | 149 | @news 150 | Notícia 151 | 152 | @read_more 153 | Ler mais 154 | 155 | @open_dev_tools 156 | Ferramentas 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Esta extensão de cuidados com os olhos ativa o modo noturno criando temas escuros para sites. O Dark Reader inverte as cores brilhantes, tornando-as de alto contraste e fáceis de ler à noite. 163 | 164 | Você pode ajustar o brilho, o contraste, o filtro sépia, o modo escuro, as configurações de fonte e a lista de ignorados. 165 | 166 | O Dark Reader não exibe anúncios e não envia dados do usuário em nenhum lugar. É totalmente open-source https://github.com/darkreader/darkreader 167 | 168 | Antes de instalar desativar extensões semelhantes. Visualização agradável! 169 | -------------------------------------------------------------------------------- /src/_locales/pt_PT.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Tema escuro para todos os sites. Cuide dos seus olhos, use o Dark Reader para navegação noturna e diária. 3 | 4 | @loading_please_wait 5 | Carregando, por favor espere 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | Ligar 12 | 13 | @off 14 | Des. 15 | 16 | @toggle_current_site 17 | Alternar o site atual 18 | 19 | @setup_hotkey_toggle_site 20 | Configurar o atalho de 21 | alternância do site atual 22 | 23 | @toggle_extension 24 | Alternar extensão 25 | 26 | @setup_hotkey_toggle_extension 27 | Tecla de atalho de alternância 28 | da extensão de configuração 29 | 30 | @time_settings 31 | Configurações de tempo 32 | 33 | @set_active_hours 34 | Defina as horas de atividade 35 | 36 | @page_protected 37 | Esta página é protegida 38 | pelo navegador 39 | 40 | @page_in_dark_list 41 | Este site está na lista 42 | negra global 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Filtro 49 | 50 | @mode 51 | Modo 52 | 53 | @dark 54 | Sombrio 55 | 56 | @light 57 | Luz 58 | 59 | @brightness 60 | Brilho 61 | 62 | @contrast 63 | Contraste 64 | 65 | @grayscale 66 | Em tons de cinza 67 | 68 | @sepia 69 | Sépia 70 | 71 | @only_for 72 | Somente para 73 | 74 | @only_for_description 75 | Aplicar configurações somente ao site atual 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Lista de sites 82 | 83 | @invert_listed_only 84 | Inverter listado 85 | 86 | @not_invert_listed 87 | Não invertido listado 88 | 89 | @add_site_to_list 90 | Adicionar site à lista 91 | 92 | @setup_add_site_hotkey 93 | Configurar uma tecla de atalho para adicionar site 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Mais 100 | 101 | @select_font 102 | Selecione uma fonte 103 | 104 | @text_stroke 105 | Traço de texto 106 | 107 | @try_experimental_theme_engines 108 | Experimente os motores de temas **experimental**: 109 | **Filtro+** preserva a saturação de cores, usa GPU 110 | **Tema estático** gera um tema rápido simples 111 | **Tema dinâmico** analisa cores e imagens 112 | 113 | @engine_filter 114 | Filtro 115 | 116 | @engine_filter_plus 117 | Filtro+ 118 | 119 | @engine_static 120 | Estático 121 | 122 | @engine_dynamic 123 | Dinâmico 124 | 125 | @theme_generation_mode 126 | Modo de geração de tema 127 | 128 | @custom_browser_theme_on 129 | Mudou 130 | 131 | @custom_browser_theme_off 132 | Predefinido 133 | 134 | @change_browser_theme 135 | Alterar o tema do navegador 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Privac. 142 | 143 | @help 144 | Ajuda 145 | 146 | @donate 147 | Doar 148 | 149 | @news 150 | Notícia 151 | 152 | @read_more 153 | Ler mais 154 | 155 | @open_dev_tools 156 | Ferramentas 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Esta extensão de cuidados com os olhos ativa o modo noturno criando temas escuros para sites. O Dark Reader inverte as cores brilhantes, tornando-as de alto contraste e fáceis de ler à noite. 163 | 164 | Você pode ajustar o brilho, o contraste, o filtro sépia, o modo escuro, as configurações de fonte e a lista de ignorados. 165 | 166 | O Dark Reader não exibe anúncios e não envia dados do usuário em nenhum lugar. É totalmente open-source https://github.com/darkreader/darkreader 167 | 168 | Antes de instalar desativar extensões semelhantes. Visualização agradável! 169 | -------------------------------------------------------------------------------- /src/_locales/ru.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | Тёмная тема для каждого сайта. Берегите зрение, используйте Дарк Ридер для ночного или ежедневного просмотра веб-страниц. 3 | 4 | @loading_please_wait 5 | Идет загрузка, подождите 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | Вкл. 12 | 13 | @off 14 | Выкл. 15 | 16 | @toggle_current_site 17 | Переключить тек. сайт 18 | 19 | @setup_hotkey_toggle_site 20 | Установите сочетание 21 | клавиш 22 | 23 | @toggle_extension 24 | Вкл/выкл расширен. 25 | 26 | @setup_hotkey_toggle_extension 27 | Уст. сочетание 28 | клавиш 29 | 30 | @time_settings 31 | Настройки времени 32 | 33 | @set_active_hours 34 | Установите период активности 35 | 36 | @page_protected 37 | Эта страница защищена 38 | браузером 39 | 40 | @page_in_dark_list 41 | Эта страница в глоб 42 | списке Тёмных Сайтов 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | Фильтр 49 | 50 | @mode 51 | Режим 52 | 53 | @dark 54 | Тёмный 55 | 56 | @light 57 | Светлый 58 | 59 | @brightness 60 | Яркость 61 | 62 | @contrast 63 | Контрастность 64 | 65 | @grayscale 66 | Оттенки серого 67 | 68 | @sepia 69 | Сепия 70 | 71 | @only_for 72 | Только для 73 | 74 | @only_for_description 75 | Применить настройки только к тек. сайту 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | Список 82 | 83 | @invert_listed_only 84 | Инверт. только эти 85 | 86 | @not_invert_listed 87 | Не инвертировать 88 | 89 | @add_site_to_list 90 | Добавить сайт в список 91 | 92 | @setup_add_site_hotkey 93 | Уст. сочет. клавиш для доб. сайта в список 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | Ещё 100 | 101 | @select_font 102 | Выберите шрифт 103 | 104 | @text_stroke 105 | Обводка текста 106 | 107 | @try_experimental_theme_engines 108 | Опробуйте **экспериментальные** режимы: 109 | **Фильтр+** сохр. яркость цветов, использует GPU 110 | **Статический режим** создаёт простую тему 111 | **Динамический** анализирует цвета и картинки 112 | 113 | @engine_filter 114 | Фильтр 115 | 116 | @engine_filter_plus 117 | Фильтр+ 118 | 119 | @engine_static 120 | Стат. 121 | 122 | @engine_dynamic 123 | Динам. 124 | 125 | @theme_generation_mode 126 | Режим генерации темы 127 | 128 | @custom_browser_theme_on 129 | Изменённая 130 | 131 | @custom_browser_theme_off 132 | По умолчанию 133 | 134 | @change_browser_theme 135 | Изменить тему браузера 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | Согл. 142 | 143 | @help 144 | Справка 145 | 146 | @donate 147 | Поддержка 148 | 149 | @news 150 | Новости 151 | 152 | @read_more 153 | Читать еще 154 | 155 | @open_dev_tools 156 | Разраб. 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | Это расширение переводит браузер в ночной режим. Дарк Ридер заменяет светлый фон тёмным, что снижает усталость глаз при долгой работе за компьютером либо при просмотре веб-страниц ночью. 163 | 164 | Имеется возможность настраивать яркость, контрастность, шрифт, режим инверсии, режим наложения жёлтого фильтра (сепия). 165 | 166 | Dark Reader не встраивает рекламу и не собирает пользовательские данные. Весь исходный код открыт https://github.com/darkreader/darkreader 167 | 168 | Перед установкой отключите подобные расширения. Приятного просмотра! 169 | -------------------------------------------------------------------------------- /src/_locales/th.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | เปลี่ยนเป็นโหมดมืดให้กับทุกๆเว็บเพื่อปกป้องสายตาของคุณ ใช้ธีมสีมืดสำหรับตอนกลางคืนและการท่องเว็บในชีวิตประจำวัน 3 | 4 | @loading_please_wait 5 | กำลังโหลด กรุณารอสักครู่ 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | เปิด 12 | 13 | @off 14 | ปิด 15 | 16 | @toggle_current_site 17 | สลับโหมดในเว็บนี้ 18 | 19 | @setup_hotkey_toggle_site 20 | ตั้งค่าคีย์ลัดสำหรับ 21 | สลับโหมดเว็บนี้ 22 | 23 | @toggle_extension 24 | สลับโหมดเปิด-ปิด 25 | 26 | @setup_hotkey_toggle_extension 27 | ตั้งค่าคีย์ลัดสำหรับ 28 | สลับโหมดเปิด-ปิด 29 | 30 | @time_settings 31 | การตั้งเวลา 32 | 33 | @set_active_hours 34 | ตั้งเวลาที่เริ่มทำงาน 35 | 36 | @page_protected 37 | หน้านี้ถูกป้องกัน 38 | โดยเว็บเบราเซอร์ 39 | 40 | @page_in_dark_list 41 | เว็บนี้อยู่ในรายชื่อ 42 | ที่มีธีมมืดให้ใช้ 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | ฟิลเตอร์ 49 | 50 | @mode 51 | โหมด 52 | 53 | @dark 54 | มืด 55 | 56 | @light 57 | สว่าง 58 | 59 | @brightness 60 | ความสว่าง 61 | 62 | @contrast 63 | คอนทราสต์ 64 | 65 | @grayscale 66 | สีเทา 67 | 68 | @sepia 69 | สีซีเปีย 70 | 71 | @only_for 72 | เฉพาะสำหรับ 73 | 74 | @only_for_description 75 | ใช้การตั้งค่ากับเว็บปัจจุบันเท่านั้น 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | รายชื่อเว็บ 82 | 83 | @invert_listed_only 84 | เว็บที่จะสลับสี 85 | 86 | @not_invert_listed 87 | เว็บที่ไม่อยากสลับสี 88 | 89 | @add_site_to_list 90 | เพิ่มเว็บลงในรายการ 91 | 92 | @setup_add_site_hotkey 93 | ตั้งค่าคีย์ลัดเพื่อเพิ่มเว็บ 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | อื่นๆ 100 | 101 | @select_font 102 | เลือกฟอนต์ 103 | 104 | @text_stroke 105 | ขอบตัวหนังสือ 106 | 107 | @try_experimental_theme_engines 108 | ลองใช้ธีมที่**อยู่ในระหว่างพัฒนา**: 109 | **ฟิลเตอร์+** เพิ่มความเข้มของสี มีการใช้ GPU 110 | **ธีมแบบคงที่** สร้างธีมสีได้ง่ายและรวดเร็ว 111 | **ธีมแบบไดนามิค** วิเคราะห์สีและภาพ 112 | 113 | @engine_filter 114 | ฟิลเตอร์ 115 | 116 | @engine_filter_plus 117 | ฟิลเตอร์+ 118 | 119 | @engine_static 120 | คงที่ 121 | 122 | @engine_dynamic 123 | ไดนามิค 124 | 125 | @theme_generation_mode 126 | โหมดการสร้างธีม 127 | 128 | @custom_browser_theme_on 129 | ปรับเอง 130 | 131 | @custom_browser_theme_off 132 | ค่าเริ่มต้น 133 | 134 | @change_browser_theme 135 | เปลี่ยนธีมชองเบราเซอร์ 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | ความเป็นส่วนตัว 142 | 143 | @help 144 | ช่วยเหลือ 145 | 146 | @donate 147 | สนับสนุน 148 | 149 | @news 150 | ข่าว 151 | 152 | @read_more 153 | อ่านเพิ่มเติม 154 | 155 | @open_dev_tools 156 | เครื่องมือนักพัฒนา 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | นี้เป็นส่วนเสริมที่คอยดูแลสายตาของคุณด้วยการเปิดโหมดกลางคืน และสร้างธีมมืดให้กับทุกๆเว็บ Dark Reader จะสลับสีสว่างๆให้เป็นสีดำหรือสีมืด และทำให้ง่ายต่อการอ่านตอนกลางคืน 163 | 164 | คุณสามารถปรับแต่งความสว่าง, คอนทราสต์, สีซีเปีย, โหมดมืด, ตั้งค่าฟอนต์ และทำรายการยกเว้นได้ 165 | 166 | Dark Reader ไม่แสดงโฆษณา และไม่ส่งข้อมูลของผู้ใช้เด็ดขาด ตัวส่วนเสริมเป็นโอเพนซอร์สโดยสมบูรณ์ดูได้ที่ https://github.com/darkreader/darkreader 167 | 168 | ก่อนที่จะติดตั้ง ปิดส่วนเสริมที่ทำหน้าที่เหมือนๆกันก่อนนะ ขอให้สนุกกับธีมสีมืดนะ! 169 | -------------------------------------------------------------------------------- /src/_locales/zh_CN.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | 黑色主题,适用于任何网站。关爱眼睛,就使用Dark Reader进行夜间和日间浏览。 3 | 4 | @loading_please_wait 5 | 加载中,请稍候 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | 打开 12 | 13 | @off 14 | 关闭 15 | 16 | @toggle_current_site 17 | 切换当前网站 18 | 19 | @setup_hotkey_toggle_site 20 | 设置当前网站的 21 | 切换快捷键 22 | 23 | @toggle_extension 24 | 开关扩展 25 | 26 | @setup_hotkey_toggle_extension 27 | 设置扩展的 28 | 开关快捷键 29 | 30 | @time_settings 31 | 时间设置 32 | 33 | @set_active_hours 34 | 设置使用时段 35 | 36 | @page_protected 37 | 此页面受 38 | 浏览器保护 39 | 40 | @page_in_dark_list 41 | 此网站位于全球 42 | 黑暗列表中 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | 滤镜 49 | 50 | @mode 51 | 模式 52 | 53 | @dark 54 | 黑暗 55 | 56 | @light 57 | 明亮 58 | 59 | @brightness 60 | 亮度 61 | 62 | @contrast 63 | 对比度 64 | 65 | @grayscale 66 | 灰度 67 | 68 | @sepia 69 | 棕褐色滤镜 70 | 71 | @only_for 72 | 仅适用于 73 | 74 | @only_for_description 75 | 仅将设置应用于当前网站 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | 网站列表 82 | 83 | @invert_listed_only 84 | 反色列表 85 | 86 | @not_invert_listed 87 | 不反色列表 88 | 89 | @add_site_to_list 90 | 将网站添加到列表 91 | 92 | @setup_add_site_hotkey 93 | 设置添加网站的快捷键 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | 更多 100 | 101 | @select_font 102 | 选择字体 103 | 104 | @text_stroke 105 | 文字描边 106 | 107 | @try_experimental_theme_engines 108 | 试用**实验**主题引擎: 109 | **滤镜+**保留颜色饱和度,使用GPU 110 | **静态主题**生成一个简单的快速主题 111 | **动态主题**分析颜色和图像 112 | 113 | @engine_filter 114 | 过滤 115 | 116 | @engine_filter_plus 117 | 过滤+ 118 | 119 | @engine_static 120 | 静态 121 | 122 | @engine_dynamic 123 | 动态 124 | 125 | @theme_generation_mode 126 | 主题生成模式 127 | 128 | @custom_browser_theme_on 129 | 自定义 130 | 131 | @custom_browser_theme_off 132 | 默认 133 | 134 | @change_browser_theme 135 | 更改浏览器主题 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | 隐私政策 142 | 143 | @help 144 | 帮助 145 | 146 | @donate 147 | 捐赠 148 | 149 | @news 150 | 新闻 151 | 152 | @read_more 153 | 阅读更多 154 | 155 | @open_dev_tools 156 | 开发者工具 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | 这是一个护眼扩展程序,通过实时生成黑色主题,为每一个网站启用夜间模式。 Dark Reader反转明亮的颜色,使其网页内容具有高对比度并且在易于夜间阅读。 163 | 164 | 您可以调整亮度,对比度,应用棕褐色滤镜,黑暗模式,设置字体和忽略的网站列表。 165 | 166 | Dark Reader无广告,也不会在任何地方发送用户的数据。它完全开源 https://github.com/darkreader/darkreader 167 | 168 | 安装前请禁用类似的扩展。冲浪愉快! 169 | -------------------------------------------------------------------------------- /src/_locales/zh_TW.config: -------------------------------------------------------------------------------- 1 | @extension_description 2 | 黑色主題,適用於任何網站。關愛眼睛,就使用Dark Reader進行夜間和日間瀏覽。 3 | 4 | @loading_please_wait 5 | 加載中,請稍候 6 | 7 | 8 | #==== Top section ==== 9 | 10 | @on 11 | 開啟 12 | 13 | @off 14 | 關閉 15 | 16 | @toggle_current_site 17 | 切換當前網站 18 | 19 | @setup_hotkey_toggle_site 20 | 設置當前網站的 21 | 切換快捷鍵 22 | 23 | @toggle_extension 24 | 開關擴展 25 | 26 | @setup_hotkey_toggle_extension 27 | 設置擴展的 28 | 開關快捷鍵 29 | 30 | @time_settings 31 | 時間設置 32 | 33 | @set_active_hours 34 | 設定使用時間 35 | 36 | @page_protected 37 | 此頁面受 38 | 瀏覽器保護 39 | 40 | @page_in_dark_list 41 | 此網站位於全局 42 | 黑暗列表中 43 | 44 | 45 | #==== Filter ==== 46 | 47 | @filter 48 | 濾鏡 49 | 50 | @mode 51 | 模式 52 | 53 | @dark 54 | 黑暗 55 | 56 | @light 57 | 明亮 58 | 59 | @brightness 60 | 亮度 61 | 62 | @contrast 63 | 對比度 64 | 65 | @grayscale 66 | 灰度 67 | 68 | @sepia 69 | 棕褐色濾鏡 70 | 71 | @only_for 72 | 僅適用於 73 | 74 | @only_for_description 75 | 僅將設置應用於當前網站 76 | 77 | 78 | #==== Site list ==== 79 | 80 | @site_list 81 | 網站列表 82 | 83 | @invert_listed_only 84 | 反色列表 85 | 86 | @not_invert_listed 87 | 不反色列表 88 | 89 | @add_site_to_list 90 | 將網站添加到列表 91 | 92 | @setup_add_site_hotkey 93 | 設置添加網站的快捷鍵 94 | 95 | 96 | #==== More settings ==== 97 | 98 | @more 99 | 更多 100 | 101 | @select_font 102 | 選擇字體 103 | 104 | @text_stroke 105 | 文字描邊 106 | 107 | @try_experimental_theme_engines 108 | 試用 **實驗** 主題引擎: 109 | **濾鏡+** 保留顏色飽和度,將會使用GPU 110 | **靜態主題** 生成一個簡單快速的主題 111 | **動態主題** 分析顏色和圖像 112 | 113 | @engine_filter 114 | 過濾 115 | 116 | @engine_filter_plus 117 | 過濾+ 118 | 119 | @engine_static 120 | 靜態 121 | 122 | @engine_dynamic 123 | 動態 124 | 125 | @theme_generation_mode 126 | 主題生成模式 127 | 128 | @custom_browser_theme_on 129 | 自定義 130 | 131 | @custom_browser_theme_off 132 | 默認 133 | 134 | @change_browser_theme 135 | 更改瀏覽器主題 136 | 137 | 138 | #==== Footer ==== 139 | 140 | @privacy 141 | 隱私政策 142 | 143 | @help 144 | 說明 145 | 146 | @donate 147 | 捐贈 148 | 149 | @news 150 | 新聞 151 | 152 | @read_more 153 | 閱讀更多 154 | 155 | @open_dev_tools 156 | 開發者工具 157 | 158 | 159 | #==== Store listing ==== 160 | 161 | @store_listing 162 | 這是一個護眼擴展程序,通過實時生成黑色主題,為每一個網站啟用夜間模式。 Dark Reader反轉明亮的顏色,使其網頁內容具有高對比度並且易於在夜間閱讀。 163 | 164 | 您可以調整亮度,對比度,應用棕褐色濾鏡,黑暗模式,設置字體和忽略的網站列表。 165 | 166 | Dark Reader無廣告,也不會在任何地方發送用戶的數據。它完全開源 https://github.com/darkreader/darkreader 167 | 168 | 安裝前請禁用類似的擴展。衝浪愉快! 169 | -------------------------------------------------------------------------------- /src/background/icon-manager.ts: -------------------------------------------------------------------------------- 1 | import {isHalloween} from '../utils/time'; 2 | 3 | const ICON_PATHS = { 4 | active_19: '../icons/dr_active_19.png', 5 | active_38: '../icons/dr_active_38.png', 6 | inactive_19: '../icons/dr_inactive_19.png', 7 | inactive_38: '../icons/dr_inactive_38.png', 8 | pumpkin_19: '../icons/pumpkin_19.png', 9 | pumpkin_38: '../icons/pumpkin_38.png', 10 | }; 11 | 12 | export default class IconManager { 13 | constructor() { 14 | this.setActive(); 15 | } 16 | 17 | setActive() { 18 | if (!chrome.browserAction.setIcon) { 19 | // Fix for Firefox Android 20 | return; 21 | } 22 | if (isHalloween()) { 23 | chrome.browserAction.setIcon({ 24 | path: { 25 | '19': ICON_PATHS.pumpkin_19, 26 | '38': ICON_PATHS.pumpkin_38, 27 | } 28 | }); 29 | return; 30 | } 31 | chrome.browserAction.setIcon({ 32 | path: { 33 | '19': ICON_PATHS.active_19, 34 | '38': ICON_PATHS.active_38 35 | } 36 | }); 37 | } 38 | 39 | setInactive() { 40 | if (!chrome.browserAction.setIcon) { 41 | // Fix for Firefox Android 42 | return; 43 | } 44 | chrome.browserAction.setIcon({ 45 | path: { 46 | '19': ICON_PATHS.inactive_19, 47 | '38': ICON_PATHS.inactive_38 48 | } 49 | }); 50 | } 51 | 52 | notifyAboutReleaseNotes(count: number) { 53 | chrome.browserAction.setBadgeBackgroundColor({ 54 | color: '#e96c4c', 55 | }); 56 | chrome.browserAction.setBadgeText({ 57 | text: String(count) 58 | }); 59 | } 60 | 61 | stopNotifyingAboutReleaseNotes() { 62 | chrome.browserAction.setBadgeText({ 63 | text: '' 64 | }); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dark Reader background 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/background/index.ts: -------------------------------------------------------------------------------- 1 | import {Extension} from './extension'; 2 | import {getHelpURL} from '../utils/links'; 3 | 4 | // Initialize extension 5 | const extension = new Extension(); 6 | extension.start(); 7 | 8 | chrome.runtime.onInstalled.addListener(({reason}) => { 9 | if (reason === 'install') { 10 | chrome.tabs.create({url: getHelpURL()}); 11 | } 12 | }); 13 | 14 | declare const __DEBUG__: boolean; 15 | const DEBUG = __DEBUG__; 16 | 17 | if (DEBUG) { 18 | // Reload extension on connection 19 | const listen = () => { 20 | const req = new XMLHttpRequest(); 21 | req.open('GET', 'http://localhost:8890/', true); 22 | req.overrideMimeType('text/plain'); 23 | req.onload = () => { 24 | if (req.status >= 200 && req.status < 300 && req.responseText === 'reload') { 25 | chrome.runtime.reload(); 26 | } else { 27 | setTimeout(listen, 2000); 28 | } 29 | }; 30 | req.onerror = () => setTimeout(listen, 2000); 31 | req.send(); 32 | }; 33 | setTimeout(listen, 2000); 34 | } 35 | -------------------------------------------------------------------------------- /src/background/newsmaker.ts: -------------------------------------------------------------------------------- 1 | import {getBlogPostURL} from '../utils/links'; 2 | import {getDuration} from '../utils/time'; 3 | import {News} from '../definitions'; 4 | 5 | export default class Newsmaker { 6 | static UPDATE_INTERVAL = getDuration({hours: 4}); 7 | 8 | latest: News[]; 9 | onUpdate: (news: News[]) => void; 10 | 11 | constructor(onUpdate: (news: News[]) => void) { 12 | this.latest = []; 13 | this.onUpdate = onUpdate; 14 | } 15 | 16 | subscribe() { 17 | this.updateNews(); 18 | setInterval(() => this.updateNews(), Newsmaker.UPDATE_INTERVAL); 19 | } 20 | 21 | private async updateNews() { 22 | const news = await this.getNews(); 23 | if (news) { 24 | this.latest = news; 25 | this.onUpdate(this.latest); 26 | } 27 | } 28 | 29 | private async getNews() { 30 | try { 31 | const response = await fetch(`https://darkreader.github.io/blog/posts.json?date=${(new Date()).toISOString().substring(0, 10)}`, {cache: 'no-cache'}); 32 | const $news = await response.json(); 33 | return new Promise((resolve, reject) => { 34 | chrome.storage.sync.get({readNews: []}, ({readNews}) => { 35 | const news: News[] = $news.map(({id, date, headline}) => { 36 | const url = getBlogPostURL(id); 37 | const read = this.isRead(id, readNews); 38 | return {id, date, headline, url, read}; 39 | }); 40 | for (let i = 0; i < news.length; i++) { 41 | const date = new Date(news[i].date); 42 | if (isNaN(date.getTime())) { 43 | reject(new Error(`Unable to parse date ${date}`)); 44 | return; 45 | } 46 | } 47 | resolve(news); 48 | }); 49 | }); 50 | } catch (err) { 51 | console.error(err); 52 | return null; 53 | } 54 | } 55 | 56 | markAsRead(...ids: string[]) { 57 | return new Promise((resolve) => { 58 | chrome.storage.sync.get({readNews: []}, ({readNews}) => { 59 | const results = readNews.slice(); 60 | let changed = false; 61 | ids.forEach((id) => { 62 | if (readNews.indexOf(id) < 0) { 63 | results.push(id); 64 | changed = true; 65 | } 66 | }); 67 | if (changed) { 68 | this.latest = this.latest.map(({id, date, url, headline}) => { 69 | const read = this.isRead(id, results); 70 | return {id, date, url, headline, read}; 71 | }); 72 | this.onUpdate(this.latest); 73 | chrome.storage.sync.set({readNews: results}, () => resolve()); 74 | } else { 75 | resolve(); 76 | } 77 | }); 78 | }); 79 | } 80 | 81 | isRead(id: string, readNews: string[]) { 82 | return readNews.includes(id); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/background/utils/extension-api.ts: -------------------------------------------------------------------------------- 1 | import {isFirefox, isEdge} from '../../utils/platform'; 2 | 3 | declare const browser: { 4 | commands: { 5 | update({name, shortcut}): void; 6 | } 7 | }; 8 | 9 | export function canInjectScript(url: string) { 10 | if (isFirefox()) { 11 | return (url 12 | && !url.startsWith('about:') 13 | && !url.startsWith('moz') 14 | && !url.startsWith('view-source:') 15 | && !url.startsWith('https://addons.mozilla.org') 16 | ); 17 | } 18 | if (isEdge()) { 19 | return (url 20 | && !url.startsWith('chrome') 21 | && !url.startsWith('edge') 22 | && !url.startsWith('https://chrome.google.com/webstore') 23 | ); 24 | } 25 | return (url 26 | && !url.startsWith('chrome') 27 | && !url.startsWith('https://chrome.google.com/webstore') 28 | ); 29 | } 30 | 31 | export function getFontList() { 32 | return new Promise((resolve) => { 33 | if (!chrome.fontSettings) { 34 | // Todo: Remove it as soon as Firefox and Edge get support. 35 | resolve([ 36 | 'serif', 37 | 'sans-serif', 38 | 'monospace', 39 | 'cursive', 40 | 'fantasy', 41 | 'system-ui' 42 | ]); 43 | return; 44 | } 45 | chrome.fontSettings.getFontList((list) => { 46 | const fonts = list.map((f) => f.fontId); 47 | resolve(fonts); 48 | }); 49 | }); 50 | } 51 | 52 | export function getCommands() { 53 | return new Promise((resolve) => { 54 | if (!chrome.commands) { 55 | resolve([]); 56 | return; 57 | } 58 | chrome.commands.getAll((commands) => { 59 | if (commands) { 60 | resolve(commands); 61 | } else { 62 | resolve([]); 63 | } 64 | }); 65 | }); 66 | } 67 | 68 | export function setShortcut(command: string, shortcut: string) { 69 | if (typeof browser !== 'undefined' && browser.commands && browser.commands.update) { 70 | browser.commands.update({name: command, shortcut}); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/background/window-theme.ts: -------------------------------------------------------------------------------- 1 | import {parse, rgbToHexString, RGBA} from '../utils/color'; 2 | import {modifyBackgroundColor, modifyForegroundColor, modifyBorderColor} from '../generators/modify-colors'; 3 | import {FilterConfig} from '../definitions'; 4 | 5 | declare const browser: { 6 | theme: { 7 | update: ((theme: any) => Promise); 8 | reset: (() => Promise); 9 | }; 10 | }; 11 | 12 | const themeColorTypes = { 13 | accentcolor: 'bg', 14 | button_background_active: 'text', 15 | button_background_hover: 'text', 16 | icons: 'text', 17 | icons_attention: 'text', 18 | popup: 'bg', 19 | popup_border: 'bg', 20 | popup_highlight: 'bg', 21 | popup_highlight_text: 'text', 22 | popup_text: 'text', 23 | tab_line: 'bg', 24 | tab_loading: 'bg', 25 | tab_selected: 'bg', 26 | textcolor: 'text', 27 | toolbar: 'bg', 28 | toolbar_bottom_separator: 'border', 29 | toolbar_field: 'bg', 30 | toolbar_field_border: 'border', 31 | toolbar_field_border_focus: 'border', 32 | toolbar_field_focus: 'bg', 33 | toolbar_field_text: 'text', 34 | toolbar_field_text_focus: 'text', 35 | toolbar_field_separator: 'border', 36 | toolbar_text: 'text', 37 | toolbar_top_separator: 'border', 38 | toolbar_vertical_separator: 'border', 39 | } 40 | 41 | const $colors = { 42 | accentcolor: '#111111', 43 | popup: '#cccccc', 44 | popup_text: 'black', 45 | tab_line: '#23aeff', 46 | tab_loading: '#23aeff', 47 | textcolor: 'white', 48 | toolbar: '#707070', 49 | toolbar_field: 'lightgray', 50 | toolbar_field_text: 'black', 51 | }; 52 | 53 | export function setWindowTheme(filter: FilterConfig, $accent?: string) { 54 | const colors = Object.entries($colors).reduce((obj, [key, value]) => { 55 | const type = themeColorTypes[key]; 56 | const modify: ((rgb: RGBA, filter: FilterConfig) => string) = { 57 | 'bg': modifyBackgroundColor, 58 | 'text': modifyForegroundColor, 59 | 'border': modifyBorderColor, 60 | }[type]; 61 | const rgb = parse(value); 62 | const modified = modify(rgb, filter); 63 | obj[key] = modified; 64 | return obj; 65 | }, {}); 66 | if (typeof browser !== 'undefined' && browser.theme && browser.theme.update) { 67 | browser.theme.update({colors}); 68 | } 69 | } 70 | 71 | export function resetWindowTheme() { 72 | if (typeof browser !== 'undefined' && browser.theme && browser.theme.reset) { 73 | // BUG: resets browser theme to entire 74 | // https://bugzilla.mozilla.org/show_bug.cgi?id=1415267 75 | browser.theme.reset(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/config/dark_sites.json: -------------------------------------------------------------------------------- 1 | [ 2 | "WARNING.THIS-JSON-FILE-IS-FOR-OLDER-BROWSERS-SO-USE.dark-sites.config", 3 | "alienwarearena.com", 4 | "animeonline.su", 5 | "asciinema.org", 6 | "avengedsevenfold.com", 7 | "bash.im/comics", 8 | "basho.com/$", 9 | "bbc.co.uk/iplayer", 10 | "beastskills.com", 11 | "bing.com/$", 12 | "bitcoinwisdom.com", 13 | "blizzard.com", 14 | "bogleech.com", 15 | "bungie.net", 16 | "caniuse.com", 17 | "codepen.io", 18 | "cryptowat.ch", 19 | "darkreader.org", 20 | "desktop.github.com", 21 | "destinytracker.com", 22 | "discordapp.com", 23 | "dota2.com", 24 | "dota2.ru", 25 | "dotabuff.com", 26 | "draculatheme.com", 27 | "ffmpeg.org", 28 | "filterblade.xyz", 29 | "frootvpn.com", 30 | "getsharex.com", 31 | "giphy.com", 32 | "glowing-bear.org", 33 | "greenmangaming.com", 34 | "gunshipmusic.com", 35 | "hackaday.com", 36 | "hangouts.google.com", 37 | "hardcoregaming101.net", 38 | "heavybit.com", 39 | "hyper.is", 40 | "illicoweb.videotron.com", 41 | "imgur.com", 42 | "ingress.com", 43 | "inker.co", 44 | "isthereanydeal.com", 45 | "killtheradio.net", 46 | "linux.org.ru", 47 | "metro.ru", 48 | "mmorpg.com", 49 | "mwomercs.com", 50 | "netflix.com", 51 | "newwatch.slingbox.com", 52 | "pathof.info", 53 | "pathofexile.com", 54 | "play.hbogo.com", 55 | "play.mubert.com", 56 | "pluralsight.com", 57 | "plus.google.com/hangouts", 58 | "poe-racing.com", 59 | "poe.trade", 60 | "poeapp.com", 61 | "poebuilds.cc", 62 | "poelab.com", 63 | "popurls.com", 64 | "raidtime.net", 65 | "realgamernewz.com", 66 | "regexr.com", 67 | "serebii.net", 68 | "sho.com", 69 | "showtimeanytime.com", 70 | "shutov.by", 71 | "spacespacespaaace.space", 72 | "spotify.com", 73 | "starz.com", 74 | "steamcommunity.com", 75 | "store.steampowered.com", 76 | "telecineplay.com.br", 77 | "totalwarwarhammer.gamepedia.com", 78 | "tweetdeck.twitter.com", 79 | "um.mos.ru", 80 | "undertale.com", 81 | "vakhtangov.ru", 82 | "vinesauce.com", 83 | "www.gdax.com", 84 | "yandexdataschool.ru/$" 85 | ] 86 | -------------------------------------------------------------------------------- /src/config/static-themes.config: -------------------------------------------------------------------------------- 1 | * 2 | 3 | NEUTRAL BG 4 | html 5 | body 6 | :not([style*="background-color:"]) 7 | 8 | NEUTRAL TEXT 9 | html 10 | body 11 | :not([style*="color:"]) 12 | 13 | RED TEXT 14 | h1:not([style*="color:"]) 15 | h2:not([style*="color:"]) 16 | h3:not([style*="color:"]) 17 | h4:not([style*="color:"]) 18 | h5:not([style*="color:"]) 19 | h6:not([style*="color:"]) 20 | 21 | GREEN TEXT 22 | cite:not([style*="color:"]) 23 | 24 | BLUE BG ACTIVE 25 | input:not([style*="background-color:"]) 26 | textarea:not([style*="background-color:"]) 27 | button:not([style*="background-color:"]) 28 | [role="button"] 29 | 30 | BLUE TEXT ACTIVE 31 | a:not([style*="color:"]) 32 | 33 | BLUE BORDER 34 | :not([style*="border-color:"]) 35 | ::before 36 | ::after 37 | 38 | FADE BG 39 | div:empty 40 | 41 | FADE TEXT 42 | input::placeholder 43 | textarea::placeholder 44 | 45 | NO IMAGE 46 | input:not([style*="background-image:"]) 47 | textarea:not([style*="background-image:"]) 48 | 49 | ================================ 50 | 51 | github.com 52 | 53 | RED TEXT 54 | .pl-k 55 | 56 | GREEN BG ACTIVE 57 | .btn-primary 58 | 59 | GREEN TEXT 60 | .pl-c 61 | 62 | BLUE TEXT 63 | .pl-s 64 | .pl-pds 65 | .pl-c1 66 | 67 | NO IMAGE 68 | .btn 69 | .btn-primary 70 | 71 | ================================ 72 | 73 | mail.google.com 74 | 75 | RED BG ACTIVE 76 | .T-I-KE 77 | 78 | NO IMAGE 79 | .T-I-KE 80 | 81 | ================================ 82 | 83 | opencollective.com 84 | 85 | BLUE BG ACTIVE 86 | .TierCard .action 87 | 88 | TRANSPARENT BG 89 | .CollectiveCover .content 90 | -------------------------------------------------------------------------------- /src/generators/dynamic-theme.ts: -------------------------------------------------------------------------------- 1 | import {formatSitesFixesConfig} from './utils/format'; 2 | import {parseSitesFixesConfig} from './utils/parse'; 3 | import {parseArray, formatArray} from '../utils/text'; 4 | import {compareURLPatterns, isURLInList} from '../utils/url'; 5 | import {DynamicThemeFix} from '../definitions'; 6 | 7 | const dynamicThemeFixesCommands = { 8 | 'INVERT': 'invert', 9 | 'CSS': 'css', 10 | }; 11 | 12 | export function parseDynamicThemeFixes(text: string) { 13 | return parseSitesFixesConfig(text, { 14 | commands: Object.keys(dynamicThemeFixesCommands), 15 | getCommandPropName: (command) => dynamicThemeFixesCommands[command] || null, 16 | parseCommandValue: (command, value) => { 17 | if (command === 'CSS') { 18 | return value.trim(); 19 | } 20 | return parseArray(value); 21 | }, 22 | }); 23 | } 24 | 25 | export function formatDynamicThemeFixes(dynamicThemeFixes: DynamicThemeFix[]) { 26 | const fixes = dynamicThemeFixes.slice().sort((a, b) => compareURLPatterns(a.url[0], b.url[0])); 27 | 28 | return formatSitesFixesConfig(fixes, { 29 | props: Object.values(dynamicThemeFixesCommands), 30 | getPropCommandName: (prop) => Object.entries(dynamicThemeFixesCommands).find(([command, p]) => p === prop)[0], 31 | formatPropValue: (prop, value) => { 32 | if (prop === 'css') { 33 | return value.trim(); 34 | } 35 | return formatArray(value).trim(); 36 | }, 37 | shouldIgnoreProp: (prop, value) => { 38 | if (prop === 'css') { 39 | return !value; 40 | } 41 | return !(Array.isArray(value) && value.length > 0); 42 | }, 43 | }); 44 | } 45 | 46 | export function getDynamicThemeFixesFor(url: string, frameURL: string, fixes: DynamicThemeFix[]) { 47 | if (fixes.length === 0 || fixes[0].url[0] !== '*') { 48 | return null; 49 | } 50 | 51 | const common = { 52 | url: fixes[0].url, 53 | invert: fixes[0].invert || [], 54 | css: fixes[0].css || [], 55 | }; 56 | 57 | const sortedBySpecificity = fixes 58 | .slice(1) 59 | .map((theme) => { 60 | return { 61 | specificity: isURLInList(frameURL || url, theme.url) ? theme.url[0].length : 0, 62 | theme 63 | }; 64 | }) 65 | .filter(({specificity}) => specificity > 0) 66 | .sort((a, b) => b.specificity - a.specificity); 67 | 68 | if (sortedBySpecificity.length === 0) { 69 | return common; 70 | } 71 | 72 | const match = sortedBySpecificity[0].theme; 73 | 74 | return { 75 | url: match.url, 76 | invert: common.invert.concat(match.invert || []), 77 | css: [common.css, match.css].filter((s) => s).join('\n'), 78 | }; 79 | } 80 | -------------------------------------------------------------------------------- /src/generators/svg-filter.ts: -------------------------------------------------------------------------------- 1 | import {createFilterMatrix, Matrix} from './utils/matrix'; 2 | import {isFirefox} from '../utils/platform'; 3 | import {cssFilterStyleheetTemplate} from './css-filter'; 4 | import {FilterConfig, InversionFix} from '../definitions'; 5 | 6 | export function createSVGFilterStylesheet(config: FilterConfig, url: string, frameURL: string, inversionFixes: InversionFix[]) { 7 | let filterValue: string; 8 | let reverseFilterValue: string; 9 | if (isFirefox()) { 10 | filterValue = getEmbeddedSVGFilterValue(getSVGFilterMatrixValue(config)); 11 | reverseFilterValue = getEmbeddedSVGFilterValue(getSVGReverseFilterMatrixValue()); 12 | } else { 13 | // Chrome fails with "Unsafe attempt to load URL ... Domains, protocols and ports must match. 14 | filterValue = 'url(#dark-reader-filter)'; 15 | reverseFilterValue = 'url(#dark-reader-reverse-filter)'; 16 | } 17 | return cssFilterStyleheetTemplate(filterValue, reverseFilterValue, config, url, frameURL, inversionFixes); 18 | } 19 | 20 | function getEmbeddedSVGFilterValue(matrixValue: string) { 21 | const id = 'dark-reader-filter'; 22 | const svg = [ 23 | '', 24 | ``, 25 | ``, 26 | '', 27 | '', 28 | ].join(''); 29 | return `url(data:image/svg+xml;base64,${btoa(svg)}#${id})`; 30 | } 31 | 32 | function toSVGMatrix(matrix: number[][]) { 33 | return matrix.slice(0, 4).map(m => m.map(m => m.toFixed(3)).join(' ')).join(' '); 34 | } 35 | 36 | export function getSVGFilterMatrixValue(config: FilterConfig) { 37 | return toSVGMatrix(createFilterMatrix(config)); 38 | } 39 | 40 | export function getSVGReverseFilterMatrixValue() { 41 | return toSVGMatrix(Matrix.invertNHue()); 42 | } 43 | -------------------------------------------------------------------------------- /src/generators/text-style.ts: -------------------------------------------------------------------------------- 1 | import {FilterConfig} from '../definitions' 2 | 3 | export function createTextStyle(config: FilterConfig): string { 4 | const lines: string[] = []; 5 | lines.push('* {'); 6 | 7 | if (config.useFont && config.fontFamily) { 8 | // TODO: Validate... 9 | lines.push(` font-family: ${config.fontFamily} !important;`); 10 | } 11 | 12 | if (config.textStroke > 0) { 13 | lines.push(` -webkit-text-stroke: ${config.textStroke}px !important;`); 14 | lines.push(` text-stroke: ${config.textStroke}px !important;`); 15 | } 16 | 17 | lines.push('}'); 18 | 19 | return lines.join('\n'); 20 | } 21 | -------------------------------------------------------------------------------- /src/generators/theme-engines.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | cssFilter: 'cssFilter', 3 | svgFilter: 'svgFilter', 4 | staticTheme: 'staticTheme', 5 | dynamicTheme: 'dynamicTheme', 6 | }; 7 | -------------------------------------------------------------------------------- /src/generators/utils/format.ts: -------------------------------------------------------------------------------- 1 | interface SiteFix { 2 | url: string[]; 3 | [prop: string]: any; 4 | } 5 | 6 | interface SitesFixesFormatOptions { 7 | props: string[]; 8 | getPropCommandName: (prop: string) => string; 9 | formatPropValue: (prop: string, value) => string; 10 | shouldIgnoreProp: (props: string, value) => boolean; 11 | } 12 | 13 | export function formatSitesFixesConfig(fixes: SiteFix[], options: SitesFixesFormatOptions) { 14 | const lines: string[] = []; 15 | 16 | fixes.forEach((fix, i) => { 17 | lines.push(...fix.url); 18 | options.props.forEach((prop) => { 19 | const command = options.getPropCommandName(prop); 20 | const value = fix[prop]; 21 | if (options.shouldIgnoreProp(prop, value)) { 22 | return; 23 | } 24 | lines.push(''); 25 | lines.push(command); 26 | const formattedValue = options.formatPropValue(prop, value); 27 | if (formattedValue) { 28 | lines.push(formattedValue); 29 | } 30 | }); 31 | if (i < fixes.length - 1) { 32 | lines.push(''); 33 | lines.push(Array.from({length: 32}).fill('=').join('')); 34 | lines.push(''); 35 | } 36 | }); 37 | 38 | lines.push(''); 39 | return lines.join('\n'); 40 | } 41 | -------------------------------------------------------------------------------- /src/generators/utils/matrix.ts: -------------------------------------------------------------------------------- 1 | import {clamp, multiplyMatrices} from '../../utils/math'; 2 | import {FilterConfig} from '../../definitions'; 3 | 4 | export function createFilterMatrix(config: FilterConfig) { 5 | let m = Matrix.identity(); 6 | if (config.sepia !== 0) { 7 | m = multiplyMatrices(m, Matrix.sepia(config.sepia / 100)); 8 | } 9 | if (config.grayscale !== 0) { 10 | m = multiplyMatrices(m, Matrix.grayscale(config.grayscale / 100)); 11 | } 12 | if (config.contrast !== 100) { 13 | m = multiplyMatrices(m, Matrix.contrast(config.contrast / 100)); 14 | } 15 | if (config.brightness !== 100) { 16 | m = multiplyMatrices(m, Matrix.brightness(config.brightness / 100)); 17 | } 18 | if (config.mode === 1) { 19 | m = multiplyMatrices(m, Matrix.invertNHue()); 20 | } 21 | return m; 22 | } 23 | 24 | export function applyColorMatrix([r, g, b]: number[], matrix: number[][]) { 25 | const rgb = [[r / 255], [g / 255], [b / 255], [1], [1]]; 26 | const result = multiplyMatrices(matrix, rgb); 27 | return [0, 1, 2].map((i) => clamp(Math.round(result[i][0] * 255), 0, 255)); 28 | } 29 | 30 | export const Matrix = { 31 | 32 | identity() { 33 | return [ 34 | [1, 0, 0, 0, 0], 35 | [0, 1, 0, 0, 0], 36 | [0, 0, 1, 0, 0], 37 | [0, 0, 0, 1, 0], 38 | [0, 0, 0, 0, 1] 39 | ]; 40 | }, 41 | 42 | invertNHue() { 43 | return [ 44 | [0.333, -0.667, -0.667, 0, 1], 45 | [-0.667, 0.333, -0.667, 0, 1], 46 | [-0.667, -0.667, 0.333, 0, 1], 47 | [0, 0, 0, 1, 0], 48 | [0, 0, 0, 0, 1] 49 | ]; 50 | }, 51 | 52 | brightness(v: number) { 53 | return [ 54 | [v, 0, 0, 0, 0], 55 | [0, v, 0, 0, 0], 56 | [0, 0, v, 0, 0], 57 | [0, 0, 0, 1, 0], 58 | [0, 0, 0, 0, 1] 59 | ]; 60 | }, 61 | 62 | contrast(v: number) { 63 | const t = (1 - v) / 2; 64 | return [ 65 | [v, 0, 0, 0, t], 66 | [0, v, 0, 0, t], 67 | [0, 0, v, 0, t], 68 | [0, 0, 0, 1, 0], 69 | [0, 0, 0, 0, 1] 70 | ]; 71 | }, 72 | 73 | sepia(v: number) { 74 | return [ 75 | [(0.393 + 0.607 * (1 - v)), (0.769 - 0.769 * (1 - v)), (0.189 - 0.189 * (1 - v)), 0, 0], 76 | [(0.349 - 0.349 * (1 - v)), (0.686 + 0.314 * (1 - v)), (0.168 - 0.168 * (1 - v)), 0, 0], 77 | [(0.272 - 0.272 * (1 - v)), (0.534 - 0.534 * (1 - v)), (0.131 + 0.869 * (1 - v)), 0, 0], 78 | [0, 0, 0, 1, 0], 79 | [0, 0, 0, 0, 1] 80 | ]; 81 | }, 82 | 83 | grayscale(v: number) { 84 | return [ 85 | [(0.2126 + 0.7874 * (1 - v)), (0.7152 - 0.7152 * (1 - v)), (0.0722 - 0.0722 * (1 - v)), 0, 0], 86 | [(0.2126 - 0.2126 * (1 - v)), (0.7152 + 0.2848 * (1 - v)), (0.0722 - 0.0722 * (1 - v)), 0, 0], 87 | [(0.2126 - 0.2126 * (1 - v)), (0.7152 - 0.7152 * (1 - v)), (0.0722 + 0.9278 * (1 - v)), 0, 0], 88 | [0, 0, 0, 1, 0], 89 | [0, 0, 0, 0, 1] 90 | ]; 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /src/generators/utils/parse.ts: -------------------------------------------------------------------------------- 1 | import {parseArray} from '../../utils/text'; 2 | 3 | interface SiteProps { 4 | url: string[]; 5 | } 6 | 7 | interface SitesFixesParserOptions { 8 | commands: string[]; 9 | getCommandPropName: (command: string) => string; 10 | parseCommandValue: (command: string, value: string) => any; 11 | } 12 | 13 | export function parseSitesFixesConfig(text: string, options: SitesFixesParserOptions) { 14 | const sites: T[] = []; 15 | 16 | const blocks = text.replace(/\r/g, '').split(/^\s*={2,}\s*$/gm); 17 | blocks.forEach((block) => { 18 | const lines = block.split('\n'); 19 | const commandIndices: number[] = []; 20 | lines.forEach((ln, i) => { 21 | if (ln.match(/^\s*[A-Z]+(\s[A-Z]+)*\s*$/)) { 22 | commandIndices.push(i); 23 | } 24 | }); 25 | 26 | if (commandIndices.length === 0) { 27 | return; 28 | } 29 | 30 | const siteFix = { 31 | url: parseArray(lines.slice(0, commandIndices[0]).join('\n')) as string[], 32 | } as T; 33 | 34 | commandIndices.forEach((commandIndex, i) => { 35 | const command = lines[commandIndex].trim(); 36 | const valueText = lines.slice(commandIndex + 1, i === commandIndices.length - 1 ? lines.length : commandIndices[i + 1]).join('\n'); 37 | const prop = options.getCommandPropName(command); 38 | if (!prop) { 39 | return; 40 | } 41 | const value = options.parseCommandValue(command, valueText); 42 | siteFix[prop] = value; 43 | }); 44 | 45 | sites.push(siteFix); 46 | }); 47 | 48 | return sites; 49 | } 50 | -------------------------------------------------------------------------------- /src/icons/dr_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_128.png -------------------------------------------------------------------------------- /src/icons/dr_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_16.png -------------------------------------------------------------------------------- /src/icons/dr_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_48.png -------------------------------------------------------------------------------- /src/icons/dr_active_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_active_19.png -------------------------------------------------------------------------------- /src/icons/dr_active_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_active_38.png -------------------------------------------------------------------------------- /src/icons/dr_inactive_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_inactive_19.png -------------------------------------------------------------------------------- /src/icons/dr_inactive_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/dr_inactive_38.png -------------------------------------------------------------------------------- /src/icons/pumpkin_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/pumpkin_19.png -------------------------------------------------------------------------------- /src/icons/pumpkin_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/icons/pumpkin_38.png -------------------------------------------------------------------------------- /src/inject/dynamic-theme/meta-theme-color.ts: -------------------------------------------------------------------------------- 1 | import {parse} from '../../utils/color'; 2 | import {modifyBackgroundColor} from '../../generators/modify-colors'; 3 | import {logWarn} from '../utils/log'; 4 | import {FilterConfig} from '../../definitions'; 5 | 6 | const metaThemeColorName = 'theme-color'; 7 | const metaThemeColorSelector = `meta[name="${metaThemeColorName}"]`; 8 | let srcMetaThemeColor: string = null; 9 | let observer: MutationObserver = null; 10 | 11 | function changeMetaThemeColor(meta: HTMLMetaElement, theme: FilterConfig) { 12 | srcMetaThemeColor = srcMetaThemeColor || meta.content; 13 | try { 14 | const color = parse(srcMetaThemeColor); 15 | meta.content = modifyBackgroundColor(color, theme); 16 | } catch (err) { 17 | logWarn(err); 18 | } 19 | } 20 | 21 | export function changeMetaThemeColorWhenAvailable(theme: FilterConfig) { 22 | const meta = document.querySelector(metaThemeColorSelector) as HTMLMetaElement; 23 | if (meta) { 24 | changeMetaThemeColor(meta, theme); 25 | } else { 26 | if (observer) { 27 | observer.disconnect(); 28 | } 29 | observer = new MutationObserver((mutations) => { 30 | loop: for (let m of mutations) { 31 | for (let node of Array.from(m.addedNodes)) { 32 | if (node instanceof HTMLMetaElement && node.name === metaThemeColorName) { 33 | observer.disconnect(); 34 | observer = null; 35 | changeMetaThemeColor(node, theme); 36 | break loop; 37 | } 38 | } 39 | } 40 | }); 41 | observer.observe(document.head, {childList: true}); 42 | } 43 | } 44 | 45 | export function restoreMetaThemeColor() { 46 | if (observer) { 47 | observer.disconnect(); 48 | observer = null; 49 | } 50 | const meta = document.querySelector(metaThemeColorSelector) as HTMLMetaElement; 51 | if (meta && srcMetaThemeColor) { 52 | meta.content = srcMetaThemeColor; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/inject/dynamic-theme/network.ts: -------------------------------------------------------------------------------- 1 | interface FetchRequest { 2 | url: string; 3 | responseType: 'data-url' | 'text'; 4 | } 5 | 6 | let counter = 0; 7 | const resolvers = new Map void>(); 8 | const rejectors = new Map void>(); 9 | 10 | export function bgFetch(request: FetchRequest) { 11 | return new Promise((resolve, reject) => { 12 | const id = ++counter; 13 | resolvers.set(id, resolve); 14 | rejectors.set(id, reject); 15 | chrome.runtime.sendMessage({type: 'fetch', data: request, id}); 16 | }); 17 | } 18 | 19 | chrome.runtime.onMessage.addListener(({type, data, error, id}) => { 20 | if (type === 'fetch-response') { 21 | const resolve = resolvers.get(id); 22 | const reject = rejectors.get(id); 23 | resolvers.delete(id); 24 | rejectors.delete(id); 25 | if (error) { 26 | reject && reject(error); 27 | } else { 28 | resolve && resolve(data); 29 | } 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /src/inject/dynamic-theme/url.ts: -------------------------------------------------------------------------------- 1 | import {getMatches} from '../../utils/text'; 2 | 3 | export function parseURL(url: string) { 4 | const a = document.createElement('a'); 5 | a.href = url; 6 | return a; 7 | } 8 | 9 | export function getAbsoluteURL($base: string, $relative: string) { 10 | if ($relative.match(/^.*?\/\//) || $relative.match(/^data\:/)) { 11 | if ($relative.startsWith('//')) { 12 | return `${location.protocol}${$relative}`; 13 | } 14 | return $relative; 15 | } 16 | const b = parseURL($base); 17 | if ($relative.startsWith('/')) { 18 | const u = parseURL(`${b.protocol}//${b.host}${$relative}`); 19 | return u.href; 20 | } 21 | const pathParts = b.pathname.split('/').concat($relative.split('/')).filter((p) => p); 22 | let backwardIndex: number 23 | while ((backwardIndex = pathParts.indexOf('..')) > 0) { 24 | pathParts.splice(backwardIndex - 1, 2); 25 | } 26 | const u = parseURL(`${b.protocol}//${b.host}/${pathParts.join('/')}`); 27 | return u.href; 28 | } 29 | -------------------------------------------------------------------------------- /src/inject/index.ts: -------------------------------------------------------------------------------- 1 | import {createOrUpdateStyle, removeStyle} from './style'; 2 | import {createOrUpdateSVGFilter, removeSVGFilter} from './svg-filter'; 3 | import {createOrUpdateDynamicTheme, removeDynamicTheme, cleanDynamicThemeCache} from './dynamic-theme'; 4 | import {logWarn} from './utils/log'; 5 | 6 | function onMessage({type, data}) { 7 | switch (type) { 8 | case 'add-css-filter': 9 | case 'add-static-theme': { 10 | const css = data; 11 | removeDynamicTheme(); 12 | createOrUpdateStyle(css); 13 | break; 14 | } 15 | case 'add-svg-filter': { 16 | const {css, svgMatrix, svgReverseMatrix} = data; 17 | removeDynamicTheme(); 18 | createOrUpdateSVGFilter(svgMatrix, svgReverseMatrix); 19 | createOrUpdateStyle(css); 20 | break; 21 | } 22 | case 'add-dynamic-theme': { 23 | const {filter, fixes, isIFrame} = data; 24 | removeStyle(); 25 | createOrUpdateDynamicTheme(filter, fixes, isIFrame); 26 | break; 27 | } 28 | case 'clean-up': { 29 | removeStyle(); 30 | removeSVGFilter(); 31 | removeDynamicTheme(); 32 | break; 33 | } 34 | } 35 | } 36 | 37 | const port = chrome.runtime.connect({name: 'tab'}); 38 | port.onMessage.addListener(onMessage); 39 | port.onDisconnect.addListener(() => { 40 | logWarn('disconnect'); 41 | cleanDynamicThemeCache(); 42 | }); 43 | -------------------------------------------------------------------------------- /src/inject/style.ts: -------------------------------------------------------------------------------- 1 | import {createNodeAsap, removeNode} from './utils/dom'; 2 | 3 | export function createOrUpdateStyle(css: string) { 4 | createNodeAsap({ 5 | selectNode: () => document.getElementById('dark-reader-style'), 6 | createNode: (target) => { 7 | const style = document.createElement('style'); 8 | style.id = 'dark-reader-style'; 9 | style.type = 'text/css'; 10 | style.textContent = css; 11 | target.appendChild(style); 12 | }, 13 | updateNode: (existing) => { 14 | if (css.replace(/^\s+/gm, '') !== existing.textContent.replace(/^\s+/gm, '')) { 15 | existing.textContent = css; 16 | } 17 | }, 18 | selectTarget: () => document.head, 19 | createTarget: () => { 20 | const head = document.createElement('head'); 21 | document.documentElement.insertBefore(head, document.documentElement.firstElementChild); 22 | return head; 23 | }, 24 | isTargetMutation: (mutation) => mutation.target.nodeName.toLowerCase() === 'head', 25 | }); 26 | } 27 | 28 | export function removeStyle() { 29 | removeNode(document.getElementById('dark-reader-style')); 30 | } 31 | -------------------------------------------------------------------------------- /src/inject/svg-filter.ts: -------------------------------------------------------------------------------- 1 | import {createNodeAsap, removeNode} from './utils/dom'; 2 | 3 | export function createOrUpdateSVGFilter(svgMatrix: string, svgReverseMatrix: string) { 4 | createNodeAsap({ 5 | selectNode: () => document.getElementById('dark-reader-svg'), 6 | createNode: (target) => { 7 | const SVG_NS = 'http://www.w3.org/2000/svg'; 8 | const createMatrixFilter = (id: string, matrix: string) => { 9 | const filter = document.createElementNS(SVG_NS, 'filter'); 10 | filter.id = id; 11 | filter.style.colorInterpolationFilters = 'sRGB'; 12 | 13 | // Fix displaying dynamic content https://bugs.chromium.org/p/chromium/issues/detail?id=647437 14 | filter.setAttribute('x', '0'); 15 | filter.setAttribute('y', '0'); 16 | filter.setAttribute('width', '99999'); 17 | filter.setAttribute('height', '99999'); 18 | 19 | filter.appendChild(createColorMatrix(matrix)); 20 | return filter; 21 | }; 22 | const createColorMatrix = (matrix: string) => { 23 | const colorMatrix = document.createElementNS(SVG_NS, 'feColorMatrix'); 24 | colorMatrix.setAttribute('type', 'matrix'); 25 | colorMatrix.setAttribute('values', matrix); 26 | return colorMatrix; 27 | }; 28 | const svg = document.createElementNS(SVG_NS, 'svg'); 29 | svg.id = 'dark-reader-svg'; 30 | svg.style.height = '0'; 31 | svg.style.width = '0'; 32 | svg.appendChild(createMatrixFilter('dark-reader-filter', svgMatrix)); 33 | svg.appendChild(createMatrixFilter('dark-reader-reverse-filter', svgReverseMatrix)); 34 | target.appendChild(svg); 35 | }, 36 | updateNode: (existing) => { 37 | const existingMatrix = existing.firstChild.firstChild as SVGFEColorMatrixElement; 38 | if (existingMatrix.getAttribute('values') !== svgMatrix) { 39 | existingMatrix.setAttribute('values', svgMatrix); 40 | 41 | // Fix not triggering repaint 42 | const style = document.getElementById('dark-reader-style'); 43 | const css = style.textContent; 44 | style.textContent = ''; 45 | style.textContent = css; 46 | } 47 | }, 48 | selectTarget: () => document.head, 49 | createTarget: () => { 50 | const head = document.createElement('head'); 51 | document.documentElement.insertBefore(head, document.documentElement.firstElementChild); 52 | return head; 53 | }, 54 | isTargetMutation: (mutation) => mutation.target.nodeName.toLowerCase() === 'head', 55 | }); 56 | } 57 | 58 | export function removeSVGFilter() { 59 | removeNode(document.getElementById('dark-reader-svg')); 60 | } 61 | -------------------------------------------------------------------------------- /src/inject/utils/log.ts: -------------------------------------------------------------------------------- 1 | declare const __DEBUG__: boolean; 2 | const DEBUG = __DEBUG__; 3 | 4 | export function logInfo(...args) { 5 | DEBUG && console.info(...args); 6 | } 7 | 8 | export function logWarn(...args) { 9 | DEBUG && console.warn(...args); 10 | } 11 | -------------------------------------------------------------------------------- /src/inject/utils/throttle.ts: -------------------------------------------------------------------------------- 1 | export function throttle(callback: T) { 2 | let pending = false; 3 | let frameId: number = null; 4 | let lastArgs: any[]; 5 | 6 | const throttled: T = ((...args) => { 7 | lastArgs = args; 8 | if (frameId) { 9 | pending = true; 10 | } else { 11 | callback(...lastArgs); 12 | frameId = requestAnimationFrame(() => { 13 | frameId = null; 14 | if (pending) { 15 | callback(...lastArgs); 16 | pending = false; 17 | } 18 | }); 19 | } 20 | }) as any; 21 | 22 | const cancel = () => { 23 | cancelAnimationFrame(frameId); 24 | pending = false; 25 | frameId = null; 26 | }; 27 | 28 | return Object.assign(throttled, {cancel}); 29 | } 30 | 31 | type Task = () => void; 32 | 33 | export function createAsyncTasksQueue() { 34 | const tasks: Task[] = []; 35 | let frameId = null; 36 | 37 | function runTasks() { 38 | let task: Task; 39 | while (task = tasks.shift()) { 40 | task(); 41 | } 42 | frameId = null; 43 | } 44 | 45 | function add(task: Task) { 46 | tasks.push(task); 47 | if (!frameId) { 48 | frameId = requestAnimationFrame(runTasks); 49 | } 50 | } 51 | 52 | function cancel() { 53 | tasks.splice(0); 54 | cancelAnimationFrame(frameId); 55 | frameId = null; 56 | } 57 | 58 | return {add, cancel}; 59 | } 60 | -------------------------------------------------------------------------------- /src/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "applications": { 3 | "gecko": { 4 | "id": "addon@darkreader.org", 5 | "strict_min_version": "54.0" 6 | } 7 | }, 8 | "background": { 9 | "page": "background/index.html" 10 | }, 11 | "permissions": [ 12 | "storage", 13 | "tabs", 14 | "theme", 15 | "" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Dark Reader", 4 | "version": "4.7.12", 5 | "author": "Alexander Shutov", 6 | "description": "__MSG_extension_description__", 7 | "default_locale": "en", 8 | "browser_action": { 9 | "default_title": "Dark Reader", 10 | "default_icon": { 11 | "38": "icons/dr_active_38.png", 12 | "19": "icons/dr_active_19.png" 13 | }, 14 | "default_popup": "ui/popup/index.html" 15 | }, 16 | "icons": { 17 | "16": "icons/dr_16.png", 18 | "48": "icons/dr_48.png", 19 | "128": "icons/dr_128.png" 20 | }, 21 | "background": { 22 | "persistent": true, 23 | "page": "background/index.html" 24 | }, 25 | "content_scripts": [ 26 | { 27 | "matches": [ 28 | "" 29 | ], 30 | "js": [ 31 | "inject/index.js" 32 | ], 33 | "run_at": "document_start", 34 | "all_frames": true, 35 | "match_about_blank": true 36 | } 37 | ], 38 | "permissions": [ 39 | "fontSettings", 40 | "storage", 41 | "tabs", 42 | "" 43 | ], 44 | "commands": { 45 | "toggle": { 46 | "suggested_key": { 47 | "default": "Alt+Shift+D" 48 | }, 49 | "description": "__MSG_toggle_extension__" 50 | }, 51 | "addSite": { 52 | "suggested_key": { 53 | "default": "Alt+Shift+A" 54 | }, 55 | "description": "__MSG_toggle_current_site__" 56 | }, 57 | "switchEngine": { 58 | "description": "__MSG_theme_generation_mode__" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "baseUrl": ".", 5 | "module": "es2015", 6 | "moduleResolution": "node", 7 | "lib": [ 8 | "es2015", 9 | "es2017", 10 | "dom" 11 | ], 12 | "types": [ 13 | "chrome" 14 | ], 15 | "allowJs": true, 16 | "noEmit": true, 17 | "jsx": "react", 18 | "jsxFactory": "m" 19 | }, 20 | "exclude": [ 21 | "node_modules" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/assets/fonts/OpenSans-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/ui/assets/fonts/OpenSans-Light.ttf -------------------------------------------------------------------------------- /src/ui/assets/fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/ui/assets/fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /src/ui/assets/fonts/OpenSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/ui/assets/fonts/OpenSans-SemiBold.ttf -------------------------------------------------------------------------------- /src/ui/assets/images/darkreader-icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/samyk/darkreader/455270f4d4b3e8ac7b15f084ff67a6562210717d/src/ui/assets/images/darkreader-icon-256x256.png -------------------------------------------------------------------------------- /src/ui/assets/images/darkreader-type.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ui/assets/images/ladybug-32.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/ui/connect/index.ts: -------------------------------------------------------------------------------- 1 | import Connector from './connector'; 2 | import {createConnectorMock} from './mock'; 3 | 4 | export default function connect() { 5 | if (typeof chrome === 'undefined' || !chrome.extension) { 6 | return createConnectorMock() as Connector; 7 | } 8 | return new Connector(); 9 | } 10 | -------------------------------------------------------------------------------- /src/ui/connect/mock.ts: -------------------------------------------------------------------------------- 1 | import {ExtensionData, TabInfo, UserSettings} from '../../definitions'; 2 | 3 | export function getMockData(override = {}): ExtensionData { 4 | return Object.assign({ 5 | isEnabled: true, 6 | isReady: true, 7 | settings: { 8 | enabled: true, 9 | theme: { 10 | mode: 1, 11 | brightness: 110, 12 | contrast: 90, 13 | grayscale: 20, 14 | sepia: 10, 15 | useFont: false, 16 | fontFamily: 'Segoe UI', 17 | textStroke: 0, 18 | engine: 'cssFilter', 19 | stylesheet: '', 20 | }, 21 | customThemes: [], 22 | siteList: [], 23 | applyToListedOnly: false, 24 | changeBrowserTheme: false, 25 | notifyOfNews: false, 26 | syncSettings: true, 27 | automation: '', 28 | time: { 29 | activation: '18:00', 30 | deactivation: '9:00', 31 | }, 32 | } as UserSettings, 33 | fonts: [ 34 | 'serif', 35 | 'sans-serif', 36 | 'monospace', 37 | 'cursive', 38 | 'fantasy', 39 | 'system-ui' 40 | ], 41 | news: [], 42 | shortcuts: { 43 | 'addSite': 'Alt+Shift+A', 44 | 'toggle': 'Alt+Shift+D' 45 | }, 46 | devDynamicThemeFixesText: '', 47 | devInversionFixesText: '', 48 | devStaticThemesText: '', 49 | }, override); 50 | } 51 | 52 | export function getMockActiveTabInfo(): TabInfo { 53 | return { 54 | url: 'https://darkreader.org/', 55 | isProtected: false, 56 | isInDarkList: false, 57 | }; 58 | } 59 | 60 | export function createConnectorMock() { 61 | let listener: (data) => void = null; 62 | const data = getMockData(); 63 | const tab = getMockActiveTabInfo(); 64 | const connector = { 65 | getData() { 66 | return Promise.resolve(data); 67 | }, 68 | getActiveTabInfo() { 69 | return Promise.resolve(tab); 70 | }, 71 | subscribeToChanges(callback) { 72 | listener = callback; 73 | }, 74 | changeSettings(settings) { 75 | Object.assign(data.settings, settings); 76 | listener(data); 77 | }, 78 | setTheme(theme) { 79 | Object.assign(data.settings.theme, theme); 80 | listener(data); 81 | }, 82 | setShortcut(command, shortcut) { 83 | Object.assign(data.shortcuts, {[command]: shortcut}); 84 | listener(data); 85 | }, 86 | toggleSitePattern(pattern) { 87 | const index = data.settings.siteList.indexOf(pattern); 88 | if (index >= 0) { 89 | data.settings.siteList.splice(pattern, 1); 90 | } else { 91 | data.settings.siteList.push(pattern); 92 | } 93 | listener(data); 94 | }, 95 | markNewsAsRead(ids) { 96 | // 97 | }, 98 | disconnect() { 99 | // 100 | }, 101 | }; 102 | return connector; 103 | } 104 | -------------------------------------------------------------------------------- /src/ui/controls/button/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import {mergeClass, omitAttrs} from '../utils'; 3 | 4 | export default function Button(props: Malevic.NodeAttrs = {}, ...children) { 5 | const cls = mergeClass('button', props.class); 6 | const attrs = omitAttrs(['class'], props); 7 | 8 | return ( 9 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/controls/button/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .button { 4 | background-color: @color-control-back; 5 | border: @size-border solid @color-border; 6 | box-sizing: content-box; 7 | color: @color-control-fore; 8 | cursor: pointer; 9 | display: inline-block; 10 | height: @size-control-inner; 11 | line-height: @size-text-normal; 12 | min-width: @size-control-inner; 13 | outline: none; 14 | overflow: hidden; 15 | padding: 0; 16 | text-align: center; 17 | transition: background-color @time-slow; 18 | -moz-user-select: none; 19 | user-select: none; 20 | 21 | &:hover { 22 | background-color: @color-control-hover; 23 | transition: background-color @time-fast; 24 | } 25 | 26 | &:active { 27 | background-color: @color-control-active; 28 | } 29 | 30 | &__wrapper { 31 | height: 100%; 32 | width: 100%; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/controls/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import {mergeClass, omitAttrs} from '../utils'; 3 | 4 | export default function CheckBox(props: Malevic.NodeAttrs = {}, ...children) { 5 | const cls = mergeClass('checkbox', props.class); 6 | const attrs = omitAttrs(['class', 'checked', 'onchange'], props); 7 | const check = (domNode: HTMLInputElement) => domNode.checked = Boolean(props.checked); 8 | 9 | return ( 10 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/controls/checkbox/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .checkbox { 4 | align-items: stretch; 5 | background-color: @color-control-back; 6 | border: @size-border solid @color-border; 7 | cursor: pointer; 8 | display: inline-flex; 9 | flex: none; 10 | flex-direction: row; 11 | height: @size-control-inner; 12 | transition: background-color @time-slow; 13 | width: @size-control-inner; 14 | 15 | &:hover { 16 | background-color: @color-control-hover; 17 | transition: background-color @time-fast; 18 | } 19 | 20 | &__input { 21 | display: none; 22 | } 23 | 24 | &__checkmark { 25 | display: inline-block; 26 | height: 100%; 27 | position: relative; 28 | width: @size-control-inner; 29 | 30 | &::before, 31 | &::after { 32 | background-color: @color-control-fore-inactive; 33 | content: ""; 34 | display: inline-block; 35 | height: @size-control-inner * 0.125; 36 | left: @size-control-inner * 0.125; 37 | position: absolute; 38 | top: @size-control-inner * 0.425; 39 | transition: all @time-fast; 40 | width: @size-control-inner * 0.75; 41 | } 42 | 43 | &::before { 44 | transform: skewY(45deg); 45 | } 46 | 47 | &::after { 48 | transform: skewY(-45deg); 49 | } 50 | } 51 | 52 | &__input:checked + &__checkmark { 53 | &::before, 54 | &::after { 55 | background-color: @color-control-fore; 56 | } 57 | 58 | &::before { 59 | top: @size-control-inner * 0.55; 60 | width: @size-control-inner * 0.25; 61 | } 62 | 63 | &::after { 64 | left: @size-control-inner * 0.375; 65 | width: @size-control-inner * 0.5; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ui/controls/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './button'; 2 | import CheckBox from './checkbox'; 3 | import MultiSwitch from './multi-switch'; 4 | import Select from './select'; 5 | import Shortcut from './shortcut'; 6 | import TabPanel from './tab-panel'; 7 | import TextBox from './textbox'; 8 | import TextList from './text-list'; 9 | import TimeRangePicker from './time-range-picker'; 10 | import Toggle from './toggle'; 11 | import UpDown from './updown'; 12 | 13 | export { 14 | Button, 15 | CheckBox, 16 | MultiSwitch, 17 | Select, 18 | Shortcut, 19 | TabPanel, 20 | TextBox, 21 | TextList, 22 | TimeRangePicker, 23 | Toggle, 24 | UpDown, 25 | }; 26 | -------------------------------------------------------------------------------- /src/ui/controls/multi-switch/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | 3 | interface MultiSwitchProps { 4 | class?: string; 5 | options: string[]; 6 | value: string; 7 | onChange: (value: string) => void; 8 | } 9 | 10 | export default function MultiSwitch(props: MultiSwitchProps) { 11 | return ( 12 | 13 | 20 | {props.options.map((option) => ( 21 | option !== props.value && props.onChange(option)} 27 | > 28 | {option} 29 | 30 | ))} 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/controls/multi-switch/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .multi-switch { 4 | background-color: @color-control-back; 5 | border: @size-border solid @color-border; 6 | box-sizing: content-box; 7 | color: @color-control-fore; 8 | display: inline-flex; 9 | height: @size-control-inner; 10 | position: relative; 11 | -moz-user-select: none; 12 | user-select: none; 13 | 14 | &__option { 15 | align-items: center; 16 | cursor: pointer; 17 | display: inline-flex; 18 | justify-content: center; 19 | height: 100%; 20 | overflow: hidden; 21 | position: relative; 22 | transition: background-color @time-slow; 23 | white-space: nowrap; 24 | width: 50%; 25 | 26 | &:hover:not(&--selected) { 27 | background-color: @color-control-hover; 28 | } 29 | } 30 | 31 | &__highlight { 32 | background-color: @color-control-active; 33 | display: inline-block; 34 | height: 100%; 35 | left: 0; 36 | position: absolute; 37 | top: 0; 38 | transition: left @time-fast, width @time-fast; 39 | width: 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ui/controls/select/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .select { 4 | display: inline-flex; 5 | flex-direction: column; 6 | position: relative; 7 | 8 | &__line { 9 | display: inline-flex; 10 | flex-direction: row; 11 | } 12 | 13 | &__textbox.textbox { 14 | border-right: none; 15 | flex: auto; 16 | width: 100%; 17 | } 18 | 19 | &__expand { 20 | border-left: none; 21 | flex: none; 22 | 23 | &__icon { 24 | display: inline-block; 25 | height: 100%; 26 | position: relative; 27 | width: 100%; 28 | 29 | &::before, 30 | &::after { 31 | background-color: @color-control-fore; 32 | content: ""; 33 | display: inline-block; 34 | height: @size-control-inner * 0.125; 35 | position: absolute; 36 | top: @size-control-inner * 0.5; 37 | width: @size-control-inner * 0.375; 38 | z-index: 1; 39 | } 40 | 41 | &::before { 42 | left: @size-control-inner * 0.125; 43 | transform: skewY(45deg); 44 | } 45 | 46 | &::after { 47 | left: @size-control-inner * 0.5; 48 | transform: skewY(-45deg); 49 | } 50 | } 51 | } 52 | 53 | &__list { 54 | background-color: @color-back; 55 | border-bottom: @size-border solid @color-border; 56 | border-left: @size-border solid @color-border; 57 | border-right: @size-border solid @color-border; 58 | box-sizing: border-box; 59 | left: 0; 60 | max-height: 0; 61 | overflow-x: hidden; 62 | overflow-y: auto; 63 | position: absolute; 64 | top: ~"calc(100% - @{size-border})"; 65 | transition: max-height @time-fast; 66 | width: 100%; 67 | z-index: 999; 68 | 69 | &--expanded { 70 | max-height: 12rem; 71 | transition: max-height @time-slow * 2; 72 | } 73 | 74 | &--short { 75 | overflow: hidden; 76 | } 77 | } 78 | 79 | &__option { 80 | align-items: center; 81 | background-color: @color-control-back; 82 | cursor: pointer; 83 | display: flex; 84 | flex: none; 85 | height: @size-control-inner; 86 | flex-direction: row; 87 | padding-left: @size-control-inner / 2 - @size-text-normal / 2; 88 | 89 | &:hover { 90 | background-color: @color-control-hover; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/ui/controls/shortcut/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .shortcut { 4 | color: @color-fore; 5 | cursor: pointer; 6 | display: inline-block; 7 | font-size: @size-text-small; 8 | line-height: @size-text-small-height; 9 | outline: none; 10 | text-align: center; 11 | text-decoration: none; 12 | white-space: pre; 13 | 14 | &:hover { 15 | text-decoration: underline; 16 | } 17 | 18 | &--edit { 19 | color: @color-input-fore-active; 20 | } 21 | 22 | &::before { 23 | content: "✎"; 24 | direction: rtl; 25 | display: inline-block; 26 | line-height: @size-text-small-height; 27 | visibility: hidden; 28 | width: 0; 29 | } 30 | 31 | &:hover, 32 | &--edit { 33 | &::before { 34 | visibility: visible; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/controls/style.less: -------------------------------------------------------------------------------- 1 | @import "./button/style"; 2 | @import "./checkbox/style"; 3 | @import "./multi-switch/style"; 4 | @import "./shortcut/style"; 5 | @import "./tab-panel/style"; 6 | @import "./select/style"; 7 | @import "./text-list/style"; 8 | @import "./textbox/style"; 9 | @import "./time-range-picker/style"; 10 | @import "./toggle/style"; 11 | @import "./updown/style"; 12 | -------------------------------------------------------------------------------- /src/ui/controls/tab-panel/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import Button from '../button'; 3 | import Tab from './tab'; 4 | 5 | interface TabPanelProps { 6 | tabs: { 7 | [name: string]: Malevic.Declaration; 8 | }; 9 | tabLabels: { 10 | [name: string]: string; 11 | }, 12 | activeTab: string; 13 | onSwitchTab: (name: string) => void; 14 | } 15 | 16 | export default function TabPanel(props: TabPanelProps) { 17 | 18 | const tabsNames = Object.keys(props.tabs); 19 | 20 | function isActiveTab(name: string, index: number) { 21 | return (name == null 22 | ? index === 0 23 | : name === props.activeTab 24 | ); 25 | } 26 | 27 | const buttons = tabsNames.map((name, i) => { 28 | const btnCls = { 29 | 'tab-panel__button': true, 30 | 'tab-panel__button--active': isActiveTab(name, i) 31 | }; 32 | return ( 33 | 37 | ); 38 | }); 39 | 40 | const tabs = tabsNames.map((name, i) => ( 41 | 42 | {props.tabs[name]} 43 | 44 | )); 45 | 46 | return ( 47 |
48 |
49 | {buttons} 50 |
51 |
52 | {tabs} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/ui/controls/tab-panel/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .tab-panel { 4 | align-items: stretch; 5 | display: flex; 6 | flex-direction: column; 7 | min-height: 0; 8 | 9 | &__button { 10 | border-bottom: @size-border solid @color-border; 11 | border-left: none; 12 | border-right: none; 13 | border-top: none; 14 | box-sizing: border-box; 15 | color: @color-control-fore-inactive; 16 | font-size: @size-text-large; 17 | font-weight: bold; 18 | height: @size-control-inner + @size-border; 19 | padding: 0 @indent-small + @size-border; 20 | white-space: pre; 21 | 22 | &:hover { 23 | color: @color-fore; 24 | } 25 | 26 | &:active { 27 | background-color: @color-control-hover; 28 | } 29 | 30 | &--active { 31 | border-bottom: none; 32 | border-left: @size-border solid @color-border; 33 | border-right: @size-border solid @color-border; 34 | border-top: @size-border solid @color-border; 35 | color: @color-heading; 36 | padding: 0 @indent-small; 37 | 38 | &:hover { 39 | color: @color-heading; 40 | } 41 | } 42 | } 43 | 44 | &__buttons { 45 | display: flex; 46 | flex: none; 47 | flex-direction: row; 48 | justify-content: center; 49 | 50 | &::before, 51 | &::after { 52 | border-bottom: @size-border solid @color-border; 53 | content: ""; 54 | flex: auto; 55 | } 56 | } 57 | 58 | &__tabs { 59 | display: flex; 60 | flex: auto; 61 | flex-direction: row; 62 | margin-top: @indent-large; 63 | min-height: 0; 64 | } 65 | 66 | &__tab { 67 | background-color: @color-back; 68 | display: flex; 69 | opacity: 0; 70 | overflow: hidden; 71 | pointer-events: none; 72 | transition: width @time-slow, opacity @time-fast; 73 | width: 0; 74 | 75 | &--active { 76 | opacity: 1; 77 | pointer-events: all; 78 | transition: width @time-slow, opacity @time-slow; 79 | width: 100%; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/ui/controls/tab-panel/tab.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | 3 | export default function Tab({isActive}, ...children) { 4 | 5 | const tabCls = { 6 | 'tab-panel__tab': true, 7 | 'tab-panel__tab--active': isActive 8 | }; 9 | 10 | return ( 11 |
12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/ui/controls/text-list/index.tsx: -------------------------------------------------------------------------------- 1 | import {m, getData, getDOMNode} from 'malevic'; 2 | import TextBox from '../textbox'; 3 | import VirtualScroll from '../virtual-scroll'; 4 | 5 | interface TextListProps { 6 | values: string[]; 7 | placeholder: string; 8 | isFocused?: boolean; 9 | class?: string; 10 | onChange: (values: string[]) => void; 11 | } 12 | 13 | const propsStore = new WeakMap(); 14 | 15 | export default function TextList(props: TextListProps) { 16 | function onTextChange(e) { 17 | const index = getData(e.target); 18 | const values = props.values.slice(); 19 | const value = e.target.value.trim(); 20 | if (values.indexOf(value) >= 0) { 21 | return; 22 | } 23 | 24 | if (!value) { 25 | values.splice(index, 1); 26 | } else if (index === values.length) { 27 | values.push(value); 28 | } else { 29 | values.splice(index, 1, value); 30 | } 31 | 32 | props.onChange(values); 33 | } 34 | 35 | function createTextBox(text: string, index: number) { 36 | return ( 37 | 43 | ); 44 | } 45 | 46 | let shouldFocus = false; 47 | 48 | const node = getDOMNode() as Element; 49 | const prevProps = node ? propsStore.get(node) : null; 50 | if (node) { 51 | propsStore.set(node, props); 52 | } 53 | if (node && props.isFocused && ( 54 | !prevProps || 55 | !prevProps.isFocused || 56 | prevProps.values.length < props.values.length 57 | )) { 58 | focusLastNode(node); 59 | } 60 | 61 | function didMount(node: Element) { 62 | propsStore.set(node, props); 63 | if (props.isFocused) { 64 | focusLastNode(node); 65 | } 66 | } 67 | 68 | function focusLastNode(node: Element) { 69 | shouldFocus = true; 70 | requestAnimationFrame(() => { 71 | const inputs = node.querySelectorAll('.text-list__textbox'); 72 | const last = inputs.item(inputs.length - 1) as HTMLInputElement; 73 | last.focus(); 74 | }); 75 | } 76 | 77 | return ( 78 | 85 | )} 86 | items={props.values 87 | .map(createTextBox) 88 | .concat(createTextBox('', props.values.length))} 89 | scrollToIndex={shouldFocus ? props.values.length : -1} 90 | /> 91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /src/ui/controls/text-list/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .text-list { 4 | border-top: @size-border solid @color-border; 5 | margin-bottom: @size-border - @size-border-inner; 6 | min-width: @size-scrollbar-thickness + @indent-small + 2 * @size-border; // Fix scrollTop when animation starts, without this it becomes 0 after focus() 7 | overflow-x: hidden; 8 | overflow-y: auto; 9 | 10 | &__textbox.textbox { 11 | border-bottom: @size-border-inner solid @color-border; 12 | border-left: @size-border solid @color-border; 13 | border-right: @size-border solid @color-border; 14 | border-top: none; 15 | box-sizing: border-box; 16 | flex: none; 17 | height: @size-control-inner + @size-border-inner; 18 | width: 100%; 19 | 20 | &:last-child { 21 | border-bottom: @size-border solid @color-border; 22 | } 23 | } 24 | 25 | &::-webkit-scrollbar { 26 | width: @size-scrollbar-thickness + @indent-small; 27 | 28 | &-thumb { 29 | border-left: @indent-small solid @color-back; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/controls/textbox/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import {mergeClass, omitAttrs} from '../utils'; 3 | 4 | export default function TextBox(props: Malevic.NodeAttrs = {}, text?: string) { 5 | const cls = mergeClass('textbox', props.class); 6 | const attrs = omitAttrs(['class', 'type'], props); 7 | 8 | return ( 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/controls/textbox/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .textbox { 4 | background-color: @color-input-back; 5 | border: @size-border solid @color-border; 6 | box-sizing: content-box; 7 | color: @color-input-fore; 8 | height: @size-control-inner; 9 | line-height: @size-control-inner; 10 | outline: none; 11 | overflow: hidden; 12 | padding: 0; 13 | text-indent: @size-control-inner / 2 - @size-text-normal / 2; 14 | transition: background-color @time-slow; 15 | 16 | &:hover, 17 | &:focus { 18 | background-color: @color-input-hover; 19 | transition: background-color @time-fast; 20 | } 21 | 22 | &:focus { 23 | color: @color-input-fore-active; 24 | } 25 | 26 | &::placeholder { 27 | color: @color-input-fore-placeholder; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ui/controls/time-range-picker/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import TextBox from '../textbox'; 3 | import {getUILanguage} from '../../../utils/locales'; 4 | import {parseTime} from '../../../utils/time'; 5 | 6 | interface TimePickerProps { 7 | startTime: string; 8 | endTime: string; 9 | onChange: ([start, end]: [string, string]) => void; 10 | } 11 | 12 | const is12H = (new Date()).toLocaleTimeString(getUILanguage()).endsWith('M'); 13 | 14 | function toLocaleTime($time: string) { 15 | const [hours, minutes] = parseTime($time); 16 | 17 | const mm = `${minutes < 10 ? '0' : ''}${minutes}`; 18 | 19 | if (is12H) { 20 | const h = (hours === 0 ? 21 | '12' : 22 | hours > 12 ? 23 | (hours - 12) : 24 | hours); 25 | return `${h}:${mm}${hours < 12 ? 'AM' : 'PM'}`; 26 | } 27 | 28 | return `${hours}:${mm}`; 29 | } 30 | 31 | function to24HTime($time: string) { 32 | const [hours, minutes] = parseTime($time); 33 | const mm = `${minutes < 10 ? '0' : ''}${minutes}`; 34 | return `${hours}:${mm}`; 35 | } 36 | 37 | export default function TimeRangePicker(props: TimePickerProps) { 38 | function onStartTimeChange($startTime: string) { 39 | props.onChange([to24HTime($startTime), props.endTime]) 40 | } 41 | 42 | function onEndTimeChange($endTime: string) { 43 | props.onChange([props.startTime, to24HTime($endTime)]) 44 | } 45 | 46 | return ( 47 | 48 | node.value = toLocaleTime(props.startTime)} 52 | didupdate={(node: HTMLInputElement) => node.value = toLocaleTime(props.startTime)} 53 | onchange={(e) => onStartTimeChange((e.target as HTMLInputElement).value)} 54 | onkeypress={(e) => { 55 | if (e.key === 'Enter') { 56 | const input = e.target as HTMLInputElement; 57 | input.blur(); 58 | onStartTimeChange(input.value); 59 | } 60 | }} 61 | 62 | /> 63 | node.value = toLocaleTime(props.endTime)} 67 | didupdate={(node: HTMLInputElement) => node.value = toLocaleTime(props.endTime)} 68 | onchange={(e) => onEndTimeChange((e.target as HTMLInputElement).value)} 69 | onkeypress={(e) => { 70 | if (e.key === 'Enter') { 71 | const input = e.target as HTMLInputElement; 72 | input.blur(); 73 | onEndTimeChange(input.value); 74 | } 75 | }} 76 | /> 77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/ui/controls/time-range-picker/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .time-range-picker { 4 | display: inline-flex; 5 | 6 | &__input { 7 | flex: auto; 8 | text-align: center; 9 | text-indent: 0; 10 | width: 100%; 11 | } 12 | 13 | &__input--start { 14 | border-right: none; 15 | } 16 | 17 | &__input--end { 18 | border-left: @size-border-inner solid @color-border; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/controls/toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import {m, Child} from 'malevic'; 2 | 3 | interface ToggleProps { 4 | checked: boolean; 5 | class?: string; 6 | labelOn: Child; 7 | labelOff: Child; 8 | onChange: (checked: boolean) => void; 9 | } 10 | 11 | export default function Toggle(props: ToggleProps) { 12 | const {checked, onChange} = props; 13 | 14 | const cls = [ 15 | 'toggle', 16 | checked ? 'toggle--checked' : null, 17 | props.class, 18 | ]; 19 | 20 | const clsOn = { 21 | 'toggle__btn': true, 22 | 'toggle__on': true, 23 | 'toggle__btn--active': checked 24 | }; 25 | 26 | const clsOff = { 27 | 'toggle__btn': true, 28 | 'toggle__off': true, 29 | 'toggle__btn--active': !checked 30 | }; 31 | 32 | return ( 33 | 34 | !checked && onChange(true) : null} 37 | > 38 | {props.labelOn} 39 | 40 | checked && onChange(false) : null} 43 | > 44 | {props.labelOff} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/controls/toggle/style.less: -------------------------------------------------------------------------------- 1 | @import "../../theme"; 2 | 3 | .toggle { 4 | background-color: @color-control-back; 5 | border: @size-border solid @color-border; 6 | box-sizing: content-box; 7 | color: @color-control-fore; 8 | display: inline-flex; 9 | height: @size-control-inner; 10 | position: relative; 11 | -moz-user-select: none; 12 | user-select: none; 13 | 14 | &__btn { 15 | align-items: center; 16 | cursor: pointer; 17 | display: inline-flex; 18 | justify-content: center; 19 | height: 100%; 20 | overflow: hidden; 21 | position: relative; 22 | transition: background-color @time-slow; 23 | white-space: nowrap; 24 | width: 50%; 25 | 26 | &:hover:not(&--active) { 27 | background-color: @color-control-hover; 28 | } 29 | } 30 | 31 | &::before { 32 | background-color: @color-control-active; 33 | content: ""; 34 | display: inline-block; 35 | height: 100%; 36 | left: 50%; 37 | position: absolute; 38 | top: 0; 39 | transition: left @time-fast; 40 | width: 50%; 41 | } 42 | 43 | &--checked { 44 | &::before { 45 | left: 0; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/controls/updown/index.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import Button from '../button'; 3 | import Track from './track'; 4 | import {getLocalMessage} from '../../../utils/locales'; 5 | 6 | interface UpDownProps { 7 | value: number; 8 | min: number; 9 | max: number; 10 | step: number; 11 | default: number; 12 | name: string; 13 | onChange: (value: number) => void; 14 | } 15 | 16 | export default function UpDown(props: UpDownProps) { 17 | 18 | const buttonDownCls = { 19 | 'updown__button': true, 20 | 'updown__button--disabled': props.value === props.min 21 | }; 22 | 23 | const buttonUpCls = { 24 | 'updown__button': true, 25 | 'updown__button--disabled': props.value === props.max 26 | }; 27 | 28 | function normalize(x: number) { 29 | const s = Math.round(x / props.step) * props.step; 30 | const exp = Math.floor(Math.log10(props.step)); 31 | if (exp >= 0) { 32 | const m = Math.pow(10, exp); 33 | return Math.round(s / m) * m; 34 | } else { 35 | const m = Math.pow(10, -exp); 36 | return Math.round(s * m) / m; 37 | } 38 | } 39 | 40 | function clamp(x: number) { 41 | return Math.max(props.min, Math.min(props.max, x)); 42 | } 43 | 44 | function onButtonDownClick() { 45 | props.onChange(clamp(normalize(props.value - props.step))); 46 | } 47 | 48 | function onButtonUpClick() { 49 | props.onChange(clamp(normalize(props.value + props.step))); 50 | } 51 | 52 | function onTrackValueChange(trackValue: number) { 53 | props.onChange(clamp(normalize(trackValue * (props.max - props.min) + props.min))); 54 | } 55 | 56 | const trackValue = (props.value - props.min) / (props.max - props.min); 57 | const valueText = (props.value === props.default 58 | ? getLocalMessage('off').toLocaleLowerCase() 59 | : props.value > props.default 60 | ? `+${normalize(props.value - props.default)}` 61 | : `-${normalize(props.default - props.value)}` 62 | ); 63 | 64 | return ( 65 |
66 |
67 | 70 | 75 | 78 |
79 | 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/ui/controls/updown/track.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | 3 | interface TrackProps { 4 | value: number; 5 | label: string; 6 | onChange?: (value: number) => void; 7 | } 8 | 9 | export default function Track(props: TrackProps) { 10 | const valueStyle = {'width': `${props.value * 100}%`}; 11 | const isClickable = props.onChange != null; 12 | 13 | function onMouseDown(e: MouseEvent) { 14 | const targetNode = e.currentTarget as HTMLElement; 15 | const valueNode = targetNode.firstElementChild as HTMLElement; 16 | targetNode.classList.add('track--active'); 17 | 18 | function getValue(clientX: number) { 19 | const rect = targetNode.getBoundingClientRect(); 20 | return (clientX - rect.left) / rect.width; 21 | } 22 | 23 | function setWidth(value: number) { 24 | valueNode.style.width = `${value * 100}%`; 25 | } 26 | 27 | function onMouseMove(e: MouseEvent) { 28 | const value = getValue(e.clientX); 29 | setWidth(value); 30 | } 31 | 32 | function onMouseUp(e: MouseEvent) { 33 | const value = getValue(e.clientX); 34 | props.onChange(value); 35 | cleanup(); 36 | } 37 | 38 | function onKeyPress(e: KeyboardEvent) { 39 | if (e.key === 'Escape') { 40 | setWidth(props.value); 41 | cleanup(); 42 | } 43 | } 44 | 45 | function cleanup() { 46 | window.removeEventListener('mousemove', onMouseMove); 47 | window.removeEventListener('mouseup', onMouseUp); 48 | window.removeEventListener('keypress', onKeyPress); 49 | targetNode.classList.remove('track--active'); 50 | } 51 | 52 | window.addEventListener('mousemove', onMouseMove); 53 | window.addEventListener('mouseup', onMouseUp); 54 | window.addEventListener('keypress', onKeyPress); 55 | 56 | const value = getValue(e.clientX); 57 | setWidth(value); 58 | } 59 | 60 | return ( 61 | 68 | 69 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/ui/controls/utils.ts: -------------------------------------------------------------------------------- 1 | import {classes} from 'malevic'; 2 | 3 | function toArray(x: T | T[]) { 4 | return Array.isArray(x) ? x : [x]; 5 | } 6 | 7 | export function mergeClass( 8 | cls: string | {[cls: string]: any;} | (string | {[cls: string]: any;})[], 9 | propsCls: string | {[cls: string]: any;} | (string | {[cls: string]: any;})[] 10 | ) { 11 | const normalized = toArray(cls).concat(toArray(propsCls)); 12 | return classes(...normalized); 13 | } 14 | 15 | export function omitAttrs(omit: string[], attrs: Malevic.NodeAttrs) { 16 | const result: Malevic.NodeAttrs = {}; 17 | Object.keys(attrs).forEach((key) => { 18 | if (omit.indexOf(key) < 0) { 19 | result[key] = attrs[key]; 20 | } 21 | }); 22 | return result; 23 | } 24 | -------------------------------------------------------------------------------- /src/ui/devtools/components/body.tsx: -------------------------------------------------------------------------------- 1 | import {m} from 'malevic'; 2 | import withState, {useState} from 'malevic/state'; 3 | import {Button} from '../../controls'; 4 | import ThemeEngines from '../../../generators/theme-engines'; 5 | import {DEVTOOLS_DOCS_URL} from '../../../utils/links'; 6 | import {ExtWrapper} from '../../../definitions'; 7 | 8 | interface BodyProps extends ExtWrapper { 9 | } 10 | 11 | function Body({data, actions}: BodyProps) { 12 | const {state, setState} = useState({errorText: null as string}) 13 | let textNode: HTMLTextAreaElement; 14 | 15 | const wrapper = (data.settings.theme.engine === ThemeEngines.staticTheme 16 | ? { 17 | header: 'Static Theme Editor', 18 | fixesText: data.devStaticThemesText, 19 | apply: (text) => actions.applyDevStaticThemes(text), 20 | reset: () => actions.resetDevStaticThemes(), 21 | } : data.settings.theme.engine === ThemeEngines.cssFilter || data.settings.theme.engine === ThemeEngines.svgFilter ? { 22 | header: 'Inversion Fix Editor', 23 | fixesText: data.devInversionFixesText, 24 | apply: (text) => actions.applyDevInversionFixes(text), 25 | reset: () => actions.resetDevInversionFixes(), 26 | } : { 27 | header: 'Dynamic Theme Editor', 28 | fixesText: data.devDynamicThemeFixesText, 29 | apply: (text) => actions.applyDevDynamicThemeFixes(text), 30 | reset: () => actions.resetDevDynamicThemeFixes(), 31 | }); 32 | 33 | function onTextRender(node) { 34 | textNode = node; 35 | if (!state.errorText) { 36 | textNode.value = wrapper.fixesText; 37 | } 38 | } 39 | 40 | async function apply() { 41 | const text = textNode.value; 42 | try { 43 | await wrapper.apply(text); 44 | setState({errorText: null}); 45 | } catch (err) { 46 | setState({ 47 | errorText: String(err), 48 | }); 49 | } 50 | } 51 | 52 | function reset() { 53 | wrapper.reset(); 54 | setState({errorText: null}); 55 | } 56 | 57 | return ( 58 | 59 |
60 | 61 |

Developer Tools

62 |
63 |

{wrapper.header}

64 |