├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets └── get-addon-badge-firefox.png └── scr ├── _locales ├── de │ ├── amo.md │ └── messages.json ├── en │ ├── amo.md │ └── messages.json ├── en_US │ ├── amo.md │ └── messages.json ├── es_ES │ ├── amo.md │ └── messages.json ├── fr_FR │ ├── amo.md │ └── messages.json ├── zh │ ├── amo.md │ └── messages.json └── zh_CN │ ├── amo.md │ └── messages.json ├── atbc.js ├── background.js ├── colour.js ├── default_values.js ├── elements.js ├── images ├── ATBC_128.png ├── ATBC_16.png ├── ATBC_32.png ├── ATBC_48.png └── ATBC_96.png ├── manifest.json ├── options ├── options.css ├── options.html └── options.js ├── popup ├── popup.css ├── popup.html └── popup.js ├── preference.js ├── shared.css └── utility.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ["https://www.paypal.com/donate/?hosted_button_id=T5GL8WC7SVLLC", "https://www.buymeacoffee.com/easonwong"] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node,macos,linux 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,node,macos,linux 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | 20 | ### macOS ### 21 | # General 22 | .DS_Store 23 | .AppleDouble 24 | .LSOverride 25 | 26 | # Icon must end with two \r 27 | Icon 28 | 29 | 30 | # Thumbnails 31 | ._* 32 | 33 | # Files that might appear in the root of a volume 34 | .DocumentRevisions-V100 35 | .fseventsd 36 | .Spotlight-V100 37 | .TemporaryItems 38 | .Trashes 39 | .VolumeIcon.icns 40 | .com.apple.timemachine.donotpresent 41 | 42 | # Directories potentially created on remote AFP share 43 | .AppleDB 44 | .AppleDesktop 45 | Network Trash Folder 46 | Temporary Items 47 | .apdisk 48 | 49 | ### Node ### 50 | # Logs 51 | logs 52 | *.log 53 | npm-debug.log* 54 | yarn-debug.log* 55 | yarn-error.log* 56 | lerna-debug.log* 57 | .pnpm-debug.log* 58 | 59 | # Diagnostic reports (https://nodejs.org/api/report.html) 60 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 61 | 62 | # Runtime data 63 | pids 64 | *.pid 65 | *.seed 66 | *.pid.lock 67 | 68 | # Directory for instrumented libs generated by jscoverage/JSCover 69 | lib-cov 70 | 71 | # Coverage directory used by tools like istanbul 72 | coverage 73 | *.lcov 74 | 75 | # nyc test coverage 76 | .nyc_output 77 | 78 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 79 | .grunt 80 | 81 | # Bower dependency directory (https://bower.io/) 82 | bower_components 83 | 84 | # node-waf configuration 85 | .lock-wscript 86 | 87 | # Compiled binary addons (https://nodejs.org/api/addons.html) 88 | build/Release 89 | 90 | # Dependency directories 91 | node_modules/ 92 | jspm_packages/ 93 | 94 | # Snowpack dependency directory (https://snowpack.dev/) 95 | web_modules/ 96 | 97 | # TypeScript cache 98 | *.tsbuildinfo 99 | 100 | # Optional npm cache directory 101 | .npm 102 | 103 | # Optional eslint cache 104 | .eslintcache 105 | 106 | # Optional stylelint cache 107 | .stylelintcache 108 | 109 | # Microbundle cache 110 | .rpt2_cache/ 111 | .rts2_cache_cjs/ 112 | .rts2_cache_es/ 113 | .rts2_cache_umd/ 114 | 115 | # Optional REPL history 116 | .node_repl_history 117 | 118 | # Output of 'npm pack' 119 | *.tgz 120 | 121 | # Yarn Integrity file 122 | .yarn-integrity 123 | 124 | # dotenv environment variable files 125 | .env 126 | .env.development.local 127 | .env.test.local 128 | .env.production.local 129 | .env.local 130 | 131 | # parcel-bundler cache (https://parceljs.org/) 132 | .cache 133 | .parcel-cache 134 | 135 | # Next.js build output 136 | .next 137 | out 138 | 139 | # Nuxt.js build / generate output 140 | .nuxt 141 | dist 142 | 143 | # Gatsby files 144 | .cache/ 145 | # Comment in the public line in if your project uses Gatsby and not Next.js 146 | # https://nextjs.org/blog/next-9-1#public-directory-support 147 | # public 148 | 149 | # vuepress build output 150 | .vuepress/dist 151 | 152 | # vuepress v2.x temp and cache directory 153 | .temp 154 | 155 | # Docusaurus cache and generated files 156 | .docusaurus 157 | 158 | # Serverless directories 159 | .serverless/ 160 | 161 | # FuseBox cache 162 | .fusebox/ 163 | 164 | # DynamoDB Local files 165 | .dynamodb/ 166 | 167 | # TernJS port file 168 | .tern-port 169 | 170 | # Stores VSCode versions used for testing VSCode extensions 171 | .vscode-test 172 | 173 | # yarn v2 174 | .yarn/cache 175 | .yarn/unplugged 176 | .yarn/build-state.yml 177 | .yarn/install-state.gz 178 | .pnp.* 179 | 180 | ### Node Patch ### 181 | # Serverless Webpack directories 182 | .webpack/ 183 | 184 | # Optional stylelint cache 185 | 186 | # SvelteKit build / generate output 187 | .svelte-kit 188 | 189 | ### VisualStudioCode ### 190 | .vscode/* 191 | !.vscode/settings.json 192 | !.vscode/tasks.json 193 | !.vscode/launch.json 194 | !.vscode/extensions.json 195 | !.vscode/*.code-snippets 196 | 197 | # Local History for Visual Studio Code 198 | .history/ 199 | 200 | # Built Visual Studio Code Extensions 201 | *.vsix 202 | 203 | ### VisualStudioCode Patch ### 204 | # Ignore all local history of files 205 | .history 206 | .ionide 207 | 208 | # Support for Project snippet scope 209 | 210 | ### Windows ### 211 | # Windows thumbnail cache files 212 | Thumbs.db 213 | Thumbs.db:encryptable 214 | ehthumbs.db 215 | ehthumbs_vista.db 216 | 217 | # Dump file 218 | *.stackdump 219 | 220 | # Folder config file 221 | [Dd]esktop.ini 222 | 223 | # Recycle Bin used on file shares 224 | $RECYCLE.BIN/ 225 | 226 | # Windows Installer files 227 | *.cab 228 | *.msi 229 | *.msix 230 | *.msm 231 | *.msp 232 | 233 | # Windows shortcuts 234 | *.lnk 235 | 236 | # End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,node,macos,linux 237 | 238 | ### Customs ### 239 | # Backup folders 240 | Backup 241 | Pictures -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Eason Wong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Icon](scr/images/ATBC_128.png) 2 | ![Mozilla Add-on Users](https://img.shields.io/amo/users/adaptive-tab-bar-colour) 3 | ![Mozilla Add-on Rating](https://img.shields.io/amo/stars/adaptive-tab-bar-colour) 4 | ![Mozilla Add-on](https://img.shields.io/amo/v/adaptive-tab-bar-colour?color=blue&label=version) 5 | ![Sponsors](https://img.shields.io/badge/sponsors-21-green) 6 | 7 | # Adaptive Tab Bar Colour 8 | 9 | Changes the colour of Firefox theme to match the website’s appearance. 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | ## What Does the Add-on Do? 18 | 19 | While you browse the web, this add-on changes the theme of Firefox to match the appearance of the website you are viewing — just like how macOS Safari tints its tab bar. 20 | 21 | 22 | 23 |
24 | 25 | ## Works Well With: 26 | 27 |
    28 |
  1. Dark Reader
  2. 29 |
  3. Stylish
  4. 30 |
  5. Dark Mode Website Switcher
  6. 31 |
  7. automaticDark
  8. 32 |
33 | 34 |
35 | 36 | ## Incompatible With: 37 | 38 |
    39 |
  1. Firefox versions older than 112.0 (released in April 2023)
  2. 40 |
  3. Adaptive Theme Creator
  4. 41 |
  5. Chameleon Dynamic Theme
  6. 42 |
  7. VivaldiFox
  8. 43 |
  9. Envify
  10. 44 |
  11. and any other add-on that changes the Firefox theme
  12. 45 |
46 | 47 |
48 | 49 | ## If You’re Using a CSS Theme: 50 | 51 | A CSS theme can work with ATBC (Adaptive Tab Bar Colour) when system colour variables are used (e.g. `--lwt-accent-color` for tab bar colour). [This](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) is an example of an ATBC-compatible CSS theme. 52 | 53 | 54 | 55 |
56 | 57 | ## If You’re Using Linux with a GTK Theme: 58 | 59 | Firefox’s titlebar buttons may revert to the Windows style. To prevent this, open Advanced Preferences (`about:config`) and set `widget.gtk.non-native-titlebar-buttons.enabled` to `false`. (Thanks to [@anselstetter](https://github.com/anselstetter/)) 60 | 61 |
62 | 63 | ## Safety Reminder: 64 | 65 | Beware of malicious web UIs: Please distinguish between the browser’s UI and the web UI, see The Line of Death. (Thanks to u/KazaHesto) 66 | -------------------------------------------------------------------------------- /assets/get-addon-badge-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/assets/get-addon-badge-firefox.png -------------------------------------------------------------------------------- /scr/_locales/de/amo.md: -------------------------------------------------------------------------------- 1 | **Was macht die Erweiterung?** 2 | 3 | Während du im Web surfst, ändert diese Erweiterung das Theme von Firefox, um es an das Erscheinungsbild der von dir besuchten Website anzupassen – ähnlich wie macOS Safari seine Tableiste einfärbt. 4 | 5 | **Funktioniert gut mit:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **Inkompatibel mit:** 12 | 13 | - Firefox-Versionen älter als [112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) (veröffentlicht im April 2023) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - und jede andere Erweiterung, die das Firefox-Theme ändert 19 | 20 | **Falls du ein CSS-Theme verwendest:** 21 | 22 | Ein CSS-Theme kann mit Anpassender Tableistenfarbe funktionieren, wenn Systemfarbvariablen verwendet werden (z. B. `--lwt-accent-color` für die Farbe der Tableiste). [Hier](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) ist ein Beispiel für ein kompatibles CSS-Theme. 23 | 24 | **Falls du Linux mit einem GTK-Theme verwendest:** 25 | 26 | Die Titelleisten-Schaltflächen von Firefox könnten auf den Windows-Stil zurückgesetzt werden. Um dies zu verhindern, öffne die erweiterten Einstellungen (`about:config`) und setze `widget.gtk.non-native-titlebar-buttons.enabled` auf `false`. (Dank an [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **Sicherheitswarnung:** 29 | 30 | Achte auf bösartige Web-UIs: Unterscheide zwischen der Benutzeroberfläche des Browsers und der Benutzeroberfläche einer Website. Siehe [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/). (Dank an [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | Gib diesem Projekt gerne einen Stern auf GitHub: [https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) -------------------------------------------------------------------------------- /scr/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Anpassende Tableistenfarbe" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "Ändert die Farbe des Firefox-Themes, um dem Erscheinungsbild der Website zu entsprechen." 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "Einstellungsdaten könnten beschädigt sein. Wenn das Problem weiterhin besteht, installieren Sie das Add-on bitte neu. Entschuldigung für die Unannehmlichkeiten." 16 | }, 17 | 18 | "loading": { 19 | "message": "Lädt…" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "Theme-Editor" 24 | }, 25 | 26 | "siteList": { 27 | "message": "Seitenliste" 28 | }, 29 | 30 | "advanced": { 31 | "message": "Erweitert" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "Tableiste" 36 | }, 37 | 38 | "background": { 39 | "message": "Hintergrund" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "Ausgewählter Tab" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "Werkzeugleiste" 48 | }, 49 | 50 | "border": { 51 | "message": "Rahmen" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "URL-Leiste" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "Hintergrund bei Fokus" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "Seitenleiste" 64 | }, 65 | 66 | "popUp": { 67 | "message": "Pop-up" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "Diese Policy wird ignoriert" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "URL / Regex / Wildcard / Domain" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "Add-on nicht gefunden" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "eine Farbe angeben" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "Theme-Farbe an / aus" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "Farbe aus Element" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "Jedes Format" 96 | }, 97 | 98 | "use": { 99 | "message": "An" 100 | }, 101 | 102 | "ignore": { 103 | "message": "Aus" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "Query-Selector" 108 | }, 109 | 110 | "delete": { 111 | "message": "Löschen" 112 | }, 113 | 114 | "reset": { 115 | "message": "Zurücksetzen" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "Neue Policy hinzufügen" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "Helle Tableiste erlauben" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "Erlaubt der Tableiste, helle Farben zu verwenden (dunkler Text auf heller Tableiste)." 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "Dunkle Tableiste erlauben" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "Erlaubt der Tableiste, dunkle Farben zu verwenden (heller Text auf dunkler Tableiste)." 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "Dynamische Farbaktualisierung" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "Aktualisiert die Farbe der Tableiste während des Surfens auf einer Webseite ständig." 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "Theme-Farbe der Website ignorieren" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "Einige Websites geben selbst eine Theme-Farbe an. Aktivieren Sie diese Option, um diese Farben zu ignorieren." 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "Minimaler Kontrast" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "Stellt den minimalen Kontrast zwischen Tableiste und ihrem Text ein. Die Farbe der Tableiste wird automatisch angepasst, wenn der Kontrast nicht erreicht wird." 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "Im hellen Modus" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "Im dunklen Modus" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "Startseitenfarbe: hell" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "Startseitenfarbe: dunkel" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "Fallback-Farbe: hell" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "Fallback-Farbe: dunkel" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "Einstellungen exportieren" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "Einstellungen wurden in Ihren Download-Ordner exportiert." 192 | }, 193 | 194 | "importSettings": { 195 | "message": "Einstellungen importieren" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "Einstellungen wurden importiert." 200 | }, 201 | 202 | "importFailed": { 203 | "message": "Einstellungen konnten nicht importiert werden. Bitte versuchen Sie es erneut." 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "Theme-Editor zurücksetzen" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "Sind Sie sicher, dass Sie den Theme-Editor zurücksetzen möchten?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "Seitenliste zurücksetzen" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "Sind Sie sicher, dass Sie die Seitenliste zurücksetzen möchten?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "Erweiterte Einstellungen zurücksetzen" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "Sind Sie sicher, dass Sie die erweiterten Einstellungen zurücksetzen möchten?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "Ein Problem melden / einen Vorschlag einreichen / dieses Add-on übersetzen" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "Weitere Einstellungen…" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "Optionenseite öffnen" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "Farbe für PDF-Viewer verwenden" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "Farbe für JSON-Viewer verwenden" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "Diese Seite wird vom Browser geschützt" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "Farbe für Plain-Text-Viewer verwenden" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "Ein Fehler ist aufgetreten, Fallback-Farbe wird verwendet" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "Die Farbe der Tableiste für die Startseite kann in den Einstellungen konfiguriert werden" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "Spezifizierte Farbe für Seiten, die zu " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": " gehören, wird verwendet" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "Standardfarbe verwenden" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "Standardfarbe verwenden" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "Empfohlene Farbe für Seiten, die zu " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " gehören, ist verfügbar" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "Empfohlene Farbe verwenden" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "Empfohlene Farbe verwenden" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "Klicken Sie auf „Eine Farbe angeben“, um die Einstellungen zu öffnen und die Farbe der Tableiste für Seiten, die zu " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": " gehören, anzugeben" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "Eine Farbe angeben" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "Einstellungen öffnen und eine Farbe für die Seiten des Add-ons angeben" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "Die von der Website definierte Theme-Farbe wird verwendet, anstatt standardmäßig ignoriert zu werden" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "Theme-Farbe wurde nicht gefunden, Farbe wird von der Webseite gewählt" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "Die von der Website definierte Theme-Farbe wird ignoriert" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "Theme-Farbe verwenden" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "Die von der Website definierte Theme-Farbe verwenden" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "Die Farbe wird von einem HTML-Element ausgewählt, das mit " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": " übereinstimmt" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "Das HTML-Element, das mit " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": " übereinstimmt, konnte nicht gefunden werden. Die Farbe wird stattdessen von der Webseite gewählt" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "Ein Fehler ist aufgetreten, während nach dem HTML-Element gesucht wurde, das mit " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": " übereinstimmt. Die Farbe wird stattdessen von der Webseite gewählt" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "Die Farbe für diese Seite ist in den Einstellungen angegeben" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "Farbe für Bild-Viewer verwenden" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "Die von der Website definierte Theme-Farbe wird verwendet" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "Theme-Farbe ignorieren" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "Die von der Website definierte Theme-Farbe ignorieren" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "Keine Farbe verfügbar, Fallback-Farbe wird verwendet" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "Farbe erfolgreich von der Webseite gewählt" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "Die Originalfarbe wurde angepasst, um den minimalen Kontrast zu gewährleisten" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/en/amo.md: -------------------------------------------------------------------------------- 1 | **What Does the Add-on Do?** 2 | 3 | While you browse the web, this add-on changes the theme of Firefox to match the appearance of the website you are viewing — just like how macOS Safari tints its tab bar. 4 | 5 | **Works Well With:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **Incompatible With:** 12 | 13 | - Firefox versions older than [112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) (released in April 2023) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - and any other add-on that changes the Firefox theme 19 | 20 | **If You’re Using a CSS Theme:** 21 | 22 | A CSS theme can work with ATBC (Adaptive Tab Bar Colour) when system colour variables are used (e.g. `--lwt-accent-color` for tab bar colour). [This](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) is an example of an ATBC-compatible CSS theme. 23 | 24 | **If You’re Using Linux with a GTK Theme:** 25 | 26 | Firefox’s titlebar buttons may revert to the Windows style. To prevent this, open Advanced Preferences (`about:config`) and set `widget.gtk.non-native-titlebar-buttons.enabled` to `false`. (Thanks to [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **Safety Reminder:** 29 | 30 | Beware of malicious web UIs: Please distinguish between the browser’s UI and the web UI. See [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/). (Thanks to [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | Feel free to star this project on GitHub: [https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) -------------------------------------------------------------------------------- /scr/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Adaptive Tab Bar Colour" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "Changes the colour of Firefox theme to match the website’s appearance." 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "Preference data may be corrupted. If the issue persists, please reinstall the add-on. We apologize for the inconvenience." 16 | }, 17 | 18 | "loading": { 19 | "message": "Loading…" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "Theme builder" 24 | }, 25 | 26 | "siteList": { 27 | "message": "Site list" 28 | }, 29 | 30 | "advanced": { 31 | "message": "Advanced" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "Tab bar" 36 | }, 37 | 38 | "background": { 39 | "message": "Background" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "Selected tab" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "Toolbar" 48 | }, 49 | 50 | "border": { 51 | "message": "Border" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "URL bar" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "Background on focus" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "Sidebar" 64 | }, 65 | 66 | "popUp": { 67 | "message": "Pop-up" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "This policy will be ignored" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "URL / regex / wildcard / domain" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "Add-on not found" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "specify a colour" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "use / ignore theme colour" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "pick colour from element" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "Any format" 96 | }, 97 | 98 | "use": { 99 | "message": "Use" 100 | }, 101 | 102 | "ignore": { 103 | "message": "Ignore" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "Query selector" 108 | }, 109 | 110 | "delete": { 111 | "message": "Delete" 112 | }, 113 | 114 | "reset": { 115 | "message": "Reset" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "Add a new rule" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "Allow light tab bar" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "Allow tab bar to use bright colour (dark text on light tab bar)." 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "Allow dark tab bar" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "Allow tab bar to use dark colour (light text on dark tab bar)." 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "Dynamic colour update" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "Keeps updating tab bar colour while browsing within a web page." 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "Ignore website’s theme colour" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "Some websites provide a theme colour themselves. Turn on this option to ignore these colours." 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "Minimum contrast" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "Set the minimum contrast ratio between the tab bar and text. The tab bar colour adjusts automatically if the ratio is unmet." 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "In light mode" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "In dark mode" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "Home page colour: light" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "Home page colour: dark" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "Fallback colour: light" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "Fallback colour: dark" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "Export settings" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "Settings have been exported to your download folder." 192 | }, 193 | 194 | "importSettings": { 195 | "message": "Import settings" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "Settings have been imported." 200 | }, 201 | 202 | "importFailed": { 203 | "message": "Failed to import settings. Please try again." 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "Reset theme builder" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "Are you sure you want to reset the theme builder?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "Reset site list" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "Are you sure you want to reset the site list?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "Reset advanced settings" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "Are you sure you want to reset the advanced settings?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "Report an issue / submit a suggestion / translate this add-on" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "More settings…" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "Open option page" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "Using colour for PDF viewer" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "Using colour for JSON viewer" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "This page is protected by browser" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "Using colour for plain text viewer" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "An error occurred, using fallback colour" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "Tab bar colour for home page can be configured in settings" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "Using specified colour for pages related to " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": "__EMPTY__" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "Use default colour" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "Use default colour" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "Recommended colour for pages related to " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " is available" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "Use recommended colour" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "Use the recommended colour" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "Click “Specify a color” to open settings and specify tab bar colour for pages related to " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": "__EMPTY__" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "Specify a colour" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "Open settings and specify a colour to pages related to the add-on" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "Theme colour defined by the website is used, instead of being ignored by default" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "Theme colour is not found, colour is picked from the web page" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "Theme colour defined by the website is ignored" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "Use theme colour" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "Use theme colour defined by the website" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "Colour is picked from an HTML element matching " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": "__EMPTY__" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "Cannot find the HTML element matching " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": ", colour is picked from the web page instead" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "An error occurred while looking for the HTML element matching " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": ", colour is picked from the web page instead" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "Colour for this site is specified in the settings" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "Using colour for image viewer" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "Using theme colour defined by the website" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "Ignore theme colour" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "Ignore theme colour defined by the website" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "No colour is available, using fallback colour" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "Colour is picked from the web page" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "The original colour has been adjusted to ensure minimum contrast" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/en_US/amo.md: -------------------------------------------------------------------------------- 1 | **What Does the Add-on Do?** 2 | 3 | While you browse the web, this add-on changes the theme of Firefox to match the appearance of the website you are viewing — just like how macOS Safari tints its tab bar. 4 | 5 | **Works Well With:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **Incompatible With:** 12 | 13 | - Firefox versions older than [112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) (released in April 2023) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - and any other add-on that changes the Firefox theme 19 | 20 | **If You’re Using a CSS Theme:** 21 | 22 | A CSS theme can work with ATBC (Adaptive Tab Bar Color) when system color variables are used (e.g. `--lwt-accent-color` for tab bar color). [This](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) is an example of an ATBC-compatible CSS theme. 23 | 24 | **If You’re Using Linux with a GTK Theme:** 25 | 26 | Firefox’s titlebar buttons may revert to the Windows style. To prevent this, open Advanced Preferences (`about:config`) and set `widget.gtk.non-native-titlebar-buttons.enabled` to `false`. (Thanks to [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **Safety Reminder:** 29 | 30 | Beware of malicious web UIs: Please distinguish between the browser’s UI and the web UI. See [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/). (Thanks to [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | Feel free to star this project on GitHub: [https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) -------------------------------------------------------------------------------- /scr/_locales/en_US/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Adaptive Tab Bar Color" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "Changes the color of Firefox theme to match the website’s appearance." 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "Preference data may be corrupted. If the issue persists, please reinstall the add-on. We apologize for the inconvenience." 16 | }, 17 | 18 | "loading": { 19 | "message": "Loading…" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "Theme builder" 24 | }, 25 | 26 | "siteList": { 27 | "message": "Site list" 28 | }, 29 | 30 | "advanced": { 31 | "message": "Advanced" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "Tab bar" 36 | }, 37 | 38 | "background": { 39 | "message": "Background" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "Selected tab" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "Toolbar" 48 | }, 49 | 50 | "border": { 51 | "message": "Border" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "URL bar" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "Background on focus" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "Sidebar" 64 | }, 65 | 66 | "popUp": { 67 | "message": "Pop-up" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "This policy will be ignored" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "URL / regex / wildcard / domain" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "Add-on not found" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "specify a color" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "use / ignore theme color" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "pick color from element" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "Any format" 96 | }, 97 | 98 | "use": { 99 | "message": "Use" 100 | }, 101 | 102 | "ignore": { 103 | "message": "Ignore" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "Query selector" 108 | }, 109 | 110 | "delete": { 111 | "message": "Delete" 112 | }, 113 | 114 | "reset": { 115 | "message": "Reset" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "Add a new rule" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "Allow light tab bar" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "Allow tab bar to use bright color (dark text on light tab bar)." 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "Allow dark tab bar" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "Allow tab bar to use dark color (light text on dark tab bar)." 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "Dynamic color update" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "Keeps updating tab bar color while browsing within a web page." 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "Ignore website’s theme color" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "Some websites provide a theme color themselves. Turn on this option to ignore these colors." 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "Minimum contrast" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "Set the minimum contrast ratio between the tab bar and text. The tab bar color adjusts automatically if the ratio is unmet." 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "In light mode" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "In dark mode" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "Home page color: light" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "Home page color: dark" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "Fallback color: light" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "Fallback color: dark" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "Export settings" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "Settings have been exported to your download folder." 192 | }, 193 | 194 | "importSettings": { 195 | "message": "Import settings" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "Settings have been imported." 200 | }, 201 | 202 | "importFailed": { 203 | "message": "Failed to import settings. Please try again." 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "Reset theme builder" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "Are you sure you want to reset the theme builder?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "Reset site list" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "Are you sure you want to reset the site list?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "Reset advanced settings" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "Are you sure you want to reset the advanced settings?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "Report an issue / submit a suggestion / translate this add-on" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "More settings…" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "Open option page" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "Using color for PDF viewer" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "Using color for JSON viewer" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "This page is protected by browser" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "Using color for plain text viewer" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "An error occurred, using fallback color" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "Tab bar color for home page can be configured in settings" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "Using specified color for pages related to " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": "__EMPTY__" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "Use default color" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "Use default color" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "Recommended color for pages related to " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " is available" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "Use recommended color" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "Use the recommended color" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "Click “Specify a color” to open settings and specify tab bar color for pages related to " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": "__EMPTY__" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "Specify a color" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "Open settings and specify a color to pages related to the add-on" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "Theme color defined by the website is used, instead of being ignored by default" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "Theme color is not found, color is picked from the web page" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "Theme color defined by the website is ignored" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "Use theme color" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "Use theme color defined by the website" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "Color is picked from an HTML element matching " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": "__EMPTY__" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "Cannot find the HTML element matching " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": ", color is picked from the web page instead" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "An error occurred while looking for the HTML element matching " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": ", color is picked from the web page instead" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "Color for this site is specified in the settings" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "Using color for image viewer" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "Using theme color defined by the website" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "Ignore theme color" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "Ignore theme color defined by the website" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "No color is available, using fallback color" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "Color is picked from the web page" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "The original color has been adjusted to ensure minimum contrast" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/es_ES/amo.md: -------------------------------------------------------------------------------- 1 | **¿Qué Hace El Complemento?** 2 | 3 | Mientras navegas por la web, este complemento cambia el tema de Firefox para que coincida con la apariencia de la página web que estás viendo. Esto es similar a cómo Safari tiñe su barra de pestañas en macOS. 4 | 5 | **Funciona Bien Con:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **Incompatible Con:** 12 | 13 | - Firefox inferior a la [versión 112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) (lanzada en abril de 2023) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - y cualquier otro complemento que modifique el tema de Firefox 19 | 20 | **Si estás usando un tema CSS:** 21 | 22 | Un tema CSS puede funcionar con ATBC (Adaptive Tab Bar Colour) cuando se utilizan variables de color del sistema (por ejemplo, `--lwt-accent-color` para el color de la barra de pestañas). [Este](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) es un ejemplo de un tema CSS compatible con ATBC. 23 | 24 | **Si estás usando Linux con un tema GTK:** 25 | 26 | Los botones de la barra de título de Firefox pueden volver al estilo de Windows. Para evitar esto, abre las Preferencias avanzadas (`about:config`) y establece `widget.gtk.non-native-titlebar-buttons.enabled` en `false`. (Gracias a [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **Advertencia De Seguridad:** 29 | 30 | Cuidado con las interfaces web maliciosas: distinga entre la interfaz de usuario del navegador y la interfaz de usuario web. Consulte [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/). (Gracias a [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | Siéntete libre de dar una estrella a este proyecto en GitHub: [https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) 33 | -------------------------------------------------------------------------------- /scr/_locales/es_ES/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Adaptive Tab Bar Colour" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "Cambia el color del tema de Firefox para que coincida con la apariencia del sitio web." 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "Los datos de preferencias podrían estar corruptos. Si el problema persiste, por favor reinstala la extensión. ¡Disculpa las molestias!" 16 | }, 17 | 18 | "loading": { 19 | "message": "Cargando…" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "Creador de temas" 24 | }, 25 | 26 | "siteList": { 27 | "message": "Lista de sitios" 28 | }, 29 | 30 | "advanced": { 31 | "message": "Avanzado" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "Barra de pestañas" 36 | }, 37 | 38 | "background": { 39 | "message": "Fondo" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "Pestaña seleccionada" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "Barra de herramientas" 48 | }, 49 | 50 | "border": { 51 | "message": "Borde" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "Barra de direcciones" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "Fondo en foco" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "Barra lateral" 64 | }, 65 | 66 | "popUp": { 67 | "message": "Ventana emergente" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "Esta política será ignorada." 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "URL / regex / wildcard / dominio" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "Extensión no encontrada" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "especificar un color" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "usar / ignorar el color del tema" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "usar el color del elemento" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "Cualquier formato" 96 | }, 97 | 98 | "use": { 99 | "message": "Usar" 100 | }, 101 | 102 | "ignore": { 103 | "message": "Ignorar" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "Selector de consulta" 108 | }, 109 | 110 | "delete": { 111 | "message": "Eliminar" 112 | }, 113 | 114 | "reset": { 115 | "message": "Restablecer" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "Agregar una nueva regla" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "Permitir barra de pestañas clara" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "Permitir que la barra de pestañas use un color brillante (texto oscuro en la barra de pestañas clara)." 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "Permitir barra de pestañas oscura" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "Permitir que la barra de pestañas use un color oscuro (texto claro en la barra de pestañas oscura)." 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "Actualización dinámica de color" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "Sigue actualizando el color de la barra de pestañas mientras navegas dentro de una página web." 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "Ignorar el color del tema del sitio web" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "Algunos sitios web proporcionan su propio color de tema. Activa esta opción para ignorar estos colores." 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "Contraste mínimo" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "Establece la relación de contraste mínima entre la barra de pestañas y el texto. El color de la barra de pestañas se ajusta automáticamente si no se cumple la relación." 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "En modo claro" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "En modo oscuro" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "Color de la página de inicio: claro" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "Color de la página de inicio: oscuro" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "Color de respaldo: claro" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "Color de respaldo: oscuro" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "Exportar configuración" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "Las configuraciones han sido exportadas a tu carpeta de descargas." 192 | }, 193 | 194 | "importSettings": { 195 | "message": "Importar configuraciones" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "Las configuraciones han sido importadas." 200 | }, 201 | 202 | "importFailed": { 203 | "message": "Error al importar configuraciones. Por favor, inténtalo de nuevo." 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "Restablecer el creador de temas" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "¿Estás seguro de que quieres restablecer el creador de temas?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "Restablecer la lista de sitios" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "¿Estás seguro de que quieres restablecer la lista de sitios?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "Restablecer la configuración avanzada" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "¿Estás seguro de que quieres restablecer la configuración avanzada?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "Reportar un problema / enviar una sugerencia / traducir esta extensión" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "Más opciones…" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "Abrir página de opciones" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "Se está usando color para el lector de PDF" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "Se está usando color para el lector de JSON" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "Esta página está protegida por el navegador" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "Se está usando color para el visor de texto plano" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "Ocurrió un error, se está usando color de respaldo" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "El color de la barra de pestañas para la página de inicio se puede configurar en la página de opciones" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "Se está usando color especificado para páginas relacionadas con " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": "__EMPTY__" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "Usar color predeterminado" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "Usar color predeterminado" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "Color recomendado para páginas relacionadas con " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " está disponible" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "Usar color recomendado" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "Usar el color recomendado" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "Haz clic en “Especificar un color” para abrir la página de opciones y especificar el color de la barra de pestañas para páginas relacionadas con " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": "__EMPTY__" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "Especificar un color" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "Abrir la página de opciones y especificar un color para las páginas relacionadas con la extensión" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "Se está utilizando el color del tema definido por el sitio web, en lugar de ser ignorado por defecto" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "No se encuentra el color del tema, el color se toma de la página web" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "El color del tema definido por el sitio web es ignorado" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "Usar color del tema" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "Usar el color del tema definido por el sitio web" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "El color se toma de un elemento HTML que coincide con " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": "__EMPTY__" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "No se puede encontrar el elemento HTML que coincide con " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": ", el color se toma de la página web en su lugar" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "Ocurrió un error al buscar el elemento HTML que coincide con " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": ", el color se toma de la página web en su lugar" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "El color para este sitio está especificado en la configuración" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "Se está usando color para el visor de imágenes" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "Se está usando el color del tema definido por el sitio web" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "Ignorar color del tema" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "Ignorar el color del tema definido por el sitio web" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "No hay color disponible, se está usando color de respaldo" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "El color se ha tomado con éxito de la página web" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "El color original ha sido ajustado para asegurar un contraste mínimo" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/fr_FR/amo.md: -------------------------------------------------------------------------------- 1 | **Que fait cette extension ?** 2 | 3 | Lorsque vous naviguez sur le web, cette extension modifie le thème de Firefox pour l'adapter à l'apparence du site web que vous consultez, tout comme Safari sur macOS colore sa barre d'onglets. 4 | 5 | **Compatible avec :** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **Incompatible avec :** 12 | 13 | - Versions de Firefox antérieures à [112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) (sortie en avril 2023) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - et tout autre extensions modifiant le thème Firefox 19 | 20 | **Si vous utilisez un thème CSS :** 21 | 22 | Un thème CSS peut fonctionner avec ATBC (Adaptative Tab Bar Colour) lorsque des variables de couleur système sont utilisées (par exemple, `--lwt-accent-color` pour la couleur de la barre d'onglets). [Ceci](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS) est un exemple de thème CSS compatible avec ATBC. 23 | 24 | **Si vous utilisez Linux avec un thème GTK :** 25 | 26 | Les boutons de la barre de titre de Firefox peuvent revenir au style de Windows. Pour éviter cela, ouvrez les Préférences avancées (`about:config`) et définissez `widget.gtk.non-native-titlebar-buttons.enabled` sur `false`. (Merci à [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **Rappel de sécurité :** 29 | 30 | Attention aux interfaces web malveillantes : Veuillez faire la distinction entre l'interface du navigateur et l'interface web. Voir [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/). (Merci à [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | N'hésitez pas à ajouter ce projet à vos favoris sur GitHub : [https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) -------------------------------------------------------------------------------- /scr/_locales/fr_FR/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Adaptive Tab Bar Colour" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "Change la couleur du thème de Firefox pour correspondre à l'apparence de sites." 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "Les données de préférences pourraient être corrompues. Si le problème persiste, veuillez réinstaller le module complémentaire. Veuillez nous excuser pour la gêne occasionnée !" 16 | }, 17 | 18 | "loading": { 19 | "message": "Chargement…" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "Constructeur de thème" 24 | }, 25 | 26 | "siteList": { 27 | "message": "Liste de sites" 28 | }, 29 | 30 | "advanced": { 31 | "message": "Avancé" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "Barre d'onglets" 36 | }, 37 | 38 | "background": { 39 | "message": "Arrière-plan" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "Onglet sélectionné" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "Barre d'outils" 48 | }, 49 | 50 | "border": { 51 | "message": "Bordure" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "Barre d'URL" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "Arrière-plan au focus" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "Barre latérale" 64 | }, 65 | 66 | "popUp": { 67 | "message": "Pop-up" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "Cette politique sera ignorée" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "URL / regex / wildcard / domaine" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "Extension non trouvée" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "spécifier une couleur" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "utiliser / ignorer la couleur du thème" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "sélectionner la couleur de l'élément" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "Tout format" 96 | }, 97 | 98 | "use": { 99 | "message": "Utiliser" 100 | }, 101 | 102 | "ignore": { 103 | "message": "Ignorer" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "Sélecteur de requête" 108 | }, 109 | 110 | "delete": { 111 | "message": "Supprimer" 112 | }, 113 | 114 | "reset": { 115 | "message": "Réinitialiser" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "Ajouter une nouvelle règle" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "Autoriser la barre d'onglets claire" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "Autorise la barre d'onglets à utiliser des couleurs claires (texte sombre sur la barre d'onglets claire)." 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "Autoriser la barre d'onglets sombre" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "Autoriser la barre d'onglets à utiliser des couleurs sombres (texte clair sur la barre d'onglets sombre)." 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "Mise à jour dynamique des couleurs" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "Met à jour continuellement la couleur de la barre d'onglets lors de la navigation sur une page Web." 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "Ignorer la couleur du thème des sites Web" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "Certains sites web proposent eux-mêmes une couleur de thème. Activez cette option pour ignorer ces couleurs." 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "Contraste minimum" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "Défini le ratio de contraste minimal entre la barre d'onglets et le texte. La couleur de la barre d'onglets s'ajuste automatiquement si le ratio n'est pas respecté." 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "En mode clair" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "En mode sombre" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "Couleur de la page d'accueil : claire" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "Couleur de la page d'accueil : sombre" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "Couleur de repli : claire" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "Couleur de repli : sombre" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "Exporter les paramètres" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "Les paramètres ont été exportés vers votre dossier de téléchargement" 192 | }, 193 | 194 | "importSettings": { 195 | "message": "Importer les paramètres" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "Les paramètres ont été importés." 200 | }, 201 | 202 | "importFailed": { 203 | "message": "Échec de l'importation des paramètres. Veuillez réessayer." 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "Réinitialiser le constructeur de thème" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "Êtes-vous sûr de vouloir réinitialiser le constructeur de thèmes ?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "Réinitialiser la liste des sites" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "Êtes-vous sûr de vouloir réinitialiser la liste des sites ?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "Réinitialiser les paramètres avancés" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "Êtes-vous sûr de vouloir réinitialiser les paramètres avancés ?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "Signaler un problème / soumettre une suggestion / traduire cette extension" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "Plus de paramètres…" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "Ouvrir la page d'options" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "Utilisation de la couleur pour la visionneuse PDF" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "Utilisation de la couleur pour la visionneuse JSON" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "Cette page est protégée par le navigateur" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "Utilisation de la couleur pour la visionneuse de texte brut" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "Une erreur s'est produite, utilisation de la couleur de repli" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "La couleur de la barre d'onglets de la page d'accueil peut être configurée dans les paramètres" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "Utilisation de la couleur spécifiée pour les pages liées à " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": "__EMPTY__" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "Utiliser la couleur par défaut" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "Utiliser la couleur par défaut" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "Couleur recommandée pour les pages liées à " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " est disponible" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "Utiliser la couleur recommandée" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "Utilisez la couleur recommandée" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "Cliquez sur “Spécifier une couleur” pour ouvrir les paramètres et spécifier la couleur de la barre d’onglets pour les pages liées à " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": "__EMPTY__" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "Spécifier une couleur" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "Ouvrez les paramètres et spécifiez une couleur pour les pages liées à l'extension" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "La couleur du thème définie par le site web est utilisée, au lieu d'être ignorée par défaut" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "La couleur du thème n'est pas trouvée, la couleur est choisie à partir de la page web" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "La couleur du thème définie par le site web est ignorée" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "Utiliser la couleur du thème" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "Utiliser la couleur du thème définie par le site web" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "La couleur est choisie à partir d'un élément HTML correspondant " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": "__EMPTY__" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "Impossible de trouver l'élément HTML correspondant " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": ", la couleur est choisie à partir de la page web à la place" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "Une erreur s'est produite lors de la recherche de l'élément HTML correspondant " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": ", la couleur est choisie à partir de la page web à la place" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "La couleur de ce site est spécifiée dans les paramètres" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "Utilisation de la couleur pour la visionneuse d'images" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "Utilisation de la couleur du thème définie par le site web" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "Ignorer la couleur du thème" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "Ignorer la couleur du thème définie par le site web" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "Aucune couleur n'est disponible, utilisation de la couleur de repli" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "La couleur est choisie à partir de la page web" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "La couleur d'origine a été ajustée pour garantir un minimum de contraste" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/zh/amo.md: -------------------------------------------------------------------------------- 1 | **主要功能** 2 | 3 | 當閣下瀏覽網頁時,此擴充套件將更新 Firefox 佈景主題,使瀏覽器介面與正在瀏覽的網頁融為一體——正如 macOS Safari 變更其標題列顏色一樣。 4 | 5 | **和此套件運作無間的有:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **和此套件不相容的有:** 12 | 13 | - 低於 [Version 112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) 版本的 Firefox(於 2023 年四月發佈) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - 以及任何改變 Firefox 佈景主題的擴充套件 19 | 20 | **若閣下使用 CSS Theme:** 21 | 22 | 一個 CSS theme 若採用默認的顏色變數,則可以與 變色標題列 相容(例如,`--lwt-accent-color` 須用於標題列顏色)。譬如,[這](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS)是一個與 變色標題列 相容的 CSS theme。 23 | 24 | **若閣下使用帶有 GTK Theme 的 Linux:** 25 | 26 | Firefox 的標題列按鈕或會重設為 Windows 風格。為免此發生,請開啟進階偏好設定(`about:config`),並將 `widget.gtk.non-native-titlebar-buttons.enabled` 設為 `false`。(感謝 [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **安全提示:** 29 | 30 | 小心懷有惡意的網頁畫面:請注意區別瀏覽器介面和網頁畫面。請參考 [The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/)。(感謝 [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | 閣下可移步 GitHub 為此專案加星標:[https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) 33 | -------------------------------------------------------------------------------- /scr/_locales/zh/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "變色標題列" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "改變 Firefox 佈景主題,使之與網頁融為一體。" 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "設定檔可能已損壞,請考慮嘗試重新安裝此擴充套件。" 16 | }, 17 | 18 | "loading": { 19 | "message": "載入中⋯⋯" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "佈景主題" 24 | }, 25 | 26 | "siteList": { 27 | "message": "自訂規則" 28 | }, 29 | 30 | "advanced": { 31 | "message": "進階設定" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "分頁標籤列" 36 | }, 37 | 38 | "background": { 39 | "message": "背景" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "選中的分頁標籤" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "工具列" 48 | }, 49 | 50 | "border": { 51 | "message": "邊界" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "網址列" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "選中時的背景" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "側邊列" 64 | }, 65 | 66 | "popUp": { 67 | "message": "選單" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "這項規則將不生效" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "網址 / 正則表達式 / 萬用字元 / 域名" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "未找到該套件" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "指定顏色" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "採用 / 忽略主題色" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "從網頁元件取色" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "任何格式" 96 | }, 97 | 98 | "use": { 99 | "message": "採用" 100 | }, 101 | 102 | "ignore": { 103 | "message": "忽略" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "CSS 選擇器" 108 | }, 109 | 110 | "delete": { 111 | "message": "移除" 112 | }, 113 | 114 | "reset": { 115 | "message": "重設" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "新建自訂規則" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "允許標題列採用亮色" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "允許標題列採用亮色(深色文字和淺色背景)。" 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "允許標題列採用暗色" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "允許標題列採用暗色(淺色文字和深色背景)。" 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "動態更新顏色" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "在同一網頁停留時,持續更新標題列顏色。" 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "總是忽略網站指定的主題色" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "有些網站會提供一個主題色。啟用此選項,這些顏色將被自動忽略。" 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "最小對比度設定" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "在此設定標題列和文字的最小對比度。若該指標未達到,標題列會調整其顏色,使之達到設定的最小對比度。" 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "在亮色模式下" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "在暗色模式下" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "首頁背景色(亮色)" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "首頁背景色(暗色)" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "備選顏色(亮色)" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "備選顏色(暗色)" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "儲存偏好設定" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "偏好設定已儲存至下載文檔夾。" 192 | }, 193 | 194 | "importSettings": { 195 | "message": "載入偏好設定" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "已載入偏好設定。" 200 | }, 201 | 202 | "importFailed": { 203 | "message": "載入偏好設定失敗。請重試。" 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "重設佈景主題設定" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "是否確認重設佈景主題設定?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "重設自訂規則" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "是否確認重設自訂規則?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "重設進階設定" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "是否確認重設進階設定?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "回報問題 / 提供建議 / 提供繙譯" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "更多設定" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "開啟設定頁" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "按 PDF 閱讀器著色" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "按 JSON 閱讀器著色" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "此頁面受瀏覽器保護" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "按文本閱讀器著色" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "發生錯誤,採用備選顏色" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "首頁背景色可在設定頁調整" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "採用為 " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": " 相關頁面指定的顏色" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "採用默認顏色" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "採用默認顏色" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "可採用為 " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " 相關頁面推介的顏色" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "採用推介顏色" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "採用推介顏色" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "點擊「指定顏色」以開啟設定頁。您可為 " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": " 相關頁面指定一個顏色" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "指定顏色" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "開啟設定頁來為此套件相關頁面指定顏色" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "此網站指定的主題色被解除忽略" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "網站未指定主題色,標題列顏色是從網頁獲取" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "此網站指定的主題色被忽略" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "採用主題色" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "採用此網站指定的主題色" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "標題列顏色是從符合 " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": "的網頁元件獲取" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "找不到符合 " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": " 的網頁元件,標題列顏色是按一般方法從網頁中獲取" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "當尋找符合 " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": " 的網頁元件時發生錯誤,標題列顏色是按一般方法從網頁中獲取" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "此網站對應的標題列顏色已被指定" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "按圖像預覽器著色" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "採用此網站指定的主題色" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "忽略此主題色" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "忽略此網站指定的主題色" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "未能從網頁獲取顏色,採用備選顏色" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "標題列顏色是按一般方法從網頁中獲取" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "為達到最近對比度,原顏色已被調整" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/_locales/zh_CN/amo.md: -------------------------------------------------------------------------------- 1 | **主要功能** 2 | 3 | 当您浏览网页时,这个插件将会动态更新 Firefox 背景色,使浏览器主题与正在浏览的网页融为一体——正如 macOS Safari 修改其标签栏颜色一样。 4 | 5 | **与本插件兼容的插件有:** 6 | 7 | - [Dark Reader](https://addons.mozilla.org/firefox/addon/darkreader/) 8 | - [Stylish](https://addons.mozilla.org/firefox/addon/stylish/) 9 | - [Dark Mode Website Switcher](https://addons.mozilla.org/firefox/addon/dark-mode-website-switcher/) 10 | 11 | **与本插件不兼容的有:** 12 | 13 | - 低于 [Version 112.0](https://www.mozilla.org/firefox/112.0/releasenotes/) 版本的 Firefox(于 2023 年四月发布) 14 | - [Adaptive Theme Creator](https://addons.mozilla.org/firefox/addon/adaptive-theme-creator/) 15 | - [Chameleon Dynamic Theme](https://addons.mozilla.org/firefox/addon/chameleon-dynamic-theme-fixed/) 16 | - [VivaldiFox](https://addons.mozilla.org/firefox/addon/vivaldifox/) 17 | - [Envify](https://addons.mozilla.org/firefox/addon/envify/) 18 | - 以及任何改变 Firefox 主题的扩展插件 19 | 20 | **如果您使用 CSS 主题:** 21 | 22 | 如果一个 CSS 主题使用默认的颜色参数,则其可以与 变色标签栏 兼容。比如,[这](https://github.com/easonwong-de/WhiteSurFirefoxThemeMacOS)是一个与 变色标签栏 兼容的的 CSS 主题。 23 | 24 | **如果您使用的是 Linux 的 GTK 主题:** 25 | 26 | Firefox 的标题栏按钮可能会被重置为 Windows 风格。为了避免这种情况,请打开高级首选项(`about:config`),并将 `widget.gtk.non-native-titlebar-buttons.enabled` 设置为 `false`。(感谢 [@anselstetter](https://github.com/anselstetter/)) 27 | 28 | **安全提示:** 29 | 30 | 小心带有恶意的网页界面:请注意区分浏览器页面和网页界面。请参考:[The Line of Death](https://textslashplain.com/2017/01/14/the-line-of-death/)。(感谢 [u/KazaHesto](https://www.reddit.com/user/KazaHesto/)) 31 | 32 | 欢迎您为 GitHub 仓库点亮 star:[https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour](https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour) 33 | -------------------------------------------------------------------------------- /scr/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "变色标签栏" 4 | }, 5 | 6 | "extensionDescription": { 7 | "message": "改变 Firefox 背景主题,使其与网页融为一体。" 8 | }, 9 | 10 | "__OPTIONS PAGE__": { 11 | "message": "====================" 12 | }, 13 | 14 | "reinstallTip": { 15 | "message": "配置文件可能已损坏,请尝试重新安装本插件。" 16 | }, 17 | 18 | "loading": { 19 | "message": "加载中……" 20 | }, 21 | 22 | "themeBuilder": { 23 | "message": "背景主题" 24 | }, 25 | 26 | "siteList": { 27 | "message": "自定义规则" 28 | }, 29 | 30 | "advanced": { 31 | "message": "高级设置" 32 | }, 33 | 34 | "tabBar": { 35 | "message": "标签栏" 36 | }, 37 | 38 | "background": { 39 | "message": "背景" 40 | }, 41 | 42 | "selectedTab": { 43 | "message": "被选中的标签页" 44 | }, 45 | 46 | "toolbar": { 47 | "message": "工具栏" 48 | }, 49 | 50 | "border": { 51 | "message": "边界" 52 | }, 53 | 54 | "URLBar": { 55 | "message": "地址栏" 56 | }, 57 | 58 | "backgroundOnFocus": { 59 | "message": "被选中时的背景" 60 | }, 61 | 62 | "sidebar": { 63 | "message": "侧栏" 64 | }, 65 | 66 | "popUp": { 67 | "message": "弹出菜单" 68 | }, 69 | 70 | "thisPolicyWillBeIgnored": { 71 | "message": "这项规则将不会生效" 72 | }, 73 | 74 | "URLDomainOrRegex": { 75 | "message": "网址 / 正则表达式 / 通配符 / 域名" 76 | }, 77 | 78 | "addonNotFound": { 79 | "message": "未找到该插件" 80 | }, 81 | 82 | "specifyAColour": { 83 | "message": "指定颜色" 84 | }, 85 | 86 | "useIgnoreThemeColour": { 87 | "message": "使用 / 忽略主题色" 88 | }, 89 | 90 | "pickColourFromElement": { 91 | "message": "从网页元素取色" 92 | }, 93 | 94 | "anyCSSColour": { 95 | "message": "任何格式" 96 | }, 97 | 98 | "use": { 99 | "message": "使用" 100 | }, 101 | 102 | "ignore": { 103 | "message": "忽略" 104 | }, 105 | 106 | "querySelector": { 107 | "message": "CSS 选择器名称" 108 | }, 109 | 110 | "delete": { 111 | "message": "删除" 112 | }, 113 | 114 | "reset": { 115 | "message": "重新设定" 116 | }, 117 | 118 | "addANewRule": { 119 | "message": "新建自定义规则" 120 | }, 121 | 122 | "allowLightTabBar": { 123 | "message": "允许标签栏使用亮色" 124 | }, 125 | 126 | "allowLightTabBarTooltip": { 127 | "message": "允许标签栏使用亮色(深色文字和浅色背景)。" 128 | }, 129 | 130 | "allowDarkTabBar": { 131 | "message": "允许标签栏使用暗色" 132 | }, 133 | 134 | "allowDarkTabBarTooltip": { 135 | "message": "允许标签栏使用暗色(浅色文字和深色背景)。" 136 | }, 137 | 138 | "dynamicColourUpdate": { 139 | "message": "动态更新颜色" 140 | }, 141 | 142 | "dynamicModeTooltip": { 143 | "message": "在同一个网页停留时,持续更新标题栏颜色。" 144 | }, 145 | 146 | "ignoreDesignatedThemeColour": { 147 | "message": "总是忽略网站指定的主题色" 148 | }, 149 | 150 | "ignoreDesignatedThemeColourTooltip": { 151 | "message": "有些网站会提供一个主题色。开启此选项,这些颜色将被自动忽略。" 152 | }, 153 | 154 | "minimumContrast": { 155 | "message": "最小对比度设置" 156 | }, 157 | 158 | "minimumContrastTooltip": { 159 | "message": "在此设置标题栏和文字的最小对比度。如果未达到该指标,标签栏的颜色将会被调整,使之达到设置的最小对比度。" 160 | }, 161 | 162 | "inLightMode": { 163 | "message": "在亮色模式下" 164 | }, 165 | 166 | "inDarkMode": { 167 | "message": "在暗色模式下" 168 | }, 169 | 170 | "homepageColourLight": { 171 | "message": "首页背景色(亮色)" 172 | }, 173 | 174 | "homepageColourDark": { 175 | "message": "首页背景色(暗色)" 176 | }, 177 | 178 | "fallbackColourLight": { 179 | "message": "备选颜色(亮色)" 180 | }, 181 | 182 | "fallbackColourDark": { 183 | "message": "备选颜色(暗色)" 184 | }, 185 | 186 | "exportSettings": { 187 | "message": "保存偏好设置至文件" 188 | }, 189 | 190 | "settingsAreExported": { 191 | "message": "偏好设置已经被保存到下载文件夹。" 192 | }, 193 | 194 | "importSettings": { 195 | "message": "从文件导入偏好设置" 196 | }, 197 | 198 | "settingsAreImported": { 199 | "message": "已导入偏好设置。" 200 | }, 201 | 202 | "importFailed": { 203 | "message": "导入偏好设置失败。请检查文件是否正确并重试。" 204 | }, 205 | 206 | "resetThemeBuilder": { 207 | "message": "重新设置背景主题" 208 | }, 209 | 210 | "confirmResetThemeBuilder": { 211 | "message": "是否确认重新设置背景主题?" 212 | }, 213 | 214 | "resetSiteList": { 215 | "message": "重新设置自定义站点规则" 216 | }, 217 | 218 | "confirmResetSiteList": { 219 | "message": "是否确认重新设置自定义站点规则?" 220 | }, 221 | 222 | "resetAdvanced": { 223 | "message": "重新设置高级设置" 224 | }, 225 | 226 | "confirmResetAdvanced": { 227 | "message": "是否确认重新设置高级设置?" 228 | }, 229 | 230 | "reportAnIssue": { 231 | "message": "报告问题 / 提供建议 / 提供翻译" 232 | }, 233 | 234 | "__POPUP__": { 235 | "message": "====================" 236 | }, 237 | 238 | "moreSettings": { 239 | "message": "更多设置" 240 | }, 241 | 242 | "moreSettingsTooltip": { 243 | "message": "打开设置页" 244 | }, 245 | 246 | "colourForPDFViewer": { 247 | "message": "从 PDF 阅读器获取颜色" 248 | }, 249 | 250 | "colourForJSONViewer": { 251 | "message": "从 JSON 阅读器获取颜色" 252 | }, 253 | 254 | "pageIsProtected": { 255 | "message": "此页面受浏览器保护" 256 | }, 257 | 258 | "colourForPlainTextViewer": { 259 | "message": "从文本阅读器获取颜色" 260 | }, 261 | 262 | "errorOccured": { 263 | "message": "发生错误,使用备选颜色" 264 | }, 265 | 266 | "colourForHomePage": { 267 | "message": "首页背景色可在设置页调整" 268 | }, 269 | 270 | "useDefaultColourForAddon": { 271 | "message": "使用 " 272 | }, 273 | 274 | "useDefaultColourForAddonEnd": { 275 | "message": " 相关页面指定的颜色" 276 | }, 277 | 278 | "useDefaultColourForAddonButton": { 279 | "message": "使用默认颜色" 280 | }, 281 | 282 | "useDefaultColourForAddonTitle": { 283 | "message": "使用默认颜色" 284 | }, 285 | 286 | "useRecommendedColourForAddon": { 287 | "message": "可使用 " 288 | }, 289 | 290 | "useRecommendedColourForAddonEnd": { 291 | "message": " 相关页面指定的颜色" 292 | }, 293 | 294 | "useRecommendedColourForAddonButton": { 295 | "message": "使用推荐的颜色" 296 | }, 297 | 298 | "useRecommendedColourForAddonTitle": { 299 | "message": "使用推荐的颜色" 300 | }, 301 | 302 | "specifyColourForAddon": { 303 | "message": "点击「指定颜色」打开设置页。您可为 " 304 | }, 305 | 306 | "specifyColourForAddonEnd": { 307 | "message": " 相关的页面指定一个颜色" 308 | }, 309 | 310 | "specifyColourForAddonButton": { 311 | "message": "指定颜色" 312 | }, 313 | 314 | "specifyColourForAddonTitle": { 315 | "message": "打开设置页来为此插件的相关页面指定颜色" 316 | }, 317 | 318 | "themeColourIsUnignored": { 319 | "message": "此网站指定的主题色被重新采用" 320 | }, 321 | 322 | "themeColourNotFound": { 323 | "message": "网站未指定主题色,标签栏的颜色已从网页获取" 324 | }, 325 | 326 | "themeColourIsIgnored": { 327 | "message": "此网站指定的主题色被忽略" 328 | }, 329 | 330 | "useThemeColourButton": { 331 | "message": "使用主题色" 332 | }, 333 | 334 | "useThemeColourTitle": { 335 | "message": "使用此网站指定的主题色" 336 | }, 337 | 338 | "colourIsPickedFrom": { 339 | "message": "标题栏的颜色是从符合 " 340 | }, 341 | 342 | "colourIsPickedFromEnd": { 343 | "message": " 的网页元素中获取" 344 | }, 345 | 346 | "cannotFindElement": { 347 | "message": "找不到符合 " 348 | }, 349 | 350 | "cannotFindElementEnd": { 351 | "message": " 的网页元素,标题栏的颜色已按普通方法从网页中获取" 352 | }, 353 | 354 | "errorOccuredLocatingElement": { 355 | "message": "当寻找符合 " 356 | }, 357 | 358 | "errorOccuredLocatingElementEnd": { 359 | "message": " 的网页元素时发生错误,标题栏的颜色已按普通方法从网页中获取" 360 | }, 361 | 362 | "colourIsSpecified": { 363 | "message": "此网站对应的标题栏颜色已被指定" 364 | }, 365 | 366 | "usingImageViewer": { 367 | "message": "按图像查看器着色" 368 | }, 369 | 370 | "usingThemeColour": { 371 | "message": "使用此网站指定的主题色" 372 | }, 373 | 374 | "ignoreThemeColourButton": { 375 | "message": "忽略此主题色" 376 | }, 377 | 378 | "ignoreThemeColourTitle": { 379 | "message": "忽略此网站指定的主题色" 380 | }, 381 | 382 | "usingFallbackColour": { 383 | "message": "未能从网页获取颜色,使用备选颜色" 384 | }, 385 | 386 | "colourPickedFromWebpage": { 387 | "message": "标题栏的颜色已按普通方法从网页中获取" 388 | }, 389 | 390 | "colourIsAdjusted": { 391 | "message": "为达到最小对比度设置,原颜色已被调整" 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /scr/atbc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Workflow of content script: 5 | * 6 | * On load: 7 | * notifies background -> background sends back configurations -> sends colours; 8 | * 9 | * After load: 10 | * 1. pref is changed, background sends configurations -> sends colour; 11 | * 2. (dynamic colour is on) sends colour automaticaly; 12 | */ 13 | 14 | /** Configurations of the content script */ 15 | const conf = { 16 | dynamic: true, 17 | noThemeColour: true, 18 | policy: undefined, 19 | }; 20 | 21 | /** 22 | * Information to be sent to the background / popup. 23 | * 24 | * `reason` determines the content shown in the popup infobox & text in the button. 25 | * 26 | * `reason` can be: `PROTECTED_PAGE`, `HOME_PAGE`, `TEXT_VIEWER`, `IMAGE_VIEWER`, `PDF_VIEWER`, `JSON_VIEWER`, `ERROR_OCCURRED`, `FALLBACK_COLOUR`, `COLOUR_PICKED`, `ADDON` (only for `popup.js`, in which case, `additionalInfo` stores the window's ID), `ADDON_SPECIFIED`, `ADDON_RECOM`, `ADDON_DEFAULT`, `THEME_UNIGNORED`, `THEME_MISSING`, `THEME_IGNORED`, `THEME_USED`, `QS_USED`, `QS_FAILED`, `QS_ERROR`, `COLOUR_SPECIFIED`. 27 | */ 28 | const response = { 29 | reason: null, 30 | additionalInfo: null, 31 | colour: { r: 0, g: 0, b: 0, a: 0 }, 32 | }; 33 | 34 | /** 35 | * Finds colour and send to background. 36 | * 37 | * Maximum frequency is 4 Hz. 38 | */ 39 | const findAndSendColour = (() => { 40 | let timeout; 41 | let lastCall = 0; 42 | const limitMs = 250; 43 | const action = async () => { 44 | if (document.visibilityState === "visible" && findColour()) 45 | browser.runtime.sendMessage({ header: "UPDATE_COLOUR", response: response }); 46 | }; 47 | return async () => { 48 | const now = Date.now(); 49 | clearTimeout(timeout); 50 | if (now - lastCall >= limitMs) { 51 | lastCall = now; 52 | await action(); 53 | } else { 54 | timeout = setTimeout(async () => { 55 | lastCall = Date.now(); 56 | await action(); 57 | }, limitMs - (now - lastCall)); 58 | } 59 | }; 60 | })(); 61 | 62 | /** 63 | * Finds colour and send to background but requires focus in document. 64 | * 65 | * This fits transition / animation events better. 66 | */ 67 | function findAndSendColour_focus() { 68 | if (document.hasFocus()) findAndSendColour(); 69 | } 70 | 71 | /** 72 | * Finds colour. 73 | */ 74 | function findColour() { 75 | if (document.fullscreenElement) return false; 76 | response.reason = null; 77 | response.additionalInfo = null; 78 | response.colour = { r: 0, g: 0, b: 0, a: 0 }; 79 | if (!findColour_policy()) findColour_noPolicy(); 80 | return true; 81 | } 82 | 83 | /** 84 | * Sets `response.colour` with the help of the custom rule. 85 | * 86 | * @returns True if a meta `theme-color` or a custom for the web page can be found. 87 | */ 88 | function findColour_policy() { 89 | if ( 90 | !conf.policy || 91 | (!conf.noThemeColour && conf.policy.type === "THEME_COLOUR" && conf.policy.value === true) || 92 | (conf.noThemeColour && conf.policy.type === "THEME_COLOUR" && conf.policy.value === false) 93 | ) { 94 | return false; 95 | } else if (conf.policy.type === "COLOUR") { 96 | return findColour_policy_colour(); 97 | } else if (conf.policy.type === "THEME_COLOUR") { 98 | return findColour_policy_themeColour(); 99 | } else if (conf.policy.type === "QUERY_SELECTOR") { 100 | return findColour_policy_querySelector(); 101 | } else { 102 | return false; 103 | } 104 | } 105 | 106 | /** 107 | * Handles COLOUR policy. 108 | */ 109 | function findColour_policy_colour() { 110 | response.reason = "COLOUR_SPECIFIED"; 111 | response.additionalInfo = null; 112 | response.colour = parseColour(conf.policy.value); 113 | return response.colour?.a === 1; 114 | } 115 | 116 | /** 117 | * Handles THEME_COLOUR policy. 118 | */ 119 | function findColour_policy_themeColour() { 120 | if (conf.noThemeColour && conf.policy.value === true) { 121 | if (findColour_theme()) { 122 | response.reason = "THEME_UNIGNORED"; 123 | } else { 124 | findColour_webpage(); 125 | response.reason = "THEME_MISSING"; 126 | } 127 | return true; 128 | } else if (!conf.noThemeColour && conf.policy.value === false) { 129 | if (findColour_theme()) { 130 | findColour_webpage(); 131 | response.reason = "THEME_IGNORED"; 132 | } else { 133 | findColour_webpage(); 134 | } 135 | return true; 136 | } 137 | return false; 138 | } 139 | 140 | /** 141 | * Handles QUERY_SELECTOR policy. 142 | */ 143 | function findColour_policy_querySelector() { 144 | const querySelector = conf.policy.value; 145 | if (querySelector === "") { 146 | findColour_webpage(); 147 | response.additionalInfo = "nothing"; 148 | response.reason = "QS_ERROR"; 149 | } else { 150 | try { 151 | const element = document.querySelector(querySelector); 152 | response.additionalInfo = querySelector; 153 | if (element) { 154 | response.colour = getColourFromElement(element); 155 | response.reason = "QS_USED"; 156 | } else { 157 | findColour_webpage(); 158 | response.reason = "QS_FAILED"; 159 | } 160 | } catch (error) { 161 | findColour_webpage(); 162 | response.reason = "QS_ERROR"; 163 | } 164 | } 165 | return response.colour?.a === 1; 166 | } 167 | 168 | /** 169 | * Detects image viewer and text viewer, otherwise looks for meta `theme-color` / computed colour. 170 | */ 171 | function findColour_noPolicy() { 172 | if ( 173 | getComputedStyle(document.documentElement).backgroundImage === 174 | `url("chrome://global/skin/media/imagedoc-darknoise.png")` 175 | ) { 176 | // Firefox chooses `imagedoc-darknoise.png` as the background of image viewer 177 | // Doesn't work with images on `data:image` url, which will be dealt with in `background.js` 178 | response.reason = "IMAGE_VIEWER"; 179 | response.colour = "IMAGEVIEWER"; 180 | } else if ( 181 | document.getElementsByTagName("link")[0]?.href === "resource://content-accessible/plaintext.css" && 182 | getColourFromElement(document.body).a !== 1 183 | ) { 184 | // Firefox seems to have blocked content script when viewing plain text online 185 | // Thus this may only works for viewing local text file 186 | response.reason = "TEXT_VIEWER"; 187 | response.colour = "PLAINTEXT"; 188 | } else if (findColour_theme()) { 189 | if (conf.noThemeColour) { 190 | findColour_webpage(); 191 | response.reason = "THEME_IGNORED"; 192 | } 193 | } else { 194 | findColour_webpage(); 195 | } 196 | } 197 | 198 | /** 199 | * Looks for pre-determined meta `theme-color`. 200 | * 201 | * @returns Returns `false` if no legal `theme-color` can be found. 202 | */ 203 | function findColour_theme() { 204 | const colourScheme = window.matchMedia("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; 205 | const metaThemeColour = 206 | document.querySelector(`meta[name="theme-color"][media="(prefers-color-scheme: ${colourScheme})"]`) ?? 207 | document.querySelector(`meta[name="theme-color"]`); 208 | if (metaThemeColour) { 209 | response.colour = parseColour(metaThemeColour.content); 210 | } else { 211 | return false; 212 | } 213 | // Return `true` if `theme-color` is opaque and can be sent to `background.js` 214 | // Otherwise, return false and trigger `getComputedColour()` 215 | if (response.colour.a === 1) { 216 | response.reason = "THEME_USED"; 217 | return true; 218 | } else { 219 | return false; 220 | } 221 | } 222 | 223 | /** 224 | * Looks for `response.colour` from the web page elements. 225 | * 226 | * If no legal colour can be found, fallback colour will be used. 227 | */ 228 | function findColour_webpage() { 229 | response.colour = { r: 0, g: 0, b: 0, a: 0 }; 230 | // Selects all the elements 3 pixels below the middle point of the top edge of the viewport 231 | // It's a shame that `elementsFromPoint()` doesn't work with elements with `pointer-events: none` 232 | for (const element of document.elementsFromPoint(window.innerWidth / 2, 3)) { 233 | // Only if the element is wide (90 % of screen) and thick (20 pixels) enough will it be included in the calculation 234 | if (element.offsetWidth / window.innerWidth >= 0.9 && element.offsetHeight >= 20) { 235 | let elementColour = getColourFromElement(element); 236 | if (elementColour.a === 0) continue; 237 | response.colour = overlayColour(response.colour, elementColour); 238 | } 239 | if (response.colour.a === 1) { 240 | response.reason = "COLOUR_PICKED"; 241 | return true; 242 | } 243 | } 244 | // Colour is still not opaque, overlay it over the document body 245 | const body = document.body; 246 | if (body) { 247 | const bodyColour = getColourFromElement(body); 248 | if (bodyColour.a === 1) { 249 | response.colour = overlayColour(response.colour, bodyColour); 250 | response.reason = "COLOUR_PICKED"; 251 | return true; 252 | } 253 | } 254 | response.colour = "FALLBACK"; 255 | response.reason = "FALLBACK_COLOUR"; 256 | return true; 257 | } 258 | 259 | /** 260 | * Gets the computed background color of an element as an RGBA object. 261 | * 262 | * @param {HTMLElement} element - The element to extract the background color from. 263 | * @returns The RGBA color object, or transparent if unavailable. 264 | */ 265 | function getColourFromElement(element) { 266 | if (!element) return { r: 0, g: 0, b: 0, a: 0 }; 267 | const style = getComputedStyle(element); 268 | const backgroundColour = style.backgroundColor; 269 | if (!backgroundColour) return { r: 0, g: 0, b: 0, a: 0 }; 270 | const rgba = parseColour(backgroundColour); 271 | const opacity = parseFloat(style.opacity); 272 | if (!isNaN(opacity) && opacity < 1) rgba.a *= opacity; 273 | return rgba; 274 | } 275 | 276 | /** 277 | * Overlays one colour over another. 278 | * 279 | * @param {Object} colourTop Colour on top. 280 | * @param {Object} colourBottom Colour underneath. 281 | * @returns Result of the addition in object. 282 | */ 283 | function overlayColour(colourTop, colourBottom) { 284 | const a = (1 - colourTop.a) * colourBottom.a + colourTop.a; 285 | if (a === 0) { 286 | // Firefox renders transparent background in rgb(236, 236, 236) 287 | return { r: 236, g: 236, b: 236, a: 0 }; 288 | } else { 289 | return { 290 | r: ((1 - colourTop.a) * colourBottom.a * colourBottom.r + colourTop.a * colourTop.r) / a, 291 | g: ((1 - colourTop.a) * colourBottom.a * colourBottom.g + colourTop.a * colourTop.g) / a, 292 | b: ((1 - colourTop.a) * colourBottom.a * colourBottom.b + colourTop.a * colourTop.b) / a, 293 | a: a, 294 | }; 295 | } 296 | } 297 | 298 | /** 299 | * Parses a CSS color string and returns its RGBA components. 300 | * 301 | * @param {string} colour - The CSS color string to parse (e.g., "#RRGGBB", "rgb(...)", "rgba(...)", or named colors). 302 | * @returns An RGBA object. 303 | */ 304 | function parseColour(colour) { 305 | if (typeof colour !== "string") return { r: 0, g: 0, b: 0, a: 0 }; 306 | const ctx = document.createElement("canvas").getContext("2d"); 307 | ctx.fillStyle = colour; 308 | const parsedColour = ctx.fillStyle; 309 | if (parsedColour.startsWith("#")) { 310 | return { 311 | r: parseInt(parsedColour.slice(1, 3), 16), 312 | g: parseInt(parsedColour.slice(3, 5), 16), 313 | b: parseInt(parsedColour.slice(5, 7), 16), 314 | a: 1, 315 | }; 316 | } else { 317 | const [r, g, b, a] = parsedColour.match(/[.?\d]+/g).map(Number); 318 | return { r, g, b, a }; 319 | } 320 | } 321 | 322 | // Receives configurations and sends back colour 323 | browser.runtime.onMessage.addListener((message, sender, sendResponse) => { 324 | conf.dynamic = message.dynamic; 325 | conf.noThemeColour = message.noThemeColour; 326 | conf.policy = message.policy; 327 | setDynamicUpdate(); 328 | findColour(); 329 | sendResponse(response); 330 | }); 331 | 332 | /** 333 | * Sets up / turns off dynamic update. 334 | */ 335 | function setDynamicUpdate() { 336 | ["click", "resize", "scroll", "visibilitychange"].forEach((event) => { 337 | document.removeEventListener(event, findAndSendColour); 338 | if (conf.dynamic) document.addEventListener(event, findAndSendColour); 339 | }); 340 | ["transitionend", "transitioncancel", "animationend", "animationcancel"].forEach((transition) => { 341 | document.removeEventListener(transition, findAndSendColour_focus); 342 | if (conf.dynamic) document.addEventListener(transition, findAndSendColour_focus); 343 | }); 344 | } 345 | 346 | // Detects `meta[name=theme-color]` changes 347 | const onThemeColourChange = new MutationObserver(findAndSendColour); 348 | const themeColourMetaTag = document.querySelector("meta[name=theme-color]"); 349 | if (themeColourMetaTag) onThemeColourChange.observe(themeColourMetaTag, { attributes: true }); 350 | 351 | // Detects Dark Reader 352 | const onDarkReaderChange = new MutationObserver(findAndSendColour); 353 | onDarkReaderChange.observe(document.documentElement, { 354 | attributes: true, 355 | attributeFilter: ["data-darkreader-mode"], 356 | }); 357 | 358 | // Detects style injections & `meta[name=theme-color]` being added or altered 359 | const onStyleInjection = new MutationObserver((mutations) => { 360 | mutations.forEach((mutation) => { 361 | if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].nodeName === "STYLE") { 362 | findAndSendColour(); 363 | } else if (mutation.removedNodes.length > 0 && mutation.removedNodes[0].nodeName === "STYLE") { 364 | findAndSendColour(); 365 | } else if ( 366 | mutation.addedNodes.length > 0 && 367 | mutation.addedNodes[0].nodeName === "META" && 368 | mutation.addedNodes[0].name === "theme-color" 369 | ) { 370 | onThemeColourChange.observe(mutation.addedNodes[0], { attributes: true }); 371 | } 372 | }); 373 | }); 374 | onStyleInjection.observe(document.documentElement, { childList: true }); 375 | onStyleInjection.observe(document.head, { childList: true }); 376 | 377 | /** 378 | * Sends colour to background as soon as the page loads 379 | */ 380 | function sendMessageOnLoad(nthTry = 0) { 381 | try { 382 | browser.runtime.sendMessage({ header: "SCRIPT_LOADED" }); 383 | } catch (error) { 384 | if (nthTry > 3) { 385 | console.error(error); 386 | } else { 387 | setTimeout(() => sendMessageOnLoad(++nthTry), 50); 388 | } 389 | } 390 | } 391 | 392 | sendMessageOnLoad(); 393 | -------------------------------------------------------------------------------- /scr/background.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Definitions of some concepts 5 | * 6 | * System colour scheme: 7 | * The colour scheme of the operating system, usually light or dark. 8 | * 9 | * Browser colour scheme: 10 | * The "website appearance" settings of Firefox, which can be light, dark, or auto. 11 | * 12 | * current.scheme: 13 | * Derived from System and Browser colour scheme and decides whether the light theme or dark theme is preferred. 14 | * 15 | * pref.allowDarkLight: 16 | * A setting that decides if a light theme is allowed to be used when current.scheme is dark, or vice versa. 17 | * 18 | * theme-color / meta theme colour: 19 | * A colour defined with a meta tag by some websites, usually static. 20 | * It is often more related to the branding than the appearance of the website. 21 | * 22 | * Theme: 23 | * An object that defines the appearance of the Firefox chrome. 24 | */ 25 | 26 | import preference from "./preference.js"; 27 | import colour from "./colour.js"; 28 | import { aboutPageColour, restrictedSiteColour } from "./default_values.js"; 29 | import { onSchemeChanged, getCurrentScheme, getSystemScheme } from "./utility.js"; 30 | 31 | /** Preference */ 32 | const pref = new preference(); 33 | 34 | /** Lookup table for codified colours */ 35 | const colourCode = Object.freeze({ 36 | HOME: { 37 | get light() { 38 | return new colour().parse(pref.homeBackground_light); 39 | }, 40 | get dark() { 41 | return new colour().parse(pref.homeBackground_dark); 42 | }, 43 | }, 44 | FALLBACK: { 45 | get light() { 46 | return new colour().parse(pref.fallbackColour_light); 47 | }, 48 | get dark() { 49 | return new colour().parse(pref.fallbackColour_dark); 50 | }, 51 | }, 52 | PLAINTEXT: { light: new colour().rgba(255, 255, 255, 1), dark: new colour().rgba(28, 27, 34, 1) }, 53 | SYSTEM: { light: new colour().rgba(255, 255, 255, 1), dark: new colour().rgba(30, 30, 30, 1) }, 54 | ADDON: { light: new colour().rgba(236, 236, 236, 1), dark: new colour().rgba(50, 50, 50, 1) }, 55 | PDFVIEWER: { light: new colour().rgba(249, 249, 250, 1), dark: new colour().rgba(56, 56, 61, 1) }, 56 | IMAGEVIEWER: { light: undefined, dark: new colour().rgba(33, 33, 33, 1) }, 57 | JSONVIEWER: { 58 | get light() { 59 | return getSystemScheme() === "light" ? new colour().rgba(249, 249, 250, 1) : undefined; 60 | }, 61 | get dark() { 62 | return getSystemScheme() === "dark" ? new colour().rgba(12, 12, 13, 1) : undefined; 63 | }, 64 | }, 65 | DEFAULT: { light: new colour().rgba(255, 255, 255, 1), dark: new colour().rgba(28, 27, 34, 1) }, 66 | }); 67 | 68 | /** Variables */ 69 | const current = { 70 | /** `light` or `dark` */ 71 | scheme: "light", 72 | /** windowId: { colour?, reason, additionalInfo?, corrected? } */ 73 | info: {}, 74 | get reversedScheme() { 75 | return this.scheme === "light" ? "dark" : "light"; 76 | }, 77 | async update() { 78 | this.scheme = await getCurrentScheme(); 79 | this.info = {}; 80 | }, 81 | }; 82 | 83 | /** 84 | * Triggers colour change in all windows. 85 | */ 86 | async function update() { 87 | if (!pref.valid()) await initialise(); 88 | const activeTabs = await browser.tabs.query({ active: true, status: "complete" }); 89 | await current.update(); 90 | activeTabs.forEach(updateTab); 91 | } 92 | 93 | /** 94 | * Initialises `pref` and `current`. 95 | */ 96 | async function initialise() { 97 | await pref.load(); 98 | await pref.normalise(); 99 | await pref.save(); 100 | await update(); 101 | } 102 | 103 | /** 104 | * Updates `pref` and triggers colour change in all windows. 105 | */ 106 | async function prefUpdate() { 107 | await pref.load(); 108 | await update(); 109 | } 110 | 111 | /** 112 | * Handles incoming messages based on their `header`. 113 | * 114 | * @param {object} message - The message object containing the `header` and any additional data. 115 | * @param {runtime.MessageSender} sender - The message sender. 116 | */ 117 | async function handleMessage(message, sender) { 118 | const tab = sender.tab; 119 | const header = message.header; 120 | switch (header) { 121 | case "INIT_REQUEST": 122 | await initialise(); 123 | break; 124 | case "PREF_CHANGED": 125 | await prefUpdate(); 126 | break; 127 | case "SCRIPT_LOADED": 128 | updateTab(tab); 129 | break; 130 | case "UPDATE_COLOUR": 131 | current.info[tab.windowId] = message.response; 132 | setFrameColour(tab, new colour().parse(message.response.colour)); 133 | break; 134 | case "SCHEME_REQUEST": 135 | return await getCurrentScheme(); 136 | case "INFO_REQUEST": 137 | return current.info[message.windowId]; 138 | default: 139 | update(); 140 | } 141 | return true; 142 | } 143 | 144 | /** 145 | * Updates the colour for a tab. 146 | * 147 | * @param {tabs.Tab} tab - The tab. 148 | */ 149 | async function updateTab(tab) { 150 | const windowId = tab.windowId; 151 | const tabColour = await getTabColour(tab); 152 | current.info[windowId] = tabColour; 153 | setFrameColour(tab, tabColour.colour); 154 | } 155 | 156 | /** 157 | * Determines the appropriate colour for a tab. 158 | * 159 | * Tries to get the colour from the content script, falling back to policy or protected page colour if needed. 160 | * 161 | * @param {tabs.Tab} tab - The tab to extract the colour from. 162 | * @returns {Promise<{colour: colour, additionalInfo?: string|undefined, reason: string}>} An object containing the colour, reason, and optional additional info. 163 | */ 164 | async function getTabColour(tab) { 165 | const policy = pref.getPolicy(pref.getURLPolicyId(tab.url)); 166 | try { 167 | const response = await browser.tabs.sendMessage(tab.id, { 168 | dynamic: pref.dynamic, 169 | noThemeColour: pref.noThemeColour, 170 | policy, 171 | }); 172 | return { 173 | colour: new colour().parse(response.colour), 174 | additionalInfo: response.additionalInfo, 175 | reason: response.reason, 176 | }; 177 | } catch (error) { 178 | if (policy?.headerType === "URL" && policy?.type === "COLOUR") { 179 | return { colour: new colour().parse(policy.value), reason: "COLOUR_SPECIFIED" }; 180 | } else { 181 | return await getProtectedPageColour(tab); 182 | } 183 | } 184 | } 185 | 186 | /** 187 | * Determines the colour for a protected page. 188 | * 189 | * @param {tabs.Tab} tab - The tab. 190 | */ 191 | async function getProtectedPageColour(tab) { 192 | const url = new URL(tab.url); 193 | if (["about:firefoxview", "about:home", "about:newtab"].some((href) => url.href.startsWith(href))) { 194 | return { colour: new colour().parse("HOME"), reason: "HOME_PAGE" }; 195 | } else if (url.href === "about:blank" && tab.title.startsWith("about:") && tab.title.endsWith("profile")) { 196 | return getAboutPageColour(tab.title.slice(6)); 197 | } else if (url.protocol === "about:") { 198 | return getAboutPageColour(url.pathname); 199 | } else if (url.protocol === "view-source:") { 200 | return { colour: new colour().parse("PLAINTEXT"), reason: "PROTECTED_PAGE" }; 201 | } else if (["chrome:", "resource:", "jar:file:"].includes(url.protocol)) { 202 | if ([".txt", ".css", ".jsm", ".js"].some((extention) => url.href.endsWith(extention))) { 203 | return { colour: new colour().parse("PLAINTEXT"), reason: "PROTECTED_PAGE" }; 204 | } else if ([".png", ".jpg"].some((extention) => url.href.endsWith(extention))) { 205 | return { colour: new colour().parse("IMAGEVIEWER"), reason: "PROTECTED_PAGE" }; 206 | } else { 207 | return { colour: new colour().parse("SYSTEM"), reason: "PROTECTED_PAGE" }; 208 | } 209 | } else if (url.protocol === "moz-extension:") { 210 | return await getAddonPageColour(url.href); 211 | } else if (url.hostname in restrictedSiteColour) { 212 | return getRestrictedSiteColour(url.hostname); 213 | } else if (url.href.startsWith("data:image")) { 214 | return { colour: new colour().parse("IMAGEVIEWER"), reason: "IMAGE_VIEWER" }; 215 | } else if (url.href.endsWith(".pdf") || tab.title.endsWith(".pdf")) { 216 | return { colour: new colour().parse("PDFVIEWER"), reason: "PDF_VIEWER" }; 217 | } else if (url.href.endsWith(".json") || tab.title.endsWith(".json")) { 218 | return { colour: new colour().parse("JSONVIEWER"), reason: "JSON_VIEWER" }; 219 | } else if (tab.favIconUrl?.startsWith("chrome:")) { 220 | return { colour: new colour().parse("DEFAULT"), reason: "PROTECTED_PAGE" }; 221 | } else if (url.href.match(new RegExp(`https?:\/\/${tab.title}$`, "i"))) { 222 | return { colour: new colour().parse("PLAINTEXT"), reason: "TEXT_VIEWER" }; 223 | } else { 224 | return { colour: new colour().parse("FALLBACK"), reason: "FALLBACK_COLOUR" }; 225 | } 226 | } 227 | 228 | /** 229 | * @param {string} pathname 230 | */ 231 | function getAboutPageColour(pathname) { 232 | if (aboutPageColour[pathname]?.[current.scheme]) { 233 | return { colour: new colour().parse(aboutPageColour[pathname][current.scheme]), reason: "PROTECTED_PAGE" }; 234 | } else if (aboutPageColour[pathname]?.[current.reversedScheme]) { 235 | return { 236 | colour: new colour().parse(aboutPageColour[pathname][current.reversedScheme]), 237 | reason: "PROTECTED_PAGE", 238 | }; 239 | } else { 240 | return { colour: new colour().parse("DEFAULT"), reason: "PROTECTED_PAGE" }; 241 | } 242 | } 243 | 244 | /** 245 | * @param {string} hostname 246 | */ 247 | function getRestrictedSiteColour(hostname) { 248 | if (restrictedSiteColour[hostname]?.[current.scheme]) { 249 | return { colour: new colour().parse(restrictedSiteColour[hostname][current.scheme]), reason: "PROTECTED_PAGE" }; 250 | } else if (restrictedSiteColour[hostname]?.[current.reversedScheme]) { 251 | return { 252 | colour: new colour().parse(restrictedSiteColour[hostname][current.reversedScheme]), 253 | reason: "PROTECTED_PAGE", 254 | }; 255 | } else { 256 | return { colour: new colour().parse("FALLBACK"), reason: "PROTECTED_PAGE" }; 257 | } 258 | } 259 | 260 | /** 261 | * @param {string} url 262 | */ 263 | async function getAddonPageColour(url) { 264 | const uuid = url.split(/\/|\?/)[2]; 265 | const addonList = await browser.management.getAll(); 266 | let addonId = null; 267 | for (const addon of addonList) { 268 | if (addon.type !== "extension" || !addon.hostPermissions) continue; 269 | if (addonId) break; 270 | for (const host of addon.hostPermissions) { 271 | if (host.startsWith("moz-extension:") && uuid === host.split(/\/|\?/)[2]) { 272 | addonId = addon.id; 273 | break; 274 | } 275 | } 276 | } 277 | if (!addonId) return { colour: new colour().parse("ADDON"), reason: "ADDON" }; 278 | const policy = pref.getPolicy(pref.getAddonPolicyId(addonId)); 279 | return policy 280 | ? { colour: new colour().parse(policy.value), reason: "ADDON", additionalInfo: addonId } 281 | : { colour: new colour().parse("ADDON"), reason: "ADDON", additionalInfo: addonId }; 282 | } 283 | 284 | /** 285 | * Applies given colour to the browser frame of a tab. 286 | * 287 | * Colour will be adjusted until the contrast ratio is adequate, and it will be stored in `current.info`. 288 | * 289 | * @param {tabs.Tab} tab - The tab in a window, whose frame is being changed. 290 | * @param {colour} colour - The colour to apply to the frame. 291 | */ 292 | function setFrameColour(tab, colour) { 293 | if (!tab?.active) return; 294 | const windowId = tab.windowId; 295 | if (colour.code) { 296 | if (colourCode[colour.code][current.scheme]) { 297 | applyTheme(windowId, colourCode[colour.code][current.scheme], current.scheme); 298 | } else if (colourCode[colour.code][current.reversedScheme] && pref.allowDarkLight) { 299 | applyTheme(windowId, colourCode[colour.code][current.reversedScheme], current.reversedScheme); 300 | } else { 301 | const correctionResult = colourCode[colour][current.reversedScheme].contrastCorrection( 302 | current.scheme, 303 | pref.allowDarkLight, 304 | pref.minContrast_light, 305 | pref.minContrast_dark 306 | ); 307 | applyTheme(windowId, correctionResult.colour, correctionResult.scheme); 308 | current.info[windowId].corrected = correctionResult.corrected; 309 | } 310 | } else { 311 | const correctionResult = colour.contrastCorrection( 312 | current.scheme, 313 | pref.allowDarkLight, 314 | pref.minContrast_light, 315 | pref.minContrast_dark 316 | ); 317 | applyTheme(windowId, correctionResult.colour, correctionResult.scheme); 318 | current.info[windowId].corrected = correctionResult.corrected; 319 | } 320 | } 321 | 322 | /** 323 | * Constructs a theme and applies it to a given window. 324 | * 325 | * @param {number} windowId - The ID of the window. 326 | * @param {colour} colour - Colour of the frame. 327 | * @param {string} colourScheme - `light` or `dark`. 328 | */ 329 | function applyTheme(windowId, colour, colourScheme) { 330 | if (colourScheme === "light") { 331 | const theme = { 332 | colors: { 333 | // adaptive 334 | button_background_active: colour.dim(-1.5 * pref.tabSelected).toRGBA(), 335 | frame: colour.dim(-1.5 * pref.tabbar).toRGBA(), 336 | frame_inactive: colour.dim(-1.5 * pref.tabbar).toRGBA(), 337 | ntp_background: colourCode.HOME[current.scheme].dim(0).toRGBA(), 338 | popup: colour.dim(-1.5 * pref.popup).toRGBA(), 339 | popup_border: colour.dim(-1.5 * (pref.popup + pref.popupBorder)).toRGBA(), 340 | sidebar: colour.dim(-1.5 * pref.sidebar).toRGBA(), 341 | sidebar_border: colour.dim(-1.5 * (pref.sidebar + pref.sidebarBorder)).toRGBA(), 342 | tab_line: colour.dim(-1.5 * (pref.tabSelectedBorder + pref.tabSelected)).toRGBA(), 343 | tab_selected: colour.dim(-1.5 * pref.tabSelected).toRGBA(), 344 | toolbar: colour.dim(-1.5 * pref.toolbar).toRGBA(), 345 | toolbar_bottom_separator: 346 | pref.toolbarBorder === 0 347 | ? "transparent" 348 | : colour.dim(-1.5 * (pref.toolbarBorder + pref.toolbar)).toRGBA(), 349 | toolbar_field: colour.dim(-1.5 * pref.toolbarField).toRGBA(), 350 | toolbar_field_border: colour.dim(-1.5 * (pref.toolbarFieldBorder + pref.toolbarField)).toRGBA(), 351 | toolbar_field_focus: colour.dim(-1.5 * pref.toolbarFieldOnFocus).toRGBA(), 352 | toolbar_top_separator: 353 | pref.tabbarBorder === 0 354 | ? "transparent" 355 | : colour.dim(-1.5 * (pref.tabbarBorder + pref.tabbar)).toRGBA(), 356 | // static 357 | icons: "rgb(0, 0, 0)", 358 | ntp_text: "rgb(0, 0, 0)", 359 | popup_text: "rgb(0, 0, 0)", 360 | sidebar_text: "rgb(0, 0, 0)", 361 | tab_background_text: "rgb(0, 0, 0)", 362 | tab_text: "rgb(0, 0, 0)", 363 | toolbar_field_text: "rgb(0, 0, 0)", 364 | toolbar_text: "rgb(0, 0, 0)", 365 | button_background_hover: "rgba(0, 0, 0, 0.11)", 366 | toolbar_vertical_separator: "rgba(0, 0, 0, 0.11)", 367 | toolbar_field_border_focus: "AccentColor", 368 | popup_highlight: "AccentColor", 369 | sidebar_highlight: "AccentColor", 370 | icons_attention: "AccentColor", 371 | }, 372 | properties: { 373 | // More on: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/theme#properties 374 | color_scheme: "system", 375 | content_color_scheme: "system", 376 | }, 377 | }; 378 | browser.theme.update(windowId, theme); 379 | } 380 | if (colourScheme === "dark") { 381 | const theme = { 382 | colors: { 383 | // adaptive 384 | button_background_active: colour.dim(pref.tabSelected).toRGBA(), 385 | frame: colour.dim(pref.tabbar).toRGBA(), 386 | frame_inactive: colour.dim(pref.tabbar).toRGBA(), 387 | ntp_background: colourCode.HOME[current.scheme].dim(0).toRGBA(), 388 | popup: colour.dim(pref.popup).toRGBA(), 389 | popup_border: colour.dim(pref.popup + pref.popupBorder).toRGBA(), 390 | sidebar: colour.dim(pref.sidebar).toRGBA(), 391 | sidebar_border: colour.dim(pref.sidebar + pref.sidebarBorder).toRGBA(), 392 | tab_line: colour.dim(pref.tabSelectedBorder + pref.tabSelected).toRGBA(), 393 | tab_selected: colour.dim(pref.tabSelected).toRGBA(), 394 | toolbar: colour.dim(pref.toolbar).toRGBA(), 395 | toolbar_bottom_separator: 396 | pref.toolbarBorder === 0 ? "transparent" : colour.dim(pref.toolbarBorder + pref.toolbar).toRGBA(), 397 | toolbar_field: colour.dim(pref.toolbarField).toRGBA(), 398 | toolbar_field_border: colour.dim(pref.toolbarFieldBorder + pref.toolbarField).toRGBA(), 399 | toolbar_field_focus: colour.dim(pref.toolbarFieldOnFocus).toRGBA(), 400 | toolbar_top_separator: 401 | pref.tabbarBorder === 0 ? "transparent" : colour.dim(pref.tabbarBorder + pref.tabbar).toRGBA(), 402 | // static 403 | icons: "rgb(255, 255, 255)", 404 | ntp_text: "rgb(255, 255, 255)", 405 | popup_text: "rgb(255, 255, 255)", 406 | sidebar_text: "rgb(255, 255, 255)", 407 | tab_background_text: "rgb(255, 255, 255)", 408 | tab_text: "rgb(255, 255, 255)", 409 | toolbar_field_text: "rgb(255, 255, 255)", 410 | toolbar_text: "rgb(255, 255, 255)", 411 | button_background_hover: "rgba(255, 255, 255, 0.11)", 412 | toolbar_vertical_separator: "rgba(255, 255, 255, 0.11)", 413 | toolbar_field_border_focus: "AccentColor", 414 | popup_highlight: "AccentColor", 415 | sidebar_highlight: "AccentColor", 416 | icons_attention: "AccentColor", 417 | }, 418 | properties: { 419 | color_scheme: "system", 420 | content_color_scheme: "system", 421 | }, 422 | }; 423 | browser.theme.update(windowId, theme); 424 | } 425 | } 426 | 427 | (async () => { 428 | await initialise(); 429 | onSchemeChanged(update); 430 | browser.tabs.onUpdated.addListener(update); 431 | browser.tabs.onActivated.addListener(update); 432 | browser.tabs.onAttached.addListener(update); 433 | browser.windows.onFocusChanged.addListener(update); 434 | browser.browserSettings.overrideContentColorScheme.onChange.addListener(update); 435 | browser.runtime.onMessage.addListener(handleMessage); 436 | })(); 437 | -------------------------------------------------------------------------------- /scr/colour.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * All possible colour codes. 5 | * 6 | * Each of which represents a certain colour determined by the browser. 7 | */ 8 | const colourCodes = [ 9 | "HOME", 10 | "FALLBACK", 11 | "PLAINTEXT", 12 | "SYSTEM", 13 | "ADDON", 14 | "PDFVIEWER", 15 | "IMAGEVIEWER ", 16 | "JSONVIEWER", 17 | "DEFAULT", 18 | "ACCENT", 19 | ]; 20 | 21 | /** 22 | * Represents a colour with RGBA channels or a predefined colour code. 23 | * 24 | * Provides methods for parsing, manipulating, and converting colours, as well as calculating contrast and luminance for accessibility. 25 | * @class 26 | */ 27 | export default class colour { 28 | #r = 0; 29 | #g = 0; 30 | #b = 0; 31 | #a = 0; 32 | #code; 33 | 34 | /** 35 | * Assigns RGBA values to the colour instance. 36 | * 37 | * @param {number} r - Red channel (0-255). 38 | * @param {number} g - Green channel (0-255). 39 | * @param {number} b - Blue channel (0-255). 40 | * @param {number} a - Alpha channel (0-1). 41 | * @returns {colour} The colour instance with the assigned RGBA values. 42 | * @throws {Error} If the colour is defined by a colour code. 43 | */ 44 | rgba(r, g, b, a) { 45 | this.r = r; 46 | this.g = g; 47 | this.b = b; 48 | this.a = a; 49 | this.#code = undefined; 50 | return this; 51 | } 52 | 53 | /** 54 | * Parses the given initialiser to set the colour value. 55 | * 56 | * @param {string|object|colour} initialiser - The value to parse. Can be a colour code string, a CSS colour string, an instance of the `colour` class, or an RGBA object. 57 | * @param {boolean} [acceptCode=true] - Whether to accept predefined colour codes. 58 | * @returns {this} Returns the current instance for chaining. 59 | * @throws {Error} Throws an error if the input value can't be parsed. 60 | */ 61 | parse(initialiser, acceptCode = true) { 62 | if (acceptCode && colourCodes.includes(initialiser)) { 63 | this.#code = initialiser; 64 | } else if (typeof initialiser === "string") { 65 | const ctx = document.createElement("canvas").getContext("2d"); 66 | ctx.fillStyle = initialiser; 67 | const parsedColour = ctx.fillStyle; 68 | if (parsedColour.startsWith("#")) { 69 | this.rgba( 70 | parseInt(parsedColour.slice(1, 3), 16), 71 | parseInt(parsedColour.slice(3, 5), 16), 72 | parseInt(parsedColour.slice(5, 7), 16), 73 | 1 74 | ); 75 | } else { 76 | this.rgba(...parsedColour.match(/[.?\d]+/g).map(Number)); 77 | } 78 | } else if (initialiser instanceof colour) { 79 | if (initialiser.code) this.#code = initialiser.code; 80 | else this.rgba(initialiser.r, initialiser.g, initialiser.b, initialiser.a); 81 | } else if ( 82 | typeof initialiser === "object" && 83 | "r" in initialiser && 84 | "g" in initialiser && 85 | "b" in initialiser && 86 | "a" in initialiser 87 | ) { 88 | this.rgba(initialiser.r, initialiser.g, initialiser.b, initialiser.a); 89 | } else { 90 | throw new Error("The input value can't be parsed"); 91 | } 92 | return this; 93 | } 94 | 95 | /** 96 | * Returns a new colour instance with its brightness adjusted by the specified percentage. 97 | * 98 | * @param {number} percentage - The dimming factor as a percentage. 99 | * - 0 returns the original colour. 100 | * - Positive values move the colour towards white (100 is white). 101 | * - Negative values move the colour towards black (-100 is black). 102 | * - Values beyond the range of -100 to 100 make the colour black or white. 103 | * @returns {colour} A new colour instance with adjusted brightness. 104 | * @throws {Error} If the colour is defined by a colour code. 105 | */ 106 | dim(percentage) { 107 | this.#noCode(); 108 | const cent = percentage / 100; 109 | if (cent > 1) { 110 | return new colour().rgba(255, 255, 255, this.#a); 111 | } else if (cent > 0) { 112 | return new colour().rgba( 113 | cent * 255 + (1 - cent) * this.#r, 114 | cent * 255 + (1 - cent) * this.#g, 115 | cent * 255 + (1 - cent) * this.#b, 116 | this.#a 117 | ); 118 | } else if (cent === 0) { 119 | return new colour().rgba(this.#r, this.#g, this.#b, this.#a); 120 | } else if (cent < 0) { 121 | return new colour().rgba((cent + 1) * this.#r, (cent + 1) * this.#g, (cent + 1) * this.#b, this.#a); 122 | } else if (cent < -1) { 123 | return new colour().rgba(0, 0, 0, this.#a); 124 | } 125 | } 126 | 127 | /** 128 | * Creates a colour instance that meets the minimum contrast ratio against a specified colour. 129 | * 130 | * @param {"light"|"dark"} preferredScheme - The preferred colour scheme. 131 | * @param {boolean} allowDarkLight - Whether to allow a result in the opposite of the preferred colour scheme. 132 | * @param {number} minContrast_lightX10 - The minimum contrast ratio required for light scheme eligibility (times 10). 133 | * @param {number} minContrast_darkX10 - The minimum contrast ratio required for dark scheme eligibility (times 10). 134 | * @param {colour} [contrastColour_light] - The colour to correct against in light mode, defaulting to black. 135 | * @param {colour} [contrastColour_dark] - The colour to correct against in dark mode, defaulting to white. 136 | * @returns {{ colour: colour, scheme: "light"|"dark", corrected: boolean }} The corrected colour, the scheme, and whether the colour was adjusted. 137 | * @throws {Error} If the colour is defined by a colour code. 138 | */ 139 | contrastCorrection( 140 | preferredScheme, 141 | allowDarkLight, 142 | minContrast_lightX10, 143 | minContrast_darkX10, 144 | contrastColour_light = new colour().rgba(0, 0, 0, 1), 145 | contrastColour_dark = new colour().rgba(255, 255, 255, 1) 146 | ) { 147 | this.#noCode(); 148 | const contrastRatio_light = this.#contrastRatio(contrastColour_light); 149 | const contrastRatio_dark = this.#contrastRatio(contrastColour_dark); 150 | const eligibility_light = contrastRatio_light > minContrast_lightX10 / 10; 151 | const eligibility_dark = contrastRatio_dark > minContrast_darkX10 / 10; 152 | if (eligibility_light && (preferredScheme === "light" || (preferredScheme === "dark" && allowDarkLight))) { 153 | return { colour: this, scheme: "light", corrected: false }; 154 | } else if ( 155 | eligibility_dark && 156 | (preferredScheme === "dark" || (preferredScheme === "light" && allowDarkLight)) 157 | ) { 158 | return { colour: this, scheme: "dark", corrected: false }; 159 | } else if (preferredScheme === "light") { 160 | const dim = 161 | (100 * 162 | ((minContrast_lightX10 / (10 * contrastRatio_light) - 1) * 163 | (this.#relativeLuminanceX255() + 12.75))) / 164 | (255 - this.#relativeLuminanceX255()); 165 | return { colour: this.dim(dim), scheme: "light", corrected: true }; 166 | } else if (preferredScheme === "dark") { 167 | const dim = (100 * (10 * contrastRatio_dark)) / minContrast_darkX10 - 100; 168 | return { colour: this.dim(dim), scheme: "dark", corrected: true }; 169 | } 170 | } 171 | 172 | /** 173 | * Calculates the contrast ratio between this colour and another colour. 174 | * 175 | * Contrast ratio over 4.5 is considered adequate. 176 | * @see https://www.w3.org/TR/WCAG21/#dfn-contrast-ratio 177 | * @private 178 | * @param {colour} colour - The colour to compare against. 179 | * @returns {number} The contrast ratio between the two colours (1.05 to 21). 180 | */ 181 | #contrastRatio(colour) { 182 | const luminance1X255 = this.#relativeLuminanceX255(); 183 | const luminance2X255 = colour.#relativeLuminanceX255(); 184 | return luminance1X255 > luminance2X255 185 | ? (luminance1X255 + 12.75) / (luminance2X255 + 12.75) 186 | : (luminance2X255 + 12.75) / (luminance1X255 + 12.75); 187 | } 188 | 189 | /** 190 | * Calculates the relative luminance of the colour (times 255). 191 | * 192 | * @see https://www.w3.org/TR/WCAG22/#dfn-relative-luminance 193 | * @private 194 | * @returns {number} The relative luminance of the colour (0-255). 195 | */ 196 | #relativeLuminanceX255() { 197 | return ( 198 | 0.2126 * this.#linearChannelLuminance(this.#r) + 199 | 0.7152 * this.#linearChannelLuminance(this.#g) + 200 | 0.0722 * this.#linearChannelLuminance(this.#b) 201 | ); 202 | } 203 | 204 | /** 205 | * Converts an sRGB channel value to linear luminance. 206 | * 207 | * @private 208 | * @param {number} value - The value of a sRGB channel (0-255). 209 | * @returns {number} The linear luminance approximation. 210 | */ 211 | #linearChannelLuminance(value) { 212 | if (value < 0) { 213 | return 0; 214 | } else if (value < 32) { 215 | return 0.1151 * value; 216 | } else if (value < 64) { 217 | return 0.2935 * value - 5.7074; 218 | } else if (value < 96) { 219 | return 0.5236 * value - 20.4339; 220 | } else if (value < 128) { 221 | return 0.788 * value - 45.8232; 222 | } else if (value < 160) { 223 | return 1.0811 * value - 83.3411; 224 | } else if (value < 192) { 225 | return 1.3992 * value - 134.2269; 226 | } else if (value < 224) { 227 | return 1.7395 * value - 199.5679; 228 | } else if (value < 256) { 229 | return 2.1001 * value - 280.341; 230 | } else { 231 | return 255; 232 | } 233 | } 234 | 235 | /** 236 | * Returns the colour as a string. 237 | * If the colour is defined by a code, returns the code. 238 | * Otherwise, returns an RGBA string. 239 | * 240 | * @returns {string} The colour code or the CSS representation of the colour. 241 | */ 242 | toString() { 243 | if (this.#code) return this.#code; 244 | else return this.toRGBA(); 245 | } 246 | 247 | /** 248 | * Returns the colour as an RGB CSS string. 249 | * 250 | * @returns {string} The RGB CSS string. 251 | * @throws {Error} If the colour is defined by a colour code. 252 | */ 253 | toRGB() { 254 | this.#noCode(); 255 | return `rgb(${this.#r}, ${this.#g}, ${this.#b})`; 256 | } 257 | 258 | /** 259 | * Returns the colour as an RGBA CSS string. 260 | * 261 | * @returns {string} The RGBA CSS string. 262 | * @throws {Error} If the colour is defined by a colour code. 263 | */ 264 | toRGBA() { 265 | this.#noCode(); 266 | return `rgb(${this.#r}, ${this.#g}, ${this.#b}, ${this.#a})`; 267 | } 268 | 269 | /** 270 | * Returns the colour as a hex string. 271 | * 272 | * @returns {string} The hex string. 273 | * @throws {Error} If the colour is defined by a colour code. 274 | */ 275 | toHex() { 276 | this.#noCode(); 277 | const hexR = Math.round(this.#r).toString(16).padStart(2, "0"); 278 | const hexG = Math.round(this.#g).toString(16).padStart(2, "0"); 279 | const hexB = Math.round(this.#b).toString(16).padStart(2, "0"); 280 | return `#${hexR}${hexG}${hexB}`; 281 | } 282 | 283 | /** 284 | * Returns the colour as a hex string with alpha. 285 | * 286 | * @returns {string} The hex string with alpha. 287 | * @throws {Error} If the colour is defined by a colour code. 288 | */ 289 | toHexa() { 290 | this.#noCode(); 291 | const hexR = Math.round(this.#r).toString(16).padStart(2, "0"); 292 | const hexG = Math.round(this.#g).toString(16).padStart(2, "0"); 293 | const hexB = Math.round(this.#b).toString(16).padStart(2, "0"); 294 | const hexA = Math.round(255 * this.#a) 295 | .toString(16) 296 | .padStart(2, "0"); 297 | return `#${hexR}${hexG}${hexB}${hexA}`; 298 | } 299 | 300 | /** 301 | * Throws if the colour is defined by a colour code. 302 | * 303 | * @private 304 | * @throws {Error} If the colour is defined by a colour code. 305 | */ 306 | #noCode() { 307 | if (this.#code) throw new Error("The colour is defined by a colour code"); 308 | } 309 | 310 | /** 311 | * Gets or sets the red channel value. 312 | * 313 | * @type {number} 314 | * @throws {Error} If the colour is defined by a colour code or value is invalid. 315 | */ 316 | get r() { 317 | this.#noCode(); 318 | return this.#r; 319 | } 320 | 321 | set r(value) { 322 | this.#noCode(); 323 | const num = Number(value); 324 | if (isNaN(num)) throw new Error("Invalid value for r"); 325 | this.#r = Math.max(0, Math.min(255, num)); 326 | } 327 | 328 | /** 329 | * Gets or sets the green channel value. 330 | * 331 | * @type {number} 332 | * @throws {Error} If the colour is defined by a colour code or value is invalid. 333 | */ 334 | get g() { 335 | this.#noCode(); 336 | return this.#g; 337 | } 338 | 339 | set g(value) { 340 | this.#noCode(); 341 | const num = Number(value); 342 | if (isNaN(num)) throw new Error("Invalid value for g"); 343 | this.#g = Math.max(0, Math.min(255, num)); 344 | } 345 | 346 | /** 347 | * Gets or sets the blue channel value. 348 | * 349 | * @type {number} 350 | * @throws {Error} If the colour is defined by a colour code or value is invalid. 351 | */ 352 | get b() { 353 | this.#noCode(); 354 | return this.#b; 355 | } 356 | 357 | set b(value) { 358 | this.#noCode(); 359 | const num = Number(value); 360 | if (isNaN(num)) throw new Error("Invalid value for b"); 361 | this.#b = Math.max(0, Math.min(255, num)); 362 | } 363 | 364 | /** 365 | * Gets or sets the alpha channel value. 366 | * 367 | * @type {number} 368 | * @throws {Error} If the colour is defined by a colour code or value is invalid. 369 | */ 370 | get a() { 371 | this.#noCode(); 372 | this.#noCode(); 373 | return this.#a; 374 | } 375 | 376 | set a(value) { 377 | this.#noCode(); 378 | const num = Number(value); 379 | if (isNaN(num)) throw new Error("Invalid value for a"); 380 | this.#a = Math.max(0, Math.min(1, num)); 381 | } 382 | 383 | /** 384 | * Gets or sets the colour code. 385 | * 386 | * @type {string|undefined} 387 | * @throws {Error} If the value is not a valid colour code. 388 | */ 389 | get code() { 390 | return this.#code; 391 | } 392 | 393 | set code(value) { 394 | if (colourCodes.includes(value)) this.#code = value; 395 | else throw new Error("Invalid colour code"); 396 | } 397 | } 398 | -------------------------------------------------------------------------------- /scr/default_values.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** The version of ATBC */ 4 | export const addonVersion = [2, 5]; 5 | 6 | /** Default light homepage colour */ 7 | export const default_homeBackground_light = "#ffffff"; 8 | /** Default dark homepage colour */ 9 | export const default_homeBackground_dark = "#2b2a33"; 10 | /** Default light fallback colours */ 11 | export const default_fallbackColour_light = "#ffffff"; 12 | /** Default dark fallback colour */ 13 | export const default_fallbackColour_dark = "#2b2a33"; 14 | 15 | /** 16 | * Colours for about:pages. 17 | */ 18 | export const aboutPageColour = Object.freeze({ 19 | checkerboard: { light: "DEFAULT", dark: undefined }, 20 | deleteprofile: { light: "DEFAULT", dark: "#2b2a33" }, 21 | "devtools-toolbox": { light: "DEFAULT", dark: "#0c0c0d" }, 22 | editprofile: { light: "DEFAULT", dark: "#2b2a33" }, 23 | firefoxview: { light: "HOME", dark: "HOME" }, 24 | home: { light: "HOME", dark: "HOME" }, 25 | logo: { light: undefined, dark: "IMAGEVIEWER" }, 26 | mozilla: { light: undefined, dark: "#800000" }, 27 | newtab: { light: "HOME", dark: "HOME" }, 28 | newprofile: { light: "DEFAULT", dark: "#2b2a33" }, 29 | performance: { light: "DEFAULT", dark: "#23222a" }, 30 | plugins: { light: "DEFAULT", dark: "#2b2a33" }, 31 | privatebrowsing: { light: undefined, dark: "#25003e" }, 32 | processes: { light: "#eeeeee", dark: "#32313a" }, 33 | "sync-log": { light: "#ececec", dark: "#282828" }, 34 | }); 35 | 36 | /** 37 | * Colours for restricted sites. 38 | */ 39 | export const restrictedSiteColour = Object.freeze({ 40 | "accounts-static.cdn.mozilla.net": { light: "DEFAULT", dark: "DEFAULT" }, 41 | "accounts.firefox.com": { light: "#fafafd", dark: undefined }, 42 | "addons.cdn.mozilla.net": { light: "DEFAULT", dark: "DEFAULT" }, 43 | "addons.mozilla.org": { light: undefined, dark: "#20123a" }, 44 | "content.cdn.mozilla.net": { light: "DEFAULT", dark: "DEFAULT" }, 45 | "discovery.addons.mozilla.org": { light: "#ececec", dark: undefined }, 46 | "install.mozilla.org": { light: "DEFAULT", dark: "DEFAULT" }, 47 | "support.mozilla.org": { light: "#ffffff", dark: undefined }, 48 | }); 49 | 50 | /** 51 | * Recommended colours for Add-ons' built-in pages. 52 | * 53 | * Contributions are welcomed. 54 | * 55 | * @todo Adds light / dark attributes. 56 | */ 57 | export const recommendedAddonPageColour = Object.freeze({ 58 | "addon@darkreader.org": "#141e24", // Dark Reader 59 | "adguardadblocker@adguard.com": "#1f1f1f", // AdGuard AdBlocker 60 | "deArrow@ajay.app": "#333333", // DeArrow 61 | "enhancerforyoutube@maximerf.addons.mozilla.org": "#292a2d", // Enhancer for YouTube™ 62 | "languagetool-webextension@languagetool.org": "#111213", // LanguageTool 63 | "sponsorBlocker@ajay.app": "#333333", // SponsorBlock for YouTube 64 | "uBlock0@raymondhill.net": "#1b1b24", // uBlock Origin 65 | "{036a55b4-5e72-4d05-a06c-cba2dfcc134a}": "#171a1b", // Translate Web Pages 66 | "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}": "#242424", // Stylus 67 | "{aecec67f-0d10-4fa7-b7c7-609a2db280cf}": "#262626", // Violentmonkey 68 | "{ce9f4b1f-24b8-4e9a-9051-b9e472b1b2f2}": "#1c1b1f", // Clear Browsing Data 69 | }); 70 | -------------------------------------------------------------------------------- /scr/elements.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import colour from "./colour.js"; 4 | 5 | /** 6 | * Sets up a checkbox element with an onChange callback. 7 | * 8 | * @param {HTMLElement} checkbox - The checkbox element to set up. 9 | * @param {(pref: string, checked: boolean) => void} onChange - Callback invoked when the checkbox is clicked. 10 | */ 11 | export function setupCheckbox(checkbox, onChange) { 12 | checkbox.onclick = () => { 13 | onChange(checkbox.dataset.pref, checkbox.checked); 14 | }; 15 | } 16 | 17 | /** 18 | * Sets the checked state of a checkbox element. 19 | * 20 | * @param {HTMLElement} checkbox - The checkbox element. 21 | * @param {boolean} value - The value to set (checked or not). 22 | */ 23 | export function setCheckboxValue(checkbox, value) { 24 | checkbox.checked = value; 25 | } 26 | 27 | /** 28 | * Sets up a slider element with increment / decrement buttons and an onChange callback. 29 | * 30 | * @param {HTMLElement} slider - The slider element to set up. 31 | * @param {(pref: string, value: number) => void} onChange - Callback invoked when the slider value changes. 32 | */ 33 | export function setupSlider(slider, onChange) { 34 | const minusButton = slider.querySelector("button:nth-of-type(1)"); 35 | const plusButton = slider.querySelector("button:nth-of-type(2)"); 36 | minusButton.addEventListener("mousedown", () => { 37 | if (+slider.dataset.value <= +slider.dataset.min) return; 38 | const value = +slider.dataset.value - +slider.dataset.step; 39 | setSliderValue(slider, value); 40 | onChange(slider.dataset.pref, value); 41 | }); 42 | plusButton.addEventListener("mousedown", () => { 43 | if (+slider.dataset.value >= +slider.dataset.max) return; 44 | const value = +slider.dataset.value + +slider.dataset.step; 45 | setSliderValue(slider, value); 46 | onChange(slider.dataset.pref, value); 47 | }); 48 | } 49 | 50 | /** 51 | * Sets the value of a slider element and updates its display. 52 | * 53 | * @param {HTMLElement} slider - The slider element. 54 | * @param {number} value - The value to set. 55 | */ 56 | export function setSliderValue(slider, value) { 57 | const sliderBody = slider.querySelector(".slider-body"); 58 | const percentage = (value - +slider.dataset.min) / (+slider.dataset.max - +slider.dataset.min); 59 | const scale = slider.dataset.scale; 60 | slider.dataset.value = value; 61 | sliderBody.textContent = 62 | scale && value !== 0 ? `${value.toString().slice(0, -scale)}.${value.toString().slice(-scale)}` : value; 63 | sliderBody.style.setProperty("--slider-position", `${100 * (1 - percentage)}%`); 64 | } 65 | 66 | /** 67 | * Sets up a policy header input field with an initial value and onChange callback. 68 | * 69 | * @param {HTMLElement} policyHeaderInputWrapper - The wrapper element containing the input. 70 | * @param {string} initialValue - The initial value to set. 71 | * @param {(value: string) => void} onChange - Callback invoked when the input value changes. 72 | */ 73 | export function setupPolicyHeaderInput(policyHeaderInputWrapper, initialValue, onChange) { 74 | const policyHeaderInput = policyHeaderInputWrapper.querySelector(".policy-header-input"); 75 | policyHeaderInput.value = initialValue; 76 | policyHeaderInput.addEventListener("focus", () => policyHeaderInput.select()); 77 | policyHeaderInput.addEventListener("input", () => onChange(policyHeaderInput.value.trim())); 78 | policyHeaderInput.addEventListener("keydown", (event) => { 79 | if (event.key === "Enter") policyHeaderInput.blur(); 80 | }); 81 | } 82 | 83 | /** 84 | * Sets the value of a policy header input field. 85 | * 86 | * @param {HTMLElement} policyHeaderInputWrapper - The wrapper element containing the input. 87 | * @param {string} value - The value to set. 88 | */ 89 | export function setPolicyHeaderInputValue(policyHeaderInputWrapper, value) { 90 | const policyHeaderInput = policyHeaderInputWrapper.querySelector(".policy-header-input"); 91 | policyHeaderInput.value = value; 92 | } 93 | 94 | /** 95 | * Gets the value of a policy header input field. 96 | * 97 | * @param {HTMLElement} policyHeaderInputWrapper - The wrapper element containing the input. 98 | * @returns {string} The current value of the input. 99 | */ 100 | export function getPolicyHeaderInputValue(policyHeaderInputWrapper) { 101 | const policyHeaderInput = policyHeaderInputWrapper.querySelector(".policy-header-input"); 102 | return policyHeaderInput.value; 103 | } 104 | 105 | /** 106 | * Sets up a colour input field and colour picker with an initial colour and onChange callback. 107 | * 108 | * @param {HTMLElement} colourInputWrapper - The wrapper element containing the colour input and picker. 109 | * @param {string} initialColour - The initial colour value (hex). 110 | * @param {(colour: string) => void} onChange - Callback invoked when the colour changes. 111 | */ 112 | export function setupColourInput(colourInputWrapper, initialColour, onChange) { 113 | const colourInput = colourInputWrapper.querySelector(".colour-input"); 114 | const colourPicker = colourInputWrapper.querySelector(".colour-picker-display"); 115 | const colourPickerInput = colourInputWrapper.querySelector("input[type='color']"); 116 | colourInput.value = initialColour; 117 | colourPicker.style.backgroundColor = initialColour; 118 | colourPickerInput.value = initialColour; 119 | colourInput.addEventListener("focus", () => colourInput.select()); 120 | colourInput.addEventListener("blur", () => { 121 | const hex = new colour().parse(colourInput.value, false).toHex(); 122 | colourInput.value = hex; 123 | colourPicker.style.backgroundColor = hex; 124 | colourPickerInput.value = hex; 125 | onChange(hex); 126 | }); 127 | colourInput.addEventListener("keydown", (event) => { 128 | if (event.key === "Enter") colourInput.blur(); 129 | }); 130 | colourInput.addEventListener("input", () => { 131 | const hex = new colour().parse(colourInput.value, false).toHex(); 132 | colourPicker.style.backgroundColor = hex; 133 | colourPickerInput.value = hex; 134 | onChange(hex); 135 | }); 136 | colourPickerInput.addEventListener("input", () => { 137 | const colour = colourPickerInput.value; 138 | colourPicker.style.backgroundColor = colour; 139 | colourInput.value = colour; 140 | onChange(colour); 141 | }); 142 | } 143 | 144 | /** 145 | * Sets the value of a colour input field and updates the picker display. 146 | * 147 | * @param {HTMLElement} colourInputWrapper - The wrapper element containing the colour input and picker. 148 | * @param {string} value - The colour value to set (hex). 149 | */ 150 | export function setColourInputValue(colourInputWrapper, value) { 151 | const colourInput = colourInputWrapper.querySelector(".colour-input"); 152 | const colourPicker = colourInputWrapper.querySelector("input[type='color']"); 153 | const colourPickerDisplay = colourInputWrapper.querySelector(".colour-picker-display"); 154 | colourInput.value = value; 155 | colourPicker.value = value; 156 | colourPickerDisplay.style.backgroundColor = value; 157 | } 158 | 159 | /** 160 | * Gets the value of a colour input field. 161 | * 162 | * @param {HTMLElement} colourInputWrapper - The wrapper element containing the colour input and picker. 163 | * @returns {string} The current colour value (hex). 164 | */ 165 | export function getColourInputValue(colourInputWrapper) { 166 | const colourPicker = colourInputWrapper.querySelector("input[type='color']"); 167 | return colourPicker.value; 168 | } 169 | 170 | /** 171 | * Sets up a theme colour switch with an initial selection and onChange callback. 172 | * 173 | * @param {HTMLElement} themeColourSwitchWrapper - The wrapper containing the radio buttons. 174 | * @param {boolean} initialSelection - Whether to use the theme colour initially. 175 | * @param {(useTheme: boolean) => void} onChange - Callback invoked when the selection changes. 176 | */ 177 | export function setupThemeColourSwitch(themeColourSwitchWrapper, initialSelection, onChange) { 178 | const useThemeColourRadioButton = themeColourSwitchWrapper.querySelector("input[type='radio']:nth-of-type(1)"); 179 | const ignoreThemeColourRadioButton = themeColourSwitchWrapper.querySelector("input[type='radio']:nth-of-type(2)"); 180 | if (initialSelection === false) ignoreThemeColourRadioButton.checked = true; 181 | useThemeColourRadioButton.addEventListener("change", () => { 182 | if (useThemeColourRadioButton.checked) onChange(true); 183 | }); 184 | ignoreThemeColourRadioButton.addEventListener("change", () => { 185 | if (ignoreThemeColourRadioButton.checked) onChange(false); 186 | }); 187 | } 188 | 189 | /** 190 | * Sets the value of a theme colour switch. 191 | * 192 | * @param {HTMLElement} themeColourSwitchWrapper - The wrapper containing the radio buttons. 193 | * @param {boolean} value - Whether to use the theme colour. 194 | */ 195 | export function setThemeColourSwitchValue(themeColourSwitchWrapper, value) { 196 | const useThemeColourRadioButton = themeColourSwitchWrapper.querySelector("input[type='radio']:nth-of-type(1)"); 197 | const ignoreThemeColourRadioButton = themeColourSwitchWrapper.querySelector("input[type='radio']:nth-of-type(2)"); 198 | useThemeColourRadioButton.checked = value; 199 | ignoreThemeColourRadioButton.checked = !value; 200 | } 201 | 202 | /** 203 | * Gets the value of a theme colour switch. 204 | * 205 | * @param {HTMLElement} themeColourSwitchWrapper - The wrapper containing the radio buttons. 206 | * @returns {boolean} True if the theme colour is selected, false otherwise. 207 | */ 208 | export function getThemeColourSwitchValue(themeColourSwitchWrapper) { 209 | const useThemeColourRadioButton = themeColourSwitchWrapper.querySelector("input[type='radio']:nth-of-type(1)"); 210 | return useThemeColourRadioButton.checked; 211 | } 212 | 213 | /** 214 | * Sets up a query selector input field with an initial value and onChange callback. 215 | * 216 | * @param {HTMLElement} QuerySelectorInputWrapper - The wrapper element containing the input. 217 | * @param {string} initialQuerySelector - The initial query selector value. 218 | * @param {(value: string) => void} onChange - Callback invoked when the input value changes. 219 | */ 220 | export function setupQuerySelectorInput(QuerySelectorInputWrapper, initialQuerySelector, onChange) { 221 | const QuerySelectorInput = QuerySelectorInputWrapper.querySelector("input[type='text']"); 222 | QuerySelectorInput.value = initialQuerySelector; 223 | QuerySelectorInput.addEventListener("focus", () => QuerySelectorInput.select()); 224 | QuerySelectorInput.addEventListener("input", () => onChange(QuerySelectorInput.value.trim())); 225 | QuerySelectorInput.addEventListener("keydown", (event) => { 226 | if (event.key === "Enter") QuerySelectorInput.blur(); 227 | }); 228 | } 229 | 230 | /** 231 | * Sets the value of a query selector input field. 232 | * 233 | * @param {HTMLElement} QuerySelectorInputWrapper - The wrapper element containing the input. 234 | * @param {string} value - The value to set. 235 | */ 236 | export function setQuerySelectorInputValue(QuerySelectorInputWrapper, value) { 237 | const QuerySelectorInput = QuerySelectorInputWrapper.querySelector("input[type='text']"); 238 | QuerySelectorInput.value = value; 239 | } 240 | 241 | /** 242 | * Gets the value of a query selector input field. 243 | * 244 | * @param {HTMLElement} QuerySelectorInputWrapper - The wrapper element containing the input. 245 | * @returns {string} The current value of the input. 246 | */ 247 | export function getQuerySelectorInputValue(QuerySelectorInputWrapper) { 248 | const QuerySelectorInput = QuerySelectorInputWrapper.querySelector("input[type='text']"); 249 | return QuerySelectorInput.value; 250 | } 251 | 252 | /** 253 | * Sets the ID for a colour policy section and updates related element attributes. 254 | * 255 | * @param {HTMLElement} policySection - The policy section element. 256 | * @param {number} id - The ID to set. 257 | */ 258 | export function setColourPolicySectionId(policySection, id) { 259 | policySection.dataset.id = id; 260 | policySection.querySelector(".colour-picker-display").htmlFor = `colour-picker-display-${id}`; 261 | policySection.querySelector("input[type='color']").id = `colour-picker-display-${id}`; 262 | } 263 | 264 | /** 265 | * Sets the ID for a flexible policy section and updates related element attributes. 266 | * 267 | * @param {HTMLElement} policySection - The policy section element. 268 | * @param {number} id - The ID to set. 269 | */ 270 | export function setFlexiblePolicySectionId(policySection, id) { 271 | policySection.dataset.id = id; 272 | policySection.querySelector(".colour-picker-display").htmlFor = `colour-picker-display-${id}`; 273 | policySection.querySelector("input[type='color']").id = `colour-picker-display-${id}`; 274 | policySection.querySelector("input.toggle-switch:nth-of-type(1)").name = `theme-colour-${id}`; 275 | policySection.querySelector("input.toggle-switch:nth-of-type(1)").id = `use-theme-colour-${id}`; 276 | policySection.querySelector("label.toggle-switch:nth-of-type(1)").htmlFor = `use-theme-colour-${id}`; 277 | policySection.querySelector("input.toggle-switch:nth-of-type(2)").name = `theme-colour-${id}`; 278 | policySection.querySelector("input.toggle-switch:nth-of-type(2)").id = `ignore-theme-colour-${id}`; 279 | policySection.querySelector("label.toggle-switch:nth-of-type(2)").htmlFor = `ignore-theme-colour-${id}`; 280 | } 281 | -------------------------------------------------------------------------------- /scr/images/ATBC_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/scr/images/ATBC_128.png -------------------------------------------------------------------------------- /scr/images/ATBC_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/scr/images/ATBC_16.png -------------------------------------------------------------------------------- /scr/images/ATBC_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/scr/images/ATBC_32.png -------------------------------------------------------------------------------- /scr/images/ATBC_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/scr/images/ATBC_48.png -------------------------------------------------------------------------------- /scr/images/ATBC_96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/easonwong-de/Adaptive-Tab-Bar-Colour/88509d6b097f115e586af681fa7e84043446fb8c/scr/images/ATBC_96.png -------------------------------------------------------------------------------- /scr/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extensionName__", 3 | "description": "__MSG_extensionDescription__", 4 | "version": "2.5", 5 | "author": "Eason Wong", 6 | "homepage_url": "https://github.com/easonwong-de/Adaptive-Tab-Bar-Colour", 7 | "default_locale": "en", 8 | "browser_specific_settings": { 9 | "gecko": { 10 | "id": "ATBC@EasonWong", 11 | "strict_min_version": "112.0" 12 | } 13 | }, 14 | "permissions": ["tabs", "theme", "storage", "browserSettings", "management"], 15 | "background": { 16 | "scripts": ["background.js"], 17 | "type": "module", 18 | "persistent": true 19 | }, 20 | "content_scripts": [ 21 | { 22 | "matches": [""], 23 | "js": ["atbc.js"] 24 | } 25 | ], 26 | "options_ui": { 27 | "page": "options/options.html", 28 | "open_in_tab": false, 29 | "browser_style": true 30 | }, 31 | "browser_action": { 32 | "default_popup": "popup/popup.html", 33 | "default_title": "__MSG_extensionName__", 34 | "browser_style": true, 35 | "default_icon": { 36 | "16": "images/ATBC_16.png", 37 | "32": "images/ATBC_32.png", 38 | "48": "images/ATBC_48.png", 39 | "96": "images/ATBC_96.png", 40 | "128": "images/ATBC_128.png" 41 | } 42 | }, 43 | "icons": { 44 | "16": "images/ATBC_16.png", 45 | "32": "images/ATBC_32.png", 46 | "48": "images/ATBC_48.png", 47 | "96": "images/ATBC_96.png", 48 | "128": "images/ATBC_128.png" 49 | }, 50 | "manifest_version": 2 51 | } 52 | -------------------------------------------------------------------------------- /scr/options/options.css: -------------------------------------------------------------------------------- 1 | @import "../shared.css"; 2 | 3 | body { 4 | background-color: var(--colour-0); 5 | color: var(--text-colour-normal); 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | #settings-wrapper { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 1rem; 14 | padding-block: 1rem; 15 | } 16 | 17 | #footer { 18 | display: contents; 19 | } 20 | 21 | /* Layout */ 22 | 23 | #settings-wrapper:not(.tab-switch-1) #tab-1, 24 | #settings-wrapper:not(.tab-switch-2) #tab-2, 25 | #settings-wrapper:not(.tab-switch-3) #tab-3 { 26 | display: none; 27 | } 28 | 29 | .tab { 30 | width: 100%; 31 | } 32 | 33 | .grid-one-column { 34 | display: grid; 35 | gap: 1rem; 36 | grid-template-columns: 1fr; 37 | } 38 | 39 | .grid-two-columns { 40 | display: grid; 41 | gap: 1rem; 42 | grid-template-columns: 1fr 1fr; 43 | 44 | @media screen and (max-width: 450px) { 45 | & { 46 | grid-template-columns: 1fr; 47 | } 48 | } 49 | } 50 | 51 | .column { 52 | display: flex; 53 | flex-direction: column; 54 | gap: 1rem; 55 | } 56 | 57 | .section { 58 | align-items: center; 59 | background-color: var(--colour-1); 60 | border-radius: 8px; 61 | display: flex; 62 | flex: 0 0 auto; 63 | flex-direction: column; 64 | gap: 0.5rem; 65 | padding: 1.25rem; 66 | } 67 | 68 | .section-title { 69 | font-weight: 600; 70 | } 71 | 72 | .section-text { 73 | color: var(--text-colour-secondary); 74 | } 75 | 76 | .section-subtitle { 77 | align-items: center; 78 | display: flex; 79 | font-size: inherit; 80 | font-weight: inherit; 81 | line-height: normal; 82 | margin-block: 0.25rem; 83 | padding: 0; 84 | } 85 | 86 | /* Elements */ 87 | 88 | a { 89 | &:link, 90 | &:visited { 91 | color: var(--link-colour-normal); 92 | text-decoration: none; 93 | } 94 | &:hover { 95 | color: var(--link-colour-hover); 96 | text-decoration: underline; 97 | } 98 | &:active { 99 | color: var(--link-colour-active); 100 | text-decoration: underline; 101 | } 102 | } 103 | 104 | hr { 105 | background-color: var(--colour-3); 106 | border-width: 0; 107 | height: 1px; 108 | margin: 0; 109 | width: 100%; 110 | } 111 | 112 | button { 113 | align-items: center; 114 | background-color: var(--colour-2); 115 | border: none; 116 | border-radius: 4px; 117 | color: var(--text-colour-normal); 118 | display: flex; 119 | height: 2rem; 120 | justify-content: center; 121 | padding: 0; 122 | text-align: center; 123 | width: 2rem; 124 | 125 | &:hover { 126 | background-color: var(--colour-3); 127 | } 128 | &:active { 129 | background-color: var(--colour-5); 130 | } 131 | } 132 | 133 | .text-button { 134 | align-items: center; 135 | background-color: var(--colour-2); 136 | border: none; 137 | border-radius: 4px; 138 | box-sizing: border-box; 139 | color: var(--text-colour-normal); 140 | display: flex; 141 | font-size: inherit; 142 | gap: 0.5rem; 143 | height: 2rem; 144 | justify-content: start; 145 | padding: 0 0.5rem; 146 | text-align: start; 147 | width: 100%; 148 | 149 | &:hover { 150 | background-color: var(--colour-3); 151 | } 152 | &:active { 153 | background-color: var(--colour-5); 154 | } 155 | } 156 | 157 | select { 158 | background-color: var(--colour-2); 159 | border: none; 160 | border-radius: 4px; 161 | color: var(--text-colour-normal); 162 | display: flex; 163 | height: 2rem; 164 | padding-inline: 0.5rem 0; 165 | vertical-align: middle; 166 | width: max-content; 167 | 168 | &:hover { 169 | background-color: var(--colour-3); 170 | } 171 | &:active { 172 | background-color: var(--colour-5); 173 | } 174 | } 175 | 176 | input[type="color"] { 177 | display: none; 178 | } 179 | 180 | input[type="text"] { 181 | background-color: var(--colour-2); 182 | border: none; 183 | border-radius: 4px; 184 | box-sizing: border-box; 185 | color: inherit; 186 | display: inline-block; 187 | font-family: monospace; 188 | height: 2rem; 189 | line-height: 100%; 190 | outline: none; 191 | outline-offset: -1px; 192 | padding-inline: 0.5rem; 193 | vertical-align: middle; 194 | width: 100%; 195 | 196 | &:hover { 197 | background-color: var(--colour-3); 198 | } 199 | 200 | &:focus-visible { 201 | background-color: transparent; 202 | outline: solid 1px var(--colour-3); 203 | } 204 | } 205 | 206 | input[type="checkbox"] { 207 | accent-color: var(--accent-colour); 208 | } 209 | 210 | input[type="radio"] { 211 | accent-color: var(--accent-colour); 212 | margin: 0; 213 | } 214 | 215 | /* Slider */ 216 | 217 | .slider { 218 | display: flex; 219 | flex-direction: row; 220 | gap: 0.5rem; 221 | margin-block-start: 0.5rem; 222 | } 223 | 224 | .slider-title { 225 | color: var(--text-colour-secondary); 226 | font-size: 80%; 227 | text-align: center; 228 | } 229 | 230 | .slider-body { 231 | align-items: center; 232 | background-color: var(--colour-2); 233 | background-image: linear-gradient(to right, var(--transparent-0), var(--transparent-0) 50%, var(--colour-2) 50%); 234 | background-position-x: var(--slider-position, 100%); 235 | background-size: 200% 100%; 236 | border-radius: 4px; 237 | display: flex; 238 | height: 2rem; 239 | justify-content: center; 240 | line-height: 2rem; 241 | text-align: center; 242 | transition: background-position-x 0.1s ease-in; 243 | width: 10rem; 244 | 245 | &.percentage::after { 246 | content: "%"; 247 | margin-inline-start: 0.5rem; 248 | } 249 | } 250 | 251 | /* Toggle */ 252 | /* To-do: use a light switch instead of a simple checkbox */ 253 | 254 | .checkbox-wrapper { 255 | display: flex; 256 | gap: 1rem; 257 | width: 100%; 258 | } 259 | 260 | /* Colour input */ 261 | 262 | .colour-input-wrapper { 263 | position: relative; 264 | } 265 | 266 | .colour-picker-display { 267 | border-radius: 50%; 268 | cursor: pointer; 269 | height: 1rem; 270 | left: 0.5rem; 271 | outline: solid 1px var(--colour-5); 272 | position: absolute; 273 | top: 0.5rem; 274 | width: 1rem; 275 | } 276 | 277 | input[type="text"].colour-input { 278 | padding-left: 2rem; 279 | } 280 | 281 | /* Toggle switch */ 282 | 283 | .toggle-switch-wrapper { 284 | background-color: var(--colour-2); 285 | border-radius: 4px; 286 | display: grid; 287 | grid-auto-columns: minmax(0, 1fr); 288 | grid-auto-flow: column; 289 | height: 2rem; 290 | overflow: hidden; 291 | width: 100%; 292 | 293 | label.toggle-switch { 294 | align-items: center; 295 | display: flex; 296 | justify-content: center; 297 | 298 | &:hover { 299 | background-color: var(--colour-3); 300 | } 301 | &:active { 302 | background-color: var(--colour-5); 303 | } 304 | } 305 | 306 | input.toggle-switch { 307 | display: none; 308 | 309 | &:checked + label.toggle-switch { 310 | background-color: var(--colour-4); 311 | color: var(--link-colour-normal); 312 | 313 | &:hover { 314 | background-color: var(--colour-5); 315 | } 316 | } 317 | } 318 | } 319 | 320 | /* Tab switch */ 321 | 322 | .tab-switch-wrapper { 323 | &.toggle-switch-wrapper { 324 | background-color: var(--colour-1); 325 | height: auto; 326 | } 327 | 328 | label.toggle-switch { 329 | font-weight: 600; 330 | padding: 0.5rem 2rem; 331 | 332 | &:hover { 333 | background-color: var(--colour-2); 334 | } 335 | &:active { 336 | background-color: var(--colour-4); 337 | } 338 | } 339 | 340 | input.toggle-switch { 341 | &:checked + label.toggle-switch { 342 | background-color: var(--colour-3); 343 | color: var(--link-colour-normal); 344 | 345 | &:hover { 346 | background-color: var(--colour-4); 347 | } 348 | } 349 | } 350 | } 351 | 352 | /* Fixed elements */ 353 | 354 | p#reinstall-tip { 355 | width: 50%; 356 | } 357 | 358 | #add-new-rule { 359 | background-color: var(--colour-1); 360 | border-radius: 6px; 361 | height: 3rem; 362 | width: 100%; 363 | 364 | &:hover { 365 | background-color: var(--colour-2); 366 | } 367 | &:active { 368 | background-color: var(--colour-4); 369 | } 370 | } 371 | 372 | #sponsor { 373 | display: flex; 374 | gap: 0.5rem; 375 | filter: saturate(0) brightness(1.1); 376 | 377 | @media (prefers-color-scheme: dark) { 378 | & { 379 | filter: sepia(1) hue-rotate(218deg) saturate(0.5) brightness(0.7); 380 | } 381 | } 382 | } 383 | 384 | /* List */ 385 | 386 | .list { 387 | display: flex; 388 | flex-direction: column; 389 | 390 | .section { 391 | border-radius: 0; 392 | display: grid; 393 | gap: 0.5rem; 394 | grid-template-columns: auto max-content 10rem max-content; 395 | padding: 0.5rem; 396 | 397 | &:nth-of-type(even) { 398 | background-color: var(--colour-1-5); 399 | } 400 | } 401 | 402 | :is(.section:first-of-type) { 403 | border-top-left-radius: 6px; 404 | border-top-right-radius: 6px; 405 | } 406 | 407 | :is(.section:last-of-type) { 408 | border-bottom-left-radius: 6px; 409 | border-bottom-right-radius: 6px; 410 | } 411 | } 412 | 413 | #tab-2:not(:has(.policy)) { 414 | gap: 0; 415 | } 416 | 417 | #tab-3 .list .section { 418 | grid-template-columns: auto max-content 7.5rem; 419 | } 420 | 421 | .policy-header { 422 | margin-inline: 0.5rem auto; 423 | max-width: 100%; 424 | } 425 | 426 | .policy .policy-header { 427 | max-width: 60%; 428 | } 429 | 430 | .policy-header-input-wrapper { 431 | align-items: center; 432 | display: flex; 433 | position: relative; 434 | } 435 | 436 | .policy-header-warning { 437 | display: none; 438 | height: 1rem; 439 | position: absolute; 440 | right: 0.5rem; 441 | top: 0.25rem; 442 | width: 1rem; 443 | } 444 | 445 | .policy.warning { 446 | .policy-header-input { 447 | padding-inline-end: 2rem; 448 | } 449 | .policy-header-warning { 450 | cursor: help; 451 | display: block; 452 | } 453 | } 454 | 455 | select { 456 | ~ .colour-input-wrapper, 457 | ~ .toggle-switch-wrapper, 458 | ~ .qs-input-wrapper { 459 | display: none; 460 | } 461 | 462 | &.COLOUR ~ .colour-input-wrapper, 463 | &.THEME_COLOUR ~ .toggle-switch-wrapper, 464 | &.QUERY_SELECTOR ~ .qs-input-wrapper { 465 | display: grid; 466 | } 467 | } 468 | -------------------------------------------------------------------------------- /scr/options/options.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import preference from "../preference.js"; 4 | import { localise, msg } from "../utility.js"; 5 | import { 6 | setupCheckbox, 7 | setCheckboxValue, 8 | setupSlider, 9 | setSliderValue, 10 | setupColourInput, 11 | setColourInputValue, 12 | getColourInputValue, 13 | setupPolicyHeaderInput, 14 | setPolicyHeaderInputValue, 15 | setupThemeColourSwitch, 16 | setThemeColourSwitchValue, 17 | getThemeColourSwitchValue, 18 | setupQuerySelectorInput, 19 | setQuerySelectorInputValue, 20 | getQuerySelectorInputValue, 21 | setColourPolicySectionId, 22 | setFlexiblePolicySectionId, 23 | } from "../elements.js"; 24 | 25 | const pref = new preference(); 26 | 27 | const settingsWrapper = document.querySelector("#settings-wrapper"); 28 | const loadingWrapper = document.querySelector("#loading-wrapper"); 29 | const policyList = document.querySelector("#policy-list"); 30 | 31 | const tabSwitches = document.querySelectorAll("input[name='tab-switch']"); 32 | tabSwitches.forEach((tabSwitch) => { 33 | tabSwitch.addEventListener("change", () => (settingsWrapper.className = tabSwitch.id)); 34 | }); 35 | 36 | const checkboxes = document.querySelectorAll("[type='checkbox']"); 37 | checkboxes.forEach((checkbox) => 38 | setupCheckbox(checkbox, async (key, value) => { 39 | pref[key] = value; 40 | await applySettings(); 41 | }) 42 | ); 43 | 44 | const sliders = document.querySelectorAll(".slider"); 45 | sliders.forEach((slider) => 46 | setupSlider(slider, async (key, value) => { 47 | pref[key] = value; 48 | await applySettings(); 49 | }) 50 | ); 51 | 52 | const fixedPolicies = document.querySelectorAll(".section.fixed-policy"); 53 | fixedPolicies.forEach((fixedPolicySection) => { 54 | const colourInputWrapper = fixedPolicySection.querySelector(".colour-input-wrapper"); 55 | const key = colourInputWrapper.dataset.pref; 56 | setupColourInput(colourInputWrapper, pref[key], async (colour) => { 57 | pref[key] = colour; 58 | await applySettings(); 59 | }); 60 | }); 61 | 62 | document.querySelector("#add-new-rule").onclick = async () => { 63 | const policy = { 64 | headerType: "URL", 65 | header: "", 66 | type: "COLOUR", 67 | value: "#000000", 68 | }; 69 | const id = pref.addPolicy(policy); 70 | policyList.appendChild(createPolicySection(id, policy)); 71 | await applySettings(); 72 | }; 73 | 74 | /** 75 | * @param {number} id 76 | * @param {object} policy 77 | * @returns 78 | */ 79 | function createPolicySection(id, policy) { 80 | if (policy.headerType === "URL") { 81 | const templateFlexiblePolicySection = document.querySelector("#template .policy.flexible-policy"); 82 | const policySection = templateFlexiblePolicySection.cloneNode(true); 83 | setupFlexiblePolicySection(policySection, id, policy); 84 | return policySection; 85 | } else if (policy.headerType === "ADDON_ID") { 86 | const templateColourPolicySection = document.querySelector("#template .policy.colour-policy"); 87 | const policySection = templateColourPolicySection.cloneNode(true); 88 | setupColourPolicySection(policySection, id, policy); 89 | return policySection; 90 | } 91 | } 92 | 93 | /** 94 | * @param {HTMLElement} policySection 95 | * @param {number} id 96 | * @param {object} policy 97 | */ 98 | function setupFlexiblePolicySection(policySection, id, policy) { 99 | setFlexiblePolicySectionId(policySection, id); 100 | policySection.classList.toggle("warning", policy.header === ""); 101 | const select = policySection.querySelector("select"); 102 | select.className = select.value = policy.type; 103 | select.addEventListener("change", async () => { 104 | pref.siteList[id].type = select.className = select.value; 105 | switch (select.value) { 106 | case "COLOUR": 107 | pref.siteList[id].value = getColourInputValue(colourInputWrapper); 108 | break; 109 | case "THEME_COLOUR": 110 | pref.siteList[id].value = getThemeColourSwitchValue(themeColourSwitch); 111 | break; 112 | case "QUERY_SELECTOR": 113 | pref.siteList[id].value = getQuerySelectorInputValue(querySelectorInputWrapper); 114 | break; 115 | default: 116 | break; 117 | } 118 | await applySettings(); 119 | }); 120 | const policyHeaderInputWrapper = policySection.querySelector(".policy-header-input-wrapper"); 121 | const colourInputWrapper = policySection.querySelector(".colour-input-wrapper"); 122 | const themeColourSwitch = policySection.querySelector(".theme-colour-switch"); 123 | const querySelectorInputWrapper = policySection.querySelector(".qs-input-wrapper"); 124 | const deleteButton = policySection.querySelector("button"); 125 | let initialColour = "#000000"; 126 | let initialUseThemeColour = true; 127 | let initialQuerySelector = ""; 128 | switch (policy.type) { 129 | case "COLOUR": 130 | initialColour = policy.value; 131 | break; 132 | case "THEME_COLOUR": 133 | initialUseThemeColour = policy.value; 134 | break; 135 | case "QUERY_SELECTOR": 136 | initialQuerySelector = policy.value; 137 | break; 138 | default: 139 | break; 140 | } 141 | setupPolicyHeaderInput(policyHeaderInputWrapper, policy.header, async (newHeader) => { 142 | policySection.classList.toggle("warning", newHeader === ""); 143 | pref.siteList[id].header = newHeader; 144 | await applySettings(); 145 | }); 146 | setupColourInput(colourInputWrapper, initialColour, async (newColour) => { 147 | pref.siteList[id].value = newColour; 148 | await applySettings(); 149 | }); 150 | setupThemeColourSwitch(themeColourSwitch, initialUseThemeColour, async (newUseThemeColour) => { 151 | pref.siteList[id].value = newUseThemeColour; 152 | await applySettings(); 153 | }); 154 | setupQuerySelectorInput(querySelectorInputWrapper, initialQuerySelector, async (newQuerySelector) => { 155 | pref.siteList[id].value = newQuerySelector; 156 | await applySettings(); 157 | }); 158 | deleteButton.onclick = async () => { 159 | pref.removePolicy(policySection.dataset.id); 160 | policySection.remove(); 161 | await applySettings(); 162 | }; 163 | } 164 | 165 | /** 166 | * @param {HTMLElement} policySection 167 | * @param {number} id 168 | * @param {object} policy 169 | */ 170 | async function setupColourPolicySection(policySection, id, policy) { 171 | setColourPolicySectionId(policySection, id); 172 | const policyHeader = policySection.querySelector(".policy-header"); 173 | const colourInputWrapper = policySection.querySelector(".colour-input-wrapper"); 174 | const deleteButton = policySection.querySelector("button"); 175 | setupColourInput(colourInputWrapper, policy.value, async (newColour) => { 176 | pref.siteList[id].value = newColour; 177 | await applySettings(); 178 | }); 179 | deleteButton.onclick = async () => { 180 | pref.removePolicy(policySection.dataset.id); 181 | policySection.remove(); 182 | await applySettings(); 183 | }; 184 | try { 185 | const addon = await browser.management.get(policy.header); 186 | policyHeader.textContent = addon.name; 187 | } catch (error) { 188 | policyHeader.textContent = msg("addonNotFound"); 189 | } 190 | } 191 | 192 | document.querySelector("#export-pref").onclick = () => { 193 | const exportPref = document.querySelector("#export-pref-link"); 194 | const blob = new Blob([pref.prefToJSON()], { type: "text/plain" }); 195 | const url = URL.createObjectURL(blob); 196 | exportPref.href = url; 197 | exportPref.click(); 198 | alert(msg("settingsAreExported")); 199 | }; 200 | 201 | const importPref = document.querySelector("#import-pref-link"); 202 | importPref.addEventListener("change", async () => { 203 | const file = importPref.files[0]; 204 | if (!file) return; 205 | const reader = new FileReader(); 206 | reader.onload = async () => { 207 | const prefJSON = reader.result; 208 | if (await pref.JSONToPref(prefJSON)) { 209 | await applySettings(); 210 | await updateUI(); 211 | alert(msg("settingsAreImported")); 212 | } else { 213 | alert(msg("importFailed")); 214 | } 215 | }; 216 | reader.onerror = () => alert(msg("importFailed")); 217 | reader.readAsText(file); 218 | }); 219 | 220 | document.querySelector("#reset-theme-builder").onclick = async () => { 221 | if (!confirm(msg("confirmResetThemeBuilder"))) return; 222 | [ 223 | "tabbar", 224 | "tabbarBorder", 225 | "tabSelected", 226 | "tabSelectedBorder", 227 | "toolbar", 228 | "toolbarBorder", 229 | "toolbarField", 230 | "toolbarFieldBorder", 231 | "toolbarFieldOnFocus", 232 | "sidebar", 233 | "sidebarBorder", 234 | "popup", 235 | "popupBorder", 236 | ].forEach((key) => pref.reset(key)); 237 | await applySettings(); 238 | await updateUI(); 239 | }; 240 | 241 | document.querySelector("#reset-site-list").onclick = async () => { 242 | if (!confirm(msg("confirmResetSiteList"))) return; 243 | pref.reset("siteList"); 244 | await applySettings(); 245 | await updateUI(); 246 | }; 247 | 248 | document.querySelector("#reset-advanced").onclick = async () => { 249 | if (!confirm(msg("confirmResetAdvanced"))) return; 250 | [ 251 | "allowDarkLight", 252 | "dynamic", 253 | "noThemeColour", 254 | "homeBackground_light", 255 | "homeBackground_dark", 256 | "fallbackColour_light", 257 | "fallbackColour_dark", 258 | "minContrast_light", 259 | "minContrast_dark", 260 | ].forEach((key) => pref.reset(key)); 261 | await applySettings(); 262 | await updateUI(); 263 | }; 264 | 265 | async function updateOptionsPage() { 266 | await pref.load(); 267 | if (pref.valid()) { 268 | if (!document.hasFocus()) await updateUI(); 269 | await updateAllowDarkLightText(); 270 | } else { 271 | browser.runtime.sendMessage({ header: "INIT_REQUEST" }); 272 | } 273 | } 274 | 275 | async function updateUI() { 276 | updateCheckboxes(); 277 | updateSliders(); 278 | updateFixedPolicySection(); 279 | await updateSiteList(); 280 | } 281 | 282 | function updateCheckboxes() { 283 | checkboxes.forEach((checkbox) => setCheckboxValue(checkbox, pref[checkbox.dataset.pref])); 284 | } 285 | 286 | function updateSliders() { 287 | sliders.forEach((slider) => setSliderValue(slider, pref[slider.dataset.pref])); 288 | } 289 | 290 | function updateFixedPolicySection() { 291 | fixedPolicies.forEach((fixedPolicySection) => { 292 | const colourInputWrapper = fixedPolicySection.querySelector(".colour-input-wrapper"); 293 | const key = colourInputWrapper.dataset.pref; 294 | setColourInputValue(colourInputWrapper, pref[key]); 295 | }); 296 | } 297 | 298 | async function updateSiteList() { 299 | for (const id in pref.siteList) { 300 | const policy = pref.siteList[id]; 301 | const policySection = policyList.querySelector(`.policy[data-id='${id}']`); 302 | if (policy && !policySection) { 303 | policyList.appendChild(createPolicySection(id, policy)); 304 | } else if (!policy && policySection) { 305 | policySection.remove(); 306 | } else if (!policy && !policySection) { 307 | continue; 308 | } else if (policy.headerType === "URL" && policySection.classList.contains("flexible-policy")) { 309 | const select = policySection.querySelector("select"); 310 | const policyHeaderInputWrapper = policySection.querySelector(".policy-header-input-wrapper"); 311 | const colourInputWrapper = policySection.querySelector(".colour-input-wrapper"); 312 | const themeColourSwitch = policySection.querySelector(".theme-colour-switch"); 313 | const querySelectorInputWrapper = policySection.querySelector(".qs-input-wrapper"); 314 | select.className = select.value = policy.type; 315 | policySection.classList.toggle("warning", policy.header === ""); 316 | setPolicyHeaderInputValue(policyHeaderInputWrapper, policy.header); 317 | switch (policy.type) { 318 | case "COLOUR": 319 | setColourInputValue(colourInputWrapper, policy.value); 320 | break; 321 | case "THEME_COLOUR": 322 | setThemeColourSwitchValue(themeColourSwitch, policy.value); 323 | break; 324 | case "QUERY_SELECTOR": 325 | setQuerySelectorInputValue(querySelectorInputWrapper, policy.value); 326 | break; 327 | default: 328 | break; 329 | } 330 | } else if (policy.headerType === "ADDON_ID" && policySection.classList.contains("colour-policy")) { 331 | const colourInputWrapper = policySection.querySelector(".colour-input-wrapper"); 332 | setColourInputValue(colourInputWrapper, policy.value); 333 | } else { 334 | policySection.replaceWith(createPolicySection(id, policy)); 335 | } 336 | } 337 | policyList.querySelectorAll(`.policy`).forEach((policySection) => { 338 | if (!(policySection.dataset.id in pref.siteList)) { 339 | policySection.remove(); 340 | } 341 | }); 342 | } 343 | 344 | /** 345 | * @param {number} nthTry 346 | */ 347 | async function updateAllowDarkLightText(nthTry = 0) { 348 | if (nthTry > 10) return; 349 | try { 350 | const allowDarkLightTitle = document.querySelector("#allow-dark-light-title"); 351 | const allowDarkLightCheckboxCaption = document.querySelector("#allow-dark-light-caption"); 352 | const scheme = await browser.runtime.sendMessage({ header: "SCHEME_REQUEST" }); 353 | if (scheme === "light") { 354 | allowDarkLightTitle.textContent = msg("allowDarkTabBar"); 355 | allowDarkLightCheckboxCaption.textContent = msg("allowDarkTabBarTooltip"); 356 | } else { 357 | allowDarkLightTitle.textContent = msg("allowLightTabBar"); 358 | allowDarkLightCheckboxCaption.textContent = msg("allowLightTabBarTooltip"); 359 | } 360 | } catch (error) { 361 | console.error(error); 362 | setTimeout(async () => await updateAllowDarkLightText(++nthTry), 50); 363 | } 364 | } 365 | 366 | /** 367 | * Saves the preference to browser storage and triggers colour update. 368 | * 369 | * Maximum frequency is 4 Hz. 370 | */ 371 | const applySettings = (() => { 372 | let timeout; 373 | let lastCall = 0; 374 | const limitMs = 250; 375 | const action = async () => { 376 | await pref.save(); 377 | await browser.runtime.sendMessage({ header: "PREF_CHANGED" }); 378 | }; 379 | return async () => { 380 | const now = Date.now(); 381 | clearTimeout(timeout); 382 | if (now - lastCall >= limitMs) { 383 | lastCall = now; 384 | await action(); 385 | } else { 386 | timeout = setTimeout(async () => { 387 | lastCall = Date.now(); 388 | await action(); 389 | }, limitMs - (now - lastCall)); 390 | } 391 | }; 392 | })(); 393 | 394 | browser.theme.onUpdated.addListener(updateOptionsPage); 395 | browser.storage.onChanged.addListener(updateOptionsPage); 396 | document.addEventListener("pageshow", updateOptionsPage); 397 | updateOptionsPage(); 398 | 399 | document.addEventListener("DOMContentLoaded", () => localise(document)); 400 | -------------------------------------------------------------------------------- /scr/popup/popup.css: -------------------------------------------------------------------------------- 1 | @import "../shared.css"; 2 | 3 | body { 4 | color: var(--text-colour, var(--text-colour-normal)); 5 | background-color: var(--background-colour, var(--colour-0)); 6 | margin: 0; 7 | padding: 8px; 8 | width: 22em !important; 9 | } 10 | 11 | /* Layout */ 12 | 13 | #info-display-wrapper, 14 | #colour-correction-info { 15 | border-radius: 4px; 16 | display: block; 17 | margin: 0; 18 | padding: 8px; 19 | text-align: left; 20 | } 21 | 22 | #loading { 23 | border-radius: 4px; 24 | display: block; 25 | margin: 0; 26 | padding: 8px; 27 | text-align: center; 28 | } 29 | 30 | #settings-wrapper { 31 | display: flex; 32 | flex-direction: column; 33 | gap: 8px; 34 | 35 | > * { 36 | flex: 1 1 auto; 37 | } 38 | } 39 | 40 | hr { 41 | background-color: var(--transparent-0); 42 | border-width: 0; 43 | color: var(--transparent-0); 44 | height: 1px; 45 | margin-block: 4px; 46 | width: 100%; 47 | } 48 | 49 | .section-group-title { 50 | background-image: linear-gradient(to top, transparent, var(--background-colour) 16px); 51 | font-weight: 600; 52 | padding: 8px 4px 16px; 53 | position: sticky; 54 | text-align: center; 55 | top: 0; 56 | } 57 | 58 | .section-group { 59 | border: 1px solid var(--transparent-0); 60 | border-radius: 4px; 61 | display: flex; 62 | flex-direction: column; 63 | max-height: 25rem; 64 | overflow-x: hidden; 65 | overflow-y: scroll; 66 | 67 | > * { 68 | flex: 1 1 auto; 69 | } 70 | } 71 | 72 | .section { 73 | display: contents; 74 | } 75 | 76 | .section-title { 77 | align-items: center; 78 | display: flex; 79 | gap: 0.5rem; 80 | margin-block: 4px; 81 | text-align: center; 82 | 83 | &::before, 84 | &::after { 85 | border-bottom: 1px solid var(--transparent-0); 86 | content: ""; 87 | flex: 1; 88 | } 89 | } 90 | 91 | #info-display-wrapper, 92 | #colour-correction-info { 93 | border: 1px solid var(--transparent-0); 94 | line-height: 1.5rem; 95 | } 96 | 97 | .info-display { 98 | display: none; 99 | 100 | > div { 101 | display: inline; 102 | } 103 | 104 | button.info-action { 105 | background-color: var(--transparent-0); 106 | display: block; 107 | margin-top: 0.5em; 108 | text-align: center; 109 | width: 100%; 110 | 111 | &:hover { 112 | background-color: var(--transparent-1); 113 | } 114 | &:active { 115 | background-color: var(--transparent-3); 116 | } 117 | } 118 | 119 | .additional-info { 120 | font-weight: 600; 121 | font-family: monospace; 122 | } 123 | } 124 | 125 | .PROTECTED_PAGE > .info-display[name="PROTECTED_PAGE"], 126 | .HOME_PAGE > .info-display[name="HOME_PAGE"], 127 | .TEXT_VIEWER > .info-display[name="TEXT_VIEWER"], 128 | .IMAGE_VIEWER > .info-display[name="IMAGE_VIEWER"], 129 | .PDF_VIEWER > .info-display[name="PDF_VIEWER"], 130 | .JSON_VIEWER > .info-display[name="JSON_VIEWER"], 131 | .ERROR_OCCURRED > .info-display[name="ERROR_OCCURRED"], 132 | .FALLBACK_COLOUR > .info-display[name="FALLBACK_COLOUR"], 133 | .COLOUR_PICKED > .info-display[name="COLOUR_PICKED"], 134 | .ADDON_SPECIFIED > .info-display[name="ADDON_SPECIFIED"], 135 | .ADDON_RECOM > .info-display[name="ADDON_RECOM"], 136 | .ADDON_DEFAULT > .info-display[name="ADDON_DEFAULT"], 137 | .THEME_UNIGNORED > .info-display[name="THEME_UNIGNORED"], 138 | .THEME_MISSING > .info-display[name="THEME_MISSING"], 139 | .THEME_IGNORED > .info-display[name="THEME_IGNORED"], 140 | .THEME_USED > .info-display[name="THEME_USED"], 141 | .QS_USED > .info-display[name="QS_USED"], 142 | .QS_FAILED > .info-display[name="QS_FAILED"], 143 | .QS_ERROR > .info-display[name="QS_ERROR"], 144 | .COLOUR_SPECIFIED > .info-display[name="COLOUR_SPECIFIED"] { 145 | display: contents; 146 | } 147 | 148 | /* Button */ 149 | 150 | button { 151 | align-items: center; 152 | background-color: transparent; 153 | border: none; 154 | border-radius: 4px; 155 | color: inherit; 156 | display: flex; 157 | margin: 0; 158 | padding: 8px; 159 | text-align: left; 160 | 161 | &:hover { 162 | background-color: var(--transparent-0); 163 | } 164 | &:active { 165 | background-color: var(--transparent-2); 166 | } 167 | } 168 | 169 | /* Slider */ 170 | 171 | .slider { 172 | align-items: center; 173 | display: flex; 174 | flex-direction: row; 175 | gap: 0.5rem; 176 | padding: 8px 16px 8px 8px; 177 | 178 | .slider-title { 179 | margin-right: auto; 180 | text-align: center; 181 | } 182 | 183 | .slider-body { 184 | align-items: center; 185 | display: flex; 186 | justify-content: center; 187 | text-align: center; 188 | width: 2.5rem; 189 | 190 | &::after { 191 | content: "%"; 192 | margin-inline-start: 0.2em; 193 | } 194 | } 195 | 196 | button { 197 | align-items: center; 198 | background-color: var(--transparent-0); 199 | border-radius: 50%; 200 | height: 1.25rem; 201 | justify-content: center; 202 | padding: 0; 203 | width: 1.25rem; 204 | 205 | &:hover { 206 | background-color: var(--transparent-1); 207 | } 208 | &:active { 209 | background-color: var(--transparent-3); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /scr/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 31 | 32 | 33 | 36 | 40 | 41 | 42 | 43 |
44 |
45 |
Loading…
46 |

47 | Preference data may be corrupted. If the issue persists, please reinstall the add-on. We apologize for 48 | the inconvenience. 49 |

50 |
51 | 443 | 444 | 445 | 446 | -------------------------------------------------------------------------------- /scr/popup/popup.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import preference from "../preference.js"; 4 | import { recommendedAddonPageColour } from "../default_values.js"; 5 | import { setSliderValue, setupSlider } from "../elements.js"; 6 | import { localise } from "../utility.js"; 7 | 8 | const pref = new preference(); 9 | 10 | const loadingWrapper = document.querySelector("#loading-wrapper"); 11 | const settingsWrapper = document.querySelector("#settings-wrapper"); 12 | const infoDisplay = document.querySelector("#info-display-wrapper"); 13 | const colourCorrectionInfo = document.querySelector("#colour-correction-info"); 14 | 15 | const moreCustomButton = document.querySelector("#custom-popup"); 16 | moreCustomButton.onclick = () => browser.runtime.openOptionsPage(); 17 | 18 | const sliders = document.querySelectorAll(".slider"); 19 | sliders.forEach((slider) => 20 | setupSlider(slider, async (key, value) => { 21 | pref[key] = value; 22 | await applySettings(); 23 | }) 24 | ); 25 | 26 | /** 27 | * Updates infobox's content and popup's text and background colour. 28 | */ 29 | async function updatePopup() { 30 | await pref.load(); 31 | if (pref.valid()) { 32 | updateSliders(); 33 | await updateInfoDisplay(); 34 | await updatePopupColour(); 35 | loadingWrapper.classList.toggle("hidden", true); 36 | settingsWrapper.classList.toggle("hidden", false); 37 | } else { 38 | browser.runtime.sendMessage({ header: "INIT_REQUEST" }); 39 | } 40 | } 41 | 42 | function updateSliders() { 43 | sliders.forEach((slider) => { 44 | setSliderValue(slider, pref[slider.dataset.pref]); 45 | }); 46 | } 47 | 48 | async function updateInfoDisplay(nthTry = 0) { 49 | if (nthTry > 3) { 50 | setInfoDisplay({ reason: "ERROR_OCCURRED" }); 51 | return; 52 | } 53 | try { 54 | const activeTabs = await browser.tabs.query({ active: true, status: "complete", currentWindow: true }); 55 | if (activeTabs.length === 0) { 56 | setInfoDisplay({ reason: "PROTECTED_PAGE" }); 57 | colourCorrectionInfo.classList.toggle("hidden", true); 58 | return; 59 | } 60 | const tab = activeTabs[0]; 61 | const windowId = tab.windowId; 62 | const info = await browser.runtime.sendMessage({ header: "INFO_REQUEST", windowId: windowId }); 63 | if (!info) return setTimeout(() => updateInfoDisplay(++nthTry), 50); 64 | const actions = { 65 | THEME_UNIGNORED: { value: false }, 66 | THEME_USED: { value: false }, 67 | THEME_IGNORED: { value: true }, 68 | }; 69 | colourCorrectionInfo.classList.toggle("hidden", !info.corrected); 70 | if (info.reason === "ADDON") { 71 | const addonInfo = await getAddonPageInfo(info.additionalInfo); 72 | setInfoDisplay(addonInfo); 73 | } else if (info.reason in actions) { 74 | setInfoDisplay({ 75 | reason: info.reason, 76 | additionalInfo: null, 77 | infoAction: async () => { 78 | const header = new URL(tab.url).hostname; 79 | const policyId = pref.getURLPolicyId(tab.url); 80 | const policy = pref.getPolicy(policyId); 81 | if (policyId && policy?.header === header && policy?.type === "THEME_COLOUR") { 82 | pref.setPolicy(policyId, { 83 | headerType: "URL", 84 | header: header, 85 | type: "THEME_COLOUR", 86 | ...actions[info.reason], 87 | }); 88 | } else { 89 | pref.addPolicy({ 90 | headerType: "URL", 91 | header: header, 92 | type: "THEME_COLOUR", 93 | ...actions[info.reason], 94 | }); 95 | } 96 | await applySettings(); 97 | }, 98 | }); 99 | } else { 100 | setInfoDisplay(info); 101 | } 102 | } catch (error) { 103 | setTimeout(() => updateInfoDisplay(++nthTry), 50); 104 | } 105 | } 106 | 107 | /** 108 | * @param {string} addonId 109 | */ 110 | async function getAddonPageInfo(addonId) { 111 | const addonName = (await browser.management.get(addonId)).name; 112 | if (pref.getPolicy(pref.getAddonPolicyId(addonId))) { 113 | return { 114 | reason: "ADDON_SPECIFIED", 115 | additionalInfo: addonName, 116 | infoAction: async () => await specifyColourForAddon(addonId, null), 117 | }; 118 | } else if (addonId in recommendedAddonPageColour) { 119 | return { 120 | reason: "ADDON_RECOM", 121 | additionalInfo: addonName, 122 | infoAction: async () => await specifyColourForAddon(addonId, recommendedAddonPageColour[addonId]), 123 | }; 124 | } else { 125 | return { 126 | reason: "ADDON_DEFAULT", 127 | additionalInfo: addonName, 128 | infoAction: async () => await specifyColourForAddon(addonId, "#333333", true), 129 | }; 130 | } 131 | } 132 | 133 | /** 134 | * @param {string} addonId 135 | * @param {string} colourHex 136 | * @param {boolean} openOptionsPage 137 | */ 138 | async function specifyColourForAddon(addonId, colourHex, openOptionsPage = false) { 139 | if (colourHex) { 140 | pref.addPolicy({ 141 | headerType: "ADDON_ID", 142 | header: addonId, 143 | type: "COLOUR", 144 | value: colourHex, 145 | }); 146 | } else { 147 | pref.removePolicy(pref.getAddonPolicyId(addonId)); 148 | } 149 | await applySettings(); 150 | if (openOptionsPage) browser.runtime.openOptionsPage(); 151 | } 152 | 153 | /** 154 | * Changes the content shown in info display panel. 155 | * 156 | * @param {Object} options Options to configure the info display panel. 157 | * @param {string} options.reason Determines which page to show on the panel by setting the class name of the info display. 158 | * @param {string | null} options.additionalInfo Additional information to display on the panel. 159 | * @param {function | null} options.infoAction The function called by the `.info-action` button being clicked. 160 | */ 161 | function setInfoDisplay({ reason = "ERROR_OCCURRED", additionalInfo = null, infoAction = null }) { 162 | infoDisplay.className = reason; 163 | const additionalInfoDisplay = infoDisplay.querySelector(`[name='${reason}'] .additional-info`); 164 | const infoActionButton = infoDisplay.querySelector(`[name='${reason}'] .info-action`); 165 | if (additionalInfo) additionalInfoDisplay.textContent = additionalInfo; 166 | if (infoAction) infoActionButton.onclick = infoAction; 167 | } 168 | 169 | /** 170 | * Updates popup's text and background colour. 171 | */ 172 | async function updatePopupColour() { 173 | const theme = await browser.theme.getCurrent(); 174 | document.documentElement.style.setProperty("--background-colour", theme.colors.popup); 175 | document.documentElement.style.setProperty("--text-colour", theme.colors.popup_text); 176 | } 177 | 178 | /** 179 | * Triggers colour update. 180 | */ 181 | async function applySettings() { 182 | await pref.save(); 183 | await browser.runtime.sendMessage({ header: "PREF_CHANGED" }); 184 | } 185 | 186 | browser.storage.onChanged.addListener(updatePopup); 187 | browser.theme.onUpdated.addListener(updatePopup); 188 | document.addEventListener("pageshow", updatePopup); 189 | updatePopup(); 190 | 191 | document.addEventListener("DOMContentLoaded", () => localise(document)); 192 | -------------------------------------------------------------------------------- /scr/preference.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import { 4 | addonVersion, 5 | default_homeBackground_light, 6 | default_homeBackground_dark, 7 | default_fallbackColour_light, 8 | default_fallbackColour_dark, 9 | } from "./default_values.js"; 10 | import colour from "./colour.js"; 11 | 12 | export default class preference { 13 | /** The content of the preference */ 14 | #content = { 15 | tabbar: 0, 16 | tabbarBorder: 0, 17 | tabSelected: 10, 18 | tabSelectedBorder: 0, 19 | toolbar: 0, 20 | toolbarBorder: 0, 21 | toolbarField: 5, 22 | toolbarFieldBorder: 5, 23 | toolbarFieldOnFocus: 5, 24 | sidebar: 5, 25 | sidebarBorder: 5, 26 | popup: 5, 27 | popupBorder: 5, 28 | minContrast_light: 90, 29 | minContrast_dark: 45, 30 | allowDarkLight: true, 31 | dynamic: true, 32 | noThemeColour: true, 33 | homeBackground_light: default_homeBackground_light, 34 | homeBackground_dark: default_homeBackground_dark, 35 | fallbackColour_light: default_fallbackColour_light, 36 | fallbackColour_dark: default_fallbackColour_dark, 37 | siteList: {}, 38 | version: addonVersion, 39 | }; 40 | 41 | /** Default content of the preference */ 42 | #default_content = { 43 | tabbar: 0, 44 | tabbarBorder: 0, 45 | tabSelected: 10, 46 | tabSelectedBorder: 0, 47 | toolbar: 0, 48 | toolbarBorder: 0, 49 | toolbarField: 5, 50 | toolbarFieldBorder: 5, 51 | toolbarFieldOnFocus: 5, 52 | sidebar: 5, 53 | sidebarBorder: 5, 54 | popup: 5, 55 | popupBorder: 5, 56 | minContrast_light: 90, 57 | minContrast_dark: 45, 58 | allowDarkLight: true, 59 | dynamic: true, 60 | noThemeColour: true, 61 | homeBackground_light: default_homeBackground_light, 62 | homeBackground_dark: default_homeBackground_dark, 63 | fallbackColour_light: default_fallbackColour_light, 64 | fallbackColour_dark: default_fallbackColour_dark, 65 | siteList: {}, 66 | version: addonVersion, 67 | }; 68 | 69 | /** Current pref keys and their legacy version */ 70 | #legacyKey = { 71 | allowDarkLight: "force", 72 | tabbar: "tabbar_color", 73 | tabSelected: "tab_selected_color", 74 | toolbar: "toolbar_color", 75 | toolbarBorder: "separator_opacity", 76 | toolbarField: "toolbar_field_color", 77 | toolbarFieldOnFocus: "toolbar_field_focus_color", 78 | sidebar: "sidebar_color", 79 | sidebarBorder: "sidebar_border_color", 80 | popup: "popup_color", 81 | popupBorder: "popup_border_color", 82 | homeBackground_light: "light_color", 83 | homeBackground_dark: "dark_color", 84 | fallbackColour_light: "light_fallback_color", 85 | fallbackColour_dark: "dark_fallback_color", 86 | siteList: "reservedColor_cs", 87 | version: "last_version", 88 | }; 89 | 90 | /** 91 | * Loads the preferences from the browser storage to the instance. 92 | */ 93 | async load() { 94 | this.#content = await browser.storage.local.get(); 95 | } 96 | 97 | /** 98 | * Stores the preferences from the instance to the browser storage. 99 | */ 100 | async save() { 101 | await browser.storage.local.clear(); 102 | await browser.storage.local.set(this.#content); 103 | } 104 | 105 | /** 106 | * Validates that each property in the `#prefContent` object has the expected data type. 107 | * 108 | * @returns {boolean} Returns `true` if all properties have the correct data types, otherwise `false`. 109 | */ 110 | valid() { 111 | if (Object.keys(this.#content).length !== Object.keys(this.#default_content).length) return false; 112 | for (const key in this.#default_content) { 113 | if (typeof this.#content[key] !== typeof this.#default_content[key]) return false; 114 | } 115 | return true; 116 | } 117 | 118 | /** 119 | * Resets a single preference if a valid key is specified. 120 | * 121 | * Resets all preferences if the key is not specified or invalid. 122 | * 123 | * @param {string | null} key The key of the preference to reset. 124 | */ 125 | reset(key = null) { 126 | if (key in this.#default_content) { 127 | this.#content[key] = this.#default_content[key]; 128 | } else { 129 | this.#content = {}; 130 | for (const key in this.#default_content) { 131 | this.#content[key] = this.#default_content[key]; 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Normalises the preferences content to a consistent format. 138 | * 139 | * The function adjusts fields as necessary, filling in any missing values to maintain a complete preference structure. 140 | * 141 | * If the existing preferences don't have a version number, date back before v1.7, or has the version number of 2.2.1, the default pref will overwrite the old pref. 142 | * 143 | * Once executed, the preferences in the instance are normalised. 144 | */ 145 | async normalise() { 146 | // If there's no version number, if last version was before v1.7, or if it was v2.2.1, resets the preference 147 | if ( 148 | (!this.#content.last_version && !this.#content.version) || 149 | (this.#content.last_version && this.#content.last_version < [1, 7]) || 150 | (this.#content.version && JSON.stringify(this.#content.version) === "[2,2,1]") 151 | ) { 152 | this.reset(); 153 | await this.save(); 154 | return; 155 | } 156 | // Transfers the stored pref into the instance 157 | const oldContent = Object.assign({}, this.#content); 158 | this.#content = {}; 159 | for (const key in this.#default_content) { 160 | this.#content[key] = oldContent[key] ?? oldContent[this.#legacyKey[key]] ?? this.#default_content[key]; 161 | if (typeof this.#content[key] !== typeof this.#default_content[key]) { 162 | this.reset(key); 163 | } 164 | } 165 | // Updating from before v1.7.5 166 | // Converts from legacy format to query selector format 167 | if (this.#content.version < [1, 7, 5]) { 168 | // Clears possible empty policies 169 | delete this.#content.siteList[undefined]; 170 | for (const site in this.#content.siteList) { 171 | const legacyPolicy = this.#content.siteList[site]; 172 | if (typeof legacyPolicy !== "string") { 173 | continue; 174 | } else if (legacyPolicy.startsWith("TAG_")) { 175 | this.#content.siteList[site] = legacyPolicy.replace("TAG_", "QS_"); 176 | } else if (legacyPolicy.startsWith("CLASS_")) { 177 | this.#content.siteList[site] = legacyPolicy.replace("CLASS_", "QS_."); 178 | } else if (legacyPolicy.startsWith("ID_")) { 179 | this.#content.siteList[site] = legacyPolicy.replace("ID_", "QS_#"); 180 | } else if (legacyPolicy.startsWith("NAME_")) { 181 | this.#content.siteList[site] = `${legacyPolicy.replace("NAME_", "QS_[name='")}']`; 182 | } else if (legacyPolicy === "") { 183 | delete this.#content.siteList[site]; 184 | } 185 | } 186 | } 187 | // Updating from before v2.2 188 | if (this.#content.version < [2, 2]) { 189 | // Turns on allow dark / light tab bar, dynamic, and no theme colour settings for once 190 | this.#content.allowDarkLight = true; 191 | this.#content.dynamic = true; 192 | this.#content.noThemeColour = true; 193 | // Re-formatting site list 194 | const newSiteList = {}; 195 | let id = 1; 196 | for (const site in this.#content.siteList) { 197 | const legacyPolicy = this.#content.siteList[site]; 198 | if (typeof legacyPolicy !== "string") { 199 | continue; 200 | } else if (legacyPolicy === "IGNORE_THEME") { 201 | newSiteList[id++] = { 202 | headerType: "URL", 203 | header: site, 204 | type: "THEME_COLOUR", 205 | value: false, 206 | }; 207 | } else if (legacyPolicy === "UN_IGNORE_THEME") { 208 | newSiteList[id++] = { 209 | headerType: "URL", 210 | header: site, 211 | type: "THEME_COLOUR", 212 | value: true, 213 | }; 214 | } else if (legacyPolicy.startsWith("QS_")) { 215 | newSiteList[id++] = { 216 | headerType: "URL", 217 | header: site, 218 | type: "QUERY_SELECTOR", 219 | value: legacyPolicy.replace("QS_", ""), 220 | }; 221 | } else if (site.startsWith("Add-on ID: ")) { 222 | newSiteList[id++] = { 223 | headerType: "ADDON_ID", 224 | header: site.replace("Add-on ID: ", ""), 225 | type: "COLOUR", 226 | value: new colour().parse(legacyPolicy).toHex(), 227 | }; 228 | } else { 229 | newSiteList[id++] = { 230 | headerType: "URL", 231 | header: site, 232 | type: "COLOUR", 233 | value: new colour().parse(legacyPolicy).toHex(), 234 | }; 235 | } 236 | } 237 | this.#content.siteList = newSiteList; 238 | } 239 | // Updating from before v2.4 240 | if (this.#content.version < [2, 4]) { 241 | browser.theme.reset(); 242 | if (this.#content.minContrast_light === 165) this.#content.minContrast_light = 90; 243 | } 244 | [ 245 | "tabbar", 246 | "tabbarBorder", 247 | "tabSelected", 248 | "tabSelectedBorder", 249 | "toolbar", 250 | "toolbarBorder", 251 | "toolbarField", 252 | "toolbarFieldBorder", 253 | "toolbarFieldOnFocus", 254 | "sidebar", 255 | "sidebarBorder", 256 | "popup", 257 | "popupBorder", 258 | ].forEach((key) => { 259 | this.#content[key] = this.#validateNumericPref(this.#content[key], { min: -50, max: 50, step: 5 }); 260 | }); 261 | ["minContrast_light", "minContrast_dark"].forEach((key) => { 262 | this.#content[key] = this.#validateNumericPref(this.#content[key], { min: 0, max: 210, step: 15 }); 263 | }); 264 | // Updates the pref version 265 | this.#content.version = addonVersion; 266 | } 267 | 268 | /** 269 | * Converts the pref to a JSON string. 270 | * 271 | * @returns The JSON string of the pref. 272 | */ 273 | prefToJSON() { 274 | return JSON.stringify(this.#content); 275 | } 276 | 277 | /** 278 | * Loads pref from a JSON string and normalises it. Returns `false` if the JSON string is invalid. 279 | * 280 | * @param {string} JSONString The JSON string to load pref from. 281 | * @returns `true` if the JSON string is converted to the pref, otherwise `false`. 282 | */ 283 | async JSONToPref(JSONString) { 284 | try { 285 | const parsedJSON = JSON.parse(JSONString); 286 | if (typeof parsedJSON !== "object" || parsedJSON === null) return false; 287 | this.#content = parsedJSON; 288 | await this.normalise(); 289 | return true; 290 | } catch (error) { 291 | return false; 292 | } 293 | } 294 | 295 | /** 296 | * Returns the policy for a policy ID from the site list. 297 | * 298 | * Newly added policies have higher priority. 299 | * 300 | * Returns `undefined` if nothing matches. 301 | * 302 | * @param {number} id - Policy ID. 303 | */ 304 | getPolicy(id) { 305 | return this.#content.siteList[id]; 306 | } 307 | 308 | /** 309 | * Adds a policy to the site list. 310 | * 311 | * @param {object} policy - The policy to add. 312 | * @returns The ID of the policy. 313 | */ 314 | addPolicy(policy) { 315 | let id = 1; 316 | while (id in this.#content.siteList) id++; 317 | this.#content.siteList[id] = policy; 318 | return id; 319 | } 320 | 321 | /** 322 | * Sets a certain policy to a given ID. 323 | * 324 | * @param {number} id - The ID of the policy. 325 | * @param {object} policy - The new policy. 326 | */ 327 | setPolicy(id, policy) { 328 | this.#content.siteList[id] = policy; 329 | } 330 | 331 | /** 332 | * Removes a policy from the site list by setting the policy to `null`. 333 | * 334 | * @param {number} id - The ID of a policy. 335 | */ 336 | removePolicy(id) { 337 | this.#content.siteList[id] = null; 338 | } 339 | 340 | /** 341 | * Finds the ID of the most recently created policy from the site list that matches the given URL. 342 | * 343 | * Policy header supports: 344 | * 345 | * - Full URL with or w/o trailing slash 346 | * - Regex 347 | * - Wildcard 348 | * - `**` matches strings of any length 349 | * - `*` matches any characters except `/`, `.`, and `:` 350 | * - `?` matches any single character 351 | * - Scheme (e.g. `https://`) is optional 352 | * - hostname 353 | * 354 | * @param {string} url - The site URL to match against the policy headers. 355 | * @returns {number} The ID of the most specific matching policy, or 0 if no match is found. 356 | */ 357 | getURLPolicyId(url) { 358 | let result = 0; 359 | for (const id in this.#content.siteList) { 360 | const policy = this.#content.siteList[id]; 361 | if (!policy || policy.header === "" || policy.headerType !== "URL") continue; 362 | if (id > result && (policy.header === url || policy.header === `${url}/`)) { 363 | result = +id; 364 | continue; 365 | } 366 | try { 367 | if (id > result && new RegExp(`^${policy.header}$`, "i").test(url)) { 368 | result = +id; 369 | continue; 370 | } 371 | } catch (error) {} 372 | if (policy.header.includes("*") || policy.header.includes("?")) { 373 | try { 374 | const wildcardPattern = policy.header 375 | .replace(/[.+^${}()|[\]\\]/g, "\\$&") 376 | .replace(/\*\*/g, "::WILDCARD_MATCH_ALL::") 377 | .replace(/\*/g, "[^/.:]*") 378 | .replace(/\?/g, ".") 379 | .replace(/::WILDCARD_MATCH_ALL::/g, ".*") 380 | .replace(/^([a-z]+:\/\/)/i, "$1") 381 | .replace(/^((?![a-z]+:\/\/).)/i, "(?:[a-z]+:\\/\\/)?$1"); 382 | if (id > result && new RegExp(`^${wildcardPattern}/?$`, "i").test(url)) { 383 | result = +id; 384 | continue; 385 | } 386 | } catch (error) {} 387 | } 388 | try { 389 | if (id > result && policy.header === new URL(url).hostname) { 390 | result = +id; 391 | continue; 392 | } 393 | } catch (error) {} 394 | } 395 | return result; 396 | } 397 | 398 | /** 399 | * Retrieves the policy ID that matches the given add-on ID. 400 | * 401 | * If multiple policies for the same add-on ID are present, return the ID of the most recently created one. 402 | * 403 | * @param {string} addonId - The add-on ID to match against the policy list. 404 | * @returns {number} The ID of the matching policy, or 0 if no match is found. 405 | */ 406 | getAddonPolicyId(addonId) { 407 | let result = 0; 408 | for (const id in this.#content.siteList) { 409 | const policy = this.#content.siteList[id]; 410 | if (!policy || policy?.headerType !== "ADDON_ID") continue; 411 | if (id > result && policy.header === addonId) { 412 | result = +id; 413 | continue; 414 | } 415 | } 416 | return result; 417 | } 418 | 419 | /** 420 | * Validates and adjusts a numeric preference based on given constraints. 421 | * 422 | * @param {number} num The number to validate. 423 | * @param {object} options The constraints for validation. 424 | * @param {number} options.min The minimum allowed value. 425 | * @param {number} options.max The maximum allowed value. 426 | * @param {number} options.step The step size for rounding. 427 | * @returns {number} The validated and adjusted number. 428 | */ 429 | #validateNumericPref(num, { min, max, step }) { 430 | if (-1 < num && num < 1) num = Math.round(num * 100); 431 | num = Math.max(min, Math.min(max, num)); 432 | const remainder = (num - min) % step; 433 | if (remainder !== 0) num = remainder >= step / 2 ? num + (step - remainder) : num - remainder; 434 | return Math.round(num); 435 | } 436 | 437 | get allowDarkLight() { 438 | return this.#content.allowDarkLight; 439 | } 440 | 441 | set allowDarkLight(value) { 442 | this.#content.allowDarkLight = value; 443 | } 444 | 445 | get dynamic() { 446 | return this.#content.dynamic; 447 | } 448 | 449 | set dynamic(value) { 450 | this.#content.dynamic = value; 451 | } 452 | 453 | get noThemeColour() { 454 | return this.#content.noThemeColour; 455 | } 456 | 457 | set noThemeColour(value) { 458 | this.#content.noThemeColour = value; 459 | } 460 | 461 | get tabbar() { 462 | return this.#content.tabbar; 463 | } 464 | 465 | set tabbar(value) { 466 | this.#content.tabbar = value; 467 | } 468 | 469 | get tabbarBorder() { 470 | return this.#content.tabbarBorder; 471 | } 472 | 473 | set tabbarBorder(value) { 474 | this.#content.tabbarBorder = value; 475 | } 476 | 477 | get tabSelected() { 478 | return this.#content.tabSelected; 479 | } 480 | 481 | set tabSelected(value) { 482 | this.#content.tabSelected = value; 483 | } 484 | 485 | get tabSelectedBorder() { 486 | return this.#content.tabSelectedBorder; 487 | } 488 | 489 | set tabSelectedBorder(value) { 490 | this.#content.tabSelectedBorder = value; 491 | } 492 | 493 | get toolbar() { 494 | return this.#content.toolbar; 495 | } 496 | 497 | set toolbar(value) { 498 | this.#content.toolbar = value; 499 | } 500 | 501 | get toolbarBorder() { 502 | return this.#content.toolbarBorder; 503 | } 504 | 505 | set toolbarBorder(value) { 506 | this.#content.toolbarBorder = value; 507 | } 508 | 509 | get toolbarField() { 510 | return this.#content.toolbarField; 511 | } 512 | 513 | set toolbarField(value) { 514 | this.#content.toolbarField = value; 515 | } 516 | 517 | get toolbarFieldBorder() { 518 | return this.#content.toolbarFieldBorder; 519 | } 520 | 521 | set toolbarFieldBorder(value) { 522 | this.#content.toolbarFieldBorder = value; 523 | } 524 | 525 | get toolbarFieldOnFocus() { 526 | return this.#content.toolbarFieldOnFocus; 527 | } 528 | 529 | set toolbarFieldOnFocus(value) { 530 | this.#content.toolbarFieldOnFocus = value; 531 | } 532 | 533 | get sidebar() { 534 | return this.#content.sidebar; 535 | } 536 | 537 | set sidebar(value) { 538 | this.#content.sidebar = value; 539 | } 540 | 541 | get sidebarBorder() { 542 | return this.#content.sidebarBorder; 543 | } 544 | 545 | set sidebarBorder(value) { 546 | this.#content.sidebarBorder = value; 547 | } 548 | 549 | get popup() { 550 | return this.#content.popup; 551 | } 552 | 553 | set popup(value) { 554 | this.#content.popup = value; 555 | } 556 | 557 | get popupBorder() { 558 | return this.#content.popupBorder; 559 | } 560 | 561 | set popupBorder(value) { 562 | this.#content.popupBorder = value; 563 | } 564 | 565 | get minContrast_light() { 566 | return this.#content.minContrast_light; 567 | } 568 | 569 | set minContrast_light(value) { 570 | this.#content.minContrast_light = value; 571 | } 572 | 573 | get minContrast_dark() { 574 | return this.#content.minContrast_dark; 575 | } 576 | 577 | set minContrast_dark(value) { 578 | this.#content.minContrast_dark = value; 579 | } 580 | 581 | get homeBackground_light() { 582 | return this.#content.homeBackground_light; 583 | } 584 | 585 | set homeBackground_light(value) { 586 | this.#content.homeBackground_light = value; 587 | } 588 | 589 | get homeBackground_dark() { 590 | return this.#content.homeBackground_dark; 591 | } 592 | 593 | set homeBackground_dark(value) { 594 | this.#content.homeBackground_dark = value; 595 | } 596 | 597 | get fallbackColour_light() { 598 | return this.#content.fallbackColour_light; 599 | } 600 | 601 | set fallbackColour_light(value) { 602 | this.#content.fallbackColour_light = value; 603 | } 604 | 605 | get fallbackColour_dark() { 606 | return this.#content.fallbackColour_dark; 607 | } 608 | 609 | set fallbackColour_dark(value) { 610 | this.#content.fallbackColour_dark = value; 611 | } 612 | 613 | get siteList() { 614 | return this.#content.siteList; 615 | } 616 | 617 | set siteList(value) { 618 | this.#content.siteList = value; 619 | } 620 | 621 | get version() { 622 | return this.#content.version; 623 | } 624 | 625 | set version(value) { 626 | this.#content.version = value; 627 | } 628 | } 629 | -------------------------------------------------------------------------------- /scr/shared.css: -------------------------------------------------------------------------------- 1 | html { 2 | --accent-colour: #0060df; 3 | --colour-0: hsl(0, 0%, 100%); 4 | --colour-1: hsl(240, 10%, 95%); 5 | --colour-1-5: hsl(240, 13%, 97.5%); 6 | --colour-2: hsl(240, 10%, 90%); 7 | --colour-3: hsl(240, 10%, 85%); 8 | --colour-4: hsl(240, 10%, 80%); 9 | --colour-5: hsl(240, 10%, 75%); 10 | --link-colour-active: #054096; 11 | --link-colour-hover: #0250bb; 12 | --link-colour-normal: #0060df; 13 | --text-colour-normal: hsl(0, 0%, 0%); 14 | --text-colour-secondary: hsl(0, 0%, 20%); 15 | --transparent-0: hsla(0, 0%, 0%, 10%); 16 | --transparent-1: hsla(0, 0%, 0%, 15%); 17 | --transparent-2: hsla(0, 0%, 0%, 20%); 18 | --transparent-3: hsla(0, 0%, 0%, 25%); 19 | --transparent-4: hsla(0, 0%, 0%, 30%); 20 | --transparent-5: hsla(0, 0%, 0%, 35%); 21 | font-family: sans-serif; 22 | } 23 | 24 | @media (prefers-color-scheme: dark) { 25 | html { 26 | --accent-colour: #00ddff; 27 | --colour-0: hsl(248, 11%, 15%); 28 | --colour-1: hsl(248, 11%, 20%); 29 | --colour-1-5: hsl(248, 11%, 17.5%); 30 | --colour-2: hsl(248, 11%, 25%); 31 | --colour-3: hsl(248, 11%, 30%); 32 | --colour-4: hsl(248, 11%, 35%); 33 | --colour-5: hsl(248, 11%, 40%); 34 | --link-colour-active: #aaf2ff; 35 | --link-colour-hover: #80ebff; 36 | --link-colour-normal: #00ddff; 37 | --text-colour-normal: hsl(0, 0%, 100%); 38 | --text-colour-secondary: hsl(0, 0%, 80%); 39 | --transparent-0: hsla(0, 0%, 100%, 10%); 40 | --transparent-1: hsla(0, 0%, 100%, 15%); 41 | --transparent-2: hsla(0, 0%, 100%, 20%); 42 | --transparent-3: hsla(0, 0%, 100%, 25%); 43 | --transparent-4: hsla(0, 0%, 100%, 30%); 44 | --transparent-5: hsla(0, 0%, 100%, 0.35); 45 | } 46 | } 47 | 48 | #reinstall-tip { 49 | animation-delay: 5s; 50 | animation-duration: 0s; 51 | animation-fill-mode: forwards; 52 | animation-name: appear; 53 | opacity: 0; 54 | overflow-wrap: break-word; 55 | white-space: normal; 56 | } 57 | 58 | @keyframes appear { 59 | from { 60 | opacity: 0; 61 | } 62 | to { 63 | opacity: 1; 64 | } 65 | } 66 | 67 | #template, 68 | .hidden { 69 | display: none !important; 70 | } 71 | 72 | svg { 73 | align-self: center; 74 | fill: none; 75 | height: 1em; 76 | margin: 0; 77 | stroke: currentColor; 78 | stroke-linecap: round; 79 | stroke-linejoin: round; 80 | stroke-width: 2px; 81 | vertical-align: middle; 82 | width: 1em; 83 | } 84 | -------------------------------------------------------------------------------- /scr/utility.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const darkSchemeDetection = window.matchMedia("(prefers-color-scheme: dark)"); 4 | 5 | /** 6 | * Registers a listener function that triggers when the colour scheme changes. 7 | * 8 | * @param {Function} listener The function to be called when a scheme change event occurs. 9 | */ 10 | export function onSchemeChanged(listener) { 11 | darkSchemeDetection?.addEventListener("change", listener); 12 | } 13 | 14 | /** 15 | * Detects the current system colour scheme. 16 | * 17 | * @returns {"dark" | "light"} The current system colour scheme, either "dark" or "light". 18 | */ 19 | export function getSystemScheme() { 20 | return darkSchemeDetection?.matches ? "dark" : "light"; 21 | } 22 | 23 | /** 24 | * Retrieves the preferred colour scheme. 25 | * 26 | * Retrieves the user's "web appearance" browser settings. If the setting is explicitly `light` or `dark`, returns it. Otherwise, falls back to the operating system's current colour scheme based on media query detection. 27 | * 28 | * This function should be called in background script to return the correct result. 29 | * 30 | * @returns {Promise<"light" | "dark">} The current colour scheme, either `light` or `dark`. 31 | */ 32 | export async function getCurrentScheme() { 33 | const webAppearanceSetting = await browser.browserSettings.overrideContentColorScheme.get({}); 34 | const webAppearance = webAppearanceSetting.value; 35 | return webAppearance === "light" || webAppearance === "dark" ? webAppearance : getSystemScheme(); 36 | } 37 | 38 | /** 39 | * Updates the text content and title of elements based on localisation data attributes. 40 | * 41 | * Finds elements with `data-text`, `data-title`, or `data-placeholder` attributes, retrieves the localised text using the `msg` function, and assigns it to the element's `textContent`, `title`, or `placeholder`. 42 | * 43 | * @param {Document} webDocument The document to localise. 44 | */ 45 | export function localise(webDocument) { 46 | webDocument.querySelectorAll("[data-text]").forEach((element) => { 47 | element.textContent = msg(element.dataset.text); 48 | }); 49 | webDocument.querySelectorAll("[data-title]").forEach((element) => { 50 | element.title = msg(element.dataset.title); 51 | }); 52 | webDocument.querySelectorAll("[data-placeholder]").forEach((element) => { 53 | element.placeholder = msg(element.dataset.placeholder); 54 | }); 55 | } 56 | 57 | /** 58 | * Inquires localised messages. 59 | * 60 | * @param {string} handle A handle in _locales. 61 | * If the handle is not found, returns `i18n <${handle}>`. 62 | * If the localisation value is `__EMPTY__`, returns an empty string. 63 | */ 64 | export function msg(handle) { 65 | const localisedMessage = browser.i18n.getMessage(handle); 66 | if (!localisedMessage) { 67 | return `i18n <${handle}>`; 68 | } else if (localisedMessage === "__EMPTY__") { 69 | return ""; 70 | } else { 71 | return localisedMessage; 72 | } 73 | } 74 | --------------------------------------------------------------------------------