├── .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 | 
2 | 
3 | 
4 | 
5 | 
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 | - Dark Reader
29 | - Stylish
30 | - Dark Mode Website Switcher
31 | - automaticDark
32 |
33 |
34 |
35 |
36 | ## Incompatible With:
37 |
38 |
39 | - Firefox versions older than 112.0 (released in April 2023)
40 | - Adaptive Theme Creator
41 | - Chameleon Dynamic Theme
42 | - VivaldiFox
43 | - Envify
44 | - and any other add-on that changes the Firefox theme
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 |
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 |
52 |
53 |
56 |
59 |
62 |
65 |
68 |
71 |
74 |
77 |
80 |
83 |
93 |
94 |
95 |
96 |
97 |
102 |
103 |
104 |
105 |
106 |
107 |
112 |
113 |
121 |
124 |
132 |
140 |
145 |
150 |
155 |
158 |
159 |
160 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
177 |
178 |
183 |
184 |
189 |
190 |
198 |
199 |
204 |
205 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
221 |
222 |
227 |
228 |
236 |
237 |
238 |
242 |
245 |
246 |
247 |
252 |
253 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
269 |
270 |
275 |
276 |
284 |
285 |
290 |
291 |
296 |
297 |
298 |
299 |
300 |
308 |
309 |
314 |
315 |
320 |
321 |
329 |
330 |
335 |
336 |
341 |
342 |
350 |
351 |
356 |
357 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
373 |
374 |
379 |
380 |
388 |
389 |
394 |
395 |
400 |
401 |
402 |
403 |
404 |
405 |
406 |
411 |
412 |
417 |
418 |
426 |
427 |
432 |
433 |
438 |
439 |
440 |
441 |
442 |
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 |
--------------------------------------------------------------------------------