├── README.md └── TangerineUI.user.js /README.md: -------------------------------------------------------------------------------- 1 | # TangerineUI-Userscript 2 | An userscript to load [TangerineUI Redesign for Mastodon's Web UI](https://github.com/nileane/TangerineUI-for-Mastodon) by [@nileane](https://github.com/nileane) 🍊🐘 on whichever instance you'd like ! 3 | 4 | # Usage and Installation 5 | 6 | ## Installing an userscript manager 7 | ### Chrome / Firefox 8 | - Install an userscript manager, either › [ViolentMonkey](https://violentmonkey.github.io/) or [Tampermonkey](https://www.tampermonkey.net/) 9 | ### Safari (iOS / macOS) 10 | - Install an userscript manager › [Userscripts](https://apps.apple.com/tt/app/userscripts/id1463298887) — ([Github](https://github.com/quoid/userscripts)) 11 | 12 | ## Installing the userscript 13 | - Once the userscript manager is installed, click on this [link](https://github.com/Write/TangerineUI-Userscript/raw/main/TangerineUI.user.js), your userscript manager should offer you to install the script. On Safari, the Userscript extension doesn't offer this automatically, you would need to then click extension icon to be offered to install the userscript. 14 | - After the userscript is installed, edit the userscript to add a @match (at the top of the file) rule for each mastodon instances you want the theme to be enabled on. Respect the same format that is given for pre-defined instances. 15 | - You can change easily switch between the normal, purple or cherry variants by changing colorScheme (instances below 4.3) or newColorScheme (instances equals or above 4.3). Cherry is only available for >= 4.3. 16 | - You can set the tag you want to use for instances strictly below 4.3.0 and for instances >= 4.3.0 by changing tag_below_4_3_0 and tag_above_or_equals_4_3_0 variables. To find tag, look at the left sidebar in the releases of TangerineUI : https://github.com/nileane/TangerineUI-for-Mastodon/releases and change it accordingly. 17 | 18 | # Changelog 19 | 20 | ### Release 2.2.4 21 | 22 | + Per Host Theme support 23 | + Handles 2.4 / 2.3 / 1.9.5 versions of TangerineUI correctly to match instance current version. 24 | 25 | ### Release 2.2.3 26 | 27 | + set default TangerineUI tag to latest release : v2.3 28 | 29 | ### Release 2.2.1 30 | 31 | + set default TangerineUI tag to latest release : v2.1 32 | + fix a race condition that injected the theme before mastodon version could be detected 33 | + "temporarily" remove the workaround that was made to avoid 'flash issues' while loading. 34 | 35 | ### Release 2.2.0 36 | 37 | + Bump default tag for v2.0 of TangerineUI. 38 | + Add support for lagoon color scheme. 39 | 40 | ### Release 2.1.0 41 | 42 | + Add support for the new Cherry color scheme, only supported for versions above 4.3.0 43 | + Since older (<= 4.3.0) instances can't have the Cherry color scheme, I had to create a new variable, hence why there's now legacyColorScheme and newColorScheme var, so you can choose which color scheme to use for the "legacy" and "new" mastodon version. 44 | 45 | ### Release 2.0.0 46 | 47 | ![Untitled](https://github.com/Write/TangerineUI-Userscript/assets/541722/e80605da-c301-4381-ac5b-65ddeea2698f) 48 | 49 | + ✨ Support Mastodon >= 4.3.0 ✨ 50 | + Dynamically loads the right CSS based on your instance version 51 | + Loaded CSS is now minified thanks to jsdelivr 52 | 53 | ### Release 1.3.0 54 | + ✨ Full support for Mastodon >= 4.1.6 ✨ 55 | + Mastodon's enhanced their CSP restriction which made the userscript not working on instance above or equals >= 4.1.6 56 | + I was kinda scared I couldn't find a workaround that, but thankfully mastodon expose a meta header of the name "style-nonce", that you can fetch and use to inject the styling url with. This, of course, add a penalty delay before theme's injection. I'd hapilly takes PR if you find a way around that. 57 | 58 | ### Release 1.2.5 59 | + Disable the script on page not stylized by TangerineUI anyway 60 | 61 | ### Release 1.2.4 62 | + Forcefully disconnect MutationObserver after 3 seconds if MutationObserver wasn't disconnect already 63 | _This fix a performance issue where MutationObserver would be running indefinitely if it never detected the element_ 64 | + Apply background && background-color to \ tag yet again 65 | + Add a configurable timeout after how much time the css applied \ and \ should be reverted, default to 1 second 66 | 67 | ### Release 1.2.3 68 | + Now apply background color pre-emptively on either light or dark mode 69 | _This fix a flashing issue on load, if the instance main's theme was set as dark mode, but the system was in light mode_ 70 | + The pre-emptively color set to \ is now dynamic, based selected theme (for light mode only, as dark mode as same color for both variants) 71 | + Correctly set background color in dark mode to TangerineUI Color instead of pure black 72 | 73 | ### Release 1.2.0 74 | - "Production ready" code 75 | - \ tag now injected instantly, as it doesn't require to wait for it. 76 | - Moved from requestAnimationFrame to MutationObserver which should inject theme even faster. 77 | - Hugely improved code's readability 78 | - Compatible with both Firefox and Chrome 79 | - Support Safari, even though injection is slow, limited to requestAnimationFrame API as MutationObserver doesn't work most of the time (except when cache is cleared with CMD + SHIFT + R), certainly a limitation from how Safari handle extensions. Safari extension is : https://apps.apple.com/us/app/userscripts/id1463298887 -- Mac & iOS support. It support UserStyles too, so you could just create one instead. 80 | -------------------------------------------------------------------------------- /TangerineUI.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name TangerineUI 🍊 for Mastodon 3 | // @namespace TangerineUI for Mastodon by @nileane 4 | // @description TangerineUI for Mastodon by @nileane 5 | // 6 | // @match https://AddYourInstanceUrlHere.tld/* 7 | // @match https://mamot.fr/* 8 | // @match https://mastodon.social/* 9 | // @match https://piaille.fr/* 10 | // @match https://eldritch.cafe/* 11 | // @match https://h-i.social/* 12 | // @match https://diaspodon.fr/* 13 | // 14 | // @downloadURL https://github.com/Write/TangerineUI-Userscript/raw/main/TangerineUI.user.js 15 | // @updateURL https://github.com/Write/TangerineUI-Userscript/raw/main/TangerineUI.user.js 16 | // @homepageURL https://github.com/Write/TangerineUI-Userscript 17 | // @grant none 18 | // @version 2.2.5.2 19 | // @author nileane (TangerineUI) & Write (Userscript) 20 | // @run-at document-start 21 | // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACE0lEQVR4AZXOA2xeUQAF4BvNW8xGi7WFY227v81otu0Fs23btq3attunvrN7//bOPMmX98414bFa4edyda12u7te0m8d1UKhRyNVT32idrtcGE6+jdMpBzjtUjOFf2KTZJddchKWsWPR22HprKLATBwnYOM6CfNni+Bjs2d0j02ZKLDOCVZrhx9xGNuT7aY2MAvmdKCjQwXP0UMidm4ToPYMiSKwcmkn+Hqbsd1L7LqWqTZ9M5injyXf4oN7O3HkQCdkGRAEFWdOCNi9vROKAnx8L4Ovt+uaZhCbpn6eTVsPpjBfRkN9F3ivLFfQ0qJ+6SXFCupqv86zvcSWVjPTml4DprhQRm21At7ZgbU1X3tBnoxGegHvlvSa2cSaUjXWklIJhh/Ae2Ge5DuA94JcyfdC3q2plVOJObncYU4qA1NcIPkO4H2soxLjXZW80wNEesDXeVNSqYcY44s1pvhiMDcvt+LB7Tbw/qPrF1rx+F77l25IKh1KjNEFw40xBfhfhpiCs4RFE/JxgDEqR6Xwj5qptZaot/0JjyE867UhPBOcMeJThCH8U6QhLNNgjMj06iMyjfQ/0RSROSQ2Nrc3+TG60I/J+tAPKgWfsI/7NaMe9SXfxBSbO4j8Kdrgdx5d0FuBQrd3zbrgN28Y2quofPK3aILeDtb6v1yt8X/5Suv/qo0Sdf6vijT+r05q/F9Hkl/kM/y+SpUd6mHfAAAAAElFTkSuQmCC 22 | // ==/UserScript== 23 | (function () { 24 | "use strict"; 25 | /* ---------------- 26 | * SETTINGS 27 | * ---------------- */ 28 | /* Default themes for hosts not configured in perHostThemes */ 29 | /* These will be used as fallback for any Mastodon instance not explicitly configured below */ 30 | /* Available themes: 'tangerine', 'purple', 'cherry', 'lagoon' */ 31 | /* Note: Only tangerine & purple is available for TangerineUI < v2.0 (legacy) */ 32 | const defaultThemes = { 33 | legacy: "tangerine", // Default theme to use when Mastodon is below 4.3.0 (legacy) 34 | modern: "tangerine", 35 | }; 36 | 37 | /* Per-host theme configuration */ 38 | /* Set specific themes for different Mastodon instances */ 39 | /* Available themes: 'tangerine', 'purple', 'cherry', 'lagoon' */ 40 | /* Note: Only tangerine & purple is available for TangerineUI < v2.0 (legacy) */ 41 | const perHostThemes = { 42 | "mamot.fr": { 43 | legacy: "tangerine", 44 | modern: "cherry", 45 | }, 46 | "mastodon.social": { 47 | legacy: "purple", 48 | modern: "purple", 49 | }, 50 | "piaille.fr": { 51 | legacy: "tangerine", 52 | modern: "lagoon", 53 | }, 54 | "eldritch.cafe": { 55 | legacy: "purple", 56 | modern: "cherry", 57 | }, 58 | "h-i.social": { 59 | legacy: "tangerine", 60 | modern: "tangerine", 61 | }, 62 | "diaspodon.fr": { 63 | legacy: "tangerine", 64 | modern: "purple", 65 | }, 66 | // Add more hosts as needed 67 | }; 68 | 69 | /* Github tags to use for different mastodon version ranges 70 | * To find tags name, go here : https://github.com/nileane/TangerineUI-for-Mastodon/releases 71 | * and look at the tag name in the left sidebar of the release 72 | */ 73 | const TAG_LEGACY = "v1.9.5"; // For Mastodon < 4.3.0 74 | const TAG_V4_3 = "v2.3"; // For Mastodon 4.3.x 75 | const TAG_V4_4 = "v2.4.4"; // For Mastodon 4.4.x 76 | const TAG_V4_5_PLUS = "v2.5.2"; // For Mastodon >= 4.5.0 77 | 78 | /* ---------------- 79 | * HELPERS 80 | * ---------------- 81 | */ 82 | const currentPagepath = window.location.pathname; 83 | const currentHost = window.location.hostname; 84 | const placeholder = "TangerineUI 🍊"; 85 | const log = (str) => console.log("* " + placeholder + " : " + str + " *"); 86 | const theme = window.matchMedia?.("(prefers-color-scheme: dark)").matches 87 | ? "dark" 88 | : "light"; 89 | const timeout = 1000; 90 | 91 | /* Get theme configuration for current host */ 92 | function getThemeForHost(isLegacy = true) { 93 | const hostConfig = perHostThemes[currentHost]; 94 | if (hostConfig) { 95 | const selectedTheme = isLegacy 96 | ? hostConfig.legacy 97 | : hostConfig.modern; 98 | log( 99 | `Using ${isLegacy ? "legacy" : "modern"} theme '${selectedTheme}' for host: ${currentHost}`, 100 | ); 101 | return selectedTheme; 102 | } else { 103 | const fallbackTheme = isLegacy 104 | ? defaultThemes.legacy 105 | : defaultThemes.modern; 106 | log( 107 | `Host '${currentHost}' not configured, using default ${isLegacy ? "legacy" : "modern"} theme: ${fallbackTheme}`, 108 | ); 109 | return fallbackTheme; 110 | } 111 | } 112 | 113 | const BASE_URL_LEGACY = 114 | "//cdn.jsdelivr.net/gh/nileane/TangerineUI-for-Mastodon@" + 115 | TAG_LEGACY + 116 | "/TangerineUI"; 117 | const BASE_URL_V4_3 = 118 | "//cdn.jsdelivr.net/gh/nileane/TangerineUI-for-Mastodon@" + 119 | TAG_V4_3 + 120 | "/TangerineUI"; 121 | const BASE_URL_V4_4 = 122 | "//cdn.jsdelivr.net/gh/nileane/TangerineUI-for-Mastodon@" + 123 | TAG_V4_4 + 124 | "/TangerineUI"; 125 | const BASE_URL_V4_5_PLUS = 126 | "//cdn.jsdelivr.net/gh/nileane/TangerineUI-for-Mastodon@" + 127 | TAG_V4_5_PLUS + 128 | "/TangerineUI"; 129 | 130 | /* 131 | * As long as no version is detected, styleUrl is set for version < 4.3.0 132 | */ 133 | let currentLegacyTheme = getThemeForHost(true); 134 | let styleUrl = BASE_URL_LEGACY; 135 | if (currentLegacyTheme == "purple") { 136 | styleUrl = BASE_URL_LEGACY + "-purple.min.css"; 137 | } else { 138 | styleUrl = BASE_URL_LEGACY + ".min.css"; 139 | } 140 | 141 | const isPurpleLegacy = currentLegacyTheme.includes("purple") ? true : false; 142 | const currentModernTheme = getThemeForHost(false); 143 | const isPurple = currentModernTheme.includes("purple") ? true : false; 144 | const isCherry = currentModernTheme.includes("cherry") ? true : false; 145 | const isLagoon = currentModernTheme.includes("lagoon") ? true : false; 146 | 147 | const onElemAvailable = async (selector) => { 148 | while (document.querySelector(selector) === null) { 149 | await new Promise((resolve) => requestAnimationFrame(resolve)); 150 | } 151 | return document.querySelector(selector); 152 | }; 153 | 154 | function createStyleNode(nonce) { 155 | var node = document.createElement("link"); 156 | log("Injected theme URI is : " + styleUrl); 157 | node.href = styleUrl; 158 | node.nonce = nonce; 159 | node.rel = "stylesheet"; 160 | node.media = "all"; 161 | node.type = "text/css"; 162 | return node; 163 | } 164 | 165 | /* ---------------- 166 | * CODE 167 | * ---------------- 168 | */ 169 | var isInjected = false; 170 | let backColor; 171 | let nChanges = 0; 172 | let timeoutExist = false; 173 | let mutationStatus = true; 174 | let isVersionDetected = false; 175 | let nonce; 176 | 177 | function isVersionAtLeast(version, minVersion) { 178 | const parseVersion = (v) => 179 | v.split(".").map((num) => parseInt(num, 10)); 180 | const current = parseVersion(version); 181 | const minimum = parseVersion(minVersion); 182 | for (let i = 0; i < Math.max(current.length, minimum.length); i++) { 183 | const currentPart = current[i] || 0; 184 | const minimumPart = minimum[i] || 0; 185 | if (currentPart > minimumPart) return true; 186 | if (currentPart < minimumPart) return false; 187 | } 188 | return true; // Equal versions 189 | } 190 | 191 | function isVersionExactly(version, targetVersion) { 192 | const parseVersion = (v) => 193 | v.split(".").map((num) => parseInt(num, 10)); 194 | const current = parseVersion(version); 195 | const target = parseVersion(targetVersion); 196 | if (current.length !== target.length) return false; 197 | for (let i = 0; i < current.length; i++) { 198 | if (current[i] !== target[i]) return false; 199 | } 200 | return true; 201 | } 202 | 203 | function getStyleUrlForVersion(mastodonVersion) { 204 | let baseUrl; 205 | 206 | if (isVersionAtLeast(mastodonVersion, "4.5.0")) { 207 | log("Version >= 4.5.0 found, using tag: " + TAG_V4_5_PLUS); 208 | baseUrl = BASE_URL_V4_5_PLUS; 209 | } else if (isVersionAtLeast(mastodonVersion, "4.4.0")) { 210 | log("Version >= 4.4.0 found, using tag: " + TAG_V4_4); 211 | baseUrl = BASE_URL_V4_4; 212 | } else if (isVersionExactly(mastodonVersion, "4.3.0")) { 213 | log("Version 4.3.0 exactly found, using tag: " + TAG_V4_3); 214 | baseUrl = BASE_URL_V4_3; 215 | } else if (isVersionAtLeast(mastodonVersion, "4.3.0")) { 216 | log("Version >= 4.3.0 but < 4.4.0 found, using tag: " + TAG_V4_3); 217 | baseUrl = BASE_URL_V4_3; 218 | } else { 219 | log("Version < 4.3.0 found, using tag: " + TAG_LEGACY); 220 | baseUrl = BASE_URL_LEGACY; 221 | } 222 | 223 | // Apply color scheme based on version 224 | if (isVersionAtLeast(mastodonVersion, "4.3.0")) { 225 | if (isPurple) return baseUrl + "-purple.min.css"; 226 | else if (isCherry) return baseUrl + "-cherry.min.css"; 227 | else if (isLagoon) return baseUrl + "-lagoon.min.css"; 228 | else return baseUrl + ".min.css"; 229 | } else { 230 | // Legacy version handling 231 | if (isPurpleLegacy) return baseUrl + "-purple.min.css"; 232 | else return baseUrl + ".min.css"; 233 | } 234 | } 235 | 236 | /* Version detection */ 237 | onElemAvailable("script[id^=initial-state]").then((selector) => { 238 | isVersionDetected = true; 239 | const mastodonVersion = JSON.parse(selector.innerText).meta.version; 240 | log("Mastodon Version Detected is : " + mastodonVersion); 241 | styleUrl = getStyleUrlForVersion(mastodonVersion); 242 | }); 243 | 244 | if (theme == "light") backColor = "#ffffff"; 245 | else backColor = "#030303"; 246 | 247 | /* Prevent script from running on pages that are not styled by TangerineUI anyway. 248 | */ 249 | if ( 250 | currentPagepath.startsWith("/auth") || 251 | currentPagepath.startsWith("/settings") || 252 | currentPagepath.startsWith("/relationships") || 253 | currentPagepath.startsWith("/filters") || 254 | currentPagepath.startsWith("/oauth") 255 | ) { 256 | return; 257 | } 258 | 259 | /* Observe and detect changes in the DOM 260 | * Usually only works on Firefox, and it's faster than requestAnimationFrame 261 | * in this case, hence why I keep this piece of code that kinda look like powerhouse but hey... 262 | */ 263 | new MutationObserver((mutations, observer) => { 264 | mutations.forEach(function (mutation) { 265 | var node, nodeType, href; 266 | if (mutation.addedNodes.length > 0) { 267 | node = mutation.addedNodes[0]; 268 | nodeType = node.nodeName.toLowerCase(); 269 | if (nodeType == "meta") { 270 | if (node.name.includes("style-nonce")) { 271 | nonce = node.content; 272 | } 273 | } else if (nodeType == "script") { 274 | if (node.id == "initial-state") { 275 | isVersionDetected = true; 276 | const mastodonVersion = JSON.parse(node.innerText).meta 277 | .version; 278 | log( 279 | "(mutationObserver) Mastodon Version Detected is : " + 280 | mastodonVersion, 281 | ); 282 | styleUrl = getStyleUrlForVersion(mastodonVersion); 283 | } 284 | } else if (nodeType == "link") { 285 | if (node.href !== undefined) { 286 | href = node.href; 287 | const rChunk = /\/packs\/css\/mastodon.*\.chunk\.css/; 288 | const rCustom = /custom\.css/; 289 | const rCustomWithHash = /custom-[a-f0-9]+\.css/; 290 | /* Inject after custom.css (old or new format) or mastodon.*.chunk.css, whichever is detected first 291 | * On my system mastodon.*.chunk.css wasn't detected on Chrome 292 | */ 293 | if ( 294 | (href.match(rCustom) || 295 | href.match(rCustomWithHash) || 296 | href.match(rChunk)) && 297 | !isInjected 298 | ) { 299 | if (nonce && isVersionDetected) { 300 | nChanges++; 301 | isInjected = true; 302 | document.head.appendChild( 303 | createStyleNode(nonce), 304 | ); 305 | log("Nonce found is " + nonce); 306 | log( 307 | "(MutationObserver) custom.css or mastodon.*.chunk.css detected, injected TangerineUI's css asap.", 308 | ); 309 | } 310 | } 311 | } 312 | } else if (nodeType == "body") { 313 | nChanges++; 314 | } 315 | } 316 | /* All mutations wanted are done (two changes) 317 | * > disconnect the observer 318 | */ 319 | if (nChanges >= 2 && mutationStatus) { 320 | mutationStatus = false; 321 | observer.disconnect(); 322 | } 323 | /* In some cases, mutation never disconnect ; for example in Safari : 324 | * when it doesn't detect .css file, and use it requestAnimationFrame's API instead, 325 | * This timeout helps to be sure the MutationObserver is disconnected to avoid performance issues 326 | */ 327 | if (!timeoutExist) { 328 | timeoutExist = true; 329 | setTimeout(function () { 330 | if (mutationStatus) { 331 | observer.disconnect(); 332 | } 333 | }, 3000); 334 | } 335 | }); 336 | }).observe(document.documentElement, { childList: true, subtree: true }); 337 | 338 | /* Workaround if MutationObserver couldn't detect the item 339 | * Usually in Safari and Chrome (for Chrome, the first load seems to use MutationObserver, but anyway) 340 | */ 341 | onElemAvailable('link[rel="stylesheet"][href*="custom"]').then((elem) => { 342 | if ( 343 | elem.href.includes("custom.css") || 344 | elem.href.match(/custom-[a-f0-9]+\.css/) 345 | ) { 346 | if (!isInjected) { 347 | if (nonce) { 348 | document.head.appendChild(createStyleNode(nonce)); 349 | isInjected = true; 350 | log( 351 | "(requestAnimationFrame) custom.css detected, injected TangerineUI's css asap", 352 | ); 353 | } else { 354 | isInjected = true; 355 | onElemAvailable("meta[name^=style\\-nonce]").then( 356 | (selector) => { 357 | let nonce_fallback = selector.content; 358 | document.head.appendChild( 359 | createStyleNode(nonce_fallback), 360 | ); 361 | log( 362 | "Nonce was lookedup with requestAnimationFrame as MutationObserver didn't catch it.", 363 | ); 364 | log("Nonce found is " + nonce_fallback); 365 | log( 366 | "(requestAnimationFrame) custom.css detected, injected TangerineUI's css asap", 367 | ); 368 | }, 369 | ); 370 | } 371 | } 372 | } 373 | }); 374 | 375 | onElemAvailable("meta[name^=style\\-nonce]").then((selector) => { 376 | nonce = selector.content; 377 | log("Nounce found (requestAnimationFrame) " + nonce); 378 | }); 379 | })(); 380 | --------------------------------------------------------------------------------