// ==UserScript== // @name Coloured hashtag-labels in Google Tasks // @namespace https://github.com/jola16/userscripts/ // @version 2024-02-29b // @description Dynamically transforms #hashtags to coloured "labels" in Google Calendar Tasks view. To see it in action, go to your Google Calender and switch to the Tasks view. // @author Jonas Larsen // @match https://tasks.google.com/embed/fullscreen* // @match https://tasks.google.com/*/embed/fullscreen* // @icon https://upload.wikimedia.org/wikipedia/commons/5/5b/Google_Tasks_2021.svg // @grant none // @downloadURL https://update.greasyfork.icu/scripts/488694/Coloured%20hashtag-labels%20in%20Google%20Tasks.user.js // @updateURL https://update.greasyfork.icu/scripts/488694/Coloured%20hashtag-labels%20in%20Google%20Tasks.meta.js // ==/UserScript== (function() { 'use strict'; // Function to create trusted HTML const myp = trustedTypes.createPolicy("myP", { createHTML: (string) => string, }); // Function to generate a consistent pastel-like background color based on the label text const generateBackgroundColor = function (text) { let backgroundColor; // Generate a hash code from the text let hash = 0; for (let i = 0; i < text.length; i++) { hash = text.charCodeAt(i) + ((hash << 5) - hash); } // Convert the hash to a hexadecimal color const color = (hash & 0x00FFFFFF).toString(16).toUpperCase().padStart(6, '0'); // Convert the hexadecimal color to an RGB color const r = Math.round(parseInt(color.substring(0, 2), 16)); const g = Math.round(parseInt(color.substring(2, 4), 16)); const b = Math.round(parseInt(color.substring(4, 6), 16)); // Calculate the pastel-like color by reducing the saturation and lightness const hsl = rgbToHsl(r, g, b); const pastelHSL = [hsl[0], hsl[1] * 0.6, Math.min(hsl[2] * 1.2, 90)]; // Convert the pastel-like HSL color back to RGB const pastelRGB = hslToRgb(...pastelHSL); // Convert the RGB color to a CSS color string backgroundColor = `rgb(${Math.round(pastelRGB[0])}, ${Math.round(pastelRGB[1])}, ${Math.round(pastelRGB[2])})`; // Check the contrast ratio with white let contrastRatio = getContrastRatio(backgroundColor, '#FFFFFF'); // If the contrast ratio is below the recommended threshold (e.g., 4.5:1), adjust the color while (contrastRatio < 4.5) { // Adjust the color to ensure sufficient contrast ratio with white backgroundColor = increaseContrastWithWhite(backgroundColor); // Recalculate the contrast ratio with white contrastRatio = getContrastRatio(backgroundColor, '#FFFFFF'); } return backgroundColor; }; // Function to calculate contrast ratio between two colors const getContrastRatio = function(color1, color2) { // Convert colors to RGBA format const rgba1 = colorToRGBA(color1); const rgba2 = hexToRgb(color2); // Calculate luminance for color 1 const lum1 = luminance(rgba1[0], rgba1[1], rgba1[2]); // Calculate luminance for color 2 const lum2 = luminance(rgba2[0], rgba2[1], rgba2[2]); // Find the lighter and darker colors const lighterColor = Math.max(lum1, lum2); const darkerColor = Math.min(lum1, lum2); // Calculate contrast ratio const contrast = (lighterColor + 0.05) / (darkerColor + 0.05); return contrast; } // Function to convert color string to RGBA array const colorToRGBA = function(color) { const rgba = color.match(/\d+/g).map(Number); return rgba; } // Function to convert hex color to RGB array const hexToRgb = function(hex) { // Remove # if present hex = hex.replace('#', ''); // Convert short hex to full hex if (hex.length === 3) { hex = hex.split('').map(function (x) { return x + x; }).join(''); } // Parse hex values const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return [r, g, b]; } // Function to calculate luminance from RGB components const luminance = function(r, g, b) { const a = [r, g, b].map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } // Function to increase the contrast ratio with white const increaseContrastWithWhite = function (backgroundColor) { // Extract RGB values from the background color string const rgbValues = backgroundColor.match(/\d+(\.\d+)?/g); const r = Math.round(parseFloat(rgbValues[0])); const g = Math.round(parseFloat(rgbValues[1])); const b = Math.round(parseFloat(rgbValues[2])); // Calculate the perceived luminance of the background color const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b; // Calculate the desired luminance of white (1) const desiredLuminance = 1; // Calculate the luminance difference const luminanceDifference = desiredLuminance - luminance; // Adjust each RGB component to darken the color while maintaining its hue and saturation const darkenedR = Math.max(Math.min(r + Math.round(luminanceDifference * 0.2126), 255), 0); const darkenedG = Math.max(Math.min(g + Math.round(luminanceDifference * 0.7152), 255), 0); const darkenedB = Math.max(Math.min(b + Math.round(luminanceDifference * 0.0722), 255), 0); // Convert the adjusted RGB values to a CSS color string return `rgb(${darkenedR}, ${darkenedG}, ${darkenedB})`; }; // Function to convert RGB color to HSL color const rgbToHsl = function (r, g, b) { r /= 255, g /= 255, b /= 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h, s, l = (max + min) / 2; if (max === min) { h = s = 0; } else { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return [h, s, l]; }; // Function to convert HSL color to RGB color const hslToRgb = function (h, s, l) { let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = function (p, q, t) { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [r * 255, g * 255, b * 255]; }; // Function to fix the label on a specified node const fixlabel = function (node) { 'use strict'; // Restore back to a simple #tag let tempHTML = node.innerHTML.replace(/(\S+)<\/label>/g, '#$1'); // Replace #tag with , with styling tempHTML = tempHTML.replace(/#(\S+)/g, function (match, p1) { // Generate a consistent pastel-like background color based on the tag content const backgroundColor = generateBackgroundColor(p1); // Create trusted HTML for the label return ``; }); // Set the modified HTML back to the node node.innerHTML = myp.createHTML(tempHTML); }; // Function to iterate through all div > html-blob elements and fix labels const fixlabels = function () { 'use strict'; const entries = document.querySelectorAll('div > html-blob'); /* console.log(entries.length); */ if (entries.length > 0) { for (const entry of entries) { fixlabel(entry); } } }; // Flag to indicate whether observer is currently running let observerRunning = false; // Function to observe mutations and call fixlabel for each changed node of type html-blob const observe = function () { 'use strict'; const observer = new MutationObserver(function(mutationsList, observer) { if (observerRunning) return; observerRunning = true; setTimeout(() => { /* console.log("slept for 1000 ms. Will now call fixlabels"); */ fixlabels(); observerRunning = false; }, 1000); }); // Start observing mutations on the entire document observer.observe(document, { subtree: true, childList: true }); }; // Wait for the entire window to be fully loaded window.addEventListener('load', function() { // console.log('Window fully loaded. Will sleep for ms before continuing'); setTimeout(() => { // console.log("slept for a short while. Will now continue"); // Debugging - Find all html-blob on the page // const blobs = document.querySelectorAll('html-blob'); // console.error(blobs); // Call fixlabels function to initially fix labels fixlabels(); // Call observe function to start observing mutations observe(); }, 200); }); })();