Sets PWA titlebar color from manifest icon and other methods, with optional debug overlay
// ==UserScript==
// @name Dynamic PWA Theme
// @namespace http://example.com
// @version 1.9
// @description Sets PWA titlebar color from manifest icon and other methods, with optional debug overlay
// @match *://*/*
// @grant none
// @run-at document-start
// @author jigglypug
// @license MIT
// ==/UserScript==
(async function() {
'use strict';
const DEBUG = 1; // 0 = off, 1 = show debug overlay
async function getImageDominantColor(url) {
return new Promise(resolve => {
const img = new Image();
img.crossOrigin = 'Anonymous';
img.src = url;
img.onload = () => {
const canvas = document.createElement('canvas');
const w = 64, h = 64;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
const data = ctx.getImageData(0, 0, w, h).data;
const colorCount = {};
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
if (a < 128) continue;
if (r > 240 && g > 240 && b > 240) continue;
if (r < 15 && g < 15 && b < 15) continue;
// Use a numeric key instead of a string to avoid thousands of string allocations
const key = (r << 16) | (g << 8) | b;
colorCount[key] = (colorCount[key] || 0) + 1;
}
let max = 0, dominant = null;
for (const key in colorCount) {
if (colorCount[key] > max) {
max = colorCount[key];
dominant = key;
}
}
if (!dominant) return resolve(null);
// Derive hex directly from the numeric key
const hex = '#' + Number(dominant).toString(16).padStart(6, '0');
resolve({ hex });
};
img.onerror = () => {
console.warn('Failed to load image for dominant color:', url);
resolve(null);
};
});
}
async function getThemeColor() {
let methodUsed = 'default #121212';
const manifestLink = document.querySelector('link[rel="manifest"]');
// 1. Fetch manifest once and try icon, then theme_color
if (manifestLink) {
try {
const res = await fetch(manifestLink.href);
const manifest = await res.json();
// 1a. Manifest icon — find the largest icon using reduce
if (manifest.icons?.length) {
const largest = manifest.icons.reduce((best, icon) => {
const size = icon.sizes ? Math.max(...icon.sizes.split('x').map(Number)) : 0;
const bestSize = best.sizes ? Math.max(...best.sizes.split('x').map(Number)) : 0;
return size > bestSize ? icon : best;
}, manifest.icons[0]);
if (largest.src) {
try {
const iconUrl = new URL(largest.src, manifestLink.href).href;
const color = await getImageDominantColor(iconUrl);
if (color) {
return { color: color.hex, iconUrl, methodUsed: 'manifest.icon' };
}
} catch(e) {}
}
}
// 1b. Manifest theme_color (same fetch, no second request)
if (manifest.theme_color) {
return { color: manifest.theme_color, iconUrl: null, methodUsed: 'manifest.theme_color' };
}
} catch {}
}
// 2. meta theme-color
const meta = document.querySelector('meta[name="theme-color"]');
if (meta?.content) {
return { color: meta.content, iconUrl: null, methodUsed: 'meta theme-color' };
}
// 3. favicon
const link = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
if (link?.href) {
const color = await getImageDominantColor(link.href);
if (color) {
return { color: color.hex, iconUrl: link.href, methodUsed: 'favicon' };
}
}
// 4. default
return { color: '#121212', iconUrl: null, methodUsed };
}
const { color, iconUrl, methodUsed } = await getThemeColor();
if (!document.querySelector('meta[name="theme-color"][data-dynamic]')) {
const metaTag = document.createElement('meta');
metaTag.name = 'theme-color';
metaTag.setAttribute('data-dynamic', 'true');
metaTag.content = color;
document.head.prepend(metaTag);
}
console.log('PWA theme color applied:', color, 'via', methodUsed);
if (DEBUG) {
const overlay = document.createElement('div');
overlay.style = `
position:fixed;
top:10px; left:10px;
z-index:99999;
padding:4px;
border:2px solid black;
background:white;
font-family:sans-serif;
font-size:12px;
display:flex;
align-items:center;
gap:4px;
`;
if (iconUrl) {
const img = document.createElement('img');
img.src = iconUrl;
img.width = 24;
img.height = 24;
img.style = 'border:1px solid black';
overlay.appendChild(img);
}
const info = document.createElement('div');
info.textContent = `[${color}] Method: ${methodUsed}`;
overlay.appendChild(info);
document.body.appendChild(overlay);
}
})();