Dynamic PWA Theme

Sets PWA titlebar color from manifest icon and other methods, with optional debug overlay

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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);
    }
})();