Dynamic PWA Theme

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

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

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