Dynamic PWA Theme

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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