Dynamic PWA Theme

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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