Greasy Fork is available in English.

LZTQuoteBackground

Add custom SVG background and stylish borders to quotes on lolz.live

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         LZTQuoteBackground
// @namespace    MeloniuM/LZT
// @version      1.3
// @description  Add custom SVG background and stylish borders to quotes on lolz.live
// @author       MeloniuM
// @match        https://lolz.live/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lolz.live
// @grant        none
// ==/UserScript==
(function () {
    'use strict';

    $("<style/>").text(`
    .lzt-quote {
        position: relative;
        padding-left: 10px;
        overflow: inherit;
        border-radius: 6px;
        background-color: var(--bg-color, rgba(0, 0, 0, 0.03)) !important;
        background-image: var(--bg-image) !important;
        background-repeat: no-repeat;
        background-size: auto 100%;
        background-position: right center;
        border-left: var(--border-left, 5px solid #2bad72) !important;
        /*border-image: var(--border-image, none) !important;*/
        border-image-slice: 1 !important;
        box-shadow: var(--box-shadow, none) !important;
    }

    .lzt-quote::before {
        content: "";
        position: absolute;
        top: 0; left: 0; bottom: 0;
        width: 5px;
        border-radius: 6px 0 0 6px;
        background: var(--border-image) !important; /* или background: <svg data-url> */
        pointer-events: none;
    }

    `).appendTo("head");

    // Конфигурация
    const MASK_GROUPS = [{
        x: 68,
        y: 1,
        originalScale: 0.2
    }, {
        x: 70,
        y: 28,
        originalScale: 0.3
    }, {
        x: 30,
        y: 12,
        originalScale: 0.17
    }, {
        x: 6,
        y: 30,
        originalScale: 0.11
    }, {
        x: 30,
        y: 50,
        originalScale: 0.13
    }];
    const MIN_PIXEL_SIZE = 10;
    const MAX_PIXEL_SIZE = 20;
    const ORIGINAL_SCALES = MASK_GROUPS.map(g => g.originalScale);
    const MIN_ORIGINAL_SCALE = Math.min(...ORIGINAL_SCALES);
    const MAX_ORIGINAL_SCALE = Math.max(...ORIGINAL_SCALES);
    const DEFAULT_COLOR = '#2BAD72';
    const DEFAULT_WIDTH = 320;
    const DEFAULT_HEIGHT = 512;
    // Кеш для SVG-фонов
    const backgroundCache = new Map();

    function generateSvgBackground(svgContent, iconWidth, iconHeight, iconColor) {
        const cacheKey = `${svgContent}|${iconColor}`;
        if (backgroundCache.has(cacheKey)) {
            return backgroundCache.get(cacheKey);
        }
        const maskContent = MASK_GROUPS.map(group => {
            const normalizedScale = (group.originalScale - MIN_ORIGINAL_SCALE) / (MAX_ORIGINAL_SCALE - MIN_ORIGINAL_SCALE);
            const pixelSize = MIN_PIXEL_SIZE + normalizedScale * (MAX_PIXEL_SIZE - MIN_PIXEL_SIZE);
            const scale = pixelSize / iconWidth;
            const cx = iconWidth / 2;
            const cy = iconHeight / 2;
            return `
                <g transform="translate(${group.x}, ${group.y}) scale(${scale})">
                    <g fill="${iconColor}" style="transform-origin: ${cx}px ${cy}px;">
                        ${svgContent}
                    </g>
                </g>
            `;
        }).join('');
        const outputSvg = `
            <svg width="112" height="68" viewBox="0 0 112 68" fill="none" xmlns="http://www.w3.org/2000/svg">
                <defs>
                    <radialGradient id="fadeGradient" cx="0.5" cy="0.5" r="0.5" fx="0.5" fy="0.5">
                        <stop offset="0%" stop-color="white" stop-opacity="1"/>
                        <stop offset="100%" stop-color="white" stop-opacity="0"/>
                    </radialGradient>
                    <mask id="fadeMask" maskUnits="userSpaceOnUse" x="0" y="0" width="112" height="68">
                        <rect width="112" height="68" fill="url(#fadeGradient)" />
                    </mask>
                </defs>
                <g mask="url(#fadeMask)">
                    ${maskContent}
                </g>
            </svg>
        `;

        const base64Svg = `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(outputSvg)))}`;
        backgroundCache.set(cacheKey, base64Svg);
        return base64Svg;
    }

    function getAnalogousColor(hex) {
        const {
            r,
            g,
            b
        } = hexToRgb(hex);
        let {
            h,
            s,
            l
        } = rgbToHsl(r, g, b);
        h = (h + 30) % 360;
        s = Math.min(s * 0.5, 0.5);
        l = Math.min(l * 0.7, 0.7);
        const {
            r: newR,
            g: newG,
            b: newB
        } = hslToRgb(h / 360, s, l);
        return `rgba(${newR}, ${newG}, ${newB}, 0.12)`;
    }

    function hexToRgb(hex) {
        hex = hex.replace(/^#/, '');
        const bigint = parseInt(hex, 16);
        return {
            r: (bigint >> 16) & 255,
            g: (bigint >> 8) & 255,
            b: bigint & 255
        };
    }

    function rgbToHsl(r, g, b) {
        r /= 255;
        g /= 255;
        b /= 255;
        const max = Math.max(r, g, b),
            min = Math.min(r, g, b);
        let h, s, l = (max + min) / 2;
        if (max === min) {
            h = s = 0;
        } else {
            const d = max - min;
            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
            switch (max) {
            case r:
                h = (g - b) / d + (g < b ? 6 : 0);
                break;
            case g:
                h = (b - r) / d + 2;
                break;
            case b:
                h = (r - g) / d + 4;
                break;
            }
            h *= 60;
        }
        return {
            h,
            s,
            l
        };
    }

    function hslToRgb(h, s, l) {
        let r, g, b;
        if (s === 0) {
            r = g = b = l;
        } else {
            const hue2rgb = (p, q, t) => {
                if (t < 0) t += 1;
                if (t > 1) t -= 1;
                if (t < 1 / 6) return p + (q - p) * 6 * t;
                if (t < 1 / 2) return q;
                if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
                return p;
            };
            const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
            const p = 2 * l - q;
            r = hue2rgb(p, q, h + 1 / 3);
            g = hue2rgb(p, q, h);
            b = hue2rgb(p, q, h - 1 / 3);
        }
        return {
            r: Math.round(r * 255),
            g: Math.round(g * 255),
            b: Math.round(b * 255)
        };
    }

    function extractBorderStyle(usernameElement) {
        if (!usernameElement) return {
            border: `5px solid ${DEFAULT_COLOR}`,
            shadow: 'none',
            image: null
        };

        const style = usernameElement.getAttribute('style') || '';
        const computedStyle = window.getComputedStyle(usernameElement);

        const bgMatch = style.match(/background\s*:\s*([^;]+)/i);
        const colorMatch = style.match(/color\s*:\s*([^;]+)/i) || (computedStyle.color !== 'rgba(0, 0, 0, 0)' ? [null, computedStyle.color] : null);
        const textShadowMatch = style.match(/text-shadow\s*:\s*([^;]+)/i) || (computedStyle.textShadow !== 'none' ? [null, computedStyle.textShadow] : null);
        const gradientMatches = style.match(/(linear|radial)-gradient\((?:(?:rgba?\([^)]+\)|[^)])+)\)/gi) ||
              (computedStyle.background.includes('gradient') ? [computedStyle.background] : []);

        let border = '';
        let image = null;
        let shadow = 'none';

        if (bgMatch && !gradientMatches.length) {
            border = `5px solid ${bgMatch[1]}`;
        } else if (colorMatch && colorMatch[1] !== 'transparent') {
            border = `5px solid ${colorMatch[1]}`;
        } else {
            border = `5px solid ${DEFAULT_COLOR}`;
        }

        if (textShadowMatch) {
            shadow = textShadowMatch[1];
        }

        if (gradientMatches.length > 0) {
            const cleanedGradients = gradientMatches.map(g => g.trim());
            // объединяем в строку через запятую
            const combinedGradient = cleanedGradients.join(', ');
            image = combinedGradient;
        }

        return {
            border,
            shadow,
            image
        };
    }


    function applyQuoteBackground($target) {
        const iconElement = $target.find('.quoteAuthor').first().find('.uniqUsernameIcon--custom svg').get(0);
        let backgroundSvg = '';
        let iconColor = DEFAULT_COLOR;

        if (iconElement) {
            const inputSvgContent = iconElement.innerHTML;

            // Поиск цвета из иконки
            const pathElement = iconElement.querySelector('path');
            if (pathElement) {
                iconColor = pathElement.getAttribute('fill') || (pathElement.getAttribute('style')?.match(/fill:\s*([^;]+)/)?.[1]);
            }
            if (!iconColor) {
                iconColor = iconElement.getAttribute('fill') || iconElement.getAttribute('style')?.match(/fill:\s*([^;]+)/)?.[1];
            }
            if (!iconColor) {
            iconColor = iconElement.getAttribute('style')?.match(/color:\s*([^;]+)/)?.[1];
            }

            if (!iconColor) {
                const gradient = iconElement.getAttribute('style')?.match(/fill:\s*url\(#(\w+)\)/)?.[1];
                if (gradient) {
                    const gradientElement = iconElement.querySelector(`#${gradient}`);
                    if (gradientElement) {
                        const firstStop = gradientElement.querySelector('stop');
                        if (firstStop) {
                            iconColor = firstStop.getAttribute('stop-color');
                        }
                    }
                }
        }

            iconColor = (iconColor || DEFAULT_COLOR).replace(/["']/g, '');

            let iconWidth = DEFAULT_WIDTH;
            let iconHeight = DEFAULT_HEIGHT;
            const viewBox = iconElement.getAttribute('viewBox');
            if (viewBox) {
            const [, , width, height] = viewBox.split(' ').map(Number);
                iconWidth = width || iconWidth;
                iconHeight = height || iconHeight;
            } else {
                iconWidth = parseFloat(iconElement.getAttribute('width')) || iconWidth;
                iconHeight = parseFloat(iconElement.getAttribute('height')) || iconHeight;
        }

            backgroundSvg = generateSvgBackground(inputSvgContent, iconWidth, iconHeight, iconColor);
        }

        // Извлекаем стиль границы из ника
        const usernameElement = $target.find('.quoteAuthor .username').first().children().first().get(0);

        const bgColor = getAnalogousColor(iconColor);
        const { border, shadow, image } = extractBorderStyle(usernameElement);

        $target.addClass('lzt-quote');

        const el = $target[0];
        el.style.setProperty('--bg-color', bgColor);
        el.style.setProperty('--bg-image', `url(${backgroundSvg})`);
        el.style.setProperty('--border-left', border);

        el.style.setProperty('--box-shadow', shadow || 'none');

        if (image) {
            // Есть border-image — делаем border-left прозрачным, чтобы показать border-image
            el.style.setProperty('--border-left', '0 solid transparent');
            el.style.setProperty('--border-image', image);
        } else {
            // Обычная граница
            el.style.setProperty('--border-left', border);
            el.style.removeProperty('--border-image');
        }

    }

    // Регистрация и начальная обработка
    XenForo.LZTQuoteBackground = function ($target) {
        applyQuoteBackground($target);
    };
    XenForo.register('.message .bbCodeQuote, .comment .bbCodeQuote', 'XenForo.LZTQuoteBackground');
    // Обработка существующих цитат
    $('.message .bbCodeQuote, .comment .bbCodeQuote').each(function () {
        applyQuoteBackground($(this));
    });
    // Обработка динамически загружаемых цитат
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) { // Element node
                    $(node).find('.bbCodeQuote').each(function () {
                        applyQuoteBackground($(this));
                    });
                }
            });
        });
    });
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });
})();