Inline Utils

инлайн утилиты для чата

Ekde 2026/05/26. Vidu La ĝisdata versio.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Inline Utils
// @namespace    нету
// @version      0
// @description  инлайн утилиты для чата
// @author       жди
// @match        *://lolz.live/*
// @match        *://zelenka.guru/*
// @match        *://lolz.guru/*
// @match        *://lolz.market/*
// @match        *://zelenka.market/*
// @match        *://lzt.market/*
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lolz.live
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        unsafeWindow
// @connect      duckduckgo.com
// @connect      api.giphy.com
// @connect      www.youtube.com
// @connect      api-v2.soundcloud.com
// @connect      img.lolz.work
// ==/UserScript==

(function () {
    'use strict';

    const AUTOSEND_SETTING_KEY = "inline_utils_autosend_enabled";
    const ANON_ID_KEY = "soundcloud_anon_id";
    const MARKER = '\u2028';

    const CALC_ICON = 'https://nztcdn.com/files/052f7204-24c1-42e3-8be2-f8ac7efa27d7.webp';
    const CURRENCY_ICON = 'https://nztcdn.com/files/740204c5-3398-4056-8c6f-9d1deb0c4e45.webp';

    const SOUNDCLOUD_CLIENT_ID = 'ROEsbH8bpjaGsqvLX2uk7VjkqxNVlinH';
    const SOUNDCLOUD_APP_VERSION = '1779782131';
    const SOUNDCLOUD_APP_LOCALE = 'en';

    const GIPHY_API_KEY = "3eFQvabDx69SMoOemSPiYfh9FY0nzO9x";

    let autoSendEnabled = GM_getValue(AUTOSEND_SETTING_KEY, false);

    let lastQuery = '';
    let lastCommand = '';
    let debounceTimer = null;
    let suppressObserver = false;
    let randomId = null;
    let cmdId = null;

    init();

    function initObserver() {
        const observer = new MutationObserver(() => {
            if (suppressObserver) return;

            const editor = document.querySelector('.tiptap.ProseMirror');
            if (!editor) return;

            const command = getCommand(editor.innerText.trim());

            if (!command) {
                removeResults();
                lastQuery = '';
                lastCommand = '';
                return;
            }

            if (!command.query) {
                removeResults();
                lastQuery = '';
                lastCommand = '';
                return;
            }

            if (command.name === lastCommand && command.query === lastQuery && document.querySelector(`.${command.name}-results-renderer`)) {
                return;
            }

            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(async () => {
                const currentEditor = document.querySelector('.tiptap.ProseMirror');
                if (!currentEditor) return;

                const currentCommand = getCommand(currentEditor.innerText.trim());

                if (!currentCommand) {
                    removeResults();
                    lastQuery = '';
                    lastCommand = '';
                    return;
                }

                if (!currentCommand.query) {
                    removeResults();
                    lastQuery = '';
                    lastCommand = '';
                    return;
                }

                if (currentCommand.name === lastCommand && currentCommand.query === lastQuery && document.querySelector(`.${currentCommand.name}-results-renderer`)) {
                    return;
                }

                try {
                    const items = await currentCommand.get(currentCommand.query);

                    lastQuery = currentCommand.query;
                    lastCommand = currentCommand.name;
                    currentCommand.render(items);
                } catch (err) {
                    console.error(`[${currentCommand.name}] req failed:`, err);
                    removeResults();
                }
            }, 400);
        });

        observer.observe(document.body, {
            childList: true, subtree: true, characterData: true
        });
    }

    function getCommand(text) {
        const match = text.match(/^@(pic|gif|vid|music|calc)\s+([\s\S]*)$/);

        if (!match) return null;

        const name = match[1];
        const query = match[2].trim();

        return {
            name,
            query,
            get: commands[name].get,
            render: commands[name].render
        };
    }

    function addStyles() {
        GM_addStyle(`
    .pic-results-renderer,
    .gif-results-renderer,
    .vid-results-renderer,
    .music-results-renderer,
    .calc-results-renderer {
        position: absolute;
        left: 60px;
        bottom: calc(100% + 8px);
        z-index: 9999;
        pointer-events: none;
    }

    .pic-popup,
    .gif-popup {
        pointer-events: auto;
        display: grid;
        grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
        gap: 8px;
        width: min(360px, calc(100vw - 80px));
        max-height: 700px;
        overflow-y: auto;
        padding: 8px;
        border-radius: 8px;
        background: #2b2b2b;
        border: 1px solid rgba(255,255,255,0.08);
        box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    }

    .pic-result,
    .gif-result {
        width: 100%;
        aspect-ratio: 1 / 1;
        border-radius: 8px;
        overflow: hidden;
        cursor: pointer;
        background: rgba(255,255,255,0.04);
    }

    .pic-result img,
    .gif-result img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        display: block;
    }

    .pic-result:hover,
    .gif-result:hover,
    .vid-result:hover,
    .music-result:hover {
        outline: 2px solid rgba(255,255,255,0.35);
    }

    .vid-popup,
    .music-popup {
        pointer-events: auto;
        display: flex;
        flex-direction: column;
        gap: 6px;
        max-height: 700px;
        overflow-y: auto;
        padding: 8px;
        border-radius: 8px;
        background: #2b2b2b;
        border: 1px solid rgba(255,255,255,0.08);
        box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    }

    .vid-popup {
        width: min(420px, calc(100vw - 80px));
    }

    .music-popup {
        width: min(440px, calc(100vw - 80px));
    }

    .vid-result,
    .music-result {
        width: 100%;
        min-height: 64px;
        border-radius: 8px;
        overflow: hidden;
        cursor: pointer;
        background: rgba(255,255,255,0.04);
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 6px;
        box-sizing: border-box;
    }

    .vid-result img,
    .music-result img {
        width: 54px;
        height: 54px;
        min-width: 54px;
        object-fit: cover;
        display: block;
        border-radius: 6px;
        background: rgba(255,255,255,0.06);
    }

    .vid-info,
    .music-info {
        min-width: 0;
        flex: 1;
    }

    .vid-title,
    .music-title {
        color: #fff;
        font-size: 13px;
        font-weight: 700;
        line-height: 1.25;
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
    }

    .vid-views,
    .music-plays {
        color: rgba(255,255,255,0.65);
        font-size: 12px;
        line-height: 1.25;
        margin-top: 4px;
    }

    .calc-popup {
        pointer-events: auto;
        width: min(360px, calc(100vw - 80px));
        padding: 8px;
        border-radius: 8px;
        background: #2b2b2b;
        border: 1px solid rgba(255,255,255,0.08);
        box-shadow: 0 8px 24px rgba(0,0,0,0.35);
    }

    .calc-result {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 8px;
        border-radius: 8px;
        cursor: pointer;
        background: rgba(255,255,255,0.04);
    }

    .calc-result:hover {
        background: rgba(255,255,255,0.08);
    }

    .calc-result img {
        width: 64px;
        height: 64px;
        flex: 0 0 64px;
        object-fit: contain;
        border-radius: 8px;
    }

    .calc-result-title {
        font-weight: 700;
        color: rgba(255,255,255,0.92);
        line-height: 1.25;
    }

    .calc-result-text {
        margin-top: 3px;
        color: rgba(255,255,255,0.68);
        line-height: 1.25;
        word-break: break-word;
    }
`);
    }

    function registerSetting() {
        if (cmdId !== null) {
            GM_unregisterMenuCommand(cmdId);
        }

        const commandTitle = (autoSendEnabled ? "Выключить" : "Включить") + " автоотправку";

        cmdId = GM_registerMenuCommand(commandTitle, () => {
            autoSendEnabled = !autoSendEnabled;
            GM_setValue(AUTOSEND_SETTING_KEY, autoSendEnabled);

            registerSetting();
        });
    }

    function removeResults() {
        suppressObserver = true;
        document.querySelectorAll('.pic-results-renderer, .gif-results-renderer, .vid-results-renderer, .music-results-renderer, .calc-results-renderer').forEach(el => el.remove());
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function getWrapper() {
        const editor = document.querySelector('.tiptap.ProseMirror');
        if (!editor) return null;

        const wrapper = document.querySelector('.editor-box-wrapper') || editor.parentElement;
        if (!wrapper) return null;

        if (getComputedStyle(wrapper).position === 'static') {
            wrapper.style.position = 'relative';
        }

        return wrapper;
    }

    function renderPopup(command, popup) {
        removeResults();

        const wrapper = getWrapper();
        if (!wrapper) return;

        const renderer = document.createElement('div');
        renderer.className = `${command}-results-renderer`;
        renderer.appendChild(popup);

        suppressObserver = true;
        wrapper.appendChild(renderer);
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function insertValue(command, value) {
        const editor = document.querySelector('.tiptap.ProseMirror');
        if (!editor) return;

        const text = editor.innerText;
        const newText = text.replace(new RegExp(`^@${command}(?:\\s+.*)?$`, 'm'), value);

        editor.focus();
        editor.innerText = newText;
        editor.dispatchEvent(new Event('input', {bubbles: true}));

        if (autoSendEnabled) setTimeout(() => unsafeWindow.$('[aria-label="send-message"]').trigger('click'), 200);
    }

    function insertCalc(value) {
        const editor = document.querySelector('.tiptap.ProseMirror');
        if (!editor) return;

        const text = editor.innerText;
        const newText = text.replace(/^@calc(?:\s+.*)?$/m, value.replace(/\n/g, MARKER));

        editor.focus();
        editor.innerText = newText;
        editor.style.whiteSpace = 'pre-wrap';
        editor.dispatchEvent(new Event('input', {bubbles: true}));

        if (autoSendEnabled) setTimeout(() => unsafeWindow.$('[aria-label="send-message"]').trigger('click'), 200);
    }

    function request(url, headers, json) {
        return new Promise((resolve, reject) => {
            toggleProgress('PseudoAjaxStart');

            GM_xmlhttpRequest({
                method: 'GET',
                url,
                headers,

                onload: res => {
                    toggleProgress('PseudoAjaxStop');

                    if (json) {
                        try {
                            resolve(JSON.parse(res.responseText));
                        } catch (err) {
                            reject(err);
                        }

                        return;
                    }

                    resolve(res.responseText);
                },

                onerror: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                },

                ontimeout: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                },

                onabort: err => {
                    toggleProgress('PseudoAjaxStop');
                    reject(err);
                }
            });
        });
    }

    async function getImages(query) {
        const q = encodeURIComponent(query);

        const searchUrl = `https://duckduckgo.com/?q=${q}&iax=images&ia=images`;

        const html = await request(searchUrl);

        const vqd = html.match(/vqd=['"]?([^'"\s&]+)['"]?/i)?.[1];

        if (!vqd) {
            throw new Error("vdq not found");
        }

        const imagesUrl = `https://duckduckgo.com/i.js?` + new URLSearchParams({
            l: "ru-ru", o: "json", q: query, vqd, f: ",,,", p: "1"
        });

        const data = await request(imagesUrl, null, true);

        return data.results.map((item) => item.image);
    }

    async function getRandomId() {
        if (randomId) return randomId;

        const url = `https://api.giphy.com/v1/randomid?api_key=${GIPHY_API_KEY}`;
        const data = await request(url, {
            Accept: 'application/json'
        }, true);

        randomId = data?.data?.random_id || '';
        return randomId;
    }

    async function getGifs(query) {
        const rid = await getRandomId();

        const params = new URLSearchParams({
            random_id: rid,
            offset: '0',
            limit: '25',
            bundle: 'messaging_non_clips',
            rating: 'g',
            api_key: GIPHY_API_KEY
        });

        params.set('q', query);
        params.set('lang', 'ru');

        const url = `https://api.giphy.com/v1/gifs/search?${params.toString()}`;
        const data = await request(url, {
            Accept: 'application/json'
        }, true);

        return parseGifs(data);
    }

    function parseGifs(data) {
        if (!data || !Array.isArray(data.data)) return [];

        const results = [];
        const seen = new Set();

        for (const item of data.data) {
            const preview =
                item?.images?.fixed_width_small?.url ||
                item?.images?.fixed_height_small?.url ||
                item?.images?.fixed_width?.url ||
                item?.images?.original?.url;

            const original =
                item?.images?.original?.url ||
                item?.images?.fixed_width?.url ||
                preview;

            if (!preview || !original) continue;
            if (seen.has(original)) continue;

            seen.add(original);

            results.push({
                preview,
                original,
                title: item.title || 'gif'
            });
        }

        return results;
    }

    async function getVideos(query) {
        const q = encodeURIComponent(query);

        const searchUrl = `https://www.youtube.com/results?search_query=${q}`;

        const html = await request(searchUrl, {
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
        });

        const data = extractData(html);

        return parseVideos(data).slice(0, 50);
    }

    function extractData(html) {
        const marker = 'var ytInitialData = ';
        let start = html.indexOf(marker);

        if (start === -1) {
            const marker2 = 'ytInitialData = ';
            start = html.indexOf(marker2);

            if (start === -1) {
                throw new Error("ytInitialData not found");
            }

            start += marker2.length;
        } else {
            start += marker.length;
        }

        let i = start;
        let depth = 0;
        let inString = false;
        let stringQuote = '';
        let escaped = false;

        for (; i < html.length; i++) {
            const ch = html[i];

            if (inString) {
                if (escaped) {
                    escaped = false;
                } else if (ch === '\\') {
                    escaped = true;
                } else if (ch === stringQuote) {
                    inString = false;
                }

                continue;
            }

            if (ch === '"' || ch === "'") {
                inString = true;
                stringQuote = ch;
                continue;
            }

            if (ch === '{') depth++;
            if (ch === '}') depth--;

            if (depth === 0 && ch === '}') {
                i++;
                break;
            }
        }

        return JSON.parse(html.slice(start, i));
    }

    function parseVideos(data) {
        const videos = [];
        const seen = new Set();

        walk(data, node => {
            if (!node.videoRenderer) return;

            const v = node.videoRenderer;
            const videoId = v.videoId;

            if (!videoId || seen.has(videoId)) return;
            seen.add(videoId);

            videos.push({
                videoId,
                title: getText(v.title),
                views: formatViews(getText(v.viewCountText)),
                thumbnail: getBestThumbnail(v.thumbnail?.thumbnails) || getThumbnail(videoId),
            });
        });

        return videos;
    }

    function walk(obj, cb) {
        if (!obj || typeof obj !== 'object') return;

        cb(obj);

        if (Array.isArray(obj)) {
            obj.forEach(item => walk(item, cb));
        } else {
            Object.values(obj).forEach(value => walk(value, cb));
        }
    }

    function getText(obj) {
        if (!obj) return "";
        if (obj.simpleText) return obj.simpleText;
        if (Array.isArray(obj.runs)) return obj.runs.map(item => item.text).join("");

        return "";
    }

    function getBestThumbnail(thumbnails) {
        if (!Array.isArray(thumbnails) || !thumbnails.length) return "";

        const best = thumbnails
            .slice()
            .sort((a, b) => (b.width || 0) - (a.width || 0))[0];

        if (!best || !best.url) return "";

        let url = best.url;

        if (url.startsWith('//')) url = 'https:' + url;
        if (url.startsWith('/')) url = 'https://www.youtube.com' + url;

        return url;
    }

    function getThumbnail(videoId) {
        return `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`;
    }

    function formatViews(text) {
        const num = Number(String(text || '').replace(/[^\d]/g, ''));

        if (!num) return text || '';

        if (num >= 900000) {
            return `${String(Math.floor(num / 100000) / 10).replace('.0', '')}M просмотров`;
        }

        if (num >= 1000) {
            return `${Math.floor(num / 1000)}K просмотров`;
        }

        return `${num} просмотров`;
    }

    async function getTracks(query) {
        const q = encodeURIComponent(query);
        const userId = getAnonId();

        const url =
            `https://api-v2.soundcloud.com/search` +
            `?q=${q}` +
            `&facet=model` +
            `&user_id=${encodeURIComponent(userId)}` +
            `&client_id=${encodeURIComponent(SOUNDCLOUD_CLIENT_ID)}` +
            `&limit=20` +
            `&offset=0` +
            `&linked_partitioning=1` +
            `&app_version=${encodeURIComponent(SOUNDCLOUD_APP_VERSION)}` +
            `&app_locale=${encodeURIComponent(SOUNDCLOUD_APP_LOCALE)}`;

        const data = await request(url, {
            'Accept': 'application/json, text/plain, */*'
        }, true);

        return parseTracks(data).slice(0, 20);
    }

    function parseTracks(data) {
        const tracks = [];
        const seen = new Set();

        const collection = Array.isArray(data?.collection) ? data.collection : [];

        for (const item of collection) {
            const track = normalizeTrack(item);

            if (!track) continue;
            if (seen.has(track.id || track.url)) continue;

            seen.add(track.id || track.url);
            tracks.push(track);
        }

        return tracks;
    }

    function normalizeTrack(item) {
        const t = item?.kind === 'track'
            ? item
            : item?.track?.kind === 'track'
                ? item.track
                : item?.model?.kind === 'track'
                    ? item.model
                    : null;

        if (!t) return null;

        const title = cleanText(t.title);
        const artist = cleanText(t.user?.username || t.publisher_metadata?.artist || 'Unknown artist');
        const url = t.permalink_url;

        if (!title || !url) return null;

        return {
            id: t.id,
            title,
            artist,
            url,
            artwork: getArtwork(t),
            plays: formatPlays(t.playback_count)
        };
    }

    function getArtwork(track) {
        let url =
            track.artwork_url ||
            track.user?.avatar_url ||
            '';

        if (!url) {
            return 'https://a-v2.sndcdn.com/assets/images/sc-icons/favicon-2cadd14bdb.ico';
        }

        url = url
            .replace('-large.', '-t300x300.')
            .replace('-t120x120.', '-t300x300.')
            .replace('-small.', '-t300x300.');

        if (url.startsWith('//')) url = 'https:' + url;

        return url;
    }

    function formatPlays(count) {
        const num = Number(count);

        if (!Number.isFinite(num) || num <= 0) {
            return '0 прослушиваний';
        }

        if (num >= 1000000000) {
            return `${trimNumber(num / 1000000000)}B прослушиваний`;
        }

        if (num >= 1000000) {
            return `${trimNumber(num / 1000000)}M прослушиваний`;
        }

        if (num >= 1000) {
            return `${trimNumber(num / 1000)}K прослушиваний`;
        }

        return `${num} прослушиваний`;
    }

    function trimNumber(value) {
        return value.toFixed(value >= 10 ? 0 : 1).replace('.0', '');
    }

    function cleanText(value) {
        return String(value || '').replace(/\s+/g, ' ').trim();
    }

    function getAnonId() {
        let id = GM_getValue(ANON_ID_KEY, '');

        if (!id) {
            id = genAID();
            GM_setValue(ANON_ID_KEY, id);
        }

        return id;
    }

    function genAID() {
        return [
            randomDigits(6),
            randomDigits(6),
            randomDigits(6),
            randomDigits(6)
        ].join('-');
    }

    function randomDigits(length) {
        let out = '';

        for (let i = 0; i < length; i++) {
            out += Math.floor(Math.random() * 10);
        }

        return out;
    }

    async function getResult(query) {
        const math = getMathResult(query);

        if (math) return math;

        const currency = await getCurrency(query);

        if (!currency) return null;

        return {
            icon: CURRENCY_ICON,
            title: `Курс ${currency.from} в ${currency.to}`,
            text: currency.formatted?.caption || `${formatAmount(currency.amount)} ${currency.from} = ${formatAmount(currency.result)} ${currency.to}`,
            insert: `[IMG]${currency.card_url}[/IMG]\n:duck_money: ${currency.formatted?.from_amount || formatAmount(currency.amount)} ${currency.from} = ${currency.formatted?.to_amount || formatAmount(currency.result)} ${currency.to}`
        };
    }

    function getMathResult(query) {
        const value = query.replace(',', '.').trim();

        if (!isMathQuery(value)) return null;

        const result = calc(value);

        if (!Number.isFinite(result)) return null;

        const formatted = formatAmount(result);

        return {
            icon: CALC_ICON, title: 'Калькулятор', text: `${query} = ${formatted}`, insert: `${query} = ${formatted}`
        };
    }

    function isMathQuery(value) {
        if (!/[+\-*/%^()]/.test(value)) return false;
        return /^[\d+\-*/%^().\s]+$/.test(value);

    }

    function calc(value) {
        const expression = value.replace(/\^/g, '**');

        return Function(`"use strict"; return (${expression})`)();
    }

    async function getCurrency(query) {
        const normalized = normalizeCurrencyQuery(query);
        const url = `https://img.lolz.work/api/convert?q=${encodeURIComponent(normalized)}`;
        const data = await request(url, {
            Accept: 'application/json'
        }, true);

        if (!data?.ok || !data?.card_url) return null;

        return data;
    }

    function normalizeCurrencyQuery(query) {
        let value = query
            .replace(',', '.')
            .replace(/\s+/g, ' ')
            .trim();

        value = value.replace(/^(\d+(?:\.\d+)?)([a-zР°-СЏ]{2,10})$/i, '$1 $2');

        const pair = value.match(/^(\d+(?:\.\d+)?)\s*([a-zР°-СЏ]{2,10})\s+([a-zР°-СЏ]{2,10})$/i);

        if (pair) {
            return `${pair[1]} ${pair[2]} to ${pair[3]}`;
        }

        const single = value.match(/^(\d+(?:\.\d+)?)\s*([a-zР°-СЏ]{2,10})$/i);

        if (single) {
            return `${single[1]} ${single[2]} to rub`;
        }

        return value;
    }

    function formatAmount(value) {
        const number = Number(value);

        if (!Number.isFinite(number)) return String(value);

        return Number(number.toFixed(3)).toString();
    }

    function renderImages(command, images) {
        if (!images || !images.length) {
            removeResults();
            return;
        }

        const popup = document.createElement('div');
        popup.className = `${command}-popup`;

        images.forEach(src => {
            const item = document.createElement('div');
            item.className = `${command}-result`;

            const img = document.createElement('img');
            img.src = src;
            img.alt = 'image';
            img.title = src;

            item.appendChild(img);

            item.addEventListener('click', () => {
                insertValue(command, `[IMG]${src}[/IMG]`);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderPopup(command, popup);
    }

    function renderGifs(images) {
        if (!images || !images.length) {
            removeResults();
            return;
        }

        const popup = document.createElement('div');
        popup.className = 'gif-popup';

        images.forEach(src => {
            const item = document.createElement('div');
            item.className = 'gif-result';

            const img = document.createElement('img');
            img.src = src.preview;
            img.alt = src.title;
            img.title = src.original;

            item.appendChild(img);

            item.addEventListener('click', () => {
                insertValue('gif', `[IMG]${src.original}[/IMG]`);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderPopup('gif', popup);
    }

    function renderVideos(videos) {
        if (!videos || !videos.length) {
            removeResults();
            return;
        }

        const popup = document.createElement('div');
        popup.className = 'vid-popup';

        videos.forEach(src => {
            const item = document.createElement('div');
            item.className = 'vid-result';

            const img = document.createElement('img');
            img.src = src.thumbnail;
            img.alt = src.title;
            img.title = src.title;

            const info = document.createElement('div');
            info.className = 'vid-info';

            const title = document.createElement('div');
            title.className = 'vid-title';
            title.textContent = src.title;

            const views = document.createElement('div');
            views.className = 'vid-views';
            views.textContent = src.views;

            info.appendChild(title);
            info.appendChild(views);

            item.appendChild(img);
            item.appendChild(info);

            item.addEventListener('click', () => {
                insertValue('vid', `[MEDIA=youtube]${src.videoId}[/MEDIA]`);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderPopup('vid', popup);
    }

    function renderTracks(tracks) {
        if (!tracks || !tracks.length) {
            removeResults();
            return;
        }

        const popup = document.createElement('div');
        popup.className = 'music-popup';

        tracks.forEach(track => {
            const item = document.createElement('div');
            item.className = 'music-result';

            const img = document.createElement('img');
            img.src = track.artwork;
            img.alt = `${track.artist} - ${track.title}`;
            img.title = `${track.artist} - ${track.title}`;

            const info = document.createElement('div');
            info.className = 'music-info';

            const title = document.createElement('div');
            title.className = 'music-title';
            title.textContent = `${track.artist} - ${track.title}`;

            const plays = document.createElement('div');
            plays.className = 'music-plays';
            plays.textContent = track.plays;

            info.appendChild(title);
            info.appendChild(plays);

            item.appendChild(img);
            item.appendChild(info);

            item.addEventListener('click', () => {
                insertValue('music', `[MEDIA=soundcloud]${track.url.replace("https://", "")}[/MEDIA]`);
                removeResults();
            });

            popup.appendChild(item);
        });

        renderPopup('music', popup);
    }

    function renderResult(result) {
        if (!result) {
            removeResults();
            return;
        }

        const popup = document.createElement('div');
        popup.className = 'calc-popup';

        const item = document.createElement('div');
        item.className = 'calc-result';

        const img = document.createElement('img');
        img.src = result.icon;
        img.alt = result.title;

        const content = document.createElement('div');

        const title = document.createElement('div');
        title.className = 'calc-result-title';
        title.textContent = result.title;

        const text = document.createElement('div');
        text.className = 'calc-result-text';
        text.textContent = result.text;

        content.appendChild(title);
        content.appendChild(text);

        item.appendChild(img);
        item.appendChild(content);

        item.addEventListener('click', () => {
            insertCalc(result.insert);
            removeResults();
        });

        popup.appendChild(item);

        renderPopup('calc', popup);
    }

    function toggleProgress(eventName) {
        const $ = unsafeWindow.jQuery || unsafeWindow.$;

        if ($) {
            $(unsafeWindow.document).trigger(eventName);
        }
    }

    function patchLineBreaks() {
        const code = `
            (() => {
                'use strict';
            
                const MARKER = ${JSON.stringify(MARKER)};
                
                const XHR = window.XMLHttpRequest;
                
                const oldOpen = XHR.prototype.open;
                const oldSend = XHR.prototype.send;
            
                function processContent(nodes) {
                    const newNodes = [];
            
                    nodes.forEach(node => {
                        if (node.type === 'text' && node.text && node.text.includes(MARKER)) {
                            const parts = node.text.split(MARKER);
            
                            parts.forEach((part, i) => {
                                if (part) newNodes.push({ type: 'text', text: part });
                                if (i < parts.length - 1) newNodes.push({ type: 'hardBreak' });
                            });
            
                            return;
                        }
            
                        if (node.content) {
                            node = { ...node, content: processContent(node.content) };
                        }
            
                        newNodes.push(node);
                    });
            
                    return newNodes;
                }
                
                XHR.prototype.open = function(method, url, ...rest) {
                    this.__patchedMethod = method;
                    this.__patchedUrl = url;
                
                    return oldOpen.call(this, method, url, ...rest);
                };
            
                XHR.prototype.send = function(data) {
                const requestUrl = this.__patchedUrl;
                const method = this.__patchedMethod;
                
                const url = new URL(requestUrl, location.href);
                                    
                const isTarget =
                    method?.toUpperCase() === 'POST' &&
                    url.pathname === '/chatbox/messages/';
                    
                    if (isTarget && typeof data === 'string' && data.includes('message=')) {
                        try {
                            const params = new URLSearchParams(data);
                            const messageJson = params.get('message');
            
                            if (messageJson && messageJson.includes(MARKER)) {
                                const msgObj = JSON.parse(messageJson);
            
                                if (msgObj && Array.isArray(msgObj.content)) {
                                    msgObj.content = processContent(msgObj.content);
                                    params.set('message', JSON.stringify(msgObj));
                                    data = params.toString();
                                }
                            }
                        } catch (e) {
                            console.error('[calc] linebreak failed:', e);
                        }
                    }
            
                    return oldSend.call(this, data);
                };
            })();
        `;

        const script = document.createElement('script');

        const nonce = document.querySelector('script[nonce]')?.nonce || document.querySelector('script[nonce]')?.getAttribute('nonce');

        if (nonce) {
            script.setAttribute('nonce', nonce);
        }

        script.textContent = code;
        document.documentElement.appendChild(script);
        script.remove();
    }

    const commands = {
        pic: {
            get: getImages,
            render: images => renderImages('pic', images)
        },

        gif: {
            get: getGifs,
            render: renderGifs
        },

        vid: {
            get: getVideos,
            render: renderVideos
        },

        music: {
            get: getTracks,
            render: renderTracks
        },

        calc: {
            get: getResult,
            render: renderResult
        }
    };

    function init() {
        initObserver();
        addStyles();
        registerSetting();
        patchLineBreaks();
    }
})();