Inline Utils

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

2026-05-26 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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