@calc

калькулятор и курсы

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         @calc
// @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      img.lolz.work
// ==/UserScript==

(function () {
    'use strict';

    const AUTOSEND_SETTING_KEY = "calc_autosend_enabled";
    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';

    let autoSendEnabled = GM_getValue(AUTOSEND_SETTING_KEY, false);

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

    init();

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

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

            const text = editor.innerText.trim();

            if (!text.startsWith('@calc ')) {
                removeResults();
                lastQuery = '';
                return;
            }

            const query = text.slice('@calc '.length).trim();

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

            if (query === lastQuery && document.querySelector('.calc-results-renderer')) {
                return;
            }

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

                const currentText = currentEditor.innerText.trim();

                if (!currentText.startsWith('@calc ')) {
                    removeResults();
                    lastQuery = '';
                    return;
                }

                const currentQuery = currentText.slice('@calc '.length).trim();

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

                if (currentQuery === lastQuery && document.querySelector('.calc-results-renderer')) {
                    return;
                }

                try {
                    const result = await getResult(currentQuery);

                    lastQuery = currentQuery;
                    renderResult(result);
                } catch (err) {
                    console.error('[calc] req failed:', err);
                    removeResults();
                }
            }, 400);
        });

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

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

    .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('.calc-results-renderer').forEach(el => el.remove());
        queueMicrotask(() => {
            suppressObserver = false;
        });
    }

    function renderResult(result) {
        removeResults();

        if (!result) return;

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

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

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

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

        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', () => {
            insertText(result.insert);
            removeResults();
        });

        popup.appendChild(item);
        renderer.appendChild(popup);

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

    function insertText(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);
    }

    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);

        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 request(url) {
        return new Promise((resolve, reject) => {
            toggleProgress('PseudoAjaxStart');

            GM_xmlhttpRequest({
                method: 'GET', url, headers: {
                    Accept: 'application/json'
                },

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

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

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

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

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

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

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

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

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

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