@calc

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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