@calc

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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