Greasy Fork is available in English.
калькулятор и курсы
// ==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();
}
})();