您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Редактирование пунктов меню
// ==UserScript== // @name LOLZ: drag, hide, add custom menu items // @namespace https://lolz.live/ // @version 1.0 // @description Редактирование пунктов меню // @author MisterLis // @match https://lolz.live/* // @grant none // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'manageItemsData_vFinal'; function qs(sel, ctx = document) { return ctx.querySelector(sel); } function qsa(sel, ctx = document) { return [...ctx.querySelectorAll(sel)]; } function normalizeHref(href) { try { const url = new URL(href, location.origin); url.searchParams.delete('_xfToken'); return url.href; } catch { return href; } } function loadData() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { order: [], hidden: [], custom: [] }; } catch { return { order: [], hidden: [], custom: [] }; } } function saveData(data) { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } function rebuildContainer() { const container = qs('.manageItems'); if (!container) return; const data = loadData(); const native = qsa('.manageItem', container) .filter(el => !el.dataset.custom) .map(el => ({ el, href: normalizeHref(el.href), isCustom: false })) .filter(it => !data.hidden.includes(it.href)); const customs = data.custom.map(c => { const a = document.createElement('a'); a.className = 'manageItem'; a.href = c.href; a.dataset.custom = '1'; a.innerHTML = ` <div class="SvgIcon duotone"> <svg width="20" height="20" fill="currentColor"><path d="${c.icon}"/></svg> </div> <span>${c.text}</span>`; return { el: a, href: c.href, isCustom: true }; }); const all = [...native, ...customs]; const orderMap = {}; data.order.forEach((h, i) => orderMap[h] = i); all.sort((a, b) => (orderMap[a.href] ?? 999) - (orderMap[b.href] ?? 999)); container.innerHTML = ''; all.forEach(it => container.appendChild(it.el)); initDragAndDrop(container); } function initDragAndDrop(container) { const items = qsa('.manageItem', container); items.forEach(it => { it.draggable = true; it.style.cursor = 'grab'; }); let dragged = null; function onStart(e) { dragged = this; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', normalizeHref(this.href)); this.classList.add('dragging'); } function onEnd() { this.classList.remove('dragging'); } function onOver(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; const after = getDragAfterElement(container, e.clientY); if (after == null) container.appendChild(dragged); else container.insertBefore(dragged, after); } function onDrop(e) { e.preventDefault(); saveOrder(); } items.forEach(it => { it.addEventListener('dragstart', onStart); it.addEventListener('dragend', onEnd); it.addEventListener('dragover', onOver); it.addEventListener('drop', onDrop); }); } function getDragAfterElement(container, y) { const els = [...qsa('.manageItem', container).filter(it => !it.classList.contains('dragging'))]; return els.reduce((closest, child) => { const box = child.getBoundingClientRect(); const offset = y - box.top - box.height / 2; return (offset < 0 && offset > closest.offset) ? { offset, element: child } : closest; }, { offset: Number.NEGATIVE_INFINITY }).element; } function saveOrder() { const data = loadData(); data.order = qsa('.manageItem').map(el => normalizeHref(el.href)); saveData(data); } function createEditTrigger() { const cont = qs('.manageItems'); if (!cont) return; const bar = document.createElement('div'); bar.className = 'editTriggerBar'; bar.innerHTML = `<svg width="24" height="24" fill="#888"><path d="M12 15.5A3.5 3.5 0 0 1 8.5 12 3.5 3.5 0 0 1 12 8.5a3.5 3.5 0 0 1 3.5 3.5 3.5 3.5 0 0 1-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.65-.07-.97l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65A.488.488 0 0 0 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.32-.07.65-.07.97 0 .33.03.65.07.97L2.46 14.6c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.31.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66Z"/></svg>`; bar.title = 'Редактировать пункты'; bar.style.cssText = 'text-align:center;padding:6px 0;cursor:pointer;opacity:.6;transition:opacity .2s'; bar.onmouseenter = () => bar.style.opacity = 1; bar.onmouseleave = () => bar.style.opacity = .6; bar.onclick = () => toggleEditMode(); cont.parentElement.insertBefore(bar, cont.nextSibling); const plus = document.createElement('a'); plus.className = 'manageItem addCustomItem'; plus.id = 'addCustomItemBtn'; plus.href = 'javascript:;'; plus.innerHTML = ` <div class="SvgIcon duotone"> <svg width="24" height="24" fill="currentColor"> <path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2Z"/> </svg> </div> <span>Добавить свой пункт</span>`; plus.style.display = 'none'; cont.parentElement.insertBefore(plus, cont.nextSibling); plus.addEventListener('click', showAddDialog); } function toggleEditMode() { const cont = qs('.manageItems'); const isEdit = cont.classList.toggle('editMode'); isEdit ? enterEditMode(cont) : exitEditMode(cont); } function enterEditMode(container) { qsa('.manageItem', container).forEach(a => { if (a.dataset.custom) return; const close = document.createElement('span'); close.innerHTML = '×'; close.className = 'itemCloser'; close.onclick = e => { e.preventDefault(); removeItem(a.href); }; a.style.position = 'relative'; a.appendChild(close); }); qsa('.manageItem[data-custom]', container).forEach(a => { const close = document.createElement('span'); close.innerHTML = '×'; close.className = 'itemCloser'; close.onclick = e => { e.preventDefault(); removeCustomItem(a.href); }; a.appendChild(close); }); qs('#addCustomItemBtn').style.display = 'flex'; } function exitEditMode(container) { qsa('.itemCloser').forEach(x => x.remove()); qs('#addCustomItemBtn').style.display = 'none'; } function rebuildAndRestoreEdit() { rebuildContainer(); const cont = qs('.manageItems'); if (cont && cont.classList.contains('editMode')) { cont.classList.remove('editMode'); cont.classList.add('editMode'); enterEditMode(cont); } } function removeItem(href) { const key = normalizeHref(href); const data = loadData(); if (!data.hidden.includes(key)) data.hidden.push(key); saveData(data); rebuildAndRestoreEdit(); } function removeCustomItem(href) { const key = normalizeHref(href); const data = loadData(); data.custom = data.custom.filter(c => c.href !== key); saveData(data); rebuildAndRestoreEdit(); } function showAddDialog() { if (qs('#customItemOverlay')) return; const overlay = document.createElement('div'); overlay.id = 'customItemOverlay'; overlay.className = 'xenOverlay formOverlay'; overlay.style.display = 'block'; const form = document.createElement('form'); form.className = 'xenForm'; form.id = 'customItemForm'; const fieldset = document.createElement('fieldset'); function createInputRow(labelText, id, type = 'text', placeholder = '') { const dl = document.createElement('dl'); dl.className = 'ctrlUnit'; const dt = document.createElement('dt'); const label = document.createElement('label'); label.setAttribute('for', id); label.textContent = labelText; dt.appendChild(label); const dd = document.createElement('dd'); const input = document.createElement('input'); input.type = type; input.id = id; input.className = 'textCtrl OptOut'; input.placeholder = placeholder; dd.appendChild(input); dl.appendChild(dt); dl.appendChild(dd); return dl; } fieldset.appendChild(createInputRow('Адрес:', 'ctrl_custom_url', 'text', 'forums/585/')); fieldset.appendChild(createInputRow('Название:', 'ctrl_custom_text', 'text', 'Мой пункт')); fieldset.appendChild(createInputRow('SVG-иконка:', 'ctrl_custom_icon', 'text', 'M4 6h16M4 12h16M4 18h16')); form.appendChild(fieldset); const footer = document.createElement('div'); footer.className = 'sectionFooter'; const saveBtn = document.createElement('input'); saveBtn.type = 'submit'; saveBtn.value = 'Сохранить'; saveBtn.className = 'button primary'; const cancelBtn = document.createElement('input'); cancelBtn.type = 'button'; cancelBtn.value = 'Отмена'; cancelBtn.className = 'button'; cancelBtn.id = 'cancelCustomItem'; footer.appendChild(saveBtn); footer.appendChild(cancelBtn); form.appendChild(footer); overlay.appendChild(form); document.body.appendChild(overlay); function closeOverlay() { overlay.remove(); } cancelBtn.onclick = closeOverlay; form.onsubmit = e => { e.preventDefault(); const url = qs('#ctrl_custom_url').value.trim(); const text = qs('#ctrl_custom_text').value.trim(); const icon = qs('#ctrl_custom_icon').value.trim(); if (!url || !text) return alert('Заполни адрес и название!'); const absHref = normalizeHref( url.startsWith('http') ? url : location.origin + '/' + url.replace(/^\/+/, '') ); const data = loadData(); const exist = data.custom.findIndex(c => c.href === absHref); if (exist !== -1) { data.custom[exist].text = text; data.custom[exist].icon = icon; } else { data.custom.push({ href: absHref, text, icon }); } saveData(data); rebuildAndRestoreEdit(); closeOverlay(); }; } function init() { if (!qs('.manageItems')) return; rebuildContainer(); createEditTrigger(); } new MutationObserver((_, ob) => { if (qs('.manageItems')) { init(); ob.disconnect(); } }).observe(document, { childList: true, subtree: true }); const style = document.createElement('style'); style.textContent = ` .manageItems.editMode .manageItem{position:relative} .itemCloser{position:absolute;top:2px;right:6px;font-size:18px;color:#e00;cursor:pointer;line-height:1} #addCustomItemBtn.manageItem{display:none;align-items:center;padding:8px 12px;gap:12px; height:52px;box-sizing:border-box;border-radius:8px; background-color:#2d2d2d;color:#aaa;text-decoration:none; transition:background-color .2s,color .2s} #addCustomItemBtn.manageItem:hover{background-color:#303030;text-decoration:none} #addCustomItemBtn.manageItem:hover span{color:#37D38D} #addCustomItemBtn.manageItem .SvgIcon svg{fill:#888;transition:fill .2s} #addCustomItemBtn.manageItem:hover .SvgIcon svg{fill:#37D38D} #customItemOverlay { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: #2d2d2d; color: #ccc; padding: 20px; border-radius: 8px; z-index: 9999; min-width: 460px; box-shadow: 0 0 15px rgba(0,0,0,.6); } #customItemOverlay fieldset { border: none; margin: 0; padding: 0; } #customItemOverlay .ctrlUnit { display: flex; align-items: center; margin-bottom: 12px; } #customItemOverlay .ctrlUnit dt { width: 120px; margin: 0; font-weight: 500; color: #aaa; } #customItemOverlay .ctrlUnit dd { flex: 1; margin: 0; } #customItemOverlay .textCtrl { width: 100%; padding: 6px 8px; border: 1px solid #444; border-radius: 4px; background: #1f1f1f; color: #ddd; } #customItemOverlay .textCtrl:focus { border-color: #37D38D; outline: none; } #customItemOverlay .sectionFooter { margin-top: 15px; text-align: right; } #customItemOverlay .button { margin-left: 8px; } `; document.head.appendChild(style); })();