您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base64编解码工具 for Discourse论坛
当前为
// ==UserScript== // @name Discourse Base64 Helper // @namespace http://tampermonkey.net/ // @version 1.0 // @description Base64编解码工具 for Discourse论坛 // @author Xavier // @match *://linux.do/* // @match *://clochat.com/* // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 样式注入 GM_addStyle(` .decoded-text { cursor: pointer; transition: all 0.2s; padding: 1px 3px; border-radius: 3px; background-color: #fff3cd !important; color: #664d03 !important; } .decoded-text:hover { background-color: #ffe69c !important; } @media (prefers-color-scheme: dark) { .decoded-text { background-color: #332100 !important; color: #ffd54f !important; } .decoded-text:hover { background-color: #664d03 !important; } } .menu-item[data-mode="restore"] { background: rgba(0, 123, 255, 0.1) !important; } `); // 初始化检测 if (document.getElementById('base64-helper-root')) return; const container = document.createElement('div'); container.id = 'base64-helper-root'; document.body.append(container); const shadowRoot = container.attachShadow({ mode: 'open' }); // Shadow DOM样式 const style = document.createElement('style'); style.textContent = ` :host { all: initial !important; position: fixed !important; z-index: 2147483647 !important; pointer-events: none !important; } .base64-helper { position: fixed; bottom: 20px; right: 20px; z-index: 2147483647; cursor: move; font-family: system-ui, -apple-system, sans-serif; opacity: 0.5; transition: opacity 0.3s ease; pointer-events: auto !important; } .base64-helper:hover { opacity: 1 !important; } .main-btn { background: #ffffff; color: #000000 !important; padding: 8px 16px; border-radius: 6px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); font-weight: 500; user-select: none; transition: all 0.2s; font-size: 14px; cursor: pointer; border: none !important; pointer-events: auto !important; } .menu { position: absolute; bottom: calc(100% + 5px); right: 0; background: #ffffff; border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: none; min-width: auto !important; width: max-content !important; pointer-events: auto !important; overflow: hidden; } .menu-item { padding: 8px 12px !important; color: #333 !important; transition: all 0.2s; font-size: 13px; cursor: pointer; position: relative; border-radius: 0 !important; isolation: isolate; white-space: nowrap !important; } .menu-item:hover::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: currentColor; opacity: 0.1; z-index: -1; } .menu-item:first-child:hover::before { border-radius: 6px 6px 0 0; } .menu-item:last-child:hover::before { border-radius: 0 0 6px 6px; } @media (prefers-color-scheme: dark) { .main-btn { background: #2d2d2d; color: #fff !important; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } .menu { background: #1a1a1a; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } .menu-item { color: #e0e0e0 !important; } .menu-item:hover::before { opacity: 0.08; } } @keyframes slideIn { from { top: -50px; opacity: 0; } to { top: 20px; opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } `; shadowRoot.appendChild(style); // 界面元素 const uiContainer = document.createElement('div'); uiContainer.className = 'base64-helper'; const mainBtn = document.createElement('button'); mainBtn.className = 'main-btn'; mainBtn.textContent = 'Base64'; const menu = document.createElement('div'); menu.className = 'menu'; const decodeBtn = document.createElement('div'); decodeBtn.className = 'menu-item'; decodeBtn.textContent = '解析本页Base64'; decodeBtn.dataset.mode = 'decode'; const encodeBtn = document.createElement('div'); encodeBtn.className = 'menu-item'; encodeBtn.textContent = '文本转Base64'; menu.append(decodeBtn, encodeBtn); uiContainer.append(mainBtn, menu); shadowRoot.appendChild(uiContainer); // 核心功能 let menuVisible = false; let isDragging = false; let startX, startY, initialX, initialY; const originalContents = new Map(); // 状态重置功能 function resetState() { if (decodeBtn.dataset.mode === 'restore') { restoreOriginalContent(); decodeBtn.textContent = '解析本页Base64'; decodeBtn.dataset.mode = 'decode'; originalContents.clear(); } } // 页面导航事件监听 window.addEventListener('popstate', resetState); window.addEventListener('turbo:render', resetState); window.addEventListener('discourse:before-auto-refresh', resetState); // 状态持久化 window.addEventListener('unload', () => { GM_setValue('lastState', { isDecoded: decodeBtn.dataset.mode === 'restore', position: uiContainer.getBoundingClientRect(), originalContents: Array.from(originalContents.entries()) }); }); // 初始化位置和状态 const initPosition = () => { const saved = GM_getValue('lastState'); const pos = saved?.position || GM_getValue('btnPosition'); if (pos) { const maxX = window.innerWidth - uiContainer.offsetWidth - 20; const maxY = window.innerHeight - uiContainer.offsetHeight - 20; uiContainer.style.left = `${Math.min(Math.max(pos.x, 20), maxX)}px`; uiContainer.style.top = `${Math.min(Math.max(pos.y, 20), maxY)}px`; uiContainer.style.right = 'unset'; uiContainer.style.bottom = 'unset'; } else { uiContainer.style.right = '20px'; uiContainer.style.bottom = '20px'; } if (saved?.isDecoded) { originalContents.clear(); saved.originalContents.forEach(([element, html]) => { originalContents.set(element, html); }); decodeBtn.textContent = '恢复本页Base64'; decodeBtn.dataset.mode = 'restore'; } }; initPosition(); // 事件监听 mainBtn.addEventListener('click', function(e) { e.stopPropagation(); menuVisible = !menuVisible; menu.style.display = menuVisible ? 'block' : 'none'; }); document.addEventListener('click', function(e) { if (menuVisible && !shadowRoot.contains(e.target)) { menuVisible = false; menu.style.display = 'none'; } }); // 拖拽功能 mainBtn.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); function startDrag(e) { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = uiContainer.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; uiContainer.style.userSelect = 'none'; } function drag(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const newX = initialX + dx; const newY = initialY + dy; const pos = checkPosition(newX, newY); uiContainer.style.left = `${pos.x}px`; uiContainer.style.top = `${pos.y}px`; uiContainer.style.right = 'unset'; uiContainer.style.bottom = 'unset'; } function stopDrag() { isDragging = false; uiContainer.style.userSelect = 'auto'; const rect = uiContainer.getBoundingClientRect(); GM_setValue('btnPosition', { x: rect.left, y: rect.top }); } function checkPosition(x, y) { return { x: Math.max(20, Math.min(x, window.innerWidth - uiContainer.offsetWidth - 20)), y: Math.max(20, Math.min(y, window.innerHeight - uiContainer.offsetHeight - 20)) }; } // 解析功能 decodeBtn.addEventListener('click', function() { if (this.dataset.mode === 'restore') { restoreOriginalContent(); this.textContent = '解析本页Base64'; this.dataset.mode = 'decode'; showNotification('已恢复原始内容', 'success'); menu.style.display = 'none'; return; } originalContents.clear(); let hasValidBase64 = false; try { document.querySelectorAll('.cooked, .post-body').forEach(element => { const regex = /(?<!\w)([A-Za-z0-9+/]{4,}?={0,2})(?!\w)/g; let newHtml = element.innerHTML; let modified = false; Array.from(newHtml.matchAll(regex)).reverse().forEach(match => { const original = match[0]; if (!validateBase64(original)) return; try { const decoded = decodeBase64(original); originalContents.set(element, element.innerHTML); newHtml = newHtml.substring(0, match.index) + `<span class="decoded-text">${decoded}</span>` + newHtml.substring(match.index + original.length); hasValidBase64 = modified = true; } catch(e) {} }); if (modified) element.innerHTML = newHtml; }); if (!hasValidBase64) { showNotification('本页未发现有效Base64内容', 'info'); originalContents.clear(); return; } document.querySelectorAll('.decoded-text').forEach(el => { el.addEventListener('click', copyToClipboard); }); decodeBtn.textContent = '恢复本页Base64'; decodeBtn.dataset.mode = 'restore'; showNotification('解析完成', 'success'); } catch (e) { showNotification('解析失败: ' + e.message, 'error'); originalContents.clear(); } menuVisible = false; menu.style.display = 'none'; }); // 编码功能 encodeBtn.addEventListener('click', function() { const text = prompt('请输入要编码的文本:'); if (text === null) return; try { const encoded = encodeBase64(text); GM_setClipboard(encoded); showNotification('Base64已复制', 'success'); } catch (e) { showNotification('编码失败: ' + e.message, 'error'); } menu.style.display = 'none'; }); // 工具函数 function validateBase64(str) { return str.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(str) && !(str.includes('=') && !/==?$/.test(str)); } function decodeBase64(str) { return decodeURIComponent(escape(atob(str))); } function encodeBase64(str) { return btoa(unescape(encodeURIComponent(str))); } function restoreOriginalContent() { originalContents.forEach((html, element) => { element.innerHTML = html; }); originalContents.clear(); } function copyToClipboard(e) { GM_setClipboard(e.target.innerText); showNotification('内容已复制', 'success'); e.stopPropagation(); } // 通知系统 function showNotification(text, type) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 6px; background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'}; color: white; z-index: 2147483647; animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); font-family: system-ui, -apple-system, sans-serif; pointer-events: none; `; notification.textContent = text; document.body.appendChild(notification); setTimeout(() => notification.remove(), 2300); } // 防冲突处理 if (window.hasBase64Helper) return; window.hasBase64Helper = true; // 窗口大小变化处理 window.addEventListener('resize', () => { const rect = uiContainer.getBoundingClientRect(); const pos = checkPosition(rect.left, rect.top); uiContainer.style.left = `${pos.x}px`; uiContainer.style.top = `${pos.y}px`; }); })();