您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base64编解码工具 for Discourse论坛
当前为
// ==UserScript== // @name Discourse Base64 Helper // @namespace http://tampermonkey.net/ // @version 1.3.4 // @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'; // 常量定义 const Z_INDEX = 2147483647; const SELECTORS = { POST_CONTENT: '.cooked, .post-body', DECODED_TEXT: '.decoded-text' }; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition' }; const BASE64_REGEX = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g; // 样式常量 const STYLES = { GLOBAL: ` /* 基础内容样式 */ .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; } /* 通知动画 */ @keyframes slideIn { from { transform: translate(-50%, -20px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } /* 暗色模式全局样式 */ @media (prefers-color-scheme: dark) { .decoded-text { background-color: #332100 !important; color: #ffd54f !important; } .decoded-text:hover { background-color: #664d03 !important; } } `, NOTIFICATION: ` .base64-notification { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 8px; z-index: ${Z_INDEX}; animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards; font-family: system-ui, -apple-system, sans-serif; pointer-events: none; backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.1); max-width: 80vw; text-align: center; line-height: 1.5; background: rgba(255, 255, 255, 0.95); color: #2d3748; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); } .base64-notification[data-type="success"] { background: rgba(72, 187, 120, 0.95) !important; color: #f7fafc !important; } .base64-notification[data-type="error"] { background: rgba(245, 101, 101, 0.95) !important; color: #f8fafc !important; } .base64-notification[data-type="info"] { background: rgba(66, 153, 225, 0.95) !important; color: #f7fafc !important; } @media (prefers-color-scheme: dark) { .base64-notification { background: rgba(26, 32, 44, 0.95) !important; color: #e2e8f0 !important; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); border-color: rgba(255, 255, 255, 0.05); } .base64-notification[data-type="success"] { background: rgba(22, 101, 52, 0.95) !important; } .base64-notification[data-type="error"] { background: rgba(155, 28, 28, 0.95) !important; } .base64-notification[data-type="info"] { background: rgba(29, 78, 216, 0.95) !important; } } `, SHADOW_DOM: ` :host { all: initial !important; position: fixed !important; z-index: ${Z_INDEX} !important; pointer-events: none !important; } .base64-helper { position: fixed; z-index: ${Z_INDEX} !important; transform: translateZ(100px); cursor: move; font-family: system-ui, -apple-system, sans-serif; opacity: 0.5; transition: opacity 0.3s ease, transform 0.2s; pointer-events: auto !important; will-change: transform; } .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; } .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; 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; } @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; } } ` }; // 样式初始化 const initStyles = () => { GM_addStyle(STYLES.GLOBAL + STYLES.NOTIFICATION); }; class Base64Helper { constructor() { this.originalContents = new Map(); this.isDragging = false; this.menuVisible = false; this.resizeTimer = null; this.initUI(); this.initEventListeners(); this.addRouteListeners(); } // UI 初始化 initUI() { if (document.getElementById('base64-helper-root')) return; this.container = document.createElement('div'); this.container.id = 'base64-helper-root'; document.body.append(this.container); this.shadowRoot = this.container.attachShadow({ mode: 'open' }); this.shadowRoot.appendChild(this.createShadowStyles()); this.shadowRoot.appendChild(this.createMainUI()); this.initPosition(); } createShadowStyles() { const style = document.createElement('style'); style.textContent = STYLES.SHADOW_DOM; return style; } createMainUI() { const uiContainer = document.createElement('div'); uiContainer.className = 'base64-helper'; this.mainBtn = this.createButton('Base64', 'main-btn'); this.menu = this.createMenu(); uiContainer.append(this.mainBtn, this.menu); return uiContainer; } createButton(text, className) { const btn = document.createElement('button'); btn.className = className; btn.textContent = text; return btn; } createMenu() { const menu = document.createElement('div'); menu.className = 'menu'; this.decodeBtn = this.createMenuItem('解析本页 Base64', 'decode'); this.encodeBtn = this.createMenuItem('文本转 Base64'); menu.append(this.decodeBtn, this.encodeBtn); return menu; } createMenuItem(text, mode) { const item = document.createElement('div'); item.className = 'menu-item'; item.textContent = text; if (mode) item.dataset.mode = mode; return item; } // 位置管理 initPosition() { const pos = this.positionManager.get() || { x: window.innerWidth - 120, y: window.innerHeight - 80 }; const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } get positionManager() { return { get: () => { const saved = GM_getValue(STORAGE_KEYS.BUTTON_POSITION); if (!saved) return null; const ui = this.shadowRoot.querySelector('.base64-helper'); const maxX = window.innerWidth - ui.offsetWidth - 20; const maxY = window.innerHeight - ui.offsetHeight - 20; return { x: Math.min(Math.max(saved.x, 20), maxX), y: Math.min(Math.max(saved.y, 20), maxY) }; }, set: (x, y) => { const ui = this.shadowRoot.querySelector('.base64-helper'); const pos = { x: Math.max(20, Math.min(x, window.innerWidth - ui.offsetWidth - 20)), y: Math.max(20, Math.min(y, window.innerHeight - ui.offsetHeight - 20)) }; GM_setValue(STORAGE_KEYS.BUTTON_POSITION, pos); return pos; } }; } // 事件监听 initEventListeners() { this.mainBtn.addEventListener('click', (e) => this.toggleMenu(e)); document.addEventListener('click', (e) => this.handleDocumentClick(e)); // 拖拽事件 this.mainBtn.addEventListener('mousedown', (e) => this.startDrag(e)); document.addEventListener('mousemove', (e) => this.drag(e)); document.addEventListener('mouseup', () => this.stopDrag()); // 功能按钮 this.decodeBtn.addEventListener('click', () => this.handleDecode()); this.encodeBtn.addEventListener('click', () => this.handleEncode()); // 窗口resize window.addEventListener('resize', () => this.handleResize()); } // 菜单切换 toggleMenu(e) { e.stopPropagation(); this.menuVisible = !this.menuVisible; this.menu.style.display = this.menuVisible ? 'block' : 'none'; } handleDocumentClick(e) { if (this.menuVisible && !this.shadowRoot.contains(e.target)) { this.menuVisible = false; this.menu.style.display = 'none'; } } // 拖拽功能 startDrag(e) { this.isDragging = true; this.startX = e.clientX; this.startY = e.clientY; const rect = this.shadowRoot.querySelector('.base64-helper').getBoundingClientRect(); this.initialX = rect.left; this.initialY = rect.top; this.shadowRoot.querySelector('.base64-helper').style.transition = 'none'; } drag(e) { if (!this.isDragging) return; const dx = e.clientX - this.startX; const dy = e.clientY - this.startY; const newX = this.initialX + dx; const newY = this.initialY + dy; const pos = this.positionManager.set(newX, newY); const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } stopDrag() { this.isDragging = false; this.shadowRoot.querySelector('.base64-helper').style.transition = 'opacity 0.3s ease'; } // 窗口resize处理 handleResize() { clearTimeout(this.resizeTimer); this.resizeTimer = setTimeout(() => { const pos = this.positionManager.get(); if (pos) { const ui = this.shadowRoot.querySelector('.base64-helper'); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } }, 100); } // 路由监听 addRouteListeners() { const handleRouteChange = () => { //GM_setValue(STORAGE_KEYS.BUTTON_POSITION, this.positionManager.get()); this.resetState(); }; // 重写history方法 const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = (...args) => { originalPushState.apply(history, args); handleRouteChange(); }; history.replaceState = (...args) => { originalReplaceState.apply(history, args); handleRouteChange(); }; // 事件监听 [ 'popstate', 'hashchange', 'turbo:render', 'discourse:before-auto-refresh', 'page:changed' ].forEach(event => { window.addEventListener(event, handleRouteChange); }); } // 核心功能 handleDecode() { if (this.decodeBtn.dataset.mode === 'restore') { this.restoreContent(); return; } this.originalContents.clear(); let hasValidBase64 = false; try { document.querySelectorAll(SELECTORS.POST_CONTENT).forEach(element => { let newHtml = element.innerHTML; let modified = false; Array.from(newHtml.matchAll(BASE64_REGEX)).reverse().forEach(match => { const original = match[0]; if (!this.validateBase64(original)) return; try { const decoded = this.decodeBase64(original); this.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) { this.showNotification('本页未发现有效 Base64 内容', 'info'); this.originalContents.clear(); return; } document.querySelectorAll(SELECTORS.DECODED_TEXT).forEach(el => { el.addEventListener('click', (e) => this.copyToClipboard(e)); }); this.decodeBtn.textContent = '恢复本页 Base64'; this.decodeBtn.dataset.mode = 'restore'; this.showNotification('解析完成', 'success'); } catch (e) { this.showNotification('解析失败: ' + e.message, 'error'); this.originalContents.clear(); } this.menuVisible = false; this.menu.style.display = 'none'; } handleEncode() { const text = prompt('请输入要编码的文本:'); if (text === null) return; try { const encoded = this.encodeBase64(text); GM_setClipboard(encoded); this.showNotification('Base64 已复制', 'success'); } catch (e) { this.showNotification('编码失败: ' + e.message, 'error'); } this.menu.style.display = 'none'; } // 工具方法 validateBase64(str) { return typeof str === 'string' && str.length >= 6 && str.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(str) && str.replace(/=+$/, '').length >= 6; } decodeBase64(str) { return decodeURIComponent(escape(atob(str))); } encodeBase64(str) { return btoa(unescape(encodeURIComponent(str))); } restoreContent() { this.originalContents.forEach((html, element) => { element.innerHTML = html; }); this.originalContents.clear(); this.decodeBtn.textContent = '解析本页 Base64'; this.decodeBtn.dataset.mode = 'decode'; this.showNotification('已恢复原始内容', 'success'); this.menu.style.display = 'none'; } copyToClipboard(e) { GM_setClipboard(e.target.innerText); this.showNotification('内容已复制', 'success'); e.stopPropagation(); } resetState() { if (this.decodeBtn.dataset.mode === 'restore') { this.restoreContent(); } } showNotification(text, type) { const notification = document.createElement('div'); notification.className = 'base64-notification'; notification.setAttribute('data-type', type); notification.textContent = text; document.body.appendChild(notification); setTimeout(() => notification.remove(), 2300); } } // 初始化 initStyles(); const instance = new Base64Helper(); // 防冲突处理和清理 if (window.__base64HelperInstance) { window.__base64HelperInstance.destroy(); } window.__base64HelperInstance = instance; // 页面卸载时清理 window.addEventListener('unload', () => { instance.destroy(); delete window.__base64HelperInstance; }); })();