您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base64编解码工具 for Discourse论坛
当前为
// ==UserScript== // @name Discourse Base64 Helper // @namespace http://tampermonkey.net/ // @version 1.3.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'; // 常量定义 const SELECTORS = { POST_CONTENT: '.cooked, .post-body', DECODED_TEXT: '.decoded-text' }; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition' }; const Z_INDEX = 2147483647; const BASE64_REGEX = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g; // 样式初始化 const initStyles = () => { 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; } `); }; 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 = ` :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; } } `; 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.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: ${Z_INDEX}; 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); } } // 初始化 initStyles(); const instance = new Base64Helper(); // 防冲突处理和清理 if (window.__base64HelperInstance) { window.__base64HelperInstance.destroy(); } window.__base64HelperInstance = instance; // 页面卸载时清理 window.addEventListener('unload', () => { instance.destroy(); delete window.__base64HelperInstance; }); })();