您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base64编解码工具 for Discourse论坛
// ==UserScript== // @name Discourse Base64 Helper // @icon https://raw.githubusercontent.com/XavierBar/Discourse-Base64-Helper/refs/heads/main/discourse.svg // @namespace http://tampermonkey.net/ // @version 1.3.13 // @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.eventListeners = []; // 用于存储事件监听器以便后续清理 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() { const listeners = [ { element: this.mainBtn, event: 'click', handler: (e) => this.toggleMenu(e), }, { element: document, event: 'click', handler: (e) => this.handleDocumentClick(e), }, { element: this.mainBtn, event: 'mousedown', handler: (e) => this.startDrag(e), }, { element: document, event: 'mousemove', handler: (e) => this.drag(e) }, { element: document, event: 'mouseup', handler: () => this.stopDrag() }, { element: this.decodeBtn, event: 'click', handler: () => this.handleDecode(), }, { element: this.encodeBtn, event: 'click', handler: () => this.handleEncode(), }, { element: window, event: 'resize', handler: () => this.handleResize(), }, ]; listeners.forEach(({ element, event, handler }) => { element.addEventListener(event, handler); this.eventListeners.push({ element, event, handler }); }); } // 清理事件监听器和全局引用 destroy() { // 清理所有事件监听器 this.eventListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); this.eventListeners = []; // 清理全局引用 if (window.__base64HelperInstance === this) { delete window.__base64HelperInstance; } // 清理 Shadow DOM 和其他 DOM 引用 if (this.container?.parentNode) { this.container.parentNode.removeChild(this.container); } history.pushState = this.originalPushState; // 恢复原始方法 history.replaceState = this.originalReplaceState; // 恢复原始方法 //清理 resize 定时器 clearTimeout(this.resizeTimer); clearTimeout(this.notificationTimer); // 清理通知定时器 clearTimeout(this.routeTimer); // 清理路由定时器 } // 菜单切换 toggleMenu(e) { if (this.clickDebounce) return; this.clickDebounce = true; setTimeout(() => (this.clickDebounce = false), 200); // 防抖 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; requestAnimationFrame(() => { // 🎯 使用动画帧优化 // 位置计算逻辑 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() { this.handleRouteChange = () => { clearTimeout(this.routeTimer); this.routeTimer = setTimeout(() => this.resetState(), 100); // 延迟 100ms 确保 DOM 更新完成 }; const routeEvents = [ // 原生事件必须绑定到 window { event: 'popstate', target: window }, { event: 'hashchange', target: window }, // Discourse自定义事件必须绑定到 document { event: 'routeDidChange', target: document }, { event: 'post:added', target: document }, { event: 'posts:inserted', target: document }, { event: 'post:highlighted', target: document }, { event: 'topic:refreshed', target: document }, { event: 'discourse:changed', target: document }, { event: 'post-stream:posted', target: document }, { event: 'post-stream:refresh', target: document }, { event: 'composer:opened', target: document }, ]; routeEvents.forEach(({ event, target }) => { target.addEventListener(event, this.handleRouteChange); this.eventListeners.push({ element: target, event, handler: this.handleRouteChange, }); }); // 重写 history 方法 this.originalPushState = history.pushState; this.originalReplaceState = history.replaceState; history.pushState = (...args) => { this.originalPushState.apply(history, args); this.handleRouteChange(); }; history.replaceState = (...args) => { this.originalReplaceState.apply(history, args); this.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 {} }); 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( atob(str) .split('') .map((c) => `%${c.charCodeAt(0).toString(16).padStart(2, '0')}`) .join('') ); } encodeBase64(str) { return btoa( encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => String.fromCharCode(`0x${p1}`) ) ); } 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); this.notificationTimer = setTimeout(() => notification.remove(), 2300); } } // 防冲突处理 if (window.__base64HelperInstance) { return window.__base64HelperInstance; } // 初始化 initStyles(); const instance = new Base64Helper(); window.__base64HelperInstance = instance; // 页面卸载时清理 window.addEventListener('unload', () => { instance.destroy(); delete window.__base64HelperInstance; }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址