// ==UserScript== // @name Websites Base64 Helper // @icon https://raw.githubusercontent.com/XavierBar/Discourse-Base64-Helper/refs/heads/main/discourse.svg // @namespace http://tampermonkey.net/ // @version 1.4.0 // @description Base64编解码工具 for all websites // @author Xavier // @match *://*/* // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 常量定义 const Z_INDEX = 2147483647; const SELECTORS = { POST_CONTENT: 'body', // 修改为扫描整个页面 DECODED_TEXT: '.decoded-text', }; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition', }; const BASE64_REGEX = /(? { 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); }; const routeEvents = [ { event: 'popstate', target: window }, { event: 'hashchange', target: window }, { event: 'DOMContentLoaded', target: window }, ]; routeEvents.forEach(({ event, target }) => { target.addEventListener(event, this.handleRouteChange); this.eventListeners.push({ element: target, event, handler: this.handleRouteChange, }); }); 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') { // 直接使用原有的 restoreContent 方法 this.restoreContent(); return; } let hasValidBase64 = false; try { const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, null, false ); let nodesToReplace = []; while (walker.nextNode()) { const node = walker.currentNode; const text = node.nodeValue; if (!text?.trim()) continue; const matches = Array.from(text.matchAll(BASE64_REGEX)); if (!matches.length) continue; let modified = false; let newHtml = text; for (const match of matches.reverse()) { const original = match[0]; if (!this.validateBase64(original)) continue; try { const decoded = this.decodeBase64(original); newHtml = `${newHtml.substring( 0, match.index )}${decoded}${newHtml.substring( match.index + original.length )}`; modified = true; hasValidBase64 = true; } catch {} } if (modified) { nodesToReplace.push({ node, newHtml }); } } // 一次性替换所有节点 nodesToReplace.forEach(({ node, newHtml }) => { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); }); if (!hasValidBase64) { this.showNotification('本页未发现有效 Base64 内容', 'info'); return; } // 添加点击复制功能 document.querySelectorAll('.decoded-text').forEach((el) => { el.addEventListener('click', (e) => { GM_setClipboard(e.target.textContent); this.showNotification('已复制文本内容', 'success'); e.stopPropagation(); }); }); this.decodeBtn.textContent = '恢复本页 Base64'; this.decodeBtn.dataset.mode = 'restore'; this.showNotification('解析完成,点击文本可复制', 'success'); } catch (e) { this.showNotification(`解析失败: ${e.message}`, 'error'); } 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() { document.querySelectorAll('.decoded-text').forEach((el) => { const textNode = document.createTextNode(el.dataset.original); el.parentNode.replaceChild(textNode, el); }); this.originalContents.clear(); this.decodeBtn.textContent = '解析本页 Base64'; this.decodeBtn.dataset.mode = 'decode'; this.showNotification('已恢复原始内容', 'success'); this.menu.style.display = 'none'; } 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; }); })();