// ==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.24 // @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 // @noframes true // @downloadURL none // ==/UserScript== (function () { ('use strict'); // 常量定义 const Z_INDEX = 2147483647; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition', }; const BASE64_REGEX = /(? { GM_addStyle(STYLES.GLOBAL + STYLES.NOTIFICATION); }; class Base64Helper { constructor() { // 确保只在主文档中创建实例 if (window.top !== window.self) { throw new Error( 'Base64Helper can only be instantiated in the main window' ); } this.originalContents = new Map(); this.isDragging = false; this.hasMoved = false; this.startX = 0; this.startY = 0; this.initialX = 0; this.initialY = 0; this.startTime = 0; this.menuVisible = false; this.resizeTimer = null; this.notifications = []; this.notificationContainer = null; this.notificationEventListeners = []; this.initUI(); this.eventListeners = []; this.initEventListeners(); this.addRouteListeners(); } // 添加正则常量 static URL_PATTERNS = { URL: /^(?!.*(?:[a-z0-9-]+\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)))(?:(?:https?|ftp):\/\/)?(?:(?:[\w-]+\.)+[a-z]{2,}|localhost)(?::\d+)?(?:\/[^\s]*)?$/i, EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, DOMAIN_PATTERNS: { POPULAR_SITES: /(?:google|youtube|facebook|twitter|instagram|linkedin|github|gitlab|bitbucket|stackoverflow|reddit|discord|twitch|tiktok|snapchat|pinterest|netflix|amazon|microsoft|apple|adobe)/i, VIDEO_SITES: /(?:bilibili|youku|iqiyi|douyin|kuaishou|nicovideo|vimeo|dailymotion)/i, CN_SITES: /(?:baidu|weibo|zhihu|taobao|tmall|jd|qq|163|sina|sohu|csdn|aliyun|tencent)/i, TLD: /\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)/i, }, }; // UI 初始化 initUI() { if ( window.top !== window.self || 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'; uiContainer.style.cursor = 'grab'; this.mainBtn = this.createButton('Base64', 'main-btn'); this.mainBtn.style.cursor = 'grab'; 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.addUnifiedEventListeners(); this.addGlobalClickListeners(); // 核心编解码事件监听 const commonListeners = [ { element: this.decodeBtn, events: [ { name: 'click', handler: (e) => { e.preventDefault(); e.stopPropagation(); this.handleDecode(); }, }, ], }, { element: this.encodeBtn, events: [ { name: 'click', handler: (e) => { e.preventDefault(); e.stopPropagation(); this.handleEncode(); }, }, ], }, ]; commonListeners.forEach(({ element, events }) => { events.forEach(({ name, handler }) => { element.addEventListener(name, handler, { passive: false }); this.eventListeners.push({ element, event: name, handler }); }); }); } addUnifiedEventListeners() { const ui = this.shadowRoot.querySelector('.base64-helper'); const btn = this.mainBtn; // 统一的开始事件处理 const startHandler = (e) => { e.preventDefault(); e.stopPropagation(); const point = e.touches ? e.touches[0] : e; this.isDragging = true; this.hasMoved = false; this.startX = point.clientX; this.startY = point.clientY; const rect = ui.getBoundingClientRect(); this.initialX = rect.left; this.initialY = rect.top; this.startTime = Date.now(); ui.style.transition = 'none'; ui.classList.add('dragging'); btn.style.cursor = 'grabbing'; }; // 统一的移动事件处理 const moveHandler = (e) => { if (!this.isDragging) return; e.preventDefault(); e.stopPropagation(); const point = e.touches ? e.touches[0] : e; const moveX = Math.abs(point.clientX - this.startX); const moveY = Math.abs(point.clientY - this.startY); if (moveX > 5 || moveY > 5) { this.hasMoved = true; const dx = point.clientX - this.startX; const dy = point.clientY - this.startY; const newX = Math.min( Math.max(20, this.initialX + dx), window.innerWidth - ui.offsetWidth - 20 ); const newY = Math.min( Math.max(20, this.initialY + dy), window.innerHeight - ui.offsetHeight - 20 ); ui.style.left = `${newX}px`; ui.style.top = `${newY}px`; } }; // 统一的结束事件处理 const endHandler = (e) => { if (!this.isDragging) return; e.preventDefault(); e.stopPropagation(); this.isDragging = false; ui.classList.remove('dragging'); btn.style.cursor = 'grab'; ui.style.transition = 'opacity 0.3s ease'; const duration = Date.now() - this.startTime; if (duration < 200 && !this.hasMoved) { this.toggleMenu(e); } else if (this.hasMoved) { const rect = ui.getBoundingClientRect(); const pos = this.positionManager.set(rect.left, rect.top); ui.style.left = `${pos.x}px`; ui.style.top = `${pos.y}px`; } }; // 统一收集所有事件监听器 const listeners = [ { element: ui, event: 'touchstart', handler: startHandler, options: { passive: false }, }, { element: ui, event: 'touchmove', handler: moveHandler, options: { passive: false }, }, { element: ui, event: 'touchend', handler: endHandler, options: { passive: false }, }, { element: ui, event: 'mousedown', handler: startHandler }, { element: document, event: 'mousemove', handler: moveHandler }, { element: document, event: 'mouseup', handler: endHandler }, { element: this.menu, event: 'touchstart', handler: (e) => e.stopPropagation(), options: { passive: false }, }, { element: this.menu, event: 'mousedown', handler: (e) => e.stopPropagation(), }, { element: window, event: 'resize', handler: () => this.handleResize(), }, ]; // 注册事件并保存引用 listeners.forEach(({ element, event, handler, options }) => { element.addEventListener(event, handler, options); this.eventListeners.push({ element, event, handler, options }); }); } toggleMenu(e) { e?.preventDefault(); e?.stopPropagation(); // 如果正在拖动或已移动,不处理菜单切换 if (this.isDragging || this.hasMoved) return; this.menuVisible = !this.menuVisible; this.menu.style.display = this.menuVisible ? 'block' : 'none'; // 重置状态 this.hasMoved = false; } addGlobalClickListeners() { const handleOutsideClick = (e) => { const ui = this.shadowRoot.querySelector('.base64-helper'); const path = e.composedPath(); if (!path.includes(ui) && this.menuVisible) { this.menuVisible = false; this.menu.style.display = 'none'; } }; // 将全局点击事件添加到 eventListeners 数组 const globalListeners = [ { element: document, event: 'click', handler: handleOutsideClick, options: true, }, { element: document, event: 'touchstart', handler: handleOutsideClick, options: { passive: false }, }, ]; globalListeners.forEach(({ element, event, handler, options }) => { element.addEventListener(event, handler, options); this.eventListeners.push({ element, event, handler, options }); }); } // 路由监听 addRouteListeners() { this.handleRouteChange = () => { clearTimeout(this.routeTimer); this.routeTimer = setTimeout(() => this.resetState(), 100); }; // 添加路由相关事件到 eventListeners 数组 const routeListeners = [ { element: window, event: 'popstate', handler: this.handleRouteChange }, { element: window, event: 'hashchange', handler: this.handleRouteChange, }, { element: window, event: 'DOMContentLoaded', handler: this.handleRouteChange, }, ]; routeListeners.forEach(({ element, event, handler }) => { element.addEventListener(event, handler); this.eventListeners.push({ element, event, handler }); }); // 修改 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; } try { const { nodesToReplace, validDecodedCount } = this.processTextNodes(); if (validDecodedCount === 0) { this.showNotification('本页未发现有效 Base64 内容', 'info'); return; } this.replaceNodes(nodesToReplace); this.addClickListenersToDecodedText(); this.decodeBtn.textContent = '恢复本页 Base64'; this.decodeBtn.dataset.mode = 'restore'; this.showNotification( `解析完成,共找到 ${validDecodedCount} 个 Base64 内容`, 'success' ); } catch (e) { console.error('Base64 decode error:', e); this.showNotification(`解析失败: ${e.message}`, 'error'); } this.menuVisible = false; this.menu.style.display = 'none'; } processTextNodes() { const startTime = Date.now(); const TIMEOUT = 5000; const excludeTags = new Set([ 'script', 'style', 'noscript', 'iframe', 'img', 'input', 'textarea', 'svg', 'canvas', 'template', 'pre', 'code', 'button', 'meta', 'link', 'head', 'title', 'select', 'form', 'object', 'embed', 'video', 'audio', 'source', 'track', 'map', 'area', 'math', 'figure', 'picture', 'portal', 'slot', 'data', ]); const excludeAttrs = new Set([ 'src', 'data-src', 'href', 'data-url', 'content', 'background', 'poster', 'data-image', 'srcset', ]); const walker = document.createTreeWalker( document.body, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const isExcludedTag = (parent) => { const tagName = parent.tagName?.toLowerCase(); return excludeTags.has(tagName); }; const isHiddenElement = (parent) => { if (!(parent instanceof HTMLElement)) return false; const style = window.getComputedStyle(parent); return ( style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || style.clipPath === 'inset(100%)' || (style.height === '0px' && style.overflow === 'hidden') ); }; const isOutOfViewport = (parent) => { if (!(parent instanceof HTMLElement)) return false; const rect = parent.getBoundingClientRect(); return rect.width === 0 || rect.height === 0; }; const hasBase64Attributes = (parent) => { if (!parent.hasAttributes()) return false; for (const attr of parent.attributes) { if (excludeAttrs.has(attr.name)) { const value = attr.value.toLowerCase(); if ( value.includes('base64') || value.match(/^[a-z0-9+/=]+$/i) ) { return true; } } } return false; }; let parent = node.parentNode; while (parent && parent !== document.body) { if ( isExcludedTag(parent) || isHiddenElement(parent) || isOutOfViewport(parent) || hasBase64Attributes(parent) ) { return NodeFilter.FILTER_REJECT; } parent = parent.parentNode; } const text = node.textContent?.trim(); if (!text || text.length < 8) { return NodeFilter.FILTER_SKIP; } return /[A-Za-z0-9+/]{6,}/.exec(text) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }, }, false ); let nodesToReplace = []; let processedMatches = new Set(); let validDecodedCount = 0; while (walker.nextNode()) { if (Date.now() - startTime > TIMEOUT) { console.warn('Base64 processing timeout'); break; } const node = walker.currentNode; const { modified, newHtml, count } = this.processMatches( node.nodeValue, processedMatches ); if (modified) { nodesToReplace.push({ node, newHtml }); validDecodedCount += count; } } return { nodesToReplace, validDecodedCount }; } processMatches(text, processedMatches) { const matches = Array.from(text.matchAll(BASE64_REGEX)); if (!matches.length) return { modified: false, newHtml: text, count: 0 }; let modified = false; let newHtml = text; let count = 0; for (const match of matches.reverse()) { const original = match[0]; if (!this.validateBase64(original)) continue; try { const decoded = this.decodeBase64(original); if (!decoded || !this.isValidText(decoded)) continue; const matchId = `${original}-${match.index}`; if (processedMatches.has(matchId)) continue; processedMatches.add(matchId); newHtml = `${newHtml.substring( 0, match.index )}${decoded}${newHtml.substring( match.index + original.length )}`; modified = true; count++; } catch (e) { continue; } } return { modified, newHtml, count }; } isValidText(text) { if (!text || text.length === 0) return false; const printableChars = text.replace(/[^\x20-\x7E]/g, '').length; return printableChars / text.length > 0.5; } replaceNodes(nodesToReplace) { nodesToReplace.forEach(({ node, newHtml }) => { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); }); } addClickListenersToDecodedText() { document.querySelectorAll('.decoded-text').forEach((el) => { el.addEventListener('click', async (e) => { const success = await this.copyToClipboard(e.target.textContent); this.showNotification( success ? '已复制文本内容' : '复制失败,请手动复制', success ? 'success' : 'error' ); e.stopPropagation(); }); }); } async handleEncode() { const text = prompt('请输入要编码的文本:'); if (text === null) return; // 用户点击取消 // 添加空输入检查 if (!text.trim()) { this.showNotification('请输入有效的文本内容', 'error'); return; } try { // 处理输入文本:去除首尾空格和多余的换行符 const processedText = text.trim().replace(/[\r\n]+/g, '\n'); const encoded = this.encodeBase64(processedText); const success = await this.copyToClipboard(encoded); this.showNotification( success ? 'Base64 已复制' : '编码成功但复制失败,请手动复制:' + encoded, success ? 'success' : 'info' ); } catch (e) { this.showNotification('编码失败: ' + e.message, 'error'); } this.menu.style.display = 'none'; } validateBase64(str) { if (!str || str.length < 8 || str.length > 1000) return false; const patterns = Base64Helper.URL_PATTERNS.DOMAIN_PATTERNS; if ( patterns.POPULAR_SITES.test(str) || patterns.VIDEO_SITES.test(str) || patterns.CN_SITES.test(str) || patterns.TLD.test(str) ) { return false; } if (!str.match(/^[A-Za-z0-9+/]*={0,2}$/)) return false; if (str.length % 4 !== 0) return false; if (str.includes('==')) { if (!str.endsWith('==')) return false; } else if (str.includes('=')) { if (!str.endsWith('=')) return false; } return str.replace(/=+$/, '').length >= 8; } 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}`) ) ); } copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { return navigator.clipboard .writeText(text) .then(() => true) .catch(() => this.fallbackCopy(text)); } return this.fallbackCopy(text); } fallbackCopy(text) { if (typeof GM_setClipboard !== 'undefined') { try { GM_setClipboard(text); return true; } catch (e) { console.debug('GM_setClipboard failed:', e); } } try { const textarea = document.createElement('textarea'); textarea.value = text; textarea.style.cssText = 'position:fixed;opacity:0;'; document.body.appendChild(textarea); if (navigator.userAgent.match(/ipad|iphone/i)) { textarea.contentEditable = true; textarea.readOnly = false; const range = document.createRange(); range.selectNodeContents(textarea); const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); textarea.setSelectionRange(0, 999999); } else { textarea.select(); } const success = document.execCommand('copy'); document.body.removeChild(textarea); return success; } catch (e) { console.debug('execCommand copy failed:', e); return false; } } 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(); } } animateNotification(notification, index) { const currentTransform = getComputedStyle(notification).transform; notification.style.transform = currentTransform; notification.style.transition = 'all 0.3s ease-out'; notification.style.transform = 'translateY(-100%)'; } handleNotificationFadeOut(notification) { notification.classList.add('fade-out'); const index = this.notifications.indexOf(notification); this.notifications.slice(0, index).forEach((prev) => { if (prev.parentNode) { prev.style.transform = 'translateY(-100%)'; } }); } cleanupNotificationContainer() { // 清理通知相关的事件监听器 this.notificationEventListeners.forEach(({ element, event, handler }) => { element.removeEventListener(event, handler); }); this.notificationEventListeners = []; // 移除所有通知元素 while (this.notificationContainer.firstChild) { this.notificationContainer.firstChild.remove(); } this.notificationContainer.remove(); this.notificationContainer = null; } handleNotificationTransitionEnd(e) { if ( e.propertyName === 'opacity' && e.target.classList.contains('fade-out') ) { const notification = e.target; const index = this.notifications.indexOf(notification); this.notifications.forEach((notif, i) => { if (i > index && notif.parentNode) { this.animateNotification(notif, i); } }); if (index > -1) { this.notifications.splice(index, 1); notification.remove(); } if (this.notifications.length === 0) { this.cleanupNotificationContainer(); } } } showNotification(text, type) { if (!this.notificationContainer) { this.notificationContainer = document.createElement('div'); this.notificationContainer.className = 'base64-notifications-container'; document.body.appendChild(this.notificationContainer); const handler = (e) => this.handleNotificationTransitionEnd(e); this.notificationContainer.addEventListener('transitionend', handler); this.notificationEventListeners.push({ element: this.notificationContainer, event: 'transitionend', handler, }); } const notification = document.createElement('div'); notification.className = 'base64-notification'; notification.setAttribute('data-type', type); notification.textContent = text; this.notifications.push(notification); this.notificationContainer.appendChild(notification); setTimeout(() => { if (notification.parentNode) { this.handleNotificationFadeOut(notification); } }, 2000); } destroy() { // 清理所有事件监听器 this.eventListeners.forEach(({ element, event, handler, options }) => { element.removeEventListener(event, handler, options); }); this.eventListeners = []; // 清理定时器 if (this.resizeTimer) clearTimeout(this.resizeTimer); if (this.routeTimer) clearTimeout(this.routeTimer); // 清理通知相关资源 if (this.notificationContainer) { this.cleanupNotificationContainer(); } this.notifications = []; // 恢复原始的 history 方法 if (this.originalPushState) history.pushState = this.originalPushState; if (this.originalReplaceState) history.replaceState = this.originalReplaceState; // 恢复原始状态 if (this.decodeBtn?.dataset.mode === 'restore') { this.restoreContent(); } // 移除 DOM 元素 if (this.container) { this.container.remove(); } // 清理引用 this.shadowRoot = null; this.mainBtn = null; this.menu = null; this.decodeBtn = null; this.encodeBtn = null; this.container = null; this.originalContents.clear(); this.originalContents = null; this.isDragging = false; this.hasMoved = false; this.menuVisible = false; } } // 确保只初始化一次 if (window.__base64HelperInstance) { return; } // 只在主窗口中初始化 if (window.top === window.self) { initStyles(); window.__base64HelperInstance = new Base64Helper(); } // 使用 { once: true } 确保事件监听器只添加一次 window.addEventListener( 'unload', () => { if (window.__base64HelperInstance) { window.__base64HelperInstance.destroy(); delete window.__base64HelperInstance; } }, { once: true } ); })();