// ==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.41 // @description Base64编解码工具 for all websites // @author Xavier // @match *://*/* // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_addValueChangeListener // @run-at document-idle // @noframes true // @downloadURL none // ==/UserScript== (function () { ('use strict'); // 常量定义 const Z_INDEX = 2147483647; const STORAGE_KEYS = { BUTTON_POSITION: 'btnPosition', SHOW_NOTIFICATION: 'showNotification', HIDE_BUTTON: 'hideButton', AUTO_DECODE: 'autoDecode', }; // 存储管理器 const storageManager = { get: (key, defaultValue) => { try { // 优先从 GM 存储获取 const value = GM_getValue(`base64helper_${key}`); if (value !== undefined) { return value; } // 尝试从 localStorage 迁移数据(兼容旧版本) const localValue = localStorage.getItem(`base64helper_${key}`); if (localValue !== null) { const parsedValue = JSON.parse(localValue); // 迁移数据到 GM 存储 GM_setValue(`base64helper_${key}`, parsedValue); // 清理 localStorage 中的旧数据 localStorage.removeItem(`base64helper_${key}`); return parsedValue; } return defaultValue; } catch (e) { console.error('Error getting value from storage:', e); return defaultValue; } }, set: (key, value) => { try { // 存储到 GM 存储 GM_setValue(`base64helper_${key}`, value); return true; } catch (e) { console.error('Error setting value to storage:', e); return false; } }, // 添加删除方法 remove: (key) => { try { GM_deleteValue(`base64helper_${key}`); return true; } catch (e) { console.error('Error removing value from storage:', e); return false; } }, // 添加监听方法 addChangeListener: (key, callback) => { return GM_addValueChangeListener(`base64helper_${key}`, (_, oldValue, newValue, remote) => { callback(newValue, oldValue, remote); } ); }, // 移除监听方法 removeChangeListener: (listenerId) => { if (listenerId) { GM_removeValueChangeListener(listenerId); } } }; const BASE64_REGEX = /([A-Za-z0-9+/]+={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: translateY(-20px); opacity: 0; } to { transform: translateY(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: ` @keyframes slideUpOut { 0% { transform: translateY(0) scale(1); opacity: 1; } 100% { transform: translateY(-30px) scale(0.95); opacity: 0; } } .base64-notifications-container { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: ${Z_INDEX}; display: flex; flex-direction: column; gap: 0; pointer-events: none; align-items: center; width: fit-content; } .base64-notification { transform-origin: top center; white-space: nowrap; padding: 12px 24px; border-radius: 8px; margin-bottom: 10px; animation: slideIn 0.3s ease forwards; font-family: system-ui, -apple-system, sans-serif; backdrop-filter: blur(4px); border: 1px solid rgba(255, 255, 255, 0.1); 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); opacity: 1; transform: translateY(0); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); will-change: transform, opacity; position: relative; height: auto; max-height: 100px; } .base64-notification.fade-out { animation: slideUpOut 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; margin-bottom: 0 !important; max-height: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; border-width: 0 !important; } .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: drag; 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.dragging { cursor: grabbing; } .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: drag; border: none !important; } .main-btn.dragging { cursor: grabbing; } .menu { position: absolute; 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.popup-top { bottom: calc(100% + 5px); } .menu.popup-bottom { top: calc(100% + 5px); } /* 新增: 左对齐样式 */ .menu.align-left { left: 0; } .menu.align-left .menu-item { text-align: left; } /* 新增: 右对齐样式 */ .menu.align-right { right: 0; } .menu.align-right .menu-item { text-align: right; } .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; // 新增以下样式防止文本被选中 user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; } .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); }; // 全局变量存储所有菜单命令ID let menuIds = { decode: null, encode: null, reset: null, notification: null, hideButton: null, autoDecode: null }; // 更新菜单命令 const updateMenuCommands = () => { // 取消注册所有菜单命令 Object.values(menuIds).forEach(id => { if (id !== null) { try { GM_unregisterMenuCommand(id); } catch (e) { console.error('Failed to unregister menu command:', e); } } }); // 重置菜单ID对象 menuIds = { decode: null, encode: null, reset: null, notification: null, hideButton: null, autoDecode: null }; // 检查当前状态,决定解析菜单文本 const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0; const decodeMenuText = hasDecodedContent ? '恢复本页 Base64' : '解析本页 Base64'; // 注册解析菜单命令 - 放在第一位 try { menuIds.decode = GM_registerMenuCommand(decodeMenuText, () => { if (window.__base64HelperInstance) { window.__base64HelperInstance.handleDecode(); } }); console.log('Registered decode menu command with ID:', menuIds.decode); } catch (e) { console.error('Failed to register decode menu command:', e); } // 文本转 Base64 try { menuIds.encode = GM_registerMenuCommand('文本转 Base64', () => { if (window.__base64HelperInstance) window.__base64HelperInstance.handleEncode(); }); console.log('Registered encode menu command with ID:', menuIds.encode); } catch (e) { console.error('Failed to register encode menu command:', e); } // 重置按钮位置 try { menuIds.reset = GM_registerMenuCommand('重置按钮位置', () => { if (window.__base64HelperInstance) { // 使用 storageManager 存储按钮位置 storageManager.set(STORAGE_KEYS.BUTTON_POSITION, { x: window.innerWidth - 120, y: window.innerHeight - 80, }); window.__base64HelperInstance.initPosition(); window.__base64HelperInstance.showNotification('按钮位置已重置', 'success'); } }); console.log('Registered reset menu command with ID:', menuIds.reset); } catch (e) { console.error('Failed to register reset menu command:', e); } // 显示解析通知开关 const showNotificationEnabled = storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true); try { menuIds.notification = GM_registerMenuCommand(`${showNotificationEnabled ? '✅' : '❌'} 显示通知`, () => { const newValue = !showNotificationEnabled; storageManager.set(STORAGE_KEYS.SHOW_NOTIFICATION, newValue); // 使用通知提示用户设置已更改 if (window.__base64HelperInstance) { window.__base64HelperInstance.showNotification( `显示通知已${newValue ? '开启' : '关闭'}`, 'success' ); } // 更新菜单文本 setTimeout(updateMenuCommands, 100); }); console.log('Registered notification menu command with ID:', menuIds.notification); } catch (e) { console.error('Failed to register notification menu command:', e); } // 隐藏按钮开关 const hideButtonEnabled = storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false); try { menuIds.hideButton = GM_registerMenuCommand(`${hideButtonEnabled ? '✅' : '❌'} 隐藏按钮`, () => { const newValue = !hideButtonEnabled; storageManager.set(STORAGE_KEYS.HIDE_BUTTON, newValue); // 使用通知提示用户设置已更改 if (window.__base64HelperInstance) { window.__base64HelperInstance.showNotification( `按钮已${newValue ? '隐藏' : '显示'}`, 'success' ); } // 更新菜单文本 setTimeout(updateMenuCommands, 100); }); console.log('Registered hideButton menu command with ID:', menuIds.hideButton); } catch (e) { console.error('Failed to register hideButton menu command:', e); } // 自动解码开关 const autoDecodeEnabled = storageManager.get(STORAGE_KEYS.AUTO_DECODE, false); try { menuIds.autoDecode = GM_registerMenuCommand(`${autoDecodeEnabled ? '✅' : '❌'} 自动解码`, () => { const newValue = !autoDecodeEnabled; storageManager.set(STORAGE_KEYS.AUTO_DECODE, newValue); // 如果启用自动解码但按钮未隐藏,提示用户 if (newValue && !storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false)) { if (confirm('建议同时隐藏按钮以获得更好的体验。\n\n是否同时隐藏按钮?')) { storageManager.set(STORAGE_KEYS.HIDE_BUTTON, true); } } // 使用通知提示用户设置已更改 if (window.__base64HelperInstance) { window.__base64HelperInstance.showNotification( `自动解码已${newValue ? '开启' : '关闭'}`, 'success' ); } // 更新菜单文本 setTimeout(updateMenuCommands, 100); }); console.log('Registered autoDecode menu command with ID:', menuIds.autoDecode); } catch (e) { console.error('Failed to register autoDecode menu command:', e); } }; // 菜单命令注册 const registerMenuCommands = () => { // 注册所有菜单命令 updateMenuCommands(); }; class Base64Helper { /** * Base64 Helper 类的构造函数 * @description 初始化所有必要的状态和UI组件,仅在主窗口中创建实例 * @throws {Error} 当在非主窗口中实例化时抛出错误 */ constructor() { // 确保只在主文档中创建实例 if (window.top !== window.self) { throw new Error( 'Base64Helper can only be instantiated in the main window' ); } // 初始化配置 this.config = { showNotification: storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true), hideButton: storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false), autoDecode: storageManager.get(STORAGE_KEYS.AUTO_DECODE, false) }; 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.eventListeners = []; // 添加缓存对象 this.base64Cache = new Map(); this.MAX_CACHE_SIZE = 1000; // 最大缓存条目数 this.MAX_TEXT_LENGTH = 10000; // 最大文本长度限制 // 初始化配置监听器 this.configListeners = { showNotification: null, hideButton: null, autoDecode: null, buttonPosition: null }; // 添加配置监听 this.setupConfigListeners(); // 初始化UI this.initUI(); this.initEventListeners(); this.addRouteListeners(); // 如果启用了自动解码,则自动解析页面 if (this.config.autoDecode) { // 使用延时确保页面已完全加载 setTimeout(() => this.handleDecode(), 1000); } } /** * 设置配置监听器 * @description 为各个配置项添加监听器,实现配置变更的实时响应 */ setupConfigListeners() { // 清理现有监听器 Object.values(this.configListeners).forEach(listenerId => { if (listenerId) { storageManager.removeChangeListener(listenerId); } }); // 监听显示通知设置变更 this.configListeners.showNotification = storageManager.addChangeListener( STORAGE_KEYS.SHOW_NOTIFICATION, (newValue) => { console.log('显示通知设置已更改:', newValue); this.config.showNotification = newValue; } ); // 监听隐藏按钮设置变更 this.configListeners.hideButton = storageManager.addChangeListener( STORAGE_KEYS.HIDE_BUTTON, (newValue) => { console.log('隐藏按钮设置已更改:', newValue); this.config.hideButton = newValue; // 实时更新UI显示状态 const ui = this.shadowRoot?.querySelector('.base64-helper'); if (ui) { ui.style.display = newValue ? 'none' : 'block'; } } ); // 监听自动解码设置变更 this.configListeners.autoDecode = storageManager.addChangeListener( STORAGE_KEYS.AUTO_DECODE, (newValue) => { console.log('自动解码设置已更改:', newValue); this.config.autoDecode = newValue; // 如果启用了自动解码,立即解析页面 if (newValue) { setTimeout(() => this.handleDecode(), 100); } } ); // 监听按钮位置变更 this.configListeners.buttonPosition = storageManager.addChangeListener( STORAGE_KEYS.BUTTON_POSITION, (newValue) => { console.log('按钮位置已更改:', newValue); // 更新按钮位置 this.initPosition(); } ); } // 添加正则常量 static URL_PATTERNS = { URL: /^(?:(?: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()); // 创建 UI 容器 const uiContainer = document.createElement('div'); uiContainer.className = 'base64-helper'; uiContainer.style.cursor = 'grab'; // 创建按钮和菜单 this.mainBtn = this.createButton('Base64', 'main-btn'); this.menu = this.createMenu(); this.decodeBtn = this.menu.querySelector('[data-mode="decode"]'); this.encodeBtn = this.menu.querySelector('.menu-item:not([data-mode])'); // 添加到 UI 容器 uiContainer.append(this.mainBtn, this.menu); this.shadowRoot.appendChild(uiContainer); // 初始化位置 this.initPosition(); // 如果配置为隐藏按钮,则设置为不可见 if (this.config.hideButton) { uiContainer.style.display = 'none'; } } createShadowStyles() { const style = document.createElement('style'); style.textContent = STYLES.SHADOW_DOM; return style; } // 不再需要 createMainUI 方法,因为我们直接在 initUI 中创建 UI 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`; // 新增: 初始化时更新菜单对齐 this.updateMenuAlignment(); } updateMenuAlignment() { const ui = this.shadowRoot.querySelector('.base64-helper'); const menu = this.menu; const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const uiRect = ui.getBoundingClientRect(); const centerX = uiRect.left + uiRect.width / 2; const centerY = uiRect.top + uiRect.height / 2; // 判断按钮是在页面左半边还是右半边 if (centerX < windowWidth / 2) { // 左对齐 menu.classList.remove('align-right'); menu.classList.add('align-left'); } else { // 右对齐 menu.classList.remove('align-left'); menu.classList.add('align-right'); } // 判断按钮是在页面上半部分还是下半部分 if (centerY < windowHeight / 2) { // 在页面上方,菜单向下弹出 menu.classList.remove('popup-top'); menu.classList.add('popup-bottom'); } else { // 在页面下方,菜单向上弹出 menu.classList.remove('popup-bottom'); menu.classList.add('popup-top'); } } get positionManager() { return { get: () => { // 使用 storageManager 获取按钮位置 const saved = storageManager.get(STORAGE_KEYS.BUTTON_POSITION, null); 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) ), }; // 使用 storageManager 存储按钮位置 storageManager.set(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`; // 新增: 拖动结束后更新菜单对齐 this.updateMenuAlignment(); } }; // 统一收集所有事件监听器 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; if (this.menuVisible) { // 在显示菜单前更新位置 this.updateMenuAlignment(); } 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(); }; } /** * 处理页面中的Base64解码操作 * @description 根据当前模式执行解码或恢复操作 * 如果当前模式是restore则恢复原始内容,否则查找并解码页面中的Base64内容 * @fires showNotification 显示操作结果通知 */ handleDecode() { // 存储当前模式的变量 let currentMode = 'decode'; // 如果按钮存在,使用按钮的模式 if (this.decodeBtn && this.decodeBtn.dataset.mode === 'restore') { currentMode = 'restore'; } // 如果是恢复模式 if (currentMode === 'restore') { this.restoreContent(); return; } try { // 隐藏菜单 if (this.menu && this.menu.style.display !== 'none') { this.menu.style.display = 'none'; this.menuVisible = false; } // 使用 setTimeout 延迟执行以避免界面冻结 setTimeout(() => { try { const { nodesToReplace, validDecodedCount } = this.processTextNodes(); if (validDecodedCount === 0) { this.showNotification('本页未发现有效 Base64 内容', 'info'); this.menuVisible = false; this.menu.style.display = 'none'; return; } // 分批处理节点替换,避免大量 DOM 操作导致界面冻结 const BATCH_SIZE = 50; // 每批处理的节点数 const processNodesBatch = (startIndex) => { const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length); const batch = nodesToReplace.slice(startIndex, endIndex); this.replaceNodes(batch); if (endIndex < nodesToReplace.length) { // 还有更多节点需要处理,安排下一批 setTimeout(() => processNodesBatch(endIndex), 0); } else { // 所有节点处理完成,添加点击监听器 this.addClickListenersToDecodedText(); this.decodeBtn.textContent = '恢复本页 Base64'; this.decodeBtn.dataset.mode = 'restore'; this.showNotification( `解析完成,共找到 ${validDecodedCount} 个 Base64 内容`, 'success' ); // 操作完成后更新菜单命令 setTimeout(updateMenuCommands, 100); } }; // 开始分批处理 processNodesBatch(0); } catch (innerError) { console.error('Base64 decode processing error:', innerError); this.showNotification(`解析失败: ${innerError.message}`, 'error'); this.menuVisible = false; this.menu.style.display = 'none'; } }, 50); // 给浏览器一点时间渲染通知 } catch (e) { console.error('Base64 decode error:', e); this.showNotification(`解析失败: ${e.message}`, 'error'); this.menuVisible = false; this.menu.style.display = 'none'; } } /** * 处理文本节点中的Base64内容 * @description 遍历文档中的文本节点,查找并处理其中的Base64内容 * 注意: 此方法包含性能优化措施,如超时检测和节点过滤 * @returns {Object} 处理结果 * @property {Array} nodesToReplace - 需要替换的节点数组 * @property {number} validDecodedCount - 有效的Base64解码数量 */ 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', 'a', 'base', // 包含href属性的base标签 'param', // object的参数 'applet', // 旧版Java小程序 'frame', // 框架 'frameset', // 框架集 'marquee', // 滚动文本 'time', // 时间标签 'wbr', // 可能的换行符 'bdo', // 文字方向 'dialog', // 对话框 'details', // 详情 'summary', // 摘要 'menu', // 菜单 'menuitem', // 菜单项 '[hidden]', // 隐藏元素 '[aria-hidden="true"]', // 可访问性隐藏 '.base64', // 自定义class '.encoded', // 自定义class ]); const excludeAttrs = new Set([ 'src', 'data-src', 'href', 'data-url', 'content', 'background', 'poster', 'data-image', 'srcset', 'data-background', // 背景图片 'data-thumbnail', // 缩略图 'data-original', // 原始图片 'data-lazy', // 懒加载 'data-defer', // 延迟加载 'data-fallback', // 后备图片 'data-preview', // 预览图 'data-avatar', // 头像 'data-icon', // 图标 'data-base64', // 显式标记的base64 'style', // 内联样式可能包含base64 'integrity', // SRI完整性校验 'crossorigin', // 跨域属性 'rel', // 关系属性 'alt', // 替代文本 'title', // 标题属性 ]); 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) { return NodeFilter.FILTER_SKIP; } return /[A-Za-z0-9+/]+/.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 }; } /** * 处理文本中的Base64匹配项 * @description 查找并处理文本中的Base64编码内容 * @param {string} text - 要处理的文本内容 * @param {Set} processedMatches - 已处理过的匹配项集合 * @returns {Object} 处理结果 * @property {boolean} modified - 文本是否被修改 * @property {string} newHtml - 处理后的HTML内容 * @property {number} count - 处理的Base64数量 */ 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]; // 使用 validateBase64 进行验证 if (!this.validateBase64(original)) { console.log('Skipped: invalid Base64 string'); continue; } try { const decoded = this.decodeBase64(original); console.log('Decoded:', decoded); if (!decoded) { console.log('Skipped: decode failed'); continue; } // 将原始Base64和位置信息添加到已处理集合中,防止重复处理 const matchKey = `${original}-${match.index}`; processedMatches.add(matchKey); // 构建新的HTML内容: // 1. 保留匹配位置之前的内容 const beforeMatch = newHtml.substring(0, match.index); // 2. 插入解码后的内容,包装在span标签中 const decodedSpan = `${decoded}`; // 3. 保留匹配位置之后的内容 const afterMatch = newHtml.substring(match.index + original.length); // 组合新的HTML newHtml = beforeMatch + decodedSpan + afterMatch; // 标记内容已被修改 modified = true; // 增加成功解码计数 count++; // 记录日志 console.log('成功解码: 发现有意义的文本或中文字符'); } catch (e) { console.error('Error processing:', e); continue; } } return { modified, newHtml, count }; } /** * 判断文本是否有意义 * @description 通过一系列规则判断解码后的文本是否具有实际意义 * @param {string} text - 要验证的文本 * @returns {boolean} 如果文本有意义返回true,否则返回false */ isMeaningfulText(text) { // 1. 基本字符检查 if (!text || typeof text !== 'string') return false; // 2. 长度检查 if (text.length < 2 || text.length > 10000) return false; // 3. 文本质量检查 const stats = { printable: 0, // 可打印字符 control: 0, // 控制字符 chinese: 0, // 中文字符 letters: 0, // 英文字母 numbers: 0, // 数字 punctuation: 0, // 标点符号 spaces: 0, // 空格 other: 0, // 其他字符 }; // 统计字符分布 for (let i = 0; i < text.length; i++) { const char = text.charAt(i); const code = text.charCodeAt(i); if (/[\u4E00-\u9FFF]/.test(char)) { stats.chinese++; stats.printable++; } else if (/[a-zA-Z]/.test(char)) { stats.letters++; stats.printable++; } else if (/[0-9]/.test(char)) { stats.numbers++; stats.printable++; } else if (/[\s]/.test(char)) { stats.spaces++; stats.printable++; } else if (/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(char)) { stats.punctuation++; stats.printable++; } else if (code < 32 || code === 127) { stats.control++; } else { stats.other++; } } // 4. 质量评估规则 const totalChars = text.length; const printableRatio = stats.printable / totalChars; const controlRatio = stats.control / totalChars; const meaningfulRatio = (stats.chinese + stats.letters + stats.numbers) / totalChars; // 判断条件: // 1. 可打印字符比例必须大于90% // 2. 控制字符比例必须小于5% // 3. 有意义字符(中文、英文、数字)比例必须大于30% // 4. 空格比例不能过高(小于50%) // 5. 其他字符比例必须很低(小于10%) return ( printableRatio > 0.9 && controlRatio < 0.05 && meaningfulRatio > 0.3 && stats.spaces / totalChars < 0.5 && stats.other / totalChars < 0.1 ); } /** * 替换页面中的节点 * @description 使用新的HTML内容替换原有节点 * @param {Array} nodesToReplace - 需要替换的节点数组 * @param {Node} nodesToReplace[].node - 原始节点 * @param {string} nodesToReplace[].newHtml - 新的HTML内容 */ replaceNodes(nodesToReplace) { nodesToReplace.forEach(({ node, newHtml }) => { const span = document.createElement('span'); span.innerHTML = newHtml; node.parentNode.replaceChild(span, node); }); } /** * 为解码后的文本添加点击复制功能 * @description 为所有解码后的文本元素添加点击事件监听器 * @fires copyToClipboard 点击时触发复制操作 * @fires showNotification 显示复制结果通知 */ 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(); }); }); } /** * 处理文本编码为Base64 * @description 提示用户输入文本并转换为Base64格式 * @async * @fires showNotification 显示编码结果通知 * @fires copyToClipboard 复制编码结果到剪贴板 */ async handleEncode() { // 隐藏菜单 if (this.menu && this.menu.style.display !== 'none') { this.menu.style.display = 'none'; this.menuVisible = false; } 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'); } } /** * 验证Base64字符串 * @description 检查字符串是否为有效的Base64格式 * @param {string} str - 要验证的字符串 * @returns {boolean} 如果是有效的Base64返回true,否则返回false * @example * validateBase64('SGVsbG8gV29ybGQ=') // returns true * validateBase64('Invalid-Base64') // returns false */ validateBase64(str) { if (!str) return false; // 使用缓存避免重复验证 if (this.base64Cache.has(str)) { return this.base64Cache.get(str); } // 检查缓存大小并在必要时清理 if (this.base64Cache.size >= this.MAX_CACHE_SIZE) { // 删除最早添加的缓存项 const oldestKey = this.base64Cache.keys().next().value; this.base64Cache.delete(oldestKey); } // 1. 基本格式检查 // - 长度必须是4的倍数 // - 只允许包含合法的Base64字符 // - =号只能出现在末尾,且最多2个 if ( !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test( str ) ) { this.base64Cache.set(str, false); return false; } // 2. 长度检查 // 过滤掉太短的字符串(至少8个字符)和过长的字符串(最多10000个字符) if (str.length < 8 || str.length > 10000) { this.base64Cache.set(str, false); return false; } // 3. 特征检查 // 过滤掉可能是图片、视频等二进制数据的Base64 if (/^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER)/.test(str)) { this.base64Cache.set(str, false); return false; } // 添加到 validateBase64 方法中 const commonPatterns = { // 常见的二进制数据头部特征 binaryHeaders: /^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER|UEsDB|H4sIA|77u\/|0M8R4)/, // 常见的文件类型标识 fileSignatures: /^(?:UEs|PK|%PDF|GIF8|RIFF|OggS|ID3|ÿØÿ|8BPS)/, // 常见的编码标识 encodingMarkers: /^(?:utf-8|utf-16|base64|quoted-printable|7bit|8bit|binary)/i, // 可疑的URL模式 urlPatterns: /^(?:https?:|ftp:|data:|blob:|file:|ws:|wss:)/i, // 常见的压缩文件头部 compressedHeaders: /^(?:eJw|H4s|Qk1Q|UEsD|N3q8|KLUv)/, }; // 在验证时使用这些模式 if ( commonPatterns.binaryHeaders.test(str) || commonPatterns.fileSignatures.test(str) || commonPatterns.encodingMarkers.test(str) || commonPatterns.urlPatterns.test(str) || commonPatterns.compressedHeaders.test(str) ) { this.base64Cache.set(str, false); return false; } try { const decoded = this.decodeBase64(str); if (!decoded) { this.base64Cache.set(str, false); return false; } // 4. 解码后的文本验证 // 检查解码后的文本是否有意义 if (!this.isMeaningfulText(decoded)) { this.base64Cache.set(str, false); return false; } this.base64Cache.set(str, true); return true; } catch (e) { console.error('Base64 validation error:', e); this.base64Cache.set(str, false); return false; } } /** * Base64解码 * @description 将Base64字符串解码为普通文本 * @param {string} str - 要解码的Base64字符串 * @returns {string|null} 解码后的文本,解码失败时返回null * @example * decodeBase64('SGVsbG8gV29ybGQ=') // returns 'Hello World' */ decodeBase64(str) { try { // 优化解码过程 const binaryStr = atob(str); const bytes = new Uint8Array(binaryStr.length); for (let i = 0; i < binaryStr.length; i++) { bytes[i] = binaryStr.charCodeAt(i); } return new TextDecoder().decode(bytes); } catch (e) { console.error('Base64 decode error:', e); return null; } } /** * Base64编码 * @description 将普通文本编码为Base64格式 * @param {string} str - 要编码的文本 * @returns {string|null} Base64编码后的字符串,编码失败时返回null * @example * encodeBase64('Hello World') // returns 'SGVsbG8gV29ybGQ=' */ encodeBase64(str) { try { // 优化编码过程 const bytes = new TextEncoder().encode(str); let binaryStr = ''; for (let i = 0; i < bytes.length; i++) { binaryStr += String.fromCharCode(bytes[i]); } return btoa(binaryStr); } catch (e) { console.error('Base64 encode error:', e); return null; } } /** * 复制文本到剪贴板 * @description 尝试使用现代API或降级方案将文本复制到剪贴板 * @param {string} text - 要复制的文本 * @returns {Promise} 复制是否成功 * @example * await copyToClipboard('Hello World') // returns true */ async copyToClipboard(text) { if (navigator.clipboard && window.isSecureContext) { try { await navigator.clipboard.writeText(text); return true; } catch (e) { return this.fallbackCopy(text); } } return this.fallbackCopy(text); } /** * 降级复制方案 * @description 当现代复制API不可用时的备选复制方案 * @param {string} text - 要复制的文本 * @returns {boolean} 复制是否成功 * @private */ fallbackCopy(text) { if (typeof GM_setClipboard !== 'undefined') { try { GM_setClipboard(text); return true; } catch (e) { console.debug('GM_setClipboard failed:', e); } } try { // 注意: execCommand 已经被废弃,但作为降级方案仍然有用 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(); } // 使用 try-catch 包裹 execCommand 调用,以防将来完全移除 let success = false; try { // @ts-ignore - 忽略废弃警告 success = document.execCommand('copy'); } catch (copyError) { console.debug('execCommand copy operation failed:', copyError); } document.body.removeChild(textarea); return success; } catch (e) { console.debug('Fallback copy method failed:', e); return false; } } /** * 恢复原始内容 * @description 将所有解码后的内容恢复为原始的Base64格式 * @fires showNotification 显示恢复结果通知 */ restoreContent() { document.querySelectorAll('.decoded-text').forEach((el) => { const textNode = document.createTextNode(el.dataset.original); el.parentNode.replaceChild(textNode, el); }); this.originalContents.clear(); // 如果按钮存在,更新按钮状态 if (this.decodeBtn) { this.decodeBtn.textContent = '解析本页 Base64'; this.decodeBtn.dataset.mode = 'decode'; } this.showNotification('已恢复原始内容', 'success'); // 只有当按钮可见时才隐藏菜单 if (!this.config.hideButton && this.menu) { this.menu.style.display = 'none'; } // 操作完成后更新菜单命令 setTimeout(updateMenuCommands, 100); } /** * 重置插件状态 * @description 重置所有状态变量并在必要时恢复原始内容 * 如果启用了自动解码,则在路由变化后自动解析页面 * @fires restoreContent 如果当前处于restore模式则触发内容恢复 * @fires handleDecode 如果启用了自动解码则触发自动解码 */ resetState() { // 检查是否需要恢复内容 let needRestore = false; // 如果按钮存在,检查按钮状态 if (this.decodeBtn && this.decodeBtn.dataset.mode === 'restore') { needRestore = true; } else { // 如果按钮不存在,检查页面上是否有解码后的内容 needRestore = document.querySelectorAll('.decoded-text').length > 0; } if (needRestore) { this.restoreContent(); } // 如果启用了自动解码,则在路由变化后自动解析页面 if (this.config.autoDecode) { // 使用延时确保页面内容已更新 setTimeout(() => this.handleDecode(), 500); } } /** * 为通知添加动画效果 * @param {HTMLElement} notification - 通知元素 */ animateNotification(notification) { const currentTransform = getComputedStyle(notification).transform; notification.style.transform = currentTransform; notification.style.transition = 'all 0.3s ease-out'; notification.style.transform = 'translateY(-100%)'; } /** * 处理通知淡出效果 * @description 为通知添加淡出效果并处理相关动画 * @param {HTMLElement} notification - 要处理的通知元素 * @fires animateNotification 触发其他通知的位置调整动画 */ 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%)'; } }); } /** * 清理通知容器 * @description 移除所有通知元素和相关事件监听器 * @fires removeEventListener 移除所有通知相关的事件监听器 */ 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; } /** * 处理通知过渡结束事件 * @description 处理通知元素的过渡动画结束后的清理工作 * @param {TransitionEvent} e - 过渡事件对象 * @fires animateNotification 触发其他通知的位置调整 */ 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); } }); if (index > -1) { this.notifications.splice(index, 1); notification.remove(); } if (this.notifications.length === 0) { this.cleanupNotificationContainer(); } } } /** * 显示通知消息 * @description 创建并显示一个通知消息,包含自动消失功能 * @param {string} text - 通知文本内容 * @param {string} type - 通知类型 ('success'|'error'|'info') * @fires handleNotificationFadeOut 触发通知淡出效果 * @example * showNotification('操作成功', 'success') */ showNotification(text, type) { // 如果禁用了通知,则不显示 if (this.config && !this.config.showNotification) { console.log(`[Base64 Helper] ${type}: ${text}`); return; } 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); } /** * 销毁插件实例 * @description 清理所有资源,移除事件监听器,恢复原始状态 * @fires restoreContent 如果需要则恢复原始内容 * @fires removeEventListener 移除所有事件监听器 */ destroy() { // 清理所有事件监听器 this.eventListeners.forEach(({ element, event, handler, options }) => { element.removeEventListener(event, handler, options); }); this.eventListeners = []; // 清理配置监听器 if (this.configListeners) { Object.values(this.configListeners).forEach(listenerId => { if (listenerId) { storageManager.removeChangeListener(listenerId); } }); // 重置配置监听器 this.configListeners = { showNotification: null, hideButton: null, autoDecode: null, buttonPosition: null }; } // 清理定时器 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(); } // 清理缓存 if (this.base64Cache) { this.base64Cache.clear(); } // 清理引用 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; this.base64Cache = null; this.configListeners = null; } } // 确保只初始化一次 if (window.__base64HelperInstance) { return; } // 只在主窗口中初始化 if (window.top === window.self) { initStyles(); window.__base64HelperInstance = new Base64Helper(); // 注册油猴菜单命令 registerMenuCommands(); } // 使用 { once: true } 确保事件监听器只添加一次 window.addEventListener( 'unload', () => { if (window.__base64HelperInstance) { window.__base64HelperInstance.destroy(); delete window.__base64HelperInstance; } }, { once: true } ); })();