您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Base64编解码工具 for all websites
当前为
// ==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 // ==/UserScript== (function () { ('use strict'); // 常量定义 const Z_INDEX = 2147483647; 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: 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); position: relative; height: auto; max-height: 100px; } .base64-notification.fade-out { animation: slideUpOut 0.3s 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; 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() { // 确保只在主文档中创建实例 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 )}<span class="decoded-text" title="点击复制" data-original="${original}">${decoded}</span>${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 } ); })();