Greasy Fork

Greasy Fork is available in English.

Websites Base64 Helper

Base64编解码工具 for all websites

当前为 2025-04-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.25
// @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);
                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;
                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();

			// 添加缓存对象
			this.base64Cache = new Map();
			this.MAX_TEXT_LENGTH = 10000; // 最大文本长度限制
		}

		// 添加正则常量
		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 || text.length > this.MAX_TEXT_LENGTH) return false;

			// 优化可打印字符检查
			let printableCount = 0;
			for (let i = 0; i < text.length; i++) {
				const char = text.charCodeAt(i);
				if (char >= 0x20 && char <= 0x7E) {
					printableCount++;
				}
			}
			return printableCount / 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;

			// 检查缓存
			if (this.base64Cache.has(str)) {
				return this.base64Cache.get(str);
			}

			const patterns = Base64Helper.URL_PATTERNS.DOMAIN_PATTERNS;

			// 合并正则检查
			const isValid = !(
				patterns.POPULAR_SITES.test(str) ||
				patterns.VIDEO_SITES.test(str) ||
				patterns.CN_SITES.test(str) ||
				patterns.TLD.test(str)
			) && 
			/^[A-Za-z0-9+/]*={0,2}$/.test(str) &&
			str.length % 4 === 0 &&
			(!str.includes('=') || str.endsWith('=')) &&
			(!str.includes('==') || str.endsWith('==')) &&
			str.replace(/=+$/, '').length >= 8;

			// 缓存结果
			this.base64Cache.set(str, isValid);
			return isValid;
		}

		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;
			}
		}

		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;
			}
		}

		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 }
	);
})();