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