您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Base64编解码工具 for Discourse论坛
当前为
// ==UserScript== // @name Discourse Base64 Helper // @namespace http://tampermonkey.net/ // @version 1.2.1 // @description Base64编解码工具 for Discourse论坛 // @author Xavier // @match *://linux.do/* // @match *://clochat.com/* // @grant GM_notification // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // 样式注入 GM_addStyle(` .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; } @media (prefers-color-scheme: dark) { .decoded-text { background-color: #332100 !important; color: #ffd54f !important; } .decoded-text:hover { background-color: #664d03 !important; } } .menu-item[data-mode="restore"] { background: rgba(0, 123, 255, 0.1) !important; } `); // 初始化检测 if (document.getElementById('base64-helper-root')) return; const container = document.createElement('div'); container.id = 'base64-helper-root'; document.body.append(container); const shadowRoot = container.attachShadow({ mode: 'open' }); // 新增:路由监听逻辑 const addRouteListeners = () => { // 监听SPA路由变化 const handleRouteChange = () => { GM_setValue('btnPosition', positionManager.get()); resetState(); }; // 重写history方法 const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); handleRouteChange(); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); handleRouteChange(); }; // 监听各种路由事件 [ 'popstate', 'hashchange', 'turbo:render', 'discourse:before-auto-refresh', 'page:changed' // 针对Turbolinks ].forEach(event => { window.addEventListener(event, handleRouteChange); }); }; // 初始化时添加路由监听 addRouteListeners(); // Shadow DOM样式 const style = document.createElement('style'); style.textContent = ` :host { all: initial !important; position: fixed !important; z-index: 2147483647 !important; pointer-events: none !important; } .base64-helper { position: fixed; z-index: 2147483647 !important; /* 最大合法值 */ transform: translateZ(100px); /* 触发GPU加速 */ cursor: move; 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: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: pointer; border: none !important; pointer-events: auto !important; } .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; pointer-events: auto !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; } .menu-item:first-child:hover::before { border-radius: 6px 6px 0 0; } .menu-item:last-child:hover::before { border-radius: 0 0 6px 6px; } @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; } } @keyframes slideIn { from { top: -50px; opacity: 0; } to { top: 20px; opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } `; shadowRoot.appendChild(style); // 界面元素 const uiContainer = document.createElement('div'); uiContainer.className = 'base64-helper'; const mainBtn = document.createElement('button'); mainBtn.className = 'main-btn'; mainBtn.textContent = 'Base64'; const menu = document.createElement('div'); menu.className = 'menu'; const decodeBtn = document.createElement('div'); decodeBtn.className = 'menu-item'; decodeBtn.textContent = '解析本页Base64'; decodeBtn.dataset.mode = 'decode'; const encodeBtn = document.createElement('div'); encodeBtn.className = 'menu-item'; encodeBtn.textContent = '文本转Base64'; menu.append(decodeBtn, encodeBtn); uiContainer.append(mainBtn, menu); shadowRoot.appendChild(uiContainer); // 核心功能 let menuVisible = false; let isDragging = false; let startX, startY, initialX, initialY; const originalContents = new Map(); // 位置管理 const positionManager = { get: () => { const saved = GM_getValue('btnPosition'); if (!saved) return null; const maxX = window.innerWidth - uiContainer.offsetWidth - 20; const maxY = window.innerHeight - uiContainer.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 pos = { x: Math.max(20, Math.min(x, window.innerWidth - uiContainer.offsetWidth - 20)), y: Math.max(20, Math.min(y, window.innerHeight - uiContainer.offsetHeight - 20)) }; GM_setValue('btnPosition', pos); return pos; } }; // 初始化位置 const initPosition = () => { const pos = positionManager.get() || { x: window.innerWidth - 120, y: window.innerHeight - 80 }; uiContainer.style.left = `${pos.x}px`; uiContainer.style.top = `${pos.y}px`; }; initPosition(); // 状态重置 function resetState() { if (decodeBtn.dataset.mode === 'restore') { restoreOriginalContent(); decodeBtn.textContent = '解析本页Base64'; decodeBtn.dataset.mode = 'decode'; originalContents.clear(); showNotification('导航状态已重置', 'info'); } } // 事件监听 mainBtn.addEventListener('click', function(e) { e.stopPropagation(); menuVisible = !menuVisible; menu.style.display = menuVisible ? 'block' : 'none'; }); document.addEventListener('click', function(e) { if (menuVisible && !shadowRoot.contains(e.target)) { menuVisible = false; menu.style.display = 'none'; } }); // 拖拽功能 mainBtn.addEventListener('mousedown', startDrag); document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag); function startDrag(e) { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = uiContainer.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; uiContainer.style.transition = 'none'; } function drag(e) { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; const newX = initialX + dx; const newY = initialY + dy; const pos = positionManager.set(newX, newY); uiContainer.style.left = `${pos.x}px`; uiContainer.style.top = `${pos.y}px`; } function stopDrag() { isDragging = false; uiContainer.style.transition = 'opacity 0.3s ease'; } // 窗口resize处理 let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { const pos = positionManager.get(); if (pos) { uiContainer.style.left = `${pos.x}px`; uiContainer.style.top = `${pos.y}px`; } }, 100); }); // 页面导航事件 window.addEventListener('popstate', resetState); window.addEventListener('turbo:render', resetState); window.addEventListener('discourse:before-auto-refresh', () => { GM_setValue('btnPosition', positionManager.get()); resetState(); }); // 新增:页面加载时重置状态 document.addEventListener('DOMContentLoaded', resetState); if (document.readyState === 'complete') { resetState(); } // 解析功能 decodeBtn.addEventListener('click', function() { if (this.dataset.mode === 'restore') { restoreOriginalContent(); this.textContent = '解析本页Base64'; this.dataset.mode = 'decode'; showNotification('已恢复原始内容', 'success'); menu.style.display = 'none'; return; } originalContents.clear(); let hasValidBase64 = false; try { document.querySelectorAll('.cooked, .post-body').forEach(element => { const regex = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g; let newHtml = element.innerHTML; let modified = false; Array.from(newHtml.matchAll(regex)).reverse().forEach(match => { const original = match[0]; if (!validateBase64(original)) return; try { const decoded = decodeBase64(original); originalContents.set(element, element.innerHTML); newHtml = newHtml.substring(0, match.index) + `<span class="decoded-text">${decoded}</span>` + newHtml.substring(match.index + original.length); hasValidBase64 = modified = true; } catch(e) {} }); if (modified) element.innerHTML = newHtml; }); if (!hasValidBase64) { showNotification('本页未发现有效Base64内容', 'info'); originalContents.clear(); return; } document.querySelectorAll('.decoded-text').forEach(el => { el.addEventListener('click', copyToClipboard); }); decodeBtn.textContent = '恢复本页Base64'; decodeBtn.dataset.mode = 'restore'; showNotification('解析完成', 'success'); } catch (e) { showNotification('解析失败: ' + e.message, 'error'); originalContents.clear(); } menuVisible = false; menu.style.display = 'none'; }); // 编码功能 encodeBtn.addEventListener('click', function() { const text = prompt('请输入要编码的文本:'); if (text === null) return; try { const encoded = encodeBase64(text); GM_setClipboard(encoded); showNotification('Base64已复制', 'success'); } catch (e) { showNotification('编码失败: ' + e.message, 'error'); } menu.style.display = 'none'; }); // 改进的校验函数 function validateBase64(str) { // 基础校验 const validLength = str.length % 4 === 0; const validChars = /^[A-Za-z0-9+/]+={0,2}$/.test(str); const validPadding = !(str.includes('=') && !/==?$/.test(str)); if (!validLength || !validChars || !validPadding) return false; // 移除填充后的校验 const baseStr = str.replace(/=+$/, ''); if (baseStr.length < 6) return false; const hasSpecialChar = /[+/0-9]/.test(baseStr); return hasSpecialChar; } // 工具函数 function decodeBase64(str) { return decodeURIComponent(escape(atob(str))); } function encodeBase64(str) { return btoa(unescape(encodeURIComponent(str))); } function restoreOriginalContent() { originalContents.forEach((html, element) => { element.innerHTML = html; }); originalContents.clear(); } function copyToClipboard(e) { GM_setClipboard(e.target.innerText); showNotification('内容已复制', 'success'); e.stopPropagation(); } // 通知系统 function showNotification(text, type) { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 6px; background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : '#2196F3'}; color: white; z-index: 2147483647; animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); font-family: system-ui, -apple-system, sans-serif; pointer-events: none; `; notification.textContent = text; document.body.appendChild(notification); setTimeout(() => notification.remove(), 2300); } // 防冲突处理 if (window.hasBase64Helper) return; window.hasBase64Helper = true; })();