Websites Base64 Helper

Base64编解码工具 for all websites

  1. // ==UserScript==
  2. // @name Websites Base64 Helper
  3. // @icon https://raw.githubusercontent.com/XavierBar/Discourse-Base64-Helper/refs/heads/main/discourse.svg
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.4.60
  6. // @description Base64编解码工具 for all websites
  7. // @author Xavier
  8. // @match *://*/*
  9. // @grant GM_notification
  10. // @grant GM_setClipboard
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_registerMenuCommand
  15. // @grant GM_unregisterMenuCommand
  16. // @grant GM_addValueChangeListener
  17. // @run-at document-idle
  18. // @noframes true
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. ('use strict');
  23.  
  24. // 常量定义
  25. const Z_INDEX = 2147483647;
  26. const STORAGE_KEYS = {
  27. BUTTON_POSITION: 'btnPosition',
  28. SHOW_NOTIFICATION: 'showNotification',
  29. HIDE_BUTTON: 'hideButton',
  30. AUTO_DECODE: 'autoDecode',
  31. };
  32.  
  33. // 存储管理器
  34. const storageManager = {
  35. get: (key, defaultValue) => {
  36. try {
  37. // 优先从 GM 存储获取
  38. const value = GM_getValue(`base64helper_${key}`);
  39. if (value !== undefined) {
  40. return value;
  41. }
  42.  
  43. // 尝试从 localStorage 迁移数据(兼容旧版本)
  44. const localValue = localStorage.getItem(`base64helper_${key}`);
  45. if (localValue !== null) {
  46. const parsedValue = JSON.parse(localValue);
  47. // 迁移数据到 GM 存储
  48. GM_setValue(`base64helper_${key}`, parsedValue);
  49. // 清理 localStorage 中的旧数据
  50. localStorage.removeItem(`base64helper_${key}`);
  51. return parsedValue;
  52. }
  53.  
  54. return defaultValue;
  55. } catch (e) {
  56. console.error('Error getting value from storage:', e);
  57. return defaultValue;
  58. }
  59. },
  60. set: (key, value) => {
  61. try {
  62. // 存储到 GM 存储
  63. GM_setValue(`base64helper_${key}`, value);
  64. return true;
  65. } catch (e) {
  66. console.error('Error setting value to storage:', e);
  67. return false;
  68. }
  69. },
  70. // 添加删除方法
  71. remove: (key) => {
  72. try {
  73. GM_deleteValue(`base64helper_${key}`);
  74. return true;
  75. } catch (e) {
  76. console.error('Error removing value from storage:', e);
  77. return false;
  78. }
  79. },
  80. // 添加监听方法
  81. addChangeListener: (key, callback) => {
  82. return GM_addValueChangeListener(`base64helper_${key}`,
  83. (_, oldValue, newValue, remote) => {
  84. callback(newValue, oldValue, remote);
  85. }
  86. );
  87. },
  88. // 移除监听方法
  89. removeChangeListener: (listenerId) => {
  90. if (listenerId) {
  91. GM_removeValueChangeListener(listenerId);
  92. }
  93. }
  94. };
  95. const BASE64_REGEX = /([A-Za-z0-9+/]+={0,2})(?!\w)/g;
  96. // 样式常量
  97. const STYLES = {
  98. GLOBAL: `
  99. /* 基础内容样式 */
  100. .decoded-text {
  101. cursor: pointer;
  102. transition: all 0.2s;
  103. padding: 1px 3px;
  104. border-radius: 3px;
  105. background-color: #fff3cd !important;
  106. color: #664d03 !important;
  107. }
  108. .decoded-text:hover {
  109. background-color: #ffe69c !important;
  110. }
  111. /* 通知动画 */
  112. @keyframes slideIn {
  113. from {
  114. transform: translateY(-20px);
  115. opacity: 0;
  116. }
  117. to {
  118. transform: translateY(0);
  119. opacity: 1;
  120. }
  121. }
  122. @keyframes fadeOut {
  123. from { opacity: 1; }
  124. to { opacity: 0; }
  125. }
  126. /* 暗色模式全局样式 */
  127. @media (prefers-color-scheme: dark) {
  128. .decoded-text {
  129. background-color: #332100 !important;
  130. color: #ffd54f !important;
  131. }
  132. .decoded-text:hover {
  133. background-color: #664d03 !important;
  134. }
  135. }
  136. `,
  137. NOTIFICATION: `
  138. @keyframes slideUpOut {
  139. 0% {
  140. transform: translateY(0) scale(1);
  141. opacity: 1;
  142. }
  143. 100% {
  144. transform: translateY(-30px) scale(0.95);
  145. opacity: 0;
  146. }
  147. }
  148. .base64-notifications-container {
  149. position: fixed;
  150. top: 20px;
  151. left: 50%;
  152. transform: translateX(-50%);
  153. z-index: ${Z_INDEX};
  154. display: flex;
  155. flex-direction: column;
  156. gap: 0;
  157. pointer-events: none;
  158. align-items: center;
  159. width: fit-content;
  160. }
  161. .base64-notification {
  162. transform-origin: top center;
  163. white-space: nowrap;
  164. padding: 12px 24px;
  165. border-radius: 8px;
  166. margin-bottom: 10px;
  167. animation: slideIn 0.3s ease forwards;
  168. font-family: system-ui, -apple-system, sans-serif;
  169. backdrop-filter: blur(4px);
  170. border: 1px solid rgba(255, 255, 255, 0.1);
  171. text-align: center;
  172. line-height: 1.5;
  173. background: rgba(255, 255, 255, 0.95);
  174. color: #2d3748;
  175. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  176. opacity: 1;
  177. transform: translateY(0);
  178. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  179. will-change: transform, opacity;
  180. position: relative;
  181. height: auto;
  182. max-height: 100px;
  183. }
  184. .base64-notification.fade-out {
  185. animation: slideUpOut 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
  186. margin-bottom: 0 !important;
  187. max-height: 0 !important;
  188. padding-top: 0 !important;
  189. padding-bottom: 0 !important;
  190. border-width: 0 !important;
  191. }
  192. .base64-notification[data-type="success"] {
  193. background: rgba(72, 187, 120, 0.95) !important;
  194. color: #f7fafc !important;
  195. }
  196. .base64-notification[data-type="error"] {
  197. background: rgba(245, 101, 101, 0.95) !important;
  198. color: #f8fafc !important;
  199. }
  200. .base64-notification[data-type="info"] {
  201. background: rgba(66, 153, 225, 0.95) !important;
  202. color: #f7fafc !important;
  203. }
  204. @media (prefers-color-scheme: dark) {
  205. .base64-notification {
  206. background: rgba(26, 32, 44, 0.95) !important;
  207. color: #e2e8f0 !important;
  208. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
  209. border-color: rgba(255, 255, 255, 0.05);
  210. }
  211. .base64-notification[data-type="success"] {
  212. background: rgba(22, 101, 52, 0.95) !important;
  213. }
  214. .base64-notification[data-type="error"] {
  215. background: rgba(155, 28, 28, 0.95) !important;
  216. }
  217. .base64-notification[data-type="info"] {
  218. background: rgba(29, 78, 216, 0.95) !important;
  219. }
  220. }
  221. `,
  222. SHADOW_DOM: `
  223. :host {
  224. all: initial !important;
  225. position: fixed !important;
  226. z-index: ${Z_INDEX} !important;
  227. pointer-events: none !important;
  228. }
  229. .base64-helper {
  230. position: fixed;
  231. z-index: ${Z_INDEX} !important;
  232. transform: translateZ(100px);
  233. cursor: drag;
  234. font-family: system-ui, -apple-system, sans-serif;
  235. opacity: 0.5;
  236. transition: opacity 0.3s ease, transform 0.2s;
  237. pointer-events: auto !important;
  238. will-change: transform;
  239. }
  240. .base64-helper.dragging {
  241. cursor: grabbing;
  242. }
  243. .base64-helper:hover {
  244. opacity: 1 !important;
  245. }
  246. .main-btn {
  247. background: #ffffff;
  248. color: #000000 !important;
  249. padding: 8px 16px;
  250. border-radius: 6px;
  251. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  252. font-weight: 500;
  253. user-select: none;
  254. transition: all 0.2s;
  255. font-size: 14px;
  256. cursor: drag;
  257. border: none !important;
  258. }
  259. .main-btn.dragging {
  260. cursor: grabbing;
  261. }
  262. .menu {
  263. position: absolute;
  264. background: #ffffff;
  265. border-radius: 6px;
  266. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  267. display: none;
  268. min-width: auto !important;
  269. width: max-content !important;
  270. overflow: hidden;
  271. }
  272.  
  273. /* 菜单弹出方向 */
  274. .menu.popup-top {
  275. bottom: calc(100% + 5px);
  276. }
  277. .menu.popup-bottom {
  278. top: calc(100% + 5px);
  279. }
  280.  
  281. /* 新增: 左对齐样式 */
  282. .menu.align-left {
  283. left: 0;
  284. }
  285. .menu.align-left .menu-item {
  286. text-align: left;
  287. }
  288.  
  289. /* 新增: 右对齐样式 */
  290. .menu.align-right {
  291. right: 0;
  292. }
  293. .menu.align-right .menu-item {
  294. text-align: right;
  295. }
  296. .menu-item {
  297. padding: 8px 12px !important;
  298. color: #333 !important;
  299. transition: all 0.2s;
  300. font-size: 13px;
  301. cursor: pointer;
  302. position: relative;
  303. border-radius: 0 !important;
  304. isolation: isolate;
  305. white-space: nowrap !important;
  306. // 新增以下样式防止文本被选中
  307. user-select: none;
  308. -webkit-user-select: none;
  309. -moz-user-select: none;
  310. -ms-user-select: none;
  311. }
  312. .menu-item:hover::before {
  313. content: '';
  314. position: absolute;
  315. top: 0;
  316. left: 0;
  317. right: 0;
  318. bottom: 0;
  319. background: currentColor;
  320. opacity: 0.1;
  321. z-index: -1;
  322. }
  323. @media (prefers-color-scheme: dark) {
  324. .main-btn {
  325. background: #2d2d2d;
  326. color: #fff !important;
  327. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  328. }
  329. .menu {
  330. background: #1a1a1a;
  331. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  332. }
  333. .menu-item {
  334. color: #e0e0e0 !important;
  335. }
  336. .menu-item:hover::before {
  337. opacity: 0.08;
  338. }
  339. }
  340. `,
  341. };
  342.  
  343. // 样式初始化
  344. const initStyles = () => {
  345. GM_addStyle(STYLES.GLOBAL + STYLES.NOTIFICATION);
  346. };
  347.  
  348. // 全局变量存储所有菜单命令ID
  349. let menuIds = {
  350. decode: null,
  351. encode: null,
  352. reset: null,
  353. notification: null,
  354. hideButton: null,
  355. autoDecode: null
  356. };
  357.  
  358. // 更新菜单命令
  359. const updateMenuCommands = () => {
  360. // 取消注册(不可用)所有菜单命令
  361. Object.values(menuIds).forEach(id => {
  362. if (id !== null) {
  363. try {
  364. GM_unregisterMenuCommand(id);
  365. } catch (e) {
  366. console.error('Failed to unregister menu command:', e);
  367. }
  368. }
  369. });
  370.  
  371. // 重置菜单ID对象
  372. menuIds = {
  373. decode: null,
  374. encode: null,
  375. reset: null,
  376. notification: null,
  377. hideButton: null,
  378. autoDecode: null
  379. };
  380.  
  381. // 检查当前状态,决定解析菜单文本
  382. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  383. const decodeMenuText = hasDecodedContent ? '恢复本页 Base64' : '解析本页 Base64';
  384.  
  385. // 注册(不可用)解析菜单命令 - 放在第一位
  386. try {
  387. menuIds.decode = GM_registerMenuCommand(decodeMenuText, () => {
  388. if (window.__base64HelperInstance) {
  389. // 直接调用实例方法
  390. window.__base64HelperInstance.handleDecode();
  391. // 操作完成后更新菜单命令
  392. setTimeout(updateMenuCommands, 100);
  393. }
  394. });
  395. console.log('Registered decode menu command with ID:', menuIds.decode);
  396. } catch (e) {
  397. console.error('Failed to register decode menu command:', e);
  398. }
  399.  
  400. // 文本转 Base64
  401. try {
  402. menuIds.encode = GM_registerMenuCommand('文本转 Base64', () => {
  403. if (window.__base64HelperInstance) window.__base64HelperInstance.handleEncode();
  404. });
  405. console.log('Registered encode menu command with ID:', menuIds.encode);
  406. } catch (e) {
  407. console.error('Failed to register encode menu command:', e);
  408. }
  409.  
  410. // 重置按钮位置
  411. try {
  412. menuIds.reset = GM_registerMenuCommand('重置按钮位置', () => {
  413. if (window.__base64HelperInstance) {
  414. // 使用 storageManager 存储按钮位置
  415. storageManager.set(STORAGE_KEYS.BUTTON_POSITION, {
  416. x: window.innerWidth - 120,
  417. y: window.innerHeight - 80,
  418. });
  419. window.__base64HelperInstance.initPosition();
  420. window.__base64HelperInstance.showNotification('按钮位置已重置', 'success');
  421. }
  422. });
  423. console.log('Registered reset menu command with ID:', menuIds.reset);
  424. } catch (e) {
  425. console.error('Failed to register reset menu command:', e);
  426. }
  427.  
  428. // 显示解析通知开关
  429. const showNotificationEnabled = storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true);
  430. try {
  431. menuIds.notification = GM_registerMenuCommand(`${showNotificationEnabled ? '✅' : '❌'} 显示通知`, () => {
  432. const newValue = !showNotificationEnabled;
  433. storageManager.set(STORAGE_KEYS.SHOW_NOTIFICATION, newValue);
  434. // 使用通知提示用户设置已更改
  435. if (window.__base64HelperInstance) {
  436. window.__base64HelperInstance.showNotification(
  437. `显示通知已${newValue ? '开启' : '关闭'}`,
  438. 'success'
  439. );
  440. }
  441. // 更新菜单文本
  442. setTimeout(updateMenuCommands, 100);
  443. });
  444. console.log('Registered notification menu command with ID:', menuIds.notification);
  445. } catch (e) {
  446. console.error('Failed to register notification menu command:', e);
  447. }
  448.  
  449. // 隐藏按钮开关
  450. const hideButtonEnabled = storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false);
  451. try {
  452. menuIds.hideButton = GM_registerMenuCommand(`${hideButtonEnabled ? '✅' : '❌'} 隐藏按钮`, () => {
  453. const newValue = !hideButtonEnabled;
  454. storageManager.set(STORAGE_KEYS.HIDE_BUTTON, newValue);
  455. // 使用通知提示用户设置已更改
  456. if (window.__base64HelperInstance) {
  457. window.__base64HelperInstance.showNotification(
  458. `按钮已${newValue ? '隐藏' : '显示'}`,
  459. 'success'
  460. );
  461. }
  462. // 更新菜单文本
  463. setTimeout(updateMenuCommands, 100);
  464. });
  465. console.log('Registered hideButton menu command with ID:', menuIds.hideButton);
  466. } catch (e) {
  467. console.error('Failed to register hideButton menu command:', e);
  468. }
  469.  
  470. // 自动解码开关
  471. const autoDecodeEnabled = storageManager.get(STORAGE_KEYS.AUTO_DECODE, false);
  472. try {
  473. menuIds.autoDecode = GM_registerMenuCommand(`${autoDecodeEnabled ? '✅' : '❌'} 自动解码`, () => {
  474. const newValue = !autoDecodeEnabled;
  475. storageManager.set(STORAGE_KEYS.AUTO_DECODE, newValue);
  476.  
  477. // 如果启用了自动解码,立即解析页面
  478. if (newValue) {
  479. // 检查是否是通过菜单命令触发的变更
  480. // 如果是通过菜单命令触发,则不再显示确认对话框
  481. // 因为菜单命令处理程序中已经处理了这些确认
  482.  
  483. // 立即解析页面
  484. this.hasAutoDecodedOnLoad = true; // 标记已执行过自动解码
  485. setTimeout(() => {
  486. this.handleDecode();
  487. // 同步按钮和菜单状态
  488. setTimeout(() => {
  489. this.syncButtonAndMenuState();
  490. // 更新油猴菜单命令
  491. updateMenuCommands();
  492. }, 200);
  493. }, 100);
  494. } else {
  495. // 如果关闭了自动解码,也需要同步状态
  496. setTimeout(() => {
  497. this.syncButtonAndMenuState();
  498. // 更新油猴菜单命令
  499. updateMenuCommands();
  500. }, 200);
  501. }
  502. });
  503. console.log('Registered autoDecode menu command with ID:', menuIds.autoDecode);
  504. } catch (e) {
  505. console.error('Failed to register autoDecode menu command:', e);
  506. }
  507. };
  508.  
  509. // 菜单命令注册(不可用)
  510. const registerMenuCommands = () => {
  511. // 注册(不可用)所有菜单命令
  512. updateMenuCommands();
  513.  
  514. // 添加 DOMContentLoaded 事件监听器,确保在页面加载完成后注册(不可用)菜单命令
  515. document.addEventListener('DOMContentLoaded', () => {
  516. console.log('DOMContentLoaded 事件触发,更新菜单命令');
  517. updateMenuCommands();
  518. });
  519. };
  520.  
  521. class Base64Helper {
  522. /**
  523. * Base64 Helper 类的构造函数
  524. * @description 初始化所有必要的状态和UI组件,仅在主窗口中创建实例
  525. * @throws {Error} 当在非主窗口中实例化时抛出错误
  526. */
  527. constructor() {
  528. // 确保只在主文档中创建实例
  529. if (window.top !== window.self) {
  530. throw new Error(
  531. 'Base64Helper can only be instantiated in the main window'
  532. );
  533. }
  534.  
  535. // 初始化配置
  536. this.config = {
  537. showNotification: storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true),
  538. hideButton: storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false),
  539. autoDecode: storageManager.get(STORAGE_KEYS.AUTO_DECODE, false)
  540. };
  541.  
  542. this.originalContents = new Map();
  543. this.isDragging = false;
  544. this.hasMoved = false;
  545. this.startX = 0;
  546. this.startY = 0;
  547. this.initialX = 0;
  548. this.initialY = 0;
  549. this.startTime = 0;
  550. this.menuVisible = false;
  551. this.resizeTimer = null;
  552. this.notifications = [];
  553. this.notificationContainer = null;
  554. this.notificationEventListeners = [];
  555. this.eventListeners = new Map(); // 使用 Map 替代数组,便于管理
  556.  
  557. // 添加缓存对象
  558. this.base64Cache = new Map();
  559. this.MAX_CACHE_SIZE = 1000; // 最大缓存条目数
  560. this.MAX_TEXT_LENGTH = 10000; // 最大文本长度限制
  561. this.cacheHits = 0;
  562. this.cacheMisses = 0;
  563.  
  564. // 添加DOM节点处理跟踪
  565. this.processedNodes = new WeakSet(); // 使用WeakSet跟踪已处理的节点,避免内存泄漏
  566. this.decodedTextNodes = new WeakMap(); // 存储节点及其解码状态
  567. this.processedMutations = new Set(); // 跟踪已处理的mutation记录
  568. this.nodeReferences = new WeakMap(); // 存储节点引用
  569.  
  570. // 初始化配置监听器
  571. this.configListeners = {
  572. showNotification: null,
  573. hideButton: null,
  574. autoDecode: null,
  575. buttonPosition: null
  576. };
  577.  
  578. // 添加初始化标志
  579. this.isInitialLoad = true;
  580. this.lastDecodeTime = 0;
  581. this.lastNavigationTime = 0; // 添加前进后退时间记录
  582. this.isShowingNotification = false; // 添加通知显示标志
  583. this.hasAutoDecodedOnLoad = false; // 添加标志,跟踪是否已在页面加载时执行过自动解码
  584. this.isPageRefresh = true; // 添加页面刷新标志,初始加载视为刷新
  585. this.pageRefreshCompleted = false; // 添加页面刷新完成标志
  586. this.isRestoringContent = false; // 添加内容恢复标志
  587. this.isDecodingContent = false; // 添加内容解码标志
  588. this.lastPageUrl = window.location.href; // 记录当前页面URL
  589. this.currentMutations = []; // 存储当前的DOM变化记录
  590. const MIN_DECODE_INTERVAL = 1000; // 最小解码间隔(毫秒)
  591.  
  592. // 初始化统一的页面稳定性跟踪器
  593. this.pageStabilityTracker = {
  594. // 状态管理
  595. lastChangeTime: Date.now(),
  596. changeCount: 0,
  597. isStable: false,
  598. pendingDecode: false,
  599. lastRouteChange: 0,
  600. lastDomChange: 0,
  601. stabilityTimer: null,
  602. decodePendingTimer: null,
  603. stabilityThreshold: 800, // 降低稳定性阈值,从2000ms降低到800ms
  604. maxChangeCount: 5,
  605.  
  606. // 检查稳定性
  607. checkStability() {
  608. const currentTime = Date.now();
  609. return (currentTime - this.lastRouteChange > this.stabilityThreshold) &&
  610. (currentTime - this.lastDomChange > this.stabilityThreshold);
  611. },
  612.  
  613. // 记录变化
  614. recordChange(type) {
  615. const currentTime = Date.now();
  616. this.lastChangeTime = currentTime;
  617.  
  618. // 更新对应类型的最后变化时间
  619. if (type === 'Route') {
  620. this.lastRouteChange = currentTime;
  621. } else if (type === 'Dom') {
  622. this.lastDomChange = currentTime;
  623. }
  624.  
  625. this.isStable = false;
  626. this.changeCount++;
  627.  
  628. if (this.changeCount > this.maxChangeCount) {
  629. this.changeCount = 1;
  630. }
  631.  
  632. // 清除之前的定时器
  633. clearTimeout(this.stabilityTimer);
  634. clearTimeout(this.decodePendingTimer);
  635.  
  636. console.log(`记录${type}变化,重置稳定性定时器`);
  637. },
  638.  
  639. // 重置状态
  640. reset() {
  641. this.changeCount = 0;
  642. this.isStable = false;
  643. this.pendingDecode = false;
  644. clearTimeout(this.stabilityTimer);
  645. clearTimeout(this.decodePendingTimer);
  646. }
  647. };
  648.  
  649. // 添加配置监听
  650. this.setupConfigListeners();
  651.  
  652. // 初始化UI
  653. this.initUI();
  654. this.initEventListeners();
  655. this.addRouteListeners();
  656.  
  657. // 优化自动解码的初始化逻辑
  658. // 在构造函数中不直接执行自动解码,而是通过 resetState 方法处理
  659. if (this.config.autoDecode) {
  660. const currentTime = Date.now();
  661. // 确保足够的时间间隔
  662. if (currentTime - this.lastDecodeTime > MIN_DECODE_INTERVAL) {
  663. this.lastDecodeTime = currentTime;
  664. console.log('构造函数中准备执行 resetState');
  665. // 使用 requestIdleCallback 在浏览器空闲时执行
  666. if (window.requestIdleCallback) {
  667. requestIdleCallback(() => this.resetState(), { timeout: 2000 });
  668. } else {
  669. // 降级使用 setTimeout
  670. setTimeout(() => this.resetState(), 800);
  671. }
  672. }
  673. }
  674.  
  675. // 添加DOM加载完成后的一次性解析
  676. const handleDOMReady = () => {
  677. // 重置标志
  678. this.isInitialLoad = false;
  679. this.isPageRefresh = false; // 重置页面刷新标志
  680. this.pageRefreshCompleted = true; // 设置页面刷新完成标志
  681.  
  682. // 检查页面上是否已有解码内容
  683. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  684.  
  685. // 如果页面上已有解码内容,更新菜单状态
  686. if (hasDecodedContent) {
  687. console.log('页面上已有解码内容,更新菜单状态');
  688. if (this.decodeBtn) {
  689. this.decodeBtn.textContent = '恢复本页 Base64';
  690. this.decodeBtn.dataset.mode = 'restore';
  691. }
  692. setTimeout(updateMenuCommands, 100);
  693. }
  694. // 注意:我们在这里不执行解码,而是依赖resetState中的解码逻辑
  695. // 这样可以避免刷新页面时解码两次导致的文本抖动
  696. };
  697.  
  698. // 如果文档已经加载完成,直接执行
  699. if (document.readyState === 'complete') {
  700. console.log('文档已加载完成,直接执行解析');
  701. handleDOMReady();
  702. } else {
  703. // 否则等待文档加载完成
  704. console.log('等待文档加载完成后执行解析');
  705. window.addEventListener('load', handleDOMReady, { once: true });
  706. }
  707.  
  708. // 添加防抖相关的变量
  709. this.decodeDebounceTimer = null;
  710. this.notificationDebounceTimer = null;
  711. this.lastDecodeTime = 0;
  712. this.lastNotificationTime = 0;
  713. this.DECODE_DEBOUNCE_DELAY = 2000; // 解码防抖延迟时间(毫秒)
  714. this.NOTIFICATION_DEBOUNCE_DELAY = 3000; // 通知防抖延迟时间(毫秒)
  715. }
  716.  
  717. /**
  718. * 设置配置监听器
  719. * @description 为各个配置项添加监听器,实现配置变更的实时响应
  720. */
  721. setupConfigListeners() {
  722. // 清理现有监听器
  723. Object.values(this.configListeners).forEach(listenerId => {
  724. if (listenerId) {
  725. storageManager.removeChangeListener(listenerId);
  726. }
  727. });
  728.  
  729. // 监听显示通知设置变更
  730. this.configListeners.showNotification = storageManager.addChangeListener(
  731. STORAGE_KEYS.SHOW_NOTIFICATION,
  732. (newValue) => {
  733. console.log('显示通知设置已更改:', newValue);
  734. this.config.showNotification = newValue;
  735. }
  736. );
  737.  
  738. // 监听隐藏按钮设置变更
  739. this.configListeners.hideButton = storageManager.addChangeListener(
  740. STORAGE_KEYS.HIDE_BUTTON,
  741. (newValue) => {
  742. console.log('隐藏按钮设置已更改:', newValue);
  743. this.config.hideButton = newValue;
  744.  
  745. // 实时更新UI显示状态
  746. const ui = this.shadowRoot?.querySelector('.base64-helper');
  747. if (ui) {
  748. ui.style.display = newValue ? 'none' : 'block';
  749. }
  750. }
  751. );
  752.  
  753. // 监听自动解码设置变更
  754. this.configListeners.autoDecode = storageManager.addChangeListener(
  755. STORAGE_KEYS.AUTO_DECODE,
  756. (newValue) => {
  757. console.log('自动解码设置已更改:', newValue);
  758. this.config.autoDecode = newValue;
  759.  
  760. // 如果启用了自动解码,立即解析页面
  761. if (newValue) {
  762. // 检查是否是通过菜单命令触发的变更
  763. // 如果是通过菜单命令触发,则不再显示确认对话框
  764. // 因为菜单命令处理程序中已经处理了这些确认
  765.  
  766. // 立即解析页面
  767. this.hasAutoDecodedOnLoad = true; // 标记已执行过自动解码
  768. setTimeout(() => {
  769. this.handleDecode();
  770. // 同步按钮和菜单状态
  771. setTimeout(() => {
  772. this.syncButtonAndMenuState();
  773. // 更新油猴菜单命令
  774. updateMenuCommands();
  775. }, 200);
  776. }, 100);
  777. } else {
  778. // 如果关闭了自动解码,也需要同步状态
  779. setTimeout(() => {
  780. this.syncButtonAndMenuState();
  781. // 更新油猴菜单命令
  782. updateMenuCommands();
  783. }, 200);
  784. }
  785. }
  786. );
  787.  
  788. // 监听按钮位置变更
  789. this.configListeners.buttonPosition = storageManager.addChangeListener(
  790. STORAGE_KEYS.BUTTON_POSITION,
  791. (newValue) => {
  792. console.log('按钮位置已更改:', newValue);
  793. // 更新按钮位置
  794. this.initPosition();
  795. }
  796. );
  797. }
  798.  
  799. // 添加正则常量
  800. static URL_PATTERNS = {
  801. URL: /^(?:(?:https?|ftp):\/\/)?(?:(?:[\w-]+\.)+[a-z]{2,}|localhost)(?::\d+)?(?:\/[^\s]*)?$/i,
  802. EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  803. DOMAIN_PATTERNS: {
  804. POPULAR_SITES:
  805. /(?:google|youtube|facebook|twitter|instagram|linkedin|github|gitlab|bitbucket|stackoverflow|reddit|discord|twitch|tiktok|snapchat|pinterest|netflix|amazon|microsoft|apple|adobe)/i,
  806. VIDEO_SITES:
  807. /(?:bilibili|youku|iqiyi|douyin|kuaishou|nicovideo|vimeo|dailymotion)/i,
  808. CN_SITES:
  809. /(?:baidu|weibo|zhihu|taobao|tmall|jd|qq|163|sina|sohu|csdn|aliyun|tencent)/i,
  810. TLD: /\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)/i,
  811. },
  812. };
  813.  
  814. // UI 初始化
  815. initUI() {
  816. if (
  817. window.top !== window.self ||
  818. document.getElementById('base64-helper-root')
  819. ) {
  820. return;
  821. }
  822.  
  823. this.container = document.createElement('div');
  824. this.container.id = 'base64-helper-root';
  825. document.body.append(this.container);
  826.  
  827. this.shadowRoot = this.container.attachShadow({ mode: 'open' });
  828. this.shadowRoot.appendChild(this.createShadowStyles());
  829.  
  830. // 创建 UI 容器
  831. const uiContainer = document.createElement('div');
  832. uiContainer.className = 'base64-helper';
  833. uiContainer.style.cursor = 'grab';
  834.  
  835. // 创建按钮和菜单
  836. this.mainBtn = this.createButton('Base64', 'main-btn');
  837. this.menu = this.createMenu();
  838. this.decodeBtn = this.menu.querySelector('[data-mode="decode"]');
  839. this.encodeBtn = this.menu.querySelector('.menu-item:not([data-mode])');
  840.  
  841. // 添加到 UI 容器
  842. uiContainer.append(this.mainBtn, this.menu);
  843. this.shadowRoot.appendChild(uiContainer);
  844.  
  845. // 初始化位置
  846. this.initPosition();
  847.  
  848. // 如果配置为隐藏按钮,则设置为不可见
  849. if (this.config.hideButton) {
  850. uiContainer.style.display = 'none';
  851. }
  852. }
  853.  
  854. createShadowStyles() {
  855. const style = document.createElement('style');
  856. style.textContent = STYLES.SHADOW_DOM;
  857. return style;
  858. }
  859.  
  860. // 不再需要 createMainUI 方法,因为我们直接在 initUI 中创建 UI
  861.  
  862. createButton(text, className) {
  863. const btn = document.createElement('button');
  864. btn.className = className;
  865. btn.textContent = text;
  866. return btn;
  867. }
  868.  
  869. createMenu() {
  870. const menu = document.createElement('div');
  871. menu.className = 'menu';
  872.  
  873. this.decodeBtn = this.createMenuItem('解析本页 Base64', 'decode');
  874. this.encodeBtn = this.createMenuItem('文本转 Base64');
  875.  
  876. menu.append(this.decodeBtn, this.encodeBtn);
  877. return menu;
  878. }
  879.  
  880. createMenuItem(text, mode) {
  881. const item = document.createElement('div');
  882. item.className = 'menu-item';
  883. item.textContent = text;
  884. if (mode) item.dataset.mode = mode;
  885. return item;
  886. }
  887.  
  888. // 位置管理
  889. initPosition() {
  890. const pos = this.positionManager.get() || {
  891. x: window.innerWidth - 120,
  892. y: window.innerHeight - 80,
  893. };
  894.  
  895. const ui = this.shadowRoot.querySelector('.base64-helper');
  896. ui.style.left = `${pos.x}px`;
  897. ui.style.top = `${pos.y}px`;
  898.  
  899. // 新增: 初始化时更新菜单对齐
  900. this.updateMenuAlignment();
  901. }
  902. updateMenuAlignment() {
  903. const ui = this.shadowRoot.querySelector('.base64-helper');
  904. const menu = this.menu;
  905. const windowWidth = window.innerWidth;
  906. const windowHeight = window.innerHeight;
  907. const uiRect = ui.getBoundingClientRect();
  908. const centerX = uiRect.left + uiRect.width / 2;
  909. const centerY = uiRect.top + uiRect.height / 2;
  910.  
  911. // 判断按钮是在页面左半边还是右半边
  912. if (centerX < windowWidth / 2) {
  913. // 左对齐
  914. menu.classList.remove('align-right');
  915. menu.classList.add('align-left');
  916. } else {
  917. // 右对齐
  918. menu.classList.remove('align-left');
  919. menu.classList.add('align-right');
  920. }
  921.  
  922. // 判断按钮是在页面上半部分还是下半部分
  923. if (centerY < windowHeight / 2) {
  924. // 在页面上方,菜单向下弹出
  925. menu.classList.remove('popup-top');
  926. menu.classList.add('popup-bottom');
  927. } else {
  928. // 在页面下方,菜单向上弹出
  929. menu.classList.remove('popup-bottom');
  930. menu.classList.add('popup-top');
  931. }
  932. }
  933. get positionManager() {
  934. return {
  935. get: () => {
  936. // 使用 storageManager 获取按钮位置
  937. const saved = storageManager.get(STORAGE_KEYS.BUTTON_POSITION, null);
  938. if (!saved) return null;
  939.  
  940. const ui = this.shadowRoot.querySelector('.base64-helper');
  941. const maxX = window.innerWidth - ui.offsetWidth - 20;
  942. const maxY = window.innerHeight - ui.offsetHeight - 20;
  943.  
  944. return {
  945. x: Math.min(Math.max(saved.x, 20), maxX),
  946. y: Math.min(Math.max(saved.y, 20), maxY),
  947. };
  948. },
  949. set: (x, y) => {
  950. const ui = this.shadowRoot.querySelector('.base64-helper');
  951. const pos = {
  952. x: Math.max(
  953. 20,
  954. Math.min(x, window.innerWidth - ui.offsetWidth - 20)
  955. ),
  956. y: Math.max(
  957. 20,
  958. Math.min(y, window.innerHeight - ui.offsetHeight - 20)
  959. ),
  960. };
  961.  
  962. // 使用 storageManager 存储按钮位置
  963. storageManager.set(STORAGE_KEYS.BUTTON_POSITION, pos);
  964. return pos;
  965. },
  966. };
  967. }
  968.  
  969. // 初始化事件监听器
  970. initEventListeners() {
  971. this.addUnifiedEventListeners();
  972. this.addGlobalClickListeners();
  973.  
  974. // 核心编解码事件监听
  975. const commonListeners = [
  976. {
  977. element: this.decodeBtn,
  978. events: [
  979. {
  980. name: 'click',
  981. handler: (e) => {
  982. e.preventDefault();
  983. e.stopPropagation();
  984. this.handleDecode();
  985. },
  986. },
  987. ],
  988. },
  989. {
  990. element: this.encodeBtn,
  991. events: [
  992. {
  993. name: 'click',
  994. handler: (e) => {
  995. e.preventDefault();
  996. e.stopPropagation();
  997. this.handleEncode();
  998. },
  999. },
  1000. ],
  1001. },
  1002. ];
  1003.  
  1004. commonListeners.forEach(({ element, events }) => {
  1005. events.forEach(({ name, handler }) => {
  1006. element.addEventListener(name, handler, { passive: false });
  1007. this.eventListeners.set(name, { element, event: name, handler });
  1008. });
  1009. });
  1010. }
  1011.  
  1012. addUnifiedEventListeners() {
  1013. const ui = this.shadowRoot.querySelector('.base64-helper');
  1014. const btn = this.mainBtn;
  1015.  
  1016. // 统一的开始事件处理
  1017. const startHandler = (e) => {
  1018. e.preventDefault();
  1019. e.stopPropagation();
  1020. const point = e.touches ? e.touches[0] : e;
  1021. this.isDragging = true;
  1022. this.hasMoved = false;
  1023. this.startX = point.clientX;
  1024. this.startY = point.clientY;
  1025. const rect = ui.getBoundingClientRect();
  1026. this.initialX = rect.left;
  1027. this.initialY = rect.top;
  1028. this.startTime = Date.now();
  1029. ui.style.transition = 'none';
  1030. ui.classList.add('dragging');
  1031. btn.style.cursor = 'grabbing';
  1032. };
  1033.  
  1034. // 统一的移动事件处理
  1035. const moveHandler = (e) => {
  1036. if (!this.isDragging) return;
  1037. e.preventDefault();
  1038. e.stopPropagation();
  1039.  
  1040. const point = e.touches ? e.touches[0] : e;
  1041. const moveX = Math.abs(point.clientX - this.startX);
  1042. const moveY = Math.abs(point.clientY - this.startY);
  1043.  
  1044. if (moveX > 5 || moveY > 5) {
  1045. this.hasMoved = true;
  1046. const dx = point.clientX - this.startX;
  1047. const dy = point.clientY - this.startY;
  1048. const newX = Math.min(
  1049. Math.max(20, this.initialX + dx),
  1050. window.innerWidth - ui.offsetWidth - 20
  1051. );
  1052. const newY = Math.min(
  1053. Math.max(20, this.initialY + dy),
  1054. window.innerHeight - ui.offsetHeight - 20
  1055. );
  1056. ui.style.left = `${newX}px`;
  1057. ui.style.top = `${newY}px`;
  1058. }
  1059. };
  1060.  
  1061. // 统一的结束事件处理
  1062. const endHandler = (e) => {
  1063. if (!this.isDragging) return;
  1064. e.preventDefault();
  1065. e.stopPropagation();
  1066.  
  1067. this.isDragging = false;
  1068. ui.classList.remove('dragging');
  1069. btn.style.cursor = 'grab';
  1070. ui.style.transition = 'opacity 0.3s ease';
  1071.  
  1072. const duration = Date.now() - this.startTime;
  1073. if (duration < 200 && !this.hasMoved) {
  1074. this.toggleMenu(e);
  1075. } else if (this.hasMoved) {
  1076. const rect = ui.getBoundingClientRect();
  1077. const pos = this.positionManager.set(rect.left, rect.top);
  1078. ui.style.left = `${pos.x}px`;
  1079. ui.style.top = `${pos.y}px`;
  1080. // 新增: 拖动结束后更新菜单对齐
  1081. this.updateMenuAlignment();
  1082. }
  1083. };
  1084.  
  1085. // 统一收集所有事件监听器
  1086. const listeners = [
  1087. {
  1088. element: ui,
  1089. event: 'touchstart',
  1090. handler: startHandler,
  1091. options: { passive: false },
  1092. },
  1093. {
  1094. element: ui,
  1095. event: 'touchmove',
  1096. handler: moveHandler,
  1097. options: { passive: false },
  1098. },
  1099. {
  1100. element: ui,
  1101. event: 'touchend',
  1102. handler: endHandler,
  1103. options: { passive: false },
  1104. },
  1105. { element: ui, event: 'mousedown', handler: startHandler },
  1106. { element: document, event: 'mousemove', handler: moveHandler },
  1107. { element: document, event: 'mouseup', handler: endHandler },
  1108. {
  1109. element: this.menu,
  1110. event: 'touchstart',
  1111. handler: (e) => e.stopPropagation(),
  1112. options: { passive: false },
  1113. },
  1114. {
  1115. element: this.menu,
  1116. event: 'mousedown',
  1117. handler: (e) => e.stopPropagation(),
  1118. },
  1119. {
  1120. element: window,
  1121. event: 'resize',
  1122. handler: () => this.handleResize(),
  1123. },
  1124. ];
  1125.  
  1126. // 注册(不可用)事件并保存引用
  1127. listeners.forEach(({ element, event, handler, options }) => {
  1128. element.addEventListener(event, handler, options);
  1129. this.eventListeners.set(event, { element, event, handler, options });
  1130. });
  1131. }
  1132.  
  1133. toggleMenu(e) {
  1134. e?.preventDefault();
  1135. e?.stopPropagation();
  1136.  
  1137. // 如果正在拖动或已移动,不处理菜单切换
  1138. if (this.isDragging || this.hasMoved) return;
  1139.  
  1140. this.menuVisible = !this.menuVisible;
  1141. if (this.menuVisible) {
  1142. // 在显示菜单前更新位置
  1143. this.updateMenuAlignment();
  1144. }
  1145. this.menu.style.display = this.menuVisible ? 'block' : 'none';
  1146.  
  1147. // 重置状态
  1148. this.hasMoved = false;
  1149. }
  1150.  
  1151. addGlobalClickListeners() {
  1152. const handleOutsideClick = (e) => {
  1153. const ui = this.shadowRoot.querySelector('.base64-helper');
  1154. const path = e.composedPath();
  1155. if (!path.includes(ui) && this.menuVisible) {
  1156. this.menuVisible = false;
  1157. this.menu.style.display = 'none';
  1158. }
  1159. };
  1160.  
  1161. // 将全局点击事件添加到 eventListeners 数组
  1162. const globalListeners = [
  1163. {
  1164. element: document,
  1165. event: 'click',
  1166. handler: handleOutsideClick,
  1167. options: true,
  1168. },
  1169. {
  1170. element: document,
  1171. event: 'touchstart',
  1172. handler: handleOutsideClick,
  1173. options: { passive: false },
  1174. },
  1175. ];
  1176.  
  1177. globalListeners.forEach(({ element, event, handler, options }) => {
  1178. element.addEventListener(event, handler, options);
  1179. this.eventListeners.set(event, { element, event, handler, options });
  1180. });
  1181. }
  1182.  
  1183. // 路由监听
  1184. addRouteListeners() {
  1185. // 统一的路由变化处理函数
  1186. this.handleRouteChange = () => {
  1187. console.log('路由变化被检测到');
  1188. // 使用防抖,避免短时间内多次触发
  1189. clearTimeout(this.routeTimer);
  1190.  
  1191. // 添加时间检查,避免短时间内多次触发
  1192. const currentTime = Date.now();
  1193. if (currentTime - this.lastDecodeTime < 1000) {
  1194. console.log('距离上次解码时间太短,跳过这次路由变化');
  1195. return;
  1196. }
  1197.  
  1198. // 如果启用了自动解码,直接执行全页解码
  1199. if (this.config.autoDecode) {
  1200. console.log('路由变化,执行全页解码');
  1201. // 确保没有正在进行的处理
  1202. if (!this.isProcessing && !this.isDecodingContent && !this.isRestoringContent) {
  1203. // 使用延时确保页面内容已更新
  1204. setTimeout(() => {
  1205. this.handleAutoDecode(true, true);
  1206. }, 500);
  1207. }
  1208. }
  1209. };
  1210.  
  1211. // 添加路由相关事件到 eventListeners 数组
  1212. const routeListeners = [
  1213. {
  1214. element: window,
  1215. event: 'popstate',
  1216. handler: (e) => {
  1217. console.log('检测到popstate前进后退事件');
  1218. // 重置页面状态标志
  1219. this.isInitialLoad = false;
  1220. this.isPageRefresh = false;
  1221. this.pageRefreshCompleted = true;
  1222.  
  1223. // 创建一个临时的MutationObserver来监听页面内容变化
  1224. const tempObserver = new MutationObserver((mutations) => {
  1225. // 检查是否有显著的DOM变化
  1226. const hasSignificantChanges = mutations.some(mutation => {
  1227. // 忽略文本节点的变化
  1228. if (mutation.type === 'characterData') return false;
  1229.  
  1230. // 忽略样式相关的属性变化
  1231. if (mutation.type === 'attributes' &&
  1232. (mutation.attributeName === 'style' ||
  1233. mutation.attributeName === 'class')) {
  1234. return false;
  1235. }
  1236.  
  1237. // 检查是否是重要的DOM变化
  1238. const isImportantNode = (node) => {
  1239. return node.nodeType === 1 && // 元素节点
  1240. (node.tagName === 'DIV' ||
  1241. node.tagName === 'ARTICLE' ||
  1242. node.tagName === 'SECTION' ||
  1243. node.tagName === 'MAIN');
  1244. };
  1245.  
  1246. return Array.from(mutation.addedNodes).some(isImportantNode) ||
  1247. Array.from(mutation.removedNodes).some(isImportantNode);
  1248. });
  1249.  
  1250. if (hasSignificantChanges) {
  1251. console.log('检测到新页面内容加载完成');
  1252. // 停止观察
  1253. tempObserver.disconnect();
  1254. // 执行解码
  1255. if (this.config.autoDecode) {
  1256. setTimeout(() => {
  1257. this.handleAutoDecode(true, true);
  1258. }, 500);
  1259. }
  1260. }
  1261. });
  1262.  
  1263. // 开始观察页面变化
  1264. tempObserver.observe(document.body, {
  1265. childList: true,
  1266. subtree: true,
  1267. attributes: false,
  1268. characterData: false
  1269. });
  1270.  
  1271. // 设置超时,防止页面变化检测失败
  1272. setTimeout(() => {
  1273. tempObserver.disconnect();
  1274. if (this.config.autoDecode) {
  1275. this.handleAutoDecode(true, true);
  1276. }
  1277. }, 3000);
  1278. }
  1279. },
  1280. {
  1281. element: window,
  1282. event: 'hashchange',
  1283. handler: this.handleRouteChange,
  1284. },
  1285. {
  1286. element: window,
  1287. event: 'DOMContentLoaded',
  1288. handler: this.handleRouteChange,
  1289. },
  1290. {
  1291. element: document,
  1292. event: 'readystatechange',
  1293. handler: () => {
  1294. if (document.readyState === 'complete') {
  1295. this.handleRouteChange();
  1296. }
  1297. },
  1298. },
  1299. {
  1300. element: window,
  1301. event: 'pageshow',
  1302. handler: (e) => {
  1303. console.log('检测到pageshow事件');
  1304. // 重置页面状态标志
  1305. this.isInitialLoad = false;
  1306. this.isPageRefresh = false;
  1307. this.pageRefreshCompleted = true;
  1308.  
  1309. // 创建一个临时的MutationObserver来监听页面内容变化
  1310. const tempObserver = new MutationObserver((mutations) => {
  1311. // 检查是否有显著的DOM变化
  1312. const hasSignificantChanges = mutations.some(mutation => {
  1313. // 忽略文本节点的变化
  1314. if (mutation.type === 'characterData') return false;
  1315.  
  1316. // 忽略样式相关的属性变化
  1317. if (mutation.type === 'attributes' &&
  1318. (mutation.attributeName === 'style' ||
  1319. mutation.attributeName === 'class')) {
  1320. return false;
  1321. }
  1322.  
  1323. // 检查是否是重要的DOM变化
  1324. const isImportantNode = (node) => {
  1325. return node.nodeType === 1 && // 元素节点
  1326. (node.tagName === 'DIV' ||
  1327. node.tagName === 'ARTICLE' ||
  1328. node.tagName === 'SECTION' ||
  1329. node.tagName === 'MAIN');
  1330. };
  1331.  
  1332. return Array.from(mutation.addedNodes).some(isImportantNode) ||
  1333. Array.from(mutation.removedNodes).some(isImportantNode);
  1334. });
  1335.  
  1336. if (hasSignificantChanges) {
  1337. console.log('检测到新页面内容加载完成');
  1338. // 停止观察
  1339. tempObserver.disconnect();
  1340. // 执行解码
  1341. if (this.config.autoDecode) {
  1342. setTimeout(() => {
  1343. this.handleAutoDecode(true, true);
  1344. }, 500);
  1345. }
  1346. }
  1347. });
  1348.  
  1349. // 开始观察页面变化
  1350. tempObserver.observe(document.body, {
  1351. childList: true,
  1352. subtree: true,
  1353. attributes: false,
  1354. characterData: false
  1355. });
  1356.  
  1357. // 设置超时,防止页面变化检测失败
  1358. setTimeout(() => {
  1359. tempObserver.disconnect();
  1360. if (this.config.autoDecode) {
  1361. this.handleAutoDecode(true, true);
  1362. }
  1363. }, 3000);
  1364. },
  1365. },
  1366. {
  1367. element: window,
  1368. event: 'pagehide',
  1369. handler: this.handleRouteChange,
  1370. },
  1371. ];
  1372.  
  1373. // 确保在页面加载完成后添加事件监听器
  1374. if (document.readyState === 'complete') {
  1375. routeListeners.forEach(({ element, event, handler }) => {
  1376. element.addEventListener(event, handler);
  1377. this.eventListeners.set(event, { element, event, handler });
  1378. });
  1379. } else {
  1380. window.addEventListener('load', () => {
  1381. routeListeners.forEach(({ element, event, handler }) => {
  1382. element.addEventListener(event, handler);
  1383. this.eventListeners.set(event, { element, event, handler });
  1384. });
  1385. }, { once: true });
  1386. }
  1387.  
  1388. // 修改 history 方法
  1389. this.originalPushState = history.pushState;
  1390. this.originalReplaceState = history.replaceState;
  1391. history.pushState = (...args) => {
  1392. this.originalPushState.apply(history, args);
  1393. console.log('history.pushState 被调用');
  1394. this.handleRouteChange();
  1395. };
  1396. history.replaceState = (...args) => {
  1397. this.originalReplaceState.apply(history, args);
  1398. console.log('history.replaceState 被调用');
  1399. this.handleRouteChange();
  1400. };
  1401.  
  1402. // 优化 MutationObserver 配置
  1403. this.observer = new MutationObserver((mutations) => {
  1404. // 如果正在处理中或正在显示通知,跳过这次变化
  1405. if (this.isProcessing || this.isShowingNotification || this.isDecodingContent || this.isRestoringContent) {
  1406. console.log('正在处理中或显示通知,跳过 DOM 变化检测');
  1407. return;
  1408. }
  1409.  
  1410. // 添加防止短时间内重复触发的防抖
  1411. const currentTime = Date.now();
  1412. if (currentTime - this.lastDecodeTime < 1500) {
  1413. console.log('距离上次解码时间太短,跳过这次 DOM 变化检测');
  1414. return;
  1415. }
  1416.  
  1417. // 存储mutations供后续使用
  1418. this.currentMutations = mutations;
  1419.  
  1420. // 检查是否有显著的 DOM 变化
  1421. const significantChanges = mutations.some(mutation => {
  1422. // 忽略文本节点的变化
  1423. if (mutation.type === 'characterData') return false;
  1424.  
  1425. // 忽略样式相关的属性变化
  1426. if (mutation.type === 'attributes' &&
  1427. (mutation.attributeName === 'style' ||
  1428. mutation.attributeName === 'class')) {
  1429. return false;
  1430. }
  1431.  
  1432. // 排除通知容器的变化
  1433. if (mutation.target &&
  1434. (mutation.target.classList?.contains('base64-notifications-container') ||
  1435. mutation.target.classList?.contains('base64-notification'))) {
  1436. return false;
  1437. }
  1438.  
  1439. // 检查添加的节点是否与通知相关
  1440. const isNotificationNode = (node) => {
  1441. if (node.nodeType !== 1) return false; // 非元素节点
  1442. return node.classList?.contains('base64-notifications-container') ||
  1443. node.classList?.contains('base64-notification') ||
  1444. node.closest('.base64-notifications-container') !== null;
  1445. };
  1446.  
  1447. // 如果添加的节点是通知相关的,则忽略
  1448. if (Array.from(mutation.addedNodes).some(isNotificationNode)) {
  1449. return false;
  1450. }
  1451.  
  1452. // 如果有大量节点添加或删除,可能是路由变化
  1453. if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) {
  1454. return true;
  1455. }
  1456.  
  1457. // 检查是否是重要的 DOM 变化
  1458. const isImportantNode = (node) => {
  1459. return node.nodeType === 1 && // 元素节点
  1460. (node.tagName === 'DIV' ||
  1461. node.tagName === 'ARTICLE' ||
  1462. node.tagName === 'SECTION');
  1463. };
  1464.  
  1465. return Array.from(mutation.addedNodes).some(isImportantNode) ||
  1466. Array.from(mutation.removedNodes).some(isImportantNode);
  1467. });
  1468.  
  1469. if (significantChanges && this.config.autoDecode) {
  1470. console.log('检测到显著的 DOM 变化,可能是路由变化');
  1471. this.handleRouteChange();
  1472. }
  1473. });
  1474.  
  1475. // 优化 MutationObserver 观察选项
  1476. this.observer.observe(document.body, {
  1477. childList: true,
  1478. subtree: true,
  1479. attributes: false, // 不观察属性变化
  1480. characterData: false // 不观察文本变化
  1481. });
  1482.  
  1483. // 监听路由变化
  1484. const observer = new MutationObserver((mutations) => {
  1485. mutations.forEach((mutation) => {
  1486. if (mutation.type === 'childList' || mutation.type === 'subtree') {
  1487. // 检查是否是路由变化
  1488. if (window.location.href !== this.lastUrl) {
  1489. this.lastUrl = window.location.href;
  1490. console.log('检测到路由变化,准备执行解码');
  1491.  
  1492. // 重置状态
  1493. this.resetState();
  1494.  
  1495. // 如果启用了自动解码,等待页面稳定后执行
  1496. if (this.config.autoDecode) {
  1497. setTimeout(() => {
  1498. const { nodesToReplace, validDecodedCount } = this.processTextNodes();
  1499. if (validDecodedCount > 0) {
  1500. this.replaceNodes(nodesToReplace);
  1501. setTimeout(() => {
  1502. this.addClickListenersToDecodedText();
  1503. this.debouncedShowNotification(`解码成功,共找到 ${validDecodedCount} Base64 内容`, 'success');
  1504. this.syncButtonAndMenuState();
  1505. }, 100);
  1506. }
  1507. }, 500);
  1508. }
  1509. }
  1510. }
  1511. });
  1512. });
  1513. }
  1514.  
  1515. /**
  1516. * 处理自动解码
  1517. * @description 根据不同场景选择全页解码或增量解码
  1518. * @param {boolean} [forceFullDecode=false] - 是否强制全页面解码
  1519. * @param {boolean} [showNotification=false] - 是否显示通知
  1520. */
  1521. handleAutoDecode(forceFullDecode = false, showNotification = false) {
  1522. // 防止重复处理
  1523. if (this.isProcessing || this.isShowingNotification || this.isDecodingContent || this.isRestoringContent) {
  1524. console.log('正在处理中,跳过自动解码请求');
  1525. return;
  1526. }
  1527.  
  1528. // 控制通知显示
  1529. this.suppressNotification = !showNotification;
  1530.  
  1531. // 更新最后解码时间
  1532. this.lastDecodeTime = Date.now();
  1533.  
  1534. console.log('执行全页面解码');
  1535. // 使用processTextNodes方法进行解码
  1536. const { nodesToReplace, validDecodedCount } = this.processTextNodes();
  1537.  
  1538. if (validDecodedCount > 0) {
  1539. // 分批处理节点替换
  1540. const BATCH_SIZE = 50;
  1541. const processNodesBatch = (startIndex) => {
  1542. const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length);
  1543. const batch = nodesToReplace.slice(startIndex, endIndex);
  1544.  
  1545. this.replaceNodes(batch);
  1546.  
  1547. if (endIndex < nodesToReplace.length) {
  1548. setTimeout(() => processNodesBatch(endIndex), 0);
  1549. } else {
  1550. setTimeout(() => {
  1551. this.addClickListenersToDecodedText();
  1552. if (showNotification) {
  1553. this.showNotification(`解码成功,共找到 ${validDecodedCount} Base64 内容`, 'success');
  1554. }
  1555. // 同步按钮和菜单状态
  1556. this.syncButtonAndMenuState();
  1557. // 更新油猴菜单命令
  1558. updateMenuCommands();
  1559. }, 100);
  1560. }
  1561. };
  1562.  
  1563. processNodesBatch(0);
  1564. }
  1565.  
  1566. // 同步按钮和菜单状态
  1567. setTimeout(() => {
  1568. this.syncButtonAndMenuState();
  1569. // 更新油猴菜单命令
  1570. updateMenuCommands();
  1571. }, 200);
  1572. }
  1573.  
  1574. /**
  1575. * 处理页面中的Base64解码操作
  1576. * @description 根据当前模式执行解码或恢复操作
  1577. * 如果当前模式是restore则恢复原始内容,否则查找并解码页面中的Base64内容
  1578. * @fires showNotification 显示操作结果通知
  1579. */
  1580. handleDecode() {
  1581. // 检查当前模式
  1582. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  1583. const currentMode = this.decodeBtn?.dataset.mode === 'restore' || hasDecodedContent ? 'restore' : 'decode';
  1584.  
  1585. // 如果是恢复模式
  1586. if (currentMode === 'restore') {
  1587. this.restoreContent();
  1588. return;
  1589. }
  1590.  
  1591. // 防止重复处理或在显示通知时触发
  1592. if (this.isProcessing || this.isShowingNotification || this.isDecodingContent || this.isRestoringContent) {
  1593. console.log('正在处理中,跳过解码请求');
  1594. return;
  1595. }
  1596.  
  1597. try {
  1598. // 隐藏菜单
  1599. if (this.menu && this.menu.style.display !== 'none') {
  1600. this.menu.style.display = 'none';
  1601. this.menuVisible = false;
  1602. }
  1603.  
  1604. // 执行解码
  1605. this.isDecodingContent = true;
  1606. const { nodesToReplace, validDecodedCount } = this.processTextNodes();
  1607.  
  1608. if (validDecodedCount === 0) {
  1609. this.showNotification('本页未发现有效 Base64 内容', 'info');
  1610. this.menuVisible = false;
  1611. this.menu.style.display = 'none';
  1612. // 重置处理标志
  1613. this.isProcessing = false;
  1614. this.isDecodingContent = false;
  1615. // 更新最后解码时间
  1616. this.lastDecodeTime = Date.now();
  1617. return;
  1618. }
  1619.  
  1620. // 分批处理节点替换,避免大量 DOM 操作导致界面冻结
  1621. const BATCH_SIZE = 50;
  1622. const processNodesBatch = (startIndex) => {
  1623. const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length);
  1624. const batch = nodesToReplace.slice(startIndex, endIndex);
  1625.  
  1626. this.replaceNodes(batch);
  1627.  
  1628. if (endIndex < nodesToReplace.length) {
  1629. // 还有更多节点需要处理,安排下一批
  1630. setTimeout(() => processNodesBatch(endIndex), 0);
  1631. } else {
  1632. // 所有节点处理完成,添加点击监听器
  1633. setTimeout(() => {
  1634. this.addClickListenersToDecodedText();
  1635. }, 100);
  1636.  
  1637. // 更新按钮状态
  1638. if (this.decodeBtn) {
  1639. this.decodeBtn.textContent = '恢复本页 Base64';
  1640. this.decodeBtn.dataset.mode = 'restore';
  1641. }
  1642.  
  1643. // 显示通知,除非被抑制
  1644. if (!this.suppressNotification) {
  1645. this.showNotification(
  1646. `解码成功,共找到 ${validDecodedCount} Base64 内容`,
  1647. 'success'
  1648. );
  1649. }
  1650.  
  1651. // 操作完成后同步按钮和菜单状态
  1652. this.syncButtonAndMenuState();
  1653. // 更新油猴菜单命令
  1654. updateMenuCommands();
  1655.  
  1656. // 重置处理标志
  1657. this.isProcessing = false;
  1658. this.isDecodingContent = false;
  1659.  
  1660. // 更新最后解码时间
  1661. this.lastDecodeTime = Date.now();
  1662. }
  1663. };
  1664.  
  1665. // 开始分批处理
  1666. processNodesBatch(0);
  1667. } catch (e) {
  1668. console.error('Base64 decode error:', e);
  1669. // 显示错误通知
  1670. this.showNotification(`解析失败: ${e.message}`, 'error');
  1671. this.menuVisible = false;
  1672. this.menu.style.display = 'none';
  1673. // 重置处理标志
  1674. this.isProcessing = false;
  1675. this.isDecodingContent = false;
  1676. // 更新最后解码时间
  1677. this.lastDecodeTime = Date.now();
  1678. }
  1679. }
  1680.  
  1681. /**
  1682. * 处理文本节点中的Base64内容
  1683. * @description 遍历文档中的文本节点,查找并处理其中的Base64内容
  1684. * 注意: 此方法包含性能优化措施,如超时检测和节点过滤
  1685. * @returns {Object} 处理结果
  1686. * @property {Array} nodesToReplace - 需要替换的节点数组
  1687. * @property {number} validDecodedCount - 有效的Base64解码数量
  1688. */
  1689. processTextNodes() {
  1690. const startTime = Date.now();
  1691. const TIMEOUT = 5000;
  1692.  
  1693. const excludeTags = new Set([
  1694. 'script',
  1695. 'style',
  1696. 'noscript',
  1697. 'iframe',
  1698. 'img',
  1699. 'input',
  1700. 'textarea',
  1701. 'svg',
  1702. 'canvas',
  1703. 'template',
  1704. 'pre',
  1705. 'code',
  1706. 'button',
  1707. 'meta',
  1708. 'link',
  1709. 'head',
  1710. 'title',
  1711. 'select',
  1712. 'form',
  1713. 'object',
  1714. 'embed',
  1715. 'video',
  1716. 'audio',
  1717. 'source',
  1718. 'track',
  1719. 'map',
  1720. 'area',
  1721. 'math',
  1722. 'figure',
  1723. 'picture',
  1724. 'portal',
  1725. 'slot',
  1726. 'data',
  1727. 'a',
  1728. 'base', // 包含href属性的base标签
  1729. 'param', // object的参数
  1730. 'applet', // 旧版Java小程序
  1731. 'frame', // 框架
  1732. 'frameset', // 框架集
  1733. 'marquee', // 滚动文本
  1734. 'time', // 时间标签
  1735. 'wbr', // 可能的换行符
  1736. 'bdo', // 文字方向
  1737. 'dialog', // 对话框
  1738. 'details', // 详情
  1739. 'summary', // 摘要
  1740. 'menu', // 菜单
  1741. 'menuitem', // 菜单项
  1742. '[hidden]', // 隐藏元素
  1743. '[aria-hidden="true"]', // 可访问性隐藏
  1744. '.base64', // 自定义class
  1745. '.encoded', // 自定义class
  1746. ]);
  1747.  
  1748. const excludeAttrs = new Set([
  1749. 'src',
  1750. 'data-src',
  1751. 'href',
  1752. 'data-url',
  1753. 'content',
  1754. 'background',
  1755. 'poster',
  1756. 'data-image',
  1757. 'srcset',
  1758. 'data-background', // 背景图片
  1759. 'data-thumbnail', // 缩略图
  1760. 'data-original', // 原始图片
  1761. 'data-lazy', // 懒加载
  1762. 'data-defer', // 延迟加载
  1763. 'data-fallback', // 后备图片
  1764. 'data-preview', // 预览图
  1765. 'data-avatar', // 头像
  1766. 'data-icon', // 图标
  1767. 'data-base64', // 显式标记的base64
  1768. 'style', // 内联样式可能包含base64
  1769. 'integrity', // SRI完整性校验
  1770. 'crossorigin', // 跨域属性
  1771. 'rel', // 关系属性
  1772. 'alt', // 替代文本
  1773. 'title', // 标题属性
  1774. ]);
  1775.  
  1776. const walker = document.createTreeWalker(
  1777. document.body,
  1778. NodeFilter.SHOW_TEXT,
  1779. {
  1780. acceptNode: (node) => {
  1781. const isExcludedTag = (parent) => {
  1782. const tagName = parent.tagName?.toLowerCase();
  1783. return excludeTags.has(tagName);
  1784. };
  1785.  
  1786. const isHiddenElement = (parent) => {
  1787. if (!(parent instanceof HTMLElement)) return false;
  1788. const style = window.getComputedStyle(parent);
  1789. return (
  1790. style.display === 'none' ||
  1791. style.visibility === 'hidden' ||
  1792. style.opacity === '0' ||
  1793. style.clipPath === 'inset(100%)' ||
  1794. (style.height === '0px' && style.overflow === 'hidden')
  1795. );
  1796. };
  1797.  
  1798. const isOutOfViewport = (parent) => {
  1799. if (!(parent instanceof HTMLElement)) return false;
  1800. const rect = parent.getBoundingClientRect();
  1801. return rect.width === 0 || rect.height === 0;
  1802. };
  1803.  
  1804. const hasBase64Attributes = (parent) => {
  1805. if (!parent.hasAttributes()) return false;
  1806. for (const attr of parent.attributes) {
  1807. if (excludeAttrs.has(attr.name)) {
  1808. const value = attr.value.toLowerCase();
  1809. if (
  1810. value.includes('base64') ||
  1811. value.match(/^[a-z0-9+/=]+$/i)
  1812. ) {
  1813. return true;
  1814. }
  1815. }
  1816. }
  1817. return false;
  1818. };
  1819.  
  1820. let parent = node.parentNode;
  1821. while (parent && parent !== document.body) {
  1822. if (
  1823. isExcludedTag(parent) ||
  1824. isHiddenElement(parent) ||
  1825. isOutOfViewport(parent) ||
  1826. hasBase64Attributes(parent)
  1827. ) {
  1828. return NodeFilter.FILTER_REJECT;
  1829. }
  1830. parent = parent.parentNode;
  1831. }
  1832.  
  1833. const text = node.textContent?.trim();
  1834. if (!text) {
  1835. return NodeFilter.FILTER_SKIP;
  1836. }
  1837.  
  1838. return /[A-Za-z0-9+/]+/.exec(text)
  1839. ? NodeFilter.FILTER_ACCEPT
  1840. : NodeFilter.FILTER_SKIP;
  1841. },
  1842. },
  1843. false
  1844. );
  1845.  
  1846. let nodesToReplace = [];
  1847. let processedMatches = new Set();
  1848. let validDecodedCount = 0;
  1849.  
  1850. while (walker.nextNode()) {
  1851. if (Date.now() - startTime > TIMEOUT) {
  1852. console.warn('Base64 processing timeout');
  1853. break;
  1854. }
  1855.  
  1856. const node = walker.currentNode;
  1857. const { modified, newHtml, count } = this.processMatches(
  1858. node.nodeValue,
  1859. processedMatches
  1860. );
  1861. if (modified) {
  1862. nodesToReplace.push({ node, newHtml });
  1863. validDecodedCount += count;
  1864. }
  1865. }
  1866.  
  1867. return { nodesToReplace, validDecodedCount };
  1868. }
  1869.  
  1870. /**
  1871. * 收集变化的节点
  1872. * @description 从变化记录中收集需要处理的节点
  1873. * @param {MutationRecord[]} mutations - 变化记录数组
  1874. * @returns {Node[]} 需要处理的节点数组
  1875. */
  1876. collectChangedNodes(mutations) {
  1877. const changedNodes = [];
  1878. const excludeTags = new Set([
  1879. 'script', 'style', 'noscript', 'iframe', 'img', 'input', 'textarea',
  1880. 'svg', 'canvas', 'template', 'pre', 'code', 'button', 'meta', 'link'
  1881. ]);
  1882.  
  1883. // 遍历所有变化记录
  1884. for (const mutation of mutations) {
  1885. // 跳过通知相关的变化
  1886. if (mutation.target && (
  1887. mutation.target.classList?.contains('base64-notifications-container') ||
  1888. mutation.target.classList?.contains('base64-notification') ||
  1889. mutation.target.closest?.('.base64-notifications-container')
  1890. )) {
  1891. continue;
  1892. }
  1893.  
  1894. // 处理新添加的节点
  1895. if (mutation.addedNodes.length > 0) {
  1896. for (const node of mutation.addedNodes) {
  1897. // 跳过已处理过的节点
  1898. if (this.processedNodes.has(node)) {
  1899. continue;
  1900. }
  1901.  
  1902. // 跳过非元素节点和排除的标签
  1903. if (node.nodeType === 1 && excludeTags.has(node.tagName.toLowerCase())) {
  1904. continue;
  1905. }
  1906.  
  1907. // 跳过已解码的文本节点
  1908. if (node.classList?.contains('decoded-text')) {
  1909. continue;
  1910. }
  1911.  
  1912. // 添加到待处理节点列表
  1913. changedNodes.push(node);
  1914. // 标记为已处理
  1915. this.processedNodes.add(node);
  1916. }
  1917. }
  1918.  
  1919. // 处理变化的目标节点
  1920. if (mutation.type === 'childList' && !this.processedNodes.has(mutation.target)) {
  1921. // 跳过非元素节点和排除的标签
  1922. if (mutation.target.nodeType === 1 &&
  1923. !excludeTags.has(mutation.target.tagName?.toLowerCase()) &&
  1924. !mutation.target.classList?.contains('decoded-text')) {
  1925. changedNodes.push(mutation.target);
  1926. this.processedNodes.add(mutation.target);
  1927. }
  1928. }
  1929. }
  1930.  
  1931. return changedNodes;
  1932. }
  1933.  
  1934. /**
  1935. * 处理增量解码
  1936. * @description 只对变化的节点进行解码处理
  1937. * @param {Node[]} changedNodes - 需要处理的节点数组
  1938. */
  1939. async handleIncrementalDecode(changedNodes) {
  1940. console.log(`开始增量解码,处理 ${changedNodes.length} 个变化节点`);
  1941.  
  1942. // 设置处理标志
  1943. this.isProcessing = true;
  1944. this.isDecodingContent = true;
  1945.  
  1946. try {
  1947. // 处理每个变化节点
  1948. let validDecodedCount = 0;
  1949. const nodesToReplace = [];
  1950. const processedMatches = new Set();
  1951.  
  1952. // 递归处理节点及其子节点
  1953. const processNode = (node) => {
  1954. // 如果是文本节点,处理其内容
  1955. if (node.nodeType === 3 && node.nodeValue?.trim()) {
  1956. const { modified, newHtml, count } = this.processMatches(node.nodeValue, processedMatches);
  1957. if (modified) {
  1958. nodesToReplace.push({ node, newHtml });
  1959. validDecodedCount += count;
  1960. }
  1961. } else if (node.nodeType === 1) {
  1962. // 如果是元素节点,递归处理其子节点
  1963. const excludeTags = new Set([
  1964. 'script', 'style', 'noscript', 'iframe', 'img', 'input', 'textarea',
  1965. 'svg', 'canvas', 'template', 'pre', 'code', 'button', 'meta', 'link'
  1966. ]);
  1967.  
  1968. // 跳过排除的标签和已解码的元素
  1969. if (excludeTags.has(node.tagName.toLowerCase()) ||
  1970. node.classList?.contains('decoded-text') ||
  1971. node.closest?.('.decoded-text')) {
  1972. return;
  1973. }
  1974.  
  1975. // 递归处理子节点
  1976. for (const child of node.childNodes) {
  1977. processNode(child);
  1978. }
  1979. }
  1980. };
  1981.  
  1982. // 处理所有变化节点
  1983. for (const node of changedNodes) {
  1984. processNode(node);
  1985. }
  1986.  
  1987. // 如果没有找到有效的解码内容
  1988. if (validDecodedCount === 0) {
  1989. console.log('增量解码未发现有效 Base64 内容');
  1990. // 重置处理标志
  1991. this.isProcessing = false;
  1992. this.isDecodingContent = false;
  1993. return;
  1994. }
  1995.  
  1996. // 分批处理节点替换,避免大量 DOM 操作导致界面冻结
  1997. const BATCH_SIZE = 50;
  1998. const processNodesBatch = async (startIndex) => {
  1999. const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length);
  2000. const batch = nodesToReplace.slice(startIndex, endIndex);
  2001.  
  2002. this.replaceNodes(batch);
  2003.  
  2004. if (endIndex < nodesToReplace.length) {
  2005. // 还有更多节点需要处理,安排下一批
  2006. setTimeout(() => processNodesBatch(endIndex), 0);
  2007. } else {
  2008. // 所有节点处理完成,添加点击监听器
  2009. await this.addClickListenersToDecodedText();
  2010.  
  2011. // 更新按钮状态
  2012. if (this.decodeBtn) {
  2013. this.decodeBtn.textContent = '恢复本页 Base64';
  2014. this.decodeBtn.dataset.mode = 'restore';
  2015. }
  2016.  
  2017. // 显示通知,除非被抑制
  2018. if (!this.suppressNotification) {
  2019. this.showNotification(
  2020. `解码成功,共找到 ${validDecodedCount} Base64 内容`,
  2021. 'success'
  2022. );
  2023. }
  2024.  
  2025. // 操作完成后同步按钮和菜单状态
  2026. this.syncButtonAndMenuState();
  2027.  
  2028. // 重置处理标志
  2029. this.isProcessing = false;
  2030. this.isDecodingContent = false;
  2031.  
  2032. // 更新最后解码时间
  2033. this.lastDecodeTime = Date.now();
  2034. }
  2035. };
  2036.  
  2037. // 开始分批处理
  2038. await processNodesBatch(0);
  2039. } catch (e) {
  2040. console.error('增量解码处理错误:', e);
  2041. // 检查是否有成功解码的内容
  2042. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  2043. // 更新按钮状态
  2044. if (hasDecodedContent) {
  2045. // 如果有成功解码的内容,更新按钮状态但不显示通知
  2046. // 在自动解码模式下,静默处理部分解码失败的情况
  2047. if (this.decodeBtn) {
  2048. this.decodeBtn.textContent = '恢复本页 Base64';
  2049. this.decodeBtn.dataset.mode = 'restore';
  2050. }
  2051. // 操作完成后同步按钮和菜单状态
  2052. this.syncButtonAndMenuState();
  2053. } else {
  2054. // 如果没有成功解码的内容,不显示失败通知
  2055. // 在自动解码模式下,静默处理解码失败的情况
  2056. console.log('自动解码未发现有效内容,静默处理');
  2057. }
  2058. // 重置处理标志
  2059. this.isProcessing = false;
  2060. this.isDecodingContent = false;
  2061. // 更新最后解码时间
  2062. this.lastDecodeTime = Date.now();
  2063. }
  2064. }
  2065.  
  2066. /**
  2067. * 处理文本中的Base64匹配项
  2068. * @description 查找并处理文本中的Base64编码内容
  2069. * @param {string} text - 要处理的文本内容
  2070. * @param {Set} processedMatches - 已处理过的匹配项集合
  2071. * @returns {Object} 处理结果
  2072. * @property {boolean} modified - 文本是否被修改
  2073. * @property {string} newHtml - 处理后的HTML内容
  2074. * @property {number} count - 处理的Base64数量
  2075. */
  2076. processMatches(text, processedMatches) {
  2077. const matches = Array.from(text.matchAll(BASE64_REGEX));
  2078. if (!matches.length) return { modified: false, newHtml: text, count: 0 };
  2079.  
  2080. let modified = false;
  2081. let newHtml = text;
  2082. let count = 0;
  2083.  
  2084. for (const match of matches.reverse()) {
  2085. const original = match[0];
  2086.  
  2087. // 使用 validateBase64 进行验证
  2088. if (!this.validateBase64(original)) {
  2089. console.log('Skipped: invalid Base64 string');
  2090. continue;
  2091. }
  2092.  
  2093. try {
  2094. const decoded = this.decodeBase64(original);
  2095. console.log('Decoded:', decoded);
  2096.  
  2097. if (!decoded) {
  2098. console.log('Skipped: decode failed');
  2099. continue;
  2100. }
  2101.  
  2102. // 将原始Base64和位置信息添加到已处理集合中,防止重复处理
  2103. const matchKey = `${original}-${match.index}`;
  2104. processedMatches.add(matchKey);
  2105.  
  2106. // 创建解码文本节点
  2107. const span = document.createElement('span');
  2108. span.className = 'decoded-text';
  2109. span.title = '点击复制';
  2110. span.dataset.original = original;
  2111. span.textContent = decoded;
  2112.  
  2113. // 直接添加点击事件监听器
  2114. span.addEventListener('click', async (e) => {
  2115. e.preventDefault();
  2116. e.stopPropagation();
  2117. const success = await this.copyToClipboard(decoded);
  2118. this.debouncedShowNotification(
  2119. success ? '已复制文本内容' : '复制失败,请手动复制',
  2120. success ? 'success' : 'error'
  2121. );
  2122. });
  2123.  
  2124. // 构建新的HTML内容
  2125. const beforeMatch = newHtml.substring(0, match.index);
  2126. const afterMatch = newHtml.substring(match.index + original.length);
  2127. newHtml = beforeMatch + span.outerHTML + afterMatch;
  2128.  
  2129. // 标记内容已被修改
  2130. modified = true;
  2131. // 增加成功解码计数
  2132. count++;
  2133.  
  2134. // 记录日志
  2135. console.log('成功解码: 发现有意义的文本或中文字符');
  2136. } catch (e) {
  2137. console.error('Error processing:', e);
  2138. continue;
  2139. }
  2140. }
  2141.  
  2142. return { modified, newHtml, count };
  2143. }
  2144.  
  2145. /**
  2146. * 判断文本是否有意义
  2147. * @description 通过一系列规则判断解码后的文本是否具有实际意义
  2148. * @param {string} text - 要验证的文本
  2149. * @returns {boolean} 如果文本有意义返回true,否则返回false
  2150. */
  2151. isMeaningfulText(text) {
  2152. // 1. 基本字符检查
  2153. if (!text || typeof text !== 'string') return false;
  2154.  
  2155. // 2. 长度检查
  2156. if (text.length < 2 || text.length > 10000) return false;
  2157.  
  2158. // 3. 文本质量检查
  2159. const stats = {
  2160. printable: 0, // 可打印字符
  2161. control: 0, // 控制字符
  2162. chinese: 0, // 中文字符
  2163. letters: 0, // 英文字母
  2164. numbers: 0, // 数字
  2165. punctuation: 0, // 标点符号
  2166. spaces: 0, // 空格
  2167. other: 0, // 其他字符
  2168. };
  2169.  
  2170. // 统计字符分布
  2171. for (let i = 0; i < text.length; i++) {
  2172. const char = text.charAt(i);
  2173. const code = text.charCodeAt(i);
  2174.  
  2175. if (/[\u4E00-\u9FFF]/.test(char)) {
  2176. stats.chinese++;
  2177. stats.printable++;
  2178. } else if (/[a-zA-Z]/.test(char)) {
  2179. stats.letters++;
  2180. stats.printable++;
  2181. } else if (/[0-9]/.test(char)) {
  2182. stats.numbers++;
  2183. stats.printable++;
  2184. } else if (/[\s]/.test(char)) {
  2185. stats.spaces++;
  2186. stats.printable++;
  2187. } else if (/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(char)) {
  2188. stats.punctuation++;
  2189. stats.printable++;
  2190. } else if (code < 32 || code === 127) {
  2191. stats.control++;
  2192. } else {
  2193. stats.other++;
  2194. }
  2195. }
  2196.  
  2197. // 4. 质量评估规则
  2198. const totalChars = text.length;
  2199. const printableRatio = stats.printable / totalChars;
  2200. const controlRatio = stats.control / totalChars;
  2201. const meaningfulRatio =
  2202. (stats.chinese + stats.letters + stats.numbers) / totalChars;
  2203.  
  2204. // 判断条件:
  2205. // 1. 可打印字符比例必须大于90%
  2206. // 2. 控制字符比例必须小于5%
  2207. // 3. 有意义字符(中文、英文、数字)比例必须大于30%
  2208. // 4. 空格比例不能过高(小于50%)
  2209. // 5. 其他字符比例必须很低(小于10%)
  2210. return (
  2211. printableRatio > 0.9 &&
  2212. controlRatio < 0.05 &&
  2213. meaningfulRatio > 0.3 &&
  2214. stats.spaces / totalChars < 0.5 &&
  2215. stats.other / totalChars < 0.1
  2216. );
  2217. }
  2218.  
  2219. /**
  2220. * 替换页面中的节点
  2221. * @description 使用新的HTML内容替换原有节点
  2222. * @param {Array} nodesToReplace - 需要替换的节点数组
  2223. * @param {Node} nodesToReplace[].node - 原始节点
  2224. * @param {string} nodesToReplace[].newHtml - 新的HTML内容
  2225. */
  2226. replaceNodes(nodesToReplace) {
  2227. nodesToReplace.forEach(({ node, newHtml }) => {
  2228. if (node && node.parentNode) {
  2229. // 创建临时容器
  2230. const temp = document.createElement('div');
  2231. temp.innerHTML = newHtml;
  2232.  
  2233. // 替换节点
  2234. while (temp.firstChild) {
  2235. node.parentNode.insertBefore(temp.firstChild, node);
  2236. }
  2237. node.parentNode.removeChild(node);
  2238. }
  2239. });
  2240. }
  2241.  
  2242. /**
  2243. * 为解码后的文本添加点击复制功能
  2244. * @description 为所有解码后的文本元素添加点击事件监听器
  2245. * @fires copyToClipboard 点击时触发复制操作
  2246. * @fires showNotification 显示复制结果通知
  2247. */
  2248. async addClickListenersToDecodedText() {
  2249. // 等待 DOM 更新完成
  2250. await this.waitForDOMUpdate();
  2251.  
  2252. // 获取所有解码文本节点
  2253. const decodedTextNodes = document.querySelectorAll('.decoded-text');
  2254. console.log('找到解码文本节点数量:', decodedTextNodes.length);
  2255.  
  2256. decodedTextNodes.forEach((el) => {
  2257. // 检查是否已经有事件监听器
  2258. if (!el.hasAttribute('data-has-listener')) {
  2259. // 添加新的事件监听器
  2260. el.addEventListener('click', async (e) => {
  2261. e.preventDefault();
  2262. e.stopPropagation();
  2263. const success = await this.copyToClipboard(e.target.textContent);
  2264. this.debouncedShowNotification(
  2265. success ? '已复制文本内容' : '复制失败,请手动复制',
  2266. success ? 'success' : 'error'
  2267. );
  2268. });
  2269.  
  2270. // 标记节点已添加事件监听器
  2271. el.setAttribute('data-has-listener', 'true');
  2272. console.log('已为节点添加点击事件监听器');
  2273. }
  2274. });
  2275. }
  2276.  
  2277. /**
  2278. * 处理文本编码为Base64
  2279. * @description 提示用户输入文本并转换为Base64格式
  2280. * @async
  2281. * @fires showNotification 显示编码结果通知
  2282. * @fires copyToClipboard 复制编码结果到剪贴板
  2283. */
  2284. async handleEncode() {
  2285. // 隐藏菜单
  2286. if (this.menu && this.menu.style.display !== 'none') {
  2287. this.menu.style.display = 'none';
  2288. this.menuVisible = false;
  2289. }
  2290.  
  2291. const text = prompt('请输入要编码的文本:');
  2292. if (text === null) return; // 用户点击取消
  2293.  
  2294. // 添加空输入检查
  2295. if (!text.trim()) {
  2296. this.debouncedShowNotification('请输入有效的文本内容', 'error');
  2297. return;
  2298. }
  2299.  
  2300. try {
  2301. // 处理输入文本:去除首尾空格和多余的换行符
  2302. const processedText = text.trim().replace(/[\r\n]+/g, '\n');
  2303. const encoded = this.encodeBase64(processedText);
  2304. const success = await this.copyToClipboard(encoded);
  2305. this.debouncedShowNotification(
  2306. success
  2307. ? 'Base64 已复制'
  2308. : '编码成功但复制失败,请手动复制:' + encoded,
  2309. success ? 'success' : 'info'
  2310. );
  2311. } catch (e) {
  2312. this.debouncedShowNotification('编码失败: ' + e.message, 'error');
  2313. }
  2314. }
  2315.  
  2316. /**
  2317. * 验证Base64字符串
  2318. * @description 检查字符串是否为有效的Base64格式
  2319. * @param {string} str - 要验证的字符串
  2320. * @returns {boolean} 如果是有效的Base64返回true,否则返回false
  2321. * @example
  2322. * validateBase64('SGVsbG8gV29ybGQ=') // returns true
  2323. * validateBase64('Invalid-Base64') // returns false
  2324. */
  2325. validateBase64(str) {
  2326. if (!str) return false;
  2327.  
  2328. // 使用缓存避免重复验证
  2329. if (this.base64Cache.has(str)) {
  2330. return this.base64Cache.get(str);
  2331. }
  2332.  
  2333. // 检查缓存大小并在必要时清理
  2334. if (this.base64Cache.size >= this.MAX_CACHE_SIZE) {
  2335. // 删除最早添加的缓存项
  2336. const oldestKey = this.base64Cache.keys().next().value;
  2337. this.base64Cache.delete(oldestKey);
  2338. }
  2339.  
  2340. // 1. 基本格式检查
  2341. // - 长度必须是4的倍数
  2342. // - 只允许包含合法的Base64字符
  2343. // - =号只能出现在末尾,且最多2个
  2344. if (
  2345. !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
  2346. str
  2347. )
  2348. ) {
  2349. this.base64Cache.set(str, false);
  2350. return false;
  2351. }
  2352.  
  2353. // 2. 长度检查
  2354. // 过滤掉太短的字符串(至少8个字符)和过长的字符串(最多10000个字符)
  2355. if (str.length < 8 || str.length > 10000) {
  2356. this.base64Cache.set(str, false);
  2357. return false;
  2358. }
  2359.  
  2360. // 3. 特征检查
  2361. // 过滤掉可能是图片、视频等二进制数据的Base64
  2362. if (/^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER)/.test(str)) {
  2363. this.base64Cache.set(str, false);
  2364. return false;
  2365. }
  2366.  
  2367. // 添加到 validateBase64 方法中
  2368. const commonPatterns = {
  2369. // 常见的二进制数据头部特征
  2370. binaryHeaders:
  2371. /^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER|UEsDB|H4sIA|77u\/|0M8R4)/,
  2372.  
  2373. // 常见的文件类型标识
  2374. fileSignatures: /^(?:UEs|PK|%PDF|GIF8|RIFF|OggS|ID3|ÿØÿ|8BPS)/,
  2375.  
  2376. // 常见的编码标识
  2377. encodingMarkers:
  2378. /^(?:utf-8|utf-16|base64|quoted-printable|7bit|8bit|binary)/i,
  2379.  
  2380. // 可疑的URL模式
  2381. urlPatterns: /^(?:https?:|ftp:|data:|blob:|file:|ws:|wss:)/i,
  2382.  
  2383. // 常见的压缩文件头部
  2384. compressedHeaders: /^(?:eJw|H4s|Qk1Q|UEsD|N3q8|KLUv)/,
  2385. };
  2386.  
  2387. // 在验证时使用这些模式
  2388. if (
  2389. commonPatterns.binaryHeaders.test(str) ||
  2390. commonPatterns.fileSignatures.test(str) ||
  2391. commonPatterns.encodingMarkers.test(str) ||
  2392. commonPatterns.urlPatterns.test(str) ||
  2393. commonPatterns.compressedHeaders.test(str)
  2394. ) {
  2395. this.base64Cache.set(str, false);
  2396. return false;
  2397. }
  2398.  
  2399. try {
  2400. const decoded = this.decodeBase64(str);
  2401. if (!decoded) {
  2402. this.base64Cache.set(str, false);
  2403. return false;
  2404. }
  2405.  
  2406. // 4. 解码后的文本验证
  2407. // 检查解码后的文本是否有意义
  2408. if (!this.isMeaningfulText(decoded)) {
  2409. this.base64Cache.set(str, false);
  2410. return false;
  2411. }
  2412.  
  2413. this.base64Cache.set(str, true);
  2414. return true;
  2415. } catch (e) {
  2416. console.error('Base64 validation error:', e);
  2417. this.base64Cache.set(str, false);
  2418. return false;
  2419. }
  2420. }
  2421.  
  2422. /**
  2423. * Base64解码
  2424. * @description 将Base64字符串解码为普通文本
  2425. * @param {string} str - 要解码的Base64字符串
  2426. * @returns {string|null} 解码后的文本,解码失败时返回null
  2427. * @example
  2428. * decodeBase64('SGVsbG8gV29ybGQ=') // returns 'Hello World'
  2429. */
  2430. decodeBase64(str) {
  2431. try {
  2432. // 优化解码过程
  2433. const binaryStr = atob(str);
  2434. const bytes = new Uint8Array(binaryStr.length);
  2435. for (let i = 0; i < binaryStr.length; i++) {
  2436. bytes[i] = binaryStr.charCodeAt(i);
  2437. }
  2438. return new TextDecoder().decode(bytes);
  2439. } catch (e) {
  2440. console.error('Base64 decode error:', e);
  2441. return null;
  2442. }
  2443. }
  2444.  
  2445. /**
  2446. * Base64编码
  2447. * @description 将普通文本编码为Base64格式
  2448. * @param {string} str - 要编码的文本
  2449. * @returns {string|null} Base64编码后的字符串,编码失败时返回null
  2450. * @example
  2451. * encodeBase64('Hello World') // returns 'SGVsbG8gV29ybGQ='
  2452. */
  2453. encodeBase64(str) {
  2454. try {
  2455. // 优化编码过程
  2456. const bytes = new TextEncoder().encode(str);
  2457. let binaryStr = '';
  2458. for (let i = 0; i < bytes.length; i++) {
  2459. binaryStr += String.fromCharCode(bytes[i]);
  2460. }
  2461. return btoa(binaryStr);
  2462. } catch (e) {
  2463. console.error('Base64 encode error:', e);
  2464. return null;
  2465. }
  2466. }
  2467.  
  2468. /**
  2469. * 复制文本到剪贴板
  2470. * @description 尝试使用现代API或降级方案将文本复制到剪贴板
  2471. * @param {string} text - 要复制的文本
  2472. * @returns {Promise<boolean>} 复制是否成功
  2473. * @example
  2474. * await copyToClipboard('Hello World') // returns true
  2475. */
  2476. async copyToClipboard(text) {
  2477. if (navigator.clipboard && window.isSecureContext) {
  2478. try {
  2479. await navigator.clipboard.writeText(text);
  2480. return true;
  2481. } catch (e) {
  2482. return this.fallbackCopy(text);
  2483. }
  2484. }
  2485.  
  2486. return this.fallbackCopy(text);
  2487. }
  2488.  
  2489. /**
  2490. * 降级复制方案
  2491. * @description 当现代复制API不可用时的备选复制方案
  2492. * @param {string} text - 要复制的文本
  2493. * @returns {boolean} 复制是否成功
  2494. * @private
  2495. */
  2496. fallbackCopy(text) {
  2497. if (typeof GM_setClipboard !== 'undefined') {
  2498. try {
  2499. GM_setClipboard(text);
  2500. return true;
  2501. } catch (e) {
  2502. console.debug('GM_setClipboard failed:', e);
  2503. }
  2504. }
  2505.  
  2506. try {
  2507. // 注意: execCommand 已经被废弃,但作为降级方案仍然有用
  2508. const textarea = document.createElement('textarea');
  2509. textarea.value = text;
  2510. textarea.style.cssText = 'position:fixed;opacity:0;';
  2511. document.body.appendChild(textarea);
  2512.  
  2513. if (navigator.userAgent.match(/ipad|iphone/i)) {
  2514. textarea.contentEditable = true;
  2515. textarea.readOnly = false;
  2516.  
  2517. const range = document.createRange();
  2518. range.selectNodeContents(textarea);
  2519.  
  2520. const selection = window.getSelection();
  2521. selection.removeAllRanges();
  2522. selection.addRange(range);
  2523. textarea.setSelectionRange(0, 999999);
  2524. } else {
  2525. textarea.select();
  2526. }
  2527.  
  2528. // 使用 try-catch 包裹 execCommand 调用,以防将来完全移除
  2529. let success = false;
  2530. try {
  2531. // @ts-ignore - 忽略废弃警告
  2532. success = document.execCommand('copy');
  2533. } catch (copyError) {
  2534. console.debug('execCommand copy operation failed:', copyError);
  2535. }
  2536.  
  2537. document.body.removeChild(textarea);
  2538. return success;
  2539. } catch (e) {
  2540. console.debug('Fallback copy method failed:', e);
  2541. return false;
  2542. }
  2543. }
  2544.  
  2545. /**
  2546. * 恢复原始内容
  2547. * @description 将所有解码后的内容恢复为原始的Base64格式
  2548. * @fires showNotification 显示恢复结果通知
  2549. */
  2550. restoreContent() {
  2551. // 设置恢复内容标志,防止重复处理
  2552. if (this.isRestoringContent) {
  2553. console.log('已经在恢复内容中,避免重复操作');
  2554. return;
  2555. }
  2556.  
  2557. this.isRestoringContent = true;
  2558.  
  2559. try {
  2560. // 获取所有需要恢复的元素
  2561. const elementsToRestore = Array.from(document.querySelectorAll('.decoded-text'));
  2562. if (elementsToRestore.length === 0) {
  2563. this.showNotification('没有需要恢复的内容', 'info');
  2564. this.isRestoringContent = false;
  2565. return;
  2566. }
  2567.  
  2568. // 分批处理节点替换,避免大量 DOM 操作导致界面冻结
  2569. const BATCH_SIZE = 50;
  2570. const processBatch = (startIndex) => {
  2571. const endIndex = Math.min(startIndex + BATCH_SIZE, elementsToRestore.length);
  2572. const batch = elementsToRestore.slice(startIndex, endIndex);
  2573.  
  2574. batch.forEach((el) => {
  2575. if (el && el.parentNode && el.dataset.original) {
  2576. const textNode = document.createTextNode(el.dataset.original);
  2577. el.parentNode.replaceChild(textNode, el);
  2578. }
  2579. });
  2580.  
  2581. if (endIndex < elementsToRestore.length) {
  2582. // 还有更多元素需要处理
  2583. setTimeout(() => processBatch(endIndex), 0);
  2584. } else {
  2585. // 所有元素处理完成
  2586. this.originalContents.clear();
  2587.  
  2588. // 如果按钮存在,更新按钮状态
  2589. if (this.decodeBtn) {
  2590. this.decodeBtn.textContent = '解析本页 Base64';
  2591. this.decodeBtn.dataset.mode = 'decode';
  2592. }
  2593.  
  2594. // 获取已恢复元素的数量
  2595. const restoredCount = elementsToRestore.length;
  2596. // 显示通知,除非被抑制
  2597. if (!this.suppressNotification) {
  2598. this.showNotification(`已恢复 ${restoredCount} Base64 内容`, 'success');
  2599. }
  2600.  
  2601. // 只有当按钮可见时才隐藏菜单
  2602. if (!this.config.hideButton && this.menu) {
  2603. this.menu.style.display = 'none';
  2604. }
  2605.  
  2606. // 操作完成后同步按钮和菜单状态
  2607. this.syncButtonAndMenuState();
  2608. // 更新油猴菜单命令
  2609. updateMenuCommands();
  2610.  
  2611. // 重置恢复内容标志
  2612. this.isRestoringContent = false;
  2613. }
  2614. };
  2615.  
  2616. // 开始处理第一批
  2617. processBatch(0);
  2618. } catch (e) {
  2619. console.error('恢复内容时出错:', e);
  2620. this.showNotification(`恢复失败: ${e.message}`, 'error');
  2621. this.isRestoringContent = false;
  2622. }
  2623. }
  2624.  
  2625. /**
  2626. * 同步按钮和菜单状态
  2627. * @description 根据页面上是否有解码内容,同步按钮和菜单状态
  2628. */
  2629. syncButtonAndMenuState() {
  2630. // 检查页面上是否有解码内容
  2631. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  2632.  
  2633. // 同步按钮状态
  2634. if (this.decodeBtn) {
  2635. if (hasDecodedContent) {
  2636. this.decodeBtn.textContent = '恢复本页 Base64';
  2637. this.decodeBtn.dataset.mode = 'restore';
  2638. } else {
  2639. this.decodeBtn.textContent = '解析本页 Base64';
  2640. this.decodeBtn.dataset.mode = 'decode';
  2641. }
  2642. }
  2643.  
  2644. // 更新菜单命令
  2645. setTimeout(updateMenuCommands, 100);
  2646. }
  2647.  
  2648. /**
  2649. * 重置插件状态
  2650. * @description 重置所有状态变量并在必要时恢复原始内容
  2651. * 如果启用了自动解码,则在路由变化后自动解析页面
  2652. * @fires restoreContent 如果当前处于restore模式则触发内容恢复
  2653. * @fires handleDecode 如果启用了自动解码则触发自动解码
  2654. */
  2655. resetState() {
  2656. console.log('执行 resetState,自动解码状态:', this.config.autoDecode);
  2657.  
  2658. // 如果正在处理中,跳过这次重置
  2659. if (this.isProcessing || this.isDecodingContent || this.isRestoringContent) {
  2660. console.log('正在处理中,跳过这次状态重置');
  2661. return;
  2662. }
  2663.  
  2664. // 检查URL是否变化,如果变化了,可能是新页面
  2665. const currentUrl = window.location.href;
  2666. const urlChanged = currentUrl !== this.lastPageUrl;
  2667. // 检查是否是前进后退事件
  2668. const isNavigationEvent = this.lastNavigationTime && (Date.now() - this.lastNavigationTime < 500);
  2669.  
  2670. if (urlChanged || isNavigationEvent) {
  2671. console.log('URL已变化或检测到前进后退事件,从', this.lastPageUrl, '到', currentUrl);
  2672. this.lastPageUrl = currentUrl;
  2673. // URL变化或前进后退时重置自动解码标志
  2674. this.hasAutoDecodedOnLoad = false;
  2675. }
  2676.  
  2677. // 页面刷新时的特殊处理
  2678. if (this.isPageRefresh && this.config.autoDecode) {
  2679. console.log('页面刷新且自动解码已启用');
  2680.  
  2681. // 如果页面刷新尚未完成,不执行任何操作,等待页面完全加载
  2682. if (!this.pageRefreshCompleted) {
  2683. console.log('页面刷新尚未完成,等待页面加载完成后再处理');
  2684. return;
  2685. }
  2686.  
  2687. // 检查是否已经执行过解码,避免重复解码
  2688. if (this.hasAutoDecodedOnLoad || document.querySelectorAll('.decoded-text').length > 0) {
  2689. console.log('页面已经执行过解码或已有解码内容,跳过重复解码');
  2690. return;
  2691. }
  2692.  
  2693. // 页面上没有已解码内容,执行自动解码
  2694. console.log('页面刷新时未发现已解码内容,执行自动解码');
  2695. this.hasAutoDecodedOnLoad = true;
  2696. // 增加延时,确保页面内容已完全加载
  2697. setTimeout(() => {
  2698. if (!this.isProcessing && !this.isDecodingContent && !this.isRestoringContent) {
  2699. // 使用processTextNodes方法进行解码
  2700. const { nodesToReplace, validDecodedCount } = this.processTextNodes();
  2701.  
  2702. if (validDecodedCount > 0) {
  2703. // 分批处理节点替换
  2704. const BATCH_SIZE = 50;
  2705. const processNodesBatch = (startIndex) => {
  2706. const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length);
  2707. const batch = nodesToReplace.slice(startIndex, endIndex);
  2708.  
  2709. this.replaceNodes(batch);
  2710.  
  2711. if (endIndex < nodesToReplace.length) {
  2712. setTimeout(() => processNodesBatch(endIndex), 0);
  2713. } else {
  2714. setTimeout(() => {
  2715. this.addClickListenersToDecodedText();
  2716. this.debouncedShowNotification(`解码成功,共找到 ${validDecodedCount} Base64 内容`, 'success');
  2717. this.syncButtonAndMenuState();
  2718. }, 100);
  2719. }
  2720. };
  2721.  
  2722. processNodesBatch(0);
  2723. }
  2724. }
  2725. }, 1000);
  2726. return;
  2727. }
  2728.  
  2729. // 如果启用了自动解码,且尚未在页面加载时执行过,则在路由变化后自动解析页面
  2730. if (this.config.autoDecode && !this.hasAutoDecodedOnLoad) {
  2731. console.log('自动解码已启用,准备解析页面');
  2732. // 标记已执行过自动解码
  2733. this.hasAutoDecodedOnLoad = true;
  2734.  
  2735. // 使用统一的页面稳定性跟踪器
  2736. const tracker = this.pageStabilityTracker;
  2737.  
  2738. // 记录路由变化
  2739. tracker.recordChange('Route');
  2740.  
  2741. // 标记有待处理的解码请求
  2742. if (this.config.autoDecode) {
  2743. tracker.pendingDecode = true;
  2744. }
  2745.  
  2746. // 设置页面稳定性定时器
  2747. tracker.stabilityTimer = setTimeout(() => {
  2748. // 检查页面是否真正稳定(路由和DOM都稳定)
  2749. if (tracker.checkStability()) {
  2750. console.log('页面已稳定(路由和DOM都稳定),准备在 resetState 中执行自动解码');
  2751.  
  2752. // 标记页面已稳定
  2753. tracker.isStable = true;
  2754.  
  2755. // 如果有待处理的解码请求,则执行解码
  2756. if (tracker.pendingDecode && this.config.autoDecode) {
  2757. // 使用延时确保页面内容已更新
  2758. tracker.decodePendingTimer = setTimeout(() => {
  2759. // 重置待处理标志
  2760. tracker.pendingDecode = false;
  2761.  
  2762. console.log('resetState 中执行自动解码');
  2763. if (
  2764. !this.isProcessing &&
  2765. !this.isDecodingContent &&
  2766. !this.isRestoringContent
  2767. ) {
  2768. // 使用自动解码方法,强制全页面解码并显示通知
  2769. console.log('在resetState中强制执行全页面解码');
  2770. this.handleAutoDecode(true, true);
  2771. // 同步按钮和菜单状态
  2772. setTimeout(() => this.syncButtonAndMenuState(), 200);
  2773. }
  2774.  
  2775. }, 500); // 页面稳定后再等待500毫秒再执行解码
  2776. }
  2777. } else {
  2778. console.log('页面尚未完全稳定,继续等待');
  2779. }
  2780. }, tracker.stabilityThreshold); // 等待页面稳定的时间
  2781. }
  2782. }
  2783.  
  2784. /**
  2785. * 为通知添加动画效果
  2786. * @param {HTMLElement} notification - 通知元素
  2787. */
  2788. animateNotification(notification) {
  2789. const currentTransform = getComputedStyle(notification).transform;
  2790. notification.style.transform = currentTransform;
  2791. notification.style.transition = 'all 0.3s ease-out';
  2792. notification.style.transform = 'translateY(-100%)';
  2793. }
  2794.  
  2795. /**
  2796. * 处理通知淡出效果
  2797. * @description 为通知添加淡出效果并处理相关动画
  2798. * @param {HTMLElement} notification - 要处理的通知元素
  2799. * @fires animateNotification 触发其他通知的位置调整动画
  2800. */
  2801. handleNotificationFadeOut(notification) {
  2802. notification.classList.add('fade-out');
  2803. const index = this.notifications.indexOf(notification);
  2804.  
  2805. this.notifications.slice(0, index).forEach((prev) => {
  2806. if (prev.parentNode) {
  2807. prev.style.transform = 'translateY(-100%)';
  2808. }
  2809. });
  2810. }
  2811.  
  2812. /**
  2813. * 清理通知容器
  2814. * @description 移除所有通知元素和相关事件监听器
  2815. * @fires removeEventListener 移除所有通知相关的事件监听器
  2816. */
  2817. cleanupNotificationContainer() {
  2818. // 清理通知相关的事件监听器
  2819. this.notificationEventListeners.forEach(({ element, event, handler }) => {
  2820. element.removeEventListener(event, handler);
  2821. });
  2822. this.notificationEventListeners = [];
  2823.  
  2824. // 移除所有通知元素
  2825. while (this.notificationContainer.firstChild) {
  2826. this.notificationContainer.firstChild.remove();
  2827. }
  2828.  
  2829. this.notificationContainer.remove();
  2830. this.notificationContainer = null;
  2831. }
  2832.  
  2833. /**
  2834. * 处理通知过渡结束事件
  2835. * @description 处理通知元素的过渡动画结束后的清理工作
  2836. * @param {TransitionEvent} e - 过渡事件对象
  2837. * @fires animateNotification 触发其他通知的位置调整
  2838. */
  2839. handleNotificationTransitionEnd(e) {
  2840. if (
  2841. e.propertyName === 'opacity' &&
  2842. e.target.classList.contains('fade-out')
  2843. ) {
  2844. const notification = e.target;
  2845. const index = this.notifications.indexOf(notification);
  2846.  
  2847. this.notifications.forEach((notif, i) => {
  2848. if (i > index && notif.parentNode) {
  2849. this.animateNotification(notif);
  2850. }
  2851. });
  2852.  
  2853. if (index > -1) {
  2854. this.notifications.splice(index, 1);
  2855. notification.remove();
  2856. }
  2857.  
  2858. if (this.notifications.length === 0) {
  2859. this.cleanupNotificationContainer();
  2860. }
  2861. }
  2862. }
  2863.  
  2864. /**
  2865. * 显示通知消息
  2866. * @description 创建并显示一个通知消息,包含自动消失功能
  2867. * @param {string} text - 通知文本内容
  2868. * @param {string} type - 通知类型 ('success'|'error'|'info')
  2869. * @fires handleNotificationFadeOut 触发通知淡出效果
  2870. * @example
  2871. * showNotification('操作成功', 'success')
  2872. */
  2873. showNotification(text, type) {
  2874. // 如果禁用了通知,则不显示
  2875. if (this.config && !this.config.showNotification) {
  2876. console.log(`[Base64 Helper] ${type}: ${text}`);
  2877. return;
  2878. }
  2879.  
  2880. // 设置通知显示标志,防止 MutationObserver 触发自动解码
  2881. this.isShowingNotification = true;
  2882.  
  2883. if (!this.notificationContainer) {
  2884. this.notificationContainer = document.createElement('div');
  2885. this.notificationContainer.className = 'base64-notifications-container';
  2886. document.body.appendChild(this.notificationContainer);
  2887.  
  2888. const handler = (e) => this.handleNotificationTransitionEnd(e);
  2889. this.notificationContainer.addEventListener('transitionend', handler);
  2890. this.notificationEventListeners.push({
  2891. element: this.notificationContainer,
  2892. event: 'transitionend',
  2893. handler,
  2894. });
  2895. }
  2896.  
  2897. const notification = document.createElement('div');
  2898. notification.className = 'base64-notification';
  2899. notification.setAttribute('data-type', type);
  2900. notification.textContent = text;
  2901.  
  2902. this.notifications.push(notification);
  2903. this.notificationContainer.appendChild(notification);
  2904.  
  2905. // 使用延时来清除通知标志,确保 DOM 变化已完成
  2906. setTimeout(() => {
  2907. this.isShowingNotification = false;
  2908. }, 100);
  2909.  
  2910. setTimeout(() => {
  2911. if (notification.parentNode) {
  2912. this.handleNotificationFadeOut(notification);
  2913. }
  2914. }, 2000);
  2915. }
  2916.  
  2917. /**
  2918. * 销毁插件实例
  2919. * @description 清理所有资源,移除事件监听器,恢复原始状态
  2920. * @fires restoreContent 如果需要则恢复原始内容
  2921. * @fires removeEventListener 移除所有事件监听器
  2922. */
  2923. destroy() {
  2924. // 清理所有事件监听器
  2925. this.eventListeners.forEach(({ element, event, handler, options }) => {
  2926. element.removeEventListener(event, handler, options);
  2927. });
  2928. this.eventListeners = [];
  2929.  
  2930. // 清理配置监听器
  2931. if (this.configListeners) {
  2932. Object.values(this.configListeners).forEach(listenerId => {
  2933. if (listenerId) {
  2934. storageManager.removeChangeListener(listenerId);
  2935. }
  2936. });
  2937. // 重置配置监听器
  2938. this.configListeners = {
  2939. showNotification: null,
  2940. hideButton: null,
  2941. autoDecode: null,
  2942. buttonPosition: null
  2943. };
  2944. }
  2945.  
  2946. // 清理定时器
  2947. if (this.resizeTimer) clearTimeout(this.resizeTimer);
  2948. if (this.routeTimer) clearTimeout(this.routeTimer);
  2949. if (this.domChangeTimer) clearTimeout(this.domChangeTimer);
  2950.  
  2951. // 清理 MutationObserver
  2952. if (this.observer) {
  2953. this.observer.disconnect();
  2954. this.observer = null;
  2955. }
  2956.  
  2957. // 清理通知相关资源
  2958. if (this.notificationContainer) {
  2959. this.cleanupNotificationContainer();
  2960. }
  2961. this.notifications = [];
  2962.  
  2963. // 恢复原始的 history 方法
  2964. if (this.originalPushState) history.pushState = this.originalPushState;
  2965. if (this.originalReplaceState)
  2966. history.replaceState = this.originalReplaceState;
  2967.  
  2968. // 恢复原始状态
  2969. if (this.decodeBtn?.dataset.mode === 'restore') {
  2970. this.restoreContent();
  2971. }
  2972.  
  2973. // 移除 DOM 元素
  2974. if (this.container) {
  2975. this.container.remove();
  2976. }
  2977.  
  2978. // 清理缓存
  2979. if (this.base64Cache) {
  2980. this.base64Cache.clear();
  2981. }
  2982.  
  2983. // 清理节点跟踪相关资源
  2984. this.processedNodes = null;
  2985. this.decodedTextNodes = null;
  2986. this.processedMutations = null;
  2987. this.currentMutations = null;
  2988.  
  2989. // 清理引用
  2990. this.shadowRoot = null;
  2991. this.mainBtn = null;
  2992. this.menu = null;
  2993. this.decodeBtn = null;
  2994. this.encodeBtn = null;
  2995. this.container = null;
  2996. this.originalContents.clear();
  2997. this.originalContents = null;
  2998. this.isDragging = false;
  2999. this.hasMoved = false;
  3000. this.menuVisible = false;
  3001. this.base64Cache = null;
  3002. this.configListeners = null;
  3003. }
  3004.  
  3005. /**
  3006. * 防抖处理解码
  3007. * @param {boolean} [forceFullDecode=false] - 是否强制全页面解码
  3008. * @param {boolean} [showNotification=false] - 是否显示通知
  3009. */
  3010. debouncedHandleAutoDecode(forceFullDecode = false, showNotification = false) {
  3011. // 清除之前的定时器
  3012. clearTimeout(this.decodeDebounceTimer);
  3013.  
  3014. // 检查距离上次解码的时间
  3015. const currentTime = Date.now();
  3016. if (currentTime - this.lastDecodeTime < this.DECODE_DEBOUNCE_DELAY) {
  3017. console.log('距离上次解码时间太短,跳过这次解码');
  3018. return;
  3019. }
  3020.  
  3021. // 设置新的定时器
  3022. this.decodeDebounceTimer = setTimeout(() => {
  3023. this.handleAutoDecode(forceFullDecode, showNotification);
  3024. this.lastDecodeTime = Date.now();
  3025. }, 500); // 添加500ms的延迟,确保页面内容已更新
  3026. }
  3027.  
  3028. /**
  3029. * 防抖处理通知
  3030. * @param {string} text - 通知文本
  3031. * @param {string} type - 通知类型
  3032. */
  3033. debouncedShowNotification(text, type) {
  3034. this.showNotification(text, type);
  3035. }
  3036.  
  3037. /**
  3038. * 等待 DOM 更新完成后再添加事件监听器
  3039. * @description 使用 MutationObserver 监听 DOM 变化,确保在节点真正被添加到 DOM 后再添加事件监听器
  3040. * @private
  3041. */
  3042. waitForDOMUpdate() {
  3043. return new Promise((resolve) => {
  3044. // 先检查是否已经有解码文本节点
  3045. const existingNodes = document.querySelectorAll('.decoded-text');
  3046. if (existingNodes.length > 0) {
  3047. resolve();
  3048. return;
  3049. }
  3050.  
  3051. const observer = new MutationObserver((mutations) => {
  3052. // 检查是否有新的 decoded-text 节点
  3053. const hasNewNodes = document.querySelectorAll('.decoded-text').length > 0;
  3054. if (hasNewNodes) {
  3055. observer.disconnect();
  3056. resolve();
  3057. }
  3058. });
  3059.  
  3060. observer.observe(document.body, {
  3061. childList: true,
  3062. subtree: true
  3063. });
  3064.  
  3065. // 设置超时,防止无限等待
  3066. setTimeout(() => {
  3067. observer.disconnect();
  3068. resolve();
  3069. }, 1000);
  3070. });
  3071. }
  3072.  
  3073. /**
  3074. * 添加事件监听器
  3075. * @param {HTMLElement} element - 目标元素
  3076. * @param {string} event - 事件名称
  3077. * @param {Function} handler - 事件处理函数
  3078. * @param {Object} options - 事件选项
  3079. * @returns {string} - 监听器ID
  3080. */
  3081. addEventListener(element, event, handler, options = {}) {
  3082. const listenerId = `${element.id || 'global'}_${event}_${Date.now()}`;
  3083. element.addEventListener(event, handler, options);
  3084. this.eventListeners.set(listenerId, { element, event, handler, options });
  3085. return listenerId;
  3086. }
  3087.  
  3088. /**
  3089. * 移除事件监听器
  3090. * @param {string} listenerId - 监听器ID
  3091. */
  3092. removeEventListener(listenerId) {
  3093. const listener = this.eventListeners.get(listenerId);
  3094. if (listener) {
  3095. const { element, event, handler, options } = listener;
  3096. element.removeEventListener(event, handler, options);
  3097. this.eventListeners.delete(listenerId);
  3098. }
  3099. }
  3100.  
  3101. /**
  3102. * 清理所有事件监听器
  3103. */
  3104. cleanupEventListeners() {
  3105. for (const [listenerId, listener] of this.eventListeners) {
  3106. const { element, event, handler, options } = listener;
  3107. element.removeEventListener(event, handler, options);
  3108. }
  3109. this.eventListeners.clear();
  3110. }
  3111.  
  3112. /**
  3113. * 添加缓存项
  3114. * @param {string} key - 缓存键
  3115. * @param {string} value - 缓存值
  3116. */
  3117. addToCache(key, value) {
  3118. // 检查缓存大小,如果超过限制则清理最旧的条目
  3119. if (this.base64Cache.size >= this.MAX_CACHE_SIZE) {
  3120. const oldestKey = this.base64Cache.keys().next().value;
  3121. this.base64Cache.delete(oldestKey);
  3122. }
  3123.  
  3124. // 只缓存长度在限制范围内的文本
  3125. if (value.length <= this.MAX_TEXT_LENGTH) {
  3126. this.base64Cache.set(key, value);
  3127. }
  3128. }
  3129.  
  3130. /**
  3131. * 从缓存中获取值
  3132. * @param {string} key - 缓存键
  3133. * @returns {string|undefined} - 缓存值或undefined
  3134. */
  3135. getFromCache(key) {
  3136. const value = this.base64Cache.get(key);
  3137. if (value !== undefined) {
  3138. this.cacheHits++;
  3139. return value;
  3140. }
  3141. this.cacheMisses++;
  3142. return undefined;
  3143. }
  3144.  
  3145. /**
  3146. * 清理缓存
  3147. */
  3148. clearCache() {
  3149. this.base64Cache.clear();
  3150. this.cacheHits = 0;
  3151. this.cacheMisses = 0;
  3152. }
  3153.  
  3154. /**
  3155. * 获取缓存统计信息
  3156. * @returns {Object} - 缓存统计信息
  3157. */
  3158. getCacheStats() {
  3159. return {
  3160. size: this.base64Cache.size,
  3161. hits: this.cacheHits,
  3162. misses: this.cacheMisses,
  3163. hitRate: this.cacheHits / (this.cacheHits + this.cacheMisses) || 0
  3164. };
  3165. }
  3166.  
  3167. /**
  3168. * 处理文本节点
  3169. * @param {Text} node - 文本节点
  3170. * @returns {boolean} - 是否处理成功
  3171. */
  3172. processTextNode(node) {
  3173. if (this.processedNodes.has(node)) {
  3174. return false;
  3175. }
  3176.  
  3177. // 检查节点是否已经被处理过
  3178. const cachedResult = this.decodedTextNodes.get(node);
  3179. if (cachedResult !== undefined) {
  3180. return cachedResult;
  3181. }
  3182.  
  3183. // 处理节点
  3184. const result = this.processNodeContent(node);
  3185. this.processedNodes.add(node);
  3186. this.decodedTextNodes.set(node, result);
  3187.  
  3188. // 存储节点引用
  3189. this.nodeReferences.set(node, {
  3190. parent: node.parentNode,
  3191. nextSibling: node.nextSibling
  3192. });
  3193.  
  3194. return result;
  3195. }
  3196.  
  3197. /**
  3198. * 清理节点引用
  3199. * @param {Text} node - 要清理的节点
  3200. */
  3201. cleanupNodeReferences(node) {
  3202. this.processedNodes.delete(node);
  3203. this.decodedTextNodes.delete(node);
  3204. this.nodeReferences.delete(node);
  3205. }
  3206.  
  3207. /**
  3208. * 批量清理节点引用
  3209. * @param {Array<Text>} nodes - 要清理的节点数组
  3210. */
  3211. cleanupNodeReferencesBatch(nodes) {
  3212. nodes.forEach(node => this.cleanupNodeReferences(node));
  3213. }
  3214.  
  3215. /**
  3216. * 恢复节点状态
  3217. * @param {Text} node - 要恢复的节点
  3218. */
  3219. restoreNodeState(node) {
  3220. const reference = this.nodeReferences.get(node);
  3221. if (reference) {
  3222. const { parent, nextSibling } = reference;
  3223. if (parent && nextSibling) {
  3224. parent.insertBefore(node, nextSibling);
  3225. }
  3226. this.cleanupNodeReferences(node);
  3227. }
  3228. }
  3229. }
  3230.  
  3231. // 确保只初始化一次
  3232. if (window.__base64HelperInstance) {
  3233. return;
  3234. }
  3235.  
  3236. // 只在主窗口中初始化
  3237. if (window.top === window.self) {
  3238.  
  3239. initStyles();
  3240.  
  3241. window.__base64HelperInstance = new Base64Helper();
  3242.  
  3243. // 注册(不可用)油猴菜单命令
  3244. registerMenuCommands();
  3245.  
  3246. // 确保在页面完全加载后更新菜单命令
  3247. window.addEventListener('load', () => {
  3248. console.log('页面加载完成,更新菜单命令');
  3249. updateMenuCommands();
  3250. });
  3251. }
  3252.  
  3253. // 使用 { once: true } 确保事件监听器只添加一次
  3254. window.addEventListener(
  3255. 'unload',
  3256. () => {
  3257. if (window.__base64HelperInstance) {
  3258. window.__base64HelperInstance.destroy();
  3259. delete window.__base64HelperInstance;
  3260. }
  3261. },
  3262. { once: true }
  3263. );
  3264. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址