Websites Base64 Helper

Base64编解码工具 for all websites

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

  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.46
  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. const showNotificationEnabled = storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true);
  481. let closeNotification = false;
  482.  
  483. // 只有当通知开启时才询问用户
  484. if (showNotificationEnabled) {
  485. closeNotification = confirm('建议关闭显示通知功能,进入免打扰模式~');
  486.  
  487. // 根据用户选择设置通知状态
  488. if (closeNotification) {
  489. storageManager.set(STORAGE_KEYS.SHOW_NOTIFICATION, false);
  490. }
  491. }
  492.  
  493. // 如果按钮未隐藏,提示用户
  494. if (!storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false)) {
  495. if (confirm('建议同时隐藏按钮以获得更好的体验。')) {
  496. storageManager.set(STORAGE_KEYS.HIDE_BUTTON, true);
  497. }
  498. }
  499.  
  500. // 显示提示(使用 GM_notification 而不是内部通知)
  501. GM_notification({
  502. title: 'Base64 Helper',
  503. text: `自动解码已开启${closeNotification || !showNotificationEnabled ? ',通知已关闭' : ''}`,
  504. timeout: 3000
  505. });
  506. } else {
  507. // 使用通知提示用户设置已更改
  508. if (window.__base64HelperInstance) {
  509. window.__base64HelperInstance.showNotification(
  510. `自动解码已关闭`,
  511. 'success'
  512. );
  513. }
  514. }
  515. // 更新菜单文本
  516. setTimeout(updateMenuCommands, 100);
  517. });
  518. console.log('Registered autoDecode menu command with ID:', menuIds.autoDecode);
  519. } catch (e) {
  520. console.error('Failed to register autoDecode menu command:', e);
  521. }
  522. };
  523.  
  524. // 菜单命令注册(不可用)
  525. const registerMenuCommands = () => {
  526. // 注册(不可用)所有菜单命令
  527. updateMenuCommands();
  528.  
  529. // 添加 DOMContentLoaded 事件监听器,确保在页面加载完成后注册(不可用)菜单命令
  530. document.addEventListener('DOMContentLoaded', () => {
  531. console.log('DOMContentLoaded 事件触发,更新菜单命令');
  532. updateMenuCommands();
  533. });
  534. };
  535.  
  536. class Base64Helper {
  537. /**
  538. * Base64 Helper 类的构造函数
  539. * @description 初始化所有必要的状态和UI组件,仅在主窗口中创建实例
  540. * @throws {Error} 当在非主窗口中实例化时抛出错误
  541. */
  542. constructor() {
  543. // 确保只在主文档中创建实例
  544. if (window.top !== window.self) {
  545. throw new Error(
  546. 'Base64Helper can only be instantiated in the main window'
  547. );
  548. }
  549.  
  550. // 初始化配置
  551. this.config = {
  552. showNotification: storageManager.get(STORAGE_KEYS.SHOW_NOTIFICATION, true),
  553. hideButton: storageManager.get(STORAGE_KEYS.HIDE_BUTTON, false),
  554. autoDecode: storageManager.get(STORAGE_KEYS.AUTO_DECODE, false)
  555. };
  556.  
  557. this.originalContents = new Map();
  558. this.isDragging = false;
  559. this.hasMoved = false;
  560. this.startX = 0;
  561. this.startY = 0;
  562. this.initialX = 0;
  563. this.initialY = 0;
  564. this.startTime = 0;
  565. this.menuVisible = false;
  566. this.resizeTimer = null;
  567. this.notifications = [];
  568. this.notificationContainer = null;
  569. this.notificationEventListeners = [];
  570. this.eventListeners = [];
  571.  
  572. // 添加缓存对象
  573. this.base64Cache = new Map();
  574. this.MAX_CACHE_SIZE = 1000; // 最大缓存条目数
  575. this.MAX_TEXT_LENGTH = 10000; // 最大文本长度限制
  576.  
  577. // 初始化配置监听器
  578. this.configListeners = {
  579. showNotification: null,
  580. hideButton: null,
  581. autoDecode: null,
  582. buttonPosition: null
  583. };
  584.  
  585. // 添加初始化标志
  586. this.isInitialLoad = true;
  587. this.lastDecodeTime = 0;
  588. this.isShowingNotification = false; // 添加通知显示标志
  589. this.hasAutoDecodedOnLoad = false; // 添加标志,跟踪是否已在页面加载时执行过自动解码
  590. this.isPageRefresh = true; // 添加页面刷新标志,初始加载视为刷新
  591. this.pageRefreshCompleted = false; // 添加页面刷新完成标志
  592. this.isRestoringContent = false; // 添加内容恢复标志
  593. this.isDecodingContent = false; // 添加内容解码标志
  594. this.lastPageUrl = window.location.href; // 记录当前页面URL
  595. const MIN_DECODE_INTERVAL = 1000; // 最小解码间隔(毫秒)
  596.  
  597. // 添加配置监听
  598. this.setupConfigListeners();
  599.  
  600. // 初始化UI
  601. this.initUI();
  602. this.initEventListeners();
  603. this.addRouteListeners();
  604.  
  605. // 优化自动解码的初始化逻辑
  606. // 在构造函数中不直接执行自动解码,而是通过 resetState 方法处理
  607. if (this.config.autoDecode) {
  608. const currentTime = Date.now();
  609. // 确保足够的时间间隔
  610. if (currentTime - this.lastDecodeTime > MIN_DECODE_INTERVAL) {
  611. this.lastDecodeTime = currentTime;
  612. console.log('构造函数中准备执行 resetState');
  613. // 使用 requestIdleCallback 在浏览器空闲时执行
  614. if (window.requestIdleCallback) {
  615. requestIdleCallback(() => this.resetState(), { timeout: 2000 });
  616. } else {
  617. // 降级使用 setTimeout
  618. setTimeout(() => this.resetState(), 800);
  619. }
  620. }
  621. }
  622.  
  623. // 初始化完成后重置标志
  624. setTimeout(() => {
  625. this.isInitialLoad = false;
  626. this.isPageRefresh = false; // 重置页面刷新标志
  627. this.pageRefreshCompleted = true; // 设置页面刷新完成标志
  628.  
  629. // 检查页面上是否已有解码内容
  630. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  631.  
  632. // 如果启用了自动解码但尚未执行,且页面上没有已解码内容,确保执行一次
  633. if (this.config.autoDecode && !this.hasAutoDecodedOnLoad && !hasDecodedContent) {
  634. console.log('初始化完成后执行自动解码');
  635. this.hasAutoDecodedOnLoad = true;
  636. this.handleDecode();
  637. } else if (hasDecodedContent) {
  638. // 如果页面上已有解码内容,更新菜单状态
  639. console.log('页面上已有解码内容,更新菜单状态');
  640. if (this.decodeBtn) {
  641. this.decodeBtn.textContent = '恢复本页 Base64';
  642. this.decodeBtn.dataset.mode = 'restore';
  643. }
  644. setTimeout(updateMenuCommands, 100);
  645. }
  646. }, 2500);
  647. }
  648.  
  649. /**
  650. * 设置配置监听器
  651. * @description 为各个配置项添加监听器,实现配置变更的实时响应
  652. */
  653. setupConfigListeners() {
  654. // 清理现有监听器
  655. Object.values(this.configListeners).forEach(listenerId => {
  656. if (listenerId) {
  657. storageManager.removeChangeListener(listenerId);
  658. }
  659. });
  660.  
  661. // 监听显示通知设置变更
  662. this.configListeners.showNotification = storageManager.addChangeListener(
  663. STORAGE_KEYS.SHOW_NOTIFICATION,
  664. (newValue) => {
  665. console.log('显示通知设置已更改:', newValue);
  666. this.config.showNotification = newValue;
  667. }
  668. );
  669.  
  670. // 监听隐藏按钮设置变更
  671. this.configListeners.hideButton = storageManager.addChangeListener(
  672. STORAGE_KEYS.HIDE_BUTTON,
  673. (newValue) => {
  674. console.log('隐藏按钮设置已更改:', newValue);
  675. this.config.hideButton = newValue;
  676.  
  677. // 实时更新UI显示状态
  678. const ui = this.shadowRoot?.querySelector('.base64-helper');
  679. if (ui) {
  680. ui.style.display = newValue ? 'none' : 'block';
  681. }
  682. }
  683. );
  684.  
  685. // 监听自动解码设置变更
  686. this.configListeners.autoDecode = storageManager.addChangeListener(
  687. STORAGE_KEYS.AUTO_DECODE,
  688. (newValue) => {
  689. console.log('自动解码设置已更改:', newValue);
  690. this.config.autoDecode = newValue;
  691.  
  692. // 如果启用了自动解码,立即解析页面
  693. if (newValue) {
  694. // 检查是否是通过菜单命令触发的变更
  695. // 如果是通过菜单命令触发,则不再显示确认对话框
  696. // 因为菜单命令处理程序中已经处理了这些确认
  697.  
  698. // 立即解析页面
  699. this.hasAutoDecodedOnLoad = true; // 标记已执行过自动解码
  700. setTimeout(() => {
  701. this.handleDecode();
  702. // 同步按钮和菜单状态
  703. setTimeout(() => this.syncButtonAndMenuState(), 200);
  704. }, 100);
  705. }
  706. }
  707. );
  708.  
  709. // 监听按钮位置变更
  710. this.configListeners.buttonPosition = storageManager.addChangeListener(
  711. STORAGE_KEYS.BUTTON_POSITION,
  712. (newValue) => {
  713. console.log('按钮位置已更改:', newValue);
  714. // 更新按钮位置
  715. this.initPosition();
  716. }
  717. );
  718. }
  719.  
  720. // 添加正则常量
  721. static URL_PATTERNS = {
  722. URL: /^(?:(?:https?|ftp):\/\/)?(?:(?:[\w-]+\.)+[a-z]{2,}|localhost)(?::\d+)?(?:\/[^\s]*)?$/i,
  723. EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  724. DOMAIN_PATTERNS: {
  725. POPULAR_SITES:
  726. /(?:google|youtube|facebook|twitter|instagram|linkedin|github|gitlab|bitbucket|stackoverflow|reddit|discord|twitch|tiktok|snapchat|pinterest|netflix|amazon|microsoft|apple|adobe)/i,
  727. VIDEO_SITES:
  728. /(?:bilibili|youku|iqiyi|douyin|kuaishou|nicovideo|vimeo|dailymotion)/i,
  729. CN_SITES:
  730. /(?:baidu|weibo|zhihu|taobao|tmall|jd|qq|163|sina|sohu|csdn|aliyun|tencent)/i,
  731. TLD: /\.(?:com|net|org|edu|gov|mil|biz|info|io|cn|me|tv|cc|uk|jp|ru|eu|au|de|fr)(?:\/|\?|#|$)/i,
  732. },
  733. };
  734.  
  735. // UI 初始化
  736. initUI() {
  737. if (
  738. window.top !== window.self ||
  739. document.getElementById('base64-helper-root')
  740. ) {
  741. return;
  742. }
  743.  
  744. this.container = document.createElement('div');
  745. this.container.id = 'base64-helper-root';
  746. document.body.append(this.container);
  747.  
  748. this.shadowRoot = this.container.attachShadow({ mode: 'open' });
  749. this.shadowRoot.appendChild(this.createShadowStyles());
  750.  
  751. // 创建 UI 容器
  752. const uiContainer = document.createElement('div');
  753. uiContainer.className = 'base64-helper';
  754. uiContainer.style.cursor = 'grab';
  755.  
  756. // 创建按钮和菜单
  757. this.mainBtn = this.createButton('Base64', 'main-btn');
  758. this.menu = this.createMenu();
  759. this.decodeBtn = this.menu.querySelector('[data-mode="decode"]');
  760. this.encodeBtn = this.menu.querySelector('.menu-item:not([data-mode])');
  761.  
  762. // 添加到 UI 容器
  763. uiContainer.append(this.mainBtn, this.menu);
  764. this.shadowRoot.appendChild(uiContainer);
  765.  
  766. // 初始化位置
  767. this.initPosition();
  768.  
  769. // 如果配置为隐藏按钮,则设置为不可见
  770. if (this.config.hideButton) {
  771. uiContainer.style.display = 'none';
  772. }
  773. }
  774.  
  775. createShadowStyles() {
  776. const style = document.createElement('style');
  777. style.textContent = STYLES.SHADOW_DOM;
  778. return style;
  779. }
  780.  
  781. // 不再需要 createMainUI 方法,因为我们直接在 initUI 中创建 UI
  782.  
  783. createButton(text, className) {
  784. const btn = document.createElement('button');
  785. btn.className = className;
  786. btn.textContent = text;
  787. return btn;
  788. }
  789.  
  790. createMenu() {
  791. const menu = document.createElement('div');
  792. menu.className = 'menu';
  793.  
  794. this.decodeBtn = this.createMenuItem('解析本页 Base64', 'decode');
  795. this.encodeBtn = this.createMenuItem('文本转 Base64');
  796.  
  797. menu.append(this.decodeBtn, this.encodeBtn);
  798. return menu;
  799. }
  800.  
  801. createMenuItem(text, mode) {
  802. const item = document.createElement('div');
  803. item.className = 'menu-item';
  804. item.textContent = text;
  805. if (mode) item.dataset.mode = mode;
  806. return item;
  807. }
  808.  
  809. // 位置管理
  810. initPosition() {
  811. const pos = this.positionManager.get() || {
  812. x: window.innerWidth - 120,
  813. y: window.innerHeight - 80,
  814. };
  815.  
  816. const ui = this.shadowRoot.querySelector('.base64-helper');
  817. ui.style.left = `${pos.x}px`;
  818. ui.style.top = `${pos.y}px`;
  819.  
  820. // 新增: 初始化时更新菜单对齐
  821. this.updateMenuAlignment();
  822. }
  823. updateMenuAlignment() {
  824. const ui = this.shadowRoot.querySelector('.base64-helper');
  825. const menu = this.menu;
  826. const windowWidth = window.innerWidth;
  827. const windowHeight = window.innerHeight;
  828. const uiRect = ui.getBoundingClientRect();
  829. const centerX = uiRect.left + uiRect.width / 2;
  830. const centerY = uiRect.top + uiRect.height / 2;
  831.  
  832. // 判断按钮是在页面左半边还是右半边
  833. if (centerX < windowWidth / 2) {
  834. // 左对齐
  835. menu.classList.remove('align-right');
  836. menu.classList.add('align-left');
  837. } else {
  838. // 右对齐
  839. menu.classList.remove('align-left');
  840. menu.classList.add('align-right');
  841. }
  842.  
  843. // 判断按钮是在页面上半部分还是下半部分
  844. if (centerY < windowHeight / 2) {
  845. // 在页面上方,菜单向下弹出
  846. menu.classList.remove('popup-top');
  847. menu.classList.add('popup-bottom');
  848. } else {
  849. // 在页面下方,菜单向上弹出
  850. menu.classList.remove('popup-bottom');
  851. menu.classList.add('popup-top');
  852. }
  853. }
  854. get positionManager() {
  855. return {
  856. get: () => {
  857. // 使用 storageManager 获取按钮位置
  858. const saved = storageManager.get(STORAGE_KEYS.BUTTON_POSITION, null);
  859. if (!saved) return null;
  860.  
  861. const ui = this.shadowRoot.querySelector('.base64-helper');
  862. const maxX = window.innerWidth - ui.offsetWidth - 20;
  863. const maxY = window.innerHeight - ui.offsetHeight - 20;
  864.  
  865. return {
  866. x: Math.min(Math.max(saved.x, 20), maxX),
  867. y: Math.min(Math.max(saved.y, 20), maxY),
  868. };
  869. },
  870. set: (x, y) => {
  871. const ui = this.shadowRoot.querySelector('.base64-helper');
  872. const pos = {
  873. x: Math.max(
  874. 20,
  875. Math.min(x, window.innerWidth - ui.offsetWidth - 20)
  876. ),
  877. y: Math.max(
  878. 20,
  879. Math.min(y, window.innerHeight - ui.offsetHeight - 20)
  880. ),
  881. };
  882.  
  883. // 使用 storageManager 存储按钮位置
  884. storageManager.set(STORAGE_KEYS.BUTTON_POSITION, pos);
  885. return pos;
  886. },
  887. };
  888. }
  889.  
  890. // 初始化事件监听器
  891. initEventListeners() {
  892. this.addUnifiedEventListeners();
  893. this.addGlobalClickListeners();
  894.  
  895. // 核心编解码事件监听
  896. const commonListeners = [
  897. {
  898. element: this.decodeBtn,
  899. events: [
  900. {
  901. name: 'click',
  902. handler: (e) => {
  903. e.preventDefault();
  904. e.stopPropagation();
  905. this.handleDecode();
  906. },
  907. },
  908. ],
  909. },
  910. {
  911. element: this.encodeBtn,
  912. events: [
  913. {
  914. name: 'click',
  915. handler: (e) => {
  916. e.preventDefault();
  917. e.stopPropagation();
  918. this.handleEncode();
  919. },
  920. },
  921. ],
  922. },
  923. ];
  924.  
  925. commonListeners.forEach(({ element, events }) => {
  926. events.forEach(({ name, handler }) => {
  927. element.addEventListener(name, handler, { passive: false });
  928. this.eventListeners.push({ element, event: name, handler });
  929. });
  930. });
  931. }
  932.  
  933. addUnifiedEventListeners() {
  934. const ui = this.shadowRoot.querySelector('.base64-helper');
  935. const btn = this.mainBtn;
  936.  
  937. // 统一的开始事件处理
  938. const startHandler = (e) => {
  939. e.preventDefault();
  940. e.stopPropagation();
  941. const point = e.touches ? e.touches[0] : e;
  942. this.isDragging = true;
  943. this.hasMoved = false;
  944. this.startX = point.clientX;
  945. this.startY = point.clientY;
  946. const rect = ui.getBoundingClientRect();
  947. this.initialX = rect.left;
  948. this.initialY = rect.top;
  949. this.startTime = Date.now();
  950. ui.style.transition = 'none';
  951. ui.classList.add('dragging');
  952. btn.style.cursor = 'grabbing';
  953. };
  954.  
  955. // 统一的移动事件处理
  956. const moveHandler = (e) => {
  957. if (!this.isDragging) return;
  958. e.preventDefault();
  959. e.stopPropagation();
  960.  
  961. const point = e.touches ? e.touches[0] : e;
  962. const moveX = Math.abs(point.clientX - this.startX);
  963. const moveY = Math.abs(point.clientY - this.startY);
  964.  
  965. if (moveX > 5 || moveY > 5) {
  966. this.hasMoved = true;
  967. const dx = point.clientX - this.startX;
  968. const dy = point.clientY - this.startY;
  969. const newX = Math.min(
  970. Math.max(20, this.initialX + dx),
  971. window.innerWidth - ui.offsetWidth - 20
  972. );
  973. const newY = Math.min(
  974. Math.max(20, this.initialY + dy),
  975. window.innerHeight - ui.offsetHeight - 20
  976. );
  977. ui.style.left = `${newX}px`;
  978. ui.style.top = `${newY}px`;
  979. }
  980. };
  981.  
  982. // 统一的结束事件处理
  983. const endHandler = (e) => {
  984. if (!this.isDragging) return;
  985. e.preventDefault();
  986. e.stopPropagation();
  987.  
  988. this.isDragging = false;
  989. ui.classList.remove('dragging');
  990. btn.style.cursor = 'grab';
  991. ui.style.transition = 'opacity 0.3s ease';
  992.  
  993. const duration = Date.now() - this.startTime;
  994. if (duration < 200 && !this.hasMoved) {
  995. this.toggleMenu(e);
  996. } else if (this.hasMoved) {
  997. const rect = ui.getBoundingClientRect();
  998. const pos = this.positionManager.set(rect.left, rect.top);
  999. ui.style.left = `${pos.x}px`;
  1000. ui.style.top = `${pos.y}px`;
  1001. // 新增: 拖动结束后更新菜单对齐
  1002. this.updateMenuAlignment();
  1003. }
  1004. };
  1005.  
  1006. // 统一收集所有事件监听器
  1007. const listeners = [
  1008. {
  1009. element: ui,
  1010. event: 'touchstart',
  1011. handler: startHandler,
  1012. options: { passive: false },
  1013. },
  1014. {
  1015. element: ui,
  1016. event: 'touchmove',
  1017. handler: moveHandler,
  1018. options: { passive: false },
  1019. },
  1020. {
  1021. element: ui,
  1022. event: 'touchend',
  1023. handler: endHandler,
  1024. options: { passive: false },
  1025. },
  1026. { element: ui, event: 'mousedown', handler: startHandler },
  1027. { element: document, event: 'mousemove', handler: moveHandler },
  1028. { element: document, event: 'mouseup', handler: endHandler },
  1029. {
  1030. element: this.menu,
  1031. event: 'touchstart',
  1032. handler: (e) => e.stopPropagation(),
  1033. options: { passive: false },
  1034. },
  1035. {
  1036. element: this.menu,
  1037. event: 'mousedown',
  1038. handler: (e) => e.stopPropagation(),
  1039. },
  1040. {
  1041. element: window,
  1042. event: 'resize',
  1043. handler: () => this.handleResize(),
  1044. },
  1045. ];
  1046.  
  1047. // 注册(不可用)事件并保存引用
  1048. listeners.forEach(({ element, event, handler, options }) => {
  1049. element.addEventListener(event, handler, options);
  1050. this.eventListeners.push({ element, event, handler, options });
  1051. });
  1052. }
  1053.  
  1054. toggleMenu(e) {
  1055. e?.preventDefault();
  1056. e?.stopPropagation();
  1057.  
  1058. // 如果正在拖动或已移动,不处理菜单切换
  1059. if (this.isDragging || this.hasMoved) return;
  1060.  
  1061. this.menuVisible = !this.menuVisible;
  1062. if (this.menuVisible) {
  1063. // 在显示菜单前更新位置
  1064. this.updateMenuAlignment();
  1065. }
  1066. this.menu.style.display = this.menuVisible ? 'block' : 'none';
  1067.  
  1068. // 重置状态
  1069. this.hasMoved = false;
  1070. }
  1071.  
  1072. addGlobalClickListeners() {
  1073. const handleOutsideClick = (e) => {
  1074. const ui = this.shadowRoot.querySelector('.base64-helper');
  1075. const path = e.composedPath();
  1076. if (!path.includes(ui) && this.menuVisible) {
  1077. this.menuVisible = false;
  1078. this.menu.style.display = 'none';
  1079. }
  1080. };
  1081.  
  1082. // 将全局点击事件添加到 eventListeners 数组
  1083. const globalListeners = [
  1084. {
  1085. element: document,
  1086. event: 'click',
  1087. handler: handleOutsideClick,
  1088. options: true,
  1089. },
  1090. {
  1091. element: document,
  1092. event: 'touchstart',
  1093. handler: handleOutsideClick,
  1094. options: { passive: false },
  1095. },
  1096. ];
  1097.  
  1098. globalListeners.forEach(({ element, event, handler, options }) => {
  1099. element.addEventListener(event, handler, options);
  1100. this.eventListeners.push({ element, event, handler, options });
  1101. });
  1102. }
  1103.  
  1104. // 路由监听
  1105. addRouteListeners() {
  1106. this.handleRouteChange = () => {
  1107. console.log('路由变化被检测到');
  1108. // 使用防抖,避免短时间内多次触发
  1109. clearTimeout(this.routeTimer);
  1110.  
  1111. // 添加时间检查,避免短时间内多次触发
  1112. const currentTime = Date.now();
  1113. if (currentTime - this.lastDecodeTime < 1000) {
  1114. console.log('距离上次解码时间太短,跳过这次路由变化');
  1115. return;
  1116. }
  1117.  
  1118. // 添加标志,防止页面刷新时的重复处理
  1119. if (this.isPageRefresh) {
  1120. console.log('页面刷新中,跳过路由变化处理');
  1121. return;
  1122. }
  1123.  
  1124. // 如果页面刷新尚未完成,不执行任何操作
  1125. if (!this.pageRefreshCompleted && this.isInitialLoad) {
  1126. console.log('页面初始加载尚未完成,跳过路由变化处理');
  1127. return;
  1128. }
  1129.  
  1130. this.routeTimer = setTimeout(() => {
  1131. if (!this.isProcessing) {
  1132. console.log('执行 resetState 方法');
  1133. this.resetState();
  1134. // 在路由变化时更新菜单命令
  1135. setTimeout(updateMenuCommands, 100);
  1136.  
  1137. // 检查页面上是否有解码内容,同步按钮状态
  1138. this.syncButtonAndMenuState();
  1139. }
  1140. }, 500);
  1141. };
  1142.  
  1143. // 添加路由相关事件到 eventListeners 数组
  1144. const routeListeners = [
  1145. { element: window, event: 'popstate', handler: this.handleRouteChange },
  1146. {
  1147. element: window,
  1148. event: 'hashchange',
  1149. handler: this.handleRouteChange,
  1150. },
  1151. {
  1152. element: window,
  1153. event: 'DOMContentLoaded',
  1154. handler: this.handleRouteChange,
  1155. },
  1156. // 添加更多事件监听器来捕获路由变化
  1157. {
  1158. element: document,
  1159. event: 'readystatechange',
  1160. handler: this.handleRouteChange,
  1161. },
  1162. ];
  1163.  
  1164. routeListeners.forEach(({ element, event, handler }) => {
  1165. element.addEventListener(event, handler);
  1166. this.eventListeners.push({ element, event, handler });
  1167. });
  1168.  
  1169. // 修改 history 方法
  1170. this.originalPushState = history.pushState;
  1171. this.originalReplaceState = history.replaceState;
  1172. history.pushState = (...args) => {
  1173. this.originalPushState.apply(history, args);
  1174. console.log('history.pushState 被调用');
  1175. this.handleRouteChange();
  1176. };
  1177. history.replaceState = (...args) => {
  1178. this.originalReplaceState.apply(history, args);
  1179. console.log('history.replaceState 被调用');
  1180. this.handleRouteChange();
  1181. };
  1182.  
  1183. // 优化 MutationObserver 配置
  1184. this.observer = new MutationObserver((mutations) => {
  1185. // 如果正在处理中或正在显示通知,跳过这次变化
  1186. if (this.isProcessing || this.isShowingNotification || this.isDecodingContent || this.isRestoringContent) {
  1187. console.log('正在处理中或显示通知,跳过 DOM 变化检测');
  1188. return;
  1189. }
  1190.  
  1191. // 添加防止短时间内重复触发的防抖
  1192. const currentTime = Date.now();
  1193. if (currentTime - this.lastDecodeTime < 1500) {
  1194. console.log('距离上次解码时间太短,跳过这次 DOM 变化检测');
  1195. return;
  1196. }
  1197.  
  1198. // 检查是否有显著的 DOM 变化
  1199. const significantChanges = mutations.some(mutation => {
  1200. // 忽略文本节点的变化
  1201. if (mutation.type === 'characterData') return false;
  1202.  
  1203. // 忽略样式相关的属性变化
  1204. if (mutation.type === 'attributes' &&
  1205. (mutation.attributeName === 'style' ||
  1206. mutation.attributeName === 'class')) {
  1207. return false;
  1208. }
  1209.  
  1210. // 排除通知容器的变化
  1211. if (mutation.target &&
  1212. (mutation.target.classList?.contains('base64-notifications-container') ||
  1213. mutation.target.classList?.contains('base64-notification'))) {
  1214. return false;
  1215. }
  1216.  
  1217. // 检查添加的节点是否与通知相关
  1218. const isNotificationNode = (node) => {
  1219. if (node.nodeType !== 1) return false; // 非元素节点
  1220. return node.classList?.contains('base64-notifications-container') ||
  1221. node.classList?.contains('base64-notification') ||
  1222. node.closest('.base64-notifications-container') !== null;
  1223. };
  1224.  
  1225. // 如果添加的节点是通知相关的,则忽略
  1226. if (Array.from(mutation.addedNodes).some(isNotificationNode)) {
  1227. return false;
  1228. }
  1229.  
  1230. // 如果有大量节点添加或删除,可能是路由变化
  1231. if (mutation.addedNodes.length > 5 || mutation.removedNodes.length > 5) {
  1232. return true;
  1233. }
  1234.  
  1235. // 检查是否是重要的 DOM 变化
  1236. const isImportantNode = (node) => {
  1237. return node.nodeType === 1 && // 元素节点
  1238. (node.tagName === 'DIV' ||
  1239. node.tagName === 'ARTICLE' ||
  1240. node.tagName === 'SECTION');
  1241. };
  1242.  
  1243. return Array.from(mutation.addedNodes).some(isImportantNode) ||
  1244. Array.from(mutation.removedNodes).some(isImportantNode);
  1245. });
  1246.  
  1247. if (significantChanges && this.config.autoDecode) {
  1248. console.log('检测到显著的 DOM 变化,可能是路由变化');
  1249. clearTimeout(this.domChangeTimer);
  1250. this.domChangeTimer = setTimeout(() => {
  1251. // 如果启用了自动解码,且不在处理中,则解析页面
  1252. if (this.config.autoDecode && !this.isProcessing && !this.isShowingNotification && !this.isDecodingContent && !this.isRestoringContent) {
  1253. // 检查是否已经执行过自动解码
  1254. const currentTime = Date.now();
  1255. if (currentTime - this.lastDecodeTime < 1500) {
  1256. console.log('距离上次解码时间太短,跳过这次 DOM 变化触发的自动解码');
  1257. return;
  1258. }
  1259.  
  1260. console.log('由于 DOM 变化触发自动解码');
  1261. // 更新最后解码时间
  1262. this.lastDecodeTime = currentTime;
  1263. this.handleDecode();
  1264.  
  1265. // 同步按钮和菜单状态
  1266. setTimeout(() => this.syncButtonAndMenuState(), 200);
  1267. }
  1268. }, 800);
  1269. }
  1270. });
  1271.  
  1272. // 优化 MutationObserver 观察选项
  1273. this.observer.observe(document.body, {
  1274. childList: true,
  1275. subtree: true,
  1276. attributes: false, // 不观察属性变化
  1277. characterData: false // 不观察文本变化
  1278. });
  1279. }
  1280.  
  1281. /**
  1282. * 处理页面中的Base64解码操作
  1283. * @description 根据当前模式执行解码或恢复操作
  1284. * 如果当前模式是restore则恢复原始内容,否则查找并解码页面中的Base64内容
  1285. * @fires showNotification 显示操作结果通知
  1286. */
  1287. handleDecode() {
  1288. // 防止重复处理或在显示通知时触发
  1289. if (this.isProcessing || this.isShowingNotification || this.isDecodingContent || this.isRestoringContent) {
  1290. console.log('正在处理中或显示通知,跳过这次解码请求');
  1291. return;
  1292. }
  1293.  
  1294. // 如果是页面刷新过程中,且页面刷新尚未完成,跳过处理
  1295. if (this.isPageRefresh && !this.pageRefreshCompleted) {
  1296. console.log('页面刷新过程中,尚未完成加载,跳过解码请求');
  1297. return;
  1298. }
  1299.  
  1300. // 检查URL是否变化,如果变化了,可能是新页面
  1301. const currentUrl = window.location.href;
  1302. if (currentUrl !== this.lastPageUrl) {
  1303. console.log('URL已变化,更新URL记录');
  1304. this.lastPageUrl = currentUrl;
  1305. }
  1306.  
  1307. // 设置处理标志
  1308. this.isProcessing = true;
  1309.  
  1310. // 存储当前模式的变量
  1311. let currentMode = 'decode';
  1312.  
  1313. // 如果按钮存在,使用按钮的模式
  1314. if (this.decodeBtn && this.decodeBtn.dataset.mode === 'restore') {
  1315. currentMode = 'restore';
  1316. } else {
  1317. // 如果按钮不存在或模式不是restore,检查页面上是否有解码后的内容
  1318. if (document.querySelectorAll('.decoded-text').length > 0) {
  1319. currentMode = 'restore';
  1320. }
  1321. }
  1322.  
  1323. // 如果是恢复模式
  1324. if (currentMode === 'restore') {
  1325. // 设置恢复内容标志
  1326. this.isRestoringContent = true;
  1327.  
  1328. // 使用setTimeout确保UI更新
  1329. setTimeout(() => {
  1330. this.restoreContent();
  1331. // 重置处理标志
  1332. this.isProcessing = false;
  1333. this.isRestoringContent = false;
  1334. // 更新最后解码时间
  1335. this.lastDecodeTime = Date.now();
  1336. }, 50);
  1337. return;
  1338. }
  1339.  
  1340. try {
  1341. // 隐藏菜单
  1342. if (this.menu && this.menu.style.display !== 'none') {
  1343. this.menu.style.display = 'none';
  1344. this.menuVisible = false;
  1345. }
  1346.  
  1347. // 设置解码内容标志
  1348. this.isDecodingContent = true;
  1349.  
  1350. // 使用 setTimeout 延迟执行以避免界面冻结
  1351. setTimeout(() => {
  1352. try {
  1353. const { nodesToReplace, validDecodedCount } = this.processTextNodes();
  1354.  
  1355. if (validDecodedCount === 0) {
  1356. this.showNotification('本页未发现有效 Base64 内容', 'info');
  1357. this.menuVisible = false;
  1358. this.menu.style.display = 'none';
  1359. // 重置处理标志
  1360. this.isProcessing = false;
  1361. this.isDecodingContent = false;
  1362. // 更新最后解码时间
  1363. this.lastDecodeTime = Date.now();
  1364. return;
  1365. }
  1366.  
  1367. // 分批处理节点替换,避免大量 DOM 操作导致界面冻结
  1368. const BATCH_SIZE = 50; // 每批处理的节点数
  1369. const processNodesBatch = (startIndex) => {
  1370. const endIndex = Math.min(startIndex + BATCH_SIZE, nodesToReplace.length);
  1371. const batch = nodesToReplace.slice(startIndex, endIndex);
  1372.  
  1373. this.replaceNodes(batch);
  1374.  
  1375. if (endIndex < nodesToReplace.length) {
  1376. // 还有更多节点需要处理,安排下一批
  1377. setTimeout(() => processNodesBatch(endIndex), 0);
  1378. } else {
  1379. // 所有节点处理完成,添加点击监听器
  1380. this.addClickListenersToDecodedText();
  1381.  
  1382. this.decodeBtn.textContent = '恢复本页 Base64';
  1383. this.decodeBtn.dataset.mode = 'restore';
  1384. this.showNotification(
  1385. `解析完成,共找到 ${validDecodedCount} Base64 内容`,
  1386. 'success'
  1387. );
  1388.  
  1389. // 操作完成后同步按钮和菜单状态
  1390. this.syncButtonAndMenuState();
  1391. // 重置处理标志
  1392. this.isProcessing = false;
  1393. this.isDecodingContent = false;
  1394. // 更新最后解码时间
  1395. this.lastDecodeTime = Date.now();
  1396. }
  1397. };
  1398.  
  1399. // 开始分批处理
  1400. processNodesBatch(0);
  1401. } catch (innerError) {
  1402. console.error('Base64 decode processing error:', innerError);
  1403. this.showNotification(`解析失败: ${innerError.message}`, 'error');
  1404. this.menuVisible = false;
  1405. this.menu.style.display = 'none';
  1406. }
  1407. }, 50); // 给浏览器一点时间渲染通知
  1408. } catch (e) {
  1409. console.error('Base64 decode error:', e);
  1410. this.showNotification(`解析失败: ${e.message}`, 'error');
  1411. this.menuVisible = false;
  1412. this.menu.style.display = 'none';
  1413. // 重置处理标志
  1414. this.isProcessing = false;
  1415. this.isDecodingContent = false;
  1416. // 更新最后解码时间
  1417. this.lastDecodeTime = Date.now();
  1418. }
  1419. }
  1420.  
  1421. /**
  1422. * 处理文本节点中的Base64内容
  1423. * @description 遍历文档中的文本节点,查找并处理其中的Base64内容
  1424. * 注意: 此方法包含性能优化措施,如超时检测和节点过滤
  1425. * @returns {Object} 处理结果
  1426. * @property {Array} nodesToReplace - 需要替换的节点数组
  1427. * @property {number} validDecodedCount - 有效的Base64解码数量
  1428. */
  1429. processTextNodes() {
  1430. const startTime = Date.now();
  1431. const TIMEOUT = 5000;
  1432.  
  1433. const excludeTags = new Set([
  1434. 'script',
  1435. 'style',
  1436. 'noscript',
  1437. 'iframe',
  1438. 'img',
  1439. 'input',
  1440. 'textarea',
  1441. 'svg',
  1442. 'canvas',
  1443. 'template',
  1444. 'pre',
  1445. 'code',
  1446. 'button',
  1447. 'meta',
  1448. 'link',
  1449. 'head',
  1450. 'title',
  1451. 'select',
  1452. 'form',
  1453. 'object',
  1454. 'embed',
  1455. 'video',
  1456. 'audio',
  1457. 'source',
  1458. 'track',
  1459. 'map',
  1460. 'area',
  1461. 'math',
  1462. 'figure',
  1463. 'picture',
  1464. 'portal',
  1465. 'slot',
  1466. 'data',
  1467. 'a',
  1468. 'base', // 包含href属性的base标签
  1469. 'param', // object的参数
  1470. 'applet', // 旧版Java小程序
  1471. 'frame', // 框架
  1472. 'frameset', // 框架集
  1473. 'marquee', // 滚动文本
  1474. 'time', // 时间标签
  1475. 'wbr', // 可能的换行符
  1476. 'bdo', // 文字方向
  1477. 'dialog', // 对话框
  1478. 'details', // 详情
  1479. 'summary', // 摘要
  1480. 'menu', // 菜单
  1481. 'menuitem', // 菜单项
  1482. '[hidden]', // 隐藏元素
  1483. '[aria-hidden="true"]', // 可访问性隐藏
  1484. '.base64', // 自定义class
  1485. '.encoded', // 自定义class
  1486. ]);
  1487.  
  1488. const excludeAttrs = new Set([
  1489. 'src',
  1490. 'data-src',
  1491. 'href',
  1492. 'data-url',
  1493. 'content',
  1494. 'background',
  1495. 'poster',
  1496. 'data-image',
  1497. 'srcset',
  1498. 'data-background', // 背景图片
  1499. 'data-thumbnail', // 缩略图
  1500. 'data-original', // 原始图片
  1501. 'data-lazy', // 懒加载
  1502. 'data-defer', // 延迟加载
  1503. 'data-fallback', // 后备图片
  1504. 'data-preview', // 预览图
  1505. 'data-avatar', // 头像
  1506. 'data-icon', // 图标
  1507. 'data-base64', // 显式标记的base64
  1508. 'style', // 内联样式可能包含base64
  1509. 'integrity', // SRI完整性校验
  1510. 'crossorigin', // 跨域属性
  1511. 'rel', // 关系属性
  1512. 'alt', // 替代文本
  1513. 'title', // 标题属性
  1514. ]);
  1515.  
  1516. const walker = document.createTreeWalker(
  1517. document.body,
  1518. NodeFilter.SHOW_TEXT,
  1519. {
  1520. acceptNode: (node) => {
  1521. const isExcludedTag = (parent) => {
  1522. const tagName = parent.tagName?.toLowerCase();
  1523. return excludeTags.has(tagName);
  1524. };
  1525.  
  1526. const isHiddenElement = (parent) => {
  1527. if (!(parent instanceof HTMLElement)) return false;
  1528. const style = window.getComputedStyle(parent);
  1529. return (
  1530. style.display === 'none' ||
  1531. style.visibility === 'hidden' ||
  1532. style.opacity === '0' ||
  1533. style.clipPath === 'inset(100%)' ||
  1534. (style.height === '0px' && style.overflow === 'hidden')
  1535. );
  1536. };
  1537.  
  1538. const isOutOfViewport = (parent) => {
  1539. if (!(parent instanceof HTMLElement)) return false;
  1540. const rect = parent.getBoundingClientRect();
  1541. return rect.width === 0 || rect.height === 0;
  1542. };
  1543.  
  1544. const hasBase64Attributes = (parent) => {
  1545. if (!parent.hasAttributes()) return false;
  1546. for (const attr of parent.attributes) {
  1547. if (excludeAttrs.has(attr.name)) {
  1548. const value = attr.value.toLowerCase();
  1549. if (
  1550. value.includes('base64') ||
  1551. value.match(/^[a-z0-9+/=]+$/i)
  1552. ) {
  1553. return true;
  1554. }
  1555. }
  1556. }
  1557. return false;
  1558. };
  1559.  
  1560. let parent = node.parentNode;
  1561. while (parent && parent !== document.body) {
  1562. if (
  1563. isExcludedTag(parent) ||
  1564. isHiddenElement(parent) ||
  1565. isOutOfViewport(parent) ||
  1566. hasBase64Attributes(parent)
  1567. ) {
  1568. return NodeFilter.FILTER_REJECT;
  1569. }
  1570. parent = parent.parentNode;
  1571. }
  1572.  
  1573. const text = node.textContent?.trim();
  1574. if (!text) {
  1575. return NodeFilter.FILTER_SKIP;
  1576. }
  1577.  
  1578. return /[A-Za-z0-9+/]+/.exec(text)
  1579. ? NodeFilter.FILTER_ACCEPT
  1580. : NodeFilter.FILTER_SKIP;
  1581. },
  1582. },
  1583. false
  1584. );
  1585.  
  1586. let nodesToReplace = [];
  1587. let processedMatches = new Set();
  1588. let validDecodedCount = 0;
  1589.  
  1590. while (walker.nextNode()) {
  1591. if (Date.now() - startTime > TIMEOUT) {
  1592. console.warn('Base64 processing timeout');
  1593. break;
  1594. }
  1595.  
  1596. const node = walker.currentNode;
  1597. const { modified, newHtml, count } = this.processMatches(
  1598. node.nodeValue,
  1599. processedMatches
  1600. );
  1601. if (modified) {
  1602. nodesToReplace.push({ node, newHtml });
  1603. validDecodedCount += count;
  1604. }
  1605. }
  1606.  
  1607. return { nodesToReplace, validDecodedCount };
  1608. }
  1609.  
  1610. /**
  1611. * 处理文本中的Base64匹配项
  1612. * @description 查找并处理文本中的Base64编码内容
  1613. * @param {string} text - 要处理的文本内容
  1614. * @param {Set} processedMatches - 已处理过的匹配项集合
  1615. * @returns {Object} 处理结果
  1616. * @property {boolean} modified - 文本是否被修改
  1617. * @property {string} newHtml - 处理后的HTML内容
  1618. * @property {number} count - 处理的Base64数量
  1619. */
  1620. processMatches(text, processedMatches) {
  1621. const matches = Array.from(text.matchAll(BASE64_REGEX));
  1622. if (!matches.length) return { modified: false, newHtml: text, count: 0 };
  1623.  
  1624. let modified = false;
  1625. let newHtml = text;
  1626. let count = 0;
  1627.  
  1628. for (const match of matches.reverse()) {
  1629. const original = match[0];
  1630.  
  1631. // 使用 validateBase64 进行验证
  1632. if (!this.validateBase64(original)) {
  1633. console.log('Skipped: invalid Base64 string');
  1634. continue;
  1635. }
  1636.  
  1637. try {
  1638. const decoded = this.decodeBase64(original);
  1639. console.log('Decoded:', decoded);
  1640.  
  1641. if (!decoded) {
  1642. console.log('Skipped: decode failed');
  1643. continue;
  1644. }
  1645.  
  1646. // 将原始Base64和位置信息添加到已处理集合中,防止重复处理
  1647. const matchKey = `${original}-${match.index}`;
  1648. processedMatches.add(matchKey);
  1649.  
  1650. // 构建新的HTML内容:
  1651. // 1. 保留匹配位置之前的内容
  1652. const beforeMatch = newHtml.substring(0, match.index);
  1653. // 2. 插入解码后的内容,包装在span标签中
  1654. const decodedSpan = `<span class="decoded-text"
  1655. title="点击复制"
  1656. data-original="${original}">${decoded}</span>`;
  1657. // 3. 保留匹配位置之后的内容
  1658. const afterMatch = newHtml.substring(match.index + original.length);
  1659.  
  1660. // 组合新的HTML
  1661. newHtml = beforeMatch + decodedSpan + afterMatch;
  1662.  
  1663. // 标记内容已被修改
  1664. modified = true;
  1665. // 增加成功解码计数
  1666. count++;
  1667.  
  1668. // 记录日志
  1669. console.log('成功解码: 发现有意义的文本或中文字符');
  1670. } catch (e) {
  1671. console.error('Error processing:', e);
  1672. continue;
  1673. }
  1674. }
  1675.  
  1676. return { modified, newHtml, count };
  1677. }
  1678.  
  1679. /**
  1680. * 判断文本是否有意义
  1681. * @description 通过一系列规则判断解码后的文本是否具有实际意义
  1682. * @param {string} text - 要验证的文本
  1683. * @returns {boolean} 如果文本有意义返回true,否则返回false
  1684. */
  1685. isMeaningfulText(text) {
  1686. // 1. 基本字符检查
  1687. if (!text || typeof text !== 'string') return false;
  1688.  
  1689. // 2. 长度检查
  1690. if (text.length < 2 || text.length > 10000) return false;
  1691.  
  1692. // 3. 文本质量检查
  1693. const stats = {
  1694. printable: 0, // 可打印字符
  1695. control: 0, // 控制字符
  1696. chinese: 0, // 中文字符
  1697. letters: 0, // 英文字母
  1698. numbers: 0, // 数字
  1699. punctuation: 0, // 标点符号
  1700. spaces: 0, // 空格
  1701. other: 0, // 其他字符
  1702. };
  1703.  
  1704. // 统计字符分布
  1705. for (let i = 0; i < text.length; i++) {
  1706. const char = text.charAt(i);
  1707. const code = text.charCodeAt(i);
  1708.  
  1709. if (/[\u4E00-\u9FFF]/.test(char)) {
  1710. stats.chinese++;
  1711. stats.printable++;
  1712. } else if (/[a-zA-Z]/.test(char)) {
  1713. stats.letters++;
  1714. stats.printable++;
  1715. } else if (/[0-9]/.test(char)) {
  1716. stats.numbers++;
  1717. stats.printable++;
  1718. } else if (/[\s]/.test(char)) {
  1719. stats.spaces++;
  1720. stats.printable++;
  1721. } else if (/[!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~]/.test(char)) {
  1722. stats.punctuation++;
  1723. stats.printable++;
  1724. } else if (code < 32 || code === 127) {
  1725. stats.control++;
  1726. } else {
  1727. stats.other++;
  1728. }
  1729. }
  1730.  
  1731. // 4. 质量评估规则
  1732. const totalChars = text.length;
  1733. const printableRatio = stats.printable / totalChars;
  1734. const controlRatio = stats.control / totalChars;
  1735. const meaningfulRatio =
  1736. (stats.chinese + stats.letters + stats.numbers) / totalChars;
  1737.  
  1738. // 判断条件:
  1739. // 1. 可打印字符比例必须大于90%
  1740. // 2. 控制字符比例必须小于5%
  1741. // 3. 有意义字符(中文、英文、数字)比例必须大于30%
  1742. // 4. 空格比例不能过高(小于50%)
  1743. // 5. 其他字符比例必须很低(小于10%)
  1744. return (
  1745. printableRatio > 0.9 &&
  1746. controlRatio < 0.05 &&
  1747. meaningfulRatio > 0.3 &&
  1748. stats.spaces / totalChars < 0.5 &&
  1749. stats.other / totalChars < 0.1
  1750. );
  1751. }
  1752.  
  1753. /**
  1754. * 替换页面中的节点
  1755. * @description 使用新的HTML内容替换原有节点
  1756. * @param {Array} nodesToReplace - 需要替换的节点数组
  1757. * @param {Node} nodesToReplace[].node - 原始节点
  1758. * @param {string} nodesToReplace[].newHtml - 新的HTML内容
  1759. */
  1760. replaceNodes(nodesToReplace) {
  1761. nodesToReplace.forEach(({ node, newHtml }) => {
  1762. const span = document.createElement('span');
  1763. span.innerHTML = newHtml;
  1764. node.parentNode.replaceChild(span, node);
  1765. });
  1766. }
  1767.  
  1768. /**
  1769. * 为解码后的文本添加点击复制功能
  1770. * @description 为所有解码后的文本元素添加点击事件监听器
  1771. * @fires copyToClipboard 点击时触发复制操作
  1772. * @fires showNotification 显示复制结果通知
  1773. */
  1774. addClickListenersToDecodedText() {
  1775. document.querySelectorAll('.decoded-text').forEach((el) => {
  1776. el.addEventListener('click', async (e) => {
  1777. const success = await this.copyToClipboard(e.target.textContent);
  1778. this.showNotification(
  1779. success ? '已复制文本内容' : '复制失败,请手动复制',
  1780. success ? 'success' : 'error'
  1781. );
  1782. e.stopPropagation();
  1783. });
  1784. });
  1785. }
  1786.  
  1787. /**
  1788. * 处理文本编码为Base64
  1789. * @description 提示用户输入文本并转换为Base64格式
  1790. * @async
  1791. * @fires showNotification 显示编码结果通知
  1792. * @fires copyToClipboard 复制编码结果到剪贴板
  1793. */
  1794. async handleEncode() {
  1795. // 隐藏菜单
  1796. if (this.menu && this.menu.style.display !== 'none') {
  1797. this.menu.style.display = 'none';
  1798. this.menuVisible = false;
  1799. }
  1800.  
  1801. const text = prompt('请输入要编码的文本:');
  1802. if (text === null) return; // 用户点击取消
  1803.  
  1804. // 添加空输入检查
  1805. if (!text.trim()) {
  1806. this.showNotification('请输入有效的文本内容', 'error');
  1807. return;
  1808. }
  1809.  
  1810. try {
  1811. // 处理输入文本:去除首尾空格和多余的换行符
  1812. const processedText = text.trim().replace(/[\r\n]+/g, '\n');
  1813. const encoded = this.encodeBase64(processedText);
  1814. const success = await this.copyToClipboard(encoded);
  1815. this.showNotification(
  1816. success
  1817. ? 'Base64 已复制'
  1818. : '编码成功但复制失败,请手动复制:' + encoded,
  1819. success ? 'success' : 'info'
  1820. );
  1821. } catch (e) {
  1822. this.showNotification('编码失败: ' + e.message, 'error');
  1823. }
  1824. }
  1825.  
  1826. /**
  1827. * 验证Base64字符串
  1828. * @description 检查字符串是否为有效的Base64格式
  1829. * @param {string} str - 要验证的字符串
  1830. * @returns {boolean} 如果是有效的Base64返回true,否则返回false
  1831. * @example
  1832. * validateBase64('SGVsbG8gV29ybGQ=') // returns true
  1833. * validateBase64('Invalid-Base64') // returns false
  1834. */
  1835. validateBase64(str) {
  1836. if (!str) return false;
  1837.  
  1838. // 使用缓存避免重复验证
  1839. if (this.base64Cache.has(str)) {
  1840. return this.base64Cache.get(str);
  1841. }
  1842.  
  1843. // 检查缓存大小并在必要时清理
  1844. if (this.base64Cache.size >= this.MAX_CACHE_SIZE) {
  1845. // 删除最早添加的缓存项
  1846. const oldestKey = this.base64Cache.keys().next().value;
  1847. this.base64Cache.delete(oldestKey);
  1848. }
  1849.  
  1850. // 1. 基本格式检查
  1851. // - 长度必须是4的倍数
  1852. // - 只允许包含合法的Base64字符
  1853. // - =号只能出现在末尾,且最多2个
  1854. if (
  1855. !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
  1856. str
  1857. )
  1858. ) {
  1859. this.base64Cache.set(str, false);
  1860. return false;
  1861. }
  1862.  
  1863. // 2. 长度检查
  1864. // 过滤掉太短的字符串(至少8个字符)和过长的字符串(最多10000个字符)
  1865. if (str.length < 8 || str.length > 10000) {
  1866. this.base64Cache.set(str, false);
  1867. return false;
  1868. }
  1869.  
  1870. // 3. 特征检查
  1871. // 过滤掉可能是图片、视频等二进制数据的Base64
  1872. if (/^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER)/.test(str)) {
  1873. this.base64Cache.set(str, false);
  1874. return false;
  1875. }
  1876.  
  1877. // 添加到 validateBase64 方法中
  1878. const commonPatterns = {
  1879. // 常见的二进制数据头部特征
  1880. binaryHeaders:
  1881. /^(?:data:|iVBOR|R0lGO|\/9j\/4|PD94bW|JVBER|UEsDB|H4sIA|77u\/|0M8R4)/,
  1882.  
  1883. // 常见的文件类型标识
  1884. fileSignatures: /^(?:UEs|PK|%PDF|GIF8|RIFF|OggS|ID3|ÿØÿ|8BPS)/,
  1885.  
  1886. // 常见的编码标识
  1887. encodingMarkers:
  1888. /^(?:utf-8|utf-16|base64|quoted-printable|7bit|8bit|binary)/i,
  1889.  
  1890. // 可疑的URL模式
  1891. urlPatterns: /^(?:https?:|ftp:|data:|blob:|file:|ws:|wss:)/i,
  1892.  
  1893. // 常见的压缩文件头部
  1894. compressedHeaders: /^(?:eJw|H4s|Qk1Q|UEsD|N3q8|KLUv)/,
  1895. };
  1896.  
  1897. // 在验证时使用这些模式
  1898. if (
  1899. commonPatterns.binaryHeaders.test(str) ||
  1900. commonPatterns.fileSignatures.test(str) ||
  1901. commonPatterns.encodingMarkers.test(str) ||
  1902. commonPatterns.urlPatterns.test(str) ||
  1903. commonPatterns.compressedHeaders.test(str)
  1904. ) {
  1905. this.base64Cache.set(str, false);
  1906. return false;
  1907. }
  1908.  
  1909. try {
  1910. const decoded = this.decodeBase64(str);
  1911. if (!decoded) {
  1912. this.base64Cache.set(str, false);
  1913. return false;
  1914. }
  1915.  
  1916. // 4. 解码后的文本验证
  1917. // 检查解码后的文本是否有意义
  1918. if (!this.isMeaningfulText(decoded)) {
  1919. this.base64Cache.set(str, false);
  1920. return false;
  1921. }
  1922.  
  1923. this.base64Cache.set(str, true);
  1924. return true;
  1925. } catch (e) {
  1926. console.error('Base64 validation error:', e);
  1927. this.base64Cache.set(str, false);
  1928. return false;
  1929. }
  1930. }
  1931.  
  1932. /**
  1933. * Base64解码
  1934. * @description 将Base64字符串解码为普通文本
  1935. * @param {string} str - 要解码的Base64字符串
  1936. * @returns {string|null} 解码后的文本,解码失败时返回null
  1937. * @example
  1938. * decodeBase64('SGVsbG8gV29ybGQ=') // returns 'Hello World'
  1939. */
  1940. decodeBase64(str) {
  1941. try {
  1942. // 优化解码过程
  1943. const binaryStr = atob(str);
  1944. const bytes = new Uint8Array(binaryStr.length);
  1945. for (let i = 0; i < binaryStr.length; i++) {
  1946. bytes[i] = binaryStr.charCodeAt(i);
  1947. }
  1948. return new TextDecoder().decode(bytes);
  1949. } catch (e) {
  1950. console.error('Base64 decode error:', e);
  1951. return null;
  1952. }
  1953. }
  1954.  
  1955. /**
  1956. * Base64编码
  1957. * @description 将普通文本编码为Base64格式
  1958. * @param {string} str - 要编码的文本
  1959. * @returns {string|null} Base64编码后的字符串,编码失败时返回null
  1960. * @example
  1961. * encodeBase64('Hello World') // returns 'SGVsbG8gV29ybGQ='
  1962. */
  1963. encodeBase64(str) {
  1964. try {
  1965. // 优化编码过程
  1966. const bytes = new TextEncoder().encode(str);
  1967. let binaryStr = '';
  1968. for (let i = 0; i < bytes.length; i++) {
  1969. binaryStr += String.fromCharCode(bytes[i]);
  1970. }
  1971. return btoa(binaryStr);
  1972. } catch (e) {
  1973. console.error('Base64 encode error:', e);
  1974. return null;
  1975. }
  1976. }
  1977.  
  1978. /**
  1979. * 复制文本到剪贴板
  1980. * @description 尝试使用现代API或降级方案将文本复制到剪贴板
  1981. * @param {string} text - 要复制的文本
  1982. * @returns {Promise<boolean>} 复制是否成功
  1983. * @example
  1984. * await copyToClipboard('Hello World') // returns true
  1985. */
  1986. async copyToClipboard(text) {
  1987. if (navigator.clipboard && window.isSecureContext) {
  1988. try {
  1989. await navigator.clipboard.writeText(text);
  1990. return true;
  1991. } catch (e) {
  1992. return this.fallbackCopy(text);
  1993. }
  1994. }
  1995.  
  1996. return this.fallbackCopy(text);
  1997. }
  1998.  
  1999. /**
  2000. * 降级复制方案
  2001. * @description 当现代复制API不可用时的备选复制方案
  2002. * @param {string} text - 要复制的文本
  2003. * @returns {boolean} 复制是否成功
  2004. * @private
  2005. */
  2006. fallbackCopy(text) {
  2007. if (typeof GM_setClipboard !== 'undefined') {
  2008. try {
  2009. GM_setClipboard(text);
  2010. return true;
  2011. } catch (e) {
  2012. console.debug('GM_setClipboard failed:', e);
  2013. }
  2014. }
  2015.  
  2016. try {
  2017. // 注意: execCommand 已经被废弃,但作为降级方案仍然有用
  2018. const textarea = document.createElement('textarea');
  2019. textarea.value = text;
  2020. textarea.style.cssText = 'position:fixed;opacity:0;';
  2021. document.body.appendChild(textarea);
  2022.  
  2023. if (navigator.userAgent.match(/ipad|iphone/i)) {
  2024. textarea.contentEditable = true;
  2025. textarea.readOnly = false;
  2026.  
  2027. const range = document.createRange();
  2028. range.selectNodeContents(textarea);
  2029.  
  2030. const selection = window.getSelection();
  2031. selection.removeAllRanges();
  2032. selection.addRange(range);
  2033. textarea.setSelectionRange(0, 999999);
  2034. } else {
  2035. textarea.select();
  2036. }
  2037.  
  2038. // 使用 try-catch 包裹 execCommand 调用,以防将来完全移除
  2039. let success = false;
  2040. try {
  2041. // @ts-ignore - 忽略废弃警告
  2042. success = document.execCommand('copy');
  2043. } catch (copyError) {
  2044. console.debug('execCommand copy operation failed:', copyError);
  2045. }
  2046.  
  2047. document.body.removeChild(textarea);
  2048. return success;
  2049. } catch (e) {
  2050. console.debug('Fallback copy method failed:', e);
  2051. return false;
  2052. }
  2053. }
  2054.  
  2055. /**
  2056. * 恢复原始内容
  2057. * @description 将所有解码后的内容恢复为原始的Base64格式
  2058. * @fires showNotification 显示恢复结果通知
  2059. */
  2060. restoreContent() {
  2061. // 设置恢复内容标志,防止重复处理
  2062. if (this.isRestoringContent) {
  2063. console.log('已经在恢复内容中,避免重复操作');
  2064. } else {
  2065. this.isRestoringContent = true;
  2066. }
  2067.  
  2068. // 收集所有需要恢复的元素
  2069. const elementsToRestore = Array.from(document.querySelectorAll('.decoded-text'));
  2070.  
  2071. if (elementsToRestore.length === 0) {
  2072. console.log('没有找到需要恢复的元素');
  2073. this.isRestoringContent = false;
  2074. return;
  2075. }
  2076.  
  2077. console.log(`找到 ${elementsToRestore.length} 个需要恢复的元素`);
  2078.  
  2079. // 分批处理恢复操作,避免大量DOM操作导致页面卡顿
  2080. const BATCH_SIZE = 50;
  2081. const processBatch = (startIndex) => {
  2082. const endIndex = Math.min(startIndex + BATCH_SIZE, elementsToRestore.length);
  2083. const batch = elementsToRestore.slice(startIndex, endIndex);
  2084.  
  2085. batch.forEach((el) => {
  2086. if (el && el.parentNode && el.dataset.original) {
  2087. const textNode = document.createTextNode(el.dataset.original);
  2088. el.parentNode.replaceChild(textNode, el);
  2089. }
  2090. });
  2091.  
  2092. if (endIndex < elementsToRestore.length) {
  2093. // 还有更多元素需要处理
  2094. setTimeout(() => processBatch(endIndex), 0);
  2095. } else {
  2096. // 所有元素处理完成
  2097. this.originalContents.clear();
  2098.  
  2099. // 如果按钮存在,更新按钮状态
  2100. if (this.decodeBtn) {
  2101. this.decodeBtn.textContent = '解析本页 Base64';
  2102. this.decodeBtn.dataset.mode = 'decode';
  2103. }
  2104.  
  2105. this.showNotification('已恢复原始内容', 'success');
  2106.  
  2107. // 只有当按钮可见时才隐藏菜单
  2108. if (!this.config.hideButton && this.menu) {
  2109. this.menu.style.display = 'none';
  2110. }
  2111.  
  2112. // 操作完成后同步按钮和菜单状态
  2113. this.syncButtonAndMenuState();
  2114.  
  2115. // 重置恢复内容标志
  2116. this.isRestoringContent = false;
  2117. }
  2118. };
  2119.  
  2120. // 开始处理第一批
  2121. processBatch(0);
  2122. }
  2123.  
  2124. /**
  2125. * 同步按钮和菜单状态
  2126. * @description 根据页面上是否有解码内容,同步按钮和菜单状态
  2127. */
  2128. syncButtonAndMenuState() {
  2129. // 检查页面上是否有解码内容
  2130. const hasDecodedContent = document.querySelectorAll('.decoded-text').length > 0;
  2131.  
  2132. // 同步按钮状态
  2133. if (this.decodeBtn) {
  2134. if (hasDecodedContent) {
  2135. this.decodeBtn.textContent = '恢复本页 Base64';
  2136. this.decodeBtn.dataset.mode = 'restore';
  2137. } else {
  2138. this.decodeBtn.textContent = '解析本页 Base64';
  2139. this.decodeBtn.dataset.mode = 'decode';
  2140. }
  2141. }
  2142.  
  2143. // 更新菜单命令
  2144. setTimeout(updateMenuCommands, 100);
  2145. }
  2146.  
  2147. /**
  2148. * 重置插件状态
  2149. * @description 重置所有状态变量并在必要时恢复原始内容
  2150. * 如果启用了自动解码,则在路由变化后自动解析页面
  2151. * @fires restoreContent 如果当前处于restore模式则触发内容恢复
  2152. * @fires handleDecode 如果启用了自动解码则触发自动解码
  2153. */
  2154. resetState() {
  2155. console.log('执行 resetState,自动解码状态:', this.config.autoDecode);
  2156.  
  2157. // 如果正在处理中,跳过这次重置
  2158. if (this.isProcessing || this.isDecodingContent || this.isRestoringContent) {
  2159. console.log('正在处理中,跳过这次状态重置');
  2160. return;
  2161. }
  2162.  
  2163. // 检查URL是否变化,如果变化了,可能是新页面
  2164. const currentUrl = window.location.href;
  2165. const urlChanged = currentUrl !== this.lastPageUrl;
  2166. if (urlChanged) {
  2167. console.log('URL已变化,从', this.lastPageUrl, '到', currentUrl);
  2168. this.lastPageUrl = currentUrl;
  2169. // URL变化时重置自动解码标志
  2170. this.hasAutoDecodedOnLoad = false;
  2171. }
  2172.  
  2173. // 页面刷新时的特殊处理
  2174. if (this.isPageRefresh && this.config.autoDecode) {
  2175. console.log('页面刷新且自动解码已启用');
  2176.  
  2177. // 如果页面刷新尚未完成,不执行任何操作,等待页面完全加载
  2178. if (!this.pageRefreshCompleted) {
  2179. console.log('页面刷新尚未完成,等待页面加载完成后再处理');
  2180. return;
  2181. }
  2182.  
  2183. // 页面上没有已解码内容,执行自动解码
  2184. console.log('页面刷新时未发现已解码内容,执行自动解码');
  2185. this.hasAutoDecodedOnLoad = true;
  2186. // 增加延时,确保页面内容已完全加载
  2187. setTimeout(() => {
  2188. if (!this.isProcessing && !this.isDecodingContent && !this.isRestoringContent) {
  2189. this.handleDecode();
  2190. // 同步按钮和菜单状态
  2191. setTimeout(() => this.syncButtonAndMenuState(), 200);
  2192. }
  2193. }, 1000);
  2194. return;
  2195. }
  2196.  
  2197. // 如果启用了自动解码,且尚未在页面加载时执行过,则在路由变化后自动解析页面
  2198. if (this.config.autoDecode && !this.hasAutoDecodedOnLoad) {
  2199. console.log('自动解码已启用,准备解析页面');
  2200. // 标记已执行过自动解码
  2201. this.hasAutoDecodedOnLoad = true;
  2202. // 使用延时确保页面内容已更新
  2203. setTimeout(() => {
  2204. console.log('resetState 中执行自动解码');
  2205. if (!this.isProcessing && !this.isDecodingContent && !this.isRestoringContent) {
  2206. this.handleDecode();
  2207. // 同步按钮和菜单状态
  2208. setTimeout(() => this.syncButtonAndMenuState(), 200);
  2209. }
  2210. }, 1000); // 增加延时时间,确保页面内容已完全加载
  2211. }
  2212. }
  2213.  
  2214. /**
  2215. * 为通知添加动画效果
  2216. * @param {HTMLElement} notification - 通知元素
  2217. */
  2218. animateNotification(notification) {
  2219. const currentTransform = getComputedStyle(notification).transform;
  2220. notification.style.transform = currentTransform;
  2221. notification.style.transition = 'all 0.3s ease-out';
  2222. notification.style.transform = 'translateY(-100%)';
  2223. }
  2224.  
  2225. /**
  2226. * 处理通知淡出效果
  2227. * @description 为通知添加淡出效果并处理相关动画
  2228. * @param {HTMLElement} notification - 要处理的通知元素
  2229. * @fires animateNotification 触发其他通知的位置调整动画
  2230. */
  2231. handleNotificationFadeOut(notification) {
  2232. notification.classList.add('fade-out');
  2233. const index = this.notifications.indexOf(notification);
  2234.  
  2235. this.notifications.slice(0, index).forEach((prev) => {
  2236. if (prev.parentNode) {
  2237. prev.style.transform = 'translateY(-100%)';
  2238. }
  2239. });
  2240. }
  2241.  
  2242. /**
  2243. * 清理通知容器
  2244. * @description 移除所有通知元素和相关事件监听器
  2245. * @fires removeEventListener 移除所有通知相关的事件监听器
  2246. */
  2247. cleanupNotificationContainer() {
  2248. // 清理通知相关的事件监听器
  2249. this.notificationEventListeners.forEach(({ element, event, handler }) => {
  2250. element.removeEventListener(event, handler);
  2251. });
  2252. this.notificationEventListeners = [];
  2253.  
  2254. // 移除所有通知元素
  2255. while (this.notificationContainer.firstChild) {
  2256. this.notificationContainer.firstChild.remove();
  2257. }
  2258.  
  2259. this.notificationContainer.remove();
  2260. this.notificationContainer = null;
  2261. }
  2262.  
  2263. /**
  2264. * 处理通知过渡结束事件
  2265. * @description 处理通知元素的过渡动画结束后的清理工作
  2266. * @param {TransitionEvent} e - 过渡事件对象
  2267. * @fires animateNotification 触发其他通知的位置调整
  2268. */
  2269. handleNotificationTransitionEnd(e) {
  2270. if (
  2271. e.propertyName === 'opacity' &&
  2272. e.target.classList.contains('fade-out')
  2273. ) {
  2274. const notification = e.target;
  2275. const index = this.notifications.indexOf(notification);
  2276.  
  2277. this.notifications.forEach((notif, i) => {
  2278. if (i > index && notif.parentNode) {
  2279. this.animateNotification(notif);
  2280. }
  2281. });
  2282.  
  2283. if (index > -1) {
  2284. this.notifications.splice(index, 1);
  2285. notification.remove();
  2286. }
  2287.  
  2288. if (this.notifications.length === 0) {
  2289. this.cleanupNotificationContainer();
  2290. }
  2291. }
  2292. }
  2293.  
  2294. /**
  2295. * 显示通知消息
  2296. * @description 创建并显示一个通知消息,包含自动消失功能
  2297. * @param {string} text - 通知文本内容
  2298. * @param {string} type - 通知类型 ('success'|'error'|'info')
  2299. * @fires handleNotificationFadeOut 触发通知淡出效果
  2300. * @example
  2301. * showNotification('操作成功', 'success')
  2302. */
  2303. showNotification(text, type) {
  2304. // 如果禁用了通知,则不显示
  2305. if (this.config && !this.config.showNotification) {
  2306. console.log(`[Base64 Helper] ${type}: ${text}`);
  2307. return;
  2308. }
  2309.  
  2310. // 设置通知显示标志,防止 MutationObserver 触发自动解码
  2311. this.isShowingNotification = true;
  2312.  
  2313. if (!this.notificationContainer) {
  2314. this.notificationContainer = document.createElement('div');
  2315. this.notificationContainer.className = 'base64-notifications-container';
  2316. document.body.appendChild(this.notificationContainer);
  2317.  
  2318. const handler = (e) => this.handleNotificationTransitionEnd(e);
  2319. this.notificationContainer.addEventListener('transitionend', handler);
  2320. this.notificationEventListeners.push({
  2321. element: this.notificationContainer,
  2322. event: 'transitionend',
  2323. handler,
  2324. });
  2325. }
  2326.  
  2327. const notification = document.createElement('div');
  2328. notification.className = 'base64-notification';
  2329. notification.setAttribute('data-type', type);
  2330. notification.textContent = text;
  2331.  
  2332. this.notifications.push(notification);
  2333. this.notificationContainer.appendChild(notification);
  2334.  
  2335. // 使用延时来清除通知标志,确保 DOM 变化已完成
  2336. setTimeout(() => {
  2337. this.isShowingNotification = false;
  2338. }, 100);
  2339.  
  2340. setTimeout(() => {
  2341. if (notification.parentNode) {
  2342. this.handleNotificationFadeOut(notification);
  2343. }
  2344. }, 2000);
  2345. }
  2346.  
  2347. /**
  2348. * 销毁插件实例
  2349. * @description 清理所有资源,移除事件监听器,恢复原始状态
  2350. * @fires restoreContent 如果需要则恢复原始内容
  2351. * @fires removeEventListener 移除所有事件监听器
  2352. */
  2353. destroy() {
  2354. // 清理所有事件监听器
  2355. this.eventListeners.forEach(({ element, event, handler, options }) => {
  2356. element.removeEventListener(event, handler, options);
  2357. });
  2358. this.eventListeners = [];
  2359.  
  2360. // 清理配置监听器
  2361. if (this.configListeners) {
  2362. Object.values(this.configListeners).forEach(listenerId => {
  2363. if (listenerId) {
  2364. storageManager.removeChangeListener(listenerId);
  2365. }
  2366. });
  2367. // 重置配置监听器
  2368. this.configListeners = {
  2369. showNotification: null,
  2370. hideButton: null,
  2371. autoDecode: null,
  2372. buttonPosition: null
  2373. };
  2374. }
  2375.  
  2376. // 清理定时器
  2377. if (this.resizeTimer) clearTimeout(this.resizeTimer);
  2378. if (this.routeTimer) clearTimeout(this.routeTimer);
  2379. if (this.domChangeTimer) clearTimeout(this.domChangeTimer);
  2380.  
  2381. // 清理 MutationObserver
  2382. if (this.observer) {
  2383. this.observer.disconnect();
  2384. this.observer = null;
  2385. }
  2386.  
  2387. // 清理通知相关资源
  2388. if (this.notificationContainer) {
  2389. this.cleanupNotificationContainer();
  2390. }
  2391. this.notifications = [];
  2392.  
  2393. // 恢复原始的 history 方法
  2394. if (this.originalPushState) history.pushState = this.originalPushState;
  2395. if (this.originalReplaceState)
  2396. history.replaceState = this.originalReplaceState;
  2397.  
  2398. // 恢复原始状态
  2399. if (this.decodeBtn?.dataset.mode === 'restore') {
  2400. this.restoreContent();
  2401. }
  2402.  
  2403. // 移除 DOM 元素
  2404. if (this.container) {
  2405. this.container.remove();
  2406. }
  2407.  
  2408. // 清理缓存
  2409. if (this.base64Cache) {
  2410. this.base64Cache.clear();
  2411. }
  2412.  
  2413. // 清理引用
  2414. this.shadowRoot = null;
  2415. this.mainBtn = null;
  2416. this.menu = null;
  2417. this.decodeBtn = null;
  2418. this.encodeBtn = null;
  2419. this.container = null;
  2420. this.originalContents.clear();
  2421. this.originalContents = null;
  2422. this.isDragging = false;
  2423. this.hasMoved = false;
  2424. this.menuVisible = false;
  2425. this.base64Cache = null;
  2426. this.configListeners = null;
  2427. }
  2428. }
  2429.  
  2430. // 确保只初始化一次
  2431. if (window.__base64HelperInstance) {
  2432. return;
  2433. }
  2434.  
  2435. // 只在主窗口中初始化
  2436. if (window.top === window.self) {
  2437. initStyles();
  2438. window.__base64HelperInstance = new Base64Helper();
  2439.  
  2440. // 注册(不可用)油猴菜单命令
  2441. registerMenuCommands();
  2442.  
  2443. // 确保在页面完全加载后更新菜单命令
  2444. window.addEventListener('load', () => {
  2445. console.log('页面加载完成,更新菜单命令');
  2446. updateMenuCommands();
  2447. });
  2448. }
  2449.  
  2450. // 使用 { once: true } 确保事件监听器只添加一次
  2451. window.addEventListener(
  2452. 'unload',
  2453. () => {
  2454. if (window.__base64HelperInstance) {
  2455. window.__base64HelperInstance.destroy();
  2456. delete window.__base64HelperInstance;
  2457. }
  2458. },
  2459. { once: true }
  2460. );
  2461. })();

QingJ © 2025

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