Discourse Base64 Helper

Base64编解码工具 for Discourse论坛

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

  1. // ==UserScript==
  2. // @name Discourse Base64 Helper
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2
  5. // @description Base64编解码工具 for Discourse论坛
  6. // @author Xavier
  7. // @match *://linux.do/*
  8. // @match *://clochat.com/*
  9. // @grant GM_notification
  10. // @grant GM_setClipboard
  11. // @grant GM_addStyle
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @run-at document-idle
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // 样式注入
  21. GM_addStyle(`
  22. .decoded-text {
  23. cursor: pointer;
  24. transition: all 0.2s;
  25. padding: 1px 3px;
  26. border-radius: 3px;
  27. background-color: #fff3cd !important;
  28. color: #664d03 !important;
  29. }
  30.  
  31. .decoded-text:hover {
  32. background-color: #ffe69c !important;
  33. }
  34.  
  35. @media (prefers-color-scheme: dark) {
  36. .decoded-text {
  37. background-color: #332100 !important;
  38. color: #ffd54f !important;
  39. }
  40. .decoded-text:hover {
  41. background-color: #664d03 !important;
  42. }
  43. }
  44.  
  45. .menu-item[data-mode="restore"] {
  46. background: rgba(0, 123, 255, 0.1) !important;
  47. }
  48. `);
  49.  
  50. // 初始化检测
  51. if (document.getElementById('base64-helper-root')) return;
  52. const container = document.createElement('div');
  53. container.id = 'base64-helper-root';
  54. document.body.append(container);
  55. const shadowRoot = container.attachShadow({ mode: 'open' });
  56.  
  57. // Shadow DOM样式
  58. const style = document.createElement('style');
  59. style.textContent = `
  60. :host {
  61. all: initial !important;
  62. position: fixed !important;
  63. z-index: 2147483647 !important;
  64. pointer-events: none !important;
  65. }
  66.  
  67. .base64-helper {
  68. position: fixed;
  69. z-index: 2147483647;
  70. cursor: move;
  71. font-family: system-ui, -apple-system, sans-serif;
  72. opacity: 0.5;
  73. transition: opacity 0.3s ease, transform 0.2s;
  74. pointer-events: auto !important;
  75. will-change: transform;
  76. }
  77.  
  78. .base64-helper:hover {
  79. opacity: 1 !important;
  80. }
  81.  
  82. .main-btn {
  83. background: #ffffff;
  84. color: #000000 !important;
  85. padding: 8px 16px;
  86. border-radius: 6px;
  87. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
  88. font-weight: 500;
  89. user-select: none;
  90. transition: all 0.2s;
  91. font-size: 14px;
  92. cursor: pointer;
  93. border: none !important;
  94. pointer-events: auto !important;
  95. }
  96.  
  97. .menu {
  98. position: absolute;
  99. bottom: calc(100% + 5px);
  100. right: 0;
  101. background: #ffffff;
  102. border-radius: 6px;
  103. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  104. display: none;
  105. min-width: auto !important;
  106. width: max-content !important;
  107. pointer-events: auto !important;
  108. overflow: hidden;
  109. }
  110.  
  111. .menu-item {
  112. padding: 8px 12px !important;
  113. color: #333 !important;
  114. transition: all 0.2s;
  115. font-size: 13px;
  116. cursor: pointer;
  117. position: relative;
  118. border-radius: 0 !important;
  119. isolation: isolate;
  120. white-space: nowrap !important;
  121. }
  122.  
  123. .menu-item:hover::before {
  124. content: '';
  125. position: absolute;
  126. top: 0;
  127. left: 0;
  128. right: 0;
  129. bottom: 0;
  130. background: currentColor;
  131. opacity: 0.1;
  132. z-index: -1;
  133. }
  134.  
  135. .menu-item:first-child:hover::before {
  136. border-radius: 6px 6px 0 0;
  137. }
  138.  
  139. .menu-item:last-child:hover::before {
  140. border-radius: 0 0 6px 6px;
  141. }
  142.  
  143. @media (prefers-color-scheme: dark) {
  144. .main-btn {
  145. background: #2d2d2d;
  146. color: #fff !important;
  147. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  148. }
  149. .menu {
  150. background: #1a1a1a;
  151. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
  152. }
  153. .menu-item {
  154. color: #e0e0e0 !important;
  155. }
  156. .menu-item:hover::before {
  157. opacity: 0.08;
  158. }
  159. }
  160.  
  161. @keyframes slideIn {
  162. from { top: -50px; opacity: 0; }
  163. to { top: 20px; opacity: 1; }
  164. }
  165.  
  166. @keyframes fadeOut {
  167. from { opacity: 1; }
  168. to { opacity: 0; }
  169. }
  170. `;
  171. shadowRoot.appendChild(style);
  172.  
  173. // 界面元素
  174. const uiContainer = document.createElement('div');
  175. uiContainer.className = 'base64-helper';
  176.  
  177. const mainBtn = document.createElement('button');
  178. mainBtn.className = 'main-btn';
  179. mainBtn.textContent = 'Base64';
  180.  
  181. const menu = document.createElement('div');
  182. menu.className = 'menu';
  183.  
  184. const decodeBtn = document.createElement('div');
  185. decodeBtn.className = 'menu-item';
  186. decodeBtn.textContent = '解析本页Base64';
  187. decodeBtn.dataset.mode = 'decode';
  188.  
  189. const encodeBtn = document.createElement('div');
  190. encodeBtn.className = 'menu-item';
  191. encodeBtn.textContent = '文本转Base64';
  192.  
  193. menu.append(decodeBtn, encodeBtn);
  194. uiContainer.append(mainBtn, menu);
  195. shadowRoot.appendChild(uiContainer);
  196.  
  197. // 核心功能
  198. let menuVisible = false;
  199. let isDragging = false;
  200. let startX, startY, initialX, initialY;
  201. const originalContents = new Map();
  202.  
  203. // 位置管理
  204. const positionManager = {
  205. get: () => {
  206. const saved = GM_getValue('btnPosition');
  207. if (!saved) return null;
  208.  
  209. const maxX = window.innerWidth - uiContainer.offsetWidth - 20;
  210. const maxY = window.innerHeight - uiContainer.offsetHeight - 20;
  211.  
  212. return {
  213. x: Math.min(Math.max(saved.x, 20), maxX),
  214. y: Math.min(Math.max(saved.y, 20), maxY)
  215. };
  216. },
  217. set: (x, y) => {
  218. const pos = {
  219. x: Math.max(20, Math.min(x, window.innerWidth - uiContainer.offsetWidth - 20)),
  220. y: Math.max(20, Math.min(y, window.innerHeight - uiContainer.offsetHeight - 20))
  221. };
  222.  
  223. GM_setValue('btnPosition', pos);
  224. return pos;
  225. }
  226. };
  227.  
  228. // 初始化位置
  229. const initPosition = () => {
  230. const pos = positionManager.get() || {
  231. x: window.innerWidth - 120,
  232. y: window.innerHeight - 80
  233. };
  234.  
  235. uiContainer.style.left = `${pos.x}px`;
  236. uiContainer.style.top = `${pos.y}px`;
  237. };
  238. initPosition();
  239.  
  240. // 状态重置
  241. function resetState() {
  242. if (decodeBtn.dataset.mode === 'restore') {
  243. restoreOriginalContent();
  244. decodeBtn.textContent = '解析本页Base64';
  245. decodeBtn.dataset.mode = 'decode';
  246. originalContents.clear();
  247. }
  248. }
  249.  
  250. // 事件监听
  251. mainBtn.addEventListener('click', function(e) {
  252. e.stopPropagation();
  253. menuVisible = !menuVisible;
  254. menu.style.display = menuVisible ? 'block' : 'none';
  255. });
  256.  
  257. document.addEventListener('click', function(e) {
  258. if (menuVisible && !shadowRoot.contains(e.target)) {
  259. menuVisible = false;
  260. menu.style.display = 'none';
  261. }
  262. });
  263.  
  264. // 拖拽功能
  265. mainBtn.addEventListener('mousedown', startDrag);
  266. document.addEventListener('mousemove', drag);
  267. document.addEventListener('mouseup', stopDrag);
  268.  
  269. function startDrag(e) {
  270. isDragging = true;
  271. startX = e.clientX;
  272. startY = e.clientY;
  273. const rect = uiContainer.getBoundingClientRect();
  274. initialX = rect.left;
  275. initialY = rect.top;
  276. uiContainer.style.transition = 'none';
  277. }
  278.  
  279. function drag(e) {
  280. if (!isDragging) return;
  281. const dx = e.clientX - startX;
  282. const dy = e.clientY - startY;
  283.  
  284. const newX = initialX + dx;
  285. const newY = initialY + dy;
  286.  
  287. const pos = positionManager.set(newX, newY);
  288. uiContainer.style.left = `${pos.x}px`;
  289. uiContainer.style.top = `${pos.y}px`;
  290. }
  291.  
  292. function stopDrag() {
  293. isDragging = false;
  294. uiContainer.style.transition = 'opacity 0.3s ease';
  295. }
  296.  
  297. // 窗口resize处理
  298. let resizeTimer;
  299. window.addEventListener('resize', () => {
  300. clearTimeout(resizeTimer);
  301. resizeTimer = setTimeout(() => {
  302. const pos = positionManager.get();
  303. if (pos) {
  304. uiContainer.style.left = `${pos.x}px`;
  305. uiContainer.style.top = `${pos.y}px`;
  306. }
  307. }, 100);
  308. });
  309.  
  310. // 页面导航事件
  311. window.addEventListener('popstate', resetState);
  312. window.addEventListener('turbo:render', resetState);
  313. window.addEventListener('discourse:before-auto-refresh', () => {
  314. GM_setValue('btnPosition', positionManager.get());
  315. resetState();
  316. });
  317.  
  318. // 解析功能
  319. decodeBtn.addEventListener('click', function() {
  320. if (this.dataset.mode === 'restore') {
  321. restoreOriginalContent();
  322. this.textContent = '解析本页Base64';
  323. this.dataset.mode = 'decode';
  324. showNotification('已恢复原始内容', 'success');
  325. menu.style.display = 'none';
  326. return;
  327. }
  328.  
  329. originalContents.clear();
  330. let hasValidBase64 = false;
  331.  
  332. try {
  333. document.querySelectorAll('.cooked, .post-body').forEach(element => {
  334. const regex = /(?<!\w)([A-Za-z0-9+/]{6,}?={0,2})(?!\w)/g;
  335. let newHtml = element.innerHTML;
  336. let modified = false;
  337.  
  338. Array.from(newHtml.matchAll(regex)).reverse().forEach(match => {
  339. const original = match[0];
  340. if (!validateBase64(original)) return;
  341.  
  342. try {
  343. const decoded = decodeBase64(original);
  344. originalContents.set(element, element.innerHTML);
  345.  
  346. newHtml = newHtml.substring(0, match.index) +
  347. `<span class="decoded-text">${decoded}</span>` +
  348. newHtml.substring(match.index + original.length);
  349.  
  350. hasValidBase64 = modified = true;
  351. } catch(e) {}
  352. });
  353.  
  354. if (modified) element.innerHTML = newHtml;
  355. });
  356.  
  357. if (!hasValidBase64) {
  358. showNotification('本页未发现有效Base64内容', 'info');
  359. originalContents.clear();
  360. return;
  361. }
  362.  
  363. document.querySelectorAll('.decoded-text').forEach(el => {
  364. el.addEventListener('click', copyToClipboard);
  365. });
  366.  
  367. decodeBtn.textContent = '恢复本页Base64';
  368. decodeBtn.dataset.mode = 'restore';
  369. showNotification('解析完成', 'success');
  370. } catch (e) {
  371. showNotification('解析失败: ' + e.message, 'error');
  372. originalContents.clear();
  373. }
  374.  
  375. menuVisible = false;
  376. menu.style.display = 'none';
  377. });
  378.  
  379. // 编码功能
  380. encodeBtn.addEventListener('click', function() {
  381. const text = prompt('请输入要编码的文本:');
  382. if (text === null) return;
  383.  
  384. try {
  385. const encoded = encodeBase64(text);
  386. GM_setClipboard(encoded);
  387. showNotification('Base64已复制', 'success');
  388. } catch (e) {
  389. showNotification('编码失败: ' + e.message, 'error');
  390. }
  391. menu.style.display = 'none';
  392. });
  393.  
  394. // 改进的校验函数
  395. function validateBase64(str) {
  396. // 基础校验
  397. const validLength = str.length % 4 === 0;
  398. const validChars = /^[A-Za-z0-9+/]+={0,2}$/.test(str);
  399. const validPadding = !(str.includes('=') && !/==?$/.test(str));
  400. if (!validLength || !validChars || !validPadding) return false;
  401.  
  402. // 移除填充后的校验
  403. const baseStr = str.replace(/=+$/, '');
  404. if (baseStr.length < 6) return false;
  405. const hasSpecialChar = /[+/0-9]/.test(baseStr);
  406. return hasSpecialChar;
  407. }
  408.  
  409. // 工具函数
  410. function decodeBase64(str) {
  411. return decodeURIComponent(escape(atob(str)));
  412. }
  413.  
  414. function encodeBase64(str) {
  415. return btoa(unescape(encodeURIComponent(str)));
  416. }
  417.  
  418. function restoreOriginalContent() {
  419. originalContents.forEach((html, element) => {
  420. element.innerHTML = html;
  421. });
  422. originalContents.clear();
  423. }
  424.  
  425. function copyToClipboard(e) {
  426. GM_setClipboard(e.target.innerText);
  427. showNotification('内容已复制', 'success');
  428. e.stopPropagation();
  429. }
  430.  
  431. // 通知系统
  432. function showNotification(text, type) {
  433. const notification = document.createElement('div');
  434. notification.style.cssText = `
  435. position: fixed;
  436. top: 20px;
  437. left: 50%;
  438. transform: translateX(-50%);
  439. padding: 12px 24px;
  440. border-radius: 6px;
  441. background: ${type === 'success' ? '#4CAF50' :
  442. type === 'error' ? '#f44336' : '#2196F3'};
  443. color: white;
  444. z-index: 2147483647;
  445. animation: slideIn 0.3s forwards, fadeOut 0.3s 2s forwards;
  446. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  447. font-family: system-ui, -apple-system, sans-serif;
  448. pointer-events: none;
  449. `;
  450. notification.textContent = text;
  451. document.body.appendChild(notification);
  452. setTimeout(() => notification.remove(), 2300);
  453. }
  454.  
  455. // 防冲突处理
  456. if (window.hasBase64Helper) return;
  457. window.hasBase64Helper = true;
  458. })();

QingJ © 2025

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