Gist 共享剪貼簿

共享選定文本到Gist並粘貼到剪貼簿

  1. // ==UserScript==
  2. // @name Gist Shared Clipboard
  3. // @name:ja Gist 共有クリップボード
  4. // @name:zh-CN Gist 共享剪贴板
  5. // @name:zh-TW Gist 共享剪貼簿
  6. // @license MIT
  7. // @namespace http://tampermonkey.net/
  8. // @version 2025.08.13
  9. // @description Share selected text to Gist and paste it to clipboard
  10. // @description:ja Gistに選択したテキストを共有し、クリップボードに貼り付ける
  11. // @description:zh-CN 共享选定文本到Gist并粘贴到剪贴板
  12. // @description:zh-TW 共享選定文本到Gist並粘貼到剪貼簿
  13. // @author Julia Lee
  14. // @match *://*/*
  15. // @noframes
  16. // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
  17. // @grant GM_registerMenuCommand
  18. // @grant GM_setValue
  19. // @grant GM_getValue
  20. // @grant GM_deleteValue
  21. // @grant GM_setClipboard
  22. // @grant GM_notification
  23. // @grant GM_xmlhttpRequest
  24. // @grant GM.xmlHttpRequest
  25. // @connect github.com
  26. // @connect api.github.com
  27. // ==/UserScript==
  28.  
  29. (async function () {
  30. 'use strict';
  31.  
  32. const GITHUB_TOKEN = await GM.getValue('GITHUB_TOKEN', ''); // GitHubのPersonal Access Tokenを指定
  33. const GIST_ID = await GM.getValue('GIST_ID', ''); // GistのIDを指定
  34. const GIST_ENCRYPT_KEY = await GM.getValue('GIST_ENCRYPT_KEY', ''); // Gistの暗号化キーを指定
  35. const FILENAME = 'GM-Shared-Clipboard.txt'; // Gist内のファイル名
  36. const RIGHT_CLICK_MAX_AGE = 20 * 1000; // 右クリックしてからTargetの保持時間(ミリ秒)
  37.  
  38. await GM.deleteValue('GIST_DOWNLOADING');
  39. await GM.deleteValue('GIST_UPLOADING');
  40.  
  41. let crtRightTgtContent = null;
  42. let crtRightTgtUpdated = 0;
  43.  
  44. if (GITHUB_TOKEN && GIST_ID && GIST_ENCRYPT_KEY) {
  45. const menu1 = GM_registerMenuCommand("Gist Share Clipboard", gistUploadClipboard, {
  46. accessKey: 'b',
  47. autoClose: true,
  48. title: 'Share Clipboard text to Gist',
  49. });
  50.  
  51. const menu2 = GM_registerMenuCommand("Gist Share Selected", gistUploadSelected, {
  52. accessKey: 'c',
  53. autoClose: true,
  54. title: 'Share selected text to Gist',
  55. });
  56.  
  57. const menu3 = GM_registerMenuCommand("Gist Paste", gistDowload, {
  58. accessKey: 'v',
  59. autoClose: true,
  60. title: 'Paste Gist content to clipboard',
  61. });
  62.  
  63. if (location.href.includes(GIST_ID)) {
  64. const menu4 = GM_registerMenuCommand("Decrypt Selected Text", decryptSelectedText, {
  65. accessKey: 'd',
  66. autoClose: true,
  67. title: 'Decrypt selected text and update clipboard',
  68. });
  69. }
  70. }
  71.  
  72. const menu0 = GM_registerMenuCommand("Gist Setup", setup, {
  73. accessKey: 'x',
  74. autoClose: true,
  75. title: 'Setup Gist ID and Token',
  76. });
  77.  
  78. document.body.addEventListener("mousedown", event => {
  79. if (event.button == 0) { // left click for mouse
  80. // crtRightTgtContent = null;
  81. } else if (event.button == 1) { // wheel click for mouse
  82. // crtRightTgtContent = null;
  83. } else if (event.button == 2) { // right click for mouse
  84. const elm = event.target;
  85. const nodName = elm.nodeName.toLowerCase();
  86.  
  87. switch (nodName) {
  88. case 'img':
  89. crtRightTgtContent = elm.src;
  90. break;
  91. case 'a':
  92. crtRightTgtContent = elm.href;
  93. break;
  94. default:
  95. crtRightTgtContent = null;
  96. break;
  97. }
  98.  
  99. if (crtRightTgtContent) {
  100. crtRightTgtUpdated = new Date();
  101. }
  102. }
  103. });
  104.  
  105. const gistUrl = `https://api.github.com/gists/${GIST_ID}`;
  106. const headers = {
  107. 'Authorization': `Bearer ${GITHUB_TOKEN}`,
  108. 'Content-Type': 'application/json',
  109. };
  110.  
  111. async function gistUploadClipboard(_event) {
  112. let clipboardText;
  113. try {
  114. clipboardText = await navigator.clipboard.readText();
  115. console.log("Clipboard Text: ", clipboardText);
  116. } catch (e) {
  117. const errorMsg = 'Please execute "Share Clipboard" by right-clicking.';
  118. await showMessage(`❌ ${errorMsg}`, 'NG', 3500);
  119. }
  120.  
  121. if (!clipboardText) { return }
  122. await gistUploadContents(clipboardText || crtRightTgtContent);
  123. }
  124.  
  125. async function gistUploadSelected(_event) {
  126. // If the target is too old, reset it
  127. if (crtRightTgtContent && (new Date()) - crtRightTgtUpdated > RIGHT_CLICK_MAX_AGE) {
  128. crtRightTgtContent = null;
  129. // crtRightTgtUpdated = 0;
  130. }
  131.  
  132. const selectedText = document.getSelection().toString();
  133. if (!crtRightTgtContent && !selectedText) { return }
  134.  
  135. await gistUploadContents(selectedText || crtRightTgtContent);
  136. }
  137.  
  138. const ENCRYPT_KEY = await GM.getValue('GIST_ENCRYPT_KEY', '');
  139.  
  140. async function gistUploadContents(contents) {
  141. if (!contents || contents.length === 0) {
  142. await showMessage('❌ No content to upload!', 'NG', 2500);
  143. return;
  144. }
  145.  
  146. if (!ENCRYPT_KEY) {
  147. await showMessage('❌ No encryption key set!', 'NG', 2500);
  148. return;
  149. }
  150.  
  151. const locked = await GM.getValue('GIST_UPLOADING');
  152. if (locked) {
  153. console.log("Gist is already uploading.");
  154. return;
  155. }
  156.  
  157. // --- ここで暗号化 ---
  158. const encrypted = await encryptContentAES(contents, ENCRYPT_KEY);
  159. const data = {
  160. files: {
  161. [FILENAME]: { content: JSON.stringify(encrypted) }
  162. }
  163. };
  164.  
  165. try {
  166. await GM.setValue('GIST_UPLOADING', true);
  167. const res = await GM.xmlHttpRequest({
  168. method: 'POST',
  169. url: gistUrl,
  170. headers,
  171. data: JSON.stringify(data),
  172. responseType: 'json'
  173. }).catch(e => { throw e; });
  174.  
  175. if (!res || res.status < 200 || res.status >= 300) {
  176. const errStr = res?.response || res?.statusText || 'Unknown error';
  177. console.error("Failed to update Gist: ", res);
  178. throw new Error(`Failed to update Gist: ${errStr}`);
  179. }
  180.  
  181. const result = res.response;
  182. console.log("Gist URL: ", result.html_url);
  183. await showMessage('✅ Target Shared!', 'OK', 2500);
  184. } catch (error) {
  185. console.error("Error: ", error);
  186. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  187. } finally {
  188. await GM.deleteValue('GIST_UPLOADING');
  189. }
  190.  
  191. }
  192.  
  193. async function gistDowload(_event) {
  194. if (inIframe()) {
  195. console.log("Gist Paste is not available in iframe.");
  196. return;
  197. }
  198.  
  199. const locked = await GM.getValue('GIST_DOWNLOADING');
  200. if (locked) {
  201. console.log("Gist is already Downloading.");
  202. return;
  203. }
  204.  
  205. try {
  206. await GM.setValue('GIST_DOWNLOADING', true);
  207. const res = await GM.xmlHttpRequest({
  208. method: 'GET',
  209. url: gistUrl,
  210. headers,
  211. responseType: 'json'
  212. }).catch(e => { throw e; });
  213.  
  214. if (!res || res.status < 200 || res.status >= 300) {
  215. const errStr = res?.response || res?.statusText || 'Unknown error';
  216. console.error("Failed to fetch Gist: ", res);
  217. throw new Error(`Failed to fetch Gist: ${errStr}`);
  218. }
  219.  
  220. const result = res.response;
  221. const encryptedContent = result.files[FILENAME].content;
  222.  
  223. if (!encryptedContent) {
  224. throw new Error('No content found in the Gist.');
  225. }
  226.  
  227. if (!ENCRYPT_KEY) {
  228. throw new Error('No encryption key set!');
  229. }
  230.  
  231. // --- 復号 ---
  232. let decrypted;
  233. try {
  234. decrypted = await decryptContentAES(JSON.parse(encryptedContent), ENCRYPT_KEY);
  235. } catch (e) {
  236. throw new Error('Failed to decrypt content!');
  237. }
  238.  
  239. await GM.setClipboard(decrypted, "text");
  240. console.log("Gist Content: ", decrypted);
  241. await showMessage('✅ Clipboard Updated!', 'OK', 2500);
  242.  
  243. } catch (error) {
  244. console.error("Error: ", error);
  245. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  246. } finally {
  247. await GM.deleteValue('GIST_DOWNLOADING');
  248. }
  249. }
  250.  
  251. async function setup() {
  252. if (inIframe()) {
  253. console.log("Gist Setup is not available in iframe.");
  254. return;
  255. }
  256.  
  257. const registerDialog = await createRegisterDialog();
  258. const gistIdInput = document.getElementById('gist-id-input');
  259. const gistTokenInput = document.getElementById('gist-token-input');
  260. const gistEncryptKeyInput = document.getElementById('gist-encrypt-key-input');
  261. const gistIdHelpLabel = document.querySelector('#gist-id-help span');
  262. const gistIdHelpLink = document.querySelector('#gist-id-help a');
  263. if (GIST_ID) {
  264. const gistUrl = `https://gist.github.com/${GIST_ID}/revisions`;
  265. gistIdHelpLabel.textContent = 'Gist Revisions URL: ';
  266. gistIdHelpLink.textContent = gistUrl.substring(0, 32) + '...';
  267. gistIdHelpLink.href = gistUrl;
  268. }
  269.  
  270. const saveButton = document.getElementById('save-button');
  271. saveButton.addEventListener('click', async () => {
  272. const gistId = gistIdInput.value;
  273. const token = gistTokenInput.value;
  274. const encryptKey = gistEncryptKeyInput.value;
  275.  
  276. if (!gistId || !token || !encryptKey) {
  277. await showMessage('❌ Gist ID, Token, and Encryption Key are required!', 'NG', 2500);
  278. return;
  279. }
  280.  
  281. await GM.setValue('GIST_ID', gistId);
  282. await GM.setValue('GITHUB_TOKEN', token);
  283. await GM.setValue('GIST_ENCRYPT_KEY', encryptKey);
  284. registerDialog.close();
  285. registerDialog.remove();
  286.  
  287. setTimeout(() => { location.reload() }, 2500); // Restart Script
  288.  
  289. await showMessage('✅ Gist ID, Token, and Encryption Key saved!', 'OK', 2500);
  290.  
  291. });
  292.  
  293. const clearInfoButton = document.getElementById('clear-button');
  294. clearInfoButton.addEventListener('click', async () => {
  295. if (!confirm('Are you sure you want to clear Gist ID, Token, and Encryption Key?')) {
  296. return;
  297. }
  298. await GM.deleteValue('GITHUB_TOKEN');
  299. await GM.deleteValue('GIST_ID');
  300. await GM.deleteValue('GIST_ENCRYPT_KEY');
  301. registerDialog.close();
  302. registerDialog.remove();
  303.  
  304. setTimeout(() => { location.reload() }, 2500); // Restart Script
  305.  
  306. await showMessage('✅ Gist ID, Token, and Encryption Key cleared!', 'OK', 2500);
  307. });
  308.  
  309. const generateKeyButton = document.getElementById('generate-key-button');
  310. generateKeyButton.addEventListener('click', () => {
  311. if (gistEncryptKeyInput.value) {
  312. let confirmMessage = 'Are you sure you want to generate a new encryption key?\n'
  313. confirmMessage += 'This will overwrite the existing key.';
  314. if (!confirm(confirmMessage)) { return }
  315. }
  316. const keyBytes = crypto.getRandomValues(new Uint8Array(16)); // 128bit = 16byte
  317. const keyBase64 = btoa(String.fromCharCode(...keyBytes));
  318. gistEncryptKeyInput.value = keyBase64;
  319. });
  320.  
  321. registerDialog.showModal();
  322. }
  323.  
  324. async function decryptSelectedText() {
  325. const selectedText = document.getSelection().toString();
  326. if (!selectedText) {
  327. await showMessage('❌ No selected text to decrypt!', 'NG', 2500);
  328. return;
  329. }
  330.  
  331. if (!ENCRYPT_KEY) {
  332. await showMessage('❌ No encryption key set!', 'NG', 2500);
  333. return;
  334. }
  335.  
  336. try {
  337. const decrypted = await decryptContentAES(JSON.parse(selectedText), ENCRYPT_KEY);
  338. await GM.setClipboard(decrypted, "text");
  339. console.log("Decrypted Content: ", decrypted);
  340. await showMessage('✅ Clipboard Updated with Decrypted Text!', 'OK', 2500);
  341. } catch (error) {
  342. console.error("Error: ", error);
  343. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  344. }
  345. }
  346.  
  347. })();
  348.  
  349. async function showMessage(text, type = 'OK', duration = 4000) {
  350. const htmlId = `GistShare_Message-${type}`;
  351. const existingMessage = document.getElementById(htmlId);
  352. if (existingMessage) { return; } // 既に表示されている場合は何もしない
  353.  
  354. if (duration < 1000) { duration = 1000; } // 最低1秒は表示する
  355.  
  356. return new Promise((resolve) => {
  357. const message = document.createElement('div');
  358. message.id = `GistShare_Message-${type}`;
  359. message.textContent = text;
  360.  
  361. // 共通スタイル
  362. Object.assign(message.style, {
  363. position: 'fixed',
  364. top: '20px',
  365. right: '20px',
  366. width: 'auto',
  367. padding: '12px 18px',
  368. borderRadius: '10px',
  369. color: '#fff',
  370. fontSize: '14px',
  371. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
  372. zIndex: 9999,
  373. transform: 'translateY(20px)',
  374. opacity: '0',
  375. transition: 'opacity 0.4s ease, transform 0.4s ease'
  376. });
  377.  
  378. // タイプ別デザイン
  379. if (type === 'OK') {
  380. message.style.backgroundColor = '#4caf50'; // 緑
  381. message.style.borderLeft = '6px solid #2e7d32';
  382. } else if (type === 'NG') {
  383. message.style.backgroundColor = '#f44336'; // 赤
  384. message.style.borderLeft = '6px solid #b71c1c';
  385. }
  386.  
  387. document.body.appendChild(message);
  388.  
  389. // フェードイン(下から)
  390. setTimeout(() => {
  391. message.style.opacity = '.95';
  392. message.style.transform = 'translateY(0)';
  393. }, 10);
  394. // requestAnimationFrame(() => {
  395. // message.style.opacity = '1';
  396. // message.style.transform = 'translateY(0)';
  397. // });
  398.  
  399. // 指定時間後にフェードアウト
  400. setTimeout(() => {
  401. message.style.opacity = '0';
  402. message.style.transform = 'translateY(-20px)';
  403. setTimeout(() => {
  404. message.remove();
  405. resolve(); // メッセージが削除された後にresolveを呼び出す
  406. }, 400); // transition と一致
  407. }, duration - 400);
  408. });
  409. }
  410.  
  411. async function createRegisterDialog() {
  412. const existDialog = document.getElementById('tm-gist-dialog');
  413. if (existDialog) existDialog.remove();
  414.  
  415. const registerDialog = document.createElement('dialog');
  416. registerDialog.id = 'tm-gist-dialog';
  417. registerDialog.style.padding = '1em';
  418. registerDialog.style.zIndex = 9999;
  419. registerDialog.style.maxWidth = '400px';
  420. registerDialog.style.margin = 'auto';
  421. registerDialog.style.border = '2px solid #ccc';
  422. registerDialog.style.borderRadius = '8px';
  423. registerDialog.style.backgroundColor = '#fff';
  424. registerDialog.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
  425. registerDialog.style.position = 'fixed';
  426. registerDialog.style.padding = '1.5em';
  427. registerDialog.style.boxSizing = 'border-box';
  428.  
  429. // --- Gist ID ラベルと入力欄 ---
  430. const gistIdLabel = document.createElement('label');
  431. gistIdLabel.textContent = 'Gist ID:';
  432. gistIdLabel.style.display = 'block';
  433. gistIdLabel.style.marginBottom = '0.5em';
  434. gistIdLabel.for = 'gist-id-input';
  435. registerDialog.appendChild(gistIdLabel);
  436.  
  437. const gistIdInput = document.createElement('input');
  438. gistIdInput.id = 'gist-id-input';
  439. gistIdInput.type = 'text';
  440. gistIdInput.style.width = '100%';
  441. gistIdInput.style.boxSizing = 'border-box';
  442. gistIdInput.style.padding = '0.5em';
  443. gistIdInput.style.border = '1px solid #ccc';
  444. gistIdInput.style.borderRadius = '4px';
  445. gistIdInput.style.marginBottom = '1em';
  446. gistIdInput.value = await GM.getValue('GIST_ID', '');
  447. gistIdInput.placeholder = 'Your Gist ID';
  448. registerDialog.appendChild(gistIdInput);
  449.  
  450. const gistIdHelpText = document.createElement('small');
  451. gistIdHelpText.id = 'gist-id-help';
  452. gistIdHelpText.style.display = 'block';
  453. gistIdHelpText.style.marginBottom = '1.1em';
  454. gistIdHelpText.style.color = '#666';
  455. const gistIdHelpLabel = document.createElement('span');
  456. gistIdHelpLabel.textContent = 'Create or Select a Gist: ';
  457. const gistIdHelpLink = document.createElement('a');
  458. gistIdHelpLink.href = 'https://gist.github.com/mine';
  459. gistIdHelpLink.target = '_blank';
  460. gistIdHelpLink.textContent = 'https://gist.github.com/';
  461. gistIdHelpText.appendChild(gistIdHelpLabel);
  462. gistIdHelpText.appendChild(gistIdHelpLink);
  463. registerDialog.appendChild(gistIdHelpText);
  464.  
  465. // --- Gist Token ラベルと入力欄 ---
  466. const gistTokenLabel = document.createElement('label');
  467. gistTokenLabel.textContent = 'Gist Token:';
  468. gistTokenLabel.style.display = 'block';
  469. gistTokenLabel.style.marginBottom = '0.5em';
  470. gistTokenLabel.for = 'gist-token-input';
  471. registerDialog.appendChild(gistTokenLabel);
  472.  
  473. const gistTokenInput = document.createElement('input');
  474. gistTokenInput.id = 'gist-token-input';
  475. // gistTokenInput.type = 'password';
  476. gistTokenInput.style.width = '100%';
  477. gistTokenInput.style.boxSizing = 'border-box';
  478. gistTokenInput.style.padding = '0.5em';
  479. gistTokenInput.style.border = '1px solid #ccc';
  480. gistTokenInput.style.borderRadius = '4px';
  481. gistTokenInput.style.marginBottom = '1em';
  482. gistTokenInput.value = await GM.getValue('GITHUB_TOKEN', '');
  483. gistTokenInput.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  484. registerDialog.appendChild(gistTokenInput);
  485.  
  486. const gistTokenHelpText = document.createElement('small');
  487. gistTokenHelpText.style.display = 'block';
  488. gistTokenHelpText.style.marginBottom = '1em';
  489. gistTokenHelpText.style.color = '#666';
  490. const gistTokenHelpLabel = document.createElement('span');
  491. gistTokenHelpLabel.textContent = 'Create a Token: ';
  492. gistTokenHelpText.appendChild(gistTokenHelpLabel);
  493. const gistTokenHelpLink = document.createElement('a');
  494. gistTokenHelpLink.href = 'https://github.com/settings/tokens';
  495. gistTokenHelpLink.target = '_blank';
  496. gistTokenHelpLink.textContent = 'https://github.com/settings/tokens';
  497. gistTokenHelpText.appendChild(gistTokenHelpLink);
  498. registerDialog.appendChild(gistTokenHelpText);
  499.  
  500. // --- Encryption Key ラベルと入力欄 ---
  501. const gistEncryptKeyLabel = document.createElement('label');
  502. gistEncryptKeyLabel.textContent = 'Encryption Key:';
  503. gistEncryptKeyLabel.style.display = 'block';
  504. gistEncryptKeyLabel.style.marginBottom = '0.5em';
  505. gistEncryptKeyLabel.for = 'gist-encrypt-key-input';
  506. registerDialog.appendChild(gistEncryptKeyLabel);
  507.  
  508. const gistEncryptKeyInput = document.createElement('input');
  509. gistEncryptKeyInput.id = 'gist-encrypt-key-input';
  510. // gistEncryptKeyInput.type = 'password';
  511. gistEncryptKeyInput.style.width = 'calc(100% - 100px)';
  512. gistEncryptKeyInput.style.boxSizing = 'border-box';
  513. gistEncryptKeyInput.style.padding = '0.5em';
  514. gistEncryptKeyInput.style.border = '1px solid #ccc';
  515. gistEncryptKeyInput.style.borderRadius = '4px';
  516. gistEncryptKeyInput.style.marginBottom = '1em';
  517. gistEncryptKeyInput.style.display = 'inline-block';
  518. gistEncryptKeyInput.value = await GM.getValue('GIST_ENCRYPT_KEY', '');
  519. gistEncryptKeyInput.placeholder = 'Base64 128bit key';
  520. registerDialog.appendChild(gistEncryptKeyInput);
  521.  
  522. // 生成ボタン
  523. const generateKeyButton = document.createElement('button');
  524. generateKeyButton.id = 'generate-key-button';
  525. generateKeyButton.textContent = 'KeyGen';
  526. generateKeyButton.type = 'button';
  527. generateKeyButton.style.width = '90px';
  528. generateKeyButton.style.marginLeft = '10px';
  529. generateKeyButton.style.marginBottom = '1em';
  530. generateKeyButton.style.padding = '0.5em 1em';
  531. generateKeyButton.style.borderRadius = '4px';
  532. generateKeyButton.style.border = '1px solid #888';
  533. generateKeyButton.style.backgroundColor = '#eee';
  534. generateKeyButton.style.cursor = 'pointer';
  535. registerDialog.appendChild(generateKeyButton);
  536.  
  537. const saveButton = document.createElement('button');
  538. saveButton.textContent = 'Save Info';
  539. saveButton.style.backgroundColor = '#4caf50';
  540. saveButton.style.color = '#fff';
  541. saveButton.style.border = 'none';
  542. saveButton.style.padding = '0.5em 1em';
  543. saveButton.style.borderRadius = '4px';
  544. saveButton.style.cursor = 'pointer';
  545. saveButton.style.marginTop = '1em';
  546. saveButton.style.float = 'right';
  547. saveButton.id = 'save-button';
  548. registerDialog.appendChild(saveButton);
  549.  
  550. const clearInfoButton = document.createElement('button');
  551. clearInfoButton.textContent = 'Clear Info';
  552. clearInfoButton.style.backgroundColor = '#f44336';
  553. clearInfoButton.style.color = '#fff';
  554. clearInfoButton.style.border = 'none';
  555. clearInfoButton.style.padding = '0.5em 1em';
  556. clearInfoButton.style.borderRadius = '4px';
  557. clearInfoButton.style.cursor = 'pointer';
  558. clearInfoButton.style.marginTop = '1em';
  559. clearInfoButton.style.marginRight = '0.5em';
  560. clearInfoButton.style.float = 'right';
  561. clearInfoButton.id = 'clear-button';
  562. registerDialog.appendChild(clearInfoButton);
  563.  
  564. const closeButton = document.createElement('button');
  565. closeButton.textContent = 'X';
  566. closeButton.style.position = 'absolute';
  567. closeButton.style.top = '7px';
  568. closeButton.style.right = '7px';
  569. closeButton.style.backgroundColor = '#ccc';
  570. closeButton.style.border = 'none';
  571. closeButton.style.borderRadius = '15%';
  572. closeButton.style.color = '#fff';
  573. closeButton.style.cursor = 'pointer';
  574. closeButton.style.padding = '0.2em 0.5em';
  575. closeButton.style.fontSize = '14px';
  576. closeButton.addEventListener('click', () => {
  577. registerDialog.close();
  578. registerDialog.remove();
  579. });
  580. registerDialog.appendChild(closeButton);
  581.  
  582. const css = document.createElement('style');
  583. css.id = 'tm-gist-css';
  584. css.textContent = `
  585. #tm-gist-dialog button:hover {
  586. opacity: 0.8;
  587. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  588. }`;
  589. registerDialog.appendChild(css);
  590.  
  591. document.body.appendChild(registerDialog);
  592.  
  593. return registerDialog;
  594. }
  595.  
  596. function inIframe() {
  597. try {
  598. return window.self !== window.top;
  599. } catch (e) {
  600. return true;
  601. }
  602. }
  603.  
  604. // --- 追加: 暗号化/復号関数 ---
  605. async function encryptContentAES(plainText, keyBase64) {
  606. // keyBase64: base64エンコードされた16byte(128bit)キー
  607. const enc = new TextEncoder();
  608. const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
  609. if (keyBytes.length !== 16) throw new Error('Encryption key must be 128bit (16 bytes, base64)');
  610. const iv = crypto.getRandomValues(new Uint8Array(16));
  611. const key = await crypto.subtle.importKey(
  612. 'raw', keyBytes, { name: 'AES-CBC' }, false, ['encrypt']
  613. );
  614. const encrypted = await crypto.subtle.encrypt(
  615. { name: 'AES-CBC', iv }, key, enc.encode(plainText)
  616. );
  617. return {
  618. iv: btoa(String.fromCharCode(...iv)),
  619. data: btoa(String.fromCharCode(...new Uint8Array(encrypted)))
  620. };
  621. }
  622.  
  623. async function decryptContentAES(encryptedObj, keyBase64) {
  624. const dec = new TextDecoder();
  625. const keyBytes = Uint8Array.from(atob(keyBase64), c => c.charCodeAt(0));
  626. if (keyBytes.length !== 16) throw new Error('Encryption key must be 128bit (16 bytes, base64)');
  627. const iv = Uint8Array.from(atob(encryptedObj.iv), c => c.charCodeAt(0));
  628. const data = Uint8Array.from(atob(encryptedObj.data), c => c.charCodeAt(0));
  629. const key = await crypto.subtle.importKey(
  630. 'raw', keyBytes, { name: 'AES-CBC' }, false, ['decrypt']
  631. );
  632. const decrypted = await crypto.subtle.decrypt(
  633. { name: 'AES-CBC', iv }, key, data
  634. );
  635. return dec.decode(decrypted);
  636. }

QingJ © 2025

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