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-05-27
  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. // @icon https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_setValue
  18. // @grant GM_getValue
  19. // @grant GM_deleteValue
  20. // @grant GM_setClipboard
  21. // ==/UserScript==
  22.  
  23. (async function () {
  24. 'use strict';
  25.  
  26. const GITHUB_TOKEN = await GM.getValue('GITHUB_TOKEN', ''); // GitHubのPersonal Access Tokenを指定
  27. const GIST_ID = await GM.getValue('GIST_ID', ''); // GistのIDを指定
  28. const FILENAME = 'GM-Shared-Clipboard.txt'; // Gist内のファイル名
  29. const RIGHT_CLICK_MAX_AGE = 20 * 1000; // 右クリックしてからTargetの保持時間(ミリ秒)
  30.  
  31. await GM.deleteValue('GIST_DOWNLOADING');
  32. await GM.deleteValue('GIST_UPLOADING');
  33.  
  34. let crtRightTgtContent = null;
  35. let crtRightTgtUpdated = 0;
  36.  
  37. if (GITHUB_TOKEN && GIST_ID) {
  38. const menu1 = GM_registerMenuCommand("Gist Share", gistUpload, {
  39. accessKey: 'c',
  40. autoClose: true,
  41. title: 'Share selected text to Gist',
  42. });
  43.  
  44. const menu2 = GM_registerMenuCommand("Gist Paste", gistDowload, {
  45. accessKey: 'v',
  46. autoClose: true,
  47. title: 'Paste Gist content to clipboard',
  48. });
  49. }
  50.  
  51. const menu3 = GM_registerMenuCommand("Gist Setup", setup, {
  52. accessKey: 'x',
  53. autoClose: true,
  54. title: 'Setup Gist ID and Token',
  55. });
  56.  
  57. document.body.addEventListener("mousedown", event => {
  58. if (event.button == 0) { // left click for mouse
  59. // crtRightTgtContent = null;
  60. } else if (event.button == 1) { // wheel click for mouse
  61. // crtRightTgtContent = null;
  62. } else if (event.button == 2) { // right click for mouse
  63. const elm = event.target;
  64. const nodName = elm.nodeName.toLowerCase();
  65.  
  66. switch (nodName) {
  67. case 'img':
  68. crtRightTgtContent = elm.src;
  69. break;
  70. case 'a':
  71. crtRightTgtContent = elm.href;
  72. break;
  73. default:
  74. crtRightTgtContent = null;
  75. break;
  76. }
  77.  
  78. if (crtRightTgtContent) {
  79. crtRightTgtUpdated = new Date();
  80. }
  81. }
  82. });
  83.  
  84. const gistUrl = `https://api.github.com/gists/${GIST_ID}`;
  85. const headers = {
  86. 'Authorization': `Bearer ${GITHUB_TOKEN}`,
  87. 'Content-Type': 'application/json',
  88. };
  89.  
  90. async function gistUpload(_event) {
  91. // If the target is too old, reset it
  92. if (crtRightTgtContent && (new Date()) - crtRightTgtUpdated > RIGHT_CLICK_MAX_AGE) {
  93. crtRightTgtContent = null;
  94. // crtRightTgtUpdated = 0;
  95. }
  96.  
  97. const selectedText = document.getSelection().toString();
  98. if (!crtRightTgtContent && !selectedText) { return }
  99.  
  100. const locked = await GM.getValue('GIST_UPLOADING');
  101. if (locked) {
  102. console.log("Gist is already uploading.");
  103. return;
  104. }
  105.  
  106. const data = {
  107. files: {
  108. [FILENAME]: { content: selectedText || crtRightTgtContent }
  109. }
  110. };
  111.  
  112. try {
  113. await GM.setValue('GIST_UPLOADING', true);
  114. const res = await fetch(gistUrl, {
  115. method: 'POST', headers,
  116. body: JSON.stringify(data)
  117. });
  118.  
  119. if (!res.ok) {
  120. const error = await res.json();
  121. throw new Error(`Failed to update Gist: ${error.message}`);
  122. }
  123.  
  124. const result = await res.json();
  125.  
  126. // await GM.setClipboard(result.html_url, "text")
  127. await GM.setClipboard(result.files[FILENAME].content, "text");
  128. console.log("Gist URL: ", result.html_url);
  129. await showMessage('✅ Target Shared!', 'OK', 2500);
  130. } catch (error) {
  131. console.error("Error: ", error);
  132. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  133. }
  134. finally {
  135. await GM.deleteValue('GIST_UPLOADING');
  136. }
  137. }
  138.  
  139. async function gistDowload(_event) {
  140. if (inIframe()) {
  141. console.log("Gist Paste is not available in iframe.");
  142. return;
  143. }
  144.  
  145. const locked = await GM.getValue('GIST_DOWNLOADING');
  146. if (locked) {
  147. console.log("Gist is already Downloading.");
  148. return;
  149. }
  150.  
  151. try {
  152. await GM.setValue('GIST_DOWNLOADING', true);
  153. const res = await fetch(gistUrl, { headers });
  154. if (!res.ok) {
  155. const error = await res.json();
  156. throw new Error(`Failed to fetch Gist: ${error.message}`);
  157. }
  158.  
  159. const result = await res.json();
  160. const content = result.files[FILENAME].content;
  161.  
  162. if (!content) {
  163. throw new Error('No content found in the Gist.');
  164. }
  165.  
  166. await GM.setClipboard(content, "text");
  167. console.log("Gist Content: ", content);
  168. await showMessage('✅ Clipboard Updated!', 'OK', 2500);
  169.  
  170. } catch (error) {
  171. console.error("Error: ", error);
  172. await showMessage(`❌ ${error.message}`, 'NG', 2500);
  173. } finally {
  174. await GM.deleteValue('GIST_DOWNLOADING');
  175. }
  176. }
  177.  
  178. async function setup() {
  179. if (inIframe()) {
  180. console.log("Gist Setup is not available in iframe.");
  181. return;
  182. }
  183.  
  184. const registerDialog = await createRegisterDialog();
  185. registerDialog.showModal();
  186. const saveButton = document.getElementById('save-button');
  187. const gistIdInput = document.getElementById('gist-id-input');
  188. const gistTokenInput = document.getElementById('gist-token-input');
  189. saveButton.addEventListener('click', async () => {
  190. const gistId = gistIdInput.value;
  191. const token = gistTokenInput.value;
  192.  
  193. if (!gistId || !token) {
  194. await showMessage('❌ Gist ID and Token are required!', 'NG', 2500);
  195. return;
  196. }
  197.  
  198. await GM.setValue('GIST_ID', gistId);
  199. await GM.setValue('GITHUB_TOKEN', token);
  200. registerDialog.close();
  201. registerDialog.remove();
  202.  
  203. setTimeout(() => { location.reload() }, 2500); // Restart Script
  204.  
  205. await showMessage('✅ Gist ID and Token saved!', 'OK', 2500);
  206.  
  207. });
  208.  
  209. const clearInfoButton = document.getElementById('clear-button');
  210. clearInfoButton.addEventListener('click', async () => {
  211. if (!confirm('Are you sure you want to clear Gist ID and Token?')) {
  212. return;
  213. }
  214. await GM.deleteValue('GITHUB_TOKEN');
  215. await GM.deleteValue('GIST_ID');
  216. registerDialog.close();
  217. registerDialog.remove();
  218.  
  219. setTimeout(() => { location.reload() }, 2500); // Restart Script
  220.  
  221. await showMessage('✅ Gist ID and Token cleared!', 'OK', 2500);
  222. });
  223. }
  224.  
  225. })();
  226.  
  227. async function showMessage(text, type = 'OK', duration = 4000) {
  228. const htmlId = `GistShare_Message-${type}`;
  229. const existingMessage = document.getElementById(htmlId);
  230. if (existingMessage) { return; } // 既に表示されている場合は何もしない
  231.  
  232. if (duration < 1000) { duration = 1000; } // 最低1秒は表示する
  233.  
  234. return new Promise((resolve) => {
  235. const message = document.createElement('div');
  236. message.id = `GistShare_Message-${type}`;
  237. message.textContent = text;
  238.  
  239. // 共通スタイル
  240. Object.assign(message.style, {
  241. position: 'fixed',
  242. top: '20px',
  243. right: '20px',
  244. padding: '12px 18px',
  245. borderRadius: '10px',
  246. color: '#fff',
  247. fontSize: '14px',
  248. boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
  249. zIndex: 9999,
  250. transform: 'translateY(20px)',
  251. opacity: '0',
  252. transition: 'opacity 0.4s ease, transform 0.4s ease'
  253. });
  254.  
  255. // タイプ別デザイン
  256. if (type === 'OK') {
  257. message.style.backgroundColor = '#4caf50'; // 緑
  258. message.style.borderLeft = '6px solid #2e7d32';
  259. } else if (type === 'NG') {
  260. message.style.backgroundColor = '#f44336'; // 赤
  261. message.style.borderLeft = '6px solid #b71c1c';
  262. }
  263.  
  264. document.body.appendChild(message);
  265.  
  266. // フェードイン(下から)
  267. setTimeout(() => {
  268. message.style.opacity = '.95';
  269. message.style.transform = 'translateY(0)';
  270. }, 10);
  271. // requestAnimationFrame(() => {
  272. // message.style.opacity = '1';
  273. // message.style.transform = 'translateY(0)';
  274. // });
  275.  
  276. // 指定時間後にフェードアウト
  277. setTimeout(() => {
  278. message.style.opacity = '0';
  279. message.style.transform = 'translateY(-20px)';
  280. setTimeout(() => {
  281. message.remove();
  282. resolve(); // メッセージが削除された後にresolveを呼び出す
  283. }, 400); // transition と一致
  284. }, duration - 400);
  285. });
  286. }
  287.  
  288. async function createRegisterDialog() {
  289. const existDialog = document.getElementById('tm-gist-dialog');
  290. if (existDialog) existDialog.remove();
  291.  
  292. const registerDialog = document.createElement('dialog');
  293. registerDialog.id = 'tm-gist-dialog';
  294. registerDialog.style.padding = '1em';
  295. registerDialog.style.zIndex = 9999;
  296. registerDialog.style.maxWidth = '400px';
  297. registerDialog.style.margin = 'auto';
  298. registerDialog.style.border = '2px solid #ccc';
  299. registerDialog.style.borderRadius = '8px';
  300. registerDialog.style.backgroundColor = '#fff';
  301. registerDialog.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)';
  302. registerDialog.style.position = 'fixed';
  303. registerDialog.style.padding = '1.5em';
  304. registerDialog.style.boxSizing = 'border-box';
  305.  
  306. const gistIdLabel = document.createElement('label');
  307. gistIdLabel.textContent = 'Gist ID:';
  308. gistIdLabel.style.display = 'block';
  309. gistIdLabel.style.marginBottom = '0.5em';
  310. gistIdLabel.for = 'gist-id-input';
  311. registerDialog.appendChild(gistIdLabel);
  312.  
  313. const gistIdInput = document.createElement('input');
  314. gistIdInput.id = 'gist-id-input';
  315. gistIdInput.type = 'text';
  316. gistIdInput.style.width = '100%';
  317. gistIdInput.style.boxSizing = 'border-box';
  318. gistIdInput.style.padding = '0.5em';
  319. gistIdInput.style.border = '1px solid #ccc';
  320. gistIdInput.style.borderRadius = '4px';
  321. gistIdInput.style.marginBottom = '1em';
  322. gistIdInput.value = await GM.getValue('GIST_ID', '');
  323. gistIdInput.placeholder = 'Your Gist ID';
  324. registerDialog.appendChild(gistIdInput);
  325.  
  326. const gistIdHelpText = document.createElement('small');
  327. gistIdHelpText.style.display = 'block';
  328. gistIdHelpText.style.marginBottom = '1.1em';
  329. gistIdHelpText.style.color = '#666';
  330. const gistIdHelpLabel = document.createElement('span');
  331. gistIdHelpLabel.textContent = 'Create or Select a Gist: ';
  332. gistIdHelpText.appendChild(gistIdHelpLabel);
  333. const gistIdHelpLink = document.createElement('a');
  334. gistIdHelpLink.href = 'https://gist.github.com/mine';
  335. gistIdHelpLink.target = '_blank';
  336. gistIdHelpLink.textContent = 'https://gist.github.com/';
  337. gistIdHelpText.appendChild(gistIdHelpLink);
  338. registerDialog.appendChild(gistIdHelpText);
  339.  
  340. const gistTokenLabel = document.createElement('label');
  341. gistTokenLabel.textContent = 'Gist Token:';
  342. gistTokenLabel.style.display = 'block';
  343. gistTokenLabel.style.marginBottom = '0.5em';
  344. gistTokenLabel.for = 'gist-token-input';
  345. registerDialog.appendChild(gistTokenLabel);
  346.  
  347. const gistTokenInput = document.createElement('input');
  348. gistTokenInput.id = 'gist-token-input';
  349. gistTokenInput.type = 'password';
  350. gistTokenInput.style.width = '100%';
  351. gistTokenInput.style.boxSizing = 'border-box';
  352. gistTokenInput.style.padding = '0.5em';
  353. gistTokenInput.style.border = '1px solid #ccc';
  354. gistTokenInput.style.borderRadius = '4px';
  355. gistTokenInput.style.marginBottom = '1em';
  356. gistTokenInput.value = await GM.getValue('GITHUB_TOKEN', '');
  357. gistTokenInput.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  358. registerDialog.appendChild(gistTokenInput);
  359.  
  360. const gistTokenHelpText = document.createElement('small');
  361. gistTokenHelpText.style.display = 'block';
  362. gistTokenHelpText.style.marginBottom = '1em';
  363. gistTokenHelpText.style.color = '#666';
  364. const gistTokenHelpLabel = document.createElement('span');
  365. gistTokenHelpLabel.textContent = 'Create a Token: ';
  366. gistTokenHelpText.appendChild(gistTokenHelpLabel);
  367. const gistTokenHelpLink = document.createElement('a');
  368. gistTokenHelpLink.href = 'https://github.com/settings/tokens';
  369. gistTokenHelpLink.target = '_blank';
  370. gistTokenHelpLink.textContent = 'https://github.com/settings/tokens';
  371. gistTokenHelpText.appendChild(gistTokenHelpLink);
  372. registerDialog.appendChild(gistTokenHelpText);
  373.  
  374. const saveButton = document.createElement('button');
  375. saveButton.textContent = 'Save Info';
  376. saveButton.style.backgroundColor = '#4caf50';
  377. saveButton.style.color = '#fff';
  378. saveButton.style.border = 'none';
  379. saveButton.style.padding = '0.5em 1em';
  380. saveButton.style.borderRadius = '4px';
  381. saveButton.style.cursor = 'pointer';
  382. saveButton.style.marginTop = '1em';
  383. saveButton.style.float = 'right';
  384. saveButton.id = 'save-button';
  385. registerDialog.appendChild(saveButton);
  386.  
  387. const clearInfoButton = document.createElement('button');
  388. clearInfoButton.textContent = 'Clear Info';
  389. clearInfoButton.style.backgroundColor = '#f44336';
  390. clearInfoButton.style.color = '#fff';
  391. clearInfoButton.style.border = 'none';
  392. clearInfoButton.style.padding = '0.5em 1em';
  393. clearInfoButton.style.borderRadius = '4px';
  394. clearInfoButton.style.cursor = 'pointer';
  395. clearInfoButton.style.marginTop = '1em';
  396. clearInfoButton.style.marginRight = '0.5em';
  397. clearInfoButton.style.float = 'right';
  398. clearInfoButton.id = 'clear-button';
  399. registerDialog.appendChild(clearInfoButton);
  400.  
  401. const closeButton = document.createElement('button');
  402. closeButton.textContent = 'X';
  403. closeButton.style.position = 'absolute';
  404. closeButton.style.top = '7px';
  405. closeButton.style.right = '7px';
  406. closeButton.style.backgroundColor = '#ccc';
  407. closeButton.style.border = 'none';
  408. closeButton.style.borderRadius = '15%';
  409. closeButton.style.color = '#fff';
  410. closeButton.style.cursor = 'pointer';
  411. closeButton.style.padding = '0.2em 0.5em';
  412. closeButton.style.fontSize = '14px';
  413. closeButton.addEventListener('click', () => {
  414. registerDialog.close();
  415. registerDialog.remove();
  416. });
  417. registerDialog.appendChild(closeButton);
  418.  
  419. const css = document.createElement('style');
  420. css.id = 'tm-gist-css';
  421. css.textContent = `
  422. #tm-gist-dialog button:hover {
  423. opacity: 0.8;
  424. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  425. }`;
  426. registerDialog.appendChild(css);
  427.  
  428. document.body.appendChild(registerDialog);
  429.  
  430. return registerDialog;
  431. }
  432.  
  433. function inIframe() {
  434. try {
  435. return window.self !== window.top;
  436. } catch (e) {
  437. return true;
  438. }
  439. }

QingJ © 2025

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