Gist 共享剪贴板

共享选定文本到Gist并粘贴到剪贴板

当前为 2025-05-22 提交的版本,查看 最新版本

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

QingJ © 2025

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