Gist Shared Clipboard

Share selected text to Gist and paste it to clipboard

// ==UserScript==
// @name                Gist Shared Clipboard
// @name:ja             Gist 共有クリップボード
// @name:zh-CN          Gist 共享剪贴板
// @name:zh-TW          Gist 共享剪貼簿
// @license             MIT
// @namespace           http://tampermonkey.net/
// @version             2025-05-22
// @description         Share selected text to Gist and paste it to clipboard
// @description:ja      Gistに選択したテキストを共有し、クリップボードに貼り付ける
// @description:zh-CN   共享选定文本到Gist并粘贴到剪贴板
// @description:zh-TW   共享選定文本到Gist並粘貼到剪貼簿
// @author              Julia Lee
// @match               *://*/*
// @icon                https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @grant               GM_registerMenuCommand
// @grant               GM_setValue
// @grant               GM_getValue
// @grant               GM_deleteValue
// @grant               GM_setClipboard
// ==/UserScript==

(async function () {
  'use strict';

  const GITHUB_TOKEN = await GM.getValue('GITHUB_TOKEN', ''); // GitHubのPersonal Access Tokenを指定
  const GIST_ID = await GM.getValue('GIST_ID', ''); // GistのIDを指定
  const FILENAME = 'GM-Shared-Clipboard.txt'; // Gist内のファイル名

  await GM.deleteValue('GIST_DOWNLOADING');
  await GM.deleteValue('GIST_UPLOADING');

  let crtRightTgtContent = null;
  let crtRightTgtUpdated = 0;

  if (GITHUB_TOKEN && GIST_ID) {
    const menu1 = GM_registerMenuCommand("Gist Share", gistUpload, {
      accessKey: 'c',
      autoClose: true,
      title: 'Share selected text to Gist',
    });

    const menu2 = GM_registerMenuCommand("Gist Paste", gistDowload, {
      accessKey: 'v',
      autoClose: true,
      title: 'Paste Gist content to clipboard',
    });
  }

  const menu3 = GM_registerMenuCommand("Gist Setup", setup, {
    accessKey: 's',
    autoClose: true,
    title: 'Setup Gist ID and Token',
  });

  if (GITHUB_TOKEN && GIST_ID) {
    const menu4 = GM_registerMenuCommand("Gist Clear", async () =>{
      await GM.deleteValue('GITHUB_TOKEN');
      await GM.deleteValue('GIST_ID');
      setTimeout(() => { location.reload() }, 2500); // Restart Script
      showMessage('✅ Gist ID and Token cleared!', 'OK', 2500);
    }, {
      // accessKey: 'x',
      autoClose: true,
      title: 'Clear Gist ID and Token',
    });
  }

  document.body.addEventListener("mousedown", event => {
    if (event.button == 0) { // left click for mouse
      // crtRightTgtContent = null;
    } else if (event.button == 1) { // wheel click for mouse
      // crtRightTgtContent = null;
    } else if (event.button == 2) { // right click for mouse
      const elm = event.target;
      const nodName = elm.nodeName.toLowerCase();

      switch (nodName) {
        case 'img':
          crtRightTgtContent = elm.src;
          break;
        case 'a':
          crtRightTgtContent = elm.href;
          break;
        default:
          crtRightTgtContent = null;
          break;
      }

      if (crtRightTgtContent) {
        crtRightTgtUpdated = new Date();
      }
    }
  });

  const gistUrl = `https://api.github.com/gists/${GIST_ID}`;
  const headers = {
    'Authorization': `Bearer ${GITHUB_TOKEN}`,
    'Content-Type': 'application/json',
  };

  async function gistUpload(_event) {
    // If the target is too old, reset it
    if (crtRightTgtContent && (new Date()) - crtRightTgtUpdated > 30*1000) {
      crtRightTgtContent = null;
      // crtRightTgtUpdated = 0;
    }

    const selectedText = document.getSelection().toString();
    if (!crtRightTgtContent && !selectedText) { return }

    const locked = await GM.getValue('GIST_UPLOADING');
    if (locked) {
      console.log("Gist is already uploading.");
      return;
    }

    const data = {
      files: {
        [FILENAME]: { content: selectedText || crtRightTgtContent }
      }
    };

    try {
      await GM.setValue('GIST_UPLOADING', true);
      const res = await fetch(gistUrl, {
        method: 'POST', headers,
        body: JSON.stringify(data)
      });

      if (!res.ok) {
        const error = await res.json();
        throw new Error(`Failed to update Gist: ${error.message}`);
      }

      const result = await res.json();

      await GM.setClipboard(result.html_url, "text")
      await showMessage('✅ Target Shared!', 'OK', 2500);
    } catch (error) {
      console.error("Error: ", error);
      await showMessage(`❌ ${error.message}`, 'NG', 2500);
    }
    finally {
      await GM.deleteValue('GIST_UPLOADING');
    }
  }

  async function gistDowload(_event) {
    if (inIframe()) {
      console.log("Gist Paste is not available in iframe.");
      return;
    }

    const locked = await GM.getValue('GIST_DOWNLOADING');
    if (locked) {
      console.log("Gist is already Downloading.");
      return;
    }

    try {
      await GM.setValue('GIST_DOWNLOADING', true);
      const res = await fetch(gistUrl, { headers });
      if (!res.ok) {
        const error = await res.json();
        throw new Error(`Failed to fetch Gist: ${error.message}`);
      }

      const result = await res.json();
      const content = result.files[FILENAME].content;

      if (!content) {
        throw new Error('No content found in the Gist.');
      }

      await GM.setClipboard(content, "text");
      console.log("Gist Content: ", content);
      await showMessage('✅ Clipboard Updated!', 'OK', 2500);

    } catch (error) {
      console.error("Error: ", error);
      await showMessage(`❌ ${error.message}`, 'NG', 2500);
    } finally {
      await GM.deleteValue('GIST_DOWNLOADING');
    }
  }

  async function setup() {
    if (inIframe()) {
      console.log("Gist Setup is not available in iframe.");
      return;
    }

    const dialog = await createRegisterDialog();
    dialog.showModal();
    const button = document.getElementById('save-button');
    const input = document.getElementById('gist-id-input');
    const input2 = document.getElementById('gist-token-input');
    button.addEventListener('click', async () => {
      const gistId = input.value;
      const token = input2.value;

      if (!gistId || !token) {
        await showMessage('❌ Gist ID and Token are required!', 'NG', 2500);
        return;
      }

      await GM.setValue('GIST_ID', gistId);
      await GM.setValue('GITHUB_TOKEN', token);
      dialog.close();
      dialog.remove();

      setTimeout(() => { location.reload() }, 2500); // Restart Script

      await showMessage('✅ Gist ID and Token saved!', 'OK', 2500);

    });
  }

})();

async function showMessage(text, type = 'OK', duration = 4000) {
  const htmlId = `GistShare_Message-${type}`;
  const existingMessage = document.getElementById(htmlId);
  if (existingMessage) { return; } // 既に表示されている場合は何もしない

  if (duration < 1000) { duration = 1000; } // 最低1秒は表示する

  return new Promise((resolve) => {
    const message = document.createElement('div');
    message.id = `GistShare_Message-${type}`;
    message.textContent = text;

    // 共通スタイル
    Object.assign(message.style, {
      position: 'fixed',
      top: '20px',
      right: '20px',
      padding: '12px 18px',
      borderRadius: '10px',
      color: '#fff',
      fontSize: '14px',
      boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
      zIndex: 9999,
      transform: 'translateY(20px)',
      opacity: '0',
      transition: 'opacity 0.4s ease, transform 0.4s ease'
    });

    // タイプ別デザイン
    if (type === 'OK') {
      message.style.backgroundColor = '#4caf50'; // 緑
      message.style.borderLeft = '6px solid #2e7d32';
    } else if (type === 'NG') {
      message.style.backgroundColor = '#f44336'; // 赤
      message.style.borderLeft = '6px solid #b71c1c';
    }

    document.body.appendChild(message);

    // フェードイン(下から)
    setTimeout(() => {
      message.style.opacity = '.95';
      message.style.transform = 'translateY(0)';
    }, 10);
    // requestAnimationFrame(() => {
    //   message.style.opacity = '1';
    //   message.style.transform = 'translateY(0)';
    // });

    // 指定時間後にフェードアウト
    setTimeout(() => {
      message.style.opacity = '0';
      message.style.transform = 'translateY(-20px)';
      setTimeout(() => {
        message.remove();
        resolve(); // メッセージが削除された後にresolveを呼び出す
      }, 400); // transition と一致
    }, duration - 400);
  });
}

async function createRegisterDialog() {
  const existing = document.getElementById('tm-gist-dialog');
  if (existing) existing.remove();

  const dialog = document.createElement('dialog');
  dialog.id = 'tm-gist-dialog';
  dialog.style.padding = '1em';
  dialog.style.zIndex = 9999;

  const label = document.createElement('label');
  label.textContent = 'Gist ID:';
  label.style.display = 'block';
  label.style.marginBottom = '0.5em';
  label.for = 'gist-id-input';
  dialog.appendChild(label);
  const input = document.createElement('input');
  input.id = 'gist-id-input';
  input.type = 'text';
  input.style.width = '100%';
  input.style.boxSizing = 'border-box';
  input.style.padding = '0.5em';
  input.style.border = '1px solid #ccc';
  input.style.borderRadius = '4px';
  input.style.marginBottom = '1em';
  input.value = await GM.getValue('GIST_ID', '');
  input.placeholder = 'Your Gist ID';
  dialog.appendChild(input);
  const small = document.createElement('small');
  small.style.display = 'block';
  small.style.marginBottom = '1.1em';
  small.style.color = '#666';
  const span = document.createElement('span');
  span.textContent = 'Create or Select a Gist: ';
  small.appendChild(span);
  const a = document.createElement('a');
  a.href = 'https://gist.github.com/mine';
  a.target = '_blank';
  a.textContent = 'https://gist.github.com/';
  small.appendChild(a);
  dialog.appendChild(small);

  const label2 = document.createElement('label');
  label2.textContent = 'Gist Token:';
  label2.style.display = 'block';
  label2.style.marginBottom = '0.5em';
  label2.for = 'gist-token-input';
  dialog.appendChild(label2);
  const input2 = document.createElement('input');
  input2.id = 'gist-token-input';
  input2.type = 'password';
  input2.style.width = '100%';
  input2.style.boxSizing = 'border-box';
  input2.style.padding = '0.5em';
  input2.style.border = '1px solid #ccc';
  input2.style.borderRadius = '4px';
  input2.style.marginBottom = '1em';
  input2.value = await GM.getValue('GITHUB_TOKEN', '');
  input2.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  dialog.appendChild(input2);
  const small2 = document.createElement('small');
  small2.style.display = 'block';
  small2.style.marginBottom = '1em';
  small2.style.color = '#666';
  const span2 = document.createElement('span');
  span2.textContent = 'Create a Token: ';
  small2.appendChild(span2);
  const a2 = document.createElement('a');
  a2.href = 'https://github.com/settings/tokens';
  a2.target = '_blank';
  a2.textContent = 'https://github.com/settings/tokens';
  small2.appendChild(a2);
  dialog.appendChild(small2);

  const button = document.createElement('button');
  button.textContent = 'Save Info';
  button.style.backgroundColor = '#4caf50';
  button.style.color = '#fff';
  button.style.border = 'none';
  button.style.padding = '0.5em 1em';
  button.style.borderRadius = '4px';
  button.style.cursor = 'pointer';
  button.style.marginTop = '1em';
  button.style.float = 'right';
  button.id = 'save-button';
  dialog.appendChild(button);

  document.body.appendChild(dialog);

  return dialog;
}

function inIframe() {
  try {
    return window.self !== window.top;
  } catch (e) {
    return true;
  }
}
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元