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-26
// @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内のファイル名
  const RIGHT_CLICK_MAX_AGE = 20 * 1000; // 右クリックしてからTargetの保持時間(ミリ秒)

  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: 'x',
    autoClose: true,
    title: 'Setup 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 > RIGHT_CLICK_MAX_AGE) {
      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 GM.setClipboard(result.files[FILENAME].content, "text");
      console.log("Gist URL: ", result.html_url);
      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 registerDialog = await createRegisterDialog();
    registerDialog.showModal();
    const saveButton = document.getElementById('save-button');
    const gistIdInput = document.getElementById('gist-id-input');
    const gistTokenInput = document.getElementById('gist-token-input');
    saveButton.addEventListener('click', async () => {
      const gistId = gistIdInput.value;
      const token = gistTokenInput.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);
      registerDialog.close();
      registerDialog.remove();

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

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

    });

    const clearInfoButton = document.getElementById('clear-button');
    clearInfoButton.addEventListener('click', async () => {
      if (!confirm('Are you sure you want to clear Gist ID and Token?')) {
        return;
      }
      await GM.deleteValue('GITHUB_TOKEN');
      await GM.deleteValue('GIST_ID');
      registerDialog.close();
      registerDialog.remove();

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

      await showMessage('✅ Gist ID and Token cleared!', '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 registerDialog = document.createElement('dialog');
  registerDialog.id = 'tm-gist-dialog';
  registerDialog.style.padding = '1em';
  registerDialog.style.zIndex = 9999;

  const gistIdLabel = document.createElement('label');
  gistIdLabel.textContent = 'Gist ID:';
  gistIdLabel.style.display = 'block';
  gistIdLabel.style.marginBottom = '0.5em';
  gistIdLabel.for = 'gist-id-input';
  registerDialog.appendChild(gistIdLabel);

  const gistIdInput = document.createElement('input');
  gistIdInput.id = 'gist-id-input';
  gistIdInput.type = 'text';
  gistIdInput.style.width = '100%';
  gistIdInput.style.boxSizing = 'border-box';
  gistIdInput.style.padding = '0.5em';
  gistIdInput.style.border = '1px solid #ccc';
  gistIdInput.style.borderRadius = '4px';
  gistIdInput.style.marginBottom = '1em';
  gistIdInput.value = await GM.getValue('GIST_ID', '');
  gistIdInput.placeholder = 'Your Gist ID';
  registerDialog.appendChild(gistIdInput);

  const gistIdHelpText = document.createElement('small');
  gistIdHelpText.style.display = 'block';
  gistIdHelpText.style.marginBottom = '1.1em';
  gistIdHelpText.style.color = '#666';
  const gistIdHelpLabel = document.createElement('span');
  gistIdHelpLabel.textContent = 'Create or Select a Gist: ';
  gistIdHelpText.appendChild(gistIdHelpLabel);
  const gistIdHelpLink = document.createElement('a');
  gistIdHelpLink.href = 'https://gist.github.com/mine';
  gistIdHelpLink.target = '_blank';
  gistIdHelpLink.textContent = 'https://gist.github.com/';
  gistIdHelpText.appendChild(gistIdHelpLink);
  registerDialog.appendChild(gistIdHelpText);

  const gistTokenLabel = document.createElement('label');
  gistTokenLabel.textContent = 'Gist Token:';
  gistTokenLabel.style.display = 'block';
  gistTokenLabel.style.marginBottom = '0.5em';
  gistTokenLabel.for = 'gist-token-input';
  registerDialog.appendChild(gistTokenLabel);

  const gistTokenInput = document.createElement('input');
  gistTokenInput.id = 'gist-token-input';
  gistTokenInput.type = 'password';
  gistTokenInput.style.width = '100%';
  gistTokenInput.style.boxSizing = 'border-box';
  gistTokenInput.style.padding = '0.5em';
  gistTokenInput.style.border = '1px solid #ccc';
  gistTokenInput.style.borderRadius = '4px';
  gistTokenInput.style.marginBottom = '1em';
  gistTokenInput.value = await GM.getValue('GITHUB_TOKEN', '');
  gistTokenInput.placeholder = 'ghp_XXXXXXXXXXXXXXXX';
  registerDialog.appendChild(gistTokenInput);

  const gistTokenHelpText = document.createElement('small');
  gistTokenHelpText.style.display = 'block';
  gistTokenHelpText.style.marginBottom = '1em';
  gistTokenHelpText.style.color = '#666';
  const gistTokenHelpLabel = document.createElement('span');
  gistTokenHelpLabel.textContent = 'Create a Token: ';
  gistTokenHelpText.appendChild(gistTokenHelpLabel);
  const gistTokenHelpLink = document.createElement('a');
  gistTokenHelpLink.href = 'https://github.com/settings/tokens';
  gistTokenHelpLink.target = '_blank';
  gistTokenHelpLink.textContent = 'https://github.com/settings/tokens';
  gistTokenHelpText.appendChild(gistTokenHelpLink);
  registerDialog.appendChild(gistTokenHelpText);

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

  const clearInfoButton = document.createElement('button');
  clearInfoButton.textContent = 'Clear Info';
  clearInfoButton.style.backgroundColor = '#f44336';
  clearInfoButton.style.color = '#fff';
  clearInfoButton.style.border = 'none';
  clearInfoButton.style.padding = '0.5em 1em';
  clearInfoButton.style.borderRadius = '4px';
  clearInfoButton.style.cursor = 'pointer';
  clearInfoButton.style.marginTop = '1em';
  clearInfoButton.style.marginRight = '0.5em';
  clearInfoButton.style.float = 'right';
  clearInfoButton.id = 'clear-button';
  registerDialog.appendChild(clearInfoButton);

  const closeButton = document.createElement('button');
  closeButton.textContent = 'X';
  closeButton.style.position = 'absolute';
  closeButton.style.top = '7px';
  closeButton.style.right = '7px';
  closeButton.style.backgroundColor = '#ccc';
  closeButton.style.border = 'none';
  closeButton.style.borderRadius = '15%';
  closeButton.style.color = '#fff';
  closeButton.style.cursor = 'pointer';
  closeButton.style.padding = '0.2em 0.5em';
  closeButton.style.fontSize = '14px';
  closeButton.addEventListener('click', () => {
    registerDialog.close();
    registerDialog.remove();
  });
  registerDialog.appendChild(closeButton);

  document.body.appendChild(registerDialog);

  return registerDialog;
}

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

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元