Gist Shared Clipboard

Share selected text to Gist and paste it to clipboard

נכון ליום 22-05-2025. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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;
  }
}