Gist Shared Clipboard

Share selected text to Gist and paste it to clipboard

Versión del día 22/05/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name            Gist Shared Clipboard
// @name:ja         Gist 共有クリップボード
// @name:zh-CN      Gist 共享剪贴板
// @name:zh-TW      Gist 共享剪貼簿
// @description     Share selected text to Gist and paste it to clipboard
// @description:ja  Gistに選択したテキストを共有し、クリップボードに貼り付ける
// @description:zh-CN 共享选定文本到Gist并粘贴到剪贴板
// @description:zh-TW 共享選定文本到Gist並粘貼到剪貼簿
// @license         MIT
// @namespace       http://tampermonkey.net/
// @version         2025-05-20
// @description     Share selected text to Gist and paste it to clipboard
// @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;
  }
}