Gist 共享剪貼簿

共享選定文本到Gist並粘貼到剪貼簿

目前為 2025-05-22 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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