Gist 共享剪貼簿

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

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

您需要先安裝使用者腳本管理器擴展,如 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 共享剪貼簿
// @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;
  }
}