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