Quickly jump to the specified link from the current site.
// ==UserScript==
// @name Custom Link Jump
// @name:zh-CN 跳转打开自定义链接
// @name:zh-TW 跳轉打開自定義連結
// @namespace crayonssr-tool
// @description Quickly jump to the specified link from the current site.
// @description:zh-CN 从当前站点,快速跳转到指定链接
// @description:zh-TW 從當前站點,快速跳轉到指定連結
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @run-at document-idle
// @noframes
// @author crayonssr
// @version 1.0.1
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAMAAADXqc3KAAAAq1BMVEX68Yr////884v6+vr+9ov19fb7+/3n5ubIx8X19fjk5OPc1Xbw54P/+Y7v533g2HKoppLQyoX38IO0rVihnHO5t6LJx7efnIfW1t7HwojSymeGg3SHhIPZ13a8tV6oqbqsp3G5tY3GwHa+vLng4ei1sHG3tZfExMqlpKjOyHPh2oDGwY62tKWUko9uaD3Av7WrqabV1NF6eHmEf2appF/LyaqVkWvDu1bX18rqIWkoAAABEUlEQVQokY1S2VaDMBTkZiEhEEpbuoDYVsBqVIRo1f7/l8kWFvXBOXmaOffOTBLL+ifQX1wD1/tJS+kvFr4bLOczcrUON9ttsNtHM0HGN4Latu2w5JZM7MjhCAY7vx9B3ukuTXpWZJDfS4Q64Zw/sJbGmCcUHn3ldcrT8wsGngleMMww2K9l2vmgVUihKmihq2ZOaxb1AeTZASpAv+l2IYiDHIXawOSi714fTJ4o0Iobgelh1YXiTHNmFOej7yIPWGvIxFDy6nYC8jmt8w4moJdm1ydgQWEU+iIW+cpHFtMkNPdlyaTJDzmwuur1Ioerlynfl2UZ8mDD1/H0RYiKlFK+ct0YEWsKUgOh9vz6DHN8Aw8/EmSFjAtmAAAAAElFTkSuQmCC
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 只在名单内的网站开启
const enableDomains = [
...getDefault(),
// 'example.com',
];
if (enableDomains.every(ed => !location.hostname.includes(ed))) return;
// --- 1. 多语言配置 ---
const translations = {
'zh-CN': {
title: '链接跳转',
placeholder: '粘贴或输入链接...',
paste: '粘贴',
jump: '跳转',
jumping: '正在跳转...',
invalid: '链接格式无效',
empty: '剪贴板为空',
clipError: '读取剪贴板失败'
},
'zh-TW': {
title: '連結跳轉',
placeholder: '粘貼或輸入連結...',
paste: '粘貼',
jump: '跳轉',
jumping: '正在跳轉...',
invalid: '連結格式無效',
empty: '剪貼簿為空',
clipError: '讀取剪貼簿失敗'
},
'en': {
title: 'Link Jump',
placeholder: 'Paste or enter link...',
paste: 'Paste',
jump: 'Jump',
jumping: 'Jumping...',
invalid: 'Invalid link format',
empty: 'Clipboard is empty',
clipError: 'Failed to read clipboard'
}
};
// 获取默认启用的网站域名。至于默认配置为什么搞成这样,复制出来打印一下内容就知道了
function getDefault() {
const bytes = Uint8Array.from(atob('MEcfCgsbBB1XCAZbR0cJChELDgoXRQYWBkdVSQMYBREQCksTG0dVSQcWBBYNEksNBEdVSQIMBhcWCgFXCAoUSUlbBAsVEgMYBRZXCAoUSUlbDQQXGAkARQYWBkdVSQYYBQEfCgsKRQ8JSTg='), c => c.charCodeAt(0));
const keyBytes = new TextEncoder().encode('key')
const result = bytes.map((byte, i) => byte ^ keyBytes[i % keyBytes.length]);
return JSON.parse(new TextDecoder().decode(result));
}
// 获取并匹配语言
function getI18n() {
const sysLang = navigator.language.toLowerCase();
if (sysLang.startsWith('zh')) {
// 区分繁体和简体
return (sysLang.includes('tw') || sysLang.includes('hk') || sysLang.includes('mo'))
? translations['zh-TW']
: translations['zh-CN'];
}
return translations['en'];
}
const i18n = getI18n();
// --- 2. 构造 UI ---
const container = document.createElement('div');
container.id = 'tampermonkey-jump-tool';
container.className = 'tm-jump-container';
container.innerHTML = `
<div class="tm-jump-blur-bg"></div>
<div class="tm-jump-content">
<div class="tm-jump-header" id="tm-jump-drag-handle">
<div class="tm-jump-title">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
<span>${i18n.title}</span>
</div>
<div class="tm-jump-referer">
<span>${location.hostname}</span>
</div>
</div>
<div class="tm-jump-body">
<input type="text" id="tm-jump-url" class="tm-jump-input" placeholder="${i18n.placeholder}" value="" autocomplete="off">
<button id="tm-jump-button" class="tm-jump-button">
<span class="btn-text">${i18n.paste}</span>
<span class="btn-icon">↵</span>
</button>
</div>
<div class="tm-jump-status" id="tm-jump-status"></div>
</div>
`;
GM_addStyle(`
:root {
--tm-primary: #6366f1;
--tm-primary-hover: #4f46e5;
--tm-paste: #10b981;
--tm-paste-hover: #059669;
--tm-danger: #ef4444;
--tm-bg-light: rgba(255, 255, 255, 0.75);
--tm-bg-dark: rgba(30, 41, 59, 0.8);
--tm-border-light: rgba(255, 255, 255, 0.6);
--tm-border-dark: rgba(255, 255, 255, 0.1);
--tm-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
}
.tm-jump-container {
position: fixed; z-index: 9999999; box-sizing: border-box; width: 330px;
border-radius: 16px; font-family: -apple-system, system-ui, sans-serif;
overflow: hidden; box-shadow: var(--tm-shadow); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); transition: transform 0.1s, box-shadow 0.3s;
inset: auto 30px 30px auto; background: var(--tm-bg-light) !important;
border: 1px solid rgba(0,0,0,0.05) !important; color: #0b0e12 !important;
}
@media (prefers-color-scheme: dark) {
.tm-jump-container {
background: var(--tm-bg-dark) !important; border: 1px solid var(--tm-border-dark) !important;
color:#f8fafc !important; box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5) !important;
}
.tm-jump-title svg { stroke: #cbd5e1 !important; }
.tm-jump-input { background: rgba(15, 23, 42, 0.6) !important; color: #f8fafc !important; border: 1px solid #475569 !important; }
.tm-jump-button { color: #f8fafc !important; border-color: #354358 !important; }
}
.tm-jump-header { padding: 12px 16px 4px; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; }
.tm-jump-title { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; opacity: 0.8; }
.tm-jump-referer { font-size: 13px; font-weight: 600; color: #32d458; padding-right: 5px; }
.tm-jump-body { padding: 10px 16px 16px; display: flex; gap: 8px; }
.tm-jump-input { flex: 1; padding: 8px 12px; border-radius: 10px; font-size: 13px; outline: none; transition: all 0.2s; width: 0; }
.tm-jump-input:focus { border-color: var(--tm-primary); box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2) !important; }
.tm-jump-button { border-radius: 10px; padding: 0 14px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 4px; background: transparent; border: 2px solid; }
.tm-jump-button:hover { transform: translateY(-1px); }
.tm-jump-status { height: 0; overflow: hidden; font-size: 11px; text-align: center; transition: height 0.3s ease; color: var(--tm-paste); font-weight: 500; }
.tm-jump-status.show { height: 24px; margin-bottom: 6px; }
.tm-jump-status.error { color: var(--tm-danger); }
.tm-jump-container.dragging { cursor: move; opacity: 0.95; }
`);
// --- 3. 拖拽逻辑 (核心修复点) ---
let position = GM_getValue('jumpToolPositionBR', { bottom: 30, right: 30 });
function applyPosition(pos) {
// 使用 clientWidth/Height 避开滚动条干扰
const maxBottom = document.documentElement.clientHeight - 80;
const maxRight = document.documentElement.clientWidth - 300;
let safeBottom = Math.max(10, Math.min(pos.bottom, maxBottom));
let safeRight = Math.max(10, Math.min(pos.right, maxRight));
container.style.bottom = `${safeBottom}px`;
container.style.right = `${safeRight}px`;
container.style.top = 'auto';
container.style.left = 'auto';
}
applyPosition(position);
document.body.appendChild(container);
const dragHandle = document.getElementById('tm-jump-drag-handle');
const urlInput = document.getElementById('tm-jump-url');
const jumpButton = document.getElementById('tm-jump-button');
const btnText = jumpButton.querySelector('.btn-text');
const statusElement = document.getElementById('tm-jump-status');
let isDragging = false;
let startX, startY, startRight, startBottom;
dragHandle.addEventListener('mousedown', (e) => {
isDragging = true;
container.classList.add('dragging');
startX = e.clientX;
startY = e.clientY;
const rect = container.getBoundingClientRect();
// 关键点:使用 clientWidth 而非 innerWidth,确保计算不包含滚动条
startRight = document.documentElement.clientWidth - rect.right;
startBottom = document.documentElement.clientHeight - rect.bottom;
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = startX - e.clientX;
const deltaY = startY - e.clientY;
let newRight = startRight + deltaX;
let newBottom = startBottom + deltaY;
// 边界限制
newRight = Math.max(10, Math.min(newRight, document.documentElement.clientWidth - 300));
newBottom = Math.max(10, Math.min(newBottom, document.documentElement.clientHeight - 100));
container.style.right = `${newRight}px`;
container.style.bottom = `${newBottom}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
container.classList.remove('dragging');
const rect = container.getBoundingClientRect();
const finalPos = {
bottom: document.documentElement.clientHeight - rect.bottom,
right: document.documentElement.clientWidth - rect.right
};
GM_setValue('jumpToolPositionBR', finalPos);
}
});
// --- 4. 交互逻辑 ---
function updateButtonState() {
btnText.textContent = urlInput.value.trim() ? i18n.jump : i18n.paste;
}
urlInput.addEventListener('input', updateButtonState);
function jumpToUrl(url) {
if (!url) return;
try {
if (!/^https?:\/\//i.test(url) && !/^\//.test(url)) {
throw new Error();
}
url = !/^https?:\/\//i.test(url) ? 'https://' + url : url;
new URL(url);
const link = document.createElement('a');
link.href = url;
document.body.appendChild(link);
link.click();
showStatus(i18n.jumping, 'success');
setTimeout(() => document.body.removeChild(link), 100);
} catch (error) {
showStatus(i18n.invalid, 'error');
}
}
let _tipTimer;
function showStatus(message, type = 'success') {
clearTimeout(_tipTimer);
statusElement.textContent = message;
statusElement.className = `tm-jump-status show ${type}`;
_tipTimer = setTimeout(() => statusElement.classList.remove('show'), 2000);
}
async function pasteAndJump() {
try {
const text = await navigator.clipboard.readText();
const trimmed = text.trim();
if (trimmed) {
urlInput.value = trimmed;
updateButtonState();
jumpToUrl(trimmed);
} else {
showStatus(i18n.empty, 'error');
}
} catch (error) {
showStatus(i18n.clipError, 'error');
}
}
jumpButton.addEventListener('click', async () => {
const url = urlInput.value.trim();
url ? jumpToUrl(url) : await pasteAndJump();
});
urlInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') jumpToUrl(urlInput.value.trim());
});
window.addEventListener('resize', () => {
applyPosition(GM_getValue('jumpToolPositionBR', { bottom: 30, right: 30 }));
});
setTimeout(() => urlInput.focus(), 500);
})();