// ==UserScript==
// @name 0501快捷跳转·圆形按钮版 + 批量导入(跨站统一存储)
// @namespace http://tampermonkey.net/
// @version 14.0
// @description 圆形站点按钮,支持收藏当前页、批量导入、导出全部,跨域名统一存储(GM存储)
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @grant GM_listValues
// @run-at document-idle
// @downloadURL https://update.greasyfork.icu/scripts/576177/0501%E5%BF%AB%E6%8D%B7%E8%B7%B3%E8%BD%AC%C2%B7%E5%9C%86%E5%BD%A2%E6%8C%89%E9%92%AE%E7%89%88%20%2B%20%E6%89%B9%E9%87%8F%E5%AF%BC%E5%85%A5%EF%BC%88%E8%B7%A8%E7%AB%99%E7%BB%9F%E4%B8%80%E5%AD%98%E5%82%A8%EF%BC%89.user.js
// @updateURL https://update.greasyfork.icu/scripts/576177/0501%E5%BF%AB%E6%8D%B7%E8%B7%B3%E8%BD%AC%C2%B7%E5%9C%86%E5%BD%A2%E6%8C%89%E9%92%AE%E7%89%88%20%2B%20%E6%89%B9%E9%87%8F%E5%AF%BC%E5%85%A5%EF%BC%88%E8%B7%A8%E7%AB%99%E7%BB%9F%E4%B8%80%E5%AD%98%E5%82%A8%EF%BC%89.meta.js
// ==/UserScript==
(function() {
'use strict';
// 存储键名
const SITE_KEY = "quick_nav_all_sites_circle_global";
const POS_KEY = "quick_nav_panel_position_circle";
const FOLD_KEY = "quick_nav_folded_circle";
// 默认示例网站
const DEFAULT_SITES = [
{ name: "B站", url: "https://www.bilibili.com/" },
{ name: "X", url: "https://x.com/" },
{ name: "油管", url: "https://www.youtube.com/" },
{ name: "P站", url: "https://www.pinterest.com/" },
{ name: "TikTok", url: "https://www.tiktok.com/" },
{ name: "Kimi", url: "https://kimi.moonshot.cn/" },
{ name: "豆包", url: "https://www.doubao.com/" },
{ name: "百度", url: "https://www.baidu.com/" },
{ name: "知乎", url: "https://www.zhihu.com/" }
];
// ---------- 全局存储读写(跨站统一)----------
function getSiteList() {
let stored = GM_getValue(SITE_KEY, null);
if (stored === null) {
// 首次运行:尝试从 localStorage 迁移数据
try {
const oldData = localStorage.getItem("quick_nav_all_sites_circle");
if (oldData) {
const parsed = JSON.parse(oldData);
if (Array.isArray(parsed)) {
stored = parsed;
GM_setValue(SITE_KEY, stored);
// 可选:清除旧 localStorage 避免混乱(但保留也无害)
localStorage.removeItem("quick_nav_all_sites_circle");
}
}
} catch(e) {}
// 如果仍然为空,使用默认列表
if (!stored) stored = [...DEFAULT_SITES];
}
return Array.isArray(stored) ? stored : [...DEFAULT_SITES];
}
function saveSiteList(arr) {
GM_setValue(SITE_KEY, arr);
}
// 位置读写(使用 localStorage 也可以,因为位置不要求跨站统一,但为了方便依然用 GM 存储)
function getPos() {
const pos = GM_getValue(POS_KEY, null);
if (pos) return pos;
return { left: 20, top: 320 };
}
function savePos(left, top) {
GM_setValue(POS_KEY, { left, top });
}
// 折叠状态
function getFolded() {
return GM_getValue(FOLD_KEY, false);
}
function setFolded(val) {
GM_setValue(FOLD_KEY, val);
}
// 防重复创建面板
if (document.getElementById('quick-nav-circle')) return;
// 创建面板(样式和之前几乎一致,只调整了按钮字体大小等细节)
const root = document.createElement('div');
root.id = 'quick-nav-circle';
const initPos = getPos();
root.style.cssText = `
all: initial;
position: fixed;
z-index: 9999998;
left: ${initPos.left}px;
top: ${initPos.top}px;
width: 280px;
font-family: system-ui, sans-serif;
user-select: none;
`;
document.body.appendChild(root);
const bar = document.createElement('div');
bar.style.cssText = `
background: #ff4081;
color: #fff;
padding: 8px 12px;
border-radius: 10px 10px 0 0;
font-size: 13px;
font-weight: 500;
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
`;
bar.innerHTML = `⚡ 快捷跳转−`;
root.appendChild(bar);
const panel = document.createElement('div');
panel.style.cssText = `
background: rgba(255,255,255,0.95);
backdrop-filter: blur(8px);
border-radius: 0 0 12px 12px;
padding: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-height: 400px;
overflow-y: auto;
`;
root.appendChild(panel);
const circleGrid = document.createElement('div');
circleGrid.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
`;
panel.appendChild(circleGrid);
const toolArea = document.createElement('div');
toolArea.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
`;
panel.appendChild(toolArea);
// 渲染圆形按钮
function renderCircles() {
circleGrid.innerHTML = '';
const sites = getSiteList();
if (sites.length === 0) {
const hint = document.createElement('div');
hint.textContent = '暂无站点,点击下方“收藏本页”或“批量导入”';
hint.style.cssText = 'width:100%; text-align:center; color:#999; font-size:12px; padding:10px;';
circleGrid.appendChild(hint);
} else {
sites.forEach((site, idx) => {
const btnWrap = document.createElement('div');
btnWrap.style.position = 'relative';
const btn = document.createElement('button');
let displayName = site.name;
if (displayName.length > 4) displayName = displayName.slice(0,3)+'..';
btn.textContent = displayName;
btn.title = `${site.name}\n${site.url}`;
btn.style.cssText = `
width: 60px;
height: 60px;
border-radius: 50%;
background: #f0f2f5;
border: none;
color: #1e2a3a;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
line-height: 1.2;
padding: 0 4px;
`;
btn.onmouseenter = () => btn.style.background = '#ffe0e7';
btn.onmouseleave = () => btn.style.background = '#f0f2f5';
btn.onclick = () => window.open(site.url, '_blank');
const delBtn = document.createElement('span');
delBtn.textContent = '×';
delBtn.style.cssText = `
position: absolute;
top: -6px;
right: -6px;
background: #ff4d4f;
color: white;
border-radius: 50%;
width: 18px;
height: 18px;
font-size: 12px;
line-height: 18px;
text-align: center;
cursor: pointer;
display: none;
font-weight: bold;
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
`;
btnWrap.onmouseenter = () => delBtn.style.display = 'block';
btnWrap.onmouseleave = () => delBtn.style.display = 'none';
delBtn.onclick = (e) => {
e.stopPropagation();
const current = getSiteList();
current.splice(idx, 1);
saveSiteList(current);
renderCircles();
tip('已删除');
};
btnWrap.appendChild(btn);
btnWrap.appendChild(delBtn);
circleGrid.appendChild(btnWrap);
});
}
}
function tip(msg) {
const t = document.createElement('div');
t.textContent = msg;
t.style.cssText = `
position:fixed; bottom:80px; left:50%; transform:translateX(-50%);
background:rgba(0,0,0,0.7); color:#fff; padding:6px 12px;
border-radius:20px; font-size:12px; z-index:999999; pointer-events:none;
`;
document.body.appendChild(t);
setTimeout(() => t.remove(), 1500);
}
function buildTools() {
// 收藏当前页面(跨站统一)
const collectBtn = document.createElement('button');
collectBtn.textContent = '⭐ 收藏本页';
collectBtn.style.cssText = `flex:1; padding:6px; background:#22c55e; border:none; border-radius:20px; color:#fff; cursor:pointer; font-size:12px;`;
collectBtn.onclick = () => {
let name = document.title.trim();
if (name.length > 20) name = name.slice(0,17)+'…';
if (!name) name = location.hostname;
const url = location.href;
const sites = getSiteList();
// 可选去重(基于 URL 完全匹配)
const exists = sites.some(s => s.url === url);
if (exists) {
tip('当前页面已在收藏列表中');
return;
}
sites.push({ name, url });
saveSiteList(sites);
renderCircles();
tip(`已收藏:${name}`);
};
// 批量导入(同上,但存储到 GM)
const importBtn = document.createElement('button');
importBtn.textContent = '📥 批量导入';
importBtn.style.cssText = `flex:1; padding:6px; background:#8c6cf7; border:none; border-radius:20px; color:#fff; cursor:pointer; font-size:12px;`;
importBtn.onclick = () => {
const input = prompt(
"请粘贴网站列表(支持两种格式):\n\n" +
"1. JSON 数组: [{\"name\":\"站点名\",\"url\":\"https://...\"}, ...]\n" +
"2. 纯文本,每行格式:站点名|https://...\n\n" +
"示例:\n豆瓣|https://www.douban.com/\nGitHub|https://github.com/"
);
if (!input) return;
let newSites = [];
try {
const parsed = JSON.parse(input);
if (Array.isArray(parsed) && parsed.every(p => p.name && p.url)) {
newSites = parsed;
} else throw new Error();
} catch(e) {
const lines = input.split(/\r?\n/);
for (let line of lines) {
line = line.trim();
if (!line) continue;
const sep = line.includes('|') ? '|' : (line.includes(',') ? ',' : null);
if (sep) {
let [name, url] = line.split(sep);
if (name && url && url.startsWith('http')) {
newSites.push({ name: name.trim(), url: url.trim() });
} else {
tip(`跳过无效行: ${line.substring(0,30)}`);
}
} else {
tip(`格式错误: ${line}`);
}
}
}
if (newSites.length === 0) {
tip('没有有效站点被导入');
return;
}
const current = getSiteList();
const merged = [...current, ...newSites];
const unique = [];
const urlSet = new Set();
for (let s of merged) {
if (!urlSet.has(s.url)) {
urlSet.add(s.url);
unique.push(s);
}
}
saveSiteList(unique);
renderCircles();
tip(`成功导入 ${newSites.length} 个站点,去重后共 ${unique.length} 个`);
};
// 导出全部
const exportBtn = document.createElement('button');
exportBtn.textContent = '📋 导出全部';
exportBtn.style.cssText = `flex:1; padding:6px; background:#1677ff; border:none; border-radius:20px; color:#fff; cursor:pointer; font-size:12px;`;
exportBtn.onclick = () => {
const sites = getSiteList();
const jsonStr = JSON.stringify(sites, null, 2);
navigator.clipboard.writeText(jsonStr).then(() => {
tip(`已复制 ${sites.length} 个站点的配置到剪贴板`);
}).catch(() => tip('复制失败'));
};
// 清空全部
const clearBtn = document.createElement('button');
clearBtn.textContent = '🗑 清空';
clearBtn.style.cssText = `flex:1; padding:6px; background:#f97316; border:none; border-radius:20px; color:#fff; cursor:pointer; font-size:12px;`;
clearBtn.onclick = () => {
if (confirm('确定清空所有站点吗?默认示例也会被清除。')) {
saveSiteList([]);
renderCircles();
tip('已清空');
}
};
toolArea.innerHTML = '';
toolArea.appendChild(collectBtn);
toolArea.appendChild(importBtn);
toolArea.appendChild(exportBtn);
toolArea.appendChild(clearBtn);
}
// 折叠逻辑
const foldSpan = bar.querySelector('#circle-fold');
if (getFolded()) {
panel.style.display = 'none';
foldSpan.textContent = '+';
} else {
panel.style.display = 'block';
foldSpan.textContent = '−';
}
foldSpan.onclick = (e) => {
e.stopPropagation();
if (panel.style.display === 'none') {
panel.style.display = 'block';
foldSpan.textContent = '−';
setFolded(false);
} else {
panel.style.display = 'none';
foldSpan.textContent = '+';
setFolded(true);
}
};
// 拖拽移动
let isDragging = false, startX, startY, startLeft, startTop;
bar.addEventListener('mousedown', (e) => {
if (e.target === foldSpan) return;
e.preventDefault();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startLeft = root.offsetLeft;
startTop = root.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
root.style.left = (startLeft + dx) + 'px';
root.style.top = (startTop + dy) + 'px';
}
function onMouseUp(e) {
if (!isDragging) return;
isDragging = false;
savePos(root.offsetLeft, root.offsetTop);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
// 初始化
renderCircles();
buildTools();
})();