// ==UserScript==
// @name CNB Cool 增强
// @namespace https://cnb.cool/IIIStudio/Code/Greasemonkey/CNB
// @version 1.5
// @description CNB.Cool 综合增强工具:直达链接解码、网格布局切换、收藏夹标签管理、与我有关标签、创建仓库按钮、输入框复制、快捷导航栏(支持拖拽排序/右键编辑/导入导出)、云原生开发模式(点击头像启动)等功能
// @author IIIStudio
// @match *://cnb.cool/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @connect api.cnb.cool
// @connect *.myqcloud.com
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const SCRIPT_VERSION = typeof GM_info !== 'undefined' ? GM_info.script.version : '';
// ===== 样式 =====
const style = document.createElement('style');
style.textContent = `
.cnb-tag-chip {
display: inline-flex; align-items: center; gap: 3px;
padding: 1px 8px; border-radius: 10px; font-size: 11px;
background: rgba(22,119,255,0.1); color: #1677ff;
border: 1px solid rgba(22,119,255,0.2); cursor: default;
white-space: nowrap;
}
.cnb-tag-chip .remove {
cursor: pointer; margin-left: 2px; font-size: 14px;
line-height: 1; color: #999;
}
.cnb-tag-chip .remove:hover { color: #ff4d4f; }
.cnb-tag-add-btn {
cursor: pointer; display: none; align-items: center;
justify-content: center; width: 20px; height: 20px;
border-radius: 50%; background: transparent;
border: 1px dashed #bbb; color: #999; font-size: 16px;
line-height: 1; flex-shrink: 0; margin-left: 4px; transition: all 0.2s;
}
.cnb-enhanced:hover .cnb-tag-add-btn {
display: inline-flex;
}
.cnb-tag-add-btn:hover {
border-color: #1677ff; color: #1677ff;
background: rgba(22,119,255,0.05);
}
.cnb-tag-popup {
position: fixed; background: #fff;
border: 1px solid #e0e0e0; border-radius: 10px;
padding: 14px 16px 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
z-index: 99999; min-width: 220px;
}
.cnb-tag-popup input {
width: 100%; box-sizing: border-box;
border: 1px solid #d0d0d0; border-radius: 6px;
padding: 6px 10px; font-size: 13px; outline: none;
}
.cnb-tag-popup input:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22,119,255,0.15);
}
.cnb-tag-popup .popup-title {
font-size: 13px; font-weight: 600; margin-bottom: 8px; color: #333;
}
.cnb-tag-popup .popup-suggestions {
display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px;
}
.cnb-tag-popup .popup-suggestions span {
padding: 2px 8px; border-radius: 6px; font-size: 12px;
background: #f5f5f5; cursor: pointer;
}
.cnb-tag-popup .popup-suggestions span:hover {
background: #e6f0ff; color: #1677ff;
}
.cnb-tag-popup .popup-actions {
display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px;
}
.cnb-tag-popup .popup-actions button {
border: none; padding: 4px 14px; border-radius: 6px;
font-size: 12px; cursor: pointer;
}
.cnb-tag-popup .popup-actions .btn-ok { background: #1677ff; color: #fff; }
.cnb-tag-popup .popup-actions .btn-ok:hover { background: #4096ff; }
.cnb-tag-popup .popup-actions .btn-cancel { background: #f0f0f0; color: #666; }
.cnb-tag-popup .popup-actions .btn-cancel:hover { background: #d9d9d9; }
.cnb-fav-filter-bar { margin-top: 8px; margin-bottom: 12px; }
.cnb-tags-container {
display: flex; flex-wrap: wrap; align-items: center;
gap: 4px;
}
body.cnb-fav-active .flex.flex-wrap.items-center.justify-between.gap-2 {
display: none !important;
}
.cnb-backup-btn-group {
display: none; gap: 8px; align-items: center;
}
body.cnb-fav-active .cnb-backup-btn-group {
display: flex;
}
.cnb-backup-btn-group button {
padding: 4px 12px; border: 1px solid #dcdcdc; border-radius: 4px;
background: #fff; color: #666; font-size: 13px; cursor: pointer;
transition: all 0.2s; display: flex; align-items: center; gap: 4px;
}
.cnb-backup-btn-group button:hover {
border-color: #1677ff; color: #1677ff;
}
/* 快捷导航栏样式 */
.cnb-quick-access-bar {
display: flex;
align-items: center;
min-w-0;
gap: 4px;
margin-left: 4px;
border-left: 1px solid rgba(0, 0, 0, 0.08);
padding-left: 6px;
}
.cnb-quick-access-item {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 3px;
overflow-hidden;
cursor: pointer;
transition: background-color 0.15s;
flex-shrink: 0;
}
.cnb-quick-access-item:hover {
background-color: var(--gray-pri-hover, rgba(0,0,0,0.06));
}
.cnb-quick-access-item img {
width: 24px;
height: 24px;
object-fit: cover;
border-radius: 2px;
pointer-events: none;
}
/* 添加按钮样式 */
.cnb-qa-add-btn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: 3px;
border: 1px dashed #bbb; color: #999; font-size: 16px;
line-height: 1; cursor: pointer; transition: all 0.2s;
flex-shrink: 0; background: transparent;
}
.cnb-qa-add-btn:hover {
border-color: #1677ff; color: #1677ff; background: rgba(22,119,255,0.05);
}
/* 拖拽样式 */
.cnb-quick-access-item { cursor: grab; position: relative; }
.cnb-quick-access-item:active { cursor: grabbing; }
.cnb-quick-access-item.dragging {
opacity: 0.4;
transform: scale(0.9);
}
.cnb-drag-indicator {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 80%;
background: #1677ff;
border-radius: 2px;
pointer-events: none;
z-index: 10;
box-shadow: 0 0 6px rgba(22,119,255,0.5);
}
/* 快捷导航弹窗 */
.cnb-qa-popup {
position: fixed; background: #fff; border: 1px solid #e0e0e0;
border-radius: 10px; padding: 14px 16px 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12); z-index: 99999; min-width: 280px; width: 360px; box-sizing: border-box;
}
.dark .cnb-qa-popup { background: #1a1a1a; border-color: #333; color: #ddd; }
.cnb-qa-popup .popup-title { font-size: 13px; font-weight: 600; margin-bottom: 10px; color: #333; }
.dark .cnb-qa-popup .popup-title { color: #eee; }
.cnb-qa-popup .popup-field { margin-bottom: 8px; }
.cnb-qa-popup .popup-field label { display: block; font-size: 12px; color: #666; margin-bottom: 3px; }
.dark .cnb-qa-popup .popup-field label { color: #aaa; }
.cnb-qa-popup .popup-field input { width: 100%; box-sizing: border-box; border: 1px solid #d0d0d0; border-radius: 6px; padding: 6px 10px; font-size: 13px; outline: none; background: #fff; color: #333; }
.dark .cnb-qa-popup .popup-field input { background: #222; border-color: #444; color: #eee; }
.cnb-qa-popup .popup-field input:focus { border-color: #1677ff; box-shadow: 0 0 0 2px rgba(22,119,255,0.15); }
.cnb-qa-popup .popup-hint { font-size: 11px; color: #999; margin-top: 4px; line-height: 1.4; }
.cnb-qa-popup .popup-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
.cnb-qa-popup .popup-actions button { border: none; padding: 4px 14px; border-radius: 6px; font-size: 12px; cursor: pointer; }
.cnb-qa-popup .popup-actions .btn-ok { background: #1677ff; color: #fff; }
.cnb-qa-popup .popup-actions .btn-ok:hover { background: #4096ff; }
.cnb-qa-popup .popup-actions .btn-cancel { background: #f0f0f0; color: #666; }
.cnb-qa-popup .popup-actions .btn-cancel:hover { background: #d9d9d9; }
/* 云原生开发开关样式 */
.cnb-qa-ws-toggle-wrap {
display: flex; align-items: center; justify-content: flex-end;
gap: 6px; margin-bottom: 8px; padding-top: 4px;
}
.cnb-qa-ws-toggle-label {
font-size: 12px; color: #666; white-space: nowrap;
}
.dark .cnb-qa-ws-toggle-label { color: #aaa; }
.cnb-qa-ws-switch {
position: relative; width: 40px; height: 22px; flex-shrink: 0;
}
.cnb-qa-ws-switch input { opacity: 0; width: 0; height: 0; }
.cnb-qa-ws-slider {
position: absolute; cursor: pointer; inset: 0;
background-color: #ccc; border-radius: 22px; transition: 0.3s;
}
.cnb-qa-ws-slider:before {
content: ""; position: absolute; height: 16px; width: 16px;
left: 3px; bottom: 3px; background: #fff;
border-radius: 50%; transition: 0.3s;
}
.cnb-qa-ws-switch input:checked + .cnb-qa-ws-slider {
background-color: #1677ff;
}
.cnb-qa-ws-switch input:checked + .cnb-qa-ws-slider:before {
transform: translateX(18px);
}
.cnb-qa-ws-fields {
display: none; border-top: 1px solid #eee; margin-top: 8px; padding-top: 10px;
}
.dark .cnb-qa-ws-fields { border-top-color: #333; }
.cnb-qa-ws-fields.visible { display: block; }
.cnb-qa-ws-fields .popup-field { margin-bottom: 6px; }
.cnb-qa-ws-fields .popup-field:last-child { margin-bottom: 0; }
.cnb-qa-ws-status {
font-size: 11px; color: #999; margin-top: 4px; min-height: 16px;
}
.cnb-qa-ws-status.ok { color: #52c41a; }
.cnb-qa-ws-status.err { color: #ff4d4f; }
.cnb-qa-ws-status.loading { color: #1677ff; }
/* 设置图标样式 */
.cnb-qa-settings-btn {
display:flex;align-items:center;justify-content:center;width:22px;height:22px;
border-radius:4px;cursor:pointer;color:#999;transition:all 0.2s;
background:transparent;border:none;padding:0;flex-shrink:0;
}
.cnb-qa-settings-btn:hover { color:#1677ff;background:rgba(22,119,255,0.08); }
.cnb-qa-cos-popup {
position:fixed;background:#fff;border:1px solid #e0e0e0;border-radius:10px;
padding:16px 18px 14px;box-shadow:0 8px 24px rgba(0,0,0,0.12);
z-index:999999;min-width:320px;width:380px;box-sizing:border-box;
}
.dark .cnb-qa-cos-popup { background:#1a1a1a;border-color:#333;color:#ddd; }
.cnb-qa-cos-popup .cos-title { font-size:14px;font-weight:600;margin-bottom:12px;color:#333;display:flex;align-items:center;gap:6px; }
.dark .cnb-qa-cos-popup .cos-title { color:#eee; }
.cnb-qa-cos-popup .cos-field { margin-bottom:10px; }
.cnb-qa-cos-popup .cos-field label { display:block;font-size:12px;color:#666;margin-bottom:3px;font-weight:500; }
.dark .cnb-qa-cos-popup .cos-field label { color:#aaa; }
.cnb-qa-cos-popup .cos-field input { width:100%;box-sizing:border-box;border:1px solid #d0d0d0;border-radius:6px;padding:7px 10px;font-size:13px;outline:none;background:#fff;color:#333; }
.dark .cnb-qa-cos-popup .cos-field input { background:#222;border-color:#444;color:#eee; }
.cnb-qa-cos-popup .cos-field input:focus { border-color:#1677ff;box-shadow:0 0 0 2px rgba(22,119,255,0.15); }
.cnb-qa-cos-popup .cos-hint { font-size:11px;color:#999;margin-top:3px;line-height:1.4; }
.cnb-qa-cos-popup .cos-actions { display:flex;justify-content:space-between;align-items:center;margin-top:14px;gap:8px; }
.cnb-qa-cos-popup .cos-actions button { border:none;padding:5px 16px;border-radius:6px;font-size:12px;cursor:pointer; }
.cnb-qa-cos-popup .cos-actions .btn-ok { background:#1677ff;color:#fff; }
.cnb-qa-cos-popup .cos-actions .btn-ok:hover { background:#4096ff; }
.cnb-qa-cos-popup .cos-actions .btn-cancel { background:#f0f0f0;color:#666; }
.cnb-qa-cos-popup .cos-actions .btn-cancel:hover { background:#d9d9d9; }
.cnb-qa-cos-popup .cos-actions .btn-test { background:#fafafa;color:#666;border:1px solid #d0d0d0; }
.cnb-qa-cos-popup .cos-actions .btn-test:hover { border-color:#1677ff;color:#1677ff; }
.cnb-qa-cos-popup .cos-status { font-size:11px;margin-top:8px;text-align:center;min-height:16px; }
.cnb-qa-cos-popup .cos-status.ok { color:#52c41a; }
.cnb-qa-cos-popup .cos-status.err { color:#ff4d4f; }
/* 自定义 Toast 通知 */
.cnb-toast {
position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
padding: 10px 24px; border-radius: 8px; font-size: 14px; font-weight: 500;
z-index: 999999; pointer-events: none; white-space: nowrap;
animation: cnbToastIn 0.25s ease;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
}
.cnb-toast.cnb-toast-ok { background: #f6ffed; color: #52c41a; border: 1px solid #b7eb8f; }
.cnb-toast.cnb-toast-err { background: #fff2f0; color: #ff4d4f; border: 1px solid #ffccc7; }
.cnb-toast.cnb-toast-out { animation: cnbToastOut 0.2s ease forwards; }
.dark .cnb-toast.cnb-toast-ok { background: rgba(82,196,26,0.12); border-color: rgba(82,196,26,0.3); }
.dark .cnb-toast.cnb-toast-err { background: rgba(255,77,79,0.12); border-color: rgba(255,77,79,0.3); }
@keyframes cnbToastIn { from { opacity:0; transform:translateX(-50%) translateY(-12px); } to { opacity:1; transform:translateX(-50%) translateY(0); } }
@keyframes cnbToastOut { from { opacity:1; transform:translateX(-50%) translateY(0); } to { opacity:0; transform:translateX(-50%) translateY(-12px); } }
/* 自定义确认弹窗 */
.cnb-confirm-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.35);
z-index: 999998; display: flex; align-items: center; justify-content: center;
}
.cnb-confirm-box {
background: #fff; border-radius: 10px; padding: 20px 24px 16px;
box-shadow: 0 8px 32px rgba(0,0,0,0.16); max-width: 400px; width: 90%;
animation: cnbToastIn 0.2s ease;
}
.dark .cnb-confirm-box { background: #1a1a1a; color: #ddd; }
.cnb-confirm-box .cnb-confirm-msg { font-size: 14px; line-height: 1.6; margin-bottom: 16px; white-space: pre-wrap; }
.cnb-confirm-box .cnb-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
.cnb-confirm-box .cnb-confirm-actions button {
border: none; padding: 5px 16px; border-radius: 6px; font-size: 13px; cursor: pointer;
}
.cnb-confirm-box .cnb-confirm-ok { background: #1677ff; color: #fff; }
.cnb-confirm-box .cnb-confirm-ok:hover { background: #4096ff; }
.cnb-confirm-box .cnb-confirm-cancel { background: #f0f0f0; color: #666; }
.cnb-confirm-box .cnb-confirm-cancel:hover { background: #d9d9d9; }
.dark .cnb-confirm-box .cnb-confirm-cancel { background: #333; color: #aaa; }
.dark .cnb-confirm-box .cnb-confirm-cancel:hover { background: #444; }
`;
document.head.appendChild(style);
const STORAGE_KEY = 'cnb_favorites_data';
// ===== SPA 路由监控工具 =====
function isStarsPage() {
return /\/u\/[^/]+\/stars/.test(window.location.pathname);
}
function cleanupIfNotStars() {
document.querySelectorAll('.cnb-fav-tab, .cnb-fav-filter-bar, .cnb-backup-btn-group').forEach(el => el.remove());
document.body.classList.remove('cnb-fav-active');
_favActive = false;
}
// ===== 数据管理 =====
function getData() {
try { return JSON.parse(GM_getValue(STORAGE_KEY, '{}')); }
catch (e) { return {}; }
}
function saveData(data) { GM_setValue(STORAGE_KEY, JSON.stringify(data)); }
function pageKey() {
const p = window.location.pathname;
return p.endsWith('/') ? p.slice(0, -1) : p;
}
// HTML 转义:将标签名安全插入 innerHTML
function escapeHTML(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function getRepoTags(repoHref) {
const data = getData(), pk = pageKey();
return (data[pk] && data[pk][repoHref] && data[pk][repoHref].tags) || [];
}
function setRepoTags(repoHref, tags) {
const data = getData(), pk = pageKey();
if (!data[pk]) data[pk] = {};
if (!data[pk][repoHref]) data[pk][repoHref] = {};
data[pk][repoHref].tags = tags;
saveData(data);
}
function getAllTags() {
const data = getData(), pk = pageKey();
const all = new Set();
if (data[pk]) Object.values(data[pk]).forEach(item => {
if (item.tags) item.tags.forEach(t => all.add(t));
});
return [...all].sort();
}
function getTaggedCount() {
const data = getData(), pk = pageKey();
let n = 0;
if (data[pk]) Object.values(data[pk]).forEach(item => {
if (item.tags && item.tags.length) n++;
});
return n;
}
function getRepoHref(card) {
const link = card.querySelector('a[href*="/"][target="_self"]');
if (!link) return null;
const h = link.getAttribute('href');
return h.endsWith('/') ? h.slice(0, -1) : h;
}
// ===== JSON 备份与还原 =====
async function exportJSON() {
const data = getData();
const qaLinks = getQALinks();
const allData = {
favorites: data,
quickAccess: qaLinks
};
const jsonStr = JSON.stringify(allData, null, 2);
if (isCosEnabled()) {
try { await cosUpload(jsonStr); showToast('已同步到 COS 云端(' + COS_FILE_KEY + ')', 'ok'); }
catch (e) { showToast('COS 上传失败:' + e.message + ',已改为本地下载', 'err'); doLocalExport(); }
} else { doLocalExport(); }
function doLocalExport() {
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(jsonStr);
const anchor = document.createElement('a');
anchor.setAttribute("href", dataStr);
anchor.setAttribute("download", `cnb_cool_backup_${new Date().getTime()}.json`);
document.body.appendChild(anchor);
anchor.click();
anchor.remove();
}
}
async function importJSON() {
if (isCosEnabled()) {
try {
const body = await cosDownload();
const parsedData = typeof body === 'string' ? JSON.parse(body) : JSON.parse(new TextDecoder().decode(body));
await applyImport(parsedData);
return;
} catch (e) {
if (!await showConfirm('COS 下载失败:' + e.message + '\n是否改用本地文件导入?')) return;
}
}
doLocalImport();
function doLocalImport() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async event => {
try {
const parsedData = JSON.parse(event.target.result);
await applyImport(parsedData);
} catch (err) {
showToast('解析失败,请确保上传的是格式正确的 JSON 备份文件!', 'err');
}
};
reader.readAsText(file);
};
input.click();
}
}
async function applyImport(parsedData) {
if (await showConfirm('警告:导入备份将覆盖当前的标签、收藏与导航栏数据,是否确认继续?')) {
if (parsedData.favorites) { saveData(parsedData.favorites); }
else { saveData(parsedData); }
if (parsedData.quickAccess && Array.isArray(parsedData.quickAccess)) { saveQALinks(parsedData.quickAccess); }
showToast('数据导入成功!页面即将刷新。', 'ok');
location.reload();
}
}
function addBackupButtons() {
if (document.querySelector('.cnb-backup-btn-group')) return;
const headers = document.querySelectorAll('.flex.justify-between.mb-4');
let targetHeader = null;
for(let header of headers) {
if (header.querySelector('.issue-list-switcher') || header.className.includes('max-laptop:flex-col')) {
targetHeader = header;
break;
}
}
if (!targetHeader) return;
const btnGroup = document.createElement('div');
btnGroup.className = 'cnb-backup-btn-group';
btnGroup.innerHTML = `
`;
btnGroup.querySelector('.cnb-upload-btn').addEventListener('click', importJSON);
btnGroup.querySelector('.cnb-download-btn').addEventListener('click', exportJSON);
const rightWrap = targetHeader.lastElementChild;
if (rightWrap && rightWrap.tagName === 'DIV' && rightWrap.classList.contains('flex')) {
rightWrap.appendChild(btnGroup);
} else {
targetHeader.appendChild(btnGroup);
}
}
// ===== 状态 =====
let _favActive = false;
// ===== 1. 添加收藏夹 tab =====
function addFavoritesTab() {
if (document.querySelector('.cnb-fav-tab')) return;
const radios = document.querySelectorAll('.t-radio-button.issue-list-switcher');
let missionsLabel = null;
for (const lb of radios) {
const txt = lb.querySelector('.t-radio-button__label');
if (txt && txt.textContent.includes('任务集')) { missionsLabel = lb; break; }
}
if (!missionsLabel) return;
const favLabel = document.createElement('label');
favLabel.className = 't-radio-button issue-list-switcher cnb-fav-tab';
favLabel.tabIndex = '0';
favLabel.innerHTML = `
收藏夹
(${getTaggedCount()})
`;
missionsLabel.parentNode.insertBefore(favLabel, missionsLabel.nextSibling);
const group = favLabel.closest('.t-radio-group');
if (!group) return;
group.addEventListener('click', function onTabClick(e) {
const label = e.target.closest('.t-radio-button.issue-list-switcher');
if (!label) return;
const isFav = label.classList.contains('cnb-fav-tab');
if (isFav) {
_favActive = true;
if (window.location.search.includes('tab=')) {
const reposInput = group.querySelector('input[value="repos"]');
if (reposInput) {
reposInput.click();
const checkInterval = setInterval(() => {
forceFavStyle(group, label);
if (!window.location.search.includes('tab=') && document.querySelector('.grid.gap-4.mt-4')) {
clearInterval(checkInterval);
toggleFavView(true);
}
}, 50);
setTimeout(() => clearInterval(checkInterval), 3000);
return;
}
}
forceFavStyle(group, label);
toggleFavView(true);
} else {
setTimeout(() => {
_favActive = false;
toggleFavView(false);
const favTab = group.querySelector('.cnb-fav-tab');
if (favTab) {
favTab.classList.remove('t-is-checked');
const favText = favTab.querySelector('.font-bold-placeholder');
if (favText) {
favText.classList.remove('text-pri', 'font-semibold');
favText.classList.add('text-sec');
}
}
label.classList.add('t-is-checked');
const textSpan = label.querySelector('.font-bold-placeholder');
if (textSpan) {
textSpan.classList.remove('text-sec');
textSpan.classList.add('text-pri', 'font-semibold');
}
setTimeout(() => updateBgBlock(label), 10);
}, 20);
}
});
}
function forceFavStyle(group, favLabel) {
favLabel.classList.add('t-is-checked');
const favText = favLabel.querySelector('.font-bold-placeholder');
if (favText) {
favText.classList.remove('text-sec');
favText.classList.add('text-pri', 'font-semibold');
}
group.querySelectorAll('.t-radio-button.issue-list-switcher:not(.cnb-fav-tab)').forEach(label => {
label.classList.remove('t-is-checked');
const textSpan = label.querySelector('.font-bold-placeholder');
if (textSpan) {
textSpan.classList.remove('text-pri', 'font-semibold');
textSpan.classList.add('text-sec');
}
});
setTimeout(() => updateBgBlock(favLabel), 20);
}
function updateBgBlock(el) {
const bg = el.closest('.t-radio-group')?.querySelector('.t-radio-group__bg-block');
if (!bg) return;
const group = el.closest('.t-radio-group');
if (!group) return;
const gr = group.getBoundingClientRect(), er = el.getBoundingClientRect();
bg.style.width = er.width + 'px';
bg.style.height = er.height + 'px';
bg.style.left = (er.left - gr.left) + 'px';
bg.style.top = (er.top - gr.top) + 'px';
}
// ===== 2. 切换收藏夹视图 =====
function toggleFavView(show) {
if (show) {
document.body.classList.add('cnb-fav-active');
} else {
document.body.classList.remove('cnb-fav-active');
}
const grid = document.querySelector('.grid.gap-4.mt-4');
if (!grid) return;
const allCards = grid.querySelectorAll(':scope > div');
const filterBar = document.querySelector('.cnb-fav-filter-bar');
if (show) {
allCards.forEach(c => c.style.display = 'none');
if (!filterBar) {
createFilterBar(grid);
} else {
filterBar.style.display = 'block';
requestAnimationFrame(() => {
const allBtn = filterBar.querySelector('.cnb-fav-filter-all');
if (allBtn) {
filterBar.querySelectorAll('.t-radio-button').forEach(b => b.classList.remove('t-is-checked'));
filterBar.querySelectorAll('input').forEach(inp => inp.checked = false);
allBtn.classList.add('t-is-checked');
const inp = allBtn.querySelector('input');
if (inp) inp.checked = true;
updateFilterBg(allBtn);
}
});
}
filterFavCards('all');
} else {
allCards.forEach(c => c.style.display = '');
if (filterBar) filterBar.style.display = 'none';
}
}
function filterFavCards(tag) {
const grid = document.querySelector('.grid.gap-4.mt-4');
if (!grid) return;
grid.querySelectorAll(':scope > div').forEach(card => {
const href = getRepoHref(card);
if (!href) { card.style.display = 'none'; return; }
const tags = getRepoTags(href);
if (tag === 'all') card.style.display = tags.length ? '' : 'none';
else card.style.display = tags.includes(tag) ? '' : 'none';
});
}
// ===== 3. 收藏夹筛选栏 =====
function createFilterBar(grid) {
if (document.querySelector('.cnb-fav-filter-bar')) return;
const bar = document.createElement('div');
bar.className = 'cnb-fav-filter-bar';
const allTags = getAllTags();
let html = '
';
bar.innerHTML = html;
grid.parentNode.insertBefore(bar, grid);
bar.querySelectorAll('.t-radio-button').forEach(btn => {
btn.addEventListener('click', function (e) {
e.preventDefault();
e.stopPropagation();
const parent = this.closest('.t-radio-group');
parent.querySelectorAll('.t-radio-button').forEach(b => b.classList.remove('t-is-checked'));
parent.querySelectorAll('input').forEach(inp => inp.checked = false);
this.classList.add('t-is-checked');
this.querySelector('input').checked = true;
updateFilterBg(this);
filterFavCards(this.getAttribute('data-tag') || 'all');
});
});
const first = bar.querySelector('.t-radio-button');
if (first) updateFilterBg(first);
}
function updateFilterBg(el) {
const bg = el.closest('.t-radio-group')?.querySelector('.t-radio-group__bg-block');
if (!bg) return;
const group = el.closest('.t-radio-group');
if (!group) return;
const gr = group.getBoundingClientRect(), er = el.getBoundingClientRect();
bg.style.width = er.width + 'px';
bg.style.height = er.height + 'px';
bg.style.left = (er.left - gr.left) + 'px';
bg.style.top = (er.top - gr.top) + 'px';
}
// ===== 4. 给卡片添加标签 =====
function enhanceRepoCards() {
const grid = document.querySelector('.grid.gap-4.mt-4');
if (!grid) return;
grid.querySelectorAll(':scope > div:not(.cnb-enhanced)').forEach(card => {
card.classList.add('cnb-enhanced');
const href = getRepoHref(card);
if (!href) return;
// 找到目标位置:flex items-center min-w-0 gap-2 (标题行) 内部末尾
const parentContainer = card.querySelector('.flex.items-center.min-w-0.gap-2');
if (!parentContainer) return;
const container = document.createElement('div');
container.className = 'cnb-tags-container';
renderTags(container, card, href);
// 手动添加标签按钮 [+]
const addBtn = document.createElement('span');
addBtn.className = 'cnb-tag-add-btn';
addBtn.textContent = '+';
addBtn.title = '手动添加分类标签';
container.appendChild(addBtn);
addBtn.addEventListener('click', function (e) {
e.stopPropagation();
showPopup(this, card, href);
});
parentContainer.appendChild(container);
});
}
function renderTags(container, card, href) {
const tags = getRepoTags(href);
container.querySelectorAll('.cnb-tag-chip').forEach(el => el.remove());
for (const tag of tags) {
const chip = document.createElement('span');
chip.className = 'cnb-tag-chip';
chip.appendChild(document.createTextNode(tag + ' '));
const removeSpan = document.createElement('span');
removeSpan.className = 'remove';
removeSpan.setAttribute('data-tag', tag);
removeSpan.textContent = '\u00D7';
chip.appendChild(removeSpan);
// 确保插在最前面(+号按钮之前)
container.insertBefore(chip, container.querySelector('.cnb-tag-add-btn') || null);
}
container.querySelectorAll('.cnb-tag-chip .remove').forEach(el => {
el.addEventListener('click', function (e) {
e.stopPropagation();
const t = this.getAttribute('data-tag');
const rh = getRepoHref(card);
if (!rh) return;
let tags = getRepoTags(rh).filter(x => x !== t);
setRepoTags(rh, tags);
renderTags(container, card, rh);
updateFavCount();
if (_favActive) {
const act = document.querySelector('.cnb-fav-filter-bar .t-is-checked');
filterFavCards(act ? act.getAttribute('data-tag') || 'all' : 'all');
}
syncFilterTags();
});
});
}
function showPopup(btn, card, href) {
document.querySelectorAll('.cnb-tag-popup').forEach(el => el.remove());
const existing = getRepoTags(href);
const allTags = getAllTags();
const popup = document.createElement('div');
popup.className = 'cnb-tag-popup';
popup.innerHTML = `
`;
document.body.appendChild(popup);
const rect = btn.getBoundingClientRect();
popup.style.left = Math.min(rect.left, window.innerWidth - 250) + 'px';
popup.style.top = (rect.bottom + 6) + 'px';
const input = popup.querySelector('input');
function renderSuggestions() {
const val = input.value.trim().toLowerCase();
const sug = popup.querySelector('.popup-suggestions');
sug.innerHTML = '';
allTags.filter(t => !existing.includes(t)).forEach(tag => {
const span = document.createElement('span');
span.textContent = tag;
if (val && !tag.toLowerCase().includes(val)) span.style.display = 'none';
span.addEventListener('click', () => { input.value = tag; });
sug.appendChild(span);
});
}
renderSuggestions();
input.addEventListener('input', renderSuggestions);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') done();
else if (e.key === 'Escape') popup.remove();
});
function done() {
const val = input.value.trim();
if (!val) { popup.remove(); return; }
let tags = getRepoTags(href);
if (!tags.includes(val)) { tags.push(val); setRepoTags(href, tags); }
const c = card.querySelector('.cnb-tags-container');
if (c) renderTags(c, card, href);
popup.remove();
updateFavCount();
if (_favActive) {
const act = document.querySelector('.cnb-fav-filter-bar .t-is-checked');
filterFavCards(act ? act.getAttribute('data-tag') || 'all' : 'all');
}
syncFilterTags();
}
popup.querySelector('.btn-ok').addEventListener('click', done);
popup.querySelector('.btn-cancel').addEventListener('click', () => popup.remove());
setTimeout(() => {
document.addEventListener('click', function oc(e) {
if (!popup.contains(e.target) && !btn.contains(e.target)) {
popup.remove();
document.removeEventListener('click', oc);
}
});
}, 10);
input.focus();
}
function updateFavCount() {
const n = getTaggedCount();
document.querySelectorAll('.cnb-fav-tab .text-ter').forEach(el => { el.textContent = `(${n})`; });
}
function syncFilterTags() {
const bar = document.querySelector('.cnb-fav-filter-bar');
if (!bar) return;
const group = bar.querySelector('.t-radio-group');
if (!group) return;
const allTags = getAllTags();
group.querySelectorAll('.cnb-fav-filter-tag').forEach(btn => {
if (!allTags.includes(btn.getAttribute('data-tag'))) btn.remove();
});
const existing = new Set();
group.querySelectorAll('.cnb-fav-filter-tag').forEach(btn => existing.add(btn.getAttribute('data-tag')));
for (const tag of allTags) {
if (existing.has(tag)) continue;
const escapedTag = escapeHTML(tag);
const label = document.createElement('label');
label.tabIndex = '0';
label.className = 't-radio-button cnb-fav-filter-tag';
label.setAttribute('data-tag', tag);
label.innerHTML = `
${escapedTag}`;
const allBtn = group.querySelector('.cnb-fav-filter-all');
(allBtn || group).after(label);
label.addEventListener('click', function (e) {
e.preventDefault(); e.stopPropagation();
group.querySelectorAll('.t-radio-button').forEach(b => b.classList.remove('t-is-checked'));
group.querySelectorAll('input').forEach(inp => inp.checked = false);
this.classList.add('t-is-checked');
this.querySelector('input').checked = true;
updateFilterBg(this);
filterFavCards(this.getAttribute('data-tag'));
});
}
}
// ===== 5. 初始化与路由监控 =====
let _inited = false;
function init() {
if (_inited) return;
_inited = true;
const checkExist = setInterval(() => {
if (isStarsPage()) {
if (document.querySelector('.issue-list-switcher')) {
clearInterval(checkExist);
addFavoritesTab();
addBackupButtons();
enhanceRepoCards();
const favTab = document.querySelector('.cnb-fav-tab');
if (favTab && favTab.classList.contains('t-is-checked')) {
toggleFavView(true);
}
}
}
}, 50);
setTimeout(() => clearInterval(checkExist), 5000);
const observer = new MutationObserver(() => {
if (isStarsPage()) {
if (window.location.search.includes('tab=') && _favActive) {
_favActive = false;
toggleFavView(false);
const favTab = document.querySelector('.cnb-fav-tab');
if (favTab) {
favTab.classList.remove('t-is-checked');
const favText = favTab.querySelector('.font-bold-placeholder');
if (favText) {
favText.classList.remove('text-pri', 'font-semibold');
favText.classList.add('text-sec');
}
}
}
if (!document.querySelector('.cnb-fav-tab') && document.querySelector('.issue-list-switcher')) {
addFavoritesTab();
addBackupButtons();
if (_favActive) {
const group = document.querySelector('.t-radio-group');
const favTab = document.querySelector('.cnb-fav-tab');
if (group && favTab) forceFavStyle(group, favTab);
}
}
const grid = document.querySelector('.grid.gap-4.mt-4');
if (grid) {
const un = grid.querySelectorAll(':scope > div:not(.cnb-enhanced)');
if (un.length) enhanceRepoCards();
}
} else {
cleanupIfNotStars();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === 'complete') init();
else window.addEventListener('load', init);
// ===== CNB 增强功能 =====
// 检查是否为 cnb.cool 域名
function isCnbDomain() {
return /(^|\.)cnb\.cool$/i.test(location.hostname);
}
function isCnbHomepage() {
return location.hostname === 'cnb.cool' && (location.pathname === '/' || location.pathname === '');
}
// 直达目标解码:获取 cnb.cool /数字?url= 的目标地址
function getCnbGotoTarget(urlLike) {
try {
const u = new URL(urlLike, location.href);
const raw = u.searchParams.get('url') || '';
if (!raw) return '';
// 解码 1-2 次,兼容已编码/双重编码
let t = decodeURIComponent(raw);
if (/%[0-9A-Fa-f]{2}/.test(t)) {
try { t = decodeURIComponent(t); } catch (_) {}
}
// 只允许 http/https
return /^https?:\/\//i.test(t) ? t : '';
} catch (_) {
return '';
}
}
// 若当前位于 cnb.cool 的数字跳转页,立即重定向到真实目标
function handleCnbGotoPage() {
if (!isCnbDomain()) return;
if (!location.pathname.match(/^\/(\d+)$/)) return;
if (!location.search.includes('url=')) return;
const target = getCnbGotoTarget(location.href);
if (target) location.replace(target);
}
// 将页面内所有 数字?url= 链接批量改写为直链
function rewriteCnbGotoLinks(root = document) {
if (!isCnbDomain()) return;
root.querySelectorAll('a[href*="?url="]').forEach(a => {
try {
const href = a.getAttribute('href');
if (!href) return;
const absUrl = new URL(href, location.href).href;
if (!absUrl.includes('cnb.cool/') || !/\/(\d+)\?url=/i.test(absUrl)) return;
const target = getCnbGotoTarget(absUrl);
if (target) a.href = target;
} catch (_) {}
});
}
// 事件委托兜底:拦截点击数字跳转链接并直接打开目标
function cnbGotoClickHandler(e) {
if (!isCnbDomain()) return;
// 查找被点击的 元素
let el = e.target;
while (el && el !== document && !(el instanceof HTMLAnchorElement)) {
el = el.parentElement;
}
if (!(el instanceof HTMLAnchorElement)) return;
const href = el.getAttribute('href');
if (!href) return;
const absUrl = new URL(href, location.href).href;
if (!/\/(\d+)\?url=/i.test(absUrl)) return;
const target = getCnbGotoTarget(absUrl);
if (!target) return;
e.preventDefault();
e.stopPropagation();
// 兼容中键或带修饰键的新开方式
const newTab = e.button === 1 || e.ctrlKey || e.metaKey;
if (newTab) {
window.open(target, '_blank', 'noopener');
} else {
location.href = target;
}
}
// 增强 CNB 页面网格布局
function enhanceGridLayout() {
if (!isCnbDomain()) return;
// 添加单栏/双栏切换按钮样式(使用 GM_addStyle)
if (!document.getElementById('cnb-grid-toggle-style')) {
const gridStyle = document.createElement('style');
gridStyle.id = 'cnb-grid-toggle-style';
gridStyle.textContent = `
.cnb-grid-toggle-btn {
padding: 4px !important;
}
.cnb-grid-toggle-active {
background-color: #3d3d3d !important;
color: #fff !important;
}
.cnb-grid-toggle-inactive {
background-color: #f3f4f6 !important;
color: #6b7280 !important;
}
.cnb-grid-toggle-inactive:hover {
background-color: #e5e7eb !important;
}
`;
document.head.appendChild(gridStyle);
}
// 添加单栏/双栏切换开关
addGridLayoutToggle();
// 应用布局
applyGridLayout();
// "执行"按钮鼠标悬停时自动打开弹窗
enhanceExecuteButtonHover();
// 添加"与我有关"标签
addMineTab();
}
// 应用网格布局
function applyGridLayout() {
const isDoubleColumn = localStorage.getItem('cnbGridLayout') === 'double';
// 只修改 class="grid grid-cols-1 gap-4 mt-4 mb-8"("最近更新"下面的网格)
document.querySelectorAll('.grid.grid-cols-1.gap-4.mt-4.mb-8').forEach(el => {
if (isDoubleColumn) {
if (!el.classList.contains('xl:grid-cols-2')) {
el.classList.add('xl:grid-cols-2');
}
} else {
el.classList.remove('xl:grid-cols-2');
}
});
}
// 添加单栏/双栏切换开关
function addGridLayoutToggle() {
// 查找包含"最近更新"文本的元素(更健壮的匹配方式)
let targetDiv = null;
// 方式1:精确匹配包含"最近更新"的元素
const allDivs = document.querySelectorAll('div.font-semibold');
for (const el of allDivs) {
const text = el.textContent.trim();
if (text === '最近更新') {
targetDiv = el;
break;
}
}
// 方式2:如果方式1失败,尝试模糊匹配
if (!targetDiv) {
const allElements = document.querySelectorAll('[class*="font-semibold"]');
for (const el of allElements) {
if (el.textContent.trim() === '最近更新' && el.closest('.grid') === null) {
targetDiv = el;
break;
}
}
}
if (!targetDiv) return;
// 检查是否已经添加过
if (targetDiv.querySelector('.cnb-grid-layout-toggle')) return;
// 获取当前布局状态
const isDoubleColumn = localStorage.getItem('cnbGridLayout') === 'double';
// 给目标div添加flex布局,让内容两端对齐
targetDiv.classList.add('flex', 'items-center', 'justify-between');
// 保存"最近更新"文本
const textContent = targetDiv.textContent.trim();
targetDiv.textContent = '';
// 创建左边的内容容器
const leftContent = document.createElement('span');
leftContent.textContent = textContent;
// 创建开关容器
const toggleContainer = document.createElement('div');
toggleContainer.className = 'cnb-grid-layout-toggle flex items-center gap-2';
// 创建单栏按钮(图标)
const singleBtn = document.createElement('button');
singleBtn.className = `cnb-grid-toggle-btn rounded-lg transition-colors ${!isDoubleColumn ? 'cnb-grid-toggle-active' : 'cnb-grid-toggle-inactive'}`;
singleBtn.innerHTML = `
`;
singleBtn.title = '单栏';
singleBtn.addEventListener('click', () => {
localStorage.setItem('cnbGridLayout', 'single');
updateToggleButtons();
applyGridLayout();
});
// 创建双栏按钮(图标)
const doubleBtn = document.createElement('button');
doubleBtn.className = `cnb-grid-toggle-btn rounded-lg transition-colors ${isDoubleColumn ? 'cnb-grid-toggle-active' : 'cnb-grid-toggle-inactive'}`;
doubleBtn.innerHTML = `
`;
doubleBtn.title = '双栏';
doubleBtn.addEventListener('click', () => {
localStorage.setItem('cnbGridLayout', 'double');
updateToggleButtons();
applyGridLayout();
});
// 更新按钮状态的函数
function updateToggleButtons() {
const isDoubleColumn = localStorage.getItem('cnbGridLayout') === 'double';
if (isDoubleColumn) {
singleBtn.className = 'cnb-grid-toggle-btn rounded-lg transition-colors cnb-grid-toggle-inactive';
doubleBtn.className = 'cnb-grid-toggle-btn rounded-lg transition-colors cnb-grid-toggle-active';
} else {
singleBtn.className = 'cnb-grid-toggle-btn rounded-lg transition-colors cnb-grid-toggle-active';
doubleBtn.className = 'cnb-grid-toggle-btn rounded-lg transition-colors cnb-grid-toggle-inactive';
}
}
toggleContainer.appendChild(singleBtn);
toggleContainer.appendChild(doubleBtn);
// 将左边内容和开关添加到div中
targetDiv.appendChild(leftContent);
targetDiv.appendChild(toggleContainer);
}
// 增强"执行"和"新建"按钮:鼠标悬停时自动打开弹窗
function enhanceExecuteButtonHover() {
if (!isCnbDomain()) return;
// 查找所有按钮,使用更健壮的选择器
document.querySelectorAll('button').forEach(btn => {
// 避免重复绑定
if (btn.dataset.cnbEnhanced) return;
const buttonText = btn.textContent || '';
const isExecuteButton = buttonText.includes('执行');
const hasChevronDown = btn.querySelector('svg#chevron-down');
const isNewButton = buttonText.includes('新建') && hasChevronDown;
if (!isNewButton && !isExecuteButton) return;
btn.dataset.cnbEnhanced = 'true';
// 鼠标进入时模拟点击打开弹窗
btn.addEventListener('mouseenter', () => {
btn.classList.add('t-popup-open');
setTimeout(() => {
btn.click();
}, 10);
}, { passive: true });
// 鼠标离开时移除 class(不关闭弹窗,让用户可以交互)
btn.addEventListener('mouseleave', () => {
btn.classList.remove('t-popup-open');
}, { passive: true });
});
}
// 添加"与我有关"标签
function addMineTab() {
// 在主页和用户主页都显示"与我有关"标签
const isHomepage = isCnbHomepage();
const isUserHomepage = /^\/u\/[^/]+$/.test(location.pathname);
if (!isHomepage && !isUserHomepage) return;
// 查找标签导航容器 - 尝试多种选择器
let navWrap = document.querySelector('.t-tabs__nav-wrap');
// 备选:查找带有 tabs 相关 class 的容器
if (!navWrap) {
const tabElements = document.querySelectorAll('[class*="t-tabs"]');
for (const el of tabElements) {
if (el.querySelector('[class*="nav-item"]') || el.classList.contains('t-tabs__nav')) {
navWrap = el;
break;
}
}
}
// 备选:查找包含"探索"、"推荐"等标签的容器
if (!navWrap) {
const tabContainers = document.querySelectorAll('[class*="nav"]');
for (const container of tabContainers) {
const text = container.textContent || '';
if (text.includes('探索') || text.includes('推荐') || text.includes('与我有关')) {
navWrap = container;
break;
}
}
}
if (!navWrap) return;
// 检查是否已经添加过
if (navWrap.querySelector('.cnb-mine-tab')) return;
// 创建新的标签项
const newTabItem = document.createElement('div');
newTabItem.className = 't-tabs__nav-item t-size-m t-is-top cnb-mine-tab';
newTabItem.innerHTML = `
`;
// 添加到标签导航中
navWrap.appendChild(newTabItem);
}
// CNB 网站功能:添加"创建仓库"按钮
function addCreateRepoButton() {
// 检查是否在 profile 页面,如果是则不添加按钮
if (location.pathname.startsWith('/profile/')) return;
// 查找目标元素:包含"仓库墙"标题
const targetDiv = document.querySelector('h1.font-semibold.text-l');
if (!targetDiv) return;
// 找到 flex gap-1 容器
const parentDiv = targetDiv.closest('.flex.items-center.gap-1');
if (!parentDiv) return;
// 检查是否已经存在原生创建仓库按钮
if (parentDiv.querySelector('.cnb-create-repo-btn')) return;
// 检查是否存在设置按钮,如果没有设置按钮也不需要添加创建仓库按钮
const settingsBtn = parentDiv.querySelector('[id="settings"]');
if (!settingsBtn) return;
// 获取当前路径
const pathParts = location.pathname.split('/').filter(Boolean);
const groupPath = pathParts.join('/');
// 创建"创建仓库"按钮
const createRepoBtn = document.createElement('button');
createRepoBtn.className = 't-button t-button--theme-primary t-button--variant-base cnb-create-repo-btn';
createRepoBtn.innerHTML = `
创建仓库
`;
// 设置链接
const createRepoUrl = groupPath ? `https://cnb.cool/new/repos?group=${groupPath}` : 'https://cnb.cool/new/repos';
createRepoBtn.addEventListener('click', () => {
window.location.href = createRepoUrl;
});
// 在设置按钮前插入
if (settingsBtn && settingsBtn.parentElement) {
parentDiv.insertBefore(createRepoBtn, settingsBtn.parentElement);
} else {
parentDiv.appendChild(createRepoBtn);
}
// 在标题后添加一个 spacer,让按钮靠右
const spacer = document.createElement('div');
spacer.style.flex = '1';
targetDiv.after(spacer);
}
// 为可复制的输入框添加点击复制功能
function addInputCopyFeature() {
// 查找目标输入框容器 - 使用更健壮的选择器
document.querySelectorAll('.t-input__wrap').forEach(wrap => {
// 避免重复绑定
if (wrap.dataset.cnbCopyEnabled) return;
// 检查是否是可复制的容器(包含输入框且有值)
const input = wrap.querySelector('input');
if (!input) return;
// 检查是否有复制相关的标识(cursor: pointer 或特定 class)
const hasPointerCursor = getComputedStyle(wrap).cursor === 'pointer';
const hasCopyClass = wrap.classList.contains('min-w-0') || wrap.classList.contains('flex-auto');
// 只处理地址/URL 输入框(通常包含 git 相关的)
const isUrlInput = (input.value && input.value.includes('http')) ||
(input.placeholder && input.placeholder.toLowerCase().includes('url'));
// 检查是否真的需要添加复制功能
if (!hasPointerCursor && !hasCopyClass && !isUrlInput) return;
wrap.dataset.cnbCopyEnabled = 'true';
// 添加点击事件
wrap.addEventListener('click', (e) => {
const inputEl = wrap.querySelector('input');
if (inputEl && inputEl.value) {
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(inputEl.value);
} else {
navigator.clipboard.writeText(inputEl.value);
}
showCopyToast(e.clientX, e.clientY);
} catch (err) {
console.error('复制失败:', err);
}
}
});
// 添加可点击的视觉反馈样式
wrap.style.cursor = 'pointer';
});
}
// 显示复制提示
function showCopyToast(x, y) {
// 移除已存在的提示
const existingToast = document.querySelector('.cnb-copy-toast');
if (existingToast) {
existingToast.remove();
}
// 创建提示元素
const toast = document.createElement('div');
toast.className = 'cnb-copy-toast';
toast.textContent = '已复制!';
toast.style.cssText = `
position: fixed;
left: ${x}px;
top: ${y - 50}px;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
z-index: 99999;
pointer-events: none;
animation: cnbFadeIn 0.2s ease;
`;
// 添加动画样式
if (!document.getElementById('cnb-copy-toast-style')) {
const style = document.createElement('style');
style.id = 'cnb-copy-toast-style';
style.textContent = `
@keyframes cnbFadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
`;
document.head.appendChild(style);
}
document.body.appendChild(toast);
// 1秒后移除
setTimeout(() => {
toast.style.transition = 'opacity 0.2s ease';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 200);
}, 1000);
}
// 自定义 Toast 通知(居中顶部,3 秒自动消失)
function showToast(msg, type) {
document.querySelectorAll('.cnb-toast').forEach(el => el.remove());
const toast = document.createElement('div');
toast.className = 'cnb-toast cnb-toast-' + (type || 'ok');
toast.textContent = msg;
document.body.appendChild(toast);
setTimeout(() => {
toast.classList.add('cnb-toast-out');
setTimeout(() => toast.remove(), 220);
}, 3000);
}
// 自定义确认弹窗(返回 Promise)
function showConfirm(msg) {
return new Promise(resolve => {
document.querySelectorAll('.cnb-confirm-overlay').forEach(el => el.remove());
const overlay = document.createElement('div');
overlay.className = 'cnb-confirm-overlay';
overlay.innerHTML = `
`;
overlay.querySelector('.cnb-confirm-ok').addEventListener('click', () => { overlay.remove(); resolve(true); });
overlay.querySelector('.cnb-confirm-cancel').addEventListener('click', () => { overlay.remove(); resolve(false); });
overlay.addEventListener('click', function(e) { if (e.target === overlay) { overlay.remove(); resolve(false); } });
document.body.appendChild(overlay);
});
}
// 为下载按钮添加点击下载功能和复制直链按钮
function addDownloadButtonFeature() {
// 查找所有包含 download 图标的按钮
document.querySelectorAll('button').forEach(button => {
// 避免重复绑定
if (button.dataset.cnbDownloadEnhanced) return;
// 检查是否是下载按钮
const hasDownloadSvg = button.querySelector('svg#download');
if (!hasDownloadSvg) return;
// 只在文件查看页(blob)生效
if (!window.location.pathname.includes('/blob/')) return;
button.dataset.cnbDownloadEnhanced = 'true';
// 给下载按钮添加 rounded-l-none -ml-[1px],作为分组的右侧按钮
if (!button.classList.contains('rounded-l-none')) {
button.classList.add('rounded-l-none', '-ml-[1px]');
}
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
// 获取当前页面 URL 并转换为 raw 下载链接
const currentUrl = window.location.href;
const downloadUrl = currentUrl.replace(/\/blob\//, '/git/raw/');
// 创建隐藏的 a 标签触发下载
const link = document.createElement('a');
link.href = downloadUrl;
link.download = '';
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// 在下载按钮旁边添加复制直链按钮
const parentDiv = button.parentElement;
if (parentDiv && !parentDiv.querySelector('.cnb-copy-url-btn')) {
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
// 复制按钮在左,去掉右边圆角
copyBtn.className = 'rounded-r-none hover:z-10 px-2 text-sec hover:text-brand-500 t-button t-button--theme-default t-button--variant-outline cnb-copy-url-btn';
copyBtn.title = '复制直链';
copyBtn.innerHTML = `
`;
copyBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const currentUrl = window.location.href;
const downloadUrl = currentUrl.replace(/\/blob\//, '/git/raw/');
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(downloadUrl);
} else {
navigator.clipboard.writeText(downloadUrl);
}
showCopyToast(e.clientX, e.clientY);
} catch (err) {
console.error('复制失败:', err);
}
});
// 插入到下载按钮前面,形成 [复制] [下载] 分组
parentDiv.insertBefore(copyBtn, button);
}
});
}
// 快捷导航栏存储 key
const QA_STORAGE_KEY = 'cnb_quick_access_links';
// COS 配置存储 key
const COS_CONFIG_KEY = 'cnb_cos_config';
const COS_FILE_KEY = 'CNB/CNB.json';
function getCosConfig() {
try { return JSON.parse(GM_getValue(COS_CONFIG_KEY, '{}')); }
catch (e) { return {}; }
}
function setCosConfig(cfg) { GM_setValue(COS_CONFIG_KEY, JSON.stringify(cfg)); }
function isCosEnabled() {
const cfg = getCosConfig();
return !!(cfg.SecretId && cfg.SecretKey && cfg.Bucket && cfg.Region);
}
// ===== COS 签名工具(基于 Web Crypto API + GM_xmlhttpRequest,无需外部 SDK)=====
async function _sha1Hex(text) {
const buf = new TextEncoder().encode(text);
const hash = await crypto.subtle.digest('SHA-1', buf);
return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function _hmacSha1Hex(keyText, dataText) {
const keyBuf = new TextEncoder().encode(keyText);
const key = await crypto.subtle.importKey('raw', keyBuf, { name: 'HMAC', hash: 'SHA-1' }, false, ['sign']);
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(dataText));
return Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
}
async function _cosSign(method, keyPath, host, secretId, secretKey) {
const now = Math.floor(Date.now() / 1000);
const expired = now + 300;
const keyTime = now + ';' + expired;
const signKey = await _hmacSha1Hex(secretKey, keyTime);
const httpStr = method.toLowerCase() + '\n/' + keyPath + '\n\nhost=' + host + '\n';
const sha1Http = await _sha1Hex(httpStr);
const strToSign = 'sha1\n' + keyTime + '\n' + sha1Http + '\n';
const signature = await _hmacSha1Hex(signKey, strToSign);
return 'q-sign-algorithm=sha1&q-ak=' + secretId +
'&q-sign-time=' + keyTime + '&q-key-time=' + keyTime +
'&q-header-list=host&q-url-param-list=&q-signature=' + signature;
}
function _cosRequest(method, keyPath, body, cfg) {
const host = cfg.Bucket + '.cos.' + cfg.Region + '.myqcloud.com';
const url = 'https://' + host + '/' + keyPath;
return new Promise((resolve, reject) => {
_cosSign(method, keyPath, host, cfg.SecretId, cfg.SecretKey).then(auth => {
const headers = { 'Authorization': auth, 'Host': host };
if (body) headers['Content-Type'] = 'application/json';
GM_xmlhttpRequest({
method: method,
url: url,
headers: headers,
data: body || undefined,
onload: function(res) {
if (res.status >= 200 && res.status < 300) resolve(res);
else reject(new Error(method + ' COS 失败: HTTP ' + res.status + ' ' + (res.responseText || '')));
},
onerror: function() { reject(new Error(method + ' COS 请求失败')); }
});
}).catch(reject);
});
}
// ===== COS 导入导出 =====
function cosUpload(jsonData) {
const cfg = getCosConfig();
if (!isCosEnabled()) return Promise.reject(new Error('COS 未配置'));
return _cosRequest('PUT', COS_FILE_KEY, jsonData, cfg).then(function() {});
}
function cosDownload() {
const cfg = getCosConfig();
if (!isCosEnabled()) return Promise.reject(new Error('COS 未配置'));
return new Promise((resolve, reject) => {
_cosRequest('GET', COS_FILE_KEY, null, cfg).then(res => {
resolve(res.responseText);
}).catch(reject);
});
}
function showCosSettingsPopup(triggerBtn) {
document.querySelectorAll('.cnb-qa-cos-popup').forEach(el => el.remove());
const cfg = getCosConfig();
const popup = document.createElement('div');
popup.className = 'cnb-qa-cos-popup';
popup.innerHTML = `
`;
document.body.appendChild(popup);
const rect = triggerBtn.getBoundingClientRect();
popup.style.left = Math.min(rect.left, window.innerWidth - 400) + 'px';
popup.style.top = Math.max(10, rect.bottom - popup.offsetHeight - 6) + 'px';
const statusEl = popup.querySelector('#cos-status');
function setStatus(msg, cls) { statusEl.textContent = msg; statusEl.className = 'cos-status ' + cls; }
// 测试连接
popup.querySelector('#cos-btn-test').addEventListener('click', async () => {
setStatus('正在测试连接...', '');
try {
const testCfg = {
SecretId: popup.querySelector('#cos-input-SecretId').value.trim(),
SecretKey: popup.querySelector('#cos-input-SecretKey').value.trim(),
Bucket: popup.querySelector('#cos-input-Bucket').value.trim(),
Region: popup.querySelector('#cos-input-Region').value.trim()
};
if (!testCfg.SecretId || !testCfg.SecretKey || !testCfg.Bucket || !testCfg.Region) {
setStatus('请填写完整配置信息', 'err'); return;
}
setCosConfig(testCfg);
await _cosRequest('HEAD', '', null, testCfg);
setStatus('连接成功!COS 可用', 'ok');
} catch (e) { setStatus('连接失败: ' + e.message, 'err'); }
});
// 保存
popup.querySelector('#cos-btn-save').addEventListener('click', () => {
setCosConfig({
SecretId: popup.querySelector('#cos-input-SecretId').value.trim(),
SecretKey: popup.querySelector('#cos-input-SecretKey').value.trim(),
Bucket: popup.querySelector('#cos-input-Bucket').value.trim(),
Region: popup.querySelector('#cos-input-Region').value.trim()
});
setStatus('设置已保存', 'ok');
setTimeout(() => popup.remove(), 800);
});
popup.querySelector('#cos-btn-cancel').addEventListener('click', () => popup.remove());
setTimeout(() => {
document.addEventListener('click', function oc(e) {
if (!popup.contains(e.target) && !triggerBtn.contains(e.target)) {
popup.remove(); document.removeEventListener('click', oc);
}
});
}, 10);
}
function getQALinks() {
try { return JSON.parse(GM_getValue(QA_STORAGE_KEY, '[]')); }
catch (e) { return []; }
}
function saveQALinks(links) { GM_setValue(QA_STORAGE_KEY, JSON.stringify(links)); }
// 显示添加/编辑弹窗
function showQAPopup(btn, editIndex) {
document.querySelectorAll('.cnb-qa-popup').forEach(el => el.remove());
const links = getQALinks();
const isEdit = typeof editIndex === 'number';
const defaultHref = isEdit ? links[editIndex].href : '';
const defaultImg = isEdit ? links[editIndex].img : '';
const defaultWs = isEdit ? (links[editIndex].workspace || null) : null;
const popup = document.createElement('div');
popup.className = 'cnb-qa-popup';
popup.innerHTML = `
`;
document.body.appendChild(popup);
const rect = btn.getBoundingClientRect();
popup.style.left = Math.min(rect.left, window.innerWidth - 340) + 'px';
popup.style.top = (rect.bottom + 6) + 'px';
const hrefInput = popup.querySelector('#qa-input-href');
const imgInput = popup.querySelector('#qa-input-img');
const hrefLabel = popup.querySelector('#qa-href-label');
const hrefHint = popup.querySelector('#qa-href-hint');
const wsToggle = popup.querySelector('#qa-ws-toggle');
const wsFields = popup.querySelector('.cnb-qa-ws-fields');
const wsBranch = popup.querySelector('#qa-ws-branch');
const wsRef = popup.querySelector('#qa-ws-ref');
const wsToken = popup.querySelector('#qa-ws-token');
const wsTokenEye = popup.querySelector('#qa-ws-token-eye');
// 小眼睛切换密码显示/隐藏
wsTokenEye.addEventListener('click', () => {
if (wsToken.type === 'password') {
wsToken.type = 'text';
wsTokenEye.textContent = '🙈';
wsTokenEye.title = '隐藏';
} else {
wsToken.type = 'password';
wsTokenEye.textContent = '👁';
wsTokenEye.title = '显示';
}
});
// 设置按钮:弹出 COS 配置弹窗
popup.querySelector('#qa-settings-btn').addEventListener('click', (e) => {
e.stopPropagation();
showCosSettingsPopup(popup.querySelector('#qa-settings-btn'));
});
// 仓库地址链接点击时实时计算输入框值并跳转
function handleRepoLinkClick(e) {
e.preventDefault();
let val = hrefInput.value.trim().replace(/^https?:\/\/cnb\.cool/i, '');
if (val && !val.startsWith('/')) val = '/' + val;
if (val) window.open('https://cnb.cool' + val, '_blank');
}
// 云原生开发模式下,链接地址字段变为 API 路径
function syncHrefField() {
if (wsToggle.checked) {
hrefLabel.innerHTML = 'API 路径(仓库地址)';
hrefInput.placeholder = '如 /IIIStudio/Code/Greasemonkey/CNB';
hrefHint.textContent = '支持完整 URL,会自动去掉 https://cnb.cool 前缀';
// 编辑时如果有workspace配置,显示api_path而非解析后的href
if (isEdit && defaultWs && defaultWs.api_path) {
hrefInput.value = defaultWs.api_path;
}
// 绑定点击事件,每次点击时实时计算最新 URL
const link = hrefLabel.querySelector('#qa-repo-link');
if (link) link.addEventListener('click', handleRepoLinkClick);
} else {
hrefLabel.textContent = '链接地址(URL)';
hrefInput.placeholder = '如 /IIIStudio 或 https://example.com';
hrefHint.textContent = '以 / 开头时,图片地址将自动推断为 cnb.cool 组织 logo';
// 恢复为原始href值
if (isEdit && defaultHref) {
hrefInput.value = defaultHref;
}
}
}
// 初始化时同步一次
syncHrefField();
// 开关切换显示/隐藏云原生开发字段 + 切换链接地址标签
wsToggle.addEventListener('change', () => {
if (wsToggle.checked) {
wsFields.classList.add('visible');
} else {
wsFields.classList.remove('visible');
}
syncHrefField();
});
function done() {
let href = hrefInput.value.trim();
if (!href && !wsToggle.checked) { popup.remove(); return; }
// 统一处理:如果 / 开头,自动补全为 cnb.cool 路径
if (href && !href.startsWith('http://') && !href.startsWith('https://')) {
if (!href.startsWith('/')) href = '/' + href;
}
let img = imgInput.value.trim();
// 自动推断图片地址
if (!img && href) {
let path = href;
if (path.startsWith('https://cnb.cool') || path.startsWith('http://cnb.cool')) {
try { path = new URL(path).pathname; } catch (_) {}
}
if (path.startsWith('/')) {
const parts = path.replace(/^\/+/, '').split('/');
if (parts[0] === 'u' && parts[1]) {
img = 'https://cnb.cool/users/' + parts[1] + '/avatar/s';
} else {
const org = parts[0] || '';
img = 'https://cnb.cool/' + org + '/-/logos/s';
}
}
}
// 保存云原生开发配置(不调用接口)
const links = getQALinks();
const linkData = { href: href, img };
if (wsToggle.checked) {
let apiPath = hrefInput.value.trim().replace(/^https?:\/\/cnb\.cool/i, '');
if (apiPath && !apiPath.startsWith('/')) apiPath = '/' + apiPath;
linkData.workspace = {
api_path: apiPath,
branch: wsBranch.value.trim(),
ref: wsRef.value.trim(),
token: wsToken.value.trim()
};
}
if (isEdit) {
links[editIndex] = linkData;
} else {
links.push(linkData);
}
saveQALinks(links);
popup.remove();
renderQuickAccessBar();
}
popup.querySelector('.btn-ok').addEventListener('click', done);
popup.querySelector('.btn-cancel:not(.btn-delete)').addEventListener('click', () => popup.remove());
if (isEdit) {
popup.querySelector('.btn-delete').addEventListener('click', () => {
const links = getQALinks();
links.splice(editIndex, 1);
saveQALinks(links);
popup.remove();
renderQuickAccessBar();
});
}
// 导入数据(含收藏夹)
popup.querySelector('#qa-btn-import').addEventListener('click', async () => {
if (isCosEnabled()) {
try {
const body = await cosDownload();
const raw = typeof body === 'string' ? body : new TextDecoder().decode(body);
const data = JSON.parse(raw);
const qaList = (data.quickAccess && Array.isArray(data.quickAccess)) ? data.quickAccess : (Array.isArray(data) ? data : []);
const hasFavorites = data.favorites && Object.keys(data.favorites).length > 0;
const msg = (hasFavorites ? '将导入 ' + qaList.length + ' 条快捷导航 + 收藏夹标签数据' : '将导入 ' + qaList.length + ' 条快捷导航') + ',是否覆盖数据?';
if (await showConfirm(msg)) {
if (data.favorites) saveData(data.favorites);
saveQALinks(qaList); popup.remove(); renderQuickAccessBar(); showToast('导入成功!', 'ok');
}
} catch (e) {
if (!await showConfirm('COS 下载失败:' + e.message + '\n是否改用本地文件导入?')) return;
doLocalImport();
}
} else { doLocalImport(); }
function doLocalImport() {
const inputEl = document.createElement('input');
inputEl.type = 'file'; inputEl.accept = '.json';
inputEl.onchange = e => {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = async ev => {
try {
const data = JSON.parse(ev.target.result);
const qaList = (data.quickAccess && Array.isArray(data.quickAccess)) ? data.quickAccess : (Array.isArray(data) ? data : []);
const hasFavorites = data.favorites && Object.keys(data.favorites).length > 0;
const msg = (hasFavorites ? '将导入 ' + qaList.length + ' 条快捷导航 + 收藏夹标签数据' : '将导入 ' + qaList.length + ' 条快捷导航') + ',是否覆盖数据?';
if (await showConfirm(msg)) {
if (data.favorites) saveData(data.favorites);
saveQALinks(qaList); popup.remove(); renderQuickAccessBar(); showToast('导入成功!', 'ok');
}
} catch (err) { showToast('导入失败:JSON 格式错误', 'err'); }
};
reader.readAsText(file);
};
inputEl.click();
}
});
// 导出快捷导航数据(含收藏夹)
popup.querySelector('#qa-btn-export').addEventListener('click', async () => {
const qaLinks = getQALinks();
const data = getData();
const allData = { favorites: data, quickAccess: qaLinks };
const jsonStr = JSON.stringify(allData, null, 2);
if (isCosEnabled()) {
try { await cosUpload(jsonStr); showToast('已同步到 COS 云端(' + COS_FILE_KEY + ')', 'ok'); return; }
catch (e) { if (!await showConfirm('COS 上传失败:' + e.message + '\n是否改用本地下载?')) return; }
}
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const anchor = document.createElement('a');
anchor.href = url; anchor.download = `cnb_cool_backup_${new Date().getTime()}.json`;
document.body.appendChild(anchor); anchor.click();
document.body.removeChild(anchor); URL.revokeObjectURL(url);
});
// 所有输入框支持回车确认、Escape关闭
[hrefInput, imgInput, wsBranch, wsRef, wsToken].forEach(input => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); done(); }
else if (e.key === 'Escape') popup.remove();
});
});
setTimeout(() => {
document.addEventListener('click', function oc(e) {
if (!popup.contains(e.target) && !btn.contains(e.target)) {
popup.remove();
document.removeEventListener('click', oc);
}
});
}, 10);
hrefInput.focus();
}
// 渲染快捷导航栏
function renderQuickAccessBar() {
const existingBar = document.querySelector('.cnb-quick-access-bar');
const targetContainer = document.querySelector('div.flex.overflow-hidden.items-center.mr-10');
if (!targetContainer) return;
if (existingBar) existingBar.remove();
const bar = document.createElement('div');
bar.className = 'cnb-quick-access-bar';
const links = getQALinks();
links.forEach((link, idx) => {
const item = document.createElement('a');
item.className = 'cnb-quick-access-item';
item.target = '_blank';
item.rel = 'noopener noreferrer';
item.title = link.href + '\n左键拖拽排序 | 右键编辑/删除' + (link.workspace ? '\n云原生开发模式:点击启动云原生开发' : '');
item.innerHTML = `
`;
item.draggable = true;
item.dataset.index = idx;
// 云原生开发模式:点击时调接口获取动态URL再跳转
if (link.workspace && link.workspace.api_path) {
item.addEventListener('click', async (e) => {
e.preventDefault();
// 已在拖拽中则不处理
if (item.classList.contains('dragging')) return;
const ws = link.workspace;
let apiPath = String(ws.api_path || '').replace(/^https?:\/\/cnb\.cool/i, '');
if (!apiPath.startsWith('/')) apiPath = '/' + apiPath;
const branch = String(ws.branch || '').trim();
const ref = String(ws.ref || '').trim();
// 校验关键字段
if (apiPath === '/' || !apiPath || apiPath.includes('{') || apiPath.includes('}')) {
showToast('API 路径无效,请右键重新编辑该快捷导航', 'err');
showQAPopup(item, idx);
return;
}
// 显示加载状态
const imgEl = item.querySelector('img');
const origOpacity = imgEl ? imgEl.style.opacity : '';
if (imgEl) imgEl.style.opacity = '0.4';
try {
const bodyObj = {};
if (branch && branch !== 'string' && branch !== '请填写') bodyObj.branch = branch;
if (ref && ref !== 'string' && ref !== '请填写') bodyObj.ref = ref;
const payload = JSON.stringify(bodyObj);
const result = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: `https://api.cnb.cool${apiPath}/-/workspace/start`,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/vnd.cnb.api+json',
...(ws.token ? { 'Authorization': ws.token } : {})
},
data: payload,
onload(res) {
if (res.status >= 200 && res.status < 300) {
try { resolve(JSON.parse(res.responseText)); }
catch (_) { resolve({}); }
} else {
reject(new Error(`HTTP ${res.status}`));
}
},
onerror() { reject(new Error('网络请求失败')); }
});
});
const workspaceUrl = result.url || '';
if (!workspaceUrl) throw new Error('未返回有效的云原生开发地址');
window.open(workspaceUrl, '_blank', 'noopener');
} catch (err) {
showToast('云原生开发启动失败:' + err.message, 'err');
} finally {
if (imgEl) imgEl.style.opacity = origOpacity;
}
});
item.href = '#'; // 阻止默认链接
} else {
item.href = link.href;
}
// 拖拽事件
item.addEventListener('dragstart', e => {
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', idx);
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
bar.querySelectorAll('.cnb-drag-indicator').forEach(el => el.remove());
});
item.addEventListener('dragover', e => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const rect = item.getBoundingClientRect();
const mid = rect.left + rect.width / 2;
const isLeft = e.clientX < mid;
// 移除其他指示器
bar.querySelectorAll('.cnb-drag-indicator').forEach(el => el.remove());
// 添加指示线
const indicator = document.createElement('div');
indicator.className = 'cnb-drag-indicator';
if (isLeft) {
indicator.style.left = '-4px';
} else {
indicator.style.right = '-4px';
}
item.appendChild(indicator);
});
item.addEventListener('dragleave', e => {
if (!item.contains(e.relatedTarget)) {
item.querySelector('.cnb-drag-indicator')?.remove();
}
});
item.addEventListener('drop', e => {
e.preventDefault();
e.stopPropagation();
const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
const toIdx = parseInt(item.dataset.index);
if (fromIdx === toIdx) return;
const links = getQALinks();
const [moved] = links.splice(fromIdx, 1);
const isLeft = e.clientX < item.getBoundingClientRect().left + item.offsetWidth / 2;
const insertAt = isLeft ? toIdx : toIdx + 1;
const realInsertAt = fromIdx < insertAt ? insertAt - 1 : insertAt;
links.splice(realInsertAt, 0, moved);
saveQALinks(links);
renderQuickAccessBar();
});
// 右键删除
item.addEventListener('contextmenu', e => {
e.preventDefault();
showQAPopup(item, idx);
});
bar.appendChild(item);
});
// 添加按钮
const addBtn = document.createElement('span');
addBtn.className = 'cnb-qa-add-btn';
addBtn.textContent = '+';
addBtn.title = '添加快捷导航';
addBtn.addEventListener('click', e => { e.stopPropagation(); showQAPopup(addBtn); });
bar.appendChild(addBtn);
// 直接放到容器末尾
targetContainer.appendChild(bar);
}
// 添加快捷导航栏
function addQuickAccessBar() {
if (!isCnbDomain()) return;
const targetContainer = document.querySelector('div.flex.overflow-hidden.items-center.mr-10');
if (!targetContainer) return;
// 已存在且位置正确则跳过
const existingBar = document.querySelector('.cnb-quick-access-bar');
if (existingBar) {
if (targetContainer.contains(existingBar)) {
// 检查是否在"公开"标签后面
const publicTag = targetContainer.querySelector('span.text-ter.border-ter');
if (!publicTag || publicTag.compareDocumentPosition(existingBar) & Node.DOCUMENT_POSITION_FOLLOWING) {
return; // 位置正确
}
// "公开"已加载但bar在前面 → 移动到末尾即可,不需要重建
targetContainer.appendChild(existingBar);
return;
}
// 不在正确容器中 → 重建
existingBar.remove();
}
renderQuickAccessBar();
}
// CNB 网站功能初始化
function initCnbFeatures() {
// 检查是否在 cnb.cool 网站
if (!isCnbDomain()) return;
// 处理短链接跳转(仅执行一次)
handleCnbGotoPage();
// 添加点击事件委托(仅添加一次)
document.addEventListener('click', cnbGotoClickHandler, true);
// 统一的处理函数
const processPage = () => {
if (isCnbDomain()) {
addCreateRepoButton();
rewriteCnbGotoLinks();
enhanceGridLayout();
addInputCopyFeature();
addDownloadButtonFeature();
addQuickAccessBar();
}
};
// 等待页面加载完成
const observer = new MutationObserver(() => {
processPage();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 立即尝试添加
processPage();
// 也监听 popstate 事件(SPA 路由变化)
window.addEventListener('popstate', () => {
setTimeout(processPage, 100);
});
// 监听 visibilitychange,确保页面可见时重新处理
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
setTimeout(processPage, 100);
}
});
}
// 初始化 CNB 增强功能
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initCnbFeatures);
} else {
initCnbFeatures();
}
})();