// ==UserScript== // @name CNB Cool 增强 // @namespace https://cnb.cool/IIIStudio/Code/Greasemonkey/CNB // @version 1.3 // @description CNB.Cool 综合增强工具:直达链接解码、网格布局切换、收藏夹标签管理、与我有关标签、创建仓库按钮、输入框复制、快捷导航栏(支持拖拽排序/右键编辑/导入导出)、云原生开发模式(点击头像启动)等功能 // @author IIIStudio // @match *://cnb.cool/* // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @connect api.cnb.cool // @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; } `; 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; } 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 备份与还原 ===== function exportJSON() { const data = getData(); const qaLinks = getQALinks(); const allData = { favorites: data, quickAccess: qaLinks }; const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(allData, null, 2)); 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(); } function importJSON() { 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 = event => { try { const parsedData = JSON.parse(event.target.result); if (confirm('警告:导入备份将覆盖当前的标签、收藏与导航栏数据,是否确认继续?')) { // 兼容旧格式(直接是 favorites 对象) if (parsedData.favorites) { saveData(parsedData.favorites); } else { saveData(parsedData); } if (parsedData.quickAccess && Array.isArray(parsedData.quickAccess)) { saveQALinks(parsedData.quickAccess); } alert('✅ 数据导入成功!页面即将刷新。'); location.reload(); } } catch (err) { alert('❌ 解析失败,请确保上传的是格式正确的 JSON 备份文件!'); } }; reader.readAsText(file); }; input.click(); } 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 = '
'; html += ''; for (const tag of allTags) { html += ``; } 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.innerHTML = `${tag} ×`; // 确保插在最前面(+号按钮之前) 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 label = document.createElement('label'); label.tabIndex = '0'; label.className = 't-radio-button cnb-fav-filter-tag'; label.setAttribute('data-tag', tag); label.innerHTML = ` ${tag}`; 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); } // 为下载按钮添加点击下载功能和复制直链按钮 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'; 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 = `
CNB ${isEdit ? '编辑快捷导航' : '添加快捷导航'}v${SCRIPT_VERSION} 云原生开发
`; 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 = '显示'; } }); // 云原生开发模式下,链接地址字段变为 API 路径 function syncHrefField() { if (wsToggle.checked) { hrefLabel.textContent = '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; } } 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', () => { 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 = ev => { try { const data = JSON.parse(ev.target.result); let qaList = []; if (data.quickAccess && Array.isArray(data.quickAccess)) { qaList = data.quickAccess; } else if (Array.isArray(data)) { qaList = data; } if (confirm(`将导入 ${qaList.length} 条快捷导航,是否覆盖当前数据?`)) { saveQALinks(qaList); popup.remove(); renderQuickAccessBar(); alert('导入成功!'); } } catch (err) { alert('导入失败:JSON 格式错误'); } }; reader.readAsText(file); }; inputEl.click(); }); // 导出快捷导航数据 popup.querySelector('#qa-btn-export').addEventListener('click', () => { const qaLinks = getQALinks(); const jsonStr = JSON.stringify(qaLinks, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = `cnb_quick_access_${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 = `${link.href.replace(/^\//, '')}`; 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('}')) { alert('API 路径无效,请右键重新编辑该快捷导航'); 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) { alert('云原生开发启动失败:' + err.message); } 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(); } })();