Better GitHub Navigation

Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.

2026-02-25 기준 버전입니다. 최신 버전을 확인하세요.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Better GitHub Navigation
// @name:zh-CN   更好的 GitHub 导航栏
// @namespace    https://github.com/ImXiangYu/better-github-nav
// @version      0.1.25
// @description  Add quick access to Dashboard, Trending, Explore, Collections, and Stars from GitHub's top navigation.
// @description:zh-CN 在 GitHub 顶部导航中加入 Dashboard、Trending、Explore、Collections、Stars 快捷入口,常用页面一键直达。
// @author       Ayubass
// @license      MIT
// @match        https://github.com/*
// @icon         https://github.githubassets.com/pinned-octocat.svg
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';
    const SCRIPT_VERSION = '0.1.25';
    const CUSTOM_BUTTON_CLASS = 'custom-gh-nav-btn';
    const CUSTOM_BUTTON_ACTIVE_CLASS = 'custom-gh-nav-btn-active';
    const CUSTOM_BUTTON_COMPACT_CLASS = 'custom-gh-nav-btn-compact';
    const QUICK_LINK_MARK_ATTR = 'data-better-gh-nav-quick-link';
    const CONFIG_STORAGE_KEY = 'better-gh-nav-config-v1';
    const UI_LANG_STORAGE_KEY = 'better-gh-nav-ui-lang-v1';
    const SETTINGS_OVERLAY_ID = 'custom-gh-nav-settings-overlay';
    const SETTINGS_PANEL_ID = 'custom-gh-nav-settings-panel';
    const SETTINGS_MESSAGE_ID = 'custom-gh-nav-settings-message';
    const DEFAULT_LINK_KEYS = ['dashboard', 'explore', 'trending', 'collections', 'stars'];
    const PRESET_LINKS = [
        { key: 'dashboard', text: 'Dashboard', path: '/dashboard', getHref: () => '/dashboard' },
        { key: 'explore', text: 'Explore', path: '/explore', getHref: () => '/explore' },
        { key: 'trending', text: 'Trending', path: '/trending', getHref: () => '/trending' },
        { key: 'collections', text: 'Collections', path: '/collections', getHref: () => '/collections' },
        { key: 'stars', text: 'Stars', path: '/stars', getHref: username => (username ? `/${username}?tab=stars` : '/stars') }
    ];
    const I18N = {
        zh: {
            menuOpenSettings: 'Better GitHub Nav: 打开设置面板',
            menuResetSettings: 'Better GitHub Nav: 重置快捷链接配置',
            menuLangZh: 'Better GitHub Nav: 界面语言 -> 中文',
            menuLangEn: 'Better GitHub Nav: 界面语言 -> English',
            menuLangAuto: 'Better GitHub Nav: 界面语言 -> 自动(跟随页面)',
            resetConfirm: '确认重置快捷链接配置为默认值吗?',
            panelTitle: 'Better GitHub Nav 设置',
            panelDesc: '勾选决定显示项,拖动整行(或右侧手柄)调整显示顺序。',
            resetDefault: '恢复默认',
            cancel: '取消',
            saveAndRefresh: '保存并刷新',
            restoredPendingSave: '已恢复默认,点击保存后生效。',
            atLeastOneLink: '至少保留 1 个快捷链接。',
            dragHandleTitle: '拖动调整顺序',
            dragRowTitle: '拖动整行调整顺序'
        },
        en: {
            menuOpenSettings: 'Better GitHub Nav: Open Settings Panel',
            menuResetSettings: 'Better GitHub Nav: Reset Quick Link Config',
            menuLangZh: 'Better GitHub Nav: UI Language -> 中文',
            menuLangEn: 'Better GitHub Nav: UI Language -> English',
            menuLangAuto: 'Better GitHub Nav: UI Language -> Auto (Follow Page)',
            resetConfirm: 'Reset quick-link config to defaults?',
            panelTitle: 'Better GitHub Nav Settings',
            panelDesc: 'Select visible links and drag the row (or handle) to reorder.',
            resetDefault: 'Reset to Default',
            cancel: 'Cancel',
            saveAndRefresh: 'Save and Refresh',
            restoredPendingSave: 'Defaults restored. Click save to apply.',
            atLeastOneLink: 'Keep at least 1 quick link.',
            dragHandleTitle: 'Drag to reorder',
            dragRowTitle: 'Drag row to reorder'
        }
    };
    let settingsEscHandler = null;
    let uiLang = detectUiLang();

    function t(key, vars = {}) {
        const dict = I18N[uiLang] || I18N.en;
        const fallback = I18N.en;
        const template = dict[key] || fallback[key] || key;
        return template.replace(/\{(\w+)\}/g, (_, varName) => String(vars[varName] ?? ''));
    }

    function detectUiLang() {
        try {
            const preferredLang = (localStorage.getItem(UI_LANG_STORAGE_KEY) || '').toLowerCase();
            if (preferredLang === 'zh' || preferredLang === 'en') return preferredLang;
        } catch (e) {
            // ignore storage read failure and fallback to auto detection
        }

        const autoLang = (document.documentElement.lang || navigator.language || '').toLowerCase();
        return autoLang.startsWith('zh') ? 'zh' : 'en';
    }

    function setUiLangPreference(lang) {
        try {
            if (lang === 'zh' || lang === 'en') {
                localStorage.setItem(UI_LANG_STORAGE_KEY, lang);
            } else {
                localStorage.removeItem(UI_LANG_STORAGE_KEY);
            }
        } catch (e) {
            // ignore storage write failure; auto detection still works
        }
        uiLang = detectUiLang();
    }

    function sanitizeKeys(keys) {
        const validSet = new Set(DEFAULT_LINK_KEYS);
        const seen = new Set();
        const result = [];
        keys.forEach(key => {
            if (validSet.has(key) && !seen.has(key)) {
                seen.add(key);
                result.push(key);
            }
        });
        return result;
    }

    function sanitizeConfig(rawConfig) {
        const enabledKeys = sanitizeKeys(Array.isArray(rawConfig?.enabledKeys) ? rawConfig.enabledKeys : DEFAULT_LINK_KEYS);
        const orderKeysRaw = sanitizeKeys(Array.isArray(rawConfig?.orderKeys) ? rawConfig.orderKeys : DEFAULT_LINK_KEYS);
        const orderSet = new Set(orderKeysRaw);
        const orderKeys = [
            ...orderKeysRaw,
            ...DEFAULT_LINK_KEYS.filter(key => !orderSet.has(key))
        ];
        return {
            enabledKeys: enabledKeys.length ? enabledKeys : DEFAULT_LINK_KEYS.slice(),
            orderKeys: orderKeys.length ? orderKeys : DEFAULT_LINK_KEYS.slice()
        };
    }

    function loadConfig() {
        try {
            const raw = localStorage.getItem(CONFIG_STORAGE_KEY);
            if (!raw) return sanitizeConfig({});
            return sanitizeConfig(JSON.parse(raw));
        } catch (e) {
            return sanitizeConfig({});
        }
    }

    function saveConfig(config) {
        localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(sanitizeConfig(config)));
    }

    function getConfiguredLinks(username) {
        const config = loadConfig();
        const presetMap = new Map(
            PRESET_LINKS.map(link => [link.key, {
                ...link,
                id: `custom-gh-btn-${link.key}`,
                href: link.getHref(username)
            }])
        );
        return config.orderKeys
            .filter(key => config.enabledKeys.includes(key))
            .map(key => presetMap.get(key))
            .filter(Boolean);
    }

    function getDisplayNameByKey(key) {
        const link = PRESET_LINKS.find(item => item.key === key);
        return link ? link.text : key;
    }

    function closeConfigPanel() {
        const overlay = document.getElementById(SETTINGS_OVERLAY_ID);
        if (overlay) overlay.remove();
        if (settingsEscHandler) {
            document.removeEventListener('keydown', settingsEscHandler);
            settingsEscHandler = null;
        }
    }

    function createPanelState(config) {
        const safeConfig = sanitizeConfig(config);
        return {
            order: safeConfig.orderKeys.slice(),
            enabledSet: new Set(safeConfig.enabledKeys)
        };
    }

    function reorderKeys(state, draggedKey, targetKey, placeAfter = false) {
        const fromIndex = state.order.indexOf(draggedKey);
        const targetIndex = state.order.indexOf(targetKey);
        if (fromIndex < 0 || targetIndex < 0 || fromIndex === targetIndex) return false;

        const [movedKey] = state.order.splice(fromIndex, 1);
        let insertIndex = targetIndex + (placeAfter ? 1 : 0);
        if (fromIndex < targetIndex) {
            insertIndex -= 1;
        }
        state.order.splice(insertIndex, 0, movedKey);
        return true;
    }

    function clearDragClasses(listEl) {
        const rows = listEl.querySelectorAll('.custom-gh-nav-settings-row');
        rows.forEach(row => {
            row.classList.remove('custom-gh-nav-settings-row-dragging');
            row.classList.remove('custom-gh-nav-settings-row-drag-over');
        });
    }

    function renderPanelRows(listEl, state) {
        listEl.replaceChildren();
        state.order.forEach(key => {
            const row = document.createElement('div');
            row.className = 'custom-gh-nav-settings-row';
            row.draggable = true;
            row.title = t('dragRowTitle');
            row.dataset.rowKey = key;

            const left = document.createElement('label');
            left.className = 'custom-gh-nav-settings-row-left';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = state.enabledSet.has(key);
            checkbox.addEventListener('change', () => {
                if (checkbox.checked) {
                    state.enabledSet.add(key);
                } else {
                    state.enabledSet.delete(key);
                }
            });

            const text = document.createElement('span');
            text.textContent = `${getDisplayNameByKey(key)} (${key})`;

            left.appendChild(checkbox);
            left.appendChild(text);

            const actions = document.createElement('div');
            actions.className = 'custom-gh-nav-settings-row-actions';

            const dragHandle = document.createElement('span');
            dragHandle.className = 'custom-gh-nav-settings-drag-handle';
            dragHandle.textContent = '≡';
            dragHandle.title = t('dragHandleTitle');
            dragHandle.setAttribute('aria-hidden', 'true');

            row.addEventListener('dragstart', event => {
                row.classList.add('custom-gh-nav-settings-row-dragging');
                listEl.dataset.dragKey = key;
                if (event.dataTransfer) {
                    event.dataTransfer.effectAllowed = 'move';
                    event.dataTransfer.setData('text/plain', key);
                }
            });
            row.addEventListener('dragend', () => {
                delete listEl.dataset.dragKey;
                clearDragClasses(listEl);
            });

            row.addEventListener('dragover', event => {
                event.preventDefault();
                row.classList.add('custom-gh-nav-settings-row-drag-over');
                if (event.dataTransfer) {
                    event.dataTransfer.dropEffect = 'move';
                }
            });
            row.addEventListener('dragleave', () => {
                row.classList.remove('custom-gh-nav-settings-row-drag-over');
            });
            row.addEventListener('drop', event => {
                event.preventDefault();
                row.classList.remove('custom-gh-nav-settings-row-drag-over');

                const draggedKey = (event.dataTransfer && event.dataTransfer.getData('text/plain'))
                    || listEl.dataset.dragKey
                    || '';
                if (!draggedKey || draggedKey === key) return;

                const rect = row.getBoundingClientRect();
                const placeAfter = event.clientY > rect.top + rect.height / 2;
                if (reorderKeys(state, draggedKey, key, placeAfter)) {
                    renderPanelRows(listEl, state);
                }
            });

            actions.appendChild(dragHandle);
            row.appendChild(left);
            row.appendChild(actions);
            listEl.appendChild(row);
        });
    }

    function openConfigPanel() {
        closeConfigPanel();
        ensureStyles();

        const state = createPanelState(loadConfig());
        const overlay = document.createElement('div');
        overlay.id = SETTINGS_OVERLAY_ID;

        const panel = document.createElement('div');
        panel.id = SETTINGS_PANEL_ID;

        const title = document.createElement('h3');
        title.className = 'custom-gh-nav-settings-title';
        title.textContent = t('panelTitle');

        const desc = document.createElement('p');
        desc.className = 'custom-gh-nav-settings-desc';
        desc.textContent = t('panelDesc');

        const list = document.createElement('div');
        list.className = 'custom-gh-nav-settings-list';
        renderPanelRows(list, state);

        const message = document.createElement('div');
        message.id = SETTINGS_MESSAGE_ID;
        message.className = 'custom-gh-nav-settings-message';
        message.setAttribute('role', 'status');
        message.setAttribute('aria-live', 'polite');

        const footer = document.createElement('div');
        footer.className = 'custom-gh-nav-settings-footer';

        const resetBtn = document.createElement('button');
        resetBtn.type = 'button';
        resetBtn.className = 'custom-gh-nav-settings-btn';
        resetBtn.textContent = t('resetDefault');
        resetBtn.addEventListener('click', () => {
            state.order = DEFAULT_LINK_KEYS.slice();
            state.enabledSet = new Set(DEFAULT_LINK_KEYS);
            renderPanelRows(list, state);
            message.textContent = t('restoredPendingSave');
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.type = 'button';
        cancelBtn.className = 'custom-gh-nav-settings-btn';
        cancelBtn.textContent = t('cancel');
        cancelBtn.addEventListener('click', closeConfigPanel);

        const saveBtn = document.createElement('button');
        saveBtn.type = 'button';
        saveBtn.className = 'custom-gh-nav-settings-btn custom-gh-nav-settings-btn-primary';
        saveBtn.textContent = t('saveAndRefresh');
        saveBtn.addEventListener('click', () => {
            const enabledKeys = state.order.filter(key => state.enabledSet.has(key));
            if (!enabledKeys.length) {
                message.textContent = t('atLeastOneLink');
                return;
            }
            saveConfig({
                enabledKeys,
                orderKeys: state.order.slice()
            });
            closeConfigPanel();
            location.reload();
        });

        footer.appendChild(resetBtn);
        footer.appendChild(cancelBtn);
        footer.appendChild(saveBtn);

        panel.appendChild(title);
        panel.appendChild(desc);
        panel.appendChild(list);
        panel.appendChild(message);
        panel.appendChild(footer);
        overlay.appendChild(panel);

        overlay.addEventListener('click', event => {
            if (event.target === overlay) closeConfigPanel();
        });

        settingsEscHandler = event => {
            if (event.key === 'Escape') closeConfigPanel();
        };
        document.addEventListener('keydown', settingsEscHandler);

        document.body.appendChild(overlay);
    }

    function registerConfigMenu() {
        if (typeof GM_registerMenuCommand !== 'function') return;
        GM_registerMenuCommand(t('menuOpenSettings'), openConfigPanel);

        GM_registerMenuCommand(t('menuResetSettings'), () => {
            const shouldReset = confirm(t('resetConfirm'));
            if (!shouldReset) return;
            localStorage.removeItem(CONFIG_STORAGE_KEY);
            closeConfigPanel();
            location.reload();
        });

        GM_registerMenuCommand(t('menuLangZh'), () => {
            setUiLangPreference('zh');
            closeConfigPanel();
            location.reload();
        });

        GM_registerMenuCommand(t('menuLangEn'), () => {
            setUiLangPreference('en');
            closeConfigPanel();
            location.reload();
        });

        GM_registerMenuCommand(t('menuLangAuto'), () => {
            setUiLangPreference('auto');
            closeConfigPanel();
            location.reload();
        });
    }

    function normalizePath(href) {
        try {
            const url = new URL(href, location.origin);
            const path = url.pathname.replace(/\/+$/, '');
            return path || '/';
        } catch (e) {
            return '';
        }
    }

    function isCurrentPage(linkPath) {
        const currentPath = location.pathname.replace(/\/+$/, '') || '/';
        if (linkPath === '/dashboard') return currentPath === '/' || currentPath === '/dashboard';
        if (currentPath === linkPath) return true;
        if (linkPath !== '/' && currentPath.startsWith(`${linkPath}/`)) return true;

        // Stars 页面常见为 /<username>?tab=stars
        return location.search.includes('tab=stars') && linkPath === normalizePath('/stars');
    }

    function ensureStyles() {
        if (document.getElementById('custom-gh-nav-style')) return;
        const style = document.createElement('style');
        style.id = 'custom-gh-nav-style';
        style.textContent = `
            a.${CUSTOM_BUTTON_CLASS} {
                border-radius: 6px;
                padding-inline: 8px;
                text-decoration: none;
            }
            a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_COMPACT_CLASS} {
                padding-inline: 4px;
            }
            a.${CUSTOM_BUTTON_CLASS},
            a.${CUSTOM_BUTTON_CLASS} span {
                font-weight: 600;
            }
            a.${CUSTOM_BUTTON_CLASS},
            a.${CUSTOM_BUTTON_CLASS} * {
                cursor: pointer;
            }
            a.${CUSTOM_BUTTON_CLASS}:hover {
                background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.12));
                text-decoration: none;
            }
            a.${CUSTOM_BUTTON_CLASS}.${CUSTOM_BUTTON_ACTIVE_CLASS} {
                background-color: var(--color-neutral-muted, rgba(177, 186, 196, 0.18));
                font-weight: 600;
            }
            #${SETTINGS_OVERLAY_ID} {
                position: fixed;
                inset: 0;
                z-index: 2147483647;
                background: rgba(0, 0, 0, 0.45);
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 16px;
                box-sizing: border-box;
            }
            #${SETTINGS_PANEL_ID} {
                width: min(560px, 100%);
                max-height: min(80vh, 720px);
                overflow: auto;
                background: var(--color-canvas-default, #fff);
                color: var(--color-fg-default, #1f2328);
                border: 1px solid var(--color-border-default, #d1d9e0);
                border-radius: 10px;
                box-shadow: 0 16px 40px rgba(0, 0, 0, 0.25);
                padding: 16px;
                box-sizing: border-box;
            }
            .custom-gh-nav-settings-title {
                margin: 0 0 8px;
                font-size: 16px;
                line-height: 1.4;
            }
            .custom-gh-nav-settings-desc {
                margin: 0 0 12px;
                color: var(--color-fg-muted, #656d76);
                font-size: 13px;
            }
            .custom-gh-nav-settings-list {
                display: flex;
                flex-direction: column;
                gap: 8px;
            }
            .custom-gh-nav-settings-row {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 12px;
                border: 1px solid var(--color-border-muted, #d8dee4);
                border-radius: 8px;
                padding: 8px 10px;
                background: var(--color-canvas-subtle, #f6f8fa);
                cursor: grab;
            }
            .custom-gh-nav-settings-row:active {
                cursor: grabbing;
            }
            .custom-gh-nav-settings-row-left {
                display: inline-flex;
                align-items: center;
                gap: 8px;
                user-select: none;
                font-size: 13px;
            }
            .custom-gh-nav-settings-row-left input {
                cursor: pointer;
            }
            .custom-gh-nav-settings-row-actions {
                display: inline-flex;
                align-items: center;
                gap: 6px;
            }
            .custom-gh-nav-settings-drag-handle {
                border: 1px solid var(--color-border-default, #d1d9e0);
                background: var(--color-btn-bg, #f6f8fa);
                color: var(--color-fg-muted, #656d76);
                border-radius: 6px;
                width: 32px;
                height: 26px;
                line-height: 1;
                font-size: 16px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                user-select: none;
                pointer-events: none;
            }
            .custom-gh-nav-settings-row-dragging {
                opacity: 0.55;
            }
            .custom-gh-nav-settings-row-drag-over {
                border-color: var(--color-accent-fg, #0969da);
                background: var(--color-accent-subtle, #ddf4ff);
            }
            .custom-gh-nav-settings-btn {
                border: 1px solid var(--color-border-default, #d1d9e0);
                background: var(--color-btn-bg, #f6f8fa);
                color: var(--color-fg-default, #1f2328);
                border-radius: 6px;
                padding: 4px 10px;
                font-size: 12px;
                cursor: pointer;
            }
            .custom-gh-nav-settings-btn:hover {
                background: var(--color-btn-hover-bg, #f3f4f6);
            }
            .custom-gh-nav-settings-btn:disabled {
                opacity: 0.45;
                cursor: not-allowed;
            }
            .custom-gh-nav-settings-btn-primary {
                background: var(--color-btn-primary-bg, #1f883d);
                border-color: var(--color-btn-primary-bg, #1f883d);
                color: var(--color-btn-primary-text, #fff);
            }
            .custom-gh-nav-settings-btn-primary:hover {
                background: var(--color-btn-primary-hover-bg, #1a7f37);
            }
            .custom-gh-nav-settings-footer {
                margin-top: 12px;
                display: flex;
                justify-content: flex-end;
                gap: 8px;
            }
            .custom-gh-nav-settings-message {
                min-height: 20px;
                margin-top: 8px;
                color: var(--color-attention-fg, #9a6700);
                font-size: 12px;
            }
        `;
        document.head.appendChild(style);
    }

    function setActiveStyle(aTag, active, compact = false) {
        aTag.classList.add(CUSTOM_BUTTON_CLASS);
        if (compact) {
            aTag.classList.add(CUSTOM_BUTTON_COMPACT_CLASS);
        } else {
            aTag.classList.remove(CUSTOM_BUTTON_COMPACT_CLASS);
        }
        if (active) {
            aTag.setAttribute('aria-current', 'page');
            aTag.classList.add(CUSTOM_BUTTON_ACTIVE_CLASS);
        } else {
            aTag.removeAttribute('aria-current');
            aTag.classList.remove(CUSTOM_BUTTON_ACTIVE_CLASS);
        }
    }

    function setLinkText(aTag, text) {
        const innerSpan = aTag.querySelector('span');
        if (innerSpan) {
            innerSpan.textContent = text;
        } else {
            aTag.textContent = text;
        }
    }

    function ensureAnchor(node, isLiParent) {
        let aTag = isLiParent ? node.querySelector('a') : (node.tagName.toLowerCase() === 'a' ? node : node.querySelector('a'));
        if (aTag) return aTag;

        const fallbackText = (node.textContent || '').trim();
        const fallbackHref = (!isLiParent && node.getAttribute && node.getAttribute('href'))
            ? node.getAttribute('href')
            : `${location.pathname}${location.search}`;
        const classSource = isLiParent
            ? node.querySelector('[class*="contextCrumb"], [class*="Breadcrumbs-Item"]')
            : node;
        const spanTemplate = document.querySelector(
            'header a[class*="contextCrumb"] span[class*="contextCrumbLast"]'
        );
        const spanSource = isLiParent ? node.querySelector('span') : node.querySelector('span');

        aTag = document.createElement('a');
        if (classSource && classSource.className) {
            aTag.className = classSource.className
                .split(/\s+/)
                .filter(cls => cls && !cls.includes('contextCrumbStatic'))
                .join(' ');
        }
        if (spanSource && spanSource.className) {
            const innerSpan = document.createElement('span');
            innerSpan.className = spanTemplate && spanTemplate.className
                ? spanTemplate.className
                : spanSource.className;
            if (fallbackText) innerSpan.textContent = fallbackText;
            aTag.appendChild(innerSpan);
        }
        if (!aTag.getAttribute('href') && fallbackHref) {
            aTag.setAttribute('href', fallbackHref);
        }
        if (!aTag.textContent.trim() && fallbackText) {
            const innerSpan = aTag.querySelector('span');
            if (innerSpan) {
                innerSpan.textContent = fallbackText;
            } else {
                aTag.textContent = fallbackText;
            }
        }

        if (isLiParent) {
            node.textContent = '';
            node.appendChild(aTag);
        } else {
            node.replaceChildren(aTag);
        }
        return aTag;
    }

    function getAnchorHostNode(anchor) {
        if (!anchor || !anchor.parentNode) return anchor;
        return anchor.parentNode.tagName.toLowerCase() === 'li' ? anchor.parentNode : anchor;
    }

    function cleanupQuickLinksForContainer(renderParent, keepNode) {
        const quickAnchors = Array.from(
            document.querySelectorAll(
                'header a[id^="custom-gh-btn-"], header a[' + QUICK_LINK_MARK_ATTR + '="1"]'
            )
        );

        quickAnchors.forEach(anchor => {
            const host = getAnchorHostNode(anchor);
            if (!host || !host.parentNode) return;
            if (host === keepNode) return;
            if (host.parentNode !== renderParent) {
                host.remove();
                return;
            }
            host.remove();
        });
    }

    function addCustomButtons() {
        // 获取当前登录的用户名,用来动态生成 Stars 页面的专属链接
        const userLoginMeta = document.querySelector('meta[name="user-login"]');
        const username = userLoginMeta ? userLoginMeta.getAttribute('content') : '';
        const navPresetLinks = getConfiguredLinks(username);
        if (!navPresetLinks.length) return;
        const primaryLink = navPresetLinks[0];
        const extraLinks = navPresetLinks.slice(1);
        const fixedPages = new Set(['/dashboard', '/trending', '/explore', '/collections']);
        const shortcutPaths = new Set(PRESET_LINKS.map(link => link.path));
        const compactPages = new Set(['/issues', '/pulls', '/repositories']);

        const isOnPresetPage = Array.from(fixedPages).some(path => isCurrentPage(path));
        const shouldUseCompactButtons = Array.from(compactPages).some(path => isCurrentPage(path));

        // 预设页面优先主导航;其他页面优先 breadcrumb/context crumb 的最后一项(如仓库名)
        let targetNode = null;
        let targetSource = '';
        if (isOnPresetPage) {
            targetNode = document.querySelector(
                'header nav a[href="/dashboard"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                'header nav a[href="/trending"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                'header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
            );
            if (targetNode) targetSource = 'preset-nav';
            if (!targetNode) {
                targetNode = document.querySelector(
                    'header nav a[id^="custom-gh-btn-"], header nav a[' + QUICK_LINK_MARK_ATTR + '="1"]'
                );
                if (targetNode) targetSource = 'preset-quick';
            }
        } else {
            const breadcrumbNodes = Array.from(document.querySelectorAll(
                'header nav[aria-label*="breadcrumb" i] a[href^="/"], ' +
                'header a[class*="contextCrumb"][href^="/"], ' +
                'header a[class*="Breadcrumbs-Item"][href^="/"]'
            )).filter(link => {
                if (link.id && link.id.startsWith('custom-gh-btn-')) return false;
                if (link.getAttribute(QUICK_LINK_MARK_ATTR) === '1') return false;
                const href = normalizePath(link.getAttribute('href') || '');
                if (!href || href === '/') return false;
                if (shortcutPaths.has(href)) return false;
                return true;
            });
            if (breadcrumbNodes.length) {
                targetNode = breadcrumbNodes[breadcrumbNodes.length - 1];
                targetSource = 'breadcrumb';
            }
        }

        // 全局导航中优先使用当前页项,避免误选最后一个导航按钮导致当前页无高亮
        if (!targetNode) {
            targetNode = document.querySelector(
                'header nav a[aria-current="page"]:not([id^="custom-gh-btn-"]), ' +
                'header nav a[data-active="true"]:not([id^="custom-gh-btn-"]), ' +
                'header nav [aria-current="page"]:not(a), ' +
                'header nav [data-active="true"]:not(a)'
            );
            if (targetNode) targetSource = 'current-nav';
        }

        // 兼容兜底:若未找到主导航,再尝试旧规则
        if (!targetNode) {
            const navLinks = document.querySelectorAll(
                'header a:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
            );
            for (let link of navLinks) {
                const text = link.textContent.trim().toLowerCase();
                const href = link.getAttribute('href');
                if (text === 'dashboard' || href === '/dashboard') {
                    targetNode = link;
                    targetSource = 'legacy-dashboard';
                    break;
                }
            }
        }

        // 通用兜底:在有全局导航的页面(如 /pulls /issues /repositories)优先按当前路径匹配
        if (!targetNode) {
            const currentPath = location.pathname.replace(/\/+$/, '') || '/';
            const globalNavCandidates = Array.from(
                document.querySelectorAll(
                    'header nav[aria-label*="global" i] a[href^="/"], ' +
                    'header nav[aria-label*="header" i] a[href^="/"], ' +
                    'header nav a[href="/pulls"], ' +
                    'header nav a[href="/issues"], ' +
                    'header nav a[href="/repositories"], ' +
                    'header nav a[href="/codespaces"], ' +
                    'header nav a[href="/marketplace"], ' +
                    'header nav a[href="/explore"]'
                )
            ).filter(link => {
                const href = normalizePath(link.getAttribute('href') || '');
                if (!href || href === '/') return false;
                if (link.id && link.id.startsWith('custom-gh-btn-')) return false;
                if (link.getAttribute(QUICK_LINK_MARK_ATTR) === '1') return false;
                return true;
            });
            if (globalNavCandidates.length) {
                targetNode = globalNavCandidates.find(link => {
                    const href = normalizePath(link.getAttribute('href') || '');
                    return href === currentPath;
                }) || globalNavCandidates[globalNavCandidates.length - 1];
                if (targetNode) targetSource = 'global-nav';
            }
        }

        // 文本型当前项兜底:部分页面当前导航项是不可点击文本(非 a)
        if (!targetNode) {
            const currentTextNode = document.querySelector(
                'header nav [aria-current="page"]:not(a), ' +
                'header nav [data-active="true"]:not(a)'
            );
            if (currentTextNode) {
                targetNode = currentTextNode;
                targetSource = 'current-text';
            }
        }

        // context crumb 文本项兜底:如 Issues/PRs 页为 span 而非 a
        if (!targetNode) {
            const contextCrumbTextNodes = document.querySelectorAll(
                'header span[class*="contextCrumbStatic"], ' +
                'header span[class*="contextCrumb"][class*="Breadcrumbs-Item"], ' +
                'header .prc-Breadcrumbs-Item-jcraJ'
            );
            if (contextCrumbTextNodes.length) {
                targetNode = contextCrumbTextNodes[contextCrumbTextNodes.length - 1];
                targetSource = 'crumb-text';
            }
        }

        // 样式模板优先使用同容器内可点击链接,避免从纯文本节点克隆导致样式不一致
        let templateNode = targetNode;
        if (targetNode) {
            const localNav = targetNode.closest('nav, ul, ol');
            const localAnchors = localNav
                ? localNav.querySelectorAll(
                    'a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
                )
                : [];

            if (localAnchors.length) {
                templateNode = localAnchors[localAnchors.length - 1];
            } else {
                const nativeNavAnchors = document.querySelectorAll(
                    'header nav[aria-label*="breadcrumb" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header a[class*="contextCrumb"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header a[class*="Breadcrumbs-Item"][href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav[aria-label*="global" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav[aria-label*="header" i] a[href^="/"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/pulls"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/issues"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/repositories"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/codespaces"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/marketplace"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"]), ' +
                    'header nav a[href="/explore"]:not([id^="custom-gh-btn-"]):not([' + QUICK_LINK_MARK_ATTR + '="1"])'
                );
                if (nativeNavAnchors.length) {
                    templateNode = nativeNavAnchors[nativeNavAnchors.length - 1];
                }
            }
        }

        if (targetNode) {
            // targetNode 用于决定插入位置,templateNode 用于克隆样式
            const isTargetLiParent = targetNode.parentNode.tagName.toLowerCase() === 'li';
            const insertAnchorNode = isTargetLiParent ? targetNode.parentNode : targetNode;
            const isTemplateLiParent = templateNode.parentNode.tagName.toLowerCase() === 'li';
            const cloneTemplateNode = isTemplateLiParent ? templateNode.parentNode : templateNode;
            const targetHasAnchor = isTargetLiParent
                ? Boolean(insertAnchorNode.querySelector('a'))
                : insertAnchorNode.tagName.toLowerCase() === 'a' || Boolean(insertAnchorNode.querySelector('a'));
            const shouldForceCreateAnchor = !targetHasAnchor && Boolean(targetNode.closest('header nav'));
            const anchorTag = (targetHasAnchor || shouldForceCreateAnchor)
                ? ensureAnchor(insertAnchorNode, isTargetLiParent)
                : null;
            cleanupQuickLinksForContainer(insertAnchorNode.parentNode, insertAnchorNode);

            const hasShortcutActive = navPresetLinks.some(link => isCurrentPage(link.path));

            if (isOnPresetPage && anchorTag && primaryLink) {
                // 预设页面:首个按钮替换为当前配置顺序中的第一个
                anchorTag.id = primaryLink.id;
                anchorTag.setAttribute(QUICK_LINK_MARK_ATTR, '1');
                anchorTag.href = primaryLink.href;
                setLinkText(anchorTag, primaryLink.text);
                setActiveStyle(anchorTag, isCurrentPage(primaryLink.path), shouldUseCompactButtons);
            } else {
                // 其他页面:保留原生当前按钮,仅做高亮
                if (anchorTag && anchorTag.id && anchorTag.id.startsWith('custom-gh-btn-')) {
                    anchorTag.removeAttribute('id');
                }
                if (anchorTag) {
                    anchorTag.removeAttribute(QUICK_LINK_MARK_ATTR);
                }
                // 若快捷按钮已有命中(如 Stars 页),则避免双高亮
                if (anchorTag) {
                    setActiveStyle(anchorTag, !hasShortcutActive, shouldUseCompactButtons);
                }
            }
            
            // 设定插入的锚点,随着循环不断向后移动,保证按钮顺序正确
            let insertAfterNode = insertAnchorNode;
            const linksToRender = isOnPresetPage ? extraLinks : navPresetLinks;

            linksToRender.forEach(linkInfo => {
                const newNode = cloneTemplateNode.cloneNode(true);
                const aTag = ensureAnchor(newNode, isTemplateLiParent);
                
                aTag.id = linkInfo.id;
                aTag.setAttribute(QUICK_LINK_MARK_ATTR, '1');
                aTag.href = linkInfo.href;
                setLinkText(aTag, linkInfo.text);

                setActiveStyle(aTag, isCurrentPage(linkInfo.path), shouldUseCompactButtons);

                // 将新按钮插入到锚点之后,并更新锚点
                insertAfterNode.parentNode.insertBefore(newNode, insertAfterNode.nextSibling);
                insertAfterNode = newNode; 
            });
        }
    }

    // 1. 页面初次加载时执行
    console.info(`[Better GitHub Navigation] loaded v${SCRIPT_VERSION}`);
    window.__betterGithubNavVersion = SCRIPT_VERSION;
    window.__openBetterGithubNavSettings = openConfigPanel;
    registerConfigMenu();
    ensureStyles();
    addCustomButtons();

    // 2. 监听 GitHub 的 Turbo/PJAX 页面跳转事件,防止切换页面后按钮消失
    document.addEventListener('turbo:load', addCustomButtons);
    document.addEventListener('pjax:end', addCustomButtons);

    // 3. 终极备用方案:使用 MutationObserver 监听 DOM 变化
    const observer = new MutationObserver(() => {
        if (!document.querySelector('[id^="custom-gh-btn-"]') && document.querySelector('header')) {
            addCustomButtons();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();