Better GitHub Navigation

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

נכון ליום 25-02-2026. ראה הגרסה האחרונה.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 });

})();