GitHub 增强工具栏

在 Github 网站顶部显示 Github.dev 和 DeepWiki 和 ZreadAi 按钮,方便更好地查看代码。当按钮过多时自动切换为图标模式。

// ==UserScript==
// @name         GitHub 增强工具栏
// @namespace    https://github.com/txy-sky
// @icon         https://github.com/favicons/favicon.svg
// @version      1.5.0
// @description  在 Github 网站顶部显示 Github.dev 和 DeepWiki 和 ZreadAi 按钮,方便更好地查看代码。当按钮过多时自动切换为图标模式。
// @author       Txy-Sky
// @match        https://github.com/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 添加全局样式以修复按钮样式
    const style = `
        .custom-github-button {
            margin: 0 4px;
            display: flex;
            align-items: center;
            height: 28px;
        }
        .custom-github-button .octicon {
            margin-right: 4px;
            vertical-align: text-bottom;
        }
        .custom-github-button.icon-only .octicon {
            margin-right: 0;
        }
        .pagehead-actions > li {
            margin-right: 8px;
        }
    `;

    // 注入样式到页面
    const styleElement = document.createElement('style');
    styleElement.textContent = style;
    document.head.appendChild(styleElement);

    // 等待元素出现的函数
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const element = document.querySelector(selector);
            if (element) {
                resolve(element);
                return;
            }

            const observer = new MutationObserver((mutations, obs) => {
                const element = document.querySelector(selector);
                if (element) {
                    obs.disconnect();
                    resolve(element);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            // 超时处理
            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`Element ${selector} not found within ${timeout}ms`));
            }, timeout);
        });
    }

    // 查找按钮容器的函数,提高兼容性
    async function findButtonContainer() {
        // 尝试多个可能的选择器
        const selectors = [
            'ul.pagehead-actions',
            '.pagehead-actions',
            '.file-navigation .d-flex',
            'nav[aria-label="Repository"] .d-flex'
        ];

        for (const selector of selectors) {
            const element = document.querySelector(selector);
            if (element) return element;
        }

        return null;
    }

    // 检测现有按钮数量的函数
    function countExistingButtons(container) {
        if (!container) return 0;
        
        // 计算现有按钮数量,排除我们即将添加的按钮
        const existingButtons = container.querySelectorAll('li:not(#githubdevButton):not(#zreadaiButton):not(#deepwikiButton)');
        return existingButtons.length;
    }

    // 统一按钮创建函数,支持仅图标模式
    function createCustomButton(id, url, iconHtml, text, iconOnly = false) {
        const li = document.createElement('li');
        li.id = id;
        li.className = 'd-flex';
        li.style.marginRight = '8px';

        const a = document.createElement('a');
        a.href = url;
        a.className = iconOnly ? 'btn btn-sm custom-github-button icon-only' : 'btn btn-sm custom-github-button';
        a.target = '_blank';
        a.rel = 'noopener noreferrer';
        
        if (iconOnly) {
            a.innerHTML = iconHtml;
            a.title = text; // 添加 tooltip 显示完整文本
        } else {
            a.innerHTML = `${iconHtml}<span>${text}</span>`;
        }

        li.appendChild(a);
        return li;
    }

    // 创建 Github.dev 按钮
    function createGithubDevButton(iconOnly = false) {
        const githubdevUrl = `https://github.dev${location.pathname}${location.search}${location.hash}`;
        const iconHtml = `<img class="octicon" width="16" height="16" src="https://github.com/favicons/favicon-codespaces.svg" />`;
        return createCustomButton('githubdevButton', githubdevUrl, iconHtml, 'Github.dev', iconOnly);
    }

    // 创建 ZreadAi 按钮
    function createZreadAiButton(iconOnly = false) {
        const zreadAiUrl = `https://zread.ai${location.pathname}${location.search}${location.hash}`;
        const iconHtml = `<svg aria-hidden="true" viewBox="0 0 32 32" version="1.1" width="16" height="16" class="octicon">
            <path d="M9.91922 3.2002H4.47922C3.77229 3.2002 3.19922 3.77327 3.19922 4.4802V9.9202C3.19922 10.6271 3.77229 11.2002 4.47922 11.2002H9.91922C10.6261 11.2002 11.1992 10.6271 11.1992 9.9202V4.4802C11.1992 3.77327 10.6261 3.2002 9.91922 3.2002Z" fill="currentColor"></path>
            <path d="M9.91922 20.7998H4.47922C3.77229 20.7998 3.19922 21.3729 3.19922 22.0798V27.5198C3.19922 28.2267 3.77229 28.7998 4.47922 28.7998H9.91922C10.6261 28.7998 11.1992 28.2267 11.1992 27.5198V22.0798C11.1992 21.3729 10.6261 20.7998 9.91922 20.7998Z" fill="currentColor"></path>
            <path d="M27.5208 3.2002H22.0808C21.3739 3.2002 20.8008 3.77327 20.8008 4.4802V9.9202C20.8008 10.6271 21.3739 11.2002 22.0808 11.2002H27.5208C28.2277 11.2002 28.8008 10.6271 28.8008 9.9202V4.4802C28.8008 3.77327 28.2277 3.2002 27.5208 3.2002Z" fill="currentColor"></path>
            <path d="M8 24L24 8L8 24Z" fill="currentColor"></path>
            <path d="M8 24L24 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"></path>
        </svg>`;
        return createCustomButton('zreadaiButton', zreadAiUrl, iconHtml, 'ZreadAi', iconOnly);
    }

    // 创建 DeepWiki 按钮
    function createDeepWikiButton(iconOnly = false) {
        const deepwikiUrl = `https://deepwiki.com${location.pathname}${location.search}${location.hash}`;
        const iconHtml = `<svg class="octicon" xmlns="http://www.w3.org/2000/svg" viewBox="110 110 460 500" width="16" height="16">
            <path d="M418.73,332.37c9.84-5.68,22.07-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.58,1.06.19.06.37.11.55.16.87.21,1.76.34,2.65.35.04,0,.08.02.13.02.1,0,.19-.03.29-.04.83-.02,1.64-.13,2.45-.32.14-.03.28-.05.42-.09.87-.24,1.7-.59,2.5-1.03.08-.04.17-.06.25-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.06,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.57-1.06-.19-.06-.37-.11-.56-.16-.88-.21-1.76-.34-2.65-.34-.13,0-.26.02-.4.02-.84.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65,2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.24.1.8.44,1.64.79,2.5,1.03.14.04.28.06.42.09.81.19,1.62.3,2.45.32.1,0,.19.04.29.04.04,0,.08-.02.13-.02.89,0,1.77-.13,2.65-.35.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.58-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-.08-.04-.16-.06-.24-.1-.8-.44-1.64-.8-2.51-1.04-.13-.04-.26-.05-.39-.09-.82-.2-1.65-.31-2.49-.33-.13,0-.25-.02-.38-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.75.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.07,5.68-31.9,0-9.84-5.68-15.95-16.27-15.95-27.63s6.11-21.95,15.95-27.63Z" fill="rgb(33, 193, 154)"></path>
            <path d="M141.09,317.65l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c.08-.04.13-.11.2-.16.78-.48,1.51-1.02,2.15-1.66.1-.1.18-.21.28-.31.57-.6,1.08-1.26,1.51-1.97.07-.12.15-.22.22-.34.44-.77.77-1.6,1.03-2.47.05-.19.1-.37.14-.56.22-.89.37-1.81.37-2.76v-29.43c0-11.36,6.11-21.95,15.96-27.63s22.06-5.68,31.91,0l25.49,14.71c.82.48,1.69.8,2.57,1.06.19.06.37.11.56.16.87.21,1.76.34,2.64.35.04,0,.09.02.13.02.1,0,.19-.04.29-.04.83-.02,1.65-.13,2.45-.32.14-.03.28-.05.41-.09.87-.24,1.71-.6,2.51-1.04.08-.04.16-.06.24-.1l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-.08.04-.13.11-.2.16-.78.48-1.51,1.02-2.15,1.66-.1.1-.18.21-.28.31-.57.6-1.08,1.26-1.51,1.97-.07.12-.15.22-.22.34-.44.77-.77,1.6-1.03,2.47-.05.19-.1.37-.14.56-.22.89-.37,1.81-.37,2.76v29.43c0,11.36-6.11,21.95-15.95,27.63-9.84,5.68-22.07,5.68-31.91,0l-25.49-14.71c-.82-.48-1.69-.8-2.58-1.06-.19-.06-.37-.11-.55-.16-.88-.21-1.76-.34-2.65-.35-.13,0-.26.02-.4.02-.83.02-1.66.13-2.47.32-.13.03-.27.05-.4.09-.87.24-1.71.6-2.51,1.04-.08.04-.16.06-.24.1l-50.97,29.43c-3.65,2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22Z" fill="rgb(57, 105, 202)"></path>
            <path d="M396.88,484.35l-50.97-29.43c-.08-.04-.17-.06-.24-.1-.8-.44-1.64-.79-2.51-1.03-.14-.04-.27-.06-.41-.09-.81-.19-1.64-.3-2.47-.32-.13,0-.26-.02-.39-.02-.89,0-1.78.13-2.66.35-.18.04-.36.1-.54.15-.88.26-1.76.59-2.58,1.07l-25.49,14.72c-9.84,5.68-22.06,5.68-31.9,0-9.84-5.68-15.96-16.27-15.96-27.63v-29.43c0-.95-.15-1.87-.37-2.76-.05-.19-.09-.37-.14-.56-.25-.86-.59-1.69-1.03-2.47-.07-.12-.15-.22-.22-.34-.43-.71-.94-1.37-1.51-1.97-.1-.1-.18-.21-.28-.31-.65-.63-1.37-1.18-2.15-1.66-.07-.04-.13-.11-.2-.16l-50.97-29.43c-3.65-2.11-8.15-2.11-11.81,0l-50.97,29.43c-3.65,2.11-5.9,6.01-5.9,10.22v58.86c0,4.22,2.25,8.11,5.9,10.22l50.97,29.43c.08.04.17.06.25.1.8.44,1.63.79,2.5,1.03.14.04.29.06.43.09.8.19,1.61.3,2.43.32.1,0,.2.04.3.04.04,0,.09-.02.13-.02.88,0,1.77-.13,2.64-.34.19-.04.37-.1.56-.16.88-.26,1.75-.59,2.57-1.06l25.49-14.71c9.84-5.68,22.06-5.68,31.91,0,9.84,5.68,15.95,16.27,15.95,27.63v29.43c0,.95.15,1.87.37,2.76.05.19.09.37.14.56.25.86.59,1.69,1.03,2.47.07.12.15.22.22.34.43.71.94,1.37,1.51,1.97.1.1.18.21.28.31.65.63,1.37,1.18,2.15,1.66.07.04.13.11.2.16l50.97,29.43c1.83,1.05,3.86,1.58,5.9,1.58s4.08-.53,5.9-1.58l50.97-29.43c3.65-2.11,5.9-6.01,5.9-10.22v-58.86c0-4.22-2.25-8.11-5.9-10.22Z" fill="rgb(2, 148, 222)"></path>
        </svg>`;
        return createCustomButton('deepwikiButton', deepwikiUrl, iconHtml, 'DeepWiki', iconOnly);
    }

    // 统一处理按钮创建和添加的函数
    async function addButtons() {
        try {
            // 查找合适的按钮容器
            const buttonContainer = await findButtonContainer();
            if (!buttonContainer) {
                console.log('GitHub按钮脚本:找不到合适的按钮容器');
                return;
            }

            // 移除可能已存在的按钮
            const existingButtons = document.querySelectorAll('#githubdevButton, #zreadaiButton, #deepwikiButton');
            existingButtons.forEach(btn => btn.remove());

            // 检测现有按钮数量,决定是否使用仅图标模式
            const existingButtonCount = countExistingButtons(buttonContainer);
            const iconOnly = existingButtonCount > 3;

            // 创建按钮并添加到容器
            const deepWikiButton = createDeepWikiButton(iconOnly);
            const zreadAiButton = createZreadAiButton(iconOnly);
            const githubDevButton = createGithubDevButton(iconOnly);

            // 使用正确的顺序添加按钮,保证它们显示在最前面
            buttonContainer.insertBefore(deepWikiButton, buttonContainer.firstChild);
            buttonContainer.insertBefore(zreadAiButton, buttonContainer.firstChild);
            buttonContainer.insertBefore(githubDevButton, buttonContainer.firstChild);
        } catch (error) {
            console.log('GitHub按钮脚本:添加按钮时发生错误', error);
        }
    }

    // 防抖函数
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // 使用防抖的按钮添加函数
    const debouncedAddButtons = debounce(addButtons, 300);

    // 页面加载完成后执行
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', debouncedAddButtons);
    } else {
        // 页面已经加载完成,延迟执行以确保所有元素都渲染完成
        setTimeout(debouncedAddButtons, 100);
    }

    // 监听 PJAX/Turbo 导航事件
    document.addEventListener("pjax:end", debouncedAddButtons);
    document.addEventListener("turbo:load", debouncedAddButtons); // 增加 Turbo 事件支持

    // 监听 URL 变化
    let currentUrl = location.href;
    const observer = new MutationObserver(() => {
        if (location.href !== currentUrl) {
            currentUrl = location.href;
            setTimeout(debouncedAddButtons, 500);
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });
})();