GitHub Release Tag Navigator

Adds 'Previous Tag' and 'Next Tag' buttons to GitHub release pages for easy navigation.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         GitHub Release Tag Navigator
// @name:zh-CN   GitHub Release 标签导航
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds 'Previous Tag' and 'Next Tag' buttons to GitHub release pages for easy navigation.
// @description:zh-CN 在 GitHub 的 releases/tag 页面添加“上一个标签”和“下一个标签”按钮,方便快捷地查看 tag 的发布信息。
// @author       JIAHE
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAACEUExURUxpcRgWFhsYGBgWFhcWFh8WFhoYGBgWFiUlJRcVFRkWFhgVFRgWFhgVFRsWFhgWFigeHhkWFv////////////r6+h4eHv///xcVFfLx8SMhIUNCQpSTk/r6+jY0NCknJ97e3ru7u+fn51BOTsPCwqGgoISDg6empmpoaK2srNDQ0FhXV3eXcCcAAAAXdFJOUwCBIZXMGP70BuRH2Ze/LpIMUunHkpQR34sfygAAAVpJREFUOMt1U+magjAMDAVb5BDU3W25b9T1/d9vaYpQKDs/rF9nSNJkArDA9ezQZ8wPbc8FE6eAiQUsOO1o19JolFibKCdHGHC0IJezOMD5snx/yE+KOYYr42fPSufSZyazqDoseTPw4lGJNOu6LBXVUPBG3lqYAOv/5ZwnNUfUifzBt8gkgfgINmjxOpgqUA147QWNaocLniqq3QsSVbQHNp45N/BAwoYQz9oUJEiE4GMGfoBSMj5gjeWRIMMqleD/CAzUHFqTLyjOA5zjNnwa4UCEZ2YK3khEcBXHjVBtEFeIZ6+NxYbPqWp1DLKV42t6Ujn2ydyiPi9nX0TTNAkVVZ/gozsl6FbrktkwaVvL2TRK0C8Ca7Hck7f5OBT6FFbLATkL2ugV0tm0RLM9fedDvhWstl8Wp9AFDjFX7yOY/lJrv8AkYuz7fuP8dv9izCYH+x3/LBnj9fYPBTpJDNzX+7cAAAAASUVORK5CYII=
// @match        https://github.com/*/*/releases/tag/*
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// @license      GPL-3.0 License
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // (i) 定义一个唯一的 ID,用于清理旧按钮
    const NAV_CONTAINER_ID = 'gh-tag-nav-container';

    /**
     * 使用 GM_xmlhttpRequest 异步获取 GitHub API 数据
     * @param {string} url - API URL
     * @returns {Promise<any>} - 解析后的 JSON 数据
     */
    function fetchGitHubAPI(url) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: url,
                headers: {
                    "Accept": "application/vnd.github.v3+json",
                    "X-GitHub-Api-Version": "2022-11-28" // 指定 API 版本
                },
                onload: (response) => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(JSON.parse(response.responseText));
                    } else {
                        reject(new Error(`GitHub API 请求失败: ${response.status} ${response.statusText}`));
                    }
                },
                onerror: (error) => reject(new Error(`GM_xmlhttpRequest 错误: ${error.statusText}`))
            });
        });
    }

    /**
     * 创建一个导航按钮 (<a> 或 <span>)
     * @param {string} text - 按钮文本
     * @param {string|null} tag - 目标 tag 名称, null 则为禁用
     * @param {string} owner - 仓库所有者
     * @param {string} repo - 仓库名称
     * @returns {HTMLElement} - 创建的按钮元素
     */
    function createNavButton(text, tag, owner, repo) {
        // 如果 tag 存在, 创建 <a> 链接; 否则创建 <span> 作为占位符
        const el = document.createElement(tag ? 'a' : 'span');
        el.textContent = text;
        el.className = 'Button--secondary Button--small Button'; // 使用 GitHub 的按钮样式

        if (tag) {
            el.href = `https://github.com/${owner}/${repo}/releases/tag/${tag}`;
        } else {
            // 样式化为禁用按钮
            el.classList.add('disabled');
            el.setAttribute('aria-disabled', 'true');
        }
        return el;
    }

    /**
     * 主函数
     */
    async function main() {
        // (1) *** 优化点 ***
        // 每次 main 函数运行时, 首先移除已存在的旧按钮, 防止重复添加
        const existingNav = document.getElementById(NAV_CONTAINER_ID);
        if (existingNav) {
            existingNav.remove();
        }

        // 1. 从 URL 解析仓库信息和当前 tag
        // 匹配 /<owner>/<repo>/releases/tag/<tag_name>
        const match = window.location.pathname.match(/\/([^\/]+)\/([^\/]+)\/releases\/tag\/(.+)/);
        if (!match) {
            // 理论上 @match 会保证这一点, 但作为安全检查
            return;
        }

        const [, owner, repo, currentTag] = match;

        // 2. 找到用于注入按钮的锚点
        // 我们希望插入到 <h1> 标题容器 (包含 tag 名称和 "Latest" 徽章) 的 *前面*
        const injectionAnchor = document.querySelector('.repository-content .d-flex.mb-3 > select-panel');
        if (!injectionAnchor) {
            console.warn('GitHub 标签导航: 未找到页面锚点。GitHub UI 可能已更改。');
            return;
        }

        // 3. 获取 releases 数据
        // 我们获取最新的100个 releases。脚本假定当前 tag 在此列表中。
        // 对于有 >100 个 release 的仓库, 这可能无法找到非常旧的 tag。
        const releasesApiUrl = `https://api.github.com/repos/${owner}/${repo}/releases?per_page=100`;
        let releases;
        try {
            releases = await fetchGitHubAPI(releasesApiUrl);
        } catch (error) {
            console.error('GitHub 标签导航错误:', error);
            return; // 获取失败, 不执行任何操作
        }

        if (!releases || releases.length === 0) {
            return; // 未找到 releases
        }

        // 4. 找到上一个和下一个 tag
        const tagNames = releases.map(release => release.tag_name);
        const currentIndex = tagNames.indexOf(currentTag);

        if (currentIndex === -1) {
            console.warn(`GitHub 标签导航: 在最新的100个 release 中未找到当前 tag "${currentTag}"。`);
            return; // Tag 不在列表中, 无法导航
        }

        // Releases API 列表是按时间倒序的 (index 0 是最新的)。
        // 所以, 按时间顺序的 "上一个" (prev) 是数组中的 "下一个" (index + 1)。
        const prevTag = (currentIndex + 1 < tagNames.length) ? tagNames[currentIndex + 1] : null;
        // 按时间顺序的 "下一个" (next) 是数组中的 "上一个" (index - 1)。
        const nextTag = (currentIndex - 1 >= 0) ? tagNames[currentIndex - 1] : null;

        // 5. 创建并注入按钮
        const navContainer = document.createElement('div');
        // (2) *** 优化点 ***
        // 为容器添加唯一 ID, 以便在下次导航时找到并移除它
        navContainer.id = NAV_CONTAINER_ID;
        // 使用 GitHub 的 CSS 工具类进行布局
        navContainer.className = 'd-flex flex-justify-between mb-3';
        navContainer.style.gap = '8px'; // 在按钮之间添加一点间隙
        navContainer.style['margin-right']='8px';

        const prevButton = createNavButton('上一个标签 (Previous)', prevTag, owner, repo);
        const nextButton = createNavButton('下一个标签 (Next)', nextTag, owner, repo);

        navContainer.appendChild(prevButton);
        navContainer.appendChild(nextButton);

        // 将导航容器注入到 <h1> 容器之前
        injectionAnchor.parentNode.insertBefore(navContainer, injectionAnchor);
    }

    // 运行脚本
    main();

    // 2. *** 优化点 ***
    // 监听 GitHub 的 Turbo (SPA) 导航事件
    // 'turbo:load' 事件会在 GitHub 异步加载并替换页面内容后触发
    document.addEventListener('turbo:load', main);
})();