GitHub Release Smart Highlighter

Smartly highlight the best GitHub Release download matching your OS & architecture — combining accurate matching, conflict exclusion, and rich visual feedback.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name:zh-CN        GitHub Release 智能高亮
// @name              GitHub Release Smart Highlighter
// @namespace         https://github.com/leeflouring
// @version           1.0.0
// @description       Smartly highlight the best GitHub Release download matching your OS & architecture — combining accurate matching, conflict exclusion, and rich visual feedback.
// @description:zh-CN 智能高亮最符合你系统架构的 GitHub Release 下载——融合精准匹配、互斥排除与丰富视觉反馈,支持 macOS WebGL 架构探测、手动按钮容错。
// @author            leeflouring
// @license           MIT
// @match             https://github.com/*/*/releases/tag/*
// @match             https://github.com/*/*/releases/latest
// @match             https://github.com/*/*/releases
// @match             https://github.com/*/releases
// @match             https://github.com/*/releases/*
// ==/UserScript==

(function () {
    'use strict';

    // ═══════════════════════════════════════════════════════════
    // §1  平台探测
    // ═══════════════════════════════════════════════════════════

    function detectArchitecture(os) {
        // macOS:通过 WebGL GPU 渲染器精确区分 Apple Silicon / Intel
        if (os === 'darwin') {
            try {
                const canvas = document.createElement('canvas');
                const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
                if (gl) {
                    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
                    if (debugInfo) {
                        const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL).toLowerCase();
                        if (renderer.includes('apple') || renderer.includes('m1') || renderer.includes('m2') || renderer.includes('m3') || renderer.includes('m4')) {
                            return 'arm64';
                        }
                    }
                }
            } catch (e) {}
            return 'amd64';
        }
        // 非 macOS:从 UA 推断
        const ua = (navigator.userAgent || '').toLowerCase();
        const plat = (navigator.platform || '').toLowerCase();
        if (/aarch64|arm64/.test(ua) || /aarch64|arm64/.test(plat)) return 'arm64';
        if (/loongarch64|loong64/.test(ua)) return 'loong64';
        if (/riscv64/.test(ua)) return 'riscv64';
        if (/[-_.]arm[-_.v]/.test(ua) || /[-_.]arm$/.test(ua)) return 'arm';
        return 'amd64';
    }

    function detectPlatform() {
        const ua = navigator.userAgent || '';
        const plat = navigator.platform || '';

        let os = '';
        if (/Windows/.test(ua)) os = 'windows';
        else if (/Mac OS X|Macintosh|MacIntel|MacPPC|Mac68K/.test(ua) || /Mac/.test(plat)) os = 'darwin';
        else if (/Linux/.test(ua) || /Linux/.test(plat) || /X11/.test(ua)) os = 'linux';
        else if (/CrOS/.test(ua)) os = 'linux';

        // Windows 版本号(NT 6.1=Win7, 6.2=Win8, 6.3=Win8.1, 10+=Win10/11)
        let winVersion = null;
        if (os === 'windows') {
            const ntMatch = ua.match(/Windows NT (\d+\.\d+)/);
            if (ntMatch) {
                const ntVer = parseFloat(ntMatch[1]);
                if (ntVer <= 6.1) winVersion = '7';
                else if (ntVer <= 6.3) winVersion = '8';
                else winVersion = '10+';
            }
        }

        const arch = detectArchitecture(os);
        return { os, arch, winVersion };
    }

    // ═══════════════════════════════════════════════════════════
    // §2  匹配与评分
    // ═══════════════════════════════════════════════════════════

    // 综合忽略后缀(融合两份列表)
    const IGNORED_EXTS = /\.(sha256|sha512|sha1|md5|asc|sig|txt|dgst|sbom|pem|key|blockmap)$/;
    const IGNORED_KEYWORDS = ['source code', 'source.tar'];

    // 互斥关键词:文件名含其他平台关键词时直接排除
    const CONFLICT_KEYWORDS = {
        darwin:  ['windows', 'win64', 'win32', 'win-', 'linux', 'ubuntu', 'debian', 'rhel', 'fedora', '.rpm', '.deb'],
        windows: ['macos', 'darwin', 'osx', 'mac-', 'linux', 'ubuntu', 'debian', '.appimage', '.deb', '.rpm'],
        linux:   ['windows', 'win64', 'win32', 'win-', 'macos', 'darwin', 'osx', 'mac-', '.exe', '.msi']
    };

    // OS 专属后缀
    const OS_SPECIFIC_EXTS = {
        darwin:  ['.dmg', '.app', '.pkg'],
        windows: ['.exe', '.msi'],
        linux:   ['.appimage', '.deb', '.rpm', '.run']
    };
    // 通用后缀
    const UNIVERSAL_EXTS = ['.zip', '.tar.gz', '.tgz', '.7z', '.gz', '.xz', '.rar'];

    // 架构关键词映射
    const ARCH_KEYWORDS = {
        arm64:    ['aarch64', 'arm64', 'armv8', 'm1', 'm2', 'm3', 'm4', 'apple silicon'],
        amd64:    ['x86_64', 'amd64', 'x64', 'win64', 'intel'],
        loong64:  ['loong64', 'loongarch64'],
        riscv64:  ['riscv64'],
        x86:      ['x86', 'i386', 'i686', 'win32', '32-bit'],
        arm:      ['armv7', 'armhf', 'armv6']
    };

    function calculateScore(filename, profile) {
        const lower = filename.toLowerCase();

        // A. 忽略项
        if (IGNORED_KEYWORDS.some(k => lower.includes(k))) return -1;
        if (IGNORED_EXTS.test(lower)) return -1;

        // B. 互斥关键词排除
        if (CONFLICT_KEYWORDS[profile.os] && CONFLICT_KEYWORDS[profile.os].some(k => lower.includes(k))) return -1;

        // C. 后缀分类
        const isOsSpecific = OS_SPECIFIC_EXTS[profile.os] && OS_SPECIFIC_EXTS[profile.os].some(ext => lower.endsWith(ext));
        const isUniversal = UNIVERSAL_EXTS.some(ext => lower.endsWith(ext));
        if (!isOsSpecific && !isUniversal) return -1;

        // D. 基础分:OS专属格式权重更高
        let score = isOsSpecific ? 100 : 60;

        // E. 架构匹配加分/减分
        const isArchMatch = ARCH_KEYWORDS[profile.arch] && ARCH_KEYWORDS[profile.arch].some(k => lower.includes(k));
        const isAmd64File = ARCH_KEYWORDS.amd64.some(k => lower.includes(k));
        const isArm64File = ARCH_KEYWORDS.arm64.some(k => lower.includes(k));
        const isX86File = ARCH_KEYWORDS.x86.some(k => lower.includes(k) && !lower.includes('x86_64'));

        if (isArchMatch) {
            score += 80;                          // 完全匹配
        } else if (profile.arch === 'arm64' && isAmd64File) {
            score += 20;                          // arm64 设备跑 amd64 可用但非最优
        } else if (profile.arch === 'amd64' && isX86File) {
            score += 10;                          // amd64 设备跑 x86 可用但非最优
        } else if (profile.arch === 'amd64' && isArm64File) {
            return -1;                            // amd64 设备不能跑 arm64
        } else if (profile.arch === 'arm64' && isX86File) {
            return -1;                            // arm64 设备不能跑 x86
        } else if (!isAmd64File && !isArm64File && !isX86File) {
            score += 30;                          // 无架构标记的通用包(如 xxx.zip)
        }

        // F. 文件格式偏好微调
        if (profile.os === 'windows') {
            if (lower.endsWith('.exe')) score += 10;
            else if (lower.endsWith('.msi')) score += 5;
        }
        if (profile.os === 'darwin' && lower.endsWith('.dmg')) score += 5;
        if (profile.os === 'linux') {
            if (lower.endsWith('.deb')) score += 5;
            else if (lower.endsWith('.appimage')) score += 4;
            else if (lower.endsWith('.rpm')) score += 3;
        }

        // G. Windows 7 特殊处理
        if (profile.os === 'windows' && /win(?:dows)?[-_]?7/i.test(lower)) {
            score += (profile.winVersion === '7') ? 40 : -80;
        }

        // H. desktop 版本略降权重(通常非核心包)
        if (lower.includes('-desktop')) score -= 3;

        return score;
    }

    // ═══════════════════════════════════════════════════════════
    // §3  视觉反馈
    // ═══════════════════════════════════════════════════════════

    function injectStyles() {
        if (document.getElementById('gh-smart-style')) return;
        const style = document.createElement('style');
        style.id = 'gh-smart-style';
        style.textContent = `
            @keyframes smart-glow {
                0%,100% { box-shadow: 0 0 4px rgba(46,160,67,.3) }
                50%     { box-shadow: 0 0 16px rgba(46,160,67,.7) }
            }
            .gh-smart-highlight {
                background: rgba(46,160,67,.1) !important;
                border-left: 4px solid #2ea043 !important;
                border-radius: 6px !important;
                animation: smart-glow 2s ease-in-out 4;
                transition: background .3s ease;
            }
            .gh-smart-badge {
                display: inline-block;
                margin-left: 8px;
                padding: 2px 10px;
                font-size: 12px;
                font-weight: 600;
                color: #fff;
                background: linear-gradient(135deg, #2ea043, #238636);
                border-radius: 12px;
                vertical-align: middle;
                line-height: 18px;
                letter-spacing: .3px;
            }
            body[data-color-mode="dark"] .gh-smart-highlight {
                background: rgba(46,160,67,.18) !important;
            }
            .gh-smart-btn {
                display: inline-flex;
                align-items: center;
                gap: 4px;
                margin-left: 8px;
                padding: 2px 10px;
                font-size: 12px;
                font-weight: 600;
                color: #fff;
                background: linear-gradient(135deg, #238636, #1a7f37);
                border: 1px solid rgba(255,255,255,.15);
                border-radius: 12px;
                cursor: pointer;
                line-height: 20px;
                vertical-align: middle;
                transition: all .15s ease;
                white-space: nowrap;
            }
            .gh-smart-btn:hover {
                background: linear-gradient(135deg, #2ea043, #238636);
                box-shadow: 0 0 8px rgba(46,160,67,.4);
                transform: scale(1.05);
            }
            .gh-smart-btn:active { transform: scale(.97) }
            .gh-smart-btn.done {
                background: linear-gradient(135deg, #1a7f37, #156d2e);
                opacity: .7;
                cursor: default;
            }
            .gh-smart-btn.warn {
                background: linear-gradient(135deg, #bd561d, #9e4216);
                opacity: .8;
            }
        `;
        document.head.appendChild(style);
    }

    // ═══════════════════════════════════════════════════════════
    // §4  高亮核心逻辑(防抖 + 清除旧标记 + 评分排序)
    // ═══════════════════════════════════════════════════════════

    function clearHighlight(container) {
        const rows = container.querySelectorAll('li.Box-row');
        rows.forEach(row => {
            row.classList.remove('gh-smart-highlight');
            const badge = row.querySelector('.gh-smart-badge');
            if (badge) badge.remove();
        });
    }

    function highlightInContainer(container, shouldScroll = false) {
        const profile = detectPlatform();
        const rows = container.querySelectorAll('li.Box-row');
        if (rows.length === 0) return 0;

        clearHighlight(container);

        let bestRow = null, bestScore = -1;
        rows.forEach(row => {
            const link = row.querySelector('a');
            if (!link) return;
            const name = link.textContent.trim();
            const score = calculateScore(name, profile.os, profile.arch, profile.winVersion);
            if (score > bestScore) { bestScore = score; bestRow = row; }
        });

        if (bestRow && bestScore > 0) {
            bestRow.classList.add('gh-smart-highlight');
            const a = bestRow.querySelector('a');
            if (a && !a.querySelector('.gh-smart-badge')) {
                const badge = document.createElement('span');
                badge.className = 'gh-smart-badge';
                badge.textContent = `✔ ${profile.os} ${profile.arch}`;
                a.appendChild(badge);
            }
            // 只有用户主动操作时才滚动,避免自动监听器不断抢夺滚动位置
            if (shouldScroll) {
                bestRow.scrollIntoView({ block: 'center', behavior: 'smooth' });
            }
            syncButtonState(container);
            return rows.length;
        }
        return 0;
    }

    // ═══════════════════════════════════════════════════════════
    // §5  手动按钮
    // ═══════════════════════════════════════════════════════════

    function createManualButton(details) {
        const profile = detectPlatform();
        const btn = document.createElement('span');
        btn.className = 'gh-smart-btn';
        btn.textContent = `📥 ${profile.os}/${profile.arch}`;
        btn.title = '手动高亮推荐下载(自动检测失败时使用)';

        btn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const count = highlightInContainer(details, true);
            if (count > 0) {
                btn.textContent = '✔ Done';
                btn.classList.add('done');
            } else {
                btn.textContent = '⚠ 无匹配文件';
                btn.classList.add('warn');
                setTimeout(() => {
                    btn.textContent = `📥 ${profile.os}/${profile.arch}`;
                    btn.classList.remove('warn');
                }, 2000);
            }
        });

        return btn;
    }

    function syncButtonState(container) {
        const btn = container.querySelector('.gh-smart-btn');
        if (!btn) return;
        if (container.querySelector('.gh-smart-highlight')) {
            btn.textContent = '✔ Done';
            btn.classList.add('done');
        }
    }

    // ═══════════════════════════════════════════════════════════
    // §6  容器注册(按钮 + 多层监听 + 即时尝试)
    // ═══════════════════════════════════════════════════════════

    function setupDetails(details) {
        injectStyles();

        // 挂载手动按钮
        if (!details.querySelector('.gh-smart-btn')) {
            const summary = details.querySelector('summary');
            if (summary) {
                const assetsSpan = summary.querySelector('.f3.text-bold');
                if (assetsSpan) {
                    assetsSpan.parentElement.appendChild(createManualButton(details));
                }
            }
        }

        // 监听 details[open]
        openObserver.observe(details, { attributes: true, attributeFilter: ['open'] });

        // include-fragment 加载监听
        const frag = details.querySelector('include-fragment[src*="expanded_assets"]');
        if (frag) setupFragmentListener(frag, details);

        // 即时尝试
        highlightInContainer(details);
    }

    function setupFragmentListener(fragment, details) {
        if (details.querySelectorAll('li.Box-row').length > 0) {
            highlightInContainer(details);
            return;
        }
        fragment.addEventListener('load', () => {
            setTimeout(() => highlightInContainer(details), 100);
        });
    }

    // ═══════════════════════════════════════════════════════════
    // §7  全局监听(防抖 + 多事件覆盖)
    // ═══════════════════════════════════════════════════════════

    // details[open] 变化
    const openObserver = new MutationObserver((mutations) => {
        mutations.forEach(m => {
            const details = m.target;
            if (details.open) {
                const frag = details.querySelector('include-fragment[src*="expanded_assets"]');
                if (frag) setupFragmentListener(frag, details);
                debouncedHighlight(details, 500);
                debouncedHighlight(details, 2000);
            }
        });
    });

    // DOM 变化(防抖版)
    let domTimer = null;
    const domObserver = new MutationObserver((mutations) => {
        if (mutations.some(m => m.addedNodes.length > 0)) {
            if (domTimer) clearTimeout(domTimer);
            domTimer = setTimeout(() => {
                const allDetails = document.querySelectorAll('details[data-target="details-toggle.detailsTarget"]');
                allDetails.forEach(d => highlightInContainer(d));
            }, 500);
        }

        // 单独处理新增的 include-fragment 和 details
        mutations.forEach(m => {
            m.addedNodes.forEach(node => {
                if (node.nodeType !== 1) return;

                if (node.tagName === 'INCLUDE-FRAGMENT' &&
                    node.getAttribute('src') && node.getAttribute('src').includes('expanded_assets')) {
                    const parentDetails = node.closest('details');
                    if (parentDetails) setupFragmentListener(node, parentDetails);
                }

                if (node.tagName === 'DETAILS' && node.dataset.target === 'details-toggle.detailsTarget') {
                    setupDetails(node);
                }
            });
        });
    });

    function debouncedHighlight(container, delay) {
        setTimeout(() => highlightInContainer(container), delay);
    }

    // ═══════════════════════════════════════════════════════════
    // §8  主入口
    // ═══════════════════════════════════════════════════════════

    function init() {
        injectStyles();
        const allDetails = document.querySelectorAll('details[data-target="details-toggle.detailsTarget"]');
        allDetails.forEach(d => setupDetails(d));
    }

    init();

    // GitHub SPA 导航全覆盖
    document.addEventListener('pjax:end', init);
    document.addEventListener('turbo:load', init);
    document.addEventListener('turbo:render', init);

    // 启动 DOM 监听
    domObserver.observe(document.body, { childList: true, subtree: true });
})();