GitHub Release Highlighter

全自动高亮最符合你系统架构的 GitHub Release

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         GitHub Release Highlighter
// @namespace    http://longlone.top/
// @version      1.0
// @description  全自动高亮最符合你系统架构的 GitHub Release
// @author       Longlone & Gemini
// @license      MIT
// @match        https://github.com/*/*/releases*
// @icon         https://github.githubassets.com/favicons/favicon.svg
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // --- 1. macos 硬件探测 ---
    function detectArchitecture(os) {
        if (os !== 'darwin') {
            const ua = navigator.userAgent.toLowerCase();
            if (ua.includes('aarch64') || ua.includes('arm64')) return 'arm64';
            return 'amd64';
        }
        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')) {
                        return 'arm64';
                    }
                }
            }
        } catch (e) {}
        return 'amd64';
    }

    function getSystemProfile() {
        const ua = navigator.userAgent.toLowerCase();
        let os = '';
        if (ua.includes('win')) os = 'windows';
        else if (ua.includes('mac')) os = 'darwin';
        else if (ua.includes('linux') || ua.includes('x11')) os = 'linux';

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

    // --- 2. 评分逻辑 ---
    function calculateScore(fileName, profile) {
        if (!fileName) return -1;
        const lowerName = fileName.toLowerCase();
        const { os, arch } = profile;

        // [A] 绝对忽略列表
        const IGNORED_EXTS = ['.sig', '.asc', '.sha256', '.sha1', '.md5', '.sbom', '.pem', '.key', '.blockmap'];
        if (IGNORED_EXTS.some(ext => lowerName.endsWith(ext))) return -1;

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

        const isUniversal = UNIVERSAL_EXTS.some(ext => lowerName.endsWith(ext));
        const isOsSpecific = OS_SPECIFIC_EXTS[os] && OS_SPECIFIC_EXTS[os].some(ext => lowerName.endsWith(ext));

        if (!isUniversal && !isOsSpecific) return -1;

        // [C] 互斥关键词
        const CONFLICT_KEYWORDS = {
            darwin:  ['windows', 'win64', 'win32', 'linux', 'ubuntu', 'debian'],
            windows: ['macos', 'darwin', 'osx', 'linux', 'ubuntu', 'debian'],
            linux:   ['windows', 'win64', 'win32', 'macos', 'darwin', 'osx']
        };

        if (CONFLICT_KEYWORDS[os] && CONFLICT_KEYWORDS[os].some(k => lowerName.includes(k))) {
            return -1;
        }

        // [D] 架构匹配
        const ARCH_KEYWORDS = {
            arm64: ['aarch64', 'arm64', 'armv8', 'm1', 'm2', 'm3', 'apple silicon'],
            amd64: ['x86_64', 'amd64', 'x64', 'win64', 'intel'],
            x86:   ['x86', 'i386', 'i686', 'win32', '32-bit']
        };

        const isArm64File = ARCH_KEYWORDS.arm64.some(k => lowerName.includes(k));
        const isAmd64File = ARCH_KEYWORDS.amd64.some(k => lowerName.includes(k));
        const isX86File   = ARCH_KEYWORDS.x86.some(k => lowerName.includes(k) && !lowerName.includes('x86_64'));

        // [E] 打分
        let baseScore = isOsSpecific ? 20 : 10;

        if (arch === 'arm64') {
            if (isArm64File) return baseScore + 80;
            if (isAmd64File) return baseScore + 40;
            if (isX86File) return -1;
            return baseScore;
        } else if (arch === 'amd64') {
            if (isAmd64File) return baseScore + 80;
            if (isX86File) return baseScore + 40;
            if (isArm64File) return -1;
            return baseScore;
        }
        return 0;
    }

    // --- 3. 渲染主循环 ---
    function processAssets() {
        const container = document.querySelector('#repo-content-turbo-frame') || document.body;
        const allAssetRows = Array.from(container.querySelectorAll('.Box-row'));
        if (allAssetRows.length === 0) return;

        const profile = getSystemProfile();

        const assetGroups = new Set(allAssetRows.map(row => row.parentElement).filter(el => el));

        assetGroups.forEach(group => {
            const rows = Array.from(group.querySelectorAll('.Box-row'));
            let fileData = [];

            rows.forEach(row => {
                let fileName = '';
                const linkEl = row.querySelector('a span.truncate-start') || row.querySelector('a[href*="/download/"], a[href*="/archive/"]');

                if (linkEl) fileName = linkEl.textContent.trim();

                if (!fileName) return;
                if (fileName.toLowerCase().includes('source code')) return;

                const score = calculateScore(fileName, profile);

                const linkTarget = row.querySelector('a span.truncate-start') || row.querySelector('a');
                if (linkTarget) {
                    linkTarget.style.color = '';
                    linkTarget.style.fontWeight = '';
                    linkTarget.style.textDecoration = '';
                }

                if (score > 0) {
                    fileData.push({ row, score, fileName, linkTarget });
                }
            });

            if (fileData.length === 0) return;

            const maxScore = Math.max(...fileData.map(d => d.score));

            fileData.forEach(item => {
                // 只有最高分才高亮
                if (item.score === maxScore && item.linkTarget) {
                    item.linkTarget.style.setProperty('color', '#d29922', 'important'); // GitHub 风格的金黄色
                    item.linkTarget.style.setProperty('font-weight', 'bold', 'important');
                }
            });
        });
    }

    let timer = null;
    const run = () => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(processAssets, 500);
    };

    function init() {
        run();

        // 仅保留异步加载必须的监听
        document.addEventListener('turbo:render', run); // GitHub 页面切换
        document.addEventListener('load', (e) => {
            if (e.target.tagName === 'INCLUDE-FRAGMENT') run(); // Assets 列表延迟加载
        }, true);

        const observer = new MutationObserver((mutations) => {
            if (mutations.some(m => m.addedNodes.length > 0)) run();
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    init();
})();