Raw Source
HaleShaw / GitHub镜像

// ==UserScript==
// @name               GitHub镜像
// @name:en            GitHub Mirror
// @description        GitHub镜像,加速访问GitHub,支持Clone、Release、Raw、Zip加速。
// @description:en     GitHub mirror. Accelerate access to GitHub. Support Clone, Release, RAW and ZIP acceleration.
// @namespace          https://github.com/HaleShaw
// @version            1.3.0
// @author             HaleShaw
// @copyright          2021+, HaleShaw (https://github.com/HaleShaw)
// @license            AGPL-3.0-or-later
// @homepage           https://github.com/HaleShaw/TM-GitHubMirror
// @supportURL         https://github.com/HaleShaw/TM-GitHubMirror/issues
// @downloadURL        https://raw.githubusercontent.com/HaleShaw/TM-GitHubMirror/master/GitHubMirror.user.js
// @updateURL          https://raw.githubusercontent.com/HaleShaw/TM-GitHubMirror/master/GitHubMirror.user.js
// @contributionURL    https://www.jianwudao.com/
// @icon               https://github.githubassets.com/favicon.ico
// @require            https://cdn.jsdelivr.net/npm/jquery/dist/jquery.min.js
// @include            *://github.com/*
// @compatible	       Chrome
// @run-at             document-end
// @grant              GM_addStyle
// @grant              GM_getValue
// @grant              GM_setValue
// ==/UserScript==

// ==OpenUserJS==
// @author             HaleShaw
// @collaborator       HaleShaw
// ==/OpenUserJS==

(function () {
    ('use strict');

    const style = `
        /* The menu container */
        .menuContainer {
            width: 600px;
        }

        .menuBlock {
            padding: 4px 0;
            color: #990000;
        }

        .menuLeftIcon{
            margin-right:5px;
        }

        .menuButtonLabel{
            margin-right: 2rem;
        }

        .menuButtonCheck{
            vertical-align: text-bottom;
            margin: 0 3px;
        }

        .clone {
            padding-left: 0 !important;
            width: calc(100% - 21px) !important;
        }
    `;

    const mirrors = [
        {
            id: 0,
            name: 'CnpmJS',
            url: 'https://github.com.cnpmjs.org',
            description: 'cnpmjs.org'
        },
        {
            id: 1,
            name: 'FastGit',
            url: 'https://hub.fastgit.org',
            description: 'KevinZonda'
        },
        {
            id: 2,
            name: 'FastGit',
            url: 'https://download.fastgit.org',
            description: 'KevinZonda'
        },
        {
            id: 3,
            name: 'FastGit',
            url: 'https://raw.fastgit.org',
            description: 'KevinZonda'
        },
        {
            id: 4,
            name: 'WuYanZheShui',
            url: 'https://github.wuyanzheshui.workers.dev',
            description: 'WuYanZheShui. Maximum of 100,000 calls per day'
        },
        {
            id: 5,
            name: 'RC1844',
            url: 'https://github.rc1844.workers.dev',
            description: 'RC1844. Maximum of 100,000 calls per day'
        },
        {
            id: 6,
            name: 'jsDelivr',
            url: 'https://cdn.jsdelivr.net/gh',
            description:
                'The total file size of the current branch of the project cannot exceed 50MB'
        },
        {
            id: 7,
            name: 'IAPK',
            url: 'https://github.iapk.cc',
            description: 'IAPK'
        },
        {
            id: 8,
            name: 'Ecalose',
            url: 'https://gh.haval.gq',
            description: 'Ecalose. Maximum of 100,000 calls per day'
        },
        {
            id: 9,
            name: 'IAPK',
            url: 'https://iapk.cc/github?url=https://github.com',
            description: 'IAPK'
        },
        {
            id: 10,
            name: 'Statically',
            url: 'https://cdn.staticaly.com/gh',
            description:
                'Only images and source code files are supported, and the file size is limited to 30MB'
        }
    ];

    //添加对应索引即可使用
    const cloneSet = [0, 1, 4];
    const browseSet = [0, 1, 4, 5, 7, 8];
    const downloadSet = [2, 4, 5, 8, 9];
    const rawSet = [3, 4, 5, 6, 8, 9, 10];

    const messages = {
        en: {
            menuButton: {
                name: 'CloneMirror',
                title: 'Open List',
                header: 'Quickly clone and Mirror sites',
                block:
                    'Please do not login in the mirror site. I will not be responsible for any loss caused by this.'
            }
        },
        zh: {
            menuButton: {
                name: '克隆与镜像',
                title: '打开列表',
                header: '快速克隆与镜像站点',
                block: '请不要在镜像网站登录账号,若因此造成任何损失本人概不负责'
            }
        }
    };

    const icons = {
        closeIcon: `
            <svg aria-label="Close menu" class="octicon octicon-x" width="16" height="16" role="img">
                <path fill-rule="evenodd" d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path>
            </svg>`,
        copyIcon: `
            <svg class="octicon octicon-clippy" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
                <path fill-rule="evenodd" d="M5.75 1a.75.75 0 00-.75.75v3c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-3a.75.75 0 00-.75-.75h-4.5zm.75 3V2.5h3V4h-3zm-2.874-.467a.75.75 0 00-.752-1.298A1.75 1.75 0 002 3.75v9.5c0 .966.784 1.75 1.75 1.75h8.5A1.75 1.75 0 0014 13.25v-9.5a1.75 1.75 0 00-.874-1.515.75.75 0 10-.752 1.298.25.25 0 01.126.217v9.5a.25.25 0 01-.25.25h-8.5a.25.25 0 01-.25-.25v-9.5a.25.25 0 01.126-.217z"></path>
            </svg>`,
        commandIcon: `
            <svg class="octicon octicon-terminal menuLeftIcon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
                <path fill-rule="evenodd" d="M0 2.75C0 1.784.784 1 1.75 1h12.5c.966 0 1.75.784 1.75 1.75v10.5A1.75 1.75 0 0114.25 15H1.75A1.75 1.75 0 010 13.25V2.75zm1.75-.25a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h12.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H1.75zM7.25 8a.75.75 0 01-.22.53l-2.25 2.25a.75.75 0 11-1.06-1.06L5.44 8 3.72 6.28a.75.75 0 111.06-1.06l2.25 2.25c.141.14.22.331.22.53zm1.5 1.5a.75.75 0 000 1.5h3a.75.75 0 000-1.5h-3z"></path>
            </svg>`,
        linkIcon: `
            <svg class="octicon octicon-link color-text-secondary menuLeftIcon" alt="custom" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
                <path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path>
            </svg>`
    };

    const clonePrefix = 'git clone ';
    const depthPrefix = '--depth=1 ';
    let message;
    let settingHtml;

    main();
    $(document).on('pjax:success', function () {
        $('#mirror-menu').remove();
        main();
    });

    function main() {
        GM_addStyle(style);
        logInfo(GM_info.script.name, GM_info.script.version);
        initSetting();
        message = getMessage(true, true);
        settingHtml = getSettingHtml();
        let menuButtonHtml =
            getMenuButtonPrefix() + getCloneList() + getBrowseList() + getMenuButtonSuffix();
        $('h1.flex-wrap.break-word.text-normal').append(menuButtonHtml);
        if (location.pathname.split('/')[3] == 'releases') {
            addReleasesList();
        }
        if (isPC()) {
            addDownloadZip();
        }
        addRawList();
    }

    /**
     * Initialize setting.
     */
    function initSetting() {
        let lang = GM_getValue('lang');
        let clone = GM_getValue('clone');
        let depth = GM_getValue('depth');
        if (lang == undefined) {
            GM_setValue('lang', 'zh');
        }
        if (clone == undefined) {
            GM_setValue('clone', true);
        }
        if (depth == undefined) {
            GM_setValue('depth', true);
        }
    }

    function getMenuButtonPrefix() {
        return `
        <details class="details-reset details-overlay mr-0 mb-0" id="mirror-menu">
            <summary class="btn ml-2 btn-primary" id="menuButtonTitle" data-hotkey="m" title="${message.menuButton.title}" aria-haspopup="menu" role="button">
                <span class="css-truncate-target" id="menuButtonName" data-menu-button="">${message.menuButton.name}</span>
                <span class="dropdown-caret"></span>
            </summary>

            <details-menu class="SelectMenu SelectMenu--hasFilter" role="menu">
                <div class="SelectMenu-modal menuContainer">
                    <header class="SelectMenu-header">
                        <span class="SelectMenu-title" id="menuButtonHeader">${message.menuButton.header}</span>
                        ${settingHtml}
                        <button class="SelectMenu-closeButton" type="button" data-toggle-for="mirror-menu">
                        ${icons.closeIcon}
                        </button>
                    </header>
                    <tab-container class="d-flex flex-column js-branches-tags-tabs" style="min-height: 0;">
                        <div role="tabpanel" class="d-flex flex-column flex-auto" tabindex="0">
                            <div class="SelectMenu-list" data-filter-list="">
                                <div class="btn-block flash-error menuBlock" id="menuButtonBlock" role="alert">
                                    ${message.menuButton.block}
                                </div>`;
    }

    function getSettingHtml() {
        const clone = GM_getValue('clone');
        const depth = GM_getValue('depth');
        const lang = GM_getValue('lang');
        const cloneStatus = clone ? ' checked' : '';
        const depthStatus = depth ? ' checked' : '';
        const langStatus = lang == 'en' ? ' checked' : '';
        return `
                <label class="menuButtonLabel"><input id="menuButtonClone" class="menuButtonCheck" type="checkbox"${cloneStatus}>Clone</input></label>
                <label class="menuButtonLabel"><input id="menuButtonDepth" class="menuButtonCheck" type="checkbox"${depthStatus}>Depth</input></label>
                <label class="menuButtonLabel"><input id="menuButtonLang" class="menuButtonCheck" type="checkbox"${langStatus}>English</input></label>
            `;
    }

    /**
     * Clone Checkbox event.
     */
    $('#menuButtonClone').change(function () {
        const status = $('#menuButtonClone').is(':checked');
        GM_setValue('clone', status);
        const inputs = $('.clone');
        for (let i = 0; i < inputs.length; i++) {
            let value = inputs[i].value;
            if (status) {
                value = clonePrefix + value;
            } else {
                value = value.replace(clonePrefix, '');
            }
            inputs[i].value = value;
            $(inputs[i]).next().children().attr('value', value);
        }
    });

    /**
     * Depth Checkbox event.
     */
    $('#menuButtonDepth').change(function () {
        const status = $('#menuButtonDepth').is(':checked');
        GM_setValue('depth', status);
        const inputs = $('.form-control.input-monospace.input-sm.clone');
        const index = clonePrefix.length;
        for (let i = 0; i < inputs.length; i++) {
            let value = inputs[i].value;
            if (status) {
                const length = value.length;
                if (value.startsWith(clonePrefix)) {
                    value = value.slice(0, index) + depthPrefix + value.slice(index, length);
                } else {
                    value = depthPrefix + value;
                }
            } else {
                value = value.replace(depthPrefix, '');
            }
            inputs[i].value = value;
            $(inputs[i]).next().children().attr('value', value);
        }
    });

    /**
     * Language Checkbox event.
     */
    $('#menuButtonLang').change(function () {
        const status = $('#menuButtonLang').is(':checked');
        const value = status ? 'en' : 'zh';
        GM_setValue('lang', value);
        message = getMessage();
        updateMessage();
    });

    /**
     * Update message by target language.
     */
    function updateMessage() {
        $('#menuButtonTitle').attr('title', message.menuButton.title);
        $('#menuButtonName').html(message.menuButton.name);
        $('#menuButtonHeader').html(message.menuButton.header);
        $('#menuButtonBlock').html(message.menuButton.block);
    }

    /**
     * Get the clone list.
     */
    function getCloneList() {
        const href = window.location.href.split('/');
        const git = href[3] + '/' + href[4] + '.git';
        let menuButtonHtml = '';
        const prefix = getClonePrefix();
        cloneSet.forEach(id => {
            menuButtonHtml += getCloneHtml(
                prefix + mirrors[id]['url'] + '/' + git,
                mirrors[id]['name']
            );
        });
        return menuButtonHtml;
    }

    function getMenuButtonSuffix() {
        return `</div></div></tab-container></div></details-menu></details>`;
    }

    /**
     * Get the clone command prefix.
     */
    function getClonePrefix() {
        let prefix = '';
        let clone = GM_getValue('clone');
        let depth = GM_getValue('depth');
        if (clone) {
            prefix += 'git clone ';
        }
        if (depth) {
            prefix += '--depth=1 ';
        }
        return prefix;
    }

    /**
     * Get the clone button html string.
     * @param {String} url url.
     * @param {tip} tip tip.
     */
    function getCloneHtml(url, tip) {
        return `
            <div class="input-group" style="padding: 0 16px;" title="${tip}">
                ${icons.commandIcon}
                <input type="text" class="form-control input-monospace input-sm clone" value="${url}" readonly=""
                    data-autoselect="">
                <div class="input-group-button">
                    <clipboard-copy value="${url}" class="btn btn-sm">
                        ${icons.copyIcon}
                    </clipboard-copy>
                </div>
            </div>`;
    }

    /**
     * Get the browse list.
     */
    function getBrowseList() {
        let menuButtonHtml = ``;
        const href = window.location.href.split('/');
        const path = window.location.pathname;
        browseSet.forEach(id => {
            menuButtonHtml += getBrowseHtml(
                mirrors[id]['url'] + path,
                mirrors[id]['name'],
                mirrors[id]['description']
            );
        });
        if (href.length == 5 || path.includes('/tree/') || path.includes('/blob/')) {
            var html = mirrors[5]['url'] + path.replace('/tree/', '@').replace('/blob/', '@');
            if (!path.includes('/blob/')) {
                html += '/';
            }
            menuButtonHtml += getBrowseHtml(html, mirrors[5]['name'], mirrors[5]['description']);
        }
        if (location.hostname != 'github.com') {
            menuButtonHtml += getBrowseHtml(`https://github.com${path}`, '返回GitHub');
        }
        return menuButtonHtml;
    }

    /**
     * Get browse html string.
     * @param {String} url url.
     * @param {String} name name.
     * @param {String} tip tip.
     * @returns
     */
    function getBrowseHtml(url, name, tip = '') {
        return `
        <a class="SelectMenu-item" href="${url}" target="_blank" title="${tip}" role="menuitemradio" aria-checked="false" rel="nofollow">
            ${icons.linkIcon}
            <span class="css-truncate css-truncate-overflow" style="width: 520px; overflow: hidden; word-break:keep-all; white-space:nowrap; text-overflow:ellipsis;">${url}</span>
            <span class="css-truncate css-truncate-overflow" style="width: 80px; text-align: right;">${name}</span>
        </a>`;
    }

    /**
     * Add Release list.
     */
    function addReleasesList() {
        $('.Box--condensed')
            .find('[href]')
            .each(function () {
                const href = $(this).attr('href');
                $(this)
                    .parent()
                    .after(`<div class="Box-body" >` + getReleaseDownloadHtml(href) + `</div>`);
                $(this).parent().removeClass('Box-body');
            });
    }

    /**
     * Get Release download button html string.
     * @param {String} href href.
     * @returns html.
     */
    function getReleaseDownloadHtml(href) {
        let html = '';
        downloadSet.forEach(id => {
            html += `<a class="flex-1 btn btn-outline get-repo-btn" rel="nofollow" href="${
                mirrors[id]['url'] + href
            }" title="${mirrors[id]['description']}">${mirrors[id]['name']}</a>`;
        });
        return html;
    }

    /**
     * Add download zip button.
     */
    function addDownloadZip() {
        $("a[data-open-app='link']").each(function () {
            var li = $(`<li class="Box-row p-0"></li>`);
            const downloadHref = $(this).attr('href');
            var aElement = $(this)
                .clone()
                .removeAttr('data-hydro-click data-hydro-click-hmac data-ga-click');
            aElement.addClass('Box-row Box-row--hover-gray');
            downloadSet.forEach(id => {
                let tempA = aElement.clone();
                tempA.attr({
                    href: mirrors[id]['url'] + downloadHref,
                    title: mirrors[id]['description']
                });
                tempA.html(
                    tempA.html().replace('Download ZIP', `Download ZIP(${mirrors[id]['name']})`)
                );
                li = li.clone().append(tempA);
            });
            $(this).parent().after(li);
        });
    }

    /**
     * Add Raw list.
     */
    function addRawList() {
        $('#raw-url').each(function () {
            var href = $(this).attr('href');
            rawSet.forEach(id => {
                if (id == 3 || id == 10) {
                    getRawHtml(id, mirrors[id]['url'] + href.replace('/raw', ''));
                } else if (id == 6) {
                    getRawHtml(id, mirrors[id]['url'] + href.replace('/raw/', '@'));
                } else {
                    getRawHtml(id, mirrors[id]['url'] + href);
                }
            });
        });
    }

    /**
     *
     * @param {Number} id id of mirrors.
     * @param {String} url url.
     */
    function getRawHtml(id, url) {
        var span = $('#raw-url').clone().removeAttr('id');
        span.attr({
            href: url,
            title: mirrors[id]['description'],
            target: '_blank'
        });
        span.text(mirrors[id]['name']);
        $('#raw-url').before(span);
    }

    /**
     * Get message by setting.
     */
    function getMessage() {
        return 'zh' == GM_getValue('lang') ? messages.zh : messages.en;
    }

    /**
     * Log the title and version at the front of the console.
     * @param {String} title title.
     * @param {String} version script version.
     */
    function logInfo(title, version) {
        const titleStyle = 'color:white;background-color:#606060';
        const versionStyle = 'color:white;background-color:#1475b2';
        const logTitle = ' ' + title + ' ';
        const logVersion = ' ' + version + ' ';
        console.log('%c' + logTitle + '%c' + logVersion, titleStyle, versionStyle);
    }

    /**
     * Check if the visitor is PC.
     */
    function isPC() {
        var userAgentInfo = navigator.userAgent;
        var agents = ['Android', 'iPhone', 'SymbianOS', 'Windows Phone', 'iPad', 'iPod'];
        var isPC = true;
        const len = agents.length;
        for (var v = 0; v < len; v++) {
            if (userAgentInfo.indexOf(agents[v]) > 0) {
                isPC = false;
                break;
            }
        }
        return isPC;
    }
})();