Factorio Mod Downloader

让你无需登录 Factorio 官方 Mod 网站,也能批量下载模组的辅助脚本。

ของเมื่อวันที่ 20-11-2025 ดู เวอร์ชันล่าสุด

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Factorio Mod Downloader
// @namespace    http://tampermonkey.net/
// @version      5.21
// @description  让你无需登录 Factorio 官方 Mod 网站,也能批量下载模组的辅助脚本。
// @license      CC-BY-4.0
// @author       gggz
// @match        https://mods.factorio.com/mod/*
// @match        https://mods.factorio.com/browse/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const MIRROR_BASE = 'https://mods-storage.re146.dev/';
    const STORAGE_KEY = 'factorioModQueue';

    function loadQueue() {
        return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
    }

    function saveQueue(queue) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(queue));
    }

    let downloadQueue = loadQueue();

    function getModNameFromURL(url) {
        const pathname = new URL(url, window.location.origin).pathname;
        const match = pathname.match(/\/mod\/([^\/]+)/);
        return match ? match[1] : null;
    }

    async function fetchLatestRelease(modName) {
        try {
            const resp = await fetch(`https://mods.factorio.com/api/mods/${modName}/full`);
            const data = await resp.json();
            if(!data.releases || data.releases.length === 0) return null;
            const sorted = data.releases.sort((a,b)=> a.version < b.version ? 1 : -1);
            return sorted[0].version;
        } catch(e) {
            console.error('API fetch failed', e);
            return null;
        }
    }

    function updateButtonState(btn, modName) {
        if(downloadQueue.some(item => item.modName === modName)) {
            btn.textContent = '已添加';
        } else {
            btn.textContent = '添加';
        }
    }

    function addDownloadButton(container, modName) {
        if(container.querySelector('.latest-download-btn')) return;

        const btn = document.createElement('button');
        btn.className = 'latest-download-btn';
        btn.style.marginTop = '4px';
        btn.style.cursor = 'pointer';
        btn.style.padding = '2px 6px';
        container.appendChild(btn);

        updateButtonState(btn, modName);

        btn.addEventListener('click', async () => {
            const index = downloadQueue.findIndex(item => item.modName === modName);
            if(index >= 0){
                downloadQueue.splice(index, 1);
                saveQueue(downloadQueue);
                btn.textContent = '添加';
            } else {
                btn.textContent = '已添加';
                const version = await fetchLatestRelease(modName);
                if(!version) {
                    alert(`未获取到最新版本: ${modName}`);
                    btn.textContent = '添加';
                    return;
                }
                if(!downloadQueue.some(item=>item.modName===modName)){
                    downloadQueue.push({modName, version});
                    saveQueue(downloadQueue);
                }
            }
        });
    }

    function createGlobalQueueButton() {
        if(document.getElementById('batch-download-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'batch-download-btn';
        btn.textContent = '下载队列';
        btn.style.position = 'fixed';
        btn.style.right = '20px';
        btn.style.bottom = '20px';
        btn.style.zIndex = 9999;
        btn.style.padding = '6px 12px';
        btn.style.backgroundColor = '#28a745';
        btn.style.color = '#fff';
        btn.style.border = 'none';
        btn.style.borderRadius = '4px';
        btn.style.cursor = 'pointer';
        document.body.appendChild(btn);

        btn.addEventListener('click', () => {
            downloadQueue = loadQueue();
            if(downloadQueue.length === 0) return alert('下载队列为空');

            const oldModal = document.getElementById('queue-confirm-modal');
            if(oldModal) oldModal.remove();

            const modal = document.createElement('div');
            modal.id = 'queue-confirm-modal';
            modal.style.position = 'fixed';
            modal.style.left = '50%';
            modal.style.top = '50%';
            modal.style.transform = 'translate(-50%, -50%)';
            modal.style.backgroundColor = '#fff';
            modal.style.border = '1px solid #ccc';
            modal.style.padding = '20px';
            modal.style.zIndex = 10000;
            modal.style.maxHeight = '70%';
            modal.style.overflowY = 'auto';
            modal.style.width = '300px';
            modal.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';

            const title = document.createElement('h3');
            title.textContent = `确认下载 (${downloadQueue.length} 个)`;
            title.style.marginTop = '0';
            modal.appendChild(title);

            const list = document.createElement('ul');
            list.style.paddingLeft = '20px';
            downloadQueue.forEach((item, idx) => {
                const li = document.createElement('li');
                li.textContent = `${item.modName} ${item.version}`;
                li.style.cursor = 'pointer';
                li.title = '点击取消';
                li.addEventListener('click', () => {
                    downloadQueue.splice(idx, 1);
                    saveQueue(downloadQueue);
                    li.remove();
                    const btns = document.querySelectorAll('.latest-download-btn');
                    btns.forEach(b=>{
                        const parentModName = getModNameFromURL(b.closest('div').querySelector('a[href^="/mod/"]')?.href || '');
                        if(parentModName===item.modName) b.textContent='添加';
                    });
                });
                list.appendChild(li);
            });
            modal.appendChild(list);

            const downloadBtn = document.createElement('button');
            downloadBtn.textContent = '确认下载';
            downloadBtn.style.marginTop = '10px';
            downloadBtn.style.padding = '4px 8px';
            downloadBtn.style.cursor = 'pointer';
            modal.appendChild(downloadBtn);

            const clearBtn = document.createElement('button');
            clearBtn.textContent = '清空队列';
            clearBtn.style.marginLeft = '10px';
            clearBtn.style.marginTop = '10px';
            clearBtn.style.padding = '4px 8px';
            clearBtn.style.cursor = 'pointer';
            modal.appendChild(clearBtn);

            const closeBtn = document.createElement('button');
            closeBtn.textContent = '关闭';
            closeBtn.style.marginLeft = '10px';
            closeBtn.style.padding = '4px 8px';
            closeBtn.style.cursor = 'pointer';
            modal.appendChild(closeBtn);

            closeBtn.addEventListener('click', () => modal.remove());

            clearBtn.addEventListener('click', () => {
                if(confirm('确定要清空整个下载队列吗?')) {
                    downloadQueue = [];
                    saveQueue(downloadQueue);
                    list.innerHTML = '';
                    const btns = document.querySelectorAll('.latest-download-btn');
                    btns.forEach(b=>b.textContent='添加');
                }
            });

            document.body.appendChild(modal);

            downloadBtn.addEventListener('click', async () => {
                for(let i=0;i<downloadQueue.length;i++){
                    const item = downloadQueue[i];
                    const url = `${MIRROR_BASE}${encodeURIComponent(item.modName)}/${encodeURIComponent(item.version)}.zip`;
                    const link = document.createElement('a');
                    link.href = url;
                    link.target = '_blank';
                    link.rel = 'noopener';
                    document.body.appendChild(link);
                    link.click();
                    link.remove();
                    await new Promise(r=>setTimeout(r,300));
                }
                modal.remove();
            });
        });
    }

    function processDetailPage() {
        const downloadBtn = document.querySelector('.button-green[title*="You need to own Factorio"]');
        if(downloadBtn){
            const container = downloadBtn.parentNode;
            const modName = getModNameFromURL(window.location.pathname);
            addDownloadButton(container, modName);
        }
    }

    function processListPage() {
        const modCards = document.querySelectorAll('a[href^="/mod/"]');
        modCards.forEach(link => {
            const container = link.closest('div');
            const modName = getModNameFromURL(link.getAttribute('href'));
            if(modName) addDownloadButton(container, modName);
        });
    }

    const observer = new MutationObserver(() => {
        if(window.location.pathname.startsWith('/mod/')) {
            processDetailPage();
        } else if(window.location.pathname.startsWith('/browse/')) {
            processListPage();
        }
        createGlobalQueueButton();
    });
    observer.observe(document.body, {childList: true, subtree: true});

    window.addEventListener('storage', e => {
        if(e.key === STORAGE_KEY){
            downloadQueue = loadQueue();
            const btns = document.querySelectorAll('.latest-download-btn');
            btns.forEach(b=>{
                const parentModName = getModNameFromURL(b.closest('div').querySelector('a[href^="/mod/"]')?.href || '');
                updateButtonState(b, parentModName);
            });
        }
    });

    if(window.location.pathname.startsWith('/mod/')) {
        processDetailPage();
    } else if(window.location.pathname.startsWith('/browse/')) {
        processListPage();
    }
    createGlobalQueueButton();

})();