让你无需登录 Factorio 官方 Mod 网站,也能批量下载模组的辅助脚本。
נכון ליום
// ==UserScript==
// @name Factorio Mod Downloader
// @namespace http://tampermonkey.net/
// @version 5.2
// @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();
})();