Downloads CYOA project.json and images from Neocities sites as a ZIP with a progress bar
当前为
// ==UserScript==
// @name Neocities CYOA Downloader
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Downloads CYOA project.json and images from Neocities sites as a ZIP with a progress bar
// @author Grok
// @license MIT
// @match *://*.neocities.org/*
// @grant none
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// ==/UserScript==
(function() {
'use strict';
// Check if on a Neocities site
if (!window.location.hostname.endsWith('.neocities.org')) {
return;
}
// Create progress bar UI
const progressContainer = document.createElement('div');
progressContainer.style.position = 'fixed';
progressContainer.style.top = '10px';
progressContainer.style.right = '10px';
progressContainer.style.zIndex = '10000';
progressContainer.style.backgroundColor = '#fff';
progressContainer.style.padding = '10px';
progressContainer.style.border = '1px solid #000';
progressContainer.style.borderRadius = '5px';
progressContainer.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
const progressLabel = document.createElement('div');
progressLabel.textContent = 'Preparing to download CYOA...';
progressLabel.style.marginBottom = '5px';
const progressBar = document.createElement('div');
progressBar.style.width = '200px';
progressBar.style.height = '20px';
progressBar.style.backgroundColor = '#e0e0e0';
progressBar.style.borderRadius = '3px';
progressBar.style.overflow = 'hidden';
const progressFill = document.createElement('div');
progressFill.style.width = '0%';
progressFill.style.height = '100%';
progressFill.style.backgroundColor = '#4caf50';
progressFill.style.transition = 'width 0.3s';
progressBar.appendChild(progressFill);
progressContainer.appendChild(progressLabel);
progressContainer.appendChild(progressBar);
document.body.appendChild(progressContainer);
// Utility functions
function extractProjectName(url) {
try {
const hostname = new URL(url).hostname;
if (hostname.endsWith('.neocities.org')) {
return hostname.replace('.neocities.org', '');
}
return hostname;
} catch (e) {
return 'project';
}
}
function updateProgress(value, max, label) {
const percentage = (value / max) * 100;
progressFill.style.width = `${percentage}%`;
progressLabel.textContent = label;
}
async function findImages(obj, baseUrl, imageUrls) {
if (typeof obj === 'object' && obj !== null) {
if (obj.image && typeof obj.image === 'string' && !obj.image.includes('base64,')) {
try {
const url = new URL(obj.image, baseUrl).href;
imageUrls.add(url);
} catch (e) {
console.warn(`Invalid image URL: ${obj.image}`);
}
}
for (const key in obj) {
await findImages(obj[key], baseUrl, imageUrls);
}
} else if (Array.isArray(obj)) {
for (const item of obj) {
await findImages(item, baseUrl, imageUrls);
}
}
}
async function downloadCYOA() {
const baseUrl = window.location.href.endsWith('/') ? window.location.href : window.location.href + '/';
const projectJsonUrl = new URL('project.json', baseUrl).href;
const projectName = extractProjectName(baseUrl);
const zip = new JSZip();
const imagesFolder = zip.folder('images');
const externalImages = [];
try {
// Download project.json
updateProgress(0, 100, 'Downloading project.json...');
const response = await fetch(projectJsonUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const projectData = await response.json();
// Save project.json
zip.file(`${projectName}.json`, JSON.stringify(projectData, null, 2));
updateProgress(10, 100, 'Scanning for images...');
// Extract image URLs
const imageUrls = new Set();
await findImages(projectData, baseUrl, imageUrls);
const imageUrlArray = Array.from(imageUrls);
// Download images
for (let i = 0; i < imageUrlArray.length; i++) {
const url = imageUrlArray[i];
try {
updateProgress(10 + (i / imageUrlArray.length) * 80, 100, `Downloading image ${i + 1}/${imageUrlArray.length}...`);
const response = await fetch(url);
if (!response.ok) {
externalImages.push(url);
continue;
}
const blob = await response.blob();
const filename = url.split('/').pop();
imagesFolder.file(filename, blob);
} catch (e) {
console.warn(`Failed to download image ${url}: ${e}`);
externalImages.push(url);
}
}
// Generate ZIP
updateProgress(90, 100, 'Creating ZIP file...');
const content = await zip.generateAsync({ type: 'blob' });
saveAs(content, `${projectName}.zip`);
updateProgress(100, 100, 'Download complete!');
setTimeout(() => progressContainer.remove(), 2000);
// Log external images
if (externalImages.length > 0) {
console.warn('Some images could not be downloaded (external/CORS issues):');
externalImages.forEach(url => console.log(url));
}
} catch (e) {
console.error(`Error: ${e}`);
progressLabel.textContent = 'Error occurred. Check console.';
progressFill.style.backgroundColor = '#f44336';
setTimeout(() => progressContainer.remove(), 5000);
}
}
// Add download button
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Download CYOA';
downloadButton.style.marginTop = '10px';
downloadButton.style.padding = '5px 10px';
downloadButton.style.cursor = 'pointer';
downloadButton.onclick = downloadCYOA;
progressContainer.appendChild(downloadButton);
})();