Greasy Fork

Neocities CYOA Downloader

Downloads CYOA project.json and images from Neocities sites as a ZIP with a progress bar

当前为 2025-04-18 提交的版本,查看 最新版本

// ==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);
})();