Greasy Fork

Greasy Fork is available in English.

AnnaUploader (Roblox Multi-File Uploader)

Upload multiple T-Shirts/Decals easily with AnnaUploader

目前为 2025-04-30 提交的版本。查看 最新版本

// ==UserScript==
// @name         AnnaUploader (Roblox Multi-File Uploader)
// @namespace    https://www.guilded.gg/u/AnnaBlox
// @version      3.5
// @description  Upload multiple T-Shirts/Decals easily with AnnaUploader
// @match        https://create.roblox.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @license MIT
// ==/UserScript==

(function() {
    'use strict';

    const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
    const ASSET_TYPE_TSHIRT = 11;
    const ASSET_TYPE_DECAL  = 13;
    const UPLOAD_RETRY_DELAY = 2000;
    const MAX_RETRIES = 3;

    // ========== PERSISTENT USER CONFIG ========== //
    // Will default to this value only once, then store whatever you enter
    let USER_ID = GM_getValue('userId', null);
    // ============================================ //

    let uploadQueue = [];
    let isUploading = false;
    let csrfToken = null;

    async function fetchCSRFToken() {
        try {
            const response = await fetch(ROBLOX_UPLOAD_URL, {
                method: 'POST',
                credentials: 'include',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({})
            });

            if (response.status === 403) {
                const token = response.headers.get('x-csrf-token');
                if (token) {
                    console.log('[CSRF] Token fetched:', token);
                    csrfToken = token;
                    return token;
                }
            }
            throw new Error('Failed to fetch CSRF token');
        } catch (error) {
            console.error('[CSRF] Fetch error:', error);
            throw error;
        }
    }

    async function uploadFile(file, assetType, retries = 0) {
        if (!csrfToken) {
            await fetchCSRFToken();
        }

        const formData = new FormData();
        formData.append("fileContent", file, file.name);
        formData.append("request", JSON.stringify({
            displayName: file.name.split('.')[0],
            description: "Uploaded Using AnnaUploader",
            assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
            creationContext: {
                creator: { userId: USER_ID },
                expectedPrice: 0
            }
        }));

        try {
            const response = await fetch(ROBLOX_UPLOAD_URL, {
                method: "POST",
                credentials: "include",
                headers: { "x-csrf-token": csrfToken },
                body: formData
            });

            if (response.ok) {
                console.log(`✅ Uploaded (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
            } else {
                const responseText = await response.text();
                console.error(`❌ Upload failed for ${file.name}: [${response.status}]`, responseText);

                if (response.status === 403 && retries < MAX_RETRIES) {
                    console.warn(`Fetching new CSRF and retrying ${file.name}...`);
                    csrfToken = null;
                    await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
                    await uploadFile(file, assetType, retries + 1);
                } else {
                    throw new Error(`Failed to upload after ${retries} retries.`);
                }
            }
        } catch (error) {
            console.error(`Upload error for ${file.name}:`, error);
            throw error;
        }
    }

    async function processUploadQueue() {
        if (isUploading || uploadQueue.length === 0) return;
        isUploading = true;

        const { file, assetType } = uploadQueue.shift();
        try {
            await uploadFile(file, assetType);
        } catch (error) {
            console.error('Queue error:', error);
        } finally {
            isUploading = false;
            processUploadQueue();
        }
    }

    function handleFileSelect(files, assetType, uploadBoth = false) {
        if (!files || files.length === 0) {
            console.warn('No files selected.');
            return;
        }
        for (let i = 0; i < files.length; i++) {
            if (uploadBoth) {
                uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_TSHIRT });
                uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_DECAL });
                console.log(`Queued (Both): ${files[i].name}`);
            } else {
                uploadQueue.push({ file: files[i], assetType });
                console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${files[i].name}`);
            }
        }
        processUploadQueue();
    }

    function createUploaderUI() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.top = '10px';
        container.style.right = '10px';
        container.style.backgroundColor = '#fff';
        container.style.border = '2px solid #000';
        container.style.padding = '15px';
        container.style.zIndex = '10000';
        container.style.borderRadius = '8px';
        container.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
        container.style.display = 'flex';
        container.style.flexDirection = 'column';
        container.style.gap = '10px';
        container.style.fontFamily = 'Arial, sans-serif';

        const title = document.createElement('h3');
        title.textContent = 'Multi-File Uploader';
        title.style.margin = '0';
        title.style.fontSize = '16px';
        container.appendChild(title);

        // Upload buttons
        const uploadTShirtBtn = document.createElement('button');
        uploadTShirtBtn.textContent = 'Upload T-Shirts';
        uploadTShirtBtn.style.padding = '8px';
        uploadTShirtBtn.style.cursor = 'pointer';
        uploadTShirtBtn.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'image/*';
            input.multiple = true;
            input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT));
            input.click();
        });

        const uploadDecalBtn = document.createElement('button');
        uploadDecalBtn.textContent = 'Upload Decals';
        uploadDecalBtn.style.padding = '8px';
        uploadDecalBtn.style.cursor = 'pointer';
        uploadDecalBtn.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'image/*';
            input.multiple = true;
            input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL));
            input.click();
        });

        const uploadBothBtn = document.createElement('button');
        uploadBothBtn.textContent = 'Upload Both';
        uploadBothBtn.style.padding = '8px';
        uploadBothBtn.style.cursor = 'pointer';
        uploadBothBtn.addEventListener('click', () => {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = 'image/*';
            input.multiple = true;
            input.addEventListener('change', e => handleFileSelect(e.target.files, null, true));
            input.click();
        });

        // Change ID button
        const changeIdBtn = document.createElement('button');
        changeIdBtn.textContent = 'Change ID';
        changeIdBtn.style.padding = '8px';
        changeIdBtn.style.cursor = 'pointer';
        changeIdBtn.addEventListener('click', () => {
            const newId = prompt("Enter your Roblox User ID:", USER_ID);
            if (newId && !isNaN(newId)) {
                USER_ID = Number(newId);
                GM_setValue('userId', USER_ID);
                alert(`User ID updated to ${USER_ID}`);
            } else {
                alert("Invalid ID. Please enter a numeric value.");
            }
        });

        const pasteHint = document.createElement('div');
        pasteHint.textContent = 'Paste images (Ctrl+V) to upload as decals!';
        pasteHint.style.fontSize = '12px';
        pasteHint.style.color = '#555';

        // Append everything
        container.appendChild(uploadTShirtBtn);
        container.appendChild(uploadDecalBtn);
        container.appendChild(uploadBothBtn);
        container.appendChild(changeIdBtn);
        container.appendChild(pasteHint);

        document.body.appendChild(container);
    }

    function handlePaste(event) {
        const items = event.clipboardData?.items;
        if (!items) return;

        let blob = null;
        for (let i = 0; i < items.length; i++) {
            if (items[i].type.indexOf('image') === 0) {
                blob = items[i].getAsFile();
                break;
            }
        }

        if (blob) {
            event.preventDefault();
            const now = new Date();
            const filename = `pasted_image_${now.toISOString().replace(/[^a-z0-9]/gi, '_')}.png`;
            const file = new File([blob], filename, { type: blob.type });
            handleFileSelect([file], ASSET_TYPE_DECAL);
        }
    }

    function init() {
        createUploaderUI();
        document.addEventListener('paste', handlePaste);
        console.log('[Uploader] Initialized with User ID:', USER_ID);
    }

    window.addEventListener('load', init);
})();