您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
allows you to Upload multiple T-Shirts/Decals easily with AnnaUploader
当前为
- // ==UserScript==
- // @name AnnaUploader (Roblox Multi-File Uploader)
- // @namespace https://www.guilded.gg/u/AnnaBlox
- // @version 4.1
- // @description allows you to Upload multiple T-Shirts/Decals easily with AnnaUploader
- // @match https://create.roblox.com/*
- // @run-at document-idle
- // @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;
- const FORCED_NAME_ON_MOD = "Uploaded Using AnnaUploader";
- let USER_ID = GM_getValue('userId', null);
- let uploadQueue = [];
- let isUploading = false;
- let csrfToken = null;
- let batchTotal = 0;
- let completedCount = 0;
- let statusElement = 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, forceName = false) {
- if (!csrfToken) {
- await fetchCSRFToken();
- }
- const displayName = forceName
- ? FORCED_NAME_ON_MOD
- : file.name.split('.')[0];
- const formData = new FormData();
- formData.append("fileContent", file, file.name);
- formData.append("request", JSON.stringify({
- displayName: displayName,
- 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}`);
- return;
- }
- const status = response.status;
- const text = await response.text();
- let json;
- try { json = JSON.parse(text); } catch {}
- const isModeratedName = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("fully moderated");
- const isInvalidNameLength = status === 400 && json?.code === "INVALID_ARGUMENT" && json?.message?.includes("name length is invalid");
- if ((isModeratedName || isInvalidNameLength) && retries < MAX_RETRIES && !forceName) {
- console.warn(`⚠️ Invalid name for ${file.name}: retrying with forced name...`);
- await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
- return await uploadFile(file, assetType, retries + 1, true);
- }
- if (status === 403 && retries < MAX_RETRIES) {
- console.warn(`🔄 CSRF expired for ${file.name}: fetching new token and retrying...`);
- csrfToken = null;
- await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
- return await uploadFile(file, assetType, retries + 1, forceName);
- }
- console.error(`❌ Upload failed for ${file.name}: [${status}]`, text);
- throw new Error(`Failed to upload ${file.name} 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);
- completedCount++;
- updateStatus();
- } catch (e) {}
- finally {
- isUploading = false;
- processUploadQueue();
- }
- }
- function updateStatus() {
- if (!statusElement) return;
- if (batchTotal > 0) {
- statusElement.textContent = `${completedCount} of ${batchTotal} files uploaded successfully`;
- } else {
- statusElement.textContent = '';
- }
- }
- function handleFileSelect(files, assetType, uploadBoth = false) {
- if (!files || files.length === 0) {
- console.warn('No files selected.');
- return;
- }
- batchTotal = uploadBoth ? files.length * 2 : files.length;
- completedCount = 0;
- updateStatus();
- for (let file of files) {
- if (uploadBoth) {
- uploadQueue.push({ file, assetType: ASSET_TYPE_TSHIRT });
- uploadQueue.push({ file, assetType: ASSET_TYPE_DECAL });
- console.log(`Queued (Both): ${file.name}`);
- } else {
- uploadQueue.push({ file, assetType });
- console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
- }
- }
- processUploadQueue();
- }
- function createUploaderUI() {
- const container = document.createElement('div');
- Object.assign(container.style, {
- position: 'fixed',
- top: '10px',
- right: '10px',
- backgroundColor: '#fff',
- border: '2px solid #000',
- padding: '15px',
- zIndex: '10000',
- borderRadius: '8px',
- boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
- display: 'flex',
- flexDirection: 'column',
- gap: '10px',
- fontFamily: 'Arial, sans-serif',
- width: '200px'
- });
- const closeBtn = document.createElement('button');
- closeBtn.textContent = '×';
- Object.assign(closeBtn.style, {
- position: 'absolute',
- top: '5px',
- right: '8px',
- background: 'transparent',
- border: 'none',
- fontSize: '16px',
- cursor: 'pointer',
- lineHeight: '1'
- });
- closeBtn.title = 'Close uploader';
- closeBtn.addEventListener('click', () => container.remove());
- container.appendChild(closeBtn);
- const title = document.createElement('h3');
- title.textContent = 'AnnaUploader';
- title.style.margin = '0 0 5px 0';
- title.style.fontSize = '16px';
- container.appendChild(title);
- const makeBtn = (text, onClick) => {
- const btn = document.createElement('button');
- btn.textContent = text;
- Object.assign(btn.style, { padding: '8px', cursor: 'pointer' });
- btn.addEventListener('click', onClick);
- return btn;
- };
- container.appendChild(makeBtn('Upload T-Shirts', () => {
- 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();
- }));
- container.appendChild(makeBtn('Upload Decals', () => {
- 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();
- }));
- container.appendChild(makeBtn('Upload Both', () => {
- 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();
- }));
- container.appendChild(makeBtn('Change ID', () => {
- const input = prompt("Enter your Roblox User ID or Profile URL:", USER_ID || '');
- if (!input) return;
- // Try to extract numeric ID from profile URL
- const urlMatch = input.match(/roblox\.com\/users\/(\d+)\/profile/i);
- let newId = null;
- if (urlMatch) {
- newId = urlMatch[1];
- } else if (!isNaN(input.trim())) {
- newId = input.trim();
- }
- if (newId) {
- USER_ID = Number(newId);
- GM_setValue('userId', USER_ID);
- alert(`User ID updated to ${USER_ID}`);
- } else {
- alert("Invalid input. Please enter a numeric ID or a valid profile URL.");
- }
- }));
- const pasteHint = document.createElement('div');
- pasteHint.textContent = 'Paste images (Ctrl+V) to upload as decals!';
- pasteHint.style.fontSize = '12px';
- pasteHint.style.color = '#555';
- container.appendChild(pasteHint);
- statusElement = document.createElement('div');
- statusElement.style.fontSize = '12px';
- statusElement.style.color = '#000';
- statusElement.textContent = '';
- container.appendChild(statusElement);
- document.body.appendChild(container);
- }
- function handlePaste(event) {
- const items = event.clipboardData?.items;
- if (!items) return;
- for (let item of items) {
- if (item.type.indexOf('image') === 0) {
- event.preventDefault();
- const blob = item.getAsFile();
- 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);
- break;
- }
- }
- }
- function init() {
- createUploaderUI();
- document.addEventListener('paste', handlePaste);
- console.log('[Uploader] Initialized with User ID:', USER_ID);
- }
- window.addEventListener('load', init);
- })();