AnnaUploader (Roblox Multi-File Uploader)

Upload multiple T-Shirts/Decals easily with AnnaUploader

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

  1. // ==UserScript==
  2. // @name AnnaUploader (Roblox Multi-File Uploader)
  3. // @namespace https://www.guilded.gg/u/AnnaBlox
  4. // @version 3.4
  5. // @description Upload multiple T-Shirts/Decals easily with AnnaUploader
  6. // @match https://create.roblox.com/*
  7. // @grant GM_getValue
  8. // @grant GM_setValue
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
  17. const ASSET_TYPE_TSHIRT = 11;
  18. const ASSET_TYPE_DECAL = 13;
  19. const UPLOAD_RETRY_DELAY = 2000;
  20. const MAX_RETRIES = 3;
  21.  
  22. // ========== PERSISTENT USER CONFIG ========== //
  23. // Will default to this value only once, then store whatever you enter
  24. let USER_ID = GM_getValue('userId', 32456865);
  25. // ============================================ //
  26.  
  27. let uploadQueue = [];
  28. let isUploading = false;
  29. let csrfToken = null;
  30.  
  31. async function fetchCSRFToken() {
  32. try {
  33. const response = await fetch(ROBLOX_UPLOAD_URL, {
  34. method: 'POST',
  35. credentials: 'include',
  36. headers: { 'Content-Type': 'application/json' },
  37. body: JSON.stringify({})
  38. });
  39.  
  40. if (response.status === 403) {
  41. const token = response.headers.get('x-csrf-token');
  42. if (token) {
  43. console.log('[CSRF] Token fetched:', token);
  44. csrfToken = token;
  45. return token;
  46. }
  47. }
  48. throw new Error('Failed to fetch CSRF token');
  49. } catch (error) {
  50. console.error('[CSRF] Fetch error:', error);
  51. throw error;
  52. }
  53. }
  54.  
  55. async function uploadFile(file, assetType, retries = 0) {
  56. if (!csrfToken) {
  57. await fetchCSRFToken();
  58. }
  59.  
  60. const formData = new FormData();
  61. formData.append("fileContent", file, file.name);
  62. formData.append("request", JSON.stringify({
  63. displayName: file.name.split('.')[0],
  64. description: "Uploaded Using AnnaUploader",
  65. assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
  66. creationContext: {
  67. creator: { userId: USER_ID },
  68. expectedPrice: 0
  69. }
  70. }));
  71.  
  72. try {
  73. const response = await fetch(ROBLOX_UPLOAD_URL, {
  74. method: "POST",
  75. credentials: "include",
  76. headers: { "x-csrf-token": csrfToken },
  77. body: formData
  78. });
  79.  
  80. if (response.ok) {
  81. console.log(`✅ Uploaded (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${file.name}`);
  82. } else {
  83. const responseText = await response.text();
  84. console.error(`❌ Upload failed for ${file.name}: [${response.status}]`, responseText);
  85.  
  86. if (response.status === 403 && retries < MAX_RETRIES) {
  87. console.warn(`Fetching new CSRF and retrying ${file.name}...`);
  88. csrfToken = null;
  89. await new Promise(res => setTimeout(res, UPLOAD_RETRY_DELAY));
  90. await uploadFile(file, assetType, retries + 1);
  91. } else {
  92. throw new Error(`Failed to upload after ${retries} retries.`);
  93. }
  94. }
  95. } catch (error) {
  96. console.error(`Upload error for ${file.name}:`, error);
  97. throw error;
  98. }
  99. }
  100.  
  101. async function processUploadQueue() {
  102. if (isUploading || uploadQueue.length === 0) return;
  103. isUploading = true;
  104.  
  105. const { file, assetType } = uploadQueue.shift();
  106. try {
  107. await uploadFile(file, assetType);
  108. } catch (error) {
  109. console.error('Queue error:', error);
  110. } finally {
  111. isUploading = false;
  112. processUploadQueue();
  113. }
  114. }
  115.  
  116. function handleFileSelect(files, assetType, uploadBoth = false) {
  117. if (!files || files.length === 0) {
  118. console.warn('No files selected.');
  119. return;
  120. }
  121. for (let i = 0; i < files.length; i++) {
  122. if (uploadBoth) {
  123. uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_TSHIRT });
  124. uploadQueue.push({ file: files[i], assetType: ASSET_TYPE_DECAL });
  125. console.log(`Queued (Both): ${files[i].name}`);
  126. } else {
  127. uploadQueue.push({ file: files[i], assetType });
  128. console.log(`Queued (${assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal"}): ${files[i].name}`);
  129. }
  130. }
  131. processUploadQueue();
  132. }
  133.  
  134. function createUploaderUI() {
  135. const container = document.createElement('div');
  136. container.style.position = 'fixed';
  137. container.style.top = '10px';
  138. container.style.right = '10px';
  139. container.style.backgroundColor = '#fff';
  140. container.style.border = '2px solid #000';
  141. container.style.padding = '15px';
  142. container.style.zIndex = '10000';
  143. container.style.borderRadius = '8px';
  144. container.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)';
  145. container.style.display = 'flex';
  146. container.style.flexDirection = 'column';
  147. container.style.gap = '10px';
  148. container.style.fontFamily = 'Arial, sans-serif';
  149.  
  150. const title = document.createElement('h3');
  151. title.textContent = 'Multi-File Uploader';
  152. title.style.margin = '0';
  153. title.style.fontSize = '16px';
  154. container.appendChild(title);
  155.  
  156. // Upload buttons
  157. const uploadTShirtBtn = document.createElement('button');
  158. uploadTShirtBtn.textContent = 'Upload T-Shirts';
  159. uploadTShirtBtn.style.padding = '8px';
  160. uploadTShirtBtn.style.cursor = 'pointer';
  161. uploadTShirtBtn.addEventListener('click', () => {
  162. const input = document.createElement('input');
  163. input.type = 'file';
  164. input.accept = 'image/*';
  165. input.multiple = true;
  166. input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT));
  167. input.click();
  168. });
  169.  
  170. const uploadDecalBtn = document.createElement('button');
  171. uploadDecalBtn.textContent = 'Upload Decals';
  172. uploadDecalBtn.style.padding = '8px';
  173. uploadDecalBtn.style.cursor = 'pointer';
  174. uploadDecalBtn.addEventListener('click', () => {
  175. const input = document.createElement('input');
  176. input.type = 'file';
  177. input.accept = 'image/*';
  178. input.multiple = true;
  179. input.addEventListener('change', e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL));
  180. input.click();
  181. });
  182.  
  183. const uploadBothBtn = document.createElement('button');
  184. uploadBothBtn.textContent = 'Upload Both';
  185. uploadBothBtn.style.padding = '8px';
  186. uploadBothBtn.style.cursor = 'pointer';
  187. uploadBothBtn.addEventListener('click', () => {
  188. const input = document.createElement('input');
  189. input.type = 'file';
  190. input.accept = 'image/*';
  191. input.multiple = true;
  192. input.addEventListener('change', e => handleFileSelect(e.target.files, null, true));
  193. input.click();
  194. });
  195.  
  196. // Change ID button
  197. const changeIdBtn = document.createElement('button');
  198. changeIdBtn.textContent = 'Change ID';
  199. changeIdBtn.style.padding = '8px';
  200. changeIdBtn.style.cursor = 'pointer';
  201. changeIdBtn.addEventListener('click', () => {
  202. const newId = prompt("Enter your Roblox User ID:", USER_ID);
  203. if (newId && !isNaN(newId)) {
  204. USER_ID = Number(newId);
  205. GM_setValue('userId', USER_ID);
  206. alert(`User ID updated to ${USER_ID}`);
  207. } else {
  208. alert("Invalid ID. Please enter a numeric value.");
  209. }
  210. });
  211.  
  212. const pasteHint = document.createElement('div');
  213. pasteHint.textContent = 'Paste images (Ctrl+V) to upload as decals!';
  214. pasteHint.style.fontSize = '12px';
  215. pasteHint.style.color = '#555';
  216.  
  217. // Append everything
  218. container.appendChild(uploadTShirtBtn);
  219. container.appendChild(uploadDecalBtn);
  220. container.appendChild(uploadBothBtn);
  221. container.appendChild(changeIdBtn);
  222. container.appendChild(pasteHint);
  223.  
  224. document.body.appendChild(container);
  225. }
  226.  
  227. function handlePaste(event) {
  228. const items = event.clipboardData?.items;
  229. if (!items) return;
  230.  
  231. let blob = null;
  232. for (let i = 0; i < items.length; i++) {
  233. if (items[i].type.indexOf('image') === 0) {
  234. blob = items[i].getAsFile();
  235. break;
  236. }
  237. }
  238.  
  239. if (blob) {
  240. event.preventDefault();
  241. const now = new Date();
  242. const filename = `pasted_image_${now.toISOString().replace(/[^a-z0-9]/gi, '_')}.png`;
  243. const file = new File([blob], filename, { type: blob.type });
  244. handleFileSelect([file], ASSET_TYPE_DECAL);
  245. }
  246. }
  247.  
  248. function init() {
  249. createUploaderUI();
  250. document.addEventListener('paste', handlePaste);
  251. console.log('[Uploader] Initialized with User ID:', USER_ID);
  252. }
  253.  
  254. window.addEventListener('load', init);
  255. })();