您需要先安装一个扩展,例如 篡改猴、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.8
- // @description allows you to upload multiple T-Shirts/Decals easily with AnnaUploader
- // @match https://create.roblox.com/*
- // @match https://www.roblox.com/users/*/profile*
- // @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 = 0;
- const MAX_RETRIES = 150;
- const FORCED_NAME = "Uploaded Using AnnaUploader";
- // Stored settings
- let USER_ID = GM_getValue('userId', null);
- let useForcedName = false; // toggle: false => use file names, true => use FORCED_NAME
- // Mass-upload state
- let massMode = false;
- let massQueue = [];
- let csrfToken = null;
- let batchTotal = 0;
- let completed = 0;
- let statusEl = null;
- let toggleBtn = null;
- let startBtn = null;
- async function fetchCSRFToken() {
- const resp = await fetch(ROBLOX_UPLOAD_URL, {
- method: 'POST',
- credentials: 'include',
- headers: {'Content-Type':'application/json'},
- body: JSON.stringify({})
- });
- if (resp.status === 403) {
- const tok = resp.headers.get('x-csrf-token');
- if (tok) {
- csrfToken = tok;
- console.log('[CSRF] fetched:', tok);
- return tok;
- }
- }
- throw new Error('Cannot fetch CSRF token');
- }
- async function uploadFile(file, assetType, retries = 0, forceName = false) {
- if (!csrfToken) await fetchCSRFToken();
- const displayName = forceName ? FORCED_NAME : file.name.split('.')[0];
- const fd = new FormData();
- fd.append('fileContent', file, file.name);
- fd.append('request', JSON.stringify({
- displayName,
- description: FORCED_NAME,
- assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
- creationContext: { creator: { userId: USER_ID }, expectedPrice: 0 }
- }));
- try {
- const resp = await fetch(ROBLOX_UPLOAD_URL, {
- method: 'POST',
- credentials: 'include',
- headers: { 'x-csrf-token': csrfToken },
- body: fd
- });
- if (resp.ok) {
- console.log(`✅ ${displayName}`);
- completed++;
- updateStatus();
- return;
- }
- const txt = await resp.text();
- let json; try { json = JSON.parse(txt); } catch{}
- const badName = resp.status===400 && json?.message?.includes('moderated');
- if (badName && retries < MAX_RETRIES && !forceName) {
- await new Promise(r=>setTimeout(r, UPLOAD_RETRY_DELAY));
- return uploadFile(file, assetType, retries+1, true);
- }
- if (resp.status===403 && retries<MAX_RETRIES) {
- csrfToken = null;
- await new Promise(r=>setTimeout(r, UPLOAD_RETRY_DELAY));
- return uploadFile(file, assetType, retries+1, forceName);
- }
- console.error(`❌ ${file.name}: [${resp.status}]`, txt);
- } catch(e) {
- console.error('Upload error', e);
- } finally {
- // even on error, count as “done” so status moves
- if (!resp?.ok) {
- completed++;
- updateStatus();
- }
- }
- }
- function updateStatus() {
- if (!statusEl) return;
- if (batchTotal > 0) {
- statusEl.textContent = `${completed} of ${batchTotal} processed`;
- } else {
- statusEl.textContent = massMode
- ? `${massQueue.length} items queued`
- : '';
- }
- }
- function handleFileSelect(files, assetType, both=false) {
- if (!files || files.length===0) return;
- if (massMode) {
- for (let f of files) {
- if (both) {
- massQueue.push({f,type:ASSET_TYPE_TSHIRT});
- massQueue.push({f,type:ASSET_TYPE_DECAL});
- } else {
- massQueue.push({f,type:assetType});
- }
- }
- updateStatus();
- return;
- }
- // immediate parallel upload
- const tasks = [];
- batchTotal = both ? files.length*2 : files.length;
- completed = 0;
- updateStatus();
- for (let f of files) {
- if (both) {
- tasks.push(uploadFile(f, ASSET_TYPE_TSHIRT, 0, useForcedName));
- tasks.push(uploadFile(f, ASSET_TYPE_DECAL, 0, useForcedName));
- } else {
- tasks.push(uploadFile(f, assetType, 0, useForcedName));
- }
- }
- Promise.all(tasks).then(()=>console.log('[Uploader] done'));
- }
- function startMassUpload() {
- if (massQueue.length===0) return alert('Nothing queued!');
- batchTotal = massQueue.length;
- completed = 0;
- updateStatus();
- const tasks = massQueue.map(item => uploadFile(item.f, item.type, 0, useForcedName));
- massQueue = [];
- updateStatus();
- Promise.all(tasks).then(()=>{
- alert('Mass upload complete!');
- toggleBtn.textContent = 'Enable Mass Upload';
- massMode = false;
- startBtn.style.display = 'none';
- });
- }
- function createUploaderUI() {
- const c = document.createElement('div');
- Object.assign(c.style, {
- position:'fixed', top:'10px', right:'10px',
- background:'#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:'8px', fontFamily:'Arial', width:'240px'
- });
- // Close button
- const close = document.createElement('button');
- close.textContent = '×';
- Object.assign(close.style, {
- position: 'absolute', top: '5px', right: '8px',
- background: 'transparent', border: 'none', fontSize: '16px', cursor: 'pointer'
- });
- close.title = 'Close';
- close.onclick = () => c.remove();
- c.appendChild(close);
- // Title
- const title = document.createElement('h3');
- title.textContent = 'AnnaUploader';
- title.style.margin = '0 0 5px 0';
- title.style.fontSize = '16px';
- c.appendChild(title);
- // Buttons factory
- const makeBtn = (txt, fn) => {
- const b = document.createElement('button');
- b.textContent = txt;
- Object.assign(b.style, { padding: '8px', cursor: 'pointer' });
- b.onclick = fn;
- return b;
- };
- // Upload controls
- c.appendChild(makeBtn('Upload T-Shirts', () => {
- const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.multiple = true;
- inp.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT);
- inp.click();
- }));
- c.appendChild(makeBtn('Upload Decals', () => {
- const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.multiple = true;
- inp.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL);
- inp.click();
- }));
- c.appendChild(makeBtn('Upload Both', () => {
- const inp = document.createElement('input'); inp.type = 'file'; inp.accept = 'image/*'; inp.multiple = true;
- inp.onchange = e => handleFileSelect(e.target.files, null, true);
- inp.click();
- }));
- // Mass-upload toggle
- toggleBtn = makeBtn('Enable Mass Upload', () => {
- massMode = !massMode;
- toggleBtn.textContent = massMode ? 'Disable Mass Upload' : 'Enable Mass Upload';
- startBtn.style.display = massMode ? 'block' : 'none';
- massQueue = [];
- batchTotal = 0; completed = 0;
- updateStatus();
- });
- c.appendChild(toggleBtn);
- // Start button
- startBtn = makeBtn('Start Mass Upload', startMassUpload);
- startBtn.style.display = 'none';
- c.appendChild(startBtn);
- // Forced name toggle
- const nameToggleBtn = makeBtn(`Use default Name: Off`, () => {
- useForcedName = !useForcedName;
- nameToggleBtn.textContent = `Use default Name: ${useForcedName ? 'On' : 'Off'}`;
- });
- c.appendChild(nameToggleBtn);
- // Change ID
- c.appendChild(makeBtn('Change ID', () => {
- const inp = prompt("Enter your Roblox User ID or Profile URL:", USER_ID || '');
- if (!inp) return;
- const m = inp.match(/users\/(\d+)/);
- const id = m ? m[1] : inp.trim();
- if (!isNaN(id)) {
- USER_ID = Number(id);
- GM_setValue('userId', USER_ID);
- alert(`User ID set to ${USER_ID}`);
- } else alert('Invalid input.');
- }));
- // Profile shortcut
- const pm = window.location.pathname.match(/^\/users\/(\d+)\/profile/);
- if (pm) {
- c.appendChild(makeBtn('Use This Profile as ID', () => {
- USER_ID = Number(pm[1]);
- GM_setValue('userId', USER_ID);
- alert(`User ID set to ${USER_ID}`);
- }));
- }
- // Paste hint & status
- const hint = document.createElement('div');
- hint.textContent = 'Paste images (Ctrl+V) to queue/upload';
- hint.style.fontSize = '12px'; hint.style.color = '#555';
- c.appendChild(hint);
- statusEl = document.createElement('div');
- statusEl.style.fontSize = '12px'; statusEl.style.color = '#000';
- c.appendChild(statusEl);
- document.body.appendChild(c);
- }
- function handlePaste(e) {
- const items = e.clipboardData?.items;
- if (!items) return;
- for (let it of items) {
- if (it.type.startsWith('image')) {
- e.preventDefault();
- const blob = it.getAsFile();
- const ts = new Date().toISOString().replace(/[^a-z0-9]/gi,'_');
- let name = prompt('Name (no ext):', `pasted_${ts}`);
- if (name === null) return;
- name = name.trim() || `pasted_${ts}`;
- const filename = name.endsWith('.png') ? name : `${name}.png`;
- let t = prompt('T=T-Shirt, D=Decal, C=Cancel','D');
- if (!t) return;
- t = t.trim().toUpperCase();
- let type = null;
- if (t === 'T') type = ASSET_TYPE_TSHIRT;
- else if (t === 'D') type = ASSET_TYPE_DECAL;
- else return;
- const file = new File([blob], filename, {type: blob.type});
- handleFileSelect([file], type);
- break;
- }
- }
- }
- window.addEventListener('load', () => {
- createUploaderUI();
- document.addEventListener('paste', handlePaste);
- console.log('[AnnaUploader] initialized, massMode=', massMode, 'useForcedName=', useForcedName);
- });
- })();