您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
allows you to upload multiple T-Shirts/Decals easily with AnnaUploader; now supports image resizing
- // ==UserScript==
- // @name AnnaUploader (Roblox Multi-File Uploader)
- // @namespace https://github.com/AnnaRoblox
- // @version 7.1
- // @description allows you to upload multiple T-Shirts/Decals easily with AnnaUploader; now supports image resizing
- // @match https://create.roblox.com/*
- // @match https://www.roblox.com/users/*/profile*
- // @match https://www.roblox.com/communities/*
- // @match https://www.roblox.com/home/*
- // @run-at document-idle
- // @grant GM_getValue
- // @grant GM_setValue
- // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
- // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
- // @license MIT
- // ==/UserScript==
- (function() {
- 'use strict';
- // Constants for Roblox API and asset types
- const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
- const ASSET_TYPE_TSHIRT = 11;
- const ASSET_TYPE_DECAL = 13;
- const FORCED_NAME = "Uploaded Using AnnaUploader"; // Default name for assets
- // Storage keys and scan interval for asset logging
- const STORAGE_KEY = 'annaUploaderAssetLog';
- const SCAN_INTERVAL_MS = 10_000;
- // Script configuration variables, managed with Tampermonkey's GM_getValue/GM_setValue
- let USER_ID = GM_getValue('userId', null);
- let IS_GROUP = GM_getValue('isGroup', false);
- let useForcedName = GM_getValue('useForcedName', false); // Persist this setting
- let useMakeUnique = GM_getValue('useMakeUnique', false); // Persist this setting
- let uniqueCopies = GM_getValue('uniqueCopies', 1); // Persist this setting
- let useDownload = GM_getValue('useDownload', false); // Persist this setting
- let useForceCanvasUpload = GM_getValue('useForceCanvasUpload', false); // Persist this setting
- // NEW SETTING: Slip Mode Pixel Method - 'all_pixels', '1-3_random', '1-4_random_single_pixel', or 'random_single_pixel_full_random_color'
- let slipModePixelMethod = GM_getValue('slipModePixelMethod', '1-3_random');
- // NEW: Image resizing settings
- let enableResize = GM_getValue('enableResize', false);
- let resizeWidth = GM_getValue('resizeWidth', 300);
- let resizeHeight = GM_getValue('resizeHeight', 300);
- // Mass upload mode variables
- let massMode = false; // True if mass upload mode is active
- let massQueue = []; // Array to hold files/metadata for mass upload
- let batchTotal = 0; // Total items to process in current batch/queue
- let completed = 0; // Number of items completed in current batch/queue
- let csrfToken = null; // Roblox CSRF token for authenticated requests
- let statusEl, toggleBtn, startBtn, copiesInput, downloadBtn; // UI elements (removed forceUploadBtn from here)
- let uiContainer; // Reference to the main UI container element
- let settingsModal; // Reference to the settings modal element
- function baseName(filename) {
- return filename.replace(/\.[^/.]+$/, '');
- }
- function loadLog() {
- const raw = GM_getValue(STORAGE_KEY, '{}');
- try { return JSON.parse(raw); }
- catch { return {}; }
- }
- function saveLog(log) {
- GM_setValue(STORAGE_KEY, JSON.stringify(log));
- }
- function logAsset(id, imageURL, name) {
- const log = loadLog();
- log[id] = {
- date: new Date().toISOString(),
- image: imageURL || log[id]?.image || null,
- name: name || log[id]?.name || '(unknown)'
- };
- saveLog(log);
- console.log(`[AssetLogger] logged asset ${id} at ${log[id].date}, name: ${log[id].name}, image: ${log[id].image || "none"}`);
- }
- function scanForAssets() {
- console.log('[AssetLogger] scanning for assets…');
- document.querySelectorAll('[href]').forEach(el => {
- let m = el.href.match(/(?:https?:\/\/create\.roblox\.com)?\/store\/asset\/(\d+)/)
- || el.href.match(/\/dashboard\/creations\/store\/(\d+)\/configure/);
- if (m) {
- const id = m[1];
- let image = null;
- const container = el.closest('*');
- const img = container?.querySelector('img');
- if (img?.src) image = img.src;
- let name = null;
- const nameEl = container?.querySelector('span.MuiTypography-root');
- if (nameEl) name = nameEl.textContent.trim();
- logAsset(id, image, name);
- }
- });
- }
- setInterval(scanForAssets, SCAN_INTERVAL_MS);
- 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] token fetched');
- return tok;
- }
- }
- throw new Error('Cannot fetch CSRF token');
- }
- function updateStatus() {
- if (!statusEl) return;
- if (massMode) {
- statusEl.textContent = `${massQueue.length} queued`;
- } else if (batchTotal > 0) {
- statusEl.textContent = `${completed} of ${batchTotal} processed`;
- } else {
- statusEl.textContent = '';
- }
- }
- async function uploadFile(file, assetType, retries = 0, forceName = false) {
- if (!csrfToken) {
- try {
- await fetchCSRFToken();
- } catch (e) {
- console.error("[Upload] Failed to fetch initial CSRF token:", e);
- completed++;
- updateStatus();
- return;
- }
- }
- const displayName = forceName ? FORCED_NAME : baseName(file.name);
- const creator = IS_GROUP
- ? { groupId: USER_ID }
- : { userId: USER_ID };
- 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, expectedPrice: 0 }
- }));
- try {
- const resp = await fetch(ROBLOX_UPLOAD_URL, {
- method: 'POST',
- credentials: 'include',
- headers: { 'x-csrf-token': csrfToken },
- body: fd
- });
- const txt = await resp.text();
- let json; try { json = JSON.parse(txt); } catch (e) {
- console.error('[Upload] Failed to parse response JSON:', e, txt);
- }
- if (json?.message && typeof json.message === 'string' && json.message.toLowerCase().includes('banned')) {
- displayMessage('Upload failed: Your account appears to be banned. Cannot complete upload.', 'error');
- console.error(`[Upload] Account banned for "${file.name}":`, txt);
- completed++;
- updateStatus();
- return;
- }
- if (resp.ok && json?.assetId) {
- logAsset(json.assetId, null, displayName);
- completed++;
- updateStatus();
- return;
- }
- if (json?.message === 'Asset name length is invalid.' && !forceName && retries < 5) {
- console.warn(`[Upload] "${file.name}" name too long, retrying with default name. Retry ${retries + 1}.`);
- return uploadFile(file, assetType, retries + 1, true);
- }
- if (resp.status === 400 && json?.message?.includes('moderated') && retries < 5) {
- console.warn(`[Upload] "${file.name}" content moderated, retrying with default name. Retry ${retries + 1}.`);
- return uploadFile(file, assetType, retries + 1, true);
- }
- if (resp.status === 403 && retries < 5) {
- console.warn(`[Upload] "${file.name}" 403 Forbidden, fetching new CSRF and retrying. Retry ${retries + 1}.`);
- csrfToken = null;
- await fetchCSRFToken();
- return uploadFile(file, assetType, retries + 1, forceName);
- }
- console.error(`[Upload] failed "${file.name}" [${resp.status}]`, txt);
- completed++;
- updateStatus();
- } catch (e) {
- console.error(`[Upload] error during fetch for "${file.name}":`, e);
- completed++;
- updateStatus();
- }
- }
- function convertWebPToPng(webpFile) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = img.width;
- canvas.height = img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0);
- canvas.toBlob(blob => {
- if (blob) {
- const newFileName = webpFile.name.replace(/\.webp$/, '.png');
- resolve(new File([blob], newFileName, { type: 'image/png' }));
- } else {
- reject(new Error('Failed to convert WebP to PNG blob.'));
- }
- }, 'image/png');
- };
- img.onerror = (e) => {
- reject(new Error(`Failed to load image for conversion: ${e.message}`));
- };
- img.src = URL.createObjectURL(webpFile);
- });
- }
- // Resize image as a File to width x height (returns a new File)
- function resizeImageFile(file, width, height) {
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = width;
- canvas.height = height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0, width, height);
- canvas.toBlob(blob => {
- if (blob) {
- // Preserve base name, but new extension png
- const newFileName = baseName(file.name) + '.png';
- resolve(new File([blob], newFileName, { type: 'image/png' }));
- } else {
- reject(new Error('Failed to resize image.'));
- }
- }, 'image/png');
- };
- img.onerror = (e) => {
- reject(new Error(`Failed to load image for resizing: ${e.message}`));
- };
- img.src = URL.createObjectURL(file);
- });
- }
- function processImageThroughCanvas(file, targetType = 'image/png', width = null, height = null) {
- // Optionally resize if width/height provided
- return new Promise((resolve, reject) => {
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = width || img.width;
- canvas.height = height || img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
- canvas.toBlob(blob => {
- if (blob) {
- const newFileName = baseName(file.name) + (targetType === 'image/png' ? '.png' : '.jpeg');
- resolve(new File([blob], newFileName, { type: targetType }));
- } else {
- reject(new Error('Failed to process image through canvas.'));
- }
- }, targetType);
- };
- img.onerror = (e) => {
- reject(new Error(`Failed to load image for canvas processing: ${e.message}`));
- };
- img.src = URL.createObjectURL(file);
- });
- }
- function makeUniqueFile(file, origBase, copyIndex, resizeW = null, resizeH = null) {
- return new Promise(resolve => {
- const img = new Image();
- img.onload = () => {
- const canvas = document.createElement('canvas');
- canvas.width = resizeW || img.width;
- canvas.height = resizeH || img.height;
- const ctx = canvas.getContext('2d');
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
- const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const data = imageData.data;
- if (slipModePixelMethod === '1-4_random_single_pixel') {
- const pixelIndex = Math.floor(Math.random() * (data.length / 4)) * 4;
- if (data[pixelIndex + 3] !== 0) {
- const delta = (Math.random() < 0.5 ? -1 : 1) * (Math.floor(Math.random() * 4) + 1);
- data[pixelIndex] = Math.min(255, Math.max(0, data[pixelIndex] + delta));
- data[pixelIndex+1] = Math.min(255, Math.max(0, data[pixelIndex+1] + delta));
- data[pixelIndex+2] = Math.min(255, Math.max(0, data[pixelIndex+2] + delta));
- }
- } else if (slipModePixelMethod === 'random_single_pixel_full_random_color') {
- const pixelIndex = Math.floor(Math.random() * (data.length / 4)) * 4;
- if (data[pixelIndex + 3] !== 0) {
- data[pixelIndex] = Math.floor(Math.random() * 256);
- data[pixelIndex + 1] = Math.floor(Math.random() * 256);
- data[pixelIndex + 2] = Math.floor(Math.random() * 256);
- }
- }
- else {
- for (let i = 0; i < data.length; i += 4) {
- if (data[i + 3] !== 0) {
- let delta;
- if (slipModePixelMethod === 'all_pixels') {
- delta = (Math.random() < 0.5 ? -1 : 1);
- data[i] = Math.min(255, Math.max(0, data[i] + delta));
- data[i+1] = Math.min(255, Math.max(0, data[i+1] + delta));
- data[i+2] = Math.min(255, Math.max(0, data[i+2] + delta));
- } else if (slipModePixelMethod === '1-3_random') {
- delta = (Math.random() < 0.5 ? -1 : 1) * (Math.floor(Math.random() * 3) + 1);
- data[i] = Math.min(255, Math.max(0, data[i] + delta));
- data[i+1] = Math.min(255, Math.max(0, data[i+1] + delta));
- data[i+2] = Math.min(255, Math.max(0, data[i+2] + delta));
- }
- }
- }
- }
- ctx.putImageData(imageData, 0, 0);
- canvas.toBlob(blob => {
- const ext = 'png';
- const newName = `${origBase}_${copyIndex}.${ext}`;
- resolve(new File([blob], newName, { type: 'image/png' }));
- }, 'image/png');
- };
- img.src = URL.createObjectURL(file);
- });
- }
- async function handleFileSelect(files, assetType, both = false) {
- if (!files?.length) return;
- const downloadsMap = {};
- const copies = useMakeUnique ? uniqueCopies : 1;
- const resizeActive = enableResize && Number(resizeWidth) > 0 && Number(resizeHeight) > 0;
- if (massMode) {
- displayMessage('Processing files to add to queue...', 'info');
- const processingTasks = [];
- for (const original of files) {
- let fileToProcess = original;
- // 1. WebP Conversion
- if (original.type === 'image/webp') {
- displayMessage(`Converting ${original.name} from WebP to PNG...`, 'info');
- try {
- fileToProcess = await convertWebPToPng(original);
- displayMessage(`${original.name} converted to PNG.`, 'success');
- } catch (error) {
- displayMessage(`Failed to convert ${original.name}: ${error.message}`, 'error');
- console.error(`[Conversion] Failed to convert ${original.name}:`, error);
- continue;
- }
- }
- // 2. Optional resizing
- if (resizeActive) {
- displayMessage(`Resizing ${fileToProcess.name} to ${resizeWidth}x${resizeHeight}...`, 'info');
- try {
- fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
- displayMessage(`${fileToProcess.name} resized.`, 'success');
- } catch (error) {
- displayMessage(`Failed to resize ${fileToProcess.name}: ${error.message}`, 'error');
- console.error(`[Resize] Failed to resize ${fileToProcess.name}:`, error);
- continue;
- }
- }
- // 3. Force Canvas Upload
- let fileAfterCanvasProcessing = fileToProcess;
- if (useForceCanvasUpload && !useMakeUnique) {
- displayMessage(`Processing ${fileToProcess.name} through canvas...`, 'info');
- try {
- fileAfterCanvasProcessing = await processImageThroughCanvas(
- fileToProcess, 'image/png',
- resizeActive ? Number(resizeWidth) : null,
- resizeActive ? Number(resizeHeight) : null
- );
- displayMessage(`${fileToProcess.name} processed through canvas.`, 'success');
- } catch (error) {
- displayMessage(`Failed to process ${fileToProcess.name} through canvas: ${error.message}`, 'error');
- console.error(`[Canvas Process] Failed to process ${fileToProcess.name}:`, error);
- continue;
- }
- }
- const origBase = baseName(fileAfterCanvasProcessing.name);
- for (let i = 1; i <= copies; i++) {
- processingTasks.push(
- (async () => {
- const fileForQueue = useMakeUnique
- ? await makeUniqueFile(
- fileAfterCanvasProcessing, origBase, i,
- resizeActive ? Number(resizeWidth) : null,
- resizeActive ? Number(resizeHeight) : null
- )
- : fileAfterCanvasProcessing;
- if (both) {
- massQueue.push({ f: fileForQueue, type: ASSET_TYPE_TSHIRT, forceName: useForcedName });
- massQueue.push({ f: fileForQueue, type: ASSET_TYPE_DECAL, forceName: useForcedName });
- } else {
- massQueue.push({ f: fileForQueue, type: assetType, forceName: useForcedName });
- }
- })()
- );
- }
- }
- await Promise.all(processingTasks);
- displayMessage(`${processingTasks.length} files added to queue!`, 'success');
- updateStatus();
- } else {
- const totalFilesToUpload = files.length * (both ? 2 : 1) * copies;
- batchTotal = totalFilesToUpload;
- completed = 0;
- updateStatus();
- displayMessage(`Starting upload of ${batchTotal} files...`, 'info');
- const uploadPromises = [];
- for (const original of files) {
- let fileToProcess = original;
- // 1. WebP Conversion
- if (original.type === 'image/webp') {
- displayMessage(`Converting ${original.name} from WebP to PNG...`, 'info');
- try {
- fileToProcess = await convertWebPToPng(original);
- displayMessage(`${original.name} converted to PNG.`, 'success');
- } catch (error) {
- displayMessage(`Failed to convert ${original.name}: ${error.message}`, 'error');
- console.error(`[Conversion] Failed to convert ${original.name}:`, error);
- continue;
- }
- }
- // 2. Optional resizing
- if (resizeActive) {
- displayMessage(`Resizing ${fileToProcess.name} to ${resizeWidth}x${resizeHeight}...`, 'info');
- try {
- fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
- displayMessage(`${fileToProcess.name} resized.`, 'success');
- } catch (error) {
- displayMessage(`Failed to resize ${fileToProcess.name}: ${error.message}`, 'error');
- console.error(`[Resize] Failed to resize ${fileToProcess.name}:`, error);
- continue;
- }
- }
- // 3. Force Canvas Upload
- let fileAfterCanvasProcessing = fileToProcess;
- if (useForceCanvasUpload && !useMakeUnique) {
- displayMessage(`Processing ${fileToProcess.name} through canvas...`, 'info');
- try {
- fileAfterCanvasProcessing = await processImageThroughCanvas(
- fileToProcess, 'image/png',
- resizeActive ? Number(resizeWidth) : null,
- resizeActive ? Number(resizeHeight) : null
- );
- displayMessage(`${fileToProcess.name} processed through canvas.`, 'success');
- } catch (error) {
- displayMessage(`Failed to process ${fileToProcess.name} through canvas: ${error.message}`, 'error');
- console.error(`[Canvas Process] Failed to process ${fileToProcess.name}:`, error);
- continue;
- }
- }
- const origBase = baseName(fileAfterCanvasProcessing.name);
- downloadsMap[origBase] = [];
- for (let i = 1; i <= copies; i++) {
- const fileToUpload = useMakeUnique
- ? await makeUniqueFile(
- fileAfterCanvasProcessing, origBase, i,
- resizeActive ? Number(resizeWidth) : null,
- resizeActive ? Number(resizeHeight) : null
- )
- : fileAfterCanvasProcessing;
- if (useMakeUnique && useDownload) downloadsMap[origBase].push(fileToUpload);
- if (both) {
- uploadPromises.push(uploadFile(fileToUpload, ASSET_TYPE_TSHIRT, 0, useForcedName));
- uploadPromises.push(uploadFile(fileToUpload, ASSET_TYPE_DECAL, 0, useForcedName));
- } else {
- uploadPromises.push(uploadFile(fileToUpload, assetType, 0, useForcedName));
- }
- }
- }
- Promise.all(uploadPromises).then(() => {
- console.log('[Uploader] batch done');
- scanForAssets();
- displayMessage('Immediate upload batch complete!', 'success');
- if (useMakeUnique && useDownload) {
- for (const [origBase, fileList] of Object.entries(downloadsMap)) {
- if (!fileList.length) continue;
- const zip = new JSZip();
- fileList.forEach(f => zip.file(f.name, f));
- zip.generateAsync({ type: 'blob' }).then(blob => {
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${origBase}.zip`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- });
- }
- }
- }).catch(error => {
- console.error("Immediate upload batch encountered an error:", error);
- displayMessage('Immediate upload batch finished with errors. Check console.', 'error');
- });
- }
- }
- function startMassUpload() {
- if (!massQueue.length) {
- displayMessage('Nothing queued for mass upload!', 'info');
- return;
- }
- batchTotal = massQueue.length;
- completed = 0;
- updateStatus();
- displayMessage(`Starting mass upload of ${batchTotal} files...`, 'info');
- const tasks = massQueue.map(item => uploadFile(item.f, item.type, 0, item.forceName));
- massQueue = [];
- Promise.all(tasks).then(() => {
- displayMessage('Mass upload complete!', 'success');
- massMode = false;
- toggleBtn.textContent = 'Enable Mass Upload';
- startBtn.style.display = 'none';
- scanForAssets();
- batchTotal = completed = 0;
- updateStatus();
- }).catch(error => {
- console.error("Mass upload encountered an error:", error);
- displayMessage('Mass upload finished with errors. Check console.', 'error');
- massMode = false;
- toggleBtn.textContent = 'Enable Mass Upload';
- startBtn.style.display = 'none';
- batchTotal = completed = 0;
- updateStatus();
- });
- }
- function displayMessage(message, type = 'info') {
- const modal = document.createElement('div');
- Object.assign(modal.style, {
- position: 'fixed',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- padding: '20px',
- background: '#333',
- color: '#fff',
- borderRadius: '8px',
- boxShadow: '0 4px 10px rgba(0,0,0,0.5)',
- zIndex: '10001',
- fontFamily: 'Inter, Arial, sans-serif',
- textAlign: 'center',
- minWidth: '250px',
- transition: 'opacity 0.3s ease-in-out',
- opacity: '0'
- });
- if (type === 'success') {
- modal.style.background = '#4CAF50';
- } else if (type === 'error') {
- modal.style.background = '#f44336';
- }
- modal.textContent = message;
- document.body.appendChild(modal);
- setTimeout(() => modal.style.opacity = '1', 10);
- setTimeout(() => {
- modal.style.opacity = '0';
- modal.addEventListener('transitionend', () => modal.remove());
- }, 3000);
- }
- function customPrompt(message, defaultValue = '') {
- return new Promise(resolve => {
- const modal = document.createElement('div');
- Object.assign(modal.style, {
- position: 'fixed',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- padding: '20px',
- background: '#222',
- color: '#fff',
- borderRadius: '8px',
- boxShadow: '0 6px 15px rgba(0,0,0,0.4)',
- zIndex: '10002',
- fontFamily: 'Inter, Arial, sans-serif',
- textAlign: 'center',
- minWidth: '300px',
- display: 'flex',
- flexDirection: 'column',
- gap: '15px',
- transition: 'opacity 0.3s ease-in-out',
- opacity: '0'
- });
- const textDiv = document.createElement('div');
- textDiv.textContent = message;
- textDiv.style.fontSize = '16px';
- modal.appendChild(textDiv);
- const input = document.createElement('input');
- input.type = 'text';
- input.value = defaultValue;
- Object.assign(input.style, {
- padding: '10px',
- borderRadius: '5px',
- border: '1px solid #555',
- background: '#333',
- color: '#fff',
- fontSize: '14px',
- outline: 'none'
- });
- modal.appendChild(input);
- const buttonContainer = document.createElement('div');
- Object.assign(buttonContainer.style, {
- display: 'flex',
- justifyContent: 'space-around',
- gap: '10px',
- marginTop: '10px'
- });
- const okBtn = document.createElement('button');
- okBtn.textContent = 'OK';
- Object.assign(okBtn.style, {
- padding: '10px 20px',
- cursor: 'pointer',
- color: '#fff',
- background: '#007bff',
- border: 'none',
- borderRadius: '5px',
- fontSize: '14px',
- flexGrow: '1'
- });
- okBtn.onmouseover = () => okBtn.style.background = '#0056b3';
- okBtn.onmouseout = () => okBtn.style.background = '#007bff';
- okBtn.onclick = () => {
- modal.style.opacity = '0';
- modal.addEventListener('transitionend', () => modal.remove());
- resolve(input.value);
- };
- buttonContainer.appendChild(okBtn);
- const cancelBtn = document.createElement('button');
- cancelBtn.textContent = 'Cancel';
- Object.assign(cancelBtn.style, {
- padding: '10px 20px',
- cursor: 'pointer',
- color: '#fff',
- background: '#6c757d',
- border: 'none',
- borderRadius: '5px',
- fontSize: '14px',
- flexGrow: '1'
- });
- cancelBtn.onmouseover = () => cancelBtn.style.background = '#5a6268';
- cancelBtn.onmouseout = () => cancelBtn.style.background = '#6c757d';
- cancelBtn.onclick = () => {
- modal.style.opacity = '0';
- modal.addEventListener('transitionend', () => modal.remove());
- resolve(null);
- };
- buttonContainer.appendChild(cancelBtn);
- modal.appendChild(buttonContainer);
- document.body.appendChild(modal);
- setTimeout(() => modal.style.opacity = '1', 10);
- input.focus();
- input.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') {
- okBtn.click();
- }
- });
- });
- }
- function createStyledButton(text, fn) {
- const b = document.createElement('button');
- b.textContent = text;
- Object.assign(b.style, {
- padding: '10px',
- cursor: 'pointer',
- color: '#fff',
- background: '#3a3a3a',
- border: '1px solid #555',
- borderRadius: '5px',
- transition: 'background 0.2s ease-in-out',
- fontSize: '14px'
- });
- b.onmouseover = () => b.style.background = '#505050';
- b.onmouseout = () => b.style.background = '#3a3a3a';
- b.onclick = fn;
- return b;
- }
- function createUI() {
- uiContainer = document.createElement('div');
- Object.assign(uiContainer.style, {
- position: 'fixed',
- top: '10px',
- right: '10px',
- width: '280px', // Adjusted width
- background: '#1a1a1a',
- border: '2px solid #333',
- color: '#e0e0e0',
- padding: '15px 15px 15px 15px', // Adjusted padding
- zIndex: 10000,
- borderRadius: '8px',
- boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
- display: 'flex',
- flexDirection: 'column',
- gap: '10px',
- fontFamily: 'Inter, Arial, sans-serif',
- transition: 'top 0.3s ease-in-out'
- });
- // Close button
- const close = createStyledButton('×', () => uiContainer.remove());
- Object.assign(close.style, {
- position: 'absolute',
- top: '5px',
- right: '8px',
- background: 'transparent',
- border: 'none',
- fontSize: '18px',
- color: '#e0e0e0',
- fontWeight: 'bold',
- transition: 'color 0.2s',
- padding: '5px 8px'
- });
- close.onmouseover = () => close.style.color = '#fff';
- close.onmouseout = () => close.style.color = '#e0e0e0';
- close.title = 'Close AnnaUploader';
- uiContainer.appendChild(close);
- // Gear icon for settings
- const settingsGear = createStyledButton('⚙️', () => {
- createSettingsUI();
- });
- Object.assign(settingsGear.style, {
- position: 'absolute',
- top: '5px',
- left: '8px',
- background: 'transparent',
- border: 'none',
- fontSize: '18px',
- color: '#e0e0e0',
- fontWeight: 'bold',
- transition: 'color 0.2s',
- padding: '5px 8px',
- });
- settingsGear.onmouseover = () => settingsGear.style.color = '#fff';
- settingsGear.onmouseout = () => settingsGear.style.color = '#e0e0e0';
- settingsGear.title = 'Settings';
- uiContainer.appendChild(settingsGear);
- const title = document.createElement('h3');
- title.textContent = 'AnnaUploader';
- title.style.margin = '0 0 10px 0';
- title.style.color = '#4af';
- title.style.textAlign = 'center';
- uiContainer.appendChild(title);
- uiContainer.appendChild(createStyledButton('Upload T-Shirts', () => {
- const i = document.createElement('input');
- i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
- i.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT);
- i.click();
- }));
- uiContainer.appendChild(createStyledButton('Upload Decals', () => {
- const i = document.createElement('input');
- i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
- i.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL);
- i.click();
- }));
- uiContainer.appendChild(createStyledButton('Upload Both', () => {
- const i = document.createElement('input');
- i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
- i.onchange = e => handleFileSelect(e.target.files, null, true);
- i.click();
- }));
- toggleBtn = createStyledButton('Enable Mass Upload', () => {
- massMode = !massMode;
- toggleBtn.textContent = massMode ? 'Disable Mass Upload' : 'Enable Mass Upload';
- startBtn.style.display = massMode ? 'block' : 'none';
- massQueue = [];
- batchTotal = completed = 0;
- updateStatus();
- displayMessage(`Mass Upload Mode: ${massMode ? 'Enabled' : 'Disabled'}`, 'info');
- });
- uiContainer.appendChild(toggleBtn);
- startBtn = createStyledButton('Start Mass Upload', startMassUpload);
- startBtn.style.display = 'none';
- Object.assign(startBtn.style, {
- background: '#28a745',
- border: '1px solid #218838'
- });
- startBtn.onmouseover = () => startBtn.style.background = '#218838';
- startBtn.onmouseout = () => startBtn.style.background = '#28a745';
- uiContainer.appendChild(startBtn);
- const slipBtn = createStyledButton(`Slip Mode: ${useMakeUnique ? 'On' : 'Off'}`, () => {
- useMakeUnique = !useMakeUnique;
- GM_setValue('useMakeUnique', useMakeUnique);
- slipBtn.textContent = `Slip Mode: ${useMakeUnique ? 'On' : 'Off'}`;
- copiesInput.style.display = useMakeUnique ? 'block' : 'none';
- downloadBtn.style.display = useMakeUnique ? 'block' : 'none';
- if (!useMakeUnique) {
- useDownload = false;
- GM_setValue('useDownload', useDownload);
- downloadBtn.textContent = 'Download Images: Off';
- }
- });
- uiContainer.appendChild(slipBtn);
- copiesInput = document.createElement('input');
- copiesInput.type = 'number'; copiesInput.min = '1'; copiesInput.value = uniqueCopies;
- Object.assign(copiesInput.style, {
- width: '100%',
- boxSizing: 'border-box',
- display: useMakeUnique ? 'block' : 'none',
- padding: '8px',
- borderRadius: '4px',
- border: '1px solid #555',
- background: '#333',
- color: '#fff',
- textAlign: 'center'
- });
- copiesInput.onchange = e => {
- const v = parseInt(e.target.value, 10);
- if (v > 0) {
- uniqueCopies = v;
- GM_setValue('uniqueCopies', uniqueCopies);
- }
- else e.target.value = uniqueCopies;
- };
- uiContainer.appendChild(copiesInput);
- downloadBtn = createStyledButton(`Download Images: ${useDownload ? 'On' : 'Off'}`, () => {
- useDownload = !useDownload;
- GM_setValue('useDownload', useDownload);
- downloadBtn.textContent = `Download Images: ${useDownload ? 'On' : 'Off'}`;
- });
- downloadBtn.style.display = useMakeUnique ? 'block' : 'none';
- uiContainer.appendChild(downloadBtn);
- uiContainer.appendChild(createStyledButton('Change ID', async () => {
- const inp = await customPrompt("Enter your Roblox User ID/URL or Group URL:", USER_ID || '');
- if (inp === null) return;
- let id, isGrp = false;
- const um = inp.match(/users\/(\d+)/);
- const gm = inp.match(/communities\/(\d+)/);
- if (um) {
- id = um[1];
- } else if (gm) {
- id = gm[1];
- isGrp = true;
- } else {
- id = inp.trim();
- if (isNaN(id) || id === '') {
- displayMessage('Invalid input. Please enter a number or a valid URL.', 'error');
- return;
- }
- }
- USER_ID = Number(id);
- IS_GROUP = isGrp;
- GM_setValue('userId', USER_ID);
- GM_setValue('isGroup', IS_GROUP);
- displayMessage(`Set to ${isGrp ? 'Group' : 'User'} ID: ${USER_ID}`, 'success');
- }));
- const pm = window.location.pathname.match(/^\/users\/(\d+)\/profile/);
- if (pm) {
- uiContainer.appendChild(createStyledButton('Use This Profile as ID', () => {
- USER_ID = Number(pm[1]);
- IS_GROUP = false;
- GM_setValue('userId', USER_ID);
- GM_setValue('isGroup', IS_GROUP);
- displayMessage(`User ID set to ${USER_ID}`, 'success');
- }));
- }
- const gm = window.location.pathname.match(/^\/communities\/(\d+)/);
- if (gm) {
- uiContainer.appendChild(createStyledButton('Use This Group as ID', () => {
- USER_ID = Number(gm[1]);
- IS_GROUP = true;
- GM_setValue('userId', USER_ID);
- GM_setValue('isGroup', IS_GROUP);
- displayMessage(`Group ID set to ${USER_ID}`, 'success');
- }));
- }
- // Removed the old 'Settings' button here.
- const hint = document.createElement('div');
- hint.textContent = 'Paste images (Ctrl+V) to queue/upload';
- hint.style.fontSize = '12px'; hint.style.color = '#aaa';
- hint.style.textAlign = 'center';
- hint.style.marginTop = '5px';
- uiContainer.appendChild(hint);
- statusEl = document.createElement('div');
- statusEl.style.fontSize = '13px'; statusEl.style.color = '#fff';
- statusEl.style.textAlign = 'center';
- statusEl.style.paddingTop = '5px';
- statusEl.style.borderTop = '1px solid #333';
- uiContainer.appendChild(statusEl);
- document.body.appendChild(uiContainer);
- }
- function createSettingsUI() {
- if (settingsModal) {
- settingsModal.style.display = 'flex';
- return;
- }
- settingsModal = document.createElement('div');
- Object.assign(settingsModal.style, {
- position: 'fixed',
- top: '50%',
- left: '50%',
- transform: 'translate(-50%, -50%)',
- width: '300px',
- background: '#1a1a1a',
- border: '2px solid #333',
- color: '#e0e0e0',
- padding: '20px',
- zIndex: 10005,
- borderRadius: '10px',
- boxShadow: '0 6px 20px rgba(0,0,0,0.6)',
- display: 'flex',
- flexDirection: 'column',
- gap: '15px',
- fontFamily: 'Inter, Arial, sans-serif',
- });
- const closeSettings = createStyledButton('×', () => {
- settingsModal.style.display = 'none';
- });
- Object.assign(closeSettings.style, {
- position: 'absolute',
- top: '8px',
- right: '10px',
- background: 'transparent',
- border: 'none',
- fontSize: '20px',
- color: '#e0e0e0',
- fontWeight: 'bold',
- transition: 'color 0.2s',
- padding: '5px 10px'
- });
- closeSettings.onmouseover = () => closeSettings.style.color = '#fff';
- closeSettings.onmouseout = () => closeSettings.style.color = '#e0e0e0';
- closeSettings.title = 'Close Settings';
- settingsModal.appendChild(closeSettings);
- const title = document.createElement('h3');
- title.textContent = 'AnnaUploader Settings';
- title.style.margin = '0 0 15px 0';
- title.style.color = '#4af';
- title.style.textAlign = 'center';
- settingsModal.appendChild(title);
- const nameBtn = createStyledButton(`Use default Name: ${useForcedName ? 'On' : 'Off'}`, () => {
- useForcedName = !useForcedName;
- GM_setValue('useForcedName', useForcedName);
- nameBtn.textContent = `Use default Name: ${useForcedName ? 'On' : 'Off'}`;
- });
- settingsModal.appendChild(nameBtn);
- // Slip Mode Pixel Method setting
- const slipModePixelMethodLabel = document.createElement('label');
- slipModePixelMethodLabel.textContent = 'Slip Mode Pixel Method:';
- Object.assign(slipModePixelMethodLabel.style, {
- display: 'block',
- marginBottom: '5px',
- fontSize: '14px',
- color: '#bbb'
- });
- settingsModal.appendChild(slipModePixelMethodLabel);
- const slipModePixelMethodSelect = document.createElement('select');
- Object.assign(slipModePixelMethodSelect.style, {
- width: '100%',
- padding: '10px',
- borderRadius: '5px',
- border: '1px solid #555',
- background: '#333',
- color: '#fff',
- fontSize: '14px',
- outline: 'none',
- marginBottom: '10px'
- });
- const optionAll = document.createElement('option');
- optionAll.value = 'all_pixels';
- optionAll.textContent = 'All Pixels (±1)';
- slipModePixelMethodSelect.appendChild(optionAll);
- const optionRandom = document.createElement('option');
- optionRandom.value = '1-3_random';
- optionRandom.textContent = 'Random Pixels (±1-3)';
- slipModePixelMethodSelect.appendChild(optionRandom);
- const optionSingleRandom = document.createElement('option');
- optionSingleRandom.value = '1-4_random_single_pixel';
- optionSingleRandom.textContent = 'Single Random Pixel (±1-4)';
- slipModePixelMethodSelect.appendChild(optionSingleRandom);
- const optionFullRandomSinglePixel = document.createElement('option');
- optionFullRandomSinglePixel.value = 'random_single_pixel_full_random_color';
- optionFullRandomSinglePixel.textContent = 'Single Random Pixel (Full Random Color)';
- slipModePixelMethodSelect.appendChild(optionFullRandomSinglePixel);
- slipModePixelMethodSelect.value = slipModePixelMethod;
- slipModePixelMethodSelect.onchange = (e) => {
- slipModePixelMethod = e.target.value;
- GM_setValue('slipModePixelMethod', slipModePixelMethod);
- displayMessage(`Slip Mode Pixel Method set to: ${e.target.options[e.target.selectedIndex].text}`, 'success');
- };
- settingsModal.appendChild(slipModePixelMethodSelect);
- // Force Upload (through Canvas) toggle
- const forceUploadBtn = createStyledButton(`Force Upload: ${useForceCanvasUpload ? 'On' : 'Off'}`, () => {
- useForceCanvasUpload = !useForceCanvasUpload;
- GM_setValue('useForceCanvasUpload', useForceCanvasUpload);
- forceUploadBtn.textContent = `Force Upload: ${useForceCanvasUpload ? 'On' : 'Off'}`;
- displayMessage(`Force Upload Mode: ${useForceCanvasUpload ? 'Enabled' : 'Disabled'}`, 'info');
- });
- settingsModal.appendChild(forceUploadBtn);
- // IMAGE RESIZE FEATURE
- const resizeContainer = document.createElement('div');
- resizeContainer.style.display = 'flex';
- resizeContainer.style.flexDirection = 'column';
- resizeContainer.style.gap = '5px';
- resizeContainer.style.margin = '10px 0';
- const resizeToggleBtn = createStyledButton(`Resize Images: ${enableResize ? 'On' : 'Off'}`, () => {
- enableResize = !enableResize;
- GM_setValue('enableResize', enableResize);
- resizeToggleBtn.textContent = `Resize Images: ${enableResize ? 'On' : 'Off'}`;
- widthInput.disabled = heightInput.disabled = !enableResize;
- });
- resizeContainer.appendChild(resizeToggleBtn);
- // Input fields for width/height
- const inputRow = document.createElement('div');
- inputRow.style.display = 'flex';
- inputRow.style.gap = '7px';
- inputRow.style.alignItems = 'center';
- const widthInput = document.createElement('input');
- widthInput.type = 'number';
- widthInput.min = '1';
- widthInput.value = resizeWidth;
- widthInput.placeholder = 'Width';
- widthInput.style.width = '60px';
- widthInput.style.padding = '6px';
- widthInput.style.borderRadius = '4px';
- widthInput.style.border = '1px solid #555';
- widthInput.style.background = '#333';
- widthInput.style.color = '#fff';
- widthInput.disabled = !enableResize;
- widthInput.onchange = () => {
- let val = Math.max(1, parseInt(widthInput.value, 10) || 512);
- widthInput.value = val;
- resizeWidth = val;
- GM_setValue('resizeWidth', resizeWidth);
- };
- inputRow.appendChild(widthInput);
- const xLabel = document.createElement('span');
- xLabel.textContent = '×';
- xLabel.style.color = '#ccc';
- inputRow.appendChild(xLabel);
- const heightInput = document.createElement('input');
- heightInput.type = 'number';
- heightInput.min = '1';
- heightInput.value = resizeHeight;
- heightInput.placeholder = 'Height';
- heightInput.style.width = '60px';
- heightInput.style.padding = '6px';
- heightInput.style.borderRadius = '4px';
- heightInput.style.border = '1px solid #555';
- heightInput.style.background = '#333';
- heightInput.style.color = '#fff';
- heightInput.disabled = !enableResize;
- heightInput.onchange = () => {
- let val = Math.max(1, parseInt(heightInput.value, 10) || 512);
- heightInput.value = val;
- resizeHeight = val;
- GM_setValue('resizeHeight', resizeHeight);
- };
- inputRow.appendChild(heightInput);
- const pxLabel = document.createElement('span');
- pxLabel.textContent = 'px';
- pxLabel.style.color = '#bbb';
- inputRow.appendChild(pxLabel);
- resizeContainer.appendChild(inputRow);
- const resizeDesc = document.createElement('div');
- resizeDesc.textContent = "If enabled, images will be resized before upload. Applies to Slip Mode too.";
- resizeDesc.style.fontSize = '12px';
- resizeDesc.style.color = '#aaa';
- resizeDesc.style.marginTop = '3px';
- resizeContainer.appendChild(resizeDesc);
- settingsModal.appendChild(resizeContainer);
- settingsModal.appendChild(createStyledButton('Show Logged Assets', () => {
- const log = loadLog();
- const entries = Object.entries(log);
- const w = window.open('', '_blank');
- w.document.write(`<!DOCTYPE html>
- <html><head><meta charset="utf-8"><title>Logged Assets</title>
- <style>
- body { font-family:Arial; padding:20px; background:#121212; color:#f0f0f0; }
- h1 { margin-bottom:15px; color:#4af; }
- ul { list-style:none; padding:0; }
- li { margin-bottom:15px; padding:10px; background:#1e1e1e; border-radius:8px; display:flex; flex-direction:column; gap:8px;}
- img { max-height:60px; border:1px solid #444; border-radius:4px; object-fit:contain; background:#333; }
- .asset-info { display:flex;align-items:center;gap:15px; }
- a { color:#7cf; text-decoration:none; font-weight:bold; }
- a:hover { text-decoration:underline; }
- .asset-name { font-size:0.9em; color:#bbb; margin-left: auto; text-align: right; }
- button { margin-bottom:20px; color:#fff; background:#3a3a3a; border:1px solid #555; padding:8px 15px; border-radius:5px; cursor:pointer; }
- button:hover { background:#505050; }
- </style></head><body>
- <button onclick="document.body.style.background=(document.body.style.background==='#121212'?'#f0f0f0':'#121212');document.body.style.color=(document.body.style.color==='#f0f0f0'?'#121212':'#f0f0f0');d[...]
- <h1>Logged Assets</h1>
- ${ entries.length ? `<ul>${entries.map(([id,entry])=>
- `<li>
- <div class="asset-info">
- ${ entry.image ? `<img src="${entry.image}" alt="Asset thumbnail">` : `<span style="color:#888;">(no image)</span>` }
- <a href="https://create.roblox.com/store/asset/${id}" target="_blank">${id}</a>
- <span style="font-size:0.85em; color:#999;">${new Date(entry.date).toLocaleString()}</span>
- </div>
- <div class="asset-name">${entry.name}</div>
- </li>`).join('') }</ul>` : `<p style="color:#888;"><em>No assets logged yet.</em></p>`}
- </body></html>`);
- w.document.close();
- }));
- document.body.appendChild(settingsModal);
- }
- async function handlePaste(e) {
- const items = e.clipboardData?.items;
- if (!items) return;
- const resizeActive = enableResize && Number(resizeWidth) > 0 && Number(resizeHeight) > 0;
- for (const it of items) {
- if (it.type.startsWith('image')) {
- e.preventDefault();
- const blob = it.getAsFile();
- const ts = new Date().toISOString().replace(/[^a-z0-9]/gi,'_');
- const pastedName = await customPrompt('Enter a name for the image (no extension):', `pasted_${ts}`);
- if (pastedName === null) return;
- let name = pastedName.trim() || `pasted_${ts}`;
- let filename = name.endsWith('.png') ? name : `${name}.png`;
- let fileToProcess = new File([blob], filename, {type: blob.type});
- if (blob.type === 'image/webp') {
- displayMessage(`Converting pasted WebP image to PNG...`, 'info');
- try {
- fileToProcess = await convertWebPToPng(fileToProcess);
- name = baseName(fileToProcess.name);
- filename = fileToProcess.name;
- displayMessage(`Pasted WebP converted to PNG.`, 'success');
- } catch (error) {
- displayMessage(`Failed to convert pasted WebP: ${error.message}`, 'error');
- console.error(`[Conversion] Failed to convert pasted WebP:`, error);
- return;
- }
- }
- // Resize if enabled
- if (resizeActive) {
- displayMessage(`Resizing pasted image to ${resizeWidth}x${resizeHeight}...`, 'info');
- try {
- fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
- name = baseName(fileToProcess.name);
- filename = fileToProcess.name;
- displayMessage(`Pasted image resized.`, 'success');
- } catch (error) {
- displayMessage(`Failed to resize pasted image: ${error.message}`, 'error');
- console.error(`[Resize] Failed to resize pasted image:`, error);
- return;
- }
- }
- if (useForceCanvasUpload) {
- displayMessage(`Processing pasted image through canvas...`, 'info');
- try {
- fileToProcess = await processImageThroughCanvas(
- fileToProcess, 'image/png',
- resizeActive ? Number(resizeWidth) : null,
- resizeActive ? Number(resizeHeight) : null
- );
- name = baseName(fileToProcess.name);
- filename = fileToProcess.name;
- displayMessage(`Pasted image processed through canvas.`, 'success');
- } catch (error) {
- displayMessage(`Failed to process pasted image through canvas: ${error.message}`, 'error');
- console.error(`[Canvas Process] Failed to process pasted image:`, error);
- return;
- }
- }
- const typeChoice = await customPrompt('Upload as T=T-Shirt, D=Decal, B=Both, or C=Cancel?', 'D');
- if (!typeChoice) return;
- const t = typeChoice.trim().toUpperCase();
- let uploadAsBoth = false;
- let type = null;
- if (t === 'T') {
- type = ASSET_TYPE_TSHIRT;
- } else if (t === 'D') {
- type = ASSET_TYPE_DECAL;
- } else if (t === 'B') {
- uploadAsBoth = true;
- } else {
- displayMessage('Invalid asset type selected. Please choose T, D, or B.', 'error');
- return;
- }
- handleFileSelect([fileToProcess], type, uploadAsBoth);
- break;
- }
- }
- }
- window.addEventListener('load', () => {
- createUI();
- document.addEventListener('paste', handlePaste);
- scanForAssets();
- console.log('[AnnaUploader] initialized; asset scan every ' + (SCAN_INTERVAL_MS/1000) + 's');
- });
- })();