AnnaUploader (Roblox Multi-File Uploader)

allows you to upload multiple T-Shirts/Decals easily with AnnaUploader; now supports image resizing

  1. // ==UserScript==
  2. // @name AnnaUploader (Roblox Multi-File Uploader)
  3. // @namespace https://github.com/AnnaRoblox
  4. // @version 7.1
  5. // @description allows you to upload multiple T-Shirts/Decals easily with AnnaUploader; now supports image resizing
  6. // @match https://create.roblox.com/*
  7. // @match https://www.roblox.com/users/*/profile*
  8. // @match https://www.roblox.com/communities/*
  9. // @match https://www.roblox.com/home/*
  10. // @run-at document-idle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Constants for Roblox API and asset types
  22. const ROBLOX_UPLOAD_URL = "https://apis.roblox.com/assets/user-auth/v1/assets";
  23. const ASSET_TYPE_TSHIRT = 11;
  24. const ASSET_TYPE_DECAL = 13;
  25. const FORCED_NAME = "Uploaded Using AnnaUploader"; // Default name for assets
  26.  
  27. // Storage keys and scan interval for asset logging
  28. const STORAGE_KEY = 'annaUploaderAssetLog';
  29. const SCAN_INTERVAL_MS = 10_000;
  30.  
  31. // Script configuration variables, managed with Tampermonkey's GM_getValue/GM_setValue
  32. let USER_ID = GM_getValue('userId', null);
  33. let IS_GROUP = GM_getValue('isGroup', false);
  34. let useForcedName = GM_getValue('useForcedName', false); // Persist this setting
  35. let useMakeUnique = GM_getValue('useMakeUnique', false); // Persist this setting
  36. let uniqueCopies = GM_getValue('uniqueCopies', 1); // Persist this setting
  37. let useDownload = GM_getValue('useDownload', false); // Persist this setting
  38. let useForceCanvasUpload = GM_getValue('useForceCanvasUpload', false); // Persist this setting
  39. // NEW SETTING: Slip Mode Pixel Method - 'all_pixels', '1-3_random', '1-4_random_single_pixel', or 'random_single_pixel_full_random_color'
  40. let slipModePixelMethod = GM_getValue('slipModePixelMethod', '1-3_random');
  41.  
  42. // NEW: Image resizing settings
  43. let enableResize = GM_getValue('enableResize', false);
  44. let resizeWidth = GM_getValue('resizeWidth', 300);
  45. let resizeHeight = GM_getValue('resizeHeight', 300);
  46.  
  47. // Mass upload mode variables
  48. let massMode = false; // True if mass upload mode is active
  49. let massQueue = []; // Array to hold files/metadata for mass upload
  50. let batchTotal = 0; // Total items to process in current batch/queue
  51. let completed = 0; // Number of items completed in current batch/queue
  52.  
  53. let csrfToken = null; // Roblox CSRF token for authenticated requests
  54. let statusEl, toggleBtn, startBtn, copiesInput, downloadBtn; // UI elements (removed forceUploadBtn from here)
  55. let uiContainer; // Reference to the main UI container element
  56. let settingsModal; // Reference to the settings modal element
  57.  
  58. function baseName(filename) {
  59. return filename.replace(/\.[^/.]+$/, '');
  60. }
  61.  
  62. function loadLog() {
  63. const raw = GM_getValue(STORAGE_KEY, '{}');
  64. try { return JSON.parse(raw); }
  65. catch { return {}; }
  66. }
  67.  
  68. function saveLog(log) {
  69. GM_setValue(STORAGE_KEY, JSON.stringify(log));
  70. }
  71.  
  72. function logAsset(id, imageURL, name) {
  73. const log = loadLog();
  74. log[id] = {
  75. date: new Date().toISOString(),
  76. image: imageURL || log[id]?.image || null,
  77. name: name || log[id]?.name || '(unknown)'
  78. };
  79. saveLog(log);
  80. console.log(`[AssetLogger] logged asset ${id} at ${log[id].date}, name: ${log[id].name}, image: ${log[id].image || "none"}`);
  81. }
  82.  
  83. function scanForAssets() {
  84. console.log('[AssetLogger] scanning for assets…');
  85. document.querySelectorAll('[href]').forEach(el => {
  86. let m = el.href.match(/(?:https?:\/\/create\.roblox\.com)?\/store\/asset\/(\d+)/)
  87. || el.href.match(/\/dashboard\/creations\/store\/(\d+)\/configure/);
  88. if (m) {
  89. const id = m[1];
  90. let image = null;
  91. const container = el.closest('*');
  92. const img = container?.querySelector('img');
  93. if (img?.src) image = img.src;
  94. let name = null;
  95. const nameEl = container?.querySelector('span.MuiTypography-root');
  96. if (nameEl) name = nameEl.textContent.trim();
  97. logAsset(id, image, name);
  98. }
  99. });
  100. }
  101. setInterval(scanForAssets, SCAN_INTERVAL_MS);
  102.  
  103. async function fetchCSRFToken() {
  104. const resp = await fetch(ROBLOX_UPLOAD_URL, {
  105. method: 'POST',
  106. credentials: 'include',
  107. headers: { 'Content-Type': 'application/json' },
  108. body: JSON.stringify({})
  109. });
  110. if (resp.status === 403) {
  111. const tok = resp.headers.get('x-csrf-token');
  112. if (tok) {
  113. csrfToken = tok;
  114. console.log('[CSRF] token fetched');
  115. return tok;
  116. }
  117. }
  118. throw new Error('Cannot fetch CSRF token');
  119. }
  120.  
  121. function updateStatus() {
  122. if (!statusEl) return;
  123. if (massMode) {
  124. statusEl.textContent = `${massQueue.length} queued`;
  125. } else if (batchTotal > 0) {
  126. statusEl.textContent = `${completed} of ${batchTotal} processed`;
  127. } else {
  128. statusEl.textContent = '';
  129. }
  130. }
  131.  
  132. async function uploadFile(file, assetType, retries = 0, forceName = false) {
  133. if (!csrfToken) {
  134. try {
  135. await fetchCSRFToken();
  136. } catch (e) {
  137. console.error("[Upload] Failed to fetch initial CSRF token:", e);
  138. completed++;
  139. updateStatus();
  140. return;
  141. }
  142. }
  143. const displayName = forceName ? FORCED_NAME : baseName(file.name);
  144. const creator = IS_GROUP
  145. ? { groupId: USER_ID }
  146. : { userId: USER_ID };
  147.  
  148. const fd = new FormData();
  149. fd.append('fileContent', file, file.name);
  150. fd.append('request', JSON.stringify({
  151. displayName,
  152. description: FORCED_NAME,
  153. assetType: assetType === ASSET_TYPE_TSHIRT ? "TShirt" : "Decal",
  154. creationContext: { creator, expectedPrice: 0 }
  155. }));
  156.  
  157. try {
  158. const resp = await fetch(ROBLOX_UPLOAD_URL, {
  159. method: 'POST',
  160. credentials: 'include',
  161. headers: { 'x-csrf-token': csrfToken },
  162. body: fd
  163. });
  164. const txt = await resp.text();
  165. let json; try { json = JSON.parse(txt); } catch (e) {
  166. console.error('[Upload] Failed to parse response JSON:', e, txt);
  167. }
  168.  
  169. if (json?.message && typeof json.message === 'string' && json.message.toLowerCase().includes('banned')) {
  170. displayMessage('Upload failed: Your account appears to be banned. Cannot complete upload.', 'error');
  171. console.error(`[Upload] Account banned for "${file.name}":`, txt);
  172. completed++;
  173. updateStatus();
  174. return;
  175. }
  176.  
  177. if (resp.ok && json?.assetId) {
  178. logAsset(json.assetId, null, displayName);
  179. completed++;
  180. updateStatus();
  181. return;
  182. }
  183.  
  184. if (json?.message === 'Asset name length is invalid.' && !forceName && retries < 5) {
  185. console.warn(`[Upload] "${file.name}" name too long, retrying with default name. Retry ${retries + 1}.`);
  186. return uploadFile(file, assetType, retries + 1, true);
  187. }
  188. if (resp.status === 400 && json?.message?.includes('moderated') && retries < 5) {
  189. console.warn(`[Upload] "${file.name}" content moderated, retrying with default name. Retry ${retries + 1}.`);
  190. return uploadFile(file, assetType, retries + 1, true);
  191. }
  192. if (resp.status === 403 && retries < 5) {
  193. console.warn(`[Upload] "${file.name}" 403 Forbidden, fetching new CSRF and retrying. Retry ${retries + 1}.`);
  194. csrfToken = null;
  195. await fetchCSRFToken();
  196. return uploadFile(file, assetType, retries + 1, forceName);
  197. }
  198.  
  199. console.error(`[Upload] failed "${file.name}" [${resp.status}]`, txt);
  200. completed++;
  201. updateStatus();
  202. } catch (e) {
  203. console.error(`[Upload] error during fetch for "${file.name}":`, e);
  204. completed++;
  205. updateStatus();
  206. }
  207. }
  208.  
  209. function convertWebPToPng(webpFile) {
  210. return new Promise((resolve, reject) => {
  211. const img = new Image();
  212. img.onload = () => {
  213. const canvas = document.createElement('canvas');
  214. canvas.width = img.width;
  215. canvas.height = img.height;
  216. const ctx = canvas.getContext('2d');
  217. ctx.drawImage(img, 0, 0);
  218.  
  219. canvas.toBlob(blob => {
  220. if (blob) {
  221. const newFileName = webpFile.name.replace(/\.webp$/, '.png');
  222. resolve(new File([blob], newFileName, { type: 'image/png' }));
  223. } else {
  224. reject(new Error('Failed to convert WebP to PNG blob.'));
  225. }
  226. }, 'image/png');
  227. };
  228. img.onerror = (e) => {
  229. reject(new Error(`Failed to load image for conversion: ${e.message}`));
  230. };
  231. img.src = URL.createObjectURL(webpFile);
  232. });
  233. }
  234.  
  235. // Resize image as a File to width x height (returns a new File)
  236. function resizeImageFile(file, width, height) {
  237. return new Promise((resolve, reject) => {
  238. const img = new Image();
  239. img.onload = () => {
  240. const canvas = document.createElement('canvas');
  241. canvas.width = width;
  242. canvas.height = height;
  243. const ctx = canvas.getContext('2d');
  244. ctx.drawImage(img, 0, 0, width, height);
  245. canvas.toBlob(blob => {
  246. if (blob) {
  247. // Preserve base name, but new extension png
  248. const newFileName = baseName(file.name) + '.png';
  249. resolve(new File([blob], newFileName, { type: 'image/png' }));
  250. } else {
  251. reject(new Error('Failed to resize image.'));
  252. }
  253. }, 'image/png');
  254. };
  255. img.onerror = (e) => {
  256. reject(new Error(`Failed to load image for resizing: ${e.message}`));
  257. };
  258. img.src = URL.createObjectURL(file);
  259. });
  260. }
  261.  
  262. function processImageThroughCanvas(file, targetType = 'image/png', width = null, height = null) {
  263. // Optionally resize if width/height provided
  264. return new Promise((resolve, reject) => {
  265. const img = new Image();
  266. img.onload = () => {
  267. const canvas = document.createElement('canvas');
  268. canvas.width = width || img.width;
  269. canvas.height = height || img.height;
  270. const ctx = canvas.getContext('2d');
  271. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  272. canvas.toBlob(blob => {
  273. if (blob) {
  274. const newFileName = baseName(file.name) + (targetType === 'image/png' ? '.png' : '.jpeg');
  275. resolve(new File([blob], newFileName, { type: targetType }));
  276. } else {
  277. reject(new Error('Failed to process image through canvas.'));
  278. }
  279. }, targetType);
  280. };
  281. img.onerror = (e) => {
  282. reject(new Error(`Failed to load image for canvas processing: ${e.message}`));
  283. };
  284. img.src = URL.createObjectURL(file);
  285. });
  286. }
  287.  
  288. function makeUniqueFile(file, origBase, copyIndex, resizeW = null, resizeH = null) {
  289. return new Promise(resolve => {
  290. const img = new Image();
  291. img.onload = () => {
  292. const canvas = document.createElement('canvas');
  293. canvas.width = resizeW || img.width;
  294. canvas.height = resizeH || img.height;
  295. const ctx = canvas.getContext('2d');
  296. ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
  297.  
  298. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  299. const data = imageData.data;
  300.  
  301. if (slipModePixelMethod === '1-4_random_single_pixel') {
  302. const pixelIndex = Math.floor(Math.random() * (data.length / 4)) * 4;
  303. if (data[pixelIndex + 3] !== 0) {
  304. const delta = (Math.random() < 0.5 ? -1 : 1) * (Math.floor(Math.random() * 4) + 1);
  305. data[pixelIndex] = Math.min(255, Math.max(0, data[pixelIndex] + delta));
  306. data[pixelIndex+1] = Math.min(255, Math.max(0, data[pixelIndex+1] + delta));
  307. data[pixelIndex+2] = Math.min(255, Math.max(0, data[pixelIndex+2] + delta));
  308. }
  309. } else if (slipModePixelMethod === 'random_single_pixel_full_random_color') {
  310. const pixelIndex = Math.floor(Math.random() * (data.length / 4)) * 4;
  311. if (data[pixelIndex + 3] !== 0) {
  312. data[pixelIndex] = Math.floor(Math.random() * 256);
  313. data[pixelIndex + 1] = Math.floor(Math.random() * 256);
  314. data[pixelIndex + 2] = Math.floor(Math.random() * 256);
  315. }
  316. }
  317. else {
  318. for (let i = 0; i < data.length; i += 4) {
  319. if (data[i + 3] !== 0) {
  320. let delta;
  321. if (slipModePixelMethod === 'all_pixels') {
  322. delta = (Math.random() < 0.5 ? -1 : 1);
  323. data[i] = Math.min(255, Math.max(0, data[i] + delta));
  324. data[i+1] = Math.min(255, Math.max(0, data[i+1] + delta));
  325. data[i+2] = Math.min(255, Math.max(0, data[i+2] + delta));
  326. } else if (slipModePixelMethod === '1-3_random') {
  327. delta = (Math.random() < 0.5 ? -1 : 1) * (Math.floor(Math.random() * 3) + 1);
  328. data[i] = Math.min(255, Math.max(0, data[i] + delta));
  329. data[i+1] = Math.min(255, Math.max(0, data[i+1] + delta));
  330. data[i+2] = Math.min(255, Math.max(0, data[i+2] + delta));
  331. }
  332. }
  333. }
  334. }
  335. ctx.putImageData(imageData, 0, 0);
  336.  
  337. canvas.toBlob(blob => {
  338. const ext = 'png';
  339. const newName = `${origBase}_${copyIndex}.${ext}`;
  340. resolve(new File([blob], newName, { type: 'image/png' }));
  341. }, 'image/png');
  342. };
  343. img.src = URL.createObjectURL(file);
  344. });
  345. }
  346.  
  347. async function handleFileSelect(files, assetType, both = false) {
  348. if (!files?.length) return;
  349.  
  350. const downloadsMap = {};
  351. const copies = useMakeUnique ? uniqueCopies : 1;
  352. const resizeActive = enableResize && Number(resizeWidth) > 0 && Number(resizeHeight) > 0;
  353.  
  354. if (massMode) {
  355. displayMessage('Processing files to add to queue...', 'info');
  356. const processingTasks = [];
  357. for (const original of files) {
  358. let fileToProcess = original;
  359.  
  360. // 1. WebP Conversion
  361. if (original.type === 'image/webp') {
  362. displayMessage(`Converting ${original.name} from WebP to PNG...`, 'info');
  363. try {
  364. fileToProcess = await convertWebPToPng(original);
  365. displayMessage(`${original.name} converted to PNG.`, 'success');
  366. } catch (error) {
  367. displayMessage(`Failed to convert ${original.name}: ${error.message}`, 'error');
  368. console.error(`[Conversion] Failed to convert ${original.name}:`, error);
  369. continue;
  370. }
  371. }
  372.  
  373. // 2. Optional resizing
  374. if (resizeActive) {
  375. displayMessage(`Resizing ${fileToProcess.name} to ${resizeWidth}x${resizeHeight}...`, 'info');
  376. try {
  377. fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
  378. displayMessage(`${fileToProcess.name} resized.`, 'success');
  379. } catch (error) {
  380. displayMessage(`Failed to resize ${fileToProcess.name}: ${error.message}`, 'error');
  381. console.error(`[Resize] Failed to resize ${fileToProcess.name}:`, error);
  382. continue;
  383. }
  384. }
  385.  
  386. // 3. Force Canvas Upload
  387. let fileAfterCanvasProcessing = fileToProcess;
  388. if (useForceCanvasUpload && !useMakeUnique) {
  389. displayMessage(`Processing ${fileToProcess.name} through canvas...`, 'info');
  390. try {
  391. fileAfterCanvasProcessing = await processImageThroughCanvas(
  392. fileToProcess, 'image/png',
  393. resizeActive ? Number(resizeWidth) : null,
  394. resizeActive ? Number(resizeHeight) : null
  395. );
  396. displayMessage(`${fileToProcess.name} processed through canvas.`, 'success');
  397. } catch (error) {
  398. displayMessage(`Failed to process ${fileToProcess.name} through canvas: ${error.message}`, 'error');
  399. console.error(`[Canvas Process] Failed to process ${fileToProcess.name}:`, error);
  400. continue;
  401. }
  402. }
  403.  
  404. const origBase = baseName(fileAfterCanvasProcessing.name);
  405. for (let i = 1; i <= copies; i++) {
  406. processingTasks.push(
  407. (async () => {
  408. const fileForQueue = useMakeUnique
  409. ? await makeUniqueFile(
  410. fileAfterCanvasProcessing, origBase, i,
  411. resizeActive ? Number(resizeWidth) : null,
  412. resizeActive ? Number(resizeHeight) : null
  413. )
  414. : fileAfterCanvasProcessing;
  415.  
  416. if (both) {
  417. massQueue.push({ f: fileForQueue, type: ASSET_TYPE_TSHIRT, forceName: useForcedName });
  418. massQueue.push({ f: fileForQueue, type: ASSET_TYPE_DECAL, forceName: useForcedName });
  419. } else {
  420. massQueue.push({ f: fileForQueue, type: assetType, forceName: useForcedName });
  421. }
  422. })()
  423. );
  424. }
  425. }
  426. await Promise.all(processingTasks);
  427. displayMessage(`${processingTasks.length} files added to queue!`, 'success');
  428. updateStatus();
  429. } else {
  430. const totalFilesToUpload = files.length * (both ? 2 : 1) * copies;
  431. batchTotal = totalFilesToUpload;
  432. completed = 0;
  433. updateStatus();
  434. displayMessage(`Starting upload of ${batchTotal} files...`, 'info');
  435.  
  436. const uploadPromises = [];
  437.  
  438. for (const original of files) {
  439. let fileToProcess = original;
  440.  
  441. // 1. WebP Conversion
  442. if (original.type === 'image/webp') {
  443. displayMessage(`Converting ${original.name} from WebP to PNG...`, 'info');
  444. try {
  445. fileToProcess = await convertWebPToPng(original);
  446. displayMessage(`${original.name} converted to PNG.`, 'success');
  447. } catch (error) {
  448. displayMessage(`Failed to convert ${original.name}: ${error.message}`, 'error');
  449. console.error(`[Conversion] Failed to convert ${original.name}:`, error);
  450. continue;
  451. }
  452. }
  453.  
  454. // 2. Optional resizing
  455. if (resizeActive) {
  456. displayMessage(`Resizing ${fileToProcess.name} to ${resizeWidth}x${resizeHeight}...`, 'info');
  457. try {
  458. fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
  459. displayMessage(`${fileToProcess.name} resized.`, 'success');
  460. } catch (error) {
  461. displayMessage(`Failed to resize ${fileToProcess.name}: ${error.message}`, 'error');
  462. console.error(`[Resize] Failed to resize ${fileToProcess.name}:`, error);
  463. continue;
  464. }
  465. }
  466.  
  467. // 3. Force Canvas Upload
  468. let fileAfterCanvasProcessing = fileToProcess;
  469. if (useForceCanvasUpload && !useMakeUnique) {
  470. displayMessage(`Processing ${fileToProcess.name} through canvas...`, 'info');
  471. try {
  472. fileAfterCanvasProcessing = await processImageThroughCanvas(
  473. fileToProcess, 'image/png',
  474. resizeActive ? Number(resizeWidth) : null,
  475. resizeActive ? Number(resizeHeight) : null
  476. );
  477. displayMessage(`${fileToProcess.name} processed through canvas.`, 'success');
  478. } catch (error) {
  479. displayMessage(`Failed to process ${fileToProcess.name} through canvas: ${error.message}`, 'error');
  480. console.error(`[Canvas Process] Failed to process ${fileToProcess.name}:`, error);
  481. continue;
  482. }
  483. }
  484.  
  485. const origBase = baseName(fileAfterCanvasProcessing.name);
  486. downloadsMap[origBase] = [];
  487.  
  488. for (let i = 1; i <= copies; i++) {
  489. const fileToUpload = useMakeUnique
  490. ? await makeUniqueFile(
  491. fileAfterCanvasProcessing, origBase, i,
  492. resizeActive ? Number(resizeWidth) : null,
  493. resizeActive ? Number(resizeHeight) : null
  494. )
  495. : fileAfterCanvasProcessing;
  496.  
  497. if (useMakeUnique && useDownload) downloadsMap[origBase].push(fileToUpload);
  498. if (both) {
  499. uploadPromises.push(uploadFile(fileToUpload, ASSET_TYPE_TSHIRT, 0, useForcedName));
  500. uploadPromises.push(uploadFile(fileToUpload, ASSET_TYPE_DECAL, 0, useForcedName));
  501. } else {
  502. uploadPromises.push(uploadFile(fileToUpload, assetType, 0, useForcedName));
  503. }
  504. }
  505. }
  506.  
  507. Promise.all(uploadPromises).then(() => {
  508. console.log('[Uploader] batch done');
  509. scanForAssets();
  510. displayMessage('Immediate upload batch complete!', 'success');
  511. if (useMakeUnique && useDownload) {
  512. for (const [origBase, fileList] of Object.entries(downloadsMap)) {
  513. if (!fileList.length) continue;
  514. const zip = new JSZip();
  515. fileList.forEach(f => zip.file(f.name, f));
  516. zip.generateAsync({ type: 'blob' }).then(blob => {
  517. const url = URL.createObjectURL(blob);
  518. const a = document.createElement('a');
  519. a.href = url;
  520. a.download = `${origBase}.zip`;
  521. document.body.appendChild(a);
  522. a.click();
  523. document.body.removeChild(a);
  524. URL.revokeObjectURL(url);
  525. });
  526. }
  527. }
  528. }).catch(error => {
  529. console.error("Immediate upload batch encountered an error:", error);
  530. displayMessage('Immediate upload batch finished with errors. Check console.', 'error');
  531. });
  532. }
  533. }
  534.  
  535. function startMassUpload() {
  536. if (!massQueue.length) {
  537. displayMessage('Nothing queued for mass upload!', 'info');
  538. return;
  539. }
  540.  
  541. batchTotal = massQueue.length;
  542. completed = 0;
  543. updateStatus();
  544. displayMessage(`Starting mass upload of ${batchTotal} files...`, 'info');
  545.  
  546. const tasks = massQueue.map(item => uploadFile(item.f, item.type, 0, item.forceName));
  547. massQueue = [];
  548.  
  549. Promise.all(tasks).then(() => {
  550. displayMessage('Mass upload complete!', 'success');
  551. massMode = false;
  552. toggleBtn.textContent = 'Enable Mass Upload';
  553. startBtn.style.display = 'none';
  554. scanForAssets();
  555. batchTotal = completed = 0;
  556. updateStatus();
  557. }).catch(error => {
  558. console.error("Mass upload encountered an error:", error);
  559. displayMessage('Mass upload finished with errors. Check console.', 'error');
  560. massMode = false;
  561. toggleBtn.textContent = 'Enable Mass Upload';
  562. startBtn.style.display = 'none';
  563. batchTotal = completed = 0;
  564. updateStatus();
  565. });
  566. }
  567.  
  568. function displayMessage(message, type = 'info') {
  569. const modal = document.createElement('div');
  570. Object.assign(modal.style, {
  571. position: 'fixed',
  572. top: '50%',
  573. left: '50%',
  574. transform: 'translate(-50%, -50%)',
  575. padding: '20px',
  576. background: '#333',
  577. color: '#fff',
  578. borderRadius: '8px',
  579. boxShadow: '0 4px 10px rgba(0,0,0,0.5)',
  580. zIndex: '10001',
  581. fontFamily: 'Inter, Arial, sans-serif',
  582. textAlign: 'center',
  583. minWidth: '250px',
  584. transition: 'opacity 0.3s ease-in-out',
  585. opacity: '0'
  586. });
  587.  
  588. if (type === 'success') {
  589. modal.style.background = '#4CAF50';
  590. } else if (type === 'error') {
  591. modal.style.background = '#f44336';
  592. }
  593.  
  594. modal.textContent = message;
  595.  
  596. document.body.appendChild(modal);
  597.  
  598. setTimeout(() => modal.style.opacity = '1', 10);
  599.  
  600. setTimeout(() => {
  601. modal.style.opacity = '0';
  602. modal.addEventListener('transitionend', () => modal.remove());
  603. }, 3000);
  604. }
  605.  
  606. function customPrompt(message, defaultValue = '') {
  607. return new Promise(resolve => {
  608. const modal = document.createElement('div');
  609. Object.assign(modal.style, {
  610. position: 'fixed',
  611. top: '50%',
  612. left: '50%',
  613. transform: 'translate(-50%, -50%)',
  614. padding: '20px',
  615. background: '#222',
  616. color: '#fff',
  617. borderRadius: '8px',
  618. boxShadow: '0 6px 15px rgba(0,0,0,0.4)',
  619. zIndex: '10002',
  620. fontFamily: 'Inter, Arial, sans-serif',
  621. textAlign: 'center',
  622. minWidth: '300px',
  623. display: 'flex',
  624. flexDirection: 'column',
  625. gap: '15px',
  626. transition: 'opacity 0.3s ease-in-out',
  627. opacity: '0'
  628. });
  629.  
  630. const textDiv = document.createElement('div');
  631. textDiv.textContent = message;
  632. textDiv.style.fontSize = '16px';
  633. modal.appendChild(textDiv);
  634.  
  635. const input = document.createElement('input');
  636. input.type = 'text';
  637. input.value = defaultValue;
  638. Object.assign(input.style, {
  639. padding: '10px',
  640. borderRadius: '5px',
  641. border: '1px solid #555',
  642. background: '#333',
  643. color: '#fff',
  644. fontSize: '14px',
  645. outline: 'none'
  646. });
  647. modal.appendChild(input);
  648.  
  649. const buttonContainer = document.createElement('div');
  650. Object.assign(buttonContainer.style, {
  651. display: 'flex',
  652. justifyContent: 'space-around',
  653. gap: '10px',
  654. marginTop: '10px'
  655. });
  656.  
  657. const okBtn = document.createElement('button');
  658. okBtn.textContent = 'OK';
  659. Object.assign(okBtn.style, {
  660. padding: '10px 20px',
  661. cursor: 'pointer',
  662. color: '#fff',
  663. background: '#007bff',
  664. border: 'none',
  665. borderRadius: '5px',
  666. fontSize: '14px',
  667. flexGrow: '1'
  668. });
  669. okBtn.onmouseover = () => okBtn.style.background = '#0056b3';
  670. okBtn.onmouseout = () => okBtn.style.background = '#007bff';
  671. okBtn.onclick = () => {
  672. modal.style.opacity = '0';
  673. modal.addEventListener('transitionend', () => modal.remove());
  674. resolve(input.value);
  675. };
  676. buttonContainer.appendChild(okBtn);
  677.  
  678. const cancelBtn = document.createElement('button');
  679. cancelBtn.textContent = 'Cancel';
  680. Object.assign(cancelBtn.style, {
  681. padding: '10px 20px',
  682. cursor: 'pointer',
  683. color: '#fff',
  684. background: '#6c757d',
  685. border: 'none',
  686. borderRadius: '5px',
  687. fontSize: '14px',
  688. flexGrow: '1'
  689. });
  690. cancelBtn.onmouseover = () => cancelBtn.style.background = '#5a6268';
  691. cancelBtn.onmouseout = () => cancelBtn.style.background = '#6c757d';
  692. cancelBtn.onclick = () => {
  693. modal.style.opacity = '0';
  694. modal.addEventListener('transitionend', () => modal.remove());
  695. resolve(null);
  696. };
  697. buttonContainer.appendChild(cancelBtn);
  698.  
  699. modal.appendChild(buttonContainer);
  700. document.body.appendChild(modal);
  701.  
  702. setTimeout(() => modal.style.opacity = '1', 10);
  703.  
  704. input.focus();
  705. input.addEventListener('keypress', (e) => {
  706. if (e.key === 'Enter') {
  707. okBtn.click();
  708. }
  709. });
  710. });
  711. }
  712.  
  713. function createStyledButton(text, fn) {
  714. const b = document.createElement('button');
  715. b.textContent = text;
  716. Object.assign(b.style, {
  717. padding: '10px',
  718. cursor: 'pointer',
  719. color: '#fff',
  720. background: '#3a3a3a',
  721. border: '1px solid #555',
  722. borderRadius: '5px',
  723. transition: 'background 0.2s ease-in-out',
  724. fontSize: '14px'
  725. });
  726. b.onmouseover = () => b.style.background = '#505050';
  727. b.onmouseout = () => b.style.background = '#3a3a3a';
  728. b.onclick = fn;
  729. return b;
  730. }
  731.  
  732. function createUI() {
  733. uiContainer = document.createElement('div');
  734. Object.assign(uiContainer.style, {
  735. position: 'fixed',
  736. top: '10px',
  737. right: '10px',
  738. width: '280px', // Adjusted width
  739. background: '#1a1a1a',
  740. border: '2px solid #333',
  741. color: '#e0e0e0',
  742. padding: '15px 15px 15px 15px', // Adjusted padding
  743. zIndex: 10000,
  744. borderRadius: '8px',
  745. boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
  746. display: 'flex',
  747. flexDirection: 'column',
  748. gap: '10px',
  749. fontFamily: 'Inter, Arial, sans-serif',
  750. transition: 'top 0.3s ease-in-out'
  751. });
  752.  
  753. // Close button
  754. const close = createStyledButton('×', () => uiContainer.remove());
  755. Object.assign(close.style, {
  756. position: 'absolute',
  757. top: '5px',
  758. right: '8px',
  759. background: 'transparent',
  760. border: 'none',
  761. fontSize: '18px',
  762. color: '#e0e0e0',
  763. fontWeight: 'bold',
  764. transition: 'color 0.2s',
  765. padding: '5px 8px'
  766. });
  767. close.onmouseover = () => close.style.color = '#fff';
  768. close.onmouseout = () => close.style.color = '#e0e0e0';
  769. close.title = 'Close AnnaUploader';
  770. uiContainer.appendChild(close);
  771.  
  772. // Gear icon for settings
  773. const settingsGear = createStyledButton('⚙️', () => {
  774. createSettingsUI();
  775. });
  776. Object.assign(settingsGear.style, {
  777. position: 'absolute',
  778. top: '5px',
  779. left: '8px',
  780. background: 'transparent',
  781. border: 'none',
  782. fontSize: '18px',
  783. color: '#e0e0e0',
  784. fontWeight: 'bold',
  785. transition: 'color 0.2s',
  786. padding: '5px 8px',
  787. });
  788. settingsGear.onmouseover = () => settingsGear.style.color = '#fff';
  789. settingsGear.onmouseout = () => settingsGear.style.color = '#e0e0e0';
  790. settingsGear.title = 'Settings';
  791. uiContainer.appendChild(settingsGear);
  792.  
  793. const title = document.createElement('h3');
  794. title.textContent = 'AnnaUploader';
  795. title.style.margin = '0 0 10px 0';
  796. title.style.color = '#4af';
  797. title.style.textAlign = 'center';
  798. uiContainer.appendChild(title);
  799.  
  800. uiContainer.appendChild(createStyledButton('Upload T-Shirts', () => {
  801. const i = document.createElement('input');
  802. i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
  803. i.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_TSHIRT);
  804. i.click();
  805. }));
  806. uiContainer.appendChild(createStyledButton('Upload Decals', () => {
  807. const i = document.createElement('input');
  808. i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
  809. i.onchange = e => handleFileSelect(e.target.files, ASSET_TYPE_DECAL);
  810. i.click();
  811. }));
  812. uiContainer.appendChild(createStyledButton('Upload Both', () => {
  813. const i = document.createElement('input');
  814. i.type = 'file'; i.accept = 'image/*'; i.multiple = true;
  815. i.onchange = e => handleFileSelect(e.target.files, null, true);
  816. i.click();
  817. }));
  818.  
  819. toggleBtn = createStyledButton('Enable Mass Upload', () => {
  820. massMode = !massMode;
  821. toggleBtn.textContent = massMode ? 'Disable Mass Upload' : 'Enable Mass Upload';
  822. startBtn.style.display = massMode ? 'block' : 'none';
  823. massQueue = [];
  824. batchTotal = completed = 0;
  825. updateStatus();
  826. displayMessage(`Mass Upload Mode: ${massMode ? 'Enabled' : 'Disabled'}`, 'info');
  827. });
  828. uiContainer.appendChild(toggleBtn);
  829.  
  830. startBtn = createStyledButton('Start Mass Upload', startMassUpload);
  831. startBtn.style.display = 'none';
  832. Object.assign(startBtn.style, {
  833. background: '#28a745',
  834. border: '1px solid #218838'
  835. });
  836. startBtn.onmouseover = () => startBtn.style.background = '#218838';
  837. startBtn.onmouseout = () => startBtn.style.background = '#28a745';
  838. uiContainer.appendChild(startBtn);
  839.  
  840. const slipBtn = createStyledButton(`Slip Mode: ${useMakeUnique ? 'On' : 'Off'}`, () => {
  841. useMakeUnique = !useMakeUnique;
  842. GM_setValue('useMakeUnique', useMakeUnique);
  843. slipBtn.textContent = `Slip Mode: ${useMakeUnique ? 'On' : 'Off'}`;
  844. copiesInput.style.display = useMakeUnique ? 'block' : 'none';
  845. downloadBtn.style.display = useMakeUnique ? 'block' : 'none';
  846.  
  847. if (!useMakeUnique) {
  848. useDownload = false;
  849. GM_setValue('useDownload', useDownload);
  850. downloadBtn.textContent = 'Download Images: Off';
  851. }
  852. });
  853. uiContainer.appendChild(slipBtn);
  854.  
  855. copiesInput = document.createElement('input');
  856. copiesInput.type = 'number'; copiesInput.min = '1'; copiesInput.value = uniqueCopies;
  857. Object.assign(copiesInput.style, {
  858. width: '100%',
  859. boxSizing: 'border-box',
  860. display: useMakeUnique ? 'block' : 'none',
  861. padding: '8px',
  862. borderRadius: '4px',
  863. border: '1px solid #555',
  864. background: '#333',
  865. color: '#fff',
  866. textAlign: 'center'
  867. });
  868. copiesInput.onchange = e => {
  869. const v = parseInt(e.target.value, 10);
  870. if (v > 0) {
  871. uniqueCopies = v;
  872. GM_setValue('uniqueCopies', uniqueCopies);
  873. }
  874. else e.target.value = uniqueCopies;
  875. };
  876. uiContainer.appendChild(copiesInput);
  877.  
  878. downloadBtn = createStyledButton(`Download Images: ${useDownload ? 'On' : 'Off'}`, () => {
  879. useDownload = !useDownload;
  880. GM_setValue('useDownload', useDownload);
  881. downloadBtn.textContent = `Download Images: ${useDownload ? 'On' : 'Off'}`;
  882. });
  883. downloadBtn.style.display = useMakeUnique ? 'block' : 'none';
  884. uiContainer.appendChild(downloadBtn);
  885.  
  886. uiContainer.appendChild(createStyledButton('Change ID', async () => {
  887. const inp = await customPrompt("Enter your Roblox User ID/URL or Group URL:", USER_ID || '');
  888. if (inp === null) return;
  889. let id, isGrp = false;
  890. const um = inp.match(/users\/(\d+)/);
  891. const gm = inp.match(/communities\/(\d+)/);
  892. if (um) {
  893. id = um[1];
  894. } else if (gm) {
  895. id = gm[1];
  896. isGrp = true;
  897. } else {
  898. id = inp.trim();
  899. if (isNaN(id) || id === '') {
  900. displayMessage('Invalid input. Please enter a number or a valid URL.', 'error');
  901. return;
  902. }
  903. }
  904. USER_ID = Number(id);
  905. IS_GROUP = isGrp;
  906. GM_setValue('userId', USER_ID);
  907. GM_setValue('isGroup', IS_GROUP);
  908. displayMessage(`Set to ${isGrp ? 'Group' : 'User'} ID: ${USER_ID}`, 'success');
  909. }));
  910.  
  911. const pm = window.location.pathname.match(/^\/users\/(\d+)\/profile/);
  912. if (pm) {
  913. uiContainer.appendChild(createStyledButton('Use This Profile as ID', () => {
  914. USER_ID = Number(pm[1]);
  915. IS_GROUP = false;
  916. GM_setValue('userId', USER_ID);
  917. GM_setValue('isGroup', IS_GROUP);
  918. displayMessage(`User ID set to ${USER_ID}`, 'success');
  919. }));
  920. }
  921.  
  922. const gm = window.location.pathname.match(/^\/communities\/(\d+)/);
  923. if (gm) {
  924. uiContainer.appendChild(createStyledButton('Use This Group as ID', () => {
  925. USER_ID = Number(gm[1]);
  926. IS_GROUP = true;
  927. GM_setValue('userId', USER_ID);
  928. GM_setValue('isGroup', IS_GROUP);
  929. displayMessage(`Group ID set to ${USER_ID}`, 'success');
  930. }));
  931. }
  932.  
  933. // Removed the old 'Settings' button here.
  934.  
  935. const hint = document.createElement('div');
  936. hint.textContent = 'Paste images (Ctrl+V) to queue/upload';
  937. hint.style.fontSize = '12px'; hint.style.color = '#aaa';
  938. hint.style.textAlign = 'center';
  939. hint.style.marginTop = '5px';
  940. uiContainer.appendChild(hint);
  941.  
  942. statusEl = document.createElement('div');
  943. statusEl.style.fontSize = '13px'; statusEl.style.color = '#fff';
  944. statusEl.style.textAlign = 'center';
  945. statusEl.style.paddingTop = '5px';
  946. statusEl.style.borderTop = '1px solid #333';
  947. uiContainer.appendChild(statusEl);
  948.  
  949. document.body.appendChild(uiContainer);
  950. }
  951.  
  952. function createSettingsUI() {
  953. if (settingsModal) {
  954. settingsModal.style.display = 'flex';
  955. return;
  956. }
  957.  
  958. settingsModal = document.createElement('div');
  959. Object.assign(settingsModal.style, {
  960. position: 'fixed',
  961. top: '50%',
  962. left: '50%',
  963. transform: 'translate(-50%, -50%)',
  964. width: '300px',
  965. background: '#1a1a1a',
  966. border: '2px solid #333',
  967. color: '#e0e0e0',
  968. padding: '20px',
  969. zIndex: 10005,
  970. borderRadius: '10px',
  971. boxShadow: '0 6px 20px rgba(0,0,0,0.6)',
  972. display: 'flex',
  973. flexDirection: 'column',
  974. gap: '15px',
  975. fontFamily: 'Inter, Arial, sans-serif',
  976. });
  977.  
  978. const closeSettings = createStyledButton('×', () => {
  979. settingsModal.style.display = 'none';
  980. });
  981. Object.assign(closeSettings.style, {
  982. position: 'absolute',
  983. top: '8px',
  984. right: '10px',
  985. background: 'transparent',
  986. border: 'none',
  987. fontSize: '20px',
  988. color: '#e0e0e0',
  989. fontWeight: 'bold',
  990. transition: 'color 0.2s',
  991. padding: '5px 10px'
  992. });
  993. closeSettings.onmouseover = () => closeSettings.style.color = '#fff';
  994. closeSettings.onmouseout = () => closeSettings.style.color = '#e0e0e0';
  995. closeSettings.title = 'Close Settings';
  996. settingsModal.appendChild(closeSettings);
  997.  
  998. const title = document.createElement('h3');
  999. title.textContent = 'AnnaUploader Settings';
  1000. title.style.margin = '0 0 15px 0';
  1001. title.style.color = '#4af';
  1002. title.style.textAlign = 'center';
  1003. settingsModal.appendChild(title);
  1004.  
  1005. const nameBtn = createStyledButton(`Use default Name: ${useForcedName ? 'On' : 'Off'}`, () => {
  1006. useForcedName = !useForcedName;
  1007. GM_setValue('useForcedName', useForcedName);
  1008. nameBtn.textContent = `Use default Name: ${useForcedName ? 'On' : 'Off'}`;
  1009. });
  1010. settingsModal.appendChild(nameBtn);
  1011.  
  1012. // Slip Mode Pixel Method setting
  1013. const slipModePixelMethodLabel = document.createElement('label');
  1014. slipModePixelMethodLabel.textContent = 'Slip Mode Pixel Method:';
  1015. Object.assign(slipModePixelMethodLabel.style, {
  1016. display: 'block',
  1017. marginBottom: '5px',
  1018. fontSize: '14px',
  1019. color: '#bbb'
  1020. });
  1021. settingsModal.appendChild(slipModePixelMethodLabel);
  1022.  
  1023. const slipModePixelMethodSelect = document.createElement('select');
  1024. Object.assign(slipModePixelMethodSelect.style, {
  1025. width: '100%',
  1026. padding: '10px',
  1027. borderRadius: '5px',
  1028. border: '1px solid #555',
  1029. background: '#333',
  1030. color: '#fff',
  1031. fontSize: '14px',
  1032. outline: 'none',
  1033. marginBottom: '10px'
  1034. });
  1035.  
  1036. const optionAll = document.createElement('option');
  1037. optionAll.value = 'all_pixels';
  1038. optionAll.textContent = 'All Pixels (±1)';
  1039. slipModePixelMethodSelect.appendChild(optionAll);
  1040.  
  1041. const optionRandom = document.createElement('option');
  1042. optionRandom.value = '1-3_random';
  1043. optionRandom.textContent = 'Random Pixels (±1-3)';
  1044. slipModePixelMethodSelect.appendChild(optionRandom);
  1045.  
  1046. const optionSingleRandom = document.createElement('option');
  1047. optionSingleRandom.value = '1-4_random_single_pixel';
  1048. optionSingleRandom.textContent = 'Single Random Pixel (±1-4)';
  1049. slipModePixelMethodSelect.appendChild(optionSingleRandom);
  1050.  
  1051. const optionFullRandomSinglePixel = document.createElement('option');
  1052. optionFullRandomSinglePixel.value = 'random_single_pixel_full_random_color';
  1053. optionFullRandomSinglePixel.textContent = 'Single Random Pixel (Full Random Color)';
  1054. slipModePixelMethodSelect.appendChild(optionFullRandomSinglePixel);
  1055.  
  1056. slipModePixelMethodSelect.value = slipModePixelMethod;
  1057.  
  1058. slipModePixelMethodSelect.onchange = (e) => {
  1059. slipModePixelMethod = e.target.value;
  1060. GM_setValue('slipModePixelMethod', slipModePixelMethod);
  1061. displayMessage(`Slip Mode Pixel Method set to: ${e.target.options[e.target.selectedIndex].text}`, 'success');
  1062. };
  1063. settingsModal.appendChild(slipModePixelMethodSelect);
  1064.  
  1065. // Force Upload (through Canvas) toggle
  1066. const forceUploadBtn = createStyledButton(`Force Upload: ${useForceCanvasUpload ? 'On' : 'Off'}`, () => {
  1067. useForceCanvasUpload = !useForceCanvasUpload;
  1068. GM_setValue('useForceCanvasUpload', useForceCanvasUpload);
  1069. forceUploadBtn.textContent = `Force Upload: ${useForceCanvasUpload ? 'On' : 'Off'}`;
  1070. displayMessage(`Force Upload Mode: ${useForceCanvasUpload ? 'Enabled' : 'Disabled'}`, 'info');
  1071. });
  1072. settingsModal.appendChild(forceUploadBtn);
  1073.  
  1074. // IMAGE RESIZE FEATURE
  1075. const resizeContainer = document.createElement('div');
  1076. resizeContainer.style.display = 'flex';
  1077. resizeContainer.style.flexDirection = 'column';
  1078. resizeContainer.style.gap = '5px';
  1079. resizeContainer.style.margin = '10px 0';
  1080.  
  1081. const resizeToggleBtn = createStyledButton(`Resize Images: ${enableResize ? 'On' : 'Off'}`, () => {
  1082. enableResize = !enableResize;
  1083. GM_setValue('enableResize', enableResize);
  1084. resizeToggleBtn.textContent = `Resize Images: ${enableResize ? 'On' : 'Off'}`;
  1085. widthInput.disabled = heightInput.disabled = !enableResize;
  1086. });
  1087. resizeContainer.appendChild(resizeToggleBtn);
  1088.  
  1089. // Input fields for width/height
  1090. const inputRow = document.createElement('div');
  1091. inputRow.style.display = 'flex';
  1092. inputRow.style.gap = '7px';
  1093. inputRow.style.alignItems = 'center';
  1094.  
  1095. const widthInput = document.createElement('input');
  1096. widthInput.type = 'number';
  1097. widthInput.min = '1';
  1098. widthInput.value = resizeWidth;
  1099. widthInput.placeholder = 'Width';
  1100. widthInput.style.width = '60px';
  1101. widthInput.style.padding = '6px';
  1102. widthInput.style.borderRadius = '4px';
  1103. widthInput.style.border = '1px solid #555';
  1104. widthInput.style.background = '#333';
  1105. widthInput.style.color = '#fff';
  1106. widthInput.disabled = !enableResize;
  1107. widthInput.onchange = () => {
  1108. let val = Math.max(1, parseInt(widthInput.value, 10) || 512);
  1109. widthInput.value = val;
  1110. resizeWidth = val;
  1111. GM_setValue('resizeWidth', resizeWidth);
  1112. };
  1113. inputRow.appendChild(widthInput);
  1114.  
  1115. const xLabel = document.createElement('span');
  1116. xLabel.textContent = '×';
  1117. xLabel.style.color = '#ccc';
  1118. inputRow.appendChild(xLabel);
  1119.  
  1120. const heightInput = document.createElement('input');
  1121. heightInput.type = 'number';
  1122. heightInput.min = '1';
  1123. heightInput.value = resizeHeight;
  1124. heightInput.placeholder = 'Height';
  1125. heightInput.style.width = '60px';
  1126. heightInput.style.padding = '6px';
  1127. heightInput.style.borderRadius = '4px';
  1128. heightInput.style.border = '1px solid #555';
  1129. heightInput.style.background = '#333';
  1130. heightInput.style.color = '#fff';
  1131. heightInput.disabled = !enableResize;
  1132. heightInput.onchange = () => {
  1133. let val = Math.max(1, parseInt(heightInput.value, 10) || 512);
  1134. heightInput.value = val;
  1135. resizeHeight = val;
  1136. GM_setValue('resizeHeight', resizeHeight);
  1137. };
  1138. inputRow.appendChild(heightInput);
  1139.  
  1140. const pxLabel = document.createElement('span');
  1141. pxLabel.textContent = 'px';
  1142. pxLabel.style.color = '#bbb';
  1143. inputRow.appendChild(pxLabel);
  1144.  
  1145. resizeContainer.appendChild(inputRow);
  1146.  
  1147. const resizeDesc = document.createElement('div');
  1148. resizeDesc.textContent = "If enabled, images will be resized before upload. Applies to Slip Mode too.";
  1149. resizeDesc.style.fontSize = '12px';
  1150. resizeDesc.style.color = '#aaa';
  1151. resizeDesc.style.marginTop = '3px';
  1152. resizeContainer.appendChild(resizeDesc);
  1153.  
  1154. settingsModal.appendChild(resizeContainer);
  1155.  
  1156. settingsModal.appendChild(createStyledButton('Show Logged Assets', () => {
  1157. const log = loadLog();
  1158. const entries = Object.entries(log);
  1159. const w = window.open('', '_blank');
  1160. w.document.write(`<!DOCTYPE html>
  1161. <html><head><meta charset="utf-8"><title>Logged Assets</title>
  1162. <style>
  1163. body { font-family:Arial; padding:20px; background:#121212; color:#f0f0f0; }
  1164. h1 { margin-bottom:15px; color:#4af; }
  1165. ul { list-style:none; padding:0; }
  1166. li { margin-bottom:15px; padding:10px; background:#1e1e1e; border-radius:8px; display:flex; flex-direction:column; gap:8px;}
  1167. img { max-height:60px; border:1px solid #444; border-radius:4px; object-fit:contain; background:#333; }
  1168. .asset-info { display:flex;align-items:center;gap:15px; }
  1169. a { color:#7cf; text-decoration:none; font-weight:bold; }
  1170. a:hover { text-decoration:underline; }
  1171. .asset-name { font-size:0.9em; color:#bbb; margin-left: auto; text-align: right; }
  1172. button { margin-bottom:20px; color:#fff; background:#3a3a3a; border:1px solid #555; padding:8px 15px; border-radius:5px; cursor:pointer; }
  1173. button:hover { background:#505050; }
  1174. </style></head><body>
  1175. <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[...]
  1176. <h1>Logged Assets</h1>
  1177. ${ entries.length ? `<ul>${entries.map(([id,entry])=>
  1178. `<li>
  1179. <div class="asset-info">
  1180. ${ entry.image ? `<img src="${entry.image}" alt="Asset thumbnail">` : `<span style="color:#888;">(no image)</span>` }
  1181. <a href="https://create.roblox.com/store/asset/${id}" target="_blank">${id}</a>
  1182. <span style="font-size:0.85em; color:#999;">${new Date(entry.date).toLocaleString()}</span>
  1183. </div>
  1184. <div class="asset-name">${entry.name}</div>
  1185. </li>`).join('') }</ul>` : `<p style="color:#888;"><em>No assets logged yet.</em></p>`}
  1186. </body></html>`);
  1187. w.document.close();
  1188. }));
  1189.  
  1190. document.body.appendChild(settingsModal);
  1191. }
  1192.  
  1193. async function handlePaste(e) {
  1194. const items = e.clipboardData?.items;
  1195. if (!items) return;
  1196.  
  1197. const resizeActive = enableResize && Number(resizeWidth) > 0 && Number(resizeHeight) > 0;
  1198.  
  1199. for (const it of items) {
  1200. if (it.type.startsWith('image')) {
  1201. e.preventDefault();
  1202. const blob = it.getAsFile();
  1203. const ts = new Date().toISOString().replace(/[^a-z0-9]/gi,'_');
  1204.  
  1205. const pastedName = await customPrompt('Enter a name for the image (no extension):', `pasted_${ts}`);
  1206. if (pastedName === null) return;
  1207. let name = pastedName.trim() || `pasted_${ts}`;
  1208. let filename = name.endsWith('.png') ? name : `${name}.png`;
  1209.  
  1210. let fileToProcess = new File([blob], filename, {type: blob.type});
  1211.  
  1212. if (blob.type === 'image/webp') {
  1213. displayMessage(`Converting pasted WebP image to PNG...`, 'info');
  1214. try {
  1215. fileToProcess = await convertWebPToPng(fileToProcess);
  1216. name = baseName(fileToProcess.name);
  1217. filename = fileToProcess.name;
  1218. displayMessage(`Pasted WebP converted to PNG.`, 'success');
  1219. } catch (error) {
  1220. displayMessage(`Failed to convert pasted WebP: ${error.message}`, 'error');
  1221. console.error(`[Conversion] Failed to convert pasted WebP:`, error);
  1222. return;
  1223. }
  1224. }
  1225.  
  1226. // Resize if enabled
  1227. if (resizeActive) {
  1228. displayMessage(`Resizing pasted image to ${resizeWidth}x${resizeHeight}...`, 'info');
  1229. try {
  1230. fileToProcess = await resizeImageFile(fileToProcess, Number(resizeWidth), Number(resizeHeight));
  1231. name = baseName(fileToProcess.name);
  1232. filename = fileToProcess.name;
  1233. displayMessage(`Pasted image resized.`, 'success');
  1234. } catch (error) {
  1235. displayMessage(`Failed to resize pasted image: ${error.message}`, 'error');
  1236. console.error(`[Resize] Failed to resize pasted image:`, error);
  1237. return;
  1238. }
  1239. }
  1240.  
  1241. if (useForceCanvasUpload) {
  1242. displayMessage(`Processing pasted image through canvas...`, 'info');
  1243. try {
  1244. fileToProcess = await processImageThroughCanvas(
  1245. fileToProcess, 'image/png',
  1246. resizeActive ? Number(resizeWidth) : null,
  1247. resizeActive ? Number(resizeHeight) : null
  1248. );
  1249. name = baseName(fileToProcess.name);
  1250. filename = fileToProcess.name;
  1251. displayMessage(`Pasted image processed through canvas.`, 'success');
  1252. } catch (error) {
  1253. displayMessage(`Failed to process pasted image through canvas: ${error.message}`, 'error');
  1254. console.error(`[Canvas Process] Failed to process pasted image:`, error);
  1255. return;
  1256. }
  1257. }
  1258.  
  1259. const typeChoice = await customPrompt('Upload as T=T-Shirt, D=Decal, B=Both, or C=Cancel?', 'D');
  1260. if (!typeChoice) return;
  1261. const t = typeChoice.trim().toUpperCase();
  1262.  
  1263. let uploadAsBoth = false;
  1264. let type = null;
  1265.  
  1266. if (t === 'T') {
  1267. type = ASSET_TYPE_TSHIRT;
  1268. } else if (t === 'D') {
  1269. type = ASSET_TYPE_DECAL;
  1270. } else if (t === 'B') {
  1271. uploadAsBoth = true;
  1272. } else {
  1273. displayMessage('Invalid asset type selected. Please choose T, D, or B.', 'error');
  1274. return;
  1275. }
  1276.  
  1277. handleFileSelect([fileToProcess], type, uploadAsBoth);
  1278. break;
  1279. }
  1280. }
  1281. }
  1282.  
  1283. window.addEventListener('load', () => {
  1284. createUI();
  1285. document.addEventListener('paste', handlePaste);
  1286. scanForAssets();
  1287. console.log('[AnnaUploader] initialized; asset scan every ' + (SCAN_INTERVAL_MS/1000) + 's');
  1288. });
  1289.  
  1290. })();