diff --git a/Auto-Image.js b/Auto-Image.js index 0baf29f..3975cd6 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -176,1156 +176,6 @@ } } - // BILINGUAL TEXT STRINGS - const TEXT = { - en: { - title: "Weplace auto", - toggleOverlay: "Toggle Overlay", - scanColors: "Scan Colors", - uploadImage: "Upload Image", - resizeImage: "Resize Image", - selectPosition: "Select Position", - startPainting: "Start Painting", - stopPainting: "Stop Painting", - checkingColors: "🔍 Checking available colors...", - noColorsFound: "❌ Open the color palette on the site and try again!", - colorsFound: "✅ {count} available colors found. Ready to upload.", - loadingImage: "🖼️ Loading image...", - imageLoaded: "✅ Image loaded with {count} valid pixels", - imageError: "❌ Error loading image", - selectPositionAlert: "Paint the first pixel at the location where you want the art to start!", - waitingPosition: "👆 Waiting for you to paint the reference pixel...", - positionSet: "✅ Position set successfully!", - positionTimeout: "❌ Timeout for position selection", - startPaintingMsg: "🎨 Starting painting...", - paintingProgress: "🧱 Progress: {painted}/{total} pixels...", - noCharges: "⌛ No charges. Waiting {time}...", - paintingStopped: "⏹️ Painting stopped by user", - paintingComplete: "✅ Painting complete! {count} pixels painted.", - paintingError: "❌ Error during painting", - missingRequirements: "❌ Load an image and select a position first", - progress: "Progress", - pixels: "Pixels", - charges: "Charges", - estimatedTime: "Estimated time", - initMessage: "Click 'Upload Image' to begin", - waitingInit: "Waiting for initialization...", - resizeSuccess: "✅ Image resized to {width}x{height}", - paintingPaused: "⏸️ Painting paused at position X: {x}, Y: {y}", - captchaNeeded: "❗ CAPTCHA token needed. Paint one pixel manually to continue.", - saveData: "Save Progress", - loadData: "Load Progress", - saveToFile: "Save to File", - loadFromFile: "Load from File", - dataManager: "Data Manager", - autoSaved: "✅ Progress saved automatically", - dataLoaded: "✅ Progress loaded successfully", - fileSaved: "✅ Progress saved to file successfully", - fileLoaded: "✅ Progress loaded from file successfully", - noSavedData: "❌ No saved progress found", - savedDataFound: "✅ Saved progress found! Load to continue?", - savedDate: "Saved on: {date}", - clickLoadToContinue: "Click 'Load Progress' to continue.", - fileError: "❌ Error processing file", - invalidFileFormat: "❌ Invalid file format", - paintingSpeed: "Painting Speed", - pixelsPerSecond: "pixels/second", - speedSetting: "Speed: {speed} pixels/sec", - settings: "Settings", - botSettings: "Bot Settings", - close: "Close", - language: "Language", - themeSettings: "Theme Settings", - themeSettingsDesc: "Choose your preferred color theme for the interface.", - languageSelectDesc: "Select your preferred language. Changes will take effect immediately.", - autoCaptcha: "Auto-CAPTCHA Solver", - autoCaptchaDesc: "Automatically attempts to solve the CAPTCHA by simulating a manual pixel placement when the token expires.", - applySettings: "Apply Settings", - settingsSaved: "✅ Settings saved successfully!", - cooldownSettings: "Cooldown Settings", - waitCharges: "Wait until charges reach", - captchaSolving: "🤖 Attempting to solve CAPTCHA...", - captchaFailed: "❌ Auto-CAPTCHA failed. Paint a pixel manually.", - automation: "Automation", - noChargesThreshold: "⌛ Waiting for charges to reach {threshold}. Currently {current}. Next in {time}...", - }, - ru: { - title: "WPlace Авто-Изображение", - scanColors: "Сканировать цвета", - uploadImage: "Загрузить изображение", - resizeImage: "Изменить размер изображения", - selectPosition: "Выбрать позицию", - startPainting: "Начать рисование", - stopPainting: "Остановить рисование", - checkingColors: "🔍 Проверка доступных цветов...", - noColorsFound: "❌ Откройте палитру цветов на сайте и попробуйте снова!", - colorsFound: "✅ Найдено доступных цветов: {count}. Готово к загрузке.", - loadingImage: "🖼️ Загрузка изображения...", - imageLoaded: "✅ Изображение загружено, валидных пикселей: {count}", - imageError: "❌ Ошибка при загрузке изображения", - selectPositionAlert: "Нарисуйте первый пиксель в месте, откуда начнётся рисунок!", - waitingPosition: "👆 Ожидание, пока вы нарисуете опорный пиксель...", - positionSet: "✅ Позиция успешно установлена!", - positionTimeout: "❌ Время ожидания выбора позиции истекло", - startPaintingMsg: "🎨 Начинаем рисование...", - paintingProgress: "🧱 Прогресс: {painted}/{total} пикселей...", - noCharges: "⌛ Нет зарядов. Ожидание {time}...", - paintingStopped: "⏹️ Рисование остановлено пользователем", - paintingComplete: "✅ Рисование завершено! Нарисовано пикселей: {count}.", - paintingError: "❌ Ошибка во время рисования", - missingRequirements: "❌ Сначала загрузите изображение и выберите позицию", - progress: "Прогресс", - pixels: "Пиксели", - charges: "Заряды", - estimatedTime: "Примерное время", - initMessage: "Нажмите 'Загрузить изображение', чтобы начать", - waitingInit: "Ожидание инициализации...", - resizeSuccess: "✅ Изображение изменено до {width}x{height}", - paintingPaused: "⏸️ Рисование приостановлено на позиции X: {x}, Y: {y}", - captchaNeeded: "❗ Требуется токен CAPTCHA. Нарисуйте один пиксель вручную, чтобы продолжить.", - saveData: "Сохранить прогресс", - loadData: "Загрузить прогресс", - saveToFile: "Сохранить в файл", - loadFromFile: "Загрузить из файла", - dataManager: "Менеджер данных", - autoSaved: "✅ Прогресс сохранён автоматически", - dataLoaded: "✅ Прогресс успешно загружен", - fileSaved: "✅ Прогресс успешно сохранён в файл", - fileLoaded: "✅ Прогресс успешно загружен из файла", - noSavedData: "❌ Сохранённый прогресс не найден", - savedDataFound: "✅ Найден сохранённый прогресс! Загрузить, чтобы продолжить?", - savedDate: "Сохранено: {date}", - clickLoadToContinue: "Нажмите 'Загрузить прогресс', чтобы продолжить.", - fileError: "❌ Ошибка при обработке файла", - invalidFileFormat: "❌ Неверный формат файла", - paintingSpeed: "Скорость рисования", - pixelsPerSecond: "пикселей/сек", - speedSetting: "Скорость: {speed} пикс./сек", - settings: "Настройки", - botSettings: "Настройки бота", - close: "Закрыть", - language: "Язык", - themeSettings: "Настройки темы", - themeSettingsDesc: "Выберите предпочтительную цветовую тему интерфейса.", - languageSelectDesc: "Выберите предпочтительный язык. Изменения вступят в силу немедленно.", - autoCaptcha: "Авто-решение CAPTCHA", - autoCaptchaDesc: "Автоматически пытается решить CAPTCHA, симулируя ручное размещение пикселя, когда токен истекает.", - applySettings: "Применить настройки", - settingsSaved: "✅ Настройки успешно сохранены!", - cooldownSettings: "Настройки перезарядки", - waitCharges: "Ждать до накопления зарядов", - captchaSolving: "🤖 Пытаюсь решить CAPTCHA...", - captchaFailed: "❌ Не удалось решить CAPTCHA. Нарисуйте пиксель вручную.", - automation: "Автоматизация", - noChargesThreshold: "⌛ Ожидание зарядов до {threshold}. Сейчас {current}. Следующий через {time}...", -}, - pt: { - // ... other languages - }, - vi: { - // ... other languages - }, - fr: { - // ... other languages - }, - } - - // GLOBAL STATE - const state = { - running: false, - imageLoaded: false, - processing: false, - totalPixels: 0, - paintedPixels: 0, - availableColors: [], - activeColorPalette: [], // User-selected colors for conversion - paintWhitePixels: true, // Default to ON - currentCharges: 0, - maxCharges: 1, // Default max charges - cooldown: CONFIG.COOLDOWN_DEFAULT, - imageData: null, - stopFlag: false, - colorsChecked: false, - startPosition: null, - selectingPosition: false, - region: null, - minimized: false, - lastPosition: { x: 0, y: 0 }, - estimatedTime: 0, - language: "en", - paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels per second - cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, - overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, - blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, - } - - // Placeholder for the resize preview update function - let _updateResizePreview = () => {}; - - // --- OVERLAY UPDATE: New OverlayManager class to handle all overlay logic --- - class OverlayManager { - constructor() { - this.isEnabled = false; - this.startCoords = null; // { region: {x, y}, pixel: {x, y} } - this.imageBitmap = null; - this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap> - this.tileSize = 1000; - } - - toggle() { - this.isEnabled = !this.isEnabled; - console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`); - return this.isEnabled; - } - - enable() { this.isEnabled = true; } - disable() { this.isEnabled = false; } - clear() { - this.disable(); - this.imageBitmap = null; - this.chunkedTiles.clear(); - } - - async setImage(imageBitmap) { - this.imageBitmap = imageBitmap; - if (this.imageBitmap && this.startCoords) { - await this.processImageIntoChunks(); - } - } - - async setPosition(startPosition, region) { - if (!startPosition || !region) { - this.startCoords = null; - this.chunkedTiles.clear(); - return; - } - this.startCoords = { region, pixel: startPosition }; - if (this.imageBitmap) { - await this.processImageIntoChunks(); - } - } - - // --- OVERLAY UPDATE: Simplified chunking logic for solid, semi-transparent overlay --- - async processImageIntoChunks() { - if (!this.imageBitmap || !this.startCoords) return; - - this.chunkedTiles.clear(); - const { width: imageWidth, height: imageHeight } = this.imageBitmap; - const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; - const { x: startRegionX, y: startRegionY } = this.startCoords.region; - - const endPixelX = startPixelX + imageWidth; - const endPixelY = startPixelY + imageHeight; - - const startTileX = startRegionX + Math.floor(startPixelX / this.tileSize); - const startTileY = startRegionY + Math.floor(startPixelY / this.tileSize); - const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize); - const endTileY = startRegionY + Math.floor(endPixelY / this.tileSize); - - for (let ty = startTileY; ty <= endTileY; ty++) { - for (let tx = startTileX; tx <= endTileX; tx++) { - const tileKey = `${tx},${ty}`; - - // Calculate the portion of the image that overlaps with this tile - const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX; - const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY; - - // Crop coordinates within the source image - const sX = Math.max(0, imgStartX); - const sY = Math.max(0, imgStartY); - const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX)); - const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY)); - - if (sW <= 0 || sH <= 0) continue; - - // Destination coordinates on the new chunk canvas - const dX = Math.max(0, -imgStartX); - const dY = Math.max(0, -imgStartY); - - const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize); - const chunkCtx = chunkCanvas.getContext('2d'); - chunkCtx.imageSmoothingEnabled = false; - - chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH); - - // --- NEW: BLUE MARBLE EFFECT --- - if (state.blueMarbleEnabled) { - const imageData = chunkCtx.getImageData(0, 0, this.tileSize, this.tileSize); - const data = imageData.data; - for (let pixelY = 0; pixelY < this.tileSize; pixelY++) { - for (let pixelX = 0; pixelX < this.tileSize; pixelX++) { - const canvasX = pixelX; - const canvasY = pixelY; - const imageX = canvasX - dX; - const imageY = canvasY - dY; - - if ((imageX + imageY) % 2 === 0) { - const index = (canvasY * this.tileSize + canvasX) * 4; - if (data[index + 3] > 0) { - data[index + 3] = 0; - } - } - } - } - chunkCtx.putImageData(imageData, 0, 0); - } - - const chunkBitmap = await chunkCanvas.transferToImageBitmap(); - this.chunkedTiles.set(tileKey, chunkBitmap); - } - } - - console.log(`Overlay processed into ${this.chunkedTiles.size} chunks.`); - } - - // --- OVERLAY UPDATE: Simplified compositing logic for solid, semi-transparent overlay --- - async processAndRespondToTileRequest(eventData) { - const { endpoint, blobID, blobData } = eventData; - - let finalBlob = blobData; - - if (this.isEnabled && this.chunkedTiles.size > 0) { - const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/); - if (tileMatch) { - const tileX = parseInt(tileMatch[1], 10); - const tileY = parseInt(tileMatch[2], 10); - const tileKey = `${tileX},${tileY}`; - - const chunkBitmap = this.chunkedTiles.get(tileKey); - if (chunkBitmap) { - try { - const originalTileBitmap = await createImageBitmap(blobData); - const canvas = new OffscreenCanvas(originalTileBitmap.width, originalTileBitmap.height); - const ctx = canvas.getContext('2d'); - ctx.imageSmoothingEnabled = false; - - // Draw original tile first - ctx.drawImage(originalTileBitmap, 0, 0); - - // Set opacity and draw our solid overlay chunk on top - ctx.globalAlpha = state.overlayOpacity; - ctx.drawImage(chunkBitmap, 0, 0); - - finalBlob = await canvas.convertToBlob({ type: 'image/png' }); - } catch (e) { - console.error("Error compositing overlay:", e); - } - } - } - } - - // Send the (possibly modified) blob back to the injected script - window.postMessage({ - source: 'auto-image-overlay', - blobID: blobID, - blobData: finalBlob - }, '*'); - } -} - -const overlayManager = new OverlayManager(); - -// Turnstile token handling (promise-based) inspired by external logic -let turnstileToken = null -let _resolveToken = null -let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - -function setTurnstileToken(t) { - if (_resolveToken) { - _resolveToken(t) - _resolveToken = null - } - turnstileToken = t -} - -async function ensureToken() { - if (!turnstileToken) { - updateUI("captchaNeeded", "error") - Utils.showAlert(Utils.t("captchaNeeded"), "error") - try { await tokenPromise } catch (_) {} - } - return turnstileToken -} - -function inject(callback) { - const script = document.createElement('script'); - script.textContent = `(${callback})();`; - document.documentElement?.appendChild(script); - script.remove(); -} - -inject(() => { - const fetchedBlobQueue = new Map(); - - window.addEventListener('message', (event) => { - const { source, blobID, blobData } = event.data; - if (source === 'auto-image-overlay' && blobID && blobData) { - const callback = fetchedBlobQueue.get(blobID); - if (typeof callback === 'function') { - callback(blobData); - } - fetchedBlobQueue.delete(blobID); - } - }); - - const originalFetch = window.fetch; - window.fetch = async function(...args) { - const response = await originalFetch.apply(this, args); - const url = (args[0] instanceof Request) ? args[0].url : args[0]; - - if (typeof url === "string") { - if (url.includes("https://backend.wplace.live/s0/pixel/")) { - try { - const payload = JSON.parse(args[1].body); - if (payload.t) { - console.log("✅ Turnstile Token Captured:", payload.t); - window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); - } - } catch (_) { /* ignore */ } - } - - const contentType = response.headers.get('content-type') || ''; - if (contentType.includes('image/png') && url.includes('.png')) { - const cloned = response.clone(); - return new Promise(async (resolve) => { - const blobUUID = crypto.randomUUID(); - const originalBlob = await cloned.blob(); - - fetchedBlobQueue.set(blobUUID, (processedBlob) => { - resolve(new Response(processedBlob, { - headers: cloned.headers, - status: cloned.status, - statusText: cloned.statusText - })); - }); - - window.postMessage({ - source: 'auto-image-tile', - endpoint: url, - blobID: blobUUID, - blobData: originalBlob, - }, '*'); - }); - } - } - - return response; - }; -}); - -window.addEventListener('message', (event) => { - const { source, endpoint, blobID, blobData, token } = event.data; - - if (source === 'auto-image-tile' && endpoint && blobID && blobData) { - overlayManager.processAndRespondToTileRequest(event.data); - } - - if (source === 'turnstile-capture' && token) { - setTurnstileToken(token); - if (document.querySelector("#statusText")?.textContent.includes("CAPTCHA")) { - Utils.showAlert("Token captured successfully! You can start the bot now.", "success"); - updateUI("colorsFound", "success", { count: state.availableColors.length }); - } - } -}); - - async function detectLanguage() { - try { - const response = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await response.json() - state.language = data.language === "pt" ? "pt" : "en" - } catch { - state.language = navigator.language.startsWith("pt") ? "pt" : "en" - } - } - - // UTILITY FUNCTIONS - const Utils = { - sleep: (ms) => new Promise((r) => setTimeout(r, ms)), - - waitForSelector: async (selector, interval = 200, timeout = 5000) => { - const start = Date.now(); - while (Date.now() - start < timeout) { - const el = document.querySelector(selector); - if (el) return el; - await Utils.sleep(interval); - } - return null; - }, - - createElement: (tag, props = {}, children = []) => { - const element = document.createElement(tag) - - Object.entries(props).forEach(([key, value]) => { - if (key === 'style' && typeof value === 'object') { - Object.assign(element.style, value) - } else if (key === 'className') { - element.className = value - } else if (key === 'innerHTML') { - element.innerHTML = value - } else { - element.setAttribute(key, value) - } - }) - - if (typeof children === 'string') { - element.textContent = children - } else if (Array.isArray(children)) { - children.forEach(child => { - if (typeof child === 'string') { - element.appendChild(document.createTextNode(child)) - } else { - element.appendChild(child) - } - }) - } - - return element - }, - - createButton: (id, text, icon, onClick, style = CONFIG.CSS_CLASSES.BUTTON_PRIMARY) => { - const button = Utils.createElement('button', { - id: id, - style: style, - innerHTML: `${icon ? `` : ''}${text}` - }) - if (onClick) button.addEventListener('click', onClick) - return button - }, - - t: (key, params = {}) => { - let text = TEXT[state.language]?.[key] || TEXT.en[key] || key - Object.keys(params).forEach((param) => { - text = text.replace(`{${param}}`, params[param]) - }) - return text - }, - - showAlert: (message, type = "info") => { - const alertDiv = document.createElement("div") - alertDiv.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - padding: 12px 20px; - border-radius: 8px; - color: white; - font-weight: 600; - z-index: 10001; - max-width: 400px; - text-align: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - animation: slideDown 0.3s ease-out; - font-family: 'Segoe UI', sans-serif; - ` - - const colors = { - info: "background: linear-gradient(135deg, #3498db, #2980b9);", - success: "background: linear-gradient(135deg, #27ae60, #229954);", - warning: "background: linear-gradient(135deg, #f39c12, #e67e22);", - error: "background: linear-gradient(135deg, #e74c3c, #c0392b);", - } - - alertDiv.style.cssText += colors[type] || colors.info - - const style = document.createElement("style") - style.textContent = ` - @keyframes slideDown { - from { transform: translateX(-50%) translateY(-20px); opacity: 0; } - to { transform: translateX(-50%) translateY(0); opacity: 1; } - } - ` - document.head.appendChild(style) - - alertDiv.textContent = message - document.body.appendChild(alertDiv) - - setTimeout(() => { - alertDiv.style.animation = "slideDown 0.3s ease-out reverse" - setTimeout(() => { - document.body.removeChild(alertDiv) - document.head.removeChild(style) - }, 300) - }, 4000) - }, - - colorDistance: (a, b) => Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2)), - - findClosestPaletteColor: (r, g, b, palette) => { - let menorDist = Infinity; - let cor = [0, 0, 0]; - if (!palette || palette.length === 0) return cor; - - for (let i = 0; i < palette.length; i++) { - const [pr, pg, pb] = palette[i]; - const rmean = (pr + r) / 2; - const rdiff = pr - r; - const gdiff = pg - g; - const bdiff = pb - b; - const dist = Math.sqrt(((512 + rmean) * rdiff * rdiff >> 8) + 4 * gdiff * gdiff + ((767 - rmean) * bdiff * bdiff >> 8)); - if (dist < menorDist) { - menorDist = dist; - cor = [pr, pg, pb]; - } - } - return cor; - }, - - isWhitePixel: (r, g, b) => - r >= CONFIG.WHITE_THRESHOLD && g >= CONFIG.WHITE_THRESHOLD && b >= CONFIG.WHITE_THRESHOLD, - - createImageUploader: () => - new Promise((resolve) => { - const input = document.createElement("input") - input.type = "file" - input.accept = "image/png,image/jpeg" - input.onchange = () => { - const fr = new FileReader() - fr.onload = () => resolve(fr.result) - fr.readAsDataURL(input.files[0]) - } - input.click() - }), - - createFileDownloader: (data, filename) => { - const blob = new Blob([data], { type: "application/json" }) - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - URL.revokeObjectURL(url) - }, - - createFileUploader: () => - new Promise((resolve, reject) => { - const input = document.createElement("input") - input.type = "file" - input.accept = ".json" - input.onchange = (e) => { - const file = e.target.files[0] - if (file) { - const reader = new FileReader() - reader.onload = () => { - try { - const data = JSON.parse(reader.result) - resolve(data) - } catch (error) { - reject(new Error("Invalid JSON file")) - } - } - reader.onerror = () => reject(new Error("File reading error")) - reader.readAsText(file) - } else { - reject(new Error("No file selected")) - } - } - input.click() - }), - - extractAvailableColors: () => { - const colorElements = document.querySelectorAll('[id^="color-"]') - return Array.from(colorElements) - .filter((el) => !el.querySelector("svg")) - .filter((el) => { - const id = Number.parseInt(el.id.replace("color-", "")) - return id !== 0 - }) - .map((el) => { - const id = Number.parseInt(el.id.replace("color-", "")) - const rgbStr = el.style.backgroundColor.match(/\d+/g) - const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0] - return { id, rgb } - }) - }, - - formatTime: (ms) => { - const seconds = Math.floor((ms / 1000) % 60) - const minutes = Math.floor((ms / (1000 * 60)) % 60) - const hours = Math.floor((ms / (1000 * 60 * 60)) % 24) - const days = Math.floor(ms / (1000 * 60 * 60 * 24)) - - let result = "" - if (days > 0) result += `${days}d ` - if (hours > 0 || days > 0) result += `${hours}h ` - if (minutes > 0 || hours > 0 || days > 0) result += `${minutes}m ` - result += `${seconds}s` - - return result - }, - - calculateEstimatedTime: (remainingPixels, charges, cooldown) => { - if (remainingPixels <= 0) return 0 - - const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000 - const timeFromSpeed = remainingPixels * paintingSpeedDelay - - const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)) - const timeFromCharges = cyclesNeeded * cooldown - - return Math.max(timeFromSpeed, timeFromCharges) - }, - - saveProgress: () => { - try { - const progressData = { - timestamp: Date.now(), - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null, - } - - localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData)) - return true - } catch (error) { - console.error("Error saving progress:", error) - return false - } - }, - - loadProgress: () => { - try { - const saved = localStorage.getItem("wplace-bot-progress") - return saved ? JSON.parse(saved) : null - } catch (error) { - console.error("Error loading progress:", error) - return null - } - }, - - clearProgress: () => { - try { - localStorage.removeItem("wplace-bot-progress") - return true - } catch (error) { - console.error("Error clearing progress:", error) - return false - } - }, - - restoreProgress: (savedData) => { - try { - Object.assign(state, savedData.state) - - if (savedData.imageData) { - state.imageData = { - ...savedData.imageData, - pixels: new Uint8ClampedArray(savedData.imageData.pixels), - } - } - - if (savedData.paintedMap) { - state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)) - } - - return true - } catch (error) { - console.error("Error restoring progress:", error) - return false - } - }, - - saveProgressToFile: () => { - try { - const progressData = { - timestamp: Date.now(), - version: "1.0", - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - paintedMap: state.paintedMap ? state.paintedMap.map((row) => Array.from(row)) : null, - } - - const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json` - Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename) - return true - } catch (error) { - console.error("Error saving to file:", error) - return false - } - }, - - loadProgressFromFile: async () => { - try { - const data = await Utils.createFileUploader() - - if (!data.version || !data.state) { - throw new Error("Invalid file format") - } - - const success = Utils.restoreProgress(data) - return success - } catch (error) { - console.error("Error loading from file:", error) - throw error - } - }, - } - - // IMAGE PROCESSOR CLASS - class ImageProcessor { - constructor(imageSrc) { - this.imageSrc = imageSrc - this.img = null - this.canvas = null - this.ctx = null - } - - async load() { - return new Promise((resolve, reject) => { - this.img = new Image() - this.img.crossOrigin = "anonymous" - this.img.onload = () => { - this.canvas = document.createElement("canvas") - this.ctx = this.canvas.getContext("2d") - this.canvas.width = this.img.width - this.canvas.height = this.img.height - this.ctx.drawImage(this.img, 0, 0) - resolve() - } - this.img.onerror = reject - this.img.src = this.imageSrc - }) - } - - getDimensions() { - return { - width: this.canvas.width, - height: this.canvas.height, - } - } - - getPixelData() { - return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data - } - - resize(newWidth, newHeight) { - const tempCanvas = document.createElement("canvas") - const tempCtx = tempCanvas.getContext("2d") - - tempCanvas.width = newWidth - tempCanvas.height = newHeight - - tempCtx.imageSmoothingEnabled = false - tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight) - - this.canvas.width = newWidth - this.canvas.height = newHeight - this.ctx.imageSmoothingEnabled = false - this.ctx.drawImage(tempCanvas, 0, 0) - - return this.ctx.getImageData(0, 0, newWidth, newHeight).data - } - - generatePreview(width, height) { - const previewCanvas = document.createElement("canvas") - const previewCtx = previewCanvas.getContext("2d") - - previewCanvas.width = width - previewCanvas.height = height - - previewCtx.imageSmoothingEnabled = false - previewCtx.drawImage(this.img, 0, 0, width, height) - - return previewCanvas.toDataURL() - } - } - - // WPLACE API SERVICE - const WPlaceService = { - async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color) { - try { - await ensureToken() - if (!turnstileToken) return "token_error" - const payload = { coords: [pixelX, pixelY], colors: [color], t: turnstileToken } - const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", - body: JSON.stringify(payload), - }) - if (res.status === 403) { - console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.") - turnstileToken = null - tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - return "token_error" - } - const data = await res.json() - return data?.painted === 1 - } catch (e) { - console.error("Paint request failed:", e) - return false - } - }, - - async getCharges() { - try { - const res = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await res.json() - return { - charges: data.charges?.count || 0, - max: data.charges?.max || 1, - cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, - } - } catch (e) { - console.error("Failed to get charges:", e) - return { - charges: 0, - max: 1, - cooldown: CONFIG.COOLDOWN_DEFAULT, - } - } - }, - } - - // COLOR MATCHING FUNCTION - Optimized with caching - const colorCache = new Map() - - function findClosestColor(targetRgb, availableColors) { - const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}` - - if (colorCache.has(cacheKey)) { - return colorCache.get(cacheKey) - } - - const isNearWhite = targetRgb[0] >= 250 && targetRgb[1] >= 250 && targetRgb[2] >= 250 - if (isNearWhite) { - const whiteEntry = availableColors.find(c => c.rgb[0] >= 250 && c.rgb[1] >= 250 && c.rgb[2] >= 250) - if (whiteEntry) { - colorCache.set(cacheKey, whiteEntry.id) - return whiteEntry.id - } - } - - let minDistance = Number.POSITIVE_INFINITY - let closestColorId = availableColors[0]?.id || 1 - - for (let i = 0; i < availableColors.length; i++) { - const color = availableColors[i] - const distance = Utils.colorDistance(targetRgb, color.rgb) - if (distance < minDistance) { - minDistance = distance - closestColorId = color.id - if (distance === 0) break - } - } - - colorCache.set(cacheKey, closestColorId) - - if (colorCache.size > 10000) { - const firstKey = colorCache.keys().next().value - colorCache.delete(firstKey) - } - - return closestColorId - } - - // UI UPDATE FUNCTIONS (declared early to avoid reference errors) - let updateUI = () => {} - let updateStats = () => {} - let updateDataButtons = () => {} - - function initializeColorPalette(container) { - const colorContainer = container.querySelector('#colors-free'); - const paidContainer = container.querySelector('#colors-paid'); - if (!colorContainer || !paidContainer) return; - - colorContainer.innerHTML = ''; - paidContainer.innerHTML = ''; - - // Only use available colors that have been captured from the site - if (!state.availableColors || state.availableColors.length === 0) { - const noColorsMsg = document.createElement('div'); - noColorsMsg.textContent = 'No colors available. Please scan colors first.'; - noColorsMsg.style.padding = '10px'; - noColorsMsg.style.color = '#fff'; - colorContainer.appendChild(noColorsMsg); - return; - } - - state.availableColors.forEach(color => { - const rgb = color.rgb; - const rgbString = rgb.join(','); - const name = CONFIG.COLOR_NAMES[rgbString] || `Color ${color.id}`; - - const colorItem = Utils.createElement('div', { className: 'wplace-color-item' }); - const swatch = Utils.createElement('button', { - className: 'wplace-color-swatch active', - title: `${name} (ID: ${color.id})`, - 'data-rgb': rgbString, - }); - swatch.style.backgroundColor = `rgb(${rgbString})`; - - const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name); - - swatch.addEventListener('click', () => { - swatch.classList.toggle('active'); - updateActiveColorPalette(); - }); - - colorItem.appendChild(swatch); - colorItem.appendChild(nameLabel); - colorContainer.appendChild(colorItem); - }); - - // Update header texts since we only have available colors - const freeHeader = container.querySelector('.wplace-section-title'); - if (freeHeader) { - freeHeader.innerHTML = ' Available Colors'; - } - - // Hide the paid section as we only use available colors - const paidSection = container.querySelector('.wplace-color-divider')?.parentNode; - if (paidSection) { - paidSection.style.display = 'none'; - } - - // Update button text and functionality - container.querySelector('#selectAllFreeBtn').textContent = 'Select All'; - container.querySelector('#unselectAllFreeBtn').textContent = 'Unselect All'; - - container.querySelector('#selectAllFreeBtn')?.addEventListener('click', () => { - const swatches = container.querySelectorAll('.wplace-color-swatch'); - if (swatches) { - swatches.forEach(swatch => { - swatch.classList.add('active'); - }); - } - updateActiveColorPalette(); - }); - - container.querySelector('#unselectAllFreeBtn')?.addEventListener('click', () => { - const swatches = container.querySelectorAll('.wplace-color-swatch'); - if (swatches) { - swatches.forEach(swatch => { - swatch.classList.remove('active'); - }); - } - updateActiveColorPalette(); - }); - - updateActiveColorPalette(); - } - - function updateActiveColorPalette() { - state.activeColorPalette = []; - const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); - if (activeSwatches) { - activeSwatches.forEach(swatch => { - const rgb = swatch.getAttribute('data-rgb').split(',').map(Number); - state.activeColorPalette.push(rgb); - }); - } - - // If no colors selected, use all available colors - if (state.activeColorPalette.length === 0 && state.availableColors) { - state.activeColorPalette = state.availableColors.map(color => color.rgb); - } - - if (document.querySelector('.resize-container')?.style.display === 'block') { - _updateResizePreview(); - } - } - - function toggleAllColors(select, isPaid) { - const selector = isPaid ? '.wplace-color-swatch.paid' : '.wplace-color-swatch:not(.paid)'; - const swatches = document.querySelectorAll(selector); - if (swatches) { - swatches.forEach(swatch => { - swatch.classList.toggle('active', select); - }); - } - updateActiveColorPalette(); - } - - // --- END: UI UPDATE FUNCTIONS --- - - const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme] - - const switchTheme = (themeName) => { - if (CONFIG.THEMES[themeName]) { - CONFIG.currentTheme = themeName - saveThemePreference() - - // Remove existing theme styles - const existingStyle = document.querySelector('style[data-wplace-theme="true"]') - if (existingStyle) { - existingStyle.remove() - } - - // Recreate UI with new theme (cleanup is handled in createUI) - createUI() - } - } - - const saveThemePreference = () => { - try { - localStorage.setItem("wplace-theme", CONFIG.currentTheme) - } catch (e) { - console.warn("Could not save theme preference:", e) - } - } - - const loadThemePreference = () => { - try { - const saved = localStorage.getItem("wplace-theme") - if (saved && CONFIG.THEMES[saved]) { - CONFIG.currentTheme = saved - } - } catch (e) { - console.warn("Could not load theme preference:", e) - } - } - - const loadLanguagePreference = () => { - try { - const saved = localStorage.getItem("wplace_language") - if (saved && TEXT[saved]) { - state.language = saved - } - } catch (e) { - console.warn("Could not load language preference:", e) - } - } - // BILINGUAL TEXT STRINGS const TEXT = { en: { @@ -2313,15 +1163,1165 @@ window.addEventListener('message', (event) => { let updateStats = () => {} let updateDataButtons = () => {} - function initializeColorPalette(container) { - const colorContainer = container.querySelector('#colors-free'); - const paidContainer = container.querySelector('#colors-paid'); - if (!colorContainer || !paidContainer) return; + function updateActiveColorPalette() { + state.activeColorPalette = []; + const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); + if (activeSwatches) { + activeSwatches.forEach(swatch => { + const rgb = swatch.getAttribute('data-rgb').split(',').map(Number); + state.activeColorPalette.push(rgb); + }); + } + if (document.querySelector('.resize-container')?.style.display === 'block') { + _updateResizePreview(); + } + } - colorContainer.innerHTML = ''; + function toggleAllColors(select, isPaid) { + const selector = isPaid ? '.wplace-color-swatch.paid' : '.wplace-color-swatch:not(.paid)'; + const swatches = document.querySelectorAll(selector); + if (swatches) { + swatches.forEach(swatch => { + swatch.classList.toggle('active', select); + }); + } + updateActiveColorPalette(); + } + + function initializeColorPalette(container) { + const freeContainer = container.querySelector('#colors-free'); + const paidContainer = container.querySelector('#colors-paid'); + if (!freeContainer || !paidContainer) return; + + freeContainer.innerHTML = ''; paidContainer.innerHTML = ''; - // Only use available colors that have been captured from the site + const uniqueColors = [...new Set(CONFIG.COLOR_PALETTE.map(JSON.stringify))].map(JSON.parse); + + uniqueColors.forEach(rgb => { + const key = rgb.join(','); + const name = CONFIG.COLOR_NAMES[key] || `rgb(${key})`; + const isPaid = CONFIG.PAID_COLORS.has(key); + + const colorItem = Utils.createElement('div', { className: 'wplace-color-item' }); + const swatch = Utils.createElement('button', { + className: `wplace-color-swatch ${isPaid ? 'paid' : ''}`, + title: name, + 'data-rgb': key, + }); + swatch.style.backgroundColor = `rgb(${key})`; + + const nameLabel = Utils.createElement('span', { className: 'wplace-color-item-name' }, name); + + if (!isPaid) { + swatch.classList.add('active'); + } + + swatch.addEventListener('click', () => { + swatch.classList.toggle('active'); + updateActiveColorPalette(); + }); + + colorItem.appendChild(swatch); + colorItem.appendChild(nameLabel); + + if (isPaid) { + paidContainer.appendChild(colorItem); + } else { + freeContainer.appendChild(colorItem); + } + }); + + container.querySelector('#selectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(true, false)); + container.querySelector('#unselectAllFreeBtn')?.addEventListener('click', () => toggleAllColors(false, false)); + container.querySelector('#selectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(true, true)); + container.querySelector('#unselectAllPaidBtn')?.addEventListener('click', () => toggleAllColors(false, true)); + + updateActiveColorPalette(); + } + async function handleCaptcha() { + return new Promise(async (resolve, reject) => { + if (!CONFIG.AUTO_CAPTCHA_ENABLED) { + return reject(new Error("Auto-CAPTCHA is disabled.")); + } + + try { + const timeoutPromise = Utils.sleep(20000).then(() => reject(new Error("Auto-CAPTCHA timed out."))); + + const solvePromise = (async () => { + const mainPaintBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl', 200, 10000); + if (!mainPaintBtn) throw new Error("Could not find the main paint button."); + mainPaintBtn.click(); + await Utils.sleep(500); + + const transBtn = await Utils.waitForSelector('button#color-0', 200, 5000); + if (!transBtn) throw new Error("Could not find the transparent color button."); + transBtn.click(); + await Utils.sleep(500); + + const canvas = await Utils.waitForSelector('canvas', 200, 5000); + if (!canvas) throw new Error("Could not find the canvas element."); + + canvas.setAttribute('tabindex', '0'); + canvas.focus(); + const rect = canvas.getBoundingClientRect(); + const centerX = Math.round(rect.left + rect.width / 2); + const centerY = Math.round(rect.top + rect.height / 2); + + canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: centerX, clientY: centerY, bubbles: true })); + canvas.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true })); + await Utils.sleep(50); + canvas.dispatchEvent(new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true })); + await Utils.sleep(500); + + let confirmBtn = await Utils.waitForSelector('button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl'); + if (!confirmBtn) { + const allPrimary = Array.from(document.querySelectorAll('button.btn-primary')); + confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null; + } + if (!confirmBtn) throw new Error("Could not find the confirmation button."); + confirmBtn.click(); + + await tokenPromise; + resolve(); + })(); + + await Promise.race([solvePromise, timeoutPromise]); + + } catch (error) { + console.error("Auto-CAPTCHA process failed:", error); + reject(error); + } + }); + } + + + async function createUI() { + await detectLanguage() + + const existingContainer = document.getElementById("wplace-image-bot-container") + const existingStats = document.getElementById("wplace-stats-container") + const existingSettings = document.getElementById("wplace-settings-container") + const existingResizeContainer = document.querySelector(".resize-container") + const existingResizeOverlay = document.querySelector(".resize-overlay") + + if (existingContainer) existingContainer.remove() + if (existingStats) existingStats.remove() + if (existingSettings) existingSettings.remove() + if (existingResizeContainer) existingResizeContainer.remove() + if (existingResizeOverlay) existingResizeOverlay.remove() + + loadThemePreference() + loadLanguagePreference() + + const theme = getCurrentTheme() + + const fontAwesome = document.createElement("link") + fontAwesome.rel = "stylesheet" + fontAwesome.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" + document.head.appendChild(fontAwesome) + + if (theme.fontFamily.includes("Press Start 2P")) { + const googleFonts = document.createElement("link") + googleFonts.rel = "stylesheet" + googleFonts.href = "https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" + document.head.appendChild(googleFonts) + } + + const style = document.createElement("style") + style.setAttribute("data-wplace-theme", "true") + + style.textContent = ` + ${ + theme.animations.glow + ? ` + @keyframes neonGlow { + 0%, 100% { + text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor; + } + 50% { + text-shadow: 0 0 2px currentColor, 0 0 5px currentColor, 0 0 8px currentColor; + } + }` + : "" + } + + ${ + theme.animations.pixelBlink + ? ` + @keyframes pixelBlink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } + }` + : "" + } + + ${ + theme.animations.scanline + ? ` + @keyframes scanline { + 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } + }` + : "" + } + + @keyframes pulse { + 0% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0.7); } + 70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); } + 100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); } + } + @keyframes slideIn { + from { transform: translateY(-10px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } + } + @keyframes shimmer { + 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } + } + + #wplace-image-bot-container { + position: fixed; + top: 20px; + right: 20px; + width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "280px"}; + max-height: calc(100vh - 40px); + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)` + : theme.primary + }; + border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; + border-radius: ${theme.borderRadius}; + padding: 0; + box-shadow: ${theme.boxShadow}; + z-index: 9998; + font-family: ${theme.fontFamily}; + color: ${theme.text}; + animation: slideIn 0.4s ease-out; + overflow-y: auto; /* Allow scrolling for main panel */ + overflow-x: hidden; + ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""} + transition: all 0.3s ease; + user-select: none; + ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} + } + + ${ + theme.animations.scanline + ? ` + #wplace-image-bot-container::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent, ${theme.neon}, transparent); + animation: scanline 3s linear infinite; + z-index: 1; + pointer-events: none; + }` + : "" + } + + ${ + CONFIG.currentTheme === "Neon Retro" + ? ` + #wplace-image-bot-container::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 255, 65, 0.03) 2px, + rgba(0, 255, 65, 0.03) 4px + ); + pointer-events: none; + z-index: 1; + }` + : "" + } + + #wplace-image-bot-container.wplace-dragging { + transition: none; + box-shadow: 0 12px 40px rgba(0,0,0,0.8), 0 0 0 2px rgba(255,255,255,0.2); + transform: scale(1.02); + z-index: 9999; + } + #wplace-image-bot-container.wplace-minimized { + width: 200px; + height: auto; + overflow: hidden; + } + #wplace-image-bot-container.wplace-compact { + width: 240px; + } + + /* Stats Container */ + #wplace-stats-container { + position: fixed; + top: 20px; + left: 20px; + width: ${CONFIG.currentTheme === "Neon Retro" ? "280px" : "280px"}; + max-height: calc(100vh - 40px); + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.primary} 0%, #1a1a1a 100%)` + : theme.primary + }; + border: ${theme.borderWidth} ${theme.borderStyle} ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; + border-radius: ${theme.borderRadius}; + padding: 0; + box-shadow: ${theme.boxShadow}; + z-index: 9997; + font-family: ${theme.fontFamily}; + color: ${theme.text}; + animation: slideIn 0.4s ease-out; + overflow-y: auto; /* Make stats panel scrollable */ + ${theme.backdropFilter ? `backdrop-filter: ${theme.backdropFilter};` : ""} + transition: all 0.3s ease; + user-select: none; + ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} + } + + /* FIX: Disable transition during drag to prevent lag */ + #wplace-stats-container.wplace-dragging { + transition: none; + } + + .wplace-header { + padding: ${CONFIG.currentTheme === "Neon Retro" ? "8px 12px" : "8px 12px"}; + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.secondary} 0%, #2a2a2a 100%)` + : theme.secondary + }; + color: ${theme.highlight}; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "11px" : "13px"}; + font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "700"}; + display: flex; + justify-content: space-between; + align-items: center; + cursor: move; + user-select: none; + border-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "2px" : "1px"} solid ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.text}; + ${CONFIG.currentTheme === "Classic Autobot" ? "text-shadow: 0 1px 2px rgba(0,0,0,0.5);" : "text-transform: uppercase; letter-spacing: 1px;"} + transition: background 0.2s ease; + position: relative; + z-index: 2; + ${theme.animations.glow ? "animation: neonGlow 2s ease-in-out infinite alternate;" : ""} + } + + .wplace-header-title { + display: flex; + align-items: center; + gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"}; + } + + .wplace-header-controls { + display: flex; + gap: ${CONFIG.currentTheme === "Neon Retro" ? "6px" : "6px"}; + } + + .wplace-header-btn { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent}; + border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"}; + color: ${theme.text}; + cursor: pointer; + border-radius: ${CONFIG.currentTheme === "Classic Autobot" ? "4px" : "0"}; + width: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"}; + height: ${CONFIG.currentTheme === "Classic Autobot" ? "18px" : "auto"}; + padding: ${CONFIG.currentTheme === "Neon Retro" ? "4px 6px" : "0"}; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"}; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; + font-family: ${theme.fontFamily}; + ${CONFIG.currentTheme === "Neon Retro" ? "image-rendering: pixelated;" : ""} + } + .wplace-header-btn:hover { + background: ${CONFIG.currentTheme === "Classic Autobot" ? theme.accent : theme.text}; + color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.text : theme.primary}; + transform: ${CONFIG.currentTheme === "Classic Autobot" ? "scale(1.1)" : "none"}; + ${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.text};` : ""} + } + + .wplace-content { + padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"}; + display: block; + position: relative; + z-index: 2; + } + .wplace-content.wplace-hidden { + display: none; + } + + .wplace-status-section { + margin-bottom: 12px; + padding: 8px; + background: rgba(255,255,255,0.03); + border-radius: ${theme.borderRadius}; + border: 1px solid rgba(255,255,255,0.1); + } + + .wplace-section { + margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "12px"}; + padding: 12px; + background: rgba(255,255,255,0.03); + border-radius: ${theme.borderRadius}; + border: 1px solid rgba(255,255,255,0.1); + } + + .wplace-section-title { + font-size: 11px; + font-weight: 600; + margin-bottom: 8px; + color: ${theme.highlight}; + display: flex; + align-items: center; + gap: 6px; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + .wplace-controls { + display: flex; + flex-direction: column; + gap: 8px; + } + .wplace-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + } + .wplace-row.single { + grid-template-columns: 1fr; + } + + .wplace-btn { + padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px 8px" : "8px 12px"}; + border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "none"}; + border-radius: ${theme.borderRadius}; + font-weight: ${CONFIG.currentTheme === "Neon Retro" ? "normal" : "500"}; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "6px"}; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + font-family: ${theme.fontFamily}; + ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px; image-rendering: pixelated;" : ""} + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.accent} 0%, #4a4a4a 100%)` + : theme.accent + }; + ${CONFIG.currentTheme === "Classic Autobot" ? "border: 1px solid rgba(255,255,255,0.1);" : ""} + } + + ${ + CONFIG.currentTheme === "Classic Autobot" + ? ` + .wplace-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); + transition: left 0.5s ease; + } + .wplace-btn:hover:not(:disabled)::before { + left: 100%; + }` + : ` + .wplace-btn::before { + content: ''; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s; + } + .wplace-btn:hover::before { + left: 100%; + }` + } + + .wplace-btn:hover:not(:disabled) { + transform: ${CONFIG.currentTheme === "Classic Autobot" ? "translateY(-1px)" : "none"}; + box-shadow: ${ + CONFIG.currentTheme === "Classic Autobot" ? "0 4px 12px rgba(0,0,0,0.4)" : "0 0 15px currentColor" + }; + ${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""} + } + .wplace-btn:active:not(:disabled) { + transform: translateY(0); + } + + .wplace-btn-primary { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.accent} 0%, #6a5acd 100%)` + : theme.accent + }; + color: ${theme.text}; + ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.text};` : ""} + } + .wplace-btn-upload { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.secondary} 0%, #4a4a4a 100%)` + : theme.purple + }; + color: ${theme.text}; + ${ + CONFIG.currentTheme === "Classic Autobot" + ? `border: 1px dashed ${theme.highlight};` + : `border-color: ${theme.text}; border-style: dashed;` + } + } + .wplace-btn-start { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.success} 0%, #228b22 100%)` + : theme.success + }; + color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; + ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.success};` : ""} + } + .wplace-btn-stop { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.error} 0%, #dc143c 100%)` + : theme.error + }; + color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.text}; + ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.error};` : ""} + } + .wplace-btn-select { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)` + : theme.highlight + }; + color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; + ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.highlight};` : ""} + } + .wplace-btn-file { + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? "linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%)" + : theme.warning + }; + color: ${CONFIG.currentTheme === "Classic Autobot" ? "white" : theme.primary}; + ${CONFIG.currentTheme === "Neon Retro" ? `border-color: ${theme.warning};` : ""} + } + .wplace-btn:disabled { + opacity: ${CONFIG.currentTheme === "Classic Autobot" ? "0.5" : "0.3"}; + cursor: not-allowed; + transform: none !important; + ${theme.animations.pixelBlink ? "animation: none !important;" : ""} + box-shadow: none !important; + } + .wplace-btn:disabled::before { + display: none; + } + + .wplace-btn-overlay.active { + background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); + box-shadow: 0 0 15px #8e2de2; + } + + .wplace-stats { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.03)" : theme.secondary}; + padding: ${CONFIG.currentTheme === "Neon Retro" ? "12px" : "8px"}; + border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"}; + border-radius: ${theme.borderRadius}; + margin-bottom: ${CONFIG.currentTheme === "Neon Retro" ? "15px" : "8px"}; + ${CONFIG.currentTheme === "Neon Retro" ? "box-shadow: inset 0 0 10px rgba(0, 255, 65, 0.1);" : ""} + } + + .wplace-stat-item { + display: flex; + justify-content: space-between; + padding: ${CONFIG.currentTheme === "Neon Retro" ? "6px 0" : "4px 0"}; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; + border-bottom: 1px solid rgba(255,255,255,0.05); + ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} + } + .wplace-stat-item:last-child { + border-bottom: none; + } + .wplace-stat-label { + display: flex; + align-items: center; + gap: 6px; + opacity: 0.9; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "10px"}; + } + .wplace-stat-value { + font-weight: 600; + color: ${theme.highlight}; + } + + .wplace-colors-section { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid rgba(255,255,255,0.05); + } + + .wplace-stat-colors-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(16px, 1fr)); + gap: 4px; + margin-top: 8px; + padding: 4px; + background: rgba(0,0,0,0.2); + border-radius: 4px; + max-height: 80px; /* Limit height and allow scrolling */ + overflow-y: auto; + } + + .wplace-stat-color-swatch { + width: 16px; + height: 16px; + border-radius: 3px; + border: 1px solid rgba(255,255,255,0.1); + box-shadow: inset 0 0 2px rgba(0,0,0,0.5); + } + + .wplace-progress { + width: 100%; + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0,0,0,0.3)" : theme.secondary}; + border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "1px solid rgba(255,255,255,0.1)"}; + border-radius: ${theme.borderRadius}; + margin: ${CONFIG.currentTheme === "Neon Retro" ? "10px 0" : "8px 0"}; + overflow: hidden; + height: ${CONFIG.currentTheme === "Neon Retro" ? "16px" : "6px"}; + position: relative; + } + + ${ + CONFIG.currentTheme === "Neon Retro" + ? ` + .wplace-progress::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + repeating-linear-gradient( + 45deg, + transparent, + transparent 2px, + rgba(0, 255, 65, 0.1) 2px, + rgba(0, 255, 65, 0.1) 4px + ); + pointer-events: none; + }` + : "" + } + + .wplace-progress-bar { + height: ${CONFIG.currentTheme === "Neon Retro" ? "100%" : "6px"}; + background: ${ + CONFIG.currentTheme === "Classic Autobot" + ? `linear-gradient(135deg, ${theme.highlight} 0%, #9370db 100%)` + : `linear-gradient(90deg, ${theme.success}, ${theme.neon})` + }; + transition: width ${CONFIG.currentTheme === "Neon Retro" ? "0.3s" : "0.5s"} ease; + position: relative; + ${CONFIG.currentTheme === "Neon Retro" ? `box-shadow: 0 0 10px ${theme.success};` : ""} + } + + ${ + CONFIG.currentTheme === "Classic Autobot" + ? ` + .wplace-progress-bar::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); + animation: shimmer 2s infinite; + }` + : ` + .wplace-progress-bar::after { + content: ''; + position: absolute; + top: 0; + right: 0; + width: 4px; + height: 100%; + background: ${theme.text}; + animation: pixelBlink 1s infinite; + }` + } + + .wplace-status { + padding: ${CONFIG.currentTheme === "Neon Retro" ? "10px" : "6px"}; + border: ${CONFIG.currentTheme === "Neon Retro" ? "2px solid" : "1px solid"}; + border-radius: ${theme.borderRadius}; + text-align: center; + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "11px"}; + ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} + position: relative; + overflow: hidden; + } + + .status-default { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255,255,255,0.1)" : theme.accent}; + border-color: ${theme.text}; + color: ${theme.text}; + } + .status-success { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(0, 255, 0, 0.1)" : theme.success}; + border-color: ${theme.success}; + color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.success : theme.primary}; + box-shadow: 0 0 15px ${theme.success}; + } + .status-error { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 0, 0, 0.1)" : theme.error}; + border-color: ${theme.error}; + color: ${CONFIG.currentTheme === "Classic Autobot" ? theme.error : theme.text}; + box-shadow: 0 0 15px ${theme.error}; + ${theme.animations.pixelBlink ? "animation: pixelBlink 0.5s infinite;" : ""} + } + .status-warning { + background: ${CONFIG.currentTheme === "Classic Autobot" ? "rgba(255, 165, 0, 0.1)" : theme.warning}; + border-color: ${theme.warning}; + color: ${CONFIG.currentTheme === "Classic Autobot" ? "orange" : theme.primary}; + box-shadow: 0 0 15px ${theme.warning}; + } + + .resize-container { + display: none; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: ${theme.primary}; + padding: 20px; + border: ${theme.borderWidth} ${theme.borderStyle} ${theme.text}; + border-radius: ${theme.borderRadius}; + z-index: 10000; + box-shadow: ${ + CONFIG.currentTheme === "Classic Autobot" ? "0 0 20px rgba(0,0,0,0.5)" : "0 0 30px rgba(0, 255, 65, 0.5)" + }; + width: 90%; + max-width: 700px; + max-height: 90%; + overflow: auto; + font-family: ${theme.fontFamily}; + } + + .resize-preview-wrapper { + display: flex; + justify-content: center; + align-items: center; + border: 1px solid ${theme.accent}; + background: rgba(0,0,0,0.2); + margin: 15px 0; + height: 300px; + overflow: auto; + } + + .resize-preview { + max-width: none; + transition: transform 0.1s ease; + image-rendering: pixelated; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; + } + + .resize-controls { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; + align-items: center; + } + + .resize-controls label { + font-size: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "12px"}; + ${CONFIG.currentTheme === "Neon Retro" ? "text-transform: uppercase; letter-spacing: 1px;" : ""} + color: ${theme.text}; + } + + .resize-slider { + width: 100%; + height: ${CONFIG.currentTheme === "Neon Retro" ? "8px" : "4px"}; + background: ${CONFIG.currentTheme === "Classic Autobot" ? "#ccc" : theme.secondary}; + border: ${CONFIG.currentTheme === "Neon Retro" ? `2px solid ${theme.text}` : "none"}; + border-radius: ${theme.borderRadius}; + outline: none; + -webkit-appearance: none; + } + + ${ + CONFIG.currentTheme === "Neon Retro" + ? ` + .resize-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: ${theme.highlight}; + border: 2px solid ${theme.text}; + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 5px ${theme.highlight}; + } + + .resize-slider::-moz-range-thumb { + width: 16px; + height: 16px; + background: ${theme.highlight}; + border: 2px solid ${theme.text}; + border-radius: 0; + cursor: pointer; + box-shadow: 0 0 5px ${theme.highlight}; + }` + : "" + } + + .resize-zoom-controls { + grid-column: 1 / -1; + display: flex; + align-items: center; + gap: 10px; + margin-top: 15px; + } + + .resize-buttons { + display: flex; + gap: 10px; + justify-content: center; + margin-top: 20px; + } + + .resize-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + z-index: 9999; + display: none; + } + .wplace-color-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); + gap: 10px; + padding-top: 8px; + } + .wplace-color-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + } + .wplace-color-item-name { + font-size: 9px; + color: #ccc; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 100%; + } + .wplace-color-swatch { + width: 22px; + height: 22px; + border: 1px solid rgba(255,255,255,0.2); + border-radius: 4px; + cursor: pointer; + transition: transform 0.1s ease, box-shadow 0.2s ease; + position: relative; + margin: 0 auto; + } + .wplace-color-swatch.paid { + border-color: gold; + } + .wplace-color-swatch:hover { + transform: scale(1.1); + z-index: 1; + } + .wplace-color-swatch:not(.active) { + opacity: 0.3; + filter: grayscale(80%); + } + .wplace-color-swatch.active::after { + content: '✔'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: white; + font-size: 12px; + font-weight: bold; + text-shadow: 0 0 3px black; + } + .wplace-color-divider { + border: none; + height: 1px; + background: rgba(255,255,255,0.1); + margin: 8px 0; + } + + .wplace-cooldown-control { + margin-top: 8px; + } + .wplace-cooldown-control label { + font-size: 11px; + margin-bottom: 4px; + display: block; + } + .wplace-slider-container { + display: flex; + align-items: center; + gap: 8px; + } + .wplace-slider { + flex: 1; + -webkit-appearance: none; + appearance: none; + height: 4px; + background: #444; + border-radius: 2px; + outline: none; + } + .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 14px; + height: 14px; + background: ${theme.highlight}; + border-radius: 50%; + cursor: pointer; + } + + + ${ + CONFIG.currentTheme === "Neon Retro" + ? ` + input[type="checkbox"] { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + width: 16px; + height: 16px; + border: 2px solid ${theme.text}; + background: ${theme.secondary}; + margin-right: 8px; + position: relative; + cursor: pointer; + } + + input[type="checkbox"]:checked { + background: ${theme.success}; + } + + input[type="checkbox"]:checked::after { + content: '✓'; + position: absolute; + top: -2px; + left: 1px; + color: ${theme.primary}; + font-size: 12px; + font-weight: bold; + } + + .fas, .fa { + filter: drop-shadow(0 0 3px currentColor); + } + + .wplace-speed-control { + margin-top: 12px; + padding: 12px; + background: ${theme.secondary}; + border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent}; + border-radius: ${theme.borderRadius}; + backdrop-filter: ${theme.backdropFilter}; + } + + .wplace-speed-label { + display: flex; + align-items: center; + margin-bottom: 8px; + color: ${theme.text}; + font-size: 13px; + font-weight: 600; + } + + .wplace-speed-label i { + margin-right: 6px; + color: ${theme.highlight}; + } + + .wplace-speed-slider-container { + display: flex; + align-items: center; + gap: 12px; + } + + .wplace-speed-slider { + flex: 1; + height: 6px; + border-radius: 3px; + background: ${theme.primary}; + outline: none; + cursor: pointer; + -webkit-appearance: none; + appearance: none; + } + + .wplace-speed-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: ${theme.highlight}; + cursor: pointer; + border: 2px solid ${theme.text}; + box-shadow: ${theme.boxShadow}; + } + + .wplace-speed-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: ${theme.highlight}; + cursor: pointer; + border: 2px solid ${theme.text}; + box-shadow: ${theme.boxShadow}; + } + + .wplace-speed-display { + display: flex; + align-items: center; + gap: 4px; + min-width: 90px; + justify-content: flex-end; + } + + #speedValue { + color: ${theme.highlight}; + font-weight: 600; + font-size: 14px; + } + + .wplace-speed-unit { + color: ${theme.text}; + font-size: 11px; + opacity: 0.8; + } + + #wplace-settings-container { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10001; + min-width: 400px; + max-width: 500px; + background: ${theme.primary}; + border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent}; + border-radius: ${theme.borderRadius}; + box-shadow: ${theme.boxShadow}; + backdrop-filter: ${theme.backdropFilter}; + } + + .wplace-settings { + padding: 16px; + max-height: 400px; + overflow-y: auto; + } + + .wplace-setting-section { + margin-bottom: 20px; + padding: 12px; + background: ${theme.secondary}; + border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent}; + border-radius: ${theme.borderRadius}; + } + + .wplace-setting-title { + display: flex; + align-items: center; + margin-bottom: 12px; + color: ${theme.text}; + font-size: 14px; + font-weight: 600; + } + + .wplace-setting-title i { + margin-right: 8px; + color: ${theme.highlight}; + } + + .wplace-setting-content { + color: ${theme.text}; + } + + .wplace-section { + margin-bottom: 20px; + padding: 15px; + background: ${theme.secondary}; + border: ${theme.borderWidth} ${theme.borderStyle} ${theme.accent}; + border-radius: ${theme.borderRadius}; + } + + .wplace-section-title { + display: flex; + align-items: center; + margin-bottom: 15px; + color: ${theme.text}; + font-size: 14px; + font-weight: 600; + } + + .wplace-section-title i { + margin-right: 8px; + color: ${theme.highlight}; + } + + .wplace-speed-container { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 10px; + } + + .wplace-slider { + flex: 1; + height: 6px; + background: ${theme.accent}; + border-radius: 3px; + outline: none; + -webkit-appearance: none; + } + + .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; width: 18px; height: 18px; background: ${theme.highlight};