-
-
-
- ${Utils.t('initMessage')}
-
-
-
-
-
-
-
🖼️ Image Management
-
-
-
-
- ${Utils.t('uploadImage')}
-
-
-
- ${Utils.t('resizeImage')}
-
-
-
-
-
- ${Utils.t('selectPosition')}
-
-
-
-
-
-
-
-
🎮 Painting Control
-
-
-
-
- ${Utils.t('startPainting')}
-
-
-
- ${Utils.t('stopPainting')}
-
-
-
-
-
- ${Utils.t('toggleOverlay')}
-
-
-
-
-
-
-
-
⏱️ ${Utils.t('cooldownSettings')}
-
-
${Utils.t('waitCharges')}:
-
-
-
-
-
-
-
💾 Data Management
-
-
-
-
- ${Utils.t('saveData')}
-
-
-
- ${Utils.t('loadData')}
-
-
-
-
-
- ${Utils.t('saveToFile')}
-
-
-
- ${Utils.t('loadFromFile')}
-
-
-
-
-
- `;
-
- // Stats Window - Separate UI
- const statsContainer = document.createElement('div');
- statsContainer.id = 'wplace-stats-container';
- statsContainer.style.display = 'none';
- statsContainer.innerHTML = `
-
-
-
-
-
-
-
- Token Source
-
-
-
- 🤖 Automatic Token Generator (Recommended)
- 🔄 Generator + Auto Fallback
- 🎯 Manual Pixel Placement
-
-
- Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement.
-
-
-
-
-
-
-
-
- ${Utils.t('automation')}
-
-
-
-
-
-
-
-
- Overlay Settings
-
-
-
-
-
-
-
-
-
-
-
Blue Marble Effect
-
Renders a dithered "shredded" overlay.
-
-
-
-
-
-
-
-
-
-
- ${Utils.t('paintOptions')}
-
-
-
-
-
-
-
- ${Utils.t('paintWhitePixels')}
-
-
- ${Utils.t('paintWhitePixelsDescription')}
-
-
-
-
-
-
-
-
-
- ${Utils.t('paintTransparentPixels')}
-
-
- ${Utils.t('paintTransparentPixelsDescription')}
-
-
-
-
-
-
-
${Utils.t('paintUnavailablePixels')}
-
${Utils.t('paintUnavailablePixelsDescription')}
-
-
-
-
-
-
-
-
-
-
- ${Utils.t('paintingSpeed')}
-
-
-
-
-
-
- Batch Mode
-
-
- 📦 Normal (Fixed Size)
- 🎲 Random (Range)
-
-
-
-
-
-
-
-
- ${CONFIG.PAINTING_SPEED.MIN}
- ${CONFIG.PAINTING_SPEED.MAX}
-
-
-
-
-
-
-
- 🎲 Random batch size between min and max values
-
-
-
-
-
-
- ${Utils.t('enablePaintingSpeedLimit')}
-
-
-
-
-
-
-
- Coordinate Generation
-
-
-
-
-
-
- Generation Mode
-
-
- 📏 Rows (Horizontal Lines)
- 📐 Columns (Vertical Lines)
- ⭕ Circle Out (Center → Edges)
- ⭕ Circle In (Edges → Center)
- 🟫 Blocks (Ordered)
- 🎲 Shuffle Blocks (Random)
-
-
-
-
-
-
-
- Starting Direction
-
-
- ↖️ Top-Left
- ↗️ Top-Right
- ↙️ Bottom-Left
- ↘️ Bottom-Right
-
-
-
-
-
-
-
-
Snake Pattern
-
Alternate direction for each row/column (zigzag pattern)
-
-
-
-
-
-
-
-
-
- 🧱 Block dimensions for block-based generation modes
-
-
-
-
-
-
-
-
- Desktop Notifications
-
-
-
-
-
-
-
-
- ${Utils.t('themeSettings')}
-
-
-
- ${Object.keys(CONFIG.THEMES)
- .map(
- (themeName) =>
- `${themeName} `
- )
- .join('')}
-
-
-
-
-
-
-
-
- ${Utils.t('language')}
-
-
-
- 🇻🇳 Tiếng Việt
- 🇮🇩 Bahasa Indonesia
- 🇷🇺 Русский
- 🇺🇦 Українська
- 🇺🇸 English
- 🇧🇷 Português
- 🇫🇷 Français
- 🇹🇷 Türkçe
- 🇨🇳 简体中文
- 🇹🇼 繁體中文
- 🇯🇵 日本語
- 🇰🇷 한국어
-
-
-
-
-
-
-
-
- `;
-
- const resizeContainer = document.createElement('div');
- resizeContainer.className = 'resize-container';
- resizeContainer.innerHTML = `
- } true if tiles are ready
+ */
+ async waitForTiles(
+ startRegionX,
+ startRegionY,
+ pixelWidth,
+ pixelHeight,
+ startPixelX = 0,
+ startPixelY = 0,
+ timeoutMs = 10000
+ ) {
+ const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange(
+ startRegionX,
+ startRegionY,
+ startPixelX,
+ startPixelY,
+ pixelWidth,
+ pixelHeight,
+ this.tileSize
+ );
+
+ const requiredTiles = [];
+ for (let ty = startTileY; ty <= endTileY; ty++) {
+ for (let tx = startTileX; tx <= endTileX; tx++) {
+ requiredTiles.push(`${tx},${ty}`);
+ }
+ }
+
+ if (requiredTiles.length === 0) return true;
+
+ const startTime = Date.now();
+
+ while (Date.now() - startTime < timeoutMs) {
+ if (state.stopFlag) {
+ console.log('waitForTiles: stopped by user');
+ return false;
+ }
+
+ const missing = requiredTiles.filter((key) => !this.originalTiles.has(key));
+ if (missing.length === 0) {
+ console.log(`✅ All ${requiredTiles.length} required tiles are loaded`);
+ return true;
+ }
+
+ await Utils.sleep(100);
+ }
+
+ console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required,
+ ${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`);
+ return false;
+ }
+ }
+
+ const overlayManager = new OverlayManager();
+
+ // Optimized Turnstile token handling with improved caching and retry logic
+ let turnstileToken = null;
+ let tokenExpiryTime = 0;
+ let tokenGenerationInProgress = false;
+ let _resolveToken = null;
+ let tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ let retryCount = 0;
+ const MAX_RETRIES = 10;
+ const MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending
+ const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety)
+
+ function setTurnstileToken(token) {
+ if (_resolveToken) {
+ _resolveToken(token);
+ _resolveToken = null;
+ }
+ turnstileToken = token;
+ tokenExpiryTime = Date.now() + TOKEN_LIFETIME;
+ console.log('✅ Turnstile token set successfully');
+ }
+
+ function isTokenValid() {
+ return turnstileToken && Date.now() < tokenExpiryTime;
+ }
+
+ function invalidateToken() {
+ turnstileToken = null;
+ tokenExpiryTime = 0;
+ console.log('🗑️ Token invalidated, will force fresh generation');
+ }
+
+ async function ensureToken(forceRefresh = false) {
+ // Return cached token if still valid and not forcing refresh
+ if (isTokenValid() && !forceRefresh) {
+ return turnstileToken;
+ }
+
+ // Invalidate token if forcing refresh
+ if (forceRefresh) invalidateToken();
+
+ // Avoid multiple simultaneous token generations
+ if (tokenGenerationInProgress) {
+ console.log('🔄 Token generation already in progress, waiting...');
+ await Utils.sleep(2000);
+ return isTokenValid() ? turnstileToken : null;
+ }
+
+ tokenGenerationInProgress = true;
+
+ try {
+ console.log('🔄 Token expired or missing, generating new one...');
+ const token = await handleCaptchaWithRetry();
+ if (token && token.length > 20) {
+ setTurnstileToken(token);
+ console.log('✅ Token captured and cached successfully');
+ return token;
+ }
+
+ console.log('⚠️ Invisible Turnstile failed, forcing browser automation...');
+ const fallbackToken = await handleCaptchaFallback();
+ if (fallbackToken && fallbackToken.length > 20) {
+ setTurnstileToken(fallbackToken);
+ console.log('✅ Fallback token captured successfully');
+ return fallbackToken;
+ }
+
+ console.log('❌ All token generation methods failed');
+ return null;
+ } finally {
+ tokenGenerationInProgress = false;
+ }
+ }
+
+ async function handleCaptchaWithRetry() {
+ const startTime = performance.now();
+
+ try {
+ const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken();
+
+ if (!sitekey) {
+ throw new Error('No valid sitekey found');
+ }
+
+ console.log('🔑 Using sitekey:', sitekey);
+
+ if (typeof window !== 'undefined' && window.navigator) {
+ console.log(
+ '🧭 UA:',
+ window.navigator.userAgent.substring(0, 50) + '...',
+ 'Platform:',
+ window.navigator.platform
+ );
+ }
+
+ let token = null;
+
+ if (
+ preGeneratedToken &&
+ typeof preGeneratedToken === 'string' &&
+ preGeneratedToken.length > 20
+ ) {
+ console.log('♻️ Reusing pre-generated Turnstile token');
+ token = preGeneratedToken;
+ } else {
+ if (isTokenValid()) {
+ console.log('♻️ Using existing cached token (from previous session)');
+ token = turnstileToken;
+ } else {
+ console.log('🔐 Generating new token with executeTurnstile...');
+ token = await Utils.executeTurnstile(sitekey, 'paint');
+ if (token) setTurnstileToken(token);
+ }
+ }
+
+ if (token && typeof token === 'string' && token.length > 20) {
+ const elapsed = Math.round(performance.now() - startTime);
+ console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`);
+ return token;
+ } else {
+ throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`);
+ }
+ } catch (error) {
+ const elapsed = Math.round(performance.now() - startTime);
+ console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error);
+ throw error;
+ }
+ }
+ const randStr = (len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') =>
+ [...Array(len)].map(() => chars[(crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) || Math.floor(Math.random() * chars.length)]).join('')
+ const fpStr32 = randStr(32);
+ async function handleCaptchaFallback() {
+ // Implementation for fallback token generation would go here
+ // This is a placeholder for browser automation fallback
+ console.log('🔄 Attempting fallback token generation...');
+ return null;
+ }
+
+ 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) {
+ // 📊 Debug log
+ console.log(
+ `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t
+ ? typeof payload.t === 'string'
+ ? payload.t.length > 50
+ ? payload.t.substring(0, 50) + '...'
+ : payload.t
+ : JSON.stringify(payload.t)
+ : 'null/undefined'
+ }, Length: ${payload.t?.length || 0}`
+ );
+ 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(Utils.t('tokenCapturedSuccess'), '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)),
+
+ dynamicSleep: async function (tickAndGetRemainingMs) {
+ let remaining = Math.max(0, await tickAndGetRemainingMs());
+ while (remaining > 0) {
+ const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100;
+ await this.sleep(Math.min(interval, remaining));
+ remaining = Math.max(0, await tickAndGetRemainingMs());
+ }
+ },
+
+ 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;
+ },
+
+ msToTimeText(ms) {
+ const totalSeconds = Math.ceil(ms / 1000);
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+
+ if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
+ if (minutes > 0) return `${minutes}m ${seconds}s`;
+ return `${seconds}s`;
+ },
+
+ // Debounced scroll-to-adjust handler for sliders
+ createScrollToAdjust: (element, updateCallback, min, max, step = 1) => {
+ let debounceTimer = null;
+
+ const handleWheel = (e) => {
+ // Only trigger when hovering over the slider
+ if (e.target !== element) return;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ // Clear existing debounce timer
+ if (debounceTimer) {
+ clearTimeout(debounceTimer);
+ }
+
+ // Debounce the adjustment to make it precise
+ debounceTimer = setTimeout(() => {
+ const currentValue = parseInt(element.value) || 0;
+ const delta = e.deltaY > 0 ? -step : step;
+ const newValue = Math.max(min, Math.min(max, currentValue + delta));
+
+ if (newValue !== currentValue) {
+ element.value = newValue;
+ updateCallback(newValue);
+ }
+ }, 50); // 50ms debounce for precise control
+ };
+
+ element.addEventListener('wheel', handleWheel, { passive: false });
+
+ // Return cleanup function
+ return () => {
+ if (debounceTimer) clearTimeout(debounceTimer);
+ element.removeEventListener('wheel', handleWheel);
+ };
+ },
+
+ /**
+ * Calculate the range of tile coordinates (in region space) that cover a given image area.
+ * @param {number} startRegionX - Base region X
+ * @param {number} startRegionY - Base region Y
+ * @param {number} startPixelX - Starting pixel X within the region grid
+ * @param {number} startPixelY - Starting pixel Y within the region grid
+ * @param {number} width - Image width in pixels
+ * @param {number} height - Image height in pixels
+ * @param {number} tileSize - Size of a tile (default 1000)
+ * @returns {{ startTileX: number, startTileY: number, endTileX: number, endTileY: number }}
+ */
+ calculateTileRange(
+ startRegionX,
+ startRegionY,
+ startPixelX,
+ startPixelY,
+ width,
+ height,
+ tileSize = 1000
+ ) {
+ const endPixelX = startPixelX + width;
+ const endPixelY = startPixelY + height;
+
+ return {
+ startTileX: startRegionX + Math.floor(startPixelX / tileSize),
+ startTileY: startRegionY + Math.floor(startPixelY / tileSize),
+ endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize),
+ endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize),
+ };
+ }, // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup
+ turnstileLoaded: false,
+ _turnstileContainer: null,
+ _turnstileOverlay: null,
+ _turnstileWidgetId: null,
+ _lastSitekey: null,
+
+ async loadTurnstile() {
+ // If Turnstile is already present, just resolve.
+ if (window.turnstile) {
+ this.turnstileLoaded = true;
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ // Avoid adding the script twice
+ if (
+ document.querySelector(
+ 'script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]'
+ )
+ ) {
+ const checkReady = () => {
+ if (window.turnstile) {
+ this.turnstileLoaded = true;
+ resolve();
+ } else {
+ setTimeout(checkReady, 100);
+ }
+ };
+ return checkReady();
+ }
+
+ const script = document.createElement('script');
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
+ script.async = true;
+ script.defer = true;
+ script.onload = () => {
+ this.turnstileLoaded = true;
+ console.log('✅ Turnstile script loaded successfully');
+ resolve();
+ };
+ script.onerror = () => {
+ console.error('❌ Failed to load Turnstile script');
+ reject(new Error('Failed to load Turnstile'));
+ };
+ document.head.appendChild(script);
+ });
+ },
+
+ // Create or reuse the turnstile container - completely hidden for token generation
+ ensureTurnstileContainer() {
+ if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) {
+ // Clean up old container if it exists
+ if (this._turnstileContainer) {
+ this._turnstileContainer.remove();
+ }
+
+ this._turnstileContainer = document.createElement('div');
+ this._turnstileContainer.className = 'wplace-turnstile-hidden';
+ this._turnstileContainer.setAttribute('aria-hidden', 'true');
+ this._turnstileContainer.id = 'turnstile-widget-container';
+ document.body.appendChild(this._turnstileContainer);
+ }
+ return this._turnstileContainer;
+ },
+
+ // Interactive overlay container for visible widgets when needed
+ ensureTurnstileOverlayContainer() {
+ if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
+ return this._turnstileOverlay;
+ }
+
+ const overlay = document.createElement('div');
+ overlay.id = 'turnstile-overlay-container';
+ overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden';
+
+ const title = document.createElement('div');
+ title.textContent = Utils.t('turnstileInstructions');
+ title.className = 'wplace-turnstile-title';
+
+ const host = document.createElement('div');
+ host.id = 'turnstile-overlay-host';
+ host.className = 'wplace-turnstile-host';
+
+ const hideBtn = document.createElement('button');
+ hideBtn.textContent = Utils.t('hideTurnstileBtn');
+ hideBtn.className = 'wplace-turnstile-hide-btn';
+ hideBtn.addEventListener('click', () => overlay.remove());
+
+ overlay.appendChild(title);
+ overlay.appendChild(host);
+ overlay.appendChild(hideBtn);
+ document.body.appendChild(overlay);
+
+ this._turnstileOverlay = overlay;
+ return overlay;
+ },
+
+ async executeTurnstile(sitekey, action = 'paint') {
+ await this.loadTurnstile();
+
+ // Try reusing existing widget first if sitekey matches
+ if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) {
+ try {
+ console.log('🔄 Reusing existing Turnstile widget...');
+ const token = await Promise.race([
+ window.turnstile.execute(this._turnstileWidgetId, { action }),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Execute timeout')), 15000)
+ ),
+ ]);
+ if (token && token.length > 20) {
+ console.log('✅ Token generated via widget reuse');
+ return token;
+ }
+ } catch (error) {
+ console.log('� Widget reuse failed, will create a fresh widget:', error.message);
+ }
+ }
+
+ // Try invisible widget first
+ const invisibleToken = await this.createTurnstileWidget(sitekey, action);
+ if (invisibleToken && invisibleToken.length > 20) {
+ return invisibleToken;
+ }
+
+ console.log('� Falling back to interactive Turnstile (visible).');
+ return await this.createTurnstileWidgetInteractive(sitekey, action);
+ },
+
+ async createTurnstileWidget(sitekey, action) {
+ return new Promise((resolve) => {
+ try {
+ // Force cleanup of any existing widget
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ console.log('🧹 Cleaned up existing Turnstile widget');
+ } catch (e) {
+ console.warn('⚠️ Widget cleanup warning:', e.message);
+ }
+ }
+
+ const container = this.ensureTurnstileContainer();
+ container.innerHTML = '';
+
+ // Verify Turnstile is available
+ if (!window.turnstile?.render) {
+ console.error('❌ Turnstile not available for rendering');
+ resolve(null);
+ return;
+ }
+
+ console.log('🔧 Creating invisible Turnstile widget...');
+ const widgetId = window.turnstile.render(container, {
+ sitekey,
+ action,
+ size: 'invisible',
+ retry: 'auto',
+ 'retry-interval': 8000,
+ callback: (token) => {
+ console.log('✅ Invisible Turnstile callback');
+ resolve(token);
+ },
+ 'error-callback': () => resolve(null),
+ 'timeout-callback': () => resolve(null),
+ });
+
+ this._turnstileWidgetId = widgetId;
+ this._lastSitekey = sitekey;
+
+ if (!widgetId) {
+ return resolve(null);
+ }
+
+ // Execute the widget and race with timeout
+ Promise.race([
+ window.turnstile.execute(widgetId, { action }),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Invisible execute timeout')), 12000)
+ ),
+ ])
+ .then(resolve)
+ .catch(() => resolve(null));
+ } catch (e) {
+ console.error('❌ Invisible Turnstile creation failed:', e);
+ resolve(null);
+ }
+ });
+ },
+
+ async createTurnstileWidgetInteractive(sitekey, action) {
+ // Create a visible widget that users can interact with if needed
+ console.log('🔄 Creating interactive Turnstile widget (visible)');
+
+ return new Promise((resolve) => {
+ try {
+ // Force cleanup of any existing widget
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ } catch (e) {
+ console.warn('⚠️ Widget cleanup warning:', e.message);
+ }
+ }
+
+ const overlay = this.ensureTurnstileOverlayContainer();
+ overlay.classList.remove('wplace-overlay-hidden');
+ overlay.style.display = 'block';
+
+ const host = overlay.querySelector('#turnstile-overlay-host');
+ host.innerHTML = '';
+
+ // Set a timeout for interactive mode
+ const timeout = setTimeout(() => {
+ console.warn('⏰ Interactive Turnstile widget timeout');
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ resolve(null);
+ }, 60000); // 60 seconds for user interaction
+
+ const widgetId = window.turnstile.render(host, {
+ sitekey,
+ action,
+ size: 'normal',
+ theme: 'light',
+ callback: (token) => {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.log('✅ Interactive Turnstile completed successfully');
+
+ if (typeof token === 'string' && token.length > 20) {
+ resolve(token);
+ } else {
+ console.warn('❌ Invalid token from interactive widget');
+ resolve(null);
+ }
+ },
+ 'error-callback': (error) => {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.warn('❌ Interactive Turnstile error:', error);
+ resolve(null);
+ },
+ });
+
+ this._turnstileWidgetId = widgetId;
+ this._lastSitekey = sitekey;
+
+ if (!widgetId) {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.warn('❌ Failed to create interactive Turnstile widget');
+ resolve(null);
+ } else {
+ console.log('✅ Interactive Turnstile widget created, waiting for user interaction...');
+ }
+ } catch (e) {
+ console.error('❌ Interactive Turnstile creation failed:', e);
+ resolve(null);
+ }
+ });
+ },
+
+ // Cleanup method for when the script is disabled/reloaded
+ cleanupTurnstile() {
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ } catch (e) {
+ console.warn('Failed to cleanup Turnstile widget:', e);
+ }
+ }
+
+ if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) {
+ this._turnstileContainer.remove();
+ }
+
+ if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
+ this._turnstileOverlay.remove();
+ }
+
+ this._turnstileWidgetId = null;
+ this._turnstileContainer = null;
+ this._turnstileOverlay = null;
+ this._lastSitekey = null;
+ },
+
+ async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') {
+ // Cache sitekey to avoid repeated DOM queries
+ if (this._cachedSitekey) {
+ console.log('🔍 Using cached sitekey:', this._cachedSitekey);
+
+ return isTokenValid()
+ ? {
+ sitekey: this._cachedSitekey,
+ token: turnstileToken,
+ }
+ : { sitekey: this._cachedSitekey, token: null };
+ }
+
+ // List of potential sitekeys to try
+ const potentialSitekeys = [
+ '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey
+ '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey
+ '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey
+ ];
+ const trySitekey = async (sitekey, source) => {
+ if (!sitekey || sitekey.length < 10) return null;
+
+ console.log(`🔍 Testing sitekey from ${source}:`, sitekey);
+ const token = await this.executeTurnstile(sitekey);
+
+ if (token && token.length >= 20) {
+ console.log(`✅ Valid token generated from ${source} sitekey`);
+ setTurnstileToken(token);
+ this._cachedSitekey = sitekey;
+ return { sitekey, token };
+ } else {
+ console.log(`❌ Failed to get token from ${source} sitekey`);
+ return null;
+ }
+ };
+
+ try {
+ // 1️⃣ data-sitekey attribute
+ const sitekeySel = document.querySelector('[data-sitekey]');
+ if (sitekeySel) {
+ const sitekey = sitekeySel.getAttribute('data-sitekey');
+ const result = await trySitekey(sitekey, 'data attribute');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 2️⃣ Turnstile element
+ const turnstileEl = document.querySelector('.cf-turnstile');
+ if (turnstileEl?.dataset?.sitekey) {
+ const sitekey = turnstileEl.dataset.sitekey;
+ const result = await trySitekey(sitekey, 'turnstile element');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 3️⃣ Meta tags
+ const metaTags = document.querySelectorAll(
+ 'meta[name*="turnstile"], meta[property*="turnstile"]'
+ );
+ for (const meta of metaTags) {
+ const content = meta.getAttribute('content');
+ const result = await trySitekey(content, 'meta tag');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 4️⃣ Global variable
+ if (window.__TURNSTILE_SITEKEY) {
+ const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 5️⃣ Script tags
+ const scripts = document.querySelectorAll('script');
+ for (const script of scripts) {
+ const content = script.textContent || script.innerHTML;
+ const match = content.match(
+ /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i
+ );
+ if (match && match[1]) {
+ const extracted = match[1].replace(/['"]/g, '');
+ const result = await trySitekey(extracted, 'script content');
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ // 6️⃣ Known potential sitekeys
+ console.log('🔍 Testing known potential sitekeys...');
+ for (const testSitekey of potentialSitekeys) {
+ const result = await trySitekey(testSitekey, 'known list');
+ if (result) {
+ return result;
+ }
+ }
+ } catch (error) {
+ console.warn('⚠️ Error during sitekey detection:', error);
+ }
+
+ // 7️⃣ Fallback
+ console.log('🔧 Trying fallback sitekey:', fallback);
+ const fallbackResult = await trySitekey(fallback, 'fallback');
+ if (fallbackResult) {
+ return fallbackResult;
+ }
+
+ console.error('❌ No working sitekey or token found.');
+ return { sitekey: null, token: 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;
+ },
+
+ // Synchronous translation function for UI rendering
+ t: (key, params = {}) => {
+ // Try to get from cache first
+ const cacheKey = `${state.language}_${key}`;
+ if (translationCache.has(cacheKey)) {
+ let text = translationCache.get(cacheKey);
+ Object.keys(params).forEach((param) => {
+ text = text.replace(`{${param}}`, params[param]);
+ });
+ return text;
+ }
+
+ // Try dynamically loaded translations (already loaded)
+ if (loadedTranslations[state.language]?.[key]) {
+ let text = loadedTranslations[state.language][key];
+ // Cache for future use
+ translationCache.set(cacheKey, text);
+ Object.keys(params).forEach((param) => {
+ text = text.replace(`{${param}}`, params[param]);
+ });
+ return text;
+ }
+
+ // Fallback to English if current language failed
+ if (state.language !== 'en' && loadedTranslations['en']?.[key]) {
+ let text = loadedTranslations['en'][key];
+ Object.keys(params).forEach((param) => {
+ text = text.replace(`{${param}}`, params[param]);
+ });
+ return text;
+ }
+
+ // Final fallback to emergency fallback or key
+ let text = FALLBACK_TEXT[state.language]?.[key] || FALLBACK_TEXT.en?.[key] || key;
+ Object.keys(params).forEach((param) => {
+ text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
+ });
+
+ // Log missing translations for debugging
+ if (text === key && key !== 'undefined') {
+ console.warn(`⚠️ Missing translation for key: ${key} (language: ${state.language})`);
+ }
+
+ return text;
+ },
+
+ showAlert: (message, type = 'info') => {
+ const alertDiv = document.createElement('div');
+ alertDiv.className = `wplace-alert-base wplace-alert-${type}`;
+
+ alertDiv.textContent = message;
+ document.body.appendChild(alertDiv);
+
+ setTimeout(() => {
+ alertDiv.style.animation = 'slide-down 0.3s ease-out reverse';
+ setTimeout(() => {
+ document.body.removeChild(alertDiv);
+ }, 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)),
+ _labCache: new Map(), // key: (r<<16)|(g<<8)|b value: [L,a,b]
+ _rgbToLab: (r, g, b) => {
+ // sRGB -> linear
+ const srgbToLinear = (v) => {
+ v /= 255;
+ return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
+ };
+ const rl = srgbToLinear(r);
+ const gl = srgbToLinear(g);
+ const bl = srgbToLinear(b);
+ let X = rl * 0.4124 + gl * 0.3576 + bl * 0.1805;
+ let Y = rl * 0.2126 + gl * 0.7152 + bl * 0.0722;
+ let Z = rl * 0.0193 + gl * 0.1192 + bl * 0.9505;
+ X /= 0.95047;
+ Y /= 1.0;
+ Z /= 1.08883;
+ const f = (t) => (t > 0.008856 ? Math.cbrt(t) : 7.787 * t + 16 / 116);
+ const fX = f(X),
+ fY = f(Y),
+ fZ = f(Z);
+ const L = 116 * fY - 16;
+ const a = 500 * (fX - fY);
+ const b2 = 200 * (fY - fZ);
+ return [L, a, b2];
+ },
+ _lab: (r, g, b) => {
+ const key = (r << 16) | (g << 8) | b;
+ let v = Utils._labCache.get(key);
+ if (!v) {
+ v = Utils._rgbToLab(r, g, b);
+ Utils._labCache.set(key, v);
+ }
+ return v;
+ },
+ findClosestPaletteColor: (r, g, b, palette) => {
+ // Use provided palette or derive from COLOR_MAP
+ if (!palette || palette.length === 0) {
+ palette = Object.values(CONFIG.COLOR_MAP)
+ .filter((c) => c.rgb)
+ .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]);
+ }
+ if (state.colorMatchingAlgorithm === 'legacy') {
+ let menorDist = Infinity;
+ let cor = [0, 0, 0];
+ 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;
+ }
+ // LAB algorithm
+ const [Lt, at, bt] = Utils._lab(r, g, b);
+ const targetChroma = Math.sqrt(at * at + bt * bt);
+ let best = null;
+ let bestDist = Infinity;
+ for (let i = 0; i < palette.length; i++) {
+ const [pr, pg, pb] = palette[i];
+ const [Lp, ap, bp] = Utils._lab(pr, pg, pb);
+ const dL = Lt - Lp;
+ const da = at - ap;
+ const db = bt - bp;
+ let dist = dL * dL + da * da + db * db;
+ if (state.enableChromaPenalty && targetChroma > 20) {
+ const candChroma = Math.sqrt(ap * ap + bp * bp);
+ if (candChroma < targetChroma) {
+ const chromaDiff = targetChroma - candChroma;
+ dist += chromaDiff * chromaDiff * state.chromaPenaltyWeight;
+ }
+ }
+ if (dist < bestDist) {
+ bestDist = dist;
+ best = palette[i];
+ if (bestDist === 0) break;
+ }
+ }
+ return best || [0, 0, 0];
+ },
+
+ isWhitePixel: (r, g, b) => {
+ const wt = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD;
+ return r >= wt && g >= wt && b >= wt;
+ },
+
+ resolveColor(targetRgb, availableColors, exactMatch = false) {
+ if (!availableColors || availableColors.length === 0) {
+ return {
+ id: null,
+ rgb: targetRgb,
+ };
+ }
+
+ const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${state.enableChromaPenalty ? 'c' : 'nc'
+ }|${state.chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`;
+
+ if (colorCache.has(cacheKey)) return colorCache.get(cacheKey);
+
+ // Check for an exact color match in availableColors.
+ // If found, return the matched color with its ID.
+ // If not found, return the target color with null ID.
+ // Cache the result for future lookups.
+ if (exactMatch) {
+ const match = availableColors.find(
+ (c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2]
+ );
+ const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb };
+ colorCache.set(cacheKey, result);
+ return result;
+ }
+
+ // check for white using threshold
+ const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD;
+ if (
+ targetRgb[0] >= whiteThreshold &&
+ targetRgb[1] >= whiteThreshold &&
+ targetRgb[2] >= whiteThreshold
+ ) {
+ const whiteEntry = availableColors.find(
+ (c) =>
+ c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold
+ );
+ if (whiteEntry) {
+ const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] };
+ colorCache.set(cacheKey, result);
+ return result;
+ }
+ }
+
+ // find nearest color
+ let bestId = availableColors[0].id;
+ let bestRgb = [...availableColors[0].rgb];
+ let bestScore = Infinity;
+
+ if (state.colorMatchingAlgorithm === 'legacy') {
+ for (let i = 0; i < availableColors.length; i++) {
+ const c = availableColors[i];
+ const [r, g, b] = c.rgb;
+ const rmean = (r + targetRgb[0]) / 2;
+ const rdiff = r - targetRgb[0];
+ const gdiff = g - targetRgb[1];
+ const bdiff = b - targetRgb[2];
+ const dist = Math.sqrt(
+ (((512 + rmean) * rdiff * rdiff) >> 8) +
+ 4 * gdiff * gdiff +
+ (((767 - rmean) * bdiff * bdiff) >> 8)
+ );
+ if (dist < bestScore) {
+ bestScore = dist;
+ bestId = c.id;
+ bestRgb = [...c.rgb];
+ if (dist === 0) break;
+ }
+ }
+ } else {
+ const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]);
+ const targetChroma = Math.sqrt(at * at + bt * bt);
+ const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0;
+
+ for (let i = 0; i < availableColors.length; i++) {
+ const c = availableColors[i];
+ const [r, g, b] = c.rgb;
+ const [L2, a2, b2] = Utils._lab(r, g, b);
+ const dL = Lt - L2,
+ da = at - a2,
+ db = bt - b2;
+ let dist = dL * dL + da * da + db * db;
+
+ if (penaltyWeight > 0 && targetChroma > 20) {
+ const candChroma = Math.sqrt(a2 * a2 + b2 * b2);
+ if (candChroma < targetChroma) {
+ const cd = targetChroma - candChroma;
+ dist += cd * cd * penaltyWeight;
+ }
+ }
+
+ if (dist < bestScore) {
+ bestScore = dist;
+ bestId = c.id;
+ bestRgb = [...c.rgb];
+ if (dist === 0) break;
+ }
+ }
+ }
+
+ const result = { id: bestId, rgb: bestRgb };
+ colorCache.set(cacheKey, result);
+
+ // limit the size of the cache
+ if (colorCache.size > 15000) {
+ const firstKey = colorCache.keys().next().value;
+ colorCache.delete(firstKey);
+ }
+
+ return result;
+ },
+
+ 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('.tooltip button[id^="color-"]');
+ if (colorElements.length === 0) {
+ console.log('❌ No color elements found on page');
+ return null;
+ }
+ // Separate available and unavailable colors
+ const availableColors = [];
+ const unavailableColors = [];
+
+ Array.from(colorElements).forEach((el) => {
+ const id = Number.parseInt(el.id.replace('color-', ''));
+ if (id === 0) return; // Skip transparent color
+
+ const rgbStr = el.style.backgroundColor.match(/\d+/g);
+ if (!rgbStr || rgbStr.length < 3) {
+ console.warn(`Skipping color element ${el.id} — cannot parse RGB`);
+ return;
+ }
+ const rgb = rgbStr.map(Number);
+
+ // Find color name from COLOR_MAP
+ const colorInfo = Object.values(CONFIG.COLOR_MAP).find((color) => color.id === id);
+ const name = colorInfo ? colorInfo.name : `Unknown Color ${id}`;
+
+ const colorData = { id, name, rgb };
+
+ // Check if color is available (no SVG overlay means available)
+ if (!el.querySelector('svg')) {
+ availableColors.push(colorData);
+ } else {
+ unavailableColors.push(colorData);
+ }
+ });
+
+ // Console log detailed color information
+ console.log('=== CAPTURED COLORS STATUS ===');
+ console.log(`Total available colors: ${availableColors.length}`);
+ console.log(`Total unavailable colors: ${unavailableColors.length}`);
+ console.log(`Total colors scanned: ${availableColors.length + unavailableColors.length}`);
+
+ if (availableColors.length > 0) {
+ console.log('\n--- AVAILABLE COLORS ---');
+ availableColors.forEach((color, index) => {
+ console.log(
+ `${index + 1
+ }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})`
+ );
+ });
+ }
+
+ if (unavailableColors.length > 0) {
+ console.log('\n--- UNAVAILABLE COLORS ---');
+ unavailableColors.forEach((color, index) => {
+ console.log(
+ `${index + 1
+ }. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]`
+ );
+ });
+ }
+
+ console.log('=== END COLOR STATUS ===');
+
+ return availableColors;
+ },
+
+ 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 timeFromSpeed + timeFromCharges; // combine instead of taking max
+ },
+
+ // --- Painted pixel tracking helpers ---
+ initializePaintedMap: (width, height) => {
+ if (!state.paintedMap || state.paintedMap.length !== height) {
+ state.paintedMap = Array(height)
+ .fill()
+ .map(() => Array(width).fill(false));
+ console.log(`📋 Initialized painted map: ${width}x${height}`);
+ }
+ },
+
+ markPixelPainted: (x, y, regionX = 0, regionY = 0) => {
+ const actualX = x + regionX;
+ const actualY = y + regionY;
+
+ if (
+ state.paintedMap &&
+ state.paintedMap[actualY] &&
+ actualX >= 0 &&
+ actualX < state.paintedMap[actualY].length
+ ) {
+ state.paintedMap[actualY][actualX] = true;
+ }
+ },
+
+ isPixelPainted: (x, y, regionX = 0, regionY = 0) => {
+ const actualX = x + regionX;
+ const actualY = y + regionY;
+
+ if (
+ state.paintedMap &&
+ state.paintedMap[actualY] &&
+ actualX >= 0 &&
+ actualX < state.paintedMap[actualY].length
+ ) {
+ return state.paintedMap[actualY][actualX];
+ }
+ return false;
+ },
+
+ // Smart save - only save if significant changes
+ shouldAutoSave: () => {
+ const now = Date.now();
+ const pixelsSinceLastSave = state.paintedPixels - state._lastSavePixelCount;
+ const timeSinceLastSave = now - state._lastSaveTime;
+
+ // Save conditions:
+ // 1. Every 25 pixels (reduced from 50 for more frequent saves)
+ // 2. At least 30 seconds since last save (prevent spam)
+ // 3. Not already saving
+ return !state._saveInProgress && pixelsSinceLastSave >= 25 && timeSinceLastSave >= 30000;
+ },
+
+ performSmartSave: () => {
+ if (!Utils.shouldAutoSave()) return false;
+
+ state._saveInProgress = true;
+ const success = Utils.saveProgress();
+
+ if (success) {
+ state._lastSavePixelCount = state.paintedPixels;
+ state._lastSaveTime = Date.now();
+ console.log(`💾 Auto-saved at ${state.paintedPixels} pixels`);
+ }
+
+ state._saveInProgress = false;
+ return success;
+ },
+
+ // --- Data management helpers ---
+
+ // Base64 compression helpers for efficient storage
+ packPaintedMapToBase64: (paintedMap, width, height) => {
+ if (!paintedMap || !width || !height) return null;
+ const totalBits = width * height;
+ const byteLen = Math.ceil(totalBits / 8);
+ const bytes = new Uint8Array(byteLen);
+ let bitIndex = 0;
+ for (let y = 0; y < height; y++) {
+ const row = paintedMap[y];
+ for (let x = 0; x < width; x++) {
+ const bit = row && row[x] ? 1 : 0;
+ const b = bitIndex >> 3; // byte index
+ const o = bitIndex & 7; // bit offset
+ if (bit) bytes[b] |= 1 << o;
+ bitIndex++;
+ }
+ }
+ let binary = '';
+ const chunk = 0x8000;
+ for (let i = 0; i < bytes.length; i += chunk) {
+ binary += String.fromCharCode.apply(
+ null,
+ bytes.subarray(i, Math.min(i + chunk, bytes.length))
+ );
+ }
+ return btoa(binary);
+ },
+
+ unpackPaintedMapFromBase64: (base64, width, height) => {
+ if (!base64 || !width || !height) return null;
+ const binary = atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
+ const map = Array(height)
+ .fill()
+ .map(() => Array(width).fill(false));
+ let bitIndex = 0;
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ const b = bitIndex >> 3;
+ const o = bitIndex & 7;
+ map[y][x] = ((bytes[b] >> o) & 1) === 1;
+ bitIndex++;
+ }
+ }
+ return map;
+ },
+
+ // Migration helpers for backward compatibility
+ migrateProgressToV2: (saved) => {
+ if (!saved) return saved;
+ const isV1 =
+ !saved.version ||
+ saved.version === '1' ||
+ saved.version === '1.0' ||
+ saved.version === '1.1';
+ if (!isV1) return saved;
+
+ try {
+ const migrated = { ...saved };
+ const width = migrated.imageData?.width;
+ const height = migrated.imageData?.height;
+ if (migrated.paintedMap && width && height) {
+ const data = Utils.packPaintedMapToBase64(migrated.paintedMap, width, height);
+ migrated.paintedMapPacked = { width, height, data };
+ }
+ delete migrated.paintedMap;
+ migrated.version = '2';
+ return migrated;
+ } catch (e) {
+ console.warn('Migration to v2 failed, using original data:', e);
+ return saved;
+ }
+ },
+
+ migrateProgressToV21: (saved) => {
+ if (!saved) return saved;
+ if (saved.version === '2.1') return saved;
+ const isV2 = saved.version === '2' || saved.version === '2.0';
+ const isV1 =
+ !saved.version ||
+ saved.version === '1' ||
+ saved.version === '1.0' ||
+ saved.version === '1.1';
+ if (!isV2 && !isV1) return saved; // save this for future
+ try {
+ const migrated = { ...saved };
+ // First migrate to v2 if needed
+ if (isV1) {
+ const width = migrated.imageData?.width;
+ const height = migrated.imageData?.height;
+ if (migrated.paintedMap && width && height) {
+ const data = Utils.packPaintedMapToBase64(migrated.paintedMap, width, height);
+ migrated.paintedMapPacked = { width, height, data };
+ }
+ delete migrated.paintedMap;
+ }
+ migrated.version = '2.1';
+ return migrated;
+ } catch (e) {
+ console.warn('Migration to v2.1 failed, using original data:', e);
+ return saved;
+ }
+ },
+
+ migrateProgressToV22: (data) => {
+ try {
+ const migrated = { ...data };
+ migrated.version = '2.2';
+
+ // Add new fields with default values
+ if (!migrated.state.coordinateMode) {
+ migrated.state.coordinateMode = CONFIG.COORDINATE_MODE;
+ }
+ if (!migrated.state.coordinateDirection) {
+ migrated.state.coordinateDirection = CONFIG.COORDINATE_DIRECTION;
+ }
+ if (!migrated.state.coordinateSnake) {
+ migrated.state.coordinateSnake = CONFIG.COORDINATE_SNAKE;
+ }
+ if (!migrated.state.blockWidth) {
+ migrated.state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH;
+ }
+ if (!migrated.state.blockHeight) {
+ migrated.state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT;
+ }
+
+ return migrated;
+ } catch (e) {
+ console.warn('Migration to v2.2 failed, using original data:', e);
+ return data;
+ }
+ },
+
+ buildPaintedMapPacked() {
+ if (state.paintedMap && state.imageData) {
+ const data = Utils.packPaintedMapToBase64(
+ state.paintedMap,
+ state.imageData.width,
+ state.imageData.height
+ );
+ if (data) {
+ return {
+ width: state.imageData.width,
+ height: state.imageData.height,
+ data: data,
+ };
+ }
+ }
+ return null;
+ },
+
+ buildProgressData() {
+ return {
+ timestamp: Date.now(),
+ version: '2.2',
+ state: {
+ totalPixels: state.totalPixels,
+ paintedPixels: state.paintedPixels,
+ lastPosition: state.lastPosition,
+ startPosition: state.startPosition,
+ region: state.region,
+ imageLoaded: state.imageLoaded,
+ colorsChecked: state.colorsChecked,
+ coordinateMode: state.coordinateMode,
+ coordinateDirection: state.coordinateDirection,
+ coordinateSnake: state.coordinateSnake,
+ blockWidth: state.blockWidth,
+ blockHeight: state.blockHeight,
+ 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,
+ paintedMapPacked: Utils.buildPaintedMapPacked(),
+ };
+ },
+
+ migrateProgress(saved) {
+ if (!saved) return null;
+
+ let data = saved;
+ const ver = data.version;
+
+ // If version is missing or ≤ 1.x → first migrate to v2
+ if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') {
+ data = Utils.migrateProgressToV2(data);
+ }
+
+ // If still older than v2.1 → migrate to 2.1
+ if (data.version === '2' || data.version === '2.0') {
+ data = Utils.migrateProgressToV21(data);
+ }
+
+ // If still older than v2.2 → migrate to 2.2
+ if (data.version === '2.1') {
+ data = Utils.migrateProgressToV22(data);
+ }
+
+ // Now data is guaranteed to be the latest version
+ return data;
+ },
+
+ saveProgress: () => {
+ try {
+ const progressData = Utils.buildProgressData(state);
+
+ 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');
+ if (!saved) return null;
+ let data = JSON.parse(saved);
+ const migrated = Utils.migrateProgress(data);
+
+ if (migrated && migrated !== data) {
+ try {
+ localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated));
+ } catch { }
+ }
+ return migrated;
+ } catch (error) {
+ console.error('Error loading progress:', error);
+ return null;
+ }
+ },
+
+ clearProgress: () => {
+ try {
+ localStorage.removeItem('wplace-bot-progress');
+ // Also clear painted map from memory
+ state.paintedMap = null;
+ state._lastSavePixelCount = 0;
+ state._lastSaveTime = 0;
+ // Reset coordinate generation settings to their default values
+ state.coordinateMode = CONFIG.COORDINATE_MODE;
+ state.coordinateDirection = CONFIG.COORDINATE_DIRECTION;
+ state.coordinateSnake = CONFIG.COORDINATE_SNAKE;
+ state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH;
+ state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT;
+ console.log('📋 Progress and painted map cleared');
+ return true;
+ } catch (error) {
+ console.error('Error clearing progress:', error);
+ return false;
+ }
+ },
+
+ restoreProgress: (savedData) => {
+ try {
+ Object.assign(state, savedData.state);
+
+ // Restore coordinate generation settings
+ if (savedData.state.coordinateMode) {
+ state.coordinateMode = savedData.state.coordinateMode;
+ }
+ if (savedData.state.coordinateDirection) {
+ state.coordinateDirection = savedData.state.coordinateDirection;
+ }
+ if (savedData.state.coordinateSnake !== undefined) {
+ state.coordinateSnake = savedData.state.coordinateSnake;
+ }
+ if (savedData.state.blockWidth) {
+ state.blockWidth = savedData.state.blockWidth;
+ }
+ if (savedData.state.blockHeight) {
+ state.blockHeight = savedData.state.blockHeight;
+ }
+
+ if (savedData.imageData) {
+ state.imageData = {
+ ...savedData.imageData,
+ pixels: new Uint8ClampedArray(savedData.imageData.pixels),
+ };
+
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = state.imageData.width;
+ canvas.height = state.imageData.height;
+ const ctx = canvas.getContext('2d');
+ const imageData = new ImageData(
+ state.imageData.pixels,
+ state.imageData.width,
+ state.imageData.height
+ );
+ ctx.putImageData(imageData, 0, 0);
+ const proc = new ImageProcessor('');
+ proc.img = canvas;
+ proc.canvas = canvas;
+ proc.ctx = ctx;
+ state.imageData.processor = proc;
+ } catch (e) {
+ console.warn('Could not rebuild processor from saved image data:', e);
+ }
+ }
+
+ // Prefer packed form if available; fallback to legacy paintedMap array for backward compatibility
+ if (savedData.paintedMapPacked && savedData.paintedMapPacked.data) {
+ const { width, height, data } = savedData.paintedMapPacked;
+ state.paintedMap = Utils.unpackPaintedMapFromBase64(data, width, height);
+ } else 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 = Utils.buildProgressData();
+ 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 || !data.state) {
+ throw new Error('Invalid file format');
+ }
+ const migrated = Utils.migrateProgress(data);
+
+ const success = Utils.restoreProgress(migrated);
+ return success;
+ } catch (error) {
+ console.error('Error loading from file:', error);
+ throw error;
+ }
+ },
+
+ // Helper function to restore overlay from loaded data
+ restoreOverlayFromData: async () => {
+ if (!state.imageLoaded || !state.imageData || !state.startPosition || !state.region) {
+ return false;
+ }
+
+ try {
+ // Recreate ImageBitmap from loaded pixel data
+ const imageData = new ImageData(
+ state.imageData.pixels,
+ state.imageData.width,
+ state.imageData.height
+ );
+
+ const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height);
+ const ctx = canvas.getContext('2d');
+ ctx.putImageData(imageData, 0, 0);
+ const imageBitmap = await canvas.transferToImageBitmap();
+
+ // Set up overlay with restored data
+ await overlayManager.setImage(imageBitmap);
+ await overlayManager.setPosition(state.startPosition, state.region);
+ overlayManager.enable();
+
+ // Update overlay button state
+ const toggleOverlayBtn = document.getElementById('toggleOverlayBtn');
+ if (toggleOverlayBtn) {
+ toggleOverlayBtn.disabled = false;
+ toggleOverlayBtn.classList.add('active');
+ }
+
+ console.log('Overlay restored from data');
+ return true;
+ } catch (error) {
+ console.error('Failed to restore overlay from data:', error);
+ return false;
+ }
+ },
+
+ updateCoordinateUI({ mode, directionControls, snakeControls, blockControls }) {
+ const isLinear = mode === 'rows' || mode === 'columns';
+ const isBlock = mode === 'blocks' || mode === 'shuffle-blocks';
+
+ if (directionControls) directionControls.style.display = isLinear ? 'block' : 'none';
+ if (snakeControls) snakeControls.style.display = isLinear ? 'block' : 'none';
+ if (blockControls) blockControls.style.display = isBlock ? 'block' : 'none';
+ },
+ };
+
+ // 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,
+ fp: fpStr32,
+ };
+ var token = await createWasmToken(regionX, regionY, payload);
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": token },
+ 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 {
+ id: data.id,
+ charges: data.charges?.count || 0,
+ max: data.charges?.max || 1,
+ cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT,
+ droplets: data.droplets || 0,
+ }
+ } catch (e) {
+ console.error("Failed to get charges:", e)
+ return {
+ id: null,
+ charges: 0,
+ max: 1,
+ cooldown: CONFIG.COOLDOWN_DEFAULT,
+ droplets: 0,
+ }
+ }
+ },
+
+ async fetchCheck() {
+ try {
+ const res = await fetch("https://backend.wplace.live/me", {
+ credentials: "include",
+ })
+ const data = await res.json()
+ return {
+ ID: data.id,
+ Charges: data.charges.count,
+ Max: data.charges.max,
+ Droplets: data.droplets
+ }
+ } catch (e) {
+ console.error("Failed to get ID:", e)
+ return {
+
+ }
+ }
+ }
+ };
+
+ // Desktop Notification Manager
+ const NotificationManager = {
+ pollTimer: null,
+ pollIntervalMs: 60_000,
+ icon() {
+ const link = document.querySelector("link[rel~='icon']");
+ return link?.href || location.origin + '/favicon.ico';
+ },
+ async requestPermission() {
+ if (!('Notification' in window)) {
+ Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning');
+ return 'denied';
+ }
+ if (Notification.permission === 'granted') return 'granted';
+ try {
+ const perm = await Notification.requestPermission();
+ return perm;
+ } catch {
+ return Notification.permission;
+ }
+ },
+ canNotify() {
+ return (
+ state.notificationsEnabled &&
+ typeof Notification !== 'undefined' &&
+ Notification.permission === 'granted'
+ );
+ },
+ notify(title, body, tag = 'wplace-charges', force = false) {
+ if (!this.canNotify()) return false;
+ if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false;
+ try {
+ new Notification(title, {
+ body,
+ tag,
+ renotify: true,
+ icon: this.icon(),
+ badge: this.icon(),
+ silent: false,
+ });
+ return true;
+ } catch {
+ // Graceful fallback
+ Utils.showAlert(body, 'info');
+ return false;
+ }
+ },
+ resetEdgeTracking() {
+ state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold;
+ state._lastChargesNotifyAt = 0;
+ },
+ maybeNotifyChargesReached(force = false) {
+ if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
+ const reached = state.displayCharges >= state.cooldownChargeThreshold;
+ const now = Date.now();
+ const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000;
+ if (reached) {
+ const shouldEdge = state._lastChargesBelow || force;
+ const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs;
+ if (shouldEdge || shouldRepeat) {
+ const msg = Utils.t('chargesReadyMessage', {
+ current: state.displayCharges,
+ max: state.maxCharges,
+ threshold: state.cooldownChargeThreshold,
+ });
+ this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges');
+ state._lastChargesNotifyAt = now;
+ }
+ state._lastChargesBelow = false;
+ } else {
+ state._lastChargesBelow = true;
+ }
+ },
+ startPolling() {
+ this.stopPolling();
+ if (!state.notificationsEnabled || !state.notifyOnChargesReached) return;
+ // lightweight background polling
+ this.pollTimer = setInterval(async () => {
+ try {
+ const { charges, cooldown, max } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ state.maxCharges = Math.max(1, Math.floor(max));
+ this.maybeNotifyChargesReached();
+ } catch {
+ /* ignore */
+ }
+ }, this.pollIntervalMs);
+ },
+ stopPolling() {
+ if (this.pollTimer) {
+ clearInterval(this.pollTimer);
+ this.pollTimer = null;
+ }
+ },
+ syncFromState() {
+ this.resetEdgeTracking();
+ if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling();
+ else this.stopPolling();
+ },
+ };
+
+ // COLOR MATCHING FUNCTION - Optimized with caching
+ const colorCache = new Map();
+
+ // UI UPDATE FUNCTIONS (declared early to avoid reference errors)
+ let updateUI = () => { };
+ let updateStats = (isManualRefresh) => { };
+ let updateDataButtons = () => { };
+
+ function updateActiveColorPalette() {
+ state.activeColorPalette = [];
+ const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active');
+ if (activeSwatches) {
+ activeSwatches.forEach((swatch) => {
+ const rgbStr = swatch.getAttribute('data-rgb');
+ if (rgbStr) {
+ const rgb = rgbStr.split(',').map(Number);
+ state.activeColorPalette.push(rgb);
+ }
+ });
+ }
+ if (document.querySelector('.resize-container')?.style.display === 'block') {
+ _updateResizePreview();
+ }
+ }
+
+ function toggleAllColors(select, showingUnavailable = false) {
+ const swatches = document.querySelectorAll('.wplace-color-swatch');
+ if (swatches) {
+ swatches.forEach((swatch) => {
+ // Only toggle colors that are available or if we're showing unavailable colors
+ const isUnavailable = swatch.classList.contains('unavailable');
+ if (!isUnavailable || showingUnavailable) {
+ // Don't try to select unavailable colors
+ if (!isUnavailable) {
+ swatch.classList.toggle('active', select);
+ }
+ }
+ });
+ }
+ updateActiveColorPalette();
+ }
+
+ function unselectAllPaidColors() {
+ const swatches = document.querySelectorAll('.wplace-color-swatch');
+ if (swatches) {
+ swatches.forEach((swatch) => {
+ const colorId = parseInt(swatch.getAttribute('data-color-id'), 10);
+ if (!isNaN(colorId) && colorId >= 32) {
+ swatch.classList.toggle('active', false);
+ }
+ });
+ }
+ updateActiveColorPalette();
+ }
+
+ function initializeColorPalette(container) {
+ const colorsContainer = container.querySelector('#colors-container');
+ const showAllToggle = container.querySelector('#showAllColorsToggle');
+ if (!colorsContainer) return;
+
+ // Use already captured colors from state (captured during upload)
+ // Don't re-fetch colors here, use what was captured when user clicked upload
+ if (!state.availableColors || state.availableColors.length === 0) {
+ // If no colors have been captured yet, show message
+ colorsContainer.innerHTML = `${Utils.t(
+ 'uploadImageFirst'
+ )}
`;
+ return;
+ }
+
+ function populateColors(showUnavailable = false) {
+ colorsContainer.innerHTML = '';
+ let availableCount = 0;
+ let totalCount = 0;
+
+ // Convert COLOR_MAP to array and filter out transparent
+ const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null);
+
+ allColors.forEach((colorData) => {
+ const { id, name, rgb } = colorData;
+ const rgbKey = `${rgb.r},${rgb.g},${rgb.b}`;
+ totalCount++;
+
+ // Check if this color is available in the captured colors
+ const isAvailable = state.availableColors.some(
+ (c) => c.rgb[0] === rgb.r && c.rgb[1] === rgb.g && c.rgb[2] === rgb.b
+ );
+
+ // If not showing all colors and this color is not available, skip it
+ if (!showUnavailable && !isAvailable) {
+ return;
+ }
+
+ if (isAvailable) availableCount++;
+
+ const colorItem = Utils.createElement('div', {
+ className: 'wplace-color-item',
+ });
+ const swatch = Utils.createElement('button', {
+ className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`,
+ title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`,
+ 'data-rgb': rgbKey,
+ 'data-color-id': id,
+ });
+ swatch.style.backgroundColor = `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`;
+
+ // Make unavailable colors visually distinct
+ if (!isAvailable) {
+ swatch.style.opacity = '0.4';
+ swatch.style.filter = 'grayscale(50%)';
+ swatch.disabled = true;
+ } else {
+ // Select available colors by default
+ swatch.classList.add('active');
+ }
+
+ const nameLabel = Utils.createElement(
+ 'span',
+ {
+ className: 'wplace-color-item-name',
+ style: !isAvailable ? 'color: #888; font-style: italic;' : '',
+ },
+ name + (!isAvailable ? ' (N/A)' : '')
+ );
+
+ // Only add click listener for available colors
+ if (isAvailable) {
+ swatch.addEventListener('click', () => {
+ swatch.classList.toggle('active');
+ updateActiveColorPalette();
+ });
+ }
+
+ colorItem.appendChild(swatch);
+ colorItem.appendChild(nameLabel);
+ colorsContainer.appendChild(colorItem);
+ });
+
+ updateActiveColorPalette();
+ }
+
+ // Initialize with only available colors
+ populateColors(false);
+
+ // Add toggle functionality
+ if (showAllToggle) {
+ showAllToggle.addEventListener('change', (e) => {
+ populateColors(e.target.checked);
+ });
+ }
+
+ container
+ .querySelector('#selectAllBtn')
+ ?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked));
+ container
+ .querySelector('#unselectAllBtn')
+ ?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked));
+ container
+ .querySelector('#unselectPaidBtn')
+ ?.addEventListener('click', () => unselectAllPaidColors());
+ }
+
+ async function handleCaptcha() {
+ const startTime = performance.now();
+
+ // Check user's token source preference
+ if (state.tokenSource === 'manual') {
+ console.log('🎯 Manual token source selected - using pixel placement automation');
+ return await handleCaptchaFallback();
+ }
+
+ // Generator mode (pure) or Hybrid mode - try generator first
+ try {
+ // Use optimized token generation with automatic sitekey detection
+ const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken();
+
+ if (!sitekey) {
+ throw new Error('No valid sitekey found');
+ }
+
+ console.log('🔑 Generating Turnstile token for sitekey:', sitekey);
+ console.log(
+ '🧭 UA:',
+ navigator.userAgent.substring(0, 50) + '...',
+ 'Platform:',
+ navigator.platform
+ );
+
+ // Add additional checks before token generation
+ if (!window.turnstile) {
+ await Utils.loadTurnstile();
+ }
+
+ let token = null;
+
+ // ✅ Reuse pre-generated token if available and valid
+ if (
+ preGeneratedToken &&
+ typeof preGeneratedToken === 'string' &&
+ preGeneratedToken.length > 20
+ ) {
+ console.log('♻️ Reusing pre-generated token from sitekey detection phase');
+ token = preGeneratedToken;
+ }
+ // ✅ Or use globally cached token if still valid
+ else if (isTokenValid()) {
+ console.log('♻️ Using existing cached token (from previous operation)');
+ token = turnstileToken;
+ }
+ // ✅ Otherwise generate a new one
+ else {
+ console.log('🔐 No valid pre-generated or cached token, creating new one...');
+ token = await Utils.executeTurnstile(sitekey, 'paint');
+ if (token) {
+ setTurnstileToken(token);
+ }
+ }
+
+ // 📊 Debug log
+ console.log(
+ `🔍 Token received - Type: ${typeof token}, Value: ${token
+ ? typeof token === 'string'
+ ? token.length > 50
+ ? token.substring(0, 50) + '...'
+ : token
+ : JSON.stringify(token)
+ : 'null/undefined'
+ }, Length: ${token?.length || 0}`
+ );
+
+ // ✅ Final validation
+ if (typeof token === 'string' && token.length > 20) {
+ const duration = Math.round(performance.now() - startTime);
+ console.log(`✅ Turnstile token generated successfully in ${duration}ms`);
+ return token;
+ } else {
+ throw new Error(
+ `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify(
+ token
+ )}, Length: ${token?.length || 0}`
+ );
+ }
+ } catch (error) {
+ const duration = Math.round(performance.now() - startTime);
+ console.error(`❌ Turnstile token generation failed after ${duration}ms:`, error);
+
+ // Fallback to manual pixel placement for hybrid mode
+ if (state.tokenSource === 'hybrid') {
+ console.log(
+ '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...'
+ );
+ const fbToken = await handleCaptchaFallback();
+ return fbToken;
+ } else {
+ // Pure generator mode - don't fallback, just fail
+ throw error;
+ }
+ }
+ }
+
+ // Keep original method as fallback
+ async function handleCaptchaFallback() {
+ return new Promise(async (resolve, reject) => {
+ try {
+ // Ensure we have a fresh promise to await for a new token capture
+ if (!_resolveToken) {
+ tokenPromise = new Promise((res) => {
+ _resolveToken = res;
+ });
+ }
+ 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);
+
+ // 800ms delay before sending confirmation
+ await Utils.sleep(800);
+
+ // Keep confirming until token is captured
+ const confirmLoop = async () => {
+ while (!turnstileToken) {
+ 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) {
+ confirmBtn.click();
+ }
+ await Utils.sleep(500); // 500ms delay between confirmation attempts
+ }
+ };
+
+ // Start confirmation loop and wait for token
+ confirmLoop();
+ const token = await tokenPromise;
+ await Utils.sleep(300); // small delay after token is captured
+ resolve(token);
+ })();
+
+ 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();
+ await initializeTranslations();
+
+ const theme = getCurrentTheme();
+ applyTheme(); // <- new: set CSS vars and theme class before building UI
+
+ function appendLinkOnce(href, attributes = {}) {
+ // Check if a link with the same href already exists in the document head
+ const exists = Array.from(document.head.querySelectorAll('link')).some(
+ (link) => link.href === href
+ );
+ if (exists) return;
+
+ // Create a new link element
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = href;
+
+ // Add any additional attributes (e.g., data-* attributes)
+ for (const [key, value] of Object.entries(attributes)) {
+ link.setAttribute(key, value);
+ }
+
+ // Append the link element to the document head
+ document.head.appendChild(link);
+ }
+
+ appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css');
+
+ if (theme.fontFamily.includes('Press Start 2P')) {
+ appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
+ }
+
+ // Load auto-image-styles.css - prioritize extension local resources
+ console.group('%c🎨 Loading Auto-Image Styles', 'color: #8b5cf6; font-weight: bold;');
+
+ let stylesLoaded = false;
+
+ // First try: Check if extension has loaded local CSS resources
+ if (window.AUTOBOT_THEMES && window.AUTOBOT_THEMES['auto-image-styles.css']) {
+ console.log('%c🔍 Found auto-image-styles.css in extension local resources!', 'color: #10b981; font-weight: bold;');
+
+ // Check if it's already injected by the extension
+ const existingStyle = document.getElementById('autobot-auto-image-styles');
+ if (existingStyle) {
+ console.log('%c✅ Styles already injected by extension - using local version', 'color: #10b981; font-weight: bold;');
+ console.log(' 📍 Source: Extension local file (already in DOM)');
+ console.log(' 🆔 Element ID: autobot-auto-image-styles');
+ stylesLoaded = true;
+ } else {
+ // Inject it ourselves
+ const styleElement = document.createElement('style');
+ styleElement.id = 'autobot-auto-image-styles-script';
+ styleElement.setAttribute('data-wplace-theme', 'true');
+ styleElement.textContent = window.AUTOBOT_THEMES['auto-image-styles.css'];
+ document.head.appendChild(styleElement);
+
+ console.log('%c✅ Injected auto-image-styles.css from extension local resources', 'color: #10b981; font-weight: bold;');
+ console.log(' 📍 Source: Extension local file');
+ console.log(' 📏 Size: ' + window.AUTOBOT_THEMES['auto-image-styles.css'].length + ' characters');
+ console.log(' 🆔 Element ID: autobot-auto-image-styles-script');
+ stylesLoaded = true;
+ }
+ } else {
+ console.log('%c📝 auto-image-styles.css not found in extension resources', 'color: #f59e0b;');
+ console.log(' 🔍 window.AUTOBOT_THEMES:', typeof window.AUTOBOT_THEMES);
+ if (window.AUTOBOT_THEMES) {
+ console.log(' 📋 Available themes:', Object.keys(window.AUTOBOT_THEMES));
+ }
+ }
+
+ // Fallback: Load from CDN if local resources not available
+ if (!stylesLoaded) {
+ console.log('%c🌐 Falling back to CDN loading...', 'color: #8b5cf6;');
+ appendLinkOnce(
+ 'https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css',
+ { 'data-wplace-theme': 'true' }
+ );
+ console.log('%c📚 Loaded auto-image-styles.css from CDN (fallback)', 'color: #f59e0b; font-weight: bold;');
+ console.log(' 📍 Source: CDN (wplace-autobot.github.io)');
+ console.log(' 🌐 URL: https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css');
+ console.log(' ⚠️ Performance: Network request required');
+ }
+
+ console.groupEnd();
+
+ // Ensure default theme is loaded from extension resources
+ if (window.applyTheme && typeof window.applyTheme === 'function') {
+ console.group('%c🎨 Loading Default Theme from Extension', 'color: #8b5cf6; font-weight: bold;');
+
+ // Determine the current theme file name
+ let defaultTheme = 'classic'; // fallback
+ if (CONFIG.currentTheme === 'Neon Retro') {
+ defaultTheme = 'neon';
+ } else if (CONFIG.currentTheme === 'Classic Light') {
+ defaultTheme = 'classic-light';
+ } else if (CONFIG.currentTheme === 'Acrylic') {
+ defaultTheme = 'acrylic';
+ }
+
+ console.log(`%c🎯 Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;');
+ const success = window.applyTheme(defaultTheme);
+
+ if (success) {
+ console.log(`%c✅ Default theme loaded from extension local resources`, 'color: #10b981; font-weight: bold;');
+ console.log(` 📍 Source: Extension local file (themes/${defaultTheme}.css)`);
+ console.log(` 🚀 Performance: Instant load (no CDN request)`);
+ console.log(` 🎨 Theme: ${CONFIG.currentTheme}`);
+ } else {
+ console.warn(`%c⚠️ Failed to load theme ${defaultTheme} from extension`, 'color: #f59e0b;');
+ console.log(` 📝 Note: Theme CSS classes will still work`);
+ }
+
+ console.groupEnd();
+ } else {
+ console.warn(`%c⚠️ Extension applyTheme() function not available`, 'color: #f59e0b; font-weight: bold;');
+ console.log(` 📝 Themes will be limited to CSS classes only`);
+ }
+
+ const container = document.createElement('div');
+ container.id = 'wplace-image-bot-container';
+ container.innerHTML = `
+
+
+
+
+
+ ${Utils.t('initMessage')}
+
+
+
+
+
+
+
🖼️ Image Management
+
+
+
+
+ ${Utils.t('uploadImage')}
+
+
+
+ ${Utils.t('resizeImage')}
+
+
+
+
+
+ ${Utils.t('selectPosition')}
+
+
+
+
+
+
+
+
🎮 Painting Control
+
+
+
+
+ ${Utils.t('startPainting')}
+
+
+
+ ${Utils.t('stopPainting')}
+
+
+
+
+
+ ${Utils.t('toggleOverlay')}
+
+
+
+
+
+
+
+
⏱️ ${Utils.t('cooldownSettings')}
+
+
${Utils.t('waitCharges')}:
+
+
+
+
+
+
+
💾 Data Management
+
+
+
+
+ ${Utils.t('saveData')}
+
+
+
+ ${Utils.t('loadData')}
+
+
+
+
+
+ ${Utils.t('saveToFile')}
+
+
+
+ ${Utils.t('loadFromFile')}
+
+
+
+
+
+ `;
+
+ // Stats Window - Separate UI
+ const statsContainer = document.createElement('div');
+ statsContainer.id = 'wplace-stats-container';
+ statsContainer.style.display = 'none';
+ statsContainer.innerHTML = `
+
+
+
+
+
+
${Utils.t(
+ 'initMessage'
+ )}
+
+
+
+
+ `;
+
+ // Modern Settings Container with Theme Support
+ // Use the theme variable already declared at the top of createUI function
+ const settingsContainer = document.createElement('div');
+ settingsContainer.id = 'wplace-settings-container';
+
+ // Apply theme-based styling
+ const themeBackground = theme.primary
+ ? `linear-gradient(135deg, ${theme.primary} 0%, ${theme.secondary || theme.primary} 100%)`
+ : `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`;
+
+ settingsContainer.className = 'wplace-settings-container-base';
+ // Apply theme-specific background
+ settingsContainer.style.background = themeBackground;
+ settingsContainer.style.cssText += `
+ min-width: 420px;
+ max-width: 480px;
+ z-index: 99999;
+ color: ${theme.text || 'white'};
+ font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"};
+ box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)'
+ };
+ backdrop-filter: ${theme.backdropFilter || 'blur(10px)'};
+ overflow: hidden;
+ animation: settings-slide-in 0.4s ease-out;
+ ${theme.animations?.glow
+ ? `
+ box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'},
+ 0 0 30px ${theme.highlight || theme.neon || '#00ffff'};
+ `
+ : ''
+ }
+ `;
+
+ // noinspection CssInvalidFunction
+ settingsContainer.innerHTML = `
+
+
+
+
+
+
+
+
+ Token Source
+
+
+
+ 🤖 Automatic Token Generator (Recommended)
+ 🔄 Generator + Auto Fallback
+ 🎯 Manual Pixel Placement
+
+
+ Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement.
+
+
+
+
+
+
+
+
+ ${Utils.t('automation')}
+
+
+
+
+
+
+
+
+ Overlay Settings
+
+
+
+
+
+
+
+
+
+
+
Blue Marble Effect
+
Renders a dithered "shredded" overlay.
+
+
+
+
+
+
+
+
+
+
+ ${Utils.t('paintOptions')}
+
+
+
+
+
+
+
+ ${Utils.t('paintWhitePixels')}
+
+
+ ${Utils.t('paintWhitePixelsDescription')}
+
+
+
+
+
+
+
+
+
+ ${Utils.t('paintTransparentPixels')}
+
+
+ ${Utils.t('paintTransparentPixelsDescription')}
+
+
+
+
+
+
+
${Utils.t('paintUnavailablePixels')}
+
${Utils.t('paintUnavailablePixelsDescription')}
+
+
+
+
+
+
+
+
+
+
+ ${Utils.t('paintingSpeed')}
+
+
+
+
+
+
+ Batch Mode
+
+
+ 📦 Normal (Fixed Size)
+ 🎲 Random (Range)
+
+
+
+
+
+
+
+
+ ${CONFIG.PAINTING_SPEED.MIN}
+ ${CONFIG.PAINTING_SPEED.MAX}
+
+
+
+
+
+
+
+ 🎲 Random batch size between min and max values
+
+
+
+
+
+
+ ${Utils.t('enablePaintingSpeedLimit')}
+
+
+
+
+
+
+
+ Coordinate Generation
+
+
+
+
+
+
+ Generation Mode
+
+
+ 📏 Rows (Horizontal Lines)
+ 📐 Columns (Vertical Lines)
+ ⭕ Circle Out (Center → Edges)
+ ⭕ Circle In (Edges → Center)
+ 🟫 Blocks (Ordered)
+ 🎲 Shuffle Blocks (Random)
+
+
+
+
+
+
+
+ Starting Direction
+
+
+ ↖️ Top-Left
+ ↗️ Top-Right
+ ↙️ Bottom-Left
+ ↘️ Bottom-Right
+
+
+
+
+
+
+
+
Snake Pattern
+
Alternate direction for each row/column (zigzag pattern)
+
+
+
+
+
+
+
+
+
+ 🧱 Block dimensions for block-based generation modes
+
+
+
+
+
+
+
+
+ Desktop Notifications
+
+
+
+
+
+
+
+
+ ${Utils.t('themeSettings')}
+
+
+
+ ${Object.keys(CONFIG.THEMES)
+ .map(
+ (themeName) =>
+ `${themeName} `
+ )
+ .join('')}
+
+
+
+
+
+
+
+
+ ${Utils.t('language')}
+
+
+
+ 🇻🇳 Tiếng Việt
+ 🇮🇩 Bahasa Indonesia
+ 🇷🇺 Русский
+ 🇺🇦 Українська
+ 🇺🇸 English
+ 🇧🇷 Português
+ 🇫🇷 Français
+ 🇹🇷 Türkçe
+ 🇨🇳 简体中文
+ 🇹🇼 繁體中文
+ 🇯🇵 日本語
+ 🇰🇷 한국어
+
+
+
+
+
+
+
+
+ `;
+
+ const resizeContainer = document.createElement('div');
+ resizeContainer.className = 'resize-container';
+ resizeContainer.innerHTML = `
+ ${Utils.t('resizeImage')}
+
+
+
+
+
+
+
+ Color Palette
+
+
+
+
+
+ ${Utils.t('showAllColorsIncluding')}
+
+
+
+ Select All
+ Unselect All
+ Unselect Paid
+
+
+
+
+
+
+
+ Advanced Color Matching
+
+
+
+ Algorithm
+
+ Perceptual (Lab)
+ Legacy (RGB)
+
+
+
+
+
Chroma Penalty
+
Preserve vivid colors (Lab only)
+
+
+
+
+
+
+
+
+
+
Enable Dithering
+
Floyd–Steinberg error diffusion in preview and applied output
+
+
+
+
+
+ Transparency
+
+
+
+ White Thresh
+
+
+
+
Reset Advanced
+
+
+
+
+
+
+ ${Utils.t('downloadPreview')}
+
+
+
+ ${Utils.t('apply')}
+
+
+
+ ${Utils.t('cancel')}
+
+
+ `;
+
+ const resizeOverlay = document.createElement('div');
+ resizeOverlay.className = 'resize-overlay';
+
+ document.body.appendChild(container);
+ document.body.appendChild(resizeOverlay);
+ document.body.appendChild(resizeContainer);
+ document.body.appendChild(statsContainer);
+ document.body.appendChild(settingsContainer);
+
+ // Show the main container after all elements are appended
+ container.style.display = 'block';
+
+ const uploadBtn = container.querySelector('#uploadBtn');
+ const resizeBtn = container.querySelector('#resizeBtn');
+ const selectPosBtn = container.querySelector('#selectPosBtn');
+ const startBtn = container.querySelector('#startBtn');
+ const stopBtn = container.querySelector('#stopBtn');
+ const saveBtn = container.querySelector('#saveBtn');
+ const loadBtn = container.querySelector('#loadBtn');
+ const saveToFileBtn = container.querySelector('#saveToFileBtn');
+ const loadFromFileBtn = container.querySelector('#loadFromFileBtn');
+
+ container.querySelectorAll('.wplace-section-title').forEach((title) => {
+ // Add a right-side arrow if it doesn't exist
+ if (!title.querySelector('i.arrow')) {
+ const arrow = document.createElement('i');
+ arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow
+ title.appendChild(arrow);
+ }
+
+ // Click event to toggle collapse/expand of the section
+ title.addEventListener('click', () => {
+ const section = title.parentElement;
+ section.classList.toggle('collapsed');
+ });
+ });
+
+ // Disable load/upload buttons until initial setup is complete (startup only)
+ if (loadBtn) {
+ loadBtn.disabled = !state.initialSetupComplete;
+ loadBtn.title = state.initialSetupComplete
+ ? ''
+ : '🔄 Waiting for initial setup to complete...';
+ }
+ if (loadFromFileBtn) {
+ loadFromFileBtn.disabled = !state.initialSetupComplete;
+ loadFromFileBtn.title = state.initialSetupComplete
+ ? ''
+ : '🔄 Waiting for initial setup to complete...';
+ }
+ if (uploadBtn) {
+ uploadBtn.disabled = !state.initialSetupComplete;
+ uploadBtn.title = state.initialSetupComplete
+ ? ''
+ : '🔄 Waiting for initial setup to complete...';
+ }
+
+ const minimizeBtn = container.querySelector('#minimizeBtn');
+ const compactBtn = container.querySelector('#compactBtn');
+ const statsBtn = container.querySelector('#statsBtn');
+ const toggleOverlayBtn = container.querySelector('#toggleOverlayBtn');
+ const statusText = container.querySelector('#statusText');
+ const progressBar = container.querySelector('#progressBar');
+ const statsArea = statsContainer.querySelector('#statsArea');
+ const content = container.querySelector('.wplace-content');
+ const closeStatsBtn = statsContainer.querySelector('#closeStatsBtn');
+ const refreshChargesBtn = statsContainer.querySelector('#refreshChargesBtn');
+ const cooldownSlider = container.querySelector('#cooldownSlider');
+ const cooldownInput = container.querySelector('#cooldownInput');
+ const cooldownDecrease = container.querySelector('#cooldownDecrease');
+ const cooldownIncrease = container.querySelector('#cooldownIncrease');
+ const cooldownValue = container.querySelector('#cooldownValue');
+
+ if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) {
+ console.error('Some UI elements not found:', {
+ uploadBtn: !!uploadBtn,
+ selectPosBtn: !!selectPosBtn,
+ startBtn: !!startBtn,
+ stopBtn: !!stopBtn,
+ });
+ }
+
+ if (!statsContainer || !statsArea || !closeStatsBtn) {
+ // Note: base CSS now aligns with this layout: main panel at left:20px (width 280), stats at left:330px.
+ }
+
+ const header = container.querySelector('.wplace-header');
+
+ makeDraggable(container);
+
+ function makeDraggable(element) {
+ let pos1 = 0,
+ pos2 = 0,
+ pos3 = 0,
+ pos4 = 0;
+ let isDragging = false;
+ const header =
+ element.querySelector('.wplace-header') || element.querySelector('.wplace-settings-header');
+
+ if (!header) {
+ console.warn('No draggable header found for element:', element);
+ return;
+ }
+
+ header.onmousedown = dragMouseDown;
+
+ function dragMouseDown(e) {
+ if (e.target.closest('.wplace-header-btn') || e.target.closest('button')) return;
+
+ e.preventDefault();
+ isDragging = true;
+
+ const rect = element.getBoundingClientRect();
+
+ element.style.transform = 'none';
+ element.style.top = rect.top + 'px';
+ element.style.left = rect.left + 'px';
+
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+ element.classList.add('wplace-dragging');
+ document.onmouseup = closeDragElement;
+ document.onmousemove = elementDrag;
+
+ document.body.style.userSelect = 'none';
+ }
+
+ function elementDrag(e) {
+ if (!isDragging) return;
+
+ e.preventDefault();
+ pos1 = pos3 - e.clientX;
+ pos2 = pos4 - e.clientY;
+ pos3 = e.clientX;
+ pos4 = e.clientY;
+
+ let newTop = element.offsetTop - pos2;
+ let newLeft = element.offsetLeft - pos1;
+
+ const rect = element.getBoundingClientRect();
+ const maxTop = window.innerHeight - rect.height;
+ const maxLeft = window.innerWidth - rect.width;
+
+ newTop = Math.max(0, Math.min(newTop, maxTop));
+ newLeft = Math.max(0, Math.min(newLeft, maxLeft));
+
+ element.style.top = newTop + 'px';
+ element.style.left = newLeft + 'px';
+ }
+
+ function closeDragElement() {
+ isDragging = false;
+ element.classList.remove('wplace-dragging');
+ document.onmouseup = null;
+ document.onmousemove = null;
+ document.body.style.userSelect = '';
+ }
+ }
+
+ makeDraggable(statsContainer);
+ makeDraggable(container);
+
+ if (statsBtn && closeStatsBtn) {
+ statsBtn.addEventListener('click', () => {
+ const isVisible = statsContainer.style.display !== 'none';
+ if (isVisible) {
+ statsContainer.style.display = 'none';
+ statsBtn.innerHTML = ' ';
+ statsBtn.title = Utils.t('showStats');
+ } else {
+ statsContainer.style.display = 'block';
+ statsBtn.innerHTML = ' ';
+ statsBtn.title = Utils.t('hideStats');
+ }
+ });
+
+ closeStatsBtn.addEventListener('click', () => {
+ statsContainer.style.display = 'none';
+ statsBtn.innerHTML = ' ';
+ statsBtn.title = Utils.t('showStats');
+ });
+
+ if (refreshChargesBtn) {
+ refreshChargesBtn.addEventListener('click', async () => {
+ refreshChargesBtn.innerHTML = ' ';
+ refreshChargesBtn.disabled = true;
+
+ try {
+ await updateStats(true);
+ } catch (error) {
+ console.error('Error refreshing charges:', error);
+ } finally {
+ refreshChargesBtn.innerHTML = ' ';
+ refreshChargesBtn.disabled = false;
+ }
+ });
+ }
+ }
+ if (statsContainer && statsBtn) {
+ // Stats container starts hidden - user clicks button to show
+ statsBtn.innerHTML = ' ';
+ statsBtn.title = Utils.t('showStats');
+ }
+
+ const settingsBtn = container.querySelector('#settingsBtn');
+ const closeSettingsBtn = settingsContainer.querySelector('#closeSettingsBtn');
+ const applySettingsBtn = settingsContainer.querySelector('#applySettingsBtn');
+
+ if (settingsBtn && closeSettingsBtn && applySettingsBtn) {
+ settingsBtn.addEventListener('click', () => {
+ const isVisible = settingsContainer.classList.contains('show');
+ if (isVisible) {
+ settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards';
+ settingsContainer.classList.remove('show');
+ setTimeout(() => {
+ settingsContainer.style.animation = '';
+ }, 300);
+ } else {
+ settingsContainer.style.top = '50%';
+ settingsContainer.style.left = '50%';
+ settingsContainer.style.transform = 'translate(-50%, -50%)';
+ settingsContainer.classList.add('show');
+ settingsContainer.style.animation = 'settings-slide-in 0.4s ease-out';
+ }
+ });
+
+ closeSettingsBtn.addEventListener('click', () => {
+ settingsContainer.style.animation = 'settings-fade-out 0.3s ease-out forwards';
+ settingsContainer.classList.remove('show');
+ setTimeout(() => {
+ settingsContainer.style.animation = '';
+ settingsContainer.style.top = '50%';
+ settingsContainer.style.left = '50%';
+ settingsContainer.style.transform = 'translate(-50%, -50%)';
+ }, 300);
+ });
+
+ applySettingsBtn.addEventListener('click', () => {
+ // Sync advanced settings before save
+ const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect');
+ if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value;
+ const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle');
+ if (enableChromaPenaltyToggle)
+ state.enableChromaPenalty = enableChromaPenaltyToggle.checked;
+ const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider');
+ if (chromaPenaltyWeightSlider)
+ state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15;
+ const transparencyThresholdInput = document.getElementById('transparencyThresholdInput');
+ if (transparencyThresholdInput) {
+ const v = parseInt(transparencyThresholdInput.value, 10);
+ if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v;
+ }
+ const whiteThresholdInput = document.getElementById('whiteThresholdInput');
+ if (whiteThresholdInput) {
+ const v = parseInt(whiteThresholdInput.value, 10);
+ if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v;
+ }
+ // Update functional thresholds
+ CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold;
+ CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold;
+ // Notifications
+ const notifEnabledToggle = document.getElementById('notifEnabledToggle');
+ const notifOnChargesToggle = document.getElementById('notifOnChargesToggle');
+ const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle');
+ const notifIntervalInput = document.getElementById('notifIntervalInput');
+ if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked;
+ if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked;
+ if (notifOnlyUnfocusedToggle)
+ state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked;
+ if (notifIntervalInput) {
+ const v = parseInt(notifIntervalInput.value, 10);
+ if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v;
+ }
+ saveBotSettings();
+ Utils.showAlert(Utils.t('settingsSaved'), 'success');
+ closeSettingsBtn.click();
+ NotificationManager.syncFromState();
+ });
+
+ makeDraggable(settingsContainer);
+
+ const tokenSourceSelect = settingsContainer.querySelector('#tokenSourceSelect');
+ if (tokenSourceSelect) {
+ tokenSourceSelect.addEventListener('change', (e) => {
+ state.tokenSource = e.target.value;
+ saveBotSettings();
+ console.log(`🔑 Token source changed to: ${state.tokenSource}`);
+ const sourceNames = {
+ generator: 'Automatic Generator',
+ hybrid: 'Generator + Auto Fallback',
+ manual: 'Manual Pixel Placement',
+ };
+ Utils.showAlert(
+ Utils.t('tokenSourceSet', { source: sourceNames[state.tokenSource] }),
+ 'success'
+ );
+ });
+ }
+
+ // Batch mode controls
+ const batchModeSelect = settingsContainer.querySelector('#batchModeSelect');
+ const normalBatchControls = settingsContainer.querySelector('#normalBatchControls');
+ const randomBatchControls = settingsContainer.querySelector('#randomBatchControls');
+ const randomBatchMin = settingsContainer.querySelector('#randomBatchMin');
+ const randomBatchMax = settingsContainer.querySelector('#randomBatchMax');
+
+ if (batchModeSelect) {
+ batchModeSelect.addEventListener('change', (e) => {
+ state.batchMode = e.target.value;
+
+ // Switch between normal and random controls
+ if (normalBatchControls && randomBatchControls) {
+ if (e.target.value === 'random') {
+ normalBatchControls.style.display = 'none';
+ randomBatchControls.style.display = 'block';
+ } else {
+ normalBatchControls.style.display = 'block';
+ randomBatchControls.style.display = 'none';
+ }
+ }
+
+ saveBotSettings();
+ console.log(`📦 Batch mode changed to: ${state.batchMode}`);
+ Utils.showAlert(
+ Utils.t('batchModeSet', {
+ mode:
+ state.batchMode === 'random' ? Utils.t('randomRange') : Utils.t('normalFixedSize'),
+ }),
+ 'success'
+ );
+ });
+ }
+
+ if (randomBatchMin) {
+ randomBatchMin.addEventListener('input', (e) => {
+ const min = parseInt(e.target.value);
+ if (min >= 1 && min <= 1000) {
+ state.randomBatchMin = min;
+ // Ensure min doesn't exceed max
+ if (randomBatchMax && min > state.randomBatchMax) {
+ state.randomBatchMax = min;
+ randomBatchMax.value = min;
+ }
+ saveBotSettings();
+ }
+ });
+ }
+
+ if (randomBatchMax) {
+ randomBatchMax.addEventListener('input', (e) => {
+ const max = parseInt(e.target.value);
+ if (max >= 1 && max <= 1000) {
+ state.randomBatchMax = max;
+ // Ensure max doesn't go below min
+ if (randomBatchMin && max < state.randomBatchMin) {
+ state.randomBatchMin = max;
+ randomBatchMin.value = max;
+ }
+ saveBotSettings();
+ }
+ });
+ }
+
+ const languageSelect = settingsContainer.querySelector('#languageSelect');
+ if (languageSelect) {
+ languageSelect.addEventListener('change', async (e) => {
+ const newLanguage = e.target.value;
+ state.language = newLanguage;
+ localStorage.setItem('wplace_language', newLanguage);
+
+ // Load the new language translations
+ await loadTranslations(newLanguage);
+
+ setTimeout(() => {
+ settingsContainer.style.display = 'none';
+ createUI();
+ }, 100);
+ });
+ }
+
+ const themeSelect = settingsContainer.querySelector('#themeSelect');
+ if (themeSelect) {
+ themeSelect.addEventListener('change', (e) => {
+ const newTheme = e.target.value;
+ switchTheme(newTheme);
+ });
+ }
+
+ const overlayOpacitySlider = settingsContainer.querySelector('#overlayOpacitySlider');
+ const overlayOpacityValue = settingsContainer.querySelector('#overlayOpacityValue');
+ const enableBlueMarbleToggle = settingsContainer.querySelector('#enableBlueMarbleToggle');
+ const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle');
+ const settingsPaintTransparentToggle = settingsContainer.querySelector(
+ '#settingsPaintTransparentToggle'
+ );
+
+ if (overlayOpacitySlider && overlayOpacityValue) {
+ const updateOpacity = (newValue) => {
+ const opacity = parseFloat(newValue);
+ state.overlayOpacity = opacity;
+ overlayOpacitySlider.value = opacity;
+ overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`;
+ };
+
+ overlayOpacitySlider.addEventListener('input', (e) => {
+ updateOpacity(e.target.value);
+ });
+
+ // Add scroll-to-adjust for overlay opacity slider
+ Utils.createScrollToAdjust(overlayOpacitySlider, updateOpacity, 0, 1, 0.05);
+ }
+
+ if (settingsPaintWhiteToggle) {
+ settingsPaintWhiteToggle.checked = state.paintWhitePixels;
+ settingsPaintWhiteToggle.addEventListener('change', (e) => {
+ state.paintWhitePixels = e.target.checked;
+ saveBotSettings();
+ console.log(`🎨 Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`);
+ const statusText = state.paintWhitePixels
+ ? 'White pixels in the template will be painted'
+ : 'White pixels will be skipped';
+ Utils.showAlert(statusText, 'success');
+ });
+ }
+
+ if (settingsPaintTransparentToggle) {
+ settingsPaintTransparentToggle.checked = state.paintTransparentPixels;
+ settingsPaintTransparentToggle.addEventListener('change', (e) => {
+ state.paintTransparentPixels = e.target.checked;
+ saveBotSettings();
+ console.log(
+ `🎨 Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}`
+ );
+ const statusText = state.paintTransparentPixels
+ ? 'Transparent pixels in the template will be painted with the closest available color'
+ : 'Transparent pixels will be skipped';
+ Utils.showAlert(statusText, 'success');
+ });
+ }
+
+ // Speed controls - both slider and input
+ const speedSlider = settingsContainer.querySelector('#speedSlider');
+ const speedInput = settingsContainer.querySelector('#speedInput');
+ const speedDecrease = settingsContainer.querySelector('#speedDecrease');
+ const speedIncrease = settingsContainer.querySelector('#speedIncrease');
+ const speedValue = settingsContainer.querySelector('#speedValue');
+
+ if (speedSlider && speedInput && speedValue && speedDecrease && speedIncrease) {
+ const updateSpeed = (newValue) => {
+ const speed = Math.max(CONFIG.PAINTING_SPEED.MIN, Math.min(CONFIG.PAINTING_SPEED.MAX, parseInt(newValue)));
+ state.paintingSpeed = speed;
+
+ // Update both controls (value shows in input, label shows unit only)
+ speedSlider.value = speed;
+ speedInput.value = speed;
+ speedValue.textContent = `pixels`;
+
+ saveBotSettings();
+ };
+
+ // Slider event listener
+ speedSlider.addEventListener('input', (e) => {
+ updateSpeed(e.target.value);
+ });
+
+ // Number input event listener
+ speedInput.addEventListener('input', (e) => {
+ updateSpeed(e.target.value);
+ });
+
+ // Decrease button
+ speedDecrease.addEventListener('click', () => {
+ updateSpeed(parseInt(speedInput.value) - 1);
+ });
+
+ // Increase button
+ speedIncrease.addEventListener('click', () => {
+ updateSpeed(parseInt(speedInput.value) + 1);
+ });
+
+ // Add scroll-to-adjust for speed slider
+ Utils.createScrollToAdjust(speedSlider, updateSpeed, CONFIG.PAINTING_SPEED.MIN, CONFIG.PAINTING_SPEED.MAX, 1);
+ }
+
+ if (enableBlueMarbleToggle) {
+ enableBlueMarbleToggle.addEventListener('click', async () => {
+ state.blueMarbleEnabled = enableBlueMarbleToggle.checked;
+ if (state.imageLoaded && overlayManager.imageBitmap) {
+ Utils.showAlert(Utils.t('reprocessingOverlay'), 'info');
+ await overlayManager.processImageIntoChunks();
+ Utils.showAlert(Utils.t('overlayUpdated'), 'success');
+ }
+ });
+ }
+
+ // (Advanced color listeners moved outside to work with resize dialog)
+ // (Advanced color listeners moved outside to work with resize dialog)
+ // Notifications listeners
+ const notifPermBtn = settingsContainer.querySelector('#notifRequestPermBtn');
+ const notifTestBtn = settingsContainer.querySelector('#notifTestBtn');
+ if (notifPermBtn) {
+ notifPermBtn.addEventListener('click', async () => {
+ const perm = await NotificationManager.requestPermission();
+ if (perm === 'granted') Utils.showAlert(Utils.t('notificationsEnabled'), 'success');
+ else Utils.showAlert(Utils.t('notificationsPermissionDenied'), 'warning');
+ });
+ }
+ if (notifTestBtn) {
+ notifTestBtn.addEventListener('click', () => {
+ NotificationManager.notify(
+ Utils.t('testNotificationTitle'),
+ Utils.t('testNotificationMessage'),
+ 'wplace-notify-test',
+ true
+ );
+ });
+ }
+ }
+
+ const widthSlider = resizeContainer.querySelector('#widthSlider');
+ const heightSlider = resizeContainer.querySelector('#heightSlider');
+ const widthValue = resizeContainer.querySelector('#widthValue');
+ const heightValue = resizeContainer.querySelector('#heightValue');
+ const keepAspect = resizeContainer.querySelector('#keepAspect');
+ const paintWhiteToggle = resizeContainer.querySelector('#paintWhiteToggle');
+ const paintTransparentToggle = resizeContainer.querySelector('#paintTransparentToggle');
+ const zoomSlider = resizeContainer.querySelector('#zoomSlider');
+ const zoomValue = resizeContainer.querySelector('#zoomValue');
+ const zoomInBtn = resizeContainer.querySelector('#zoomInBtn');
+ const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn');
+ const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn');
+ const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn');
+ const panModeBtn = resizeContainer.querySelector('#panModeBtn');
+ const panStage = resizeContainer.querySelector('#resizePanStage');
+ const canvasStack = resizeContainer.querySelector('#resizeCanvasStack');
+ const baseCanvas = resizeContainer.querySelector('#resizeCanvas');
+ const maskCanvas = resizeContainer.querySelector('#maskCanvas');
+ const baseCtx = baseCanvas.getContext('2d');
+ const maskCtx = maskCanvas.getContext('2d');
+ const confirmResize = resizeContainer.querySelector('#confirmResize');
+ const cancelResize = resizeContainer.querySelector('#cancelResize');
+ const downloadPreviewBtn = resizeContainer.querySelector('#downloadPreviewBtn');
+ const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn');
+
+ // Coordinate generation controls with smart visibility
+ const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect');
+ const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect');
+ const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle');
+ const directionControls = settingsContainer.querySelector('#directionControls');
+ const snakeControls = settingsContainer.querySelector('#snakeControls');
+ const blockControls = settingsContainer.querySelector('#blockControls');
+ const blockWidthInput = settingsContainer.querySelector('#blockWidthInput');
+ const blockHeightInput = settingsContainer.querySelector('#blockHeightInput');
+ const paintUnavailablePixelsToggle = settingsContainer.querySelector(
+ '#paintUnavailablePixelsToggle'
+ );
+
+ if (paintUnavailablePixelsToggle) {
+ paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels;
+ paintUnavailablePixelsToggle.addEventListener('change', (e) => {
+ state.paintUnavailablePixels = e.target.checked;
+ saveBotSettings();
+ console.log(`🎨 Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`);
+ const statusText = state.paintUnavailablePixels
+ ? 'Unavailable template colors will be painted with the closest available color'
+ : 'Unavailable template colors will be skipped';
+ Utils.showAlert(statusText, 'success');
+ });
+ }
+ if (coordinateModeSelect) {
+ coordinateModeSelect.value = state.coordinateMode;
+ coordinateModeSelect.addEventListener('change', (e) => {
+ state.coordinateMode = e.target.value;
+ Utils.updateCoordinateUI({
+ mode: state.coordinateMode,
+ directionControls,
+ snakeControls,
+ blockControls,
+ });
+ saveBotSettings();
+ console.log(`🔄 Coordinate mode changed to: ${state.coordinateMode}`);
+ Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success');
+ });
+ }
+
+ if (coordinateDirectionSelect) {
+ coordinateDirectionSelect.value = state.coordinateDirection;
+ coordinateDirectionSelect.addEventListener('change', (e) => {
+ state.coordinateDirection = e.target.value;
+ saveBotSettings();
+ console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`);
+ Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success');
+ });
+ }
+
+ if (coordinateSnakeToggle) {
+ coordinateSnakeToggle.checked = state.coordinateSnake;
+ coordinateSnakeToggle.addEventListener('change', (e) => {
+ state.coordinateSnake = e.target.checked;
+ saveBotSettings();
+ console.log(`🐍 Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`);
+ Utils.showAlert(
+ `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`,
+ 'success'
+ );
+ });
+ }
+
+ if (blockWidthInput) {
+ blockWidthInput.value = state.blockWidth;
+ blockWidthInput.addEventListener('input', (e) => {
+ const width = parseInt(e.target.value);
+ if (width >= 1 && width <= 50) {
+ state.blockWidth = width;
+ saveBotSettings();
+ }
+ });
+ }
+
+ if (blockHeightInput) {
+ blockHeightInput.value = state.blockHeight;
+ blockHeightInput.addEventListener('change', (e) => {
+ const height = parseInt(e.target.value);
+ if (height >= 1 && height <= 50) {
+ state.blockHeight = height;
+ saveBotSettings();
+ }
+ });
+ }
+
+ if (compactBtn) {
+ compactBtn.addEventListener('click', () => {
+ container.classList.toggle('wplace-compact');
+ const isCompact = container.classList.contains('wplace-compact');
+
+ if (isCompact) {
+ compactBtn.innerHTML = ' ';
+ compactBtn.title = Utils.t('expandMode');
+ } else {
+ compactBtn.innerHTML = ' ';
+ compactBtn.title = Utils.t('compactMode');
+ }
+ });
+ }
+
+ if (minimizeBtn) {
+ minimizeBtn.addEventListener('click', () => {
+ state.minimized = !state.minimized;
+ if (state.minimized) {
+ container.classList.add('wplace-minimized');
+ content.classList.add('wplace-hidden');
+ minimizeBtn.innerHTML = ' ';
+ minimizeBtn.title = Utils.t('restore');
+ } else {
+ container.classList.remove('wplace-minimized');
+ content.classList.remove('wplace-hidden');
+ minimizeBtn.innerHTML = ' ';
+ minimizeBtn.title = Utils.t('minimize');
+ }
+ saveBotSettings();
+ });
+ }
+
+ if (toggleOverlayBtn) {
+ toggleOverlayBtn.addEventListener('click', () => {
+ const isEnabled = overlayManager.toggle();
+ toggleOverlayBtn.classList.toggle('active', isEnabled);
+ toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false');
+ Utils.showAlert(isEnabled ? Utils.t('overlayEnabled') : Utils.t('overlayDisabled'), 'info');
+ });
+ }
+
+ if (state.minimized) {
+ container.classList.add('wplace-minimized');
+ content.classList.add('wplace-hidden');
+ if (minimizeBtn) {
+ minimizeBtn.innerHTML = ' ';
+ minimizeBtn.title = Utils.t('restore');
+ }
+ } else {
+ container.classList.remove('wplace-minimized');
+ content.classList.remove('wplace-hidden');
+ if (minimizeBtn) {
+ minimizeBtn.innerHTML = ' ';
+ minimizeBtn.title = Utils.t('minimize');
+ }
+ }
+
+ if (saveBtn) {
+ saveBtn.addEventListener('click', () => {
+ if (!state.imageLoaded) {
+ Utils.showAlert(Utils.t('missingRequirements'), 'error');
+ return;
+ }
+
+ const success = Utils.saveProgress();
+ if (success) {
+ updateUI('autoSaved', 'success');
+ Utils.showAlert(Utils.t('autoSaved'), 'success');
+ } else {
+ Utils.showAlert(Utils.t('errorSavingProgress'), 'error');
+ }
+ });
+ }
+
+ if (loadBtn) {
+ loadBtn.addEventListener('click', () => {
+ // Check if initial setup is complete
+ if (!state.initialSetupComplete) {
+ Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning');
+ return;
+ }
+
+ const savedData = Utils.loadProgress();
+ if (!savedData) {
+ updateUI('noSavedData', 'warning');
+ Utils.showAlert(Utils.t('noSavedData'), 'warning');
+ return;
+ }
+
+ const confirmLoad = confirm(
+ `${Utils.t('savedDataFound')}\n\n` +
+ `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` +
+ `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels`
+ );
+
+ if (confirmLoad) {
+ const success = Utils.restoreProgress(savedData);
+ if (success) {
+ updateUI('dataLoaded', 'success');
+ Utils.showAlert(Utils.t('dataLoaded'), 'success');
+ updateDataButtons();
+
+ updateStats();
+
+ // Restore overlay if image data was loaded from localStorage
+ Utils.restoreOverlayFromData().catch((error) => {
+ console.error('Failed to restore overlay from localStorage:', error);
+ });
+
+ if (!state.colorsChecked) {
+ uploadBtn.disabled = false;
+ } else {
+ uploadBtn.disabled = false;
+ selectPosBtn.disabled = false;
+ }
+
+ if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
+ startBtn.disabled = false;
+ }
+ } else {
+ Utils.showAlert(Utils.t('errorLoadingProgress'), 'error');
+ }
+ }
+ });
+ }
+
+ if (saveToFileBtn) {
+ saveToFileBtn.addEventListener('click', () => {
+ const success = Utils.saveProgressToFile();
+ if (success) {
+ updateUI('fileSaved', 'success');
+ Utils.showAlert(Utils.t('fileSaved'), 'success');
+ } else {
+ Utils.showAlert(Utils.t('fileError'), 'error');
+ }
+ });
+ }
+
+ if (loadFromFileBtn) {
+ loadFromFileBtn.addEventListener('click', async () => {
+ // Check if initial setup is complete
+ if (!state.initialSetupComplete) {
+ Utils.showAlert(Utils.t('pleaseWaitFileSetup'), 'warning');
+ return;
+ }
+
+ try {
+ const success = await Utils.loadProgressFromFile();
+ if (success) {
+ updateUI('fileLoaded', 'success');
+ Utils.showAlert(Utils.t('fileLoaded'), 'success');
+ updateDataButtons();
+
+ await updateStats();
+
+ // Restore overlay if image data was loaded from file
+ await Utils.restoreOverlayFromData().catch((error) => {
+ console.error('Failed to restore overlay from file:', error);
+ });
+
+ if (state.colorsChecked) {
+ uploadBtn.disabled = false;
+ selectPosBtn.disabled = false;
+ resizeBtn.disabled = false;
+ } else {
+ uploadBtn.disabled = false;
+ }
+
+ if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) {
+ startBtn.disabled = false;
+ }
+ }
+ } catch (error) {
+ if (error.message === 'Invalid JSON file') {
+ Utils.showAlert(Utils.t('invalidFileFormat'), 'error');
+ } else {
+ Utils.showAlert(Utils.t('fileError'), 'error');
+ }
+ }
+ });
+ }
+
+ updateUI = (messageKey, type = 'default', params = {}, silent = false) => {
+ const message = Utils.t(messageKey, params);
+ statusText.textContent = message;
+ statusText.className = `wplace-status status-${type}`;
+
+ if (!silent) {
+ // Trigger animation only when silent = false
+ statusText.style.animation = 'none';
+ void statusText.offsetWidth; // trick to restart the animation
+ statusText.style.animation = 'slide-in 0.3s ease-out';
+ }
+ };
+
+ function updateChargeStatsDisplay(intervalMs) {
+ const currentChargesEl = document.getElementById('wplace-stat-charges-value');
+ const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value');
+ if (!fullChargeEl && !currentChargesEl) return;
+ if (!state.fullChargeData) {
+ fullChargeEl.textContent = '--:--:--';
+ return;
+ }
+
+ const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData;
+ const elapsed = Date.now() - startTime;
+
+ // total charges including elapsed time and spent during painting since snapshot
+ const chargesGained = elapsed / cooldownMs;
+ const rawCharges = current + chargesGained - spentSinceShot;
+ const cappedCharges = Math.min(rawCharges, max);
+
+ // rounding with 0.95 threshold
+ let displayCharges;
+ const fraction = cappedCharges - Math.floor(cappedCharges);
+ if (fraction >= 0.95) {
+ displayCharges = Math.ceil(cappedCharges);
+ } else {
+ displayCharges = Math.floor(cappedCharges);
+ }
+
+ state.displayCharges = Math.max(0, displayCharges);
+ state.preciseCurrentCharges = cappedCharges;
+
+ const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs);
+ const timeText = Utils.msToTimeText(remainingMs);
+
+ if (currentChargesEl) {
+ currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`;
+ }
+
+ if (
+ state.displayCharges < state.cooldownChargeThreshold &&
+ !state.stopFlag &&
+ state.running
+ ) {
+ updateChargesThresholdUI(intervalMs);
+ }
+
+ if (fullChargeEl) {
+ if (state.displayCharges >= max) {
+ fullChargeEl.innerHTML = `FULL `;
+ } else {
+ fullChargeEl.innerHTML = `
+ ${timeText}
+ `;
+ }
+ }
+ }
+
+ updateStats = async (isManualRefresh = false) => {
+ const isForcedRefresh = isManualRefresh;
+ const isFirstCheck = !state.fullChargeData?.startTime;
+
+ const minUpdateInterval = 60_000;
+ const maxUpdateInterval = 90_000;
+ const randomUpdateThreshold =
+ minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval);
+ const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0);
+ const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold;
+
+ const shouldCallApi = isForcedRefresh || isFirstCheck || isTimeToUpdate;
+
+ if (shouldCallApi) {
+ const { charges, max, cooldown } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.preciseCurrentCharges = charges;
+ state.cooldown = cooldown;
+ state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges;
+
+ state.fullChargeData = {
+ current: charges,
+ max: max,
+ cooldownMs: cooldown,
+ startTime: Date.now(),
+ spentSinceShot: 0,
+ };
+ // Evaluate notifications every time we refresh server-side charges
+ NotificationManager.maybeNotifyChargesReached();
+ }
+
+ if (state.fullChargeInterval) {
+ clearInterval(state.fullChargeInterval);
+ state.fullChargeInterval = null;
+ }
+ const intervalMs = 1000;
+ state.fullChargeInterval = setInterval(
+ () => updateChargeStatsDisplay(intervalMs),
+ intervalMs
+ );
+
+ if (cooldownSlider && cooldownSlider.max !== state.maxCharges) {
+ cooldownSlider.max = state.maxCharges;
+ }
+ if (cooldownInput && cooldownInput.max !== state.maxCharges) {
+ cooldownInput.max = state.maxCharges;
+ }
+
+ let imageStatsHTML = '';
+ if (state.imageLoaded) {
+ const progress =
+ state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0;
+ const remainingPixels = state.totalPixels - state.paintedPixels;
+ state.estimatedTime = Utils.calculateEstimatedTime(
+ remainingPixels,
+ state.displayCharges,
+ state.cooldown
+ );
+ progressBar.style.width = `${progress}%`;
+
+ imageStatsHTML = `
+
+
${Utils.t('progress')}
+
${progress}%
+
+
+
${Utils.t(
+ 'pixels'
+ )}
+
${state.paintedPixels}/${state.totalPixels}
+
+
+
${Utils.t(
+ 'estimatedTime'
+ )}
+
${Utils.formatTime(state.estimatedTime)}
+
+ `;
+ }
+
+ let colorSwatchesHTML = '';
+ state.availableColors = state.availableColors.filter(
+ (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null
+ );
+
+ const availableColors = Utils.extractAvailableColors();
+ const newCount = Array.isArray(availableColors) ? availableColors.length : 0;
+
+ if (newCount === 0 && isManualRefresh) {
+ Utils.showAlert(Utils.t('noColorsFound'), 'warning');
+ } else if (newCount > 0 && state.availableColors.length < newCount) {
+ const oldCount = state.availableColors.length;
+
+ Utils.showAlert(
+ Utils.t('colorsUpdated', {
+ oldCount,
+ newCount: newCount,
+ diffCount: newCount - oldCount,
+ }),
+ 'success'
+ );
+ state.availableColors = availableColors;
+ }
+ if (state.colorsChecked) {
+ colorSwatchesHTML = state.availableColors
+ .map((color) => {
+ const rgbString = `rgb(${color.rgb.join(',')})`;
+ return `
`;
+ })
+ .join('');
+ }
+
+ statsArea.innerHTML = `
+ ${imageStatsHTML}
+
+
+ ${Utils.t('charges')}
+
+
+ ${state.displayCharges} / ${state.maxCharges}
+
+
+
+
+ ${Utils.t('fullChargeIn')}
+
+
--:--:--
+
+ ${state.colorsChecked
+ ? `
+
+
${Utils.t(
+ 'availableColors',
+ { count: state.availableColors.length }
+ )}
+
+ ${colorSwatchesHTML}
+
+
+ `
+ : ''
+ }
+ `;
+
+ // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial
+ updateChargeStatsDisplay(intervalMs);
+ };
+
+ updateDataButtons = () => {
+ const hasImageData = state.imageLoaded && state.imageData;
+ saveBtn.disabled = !hasImageData;
+ saveToFileBtn.disabled = !hasImageData;
+ };
+
+ updateDataButtons();
+
+ function showResizeDialog(processor) {
+ let baseProcessor = processor;
+ let width, height;
+ if (state.originalImage?.dataUrl) {
+ baseProcessor = new ImageProcessor(state.originalImage.dataUrl);
+ width = state.originalImage.width;
+ height = state.originalImage.height;
+ } else {
+ const dims = processor.getDimensions();
+ width = dims.width;
+ height = dims.height;
+ }
+ const aspectRatio = width / height;
+
+ const rs = state.resizeSettings;
+ widthSlider.max = width * 2;
+ heightSlider.max = height * 2;
+ let initialW = width;
+ let initialH = height;
+ if (
+ rs &&
+ Number.isFinite(rs.width) &&
+ Number.isFinite(rs.height) &&
+ rs.width > 0 &&
+ rs.height > 0
+ ) {
+ initialW = rs.width;
+ initialH = rs.height;
+ }
+ // Clamp to slider ranges
+ initialW = Math.max(
+ parseInt(widthSlider.min, 10) || 10,
+ Math.min(initialW, parseInt(widthSlider.max, 10))
+ );
+ initialH = Math.max(
+ parseInt(heightSlider.min, 10) || 10,
+ Math.min(initialH, parseInt(heightSlider.max, 10))
+ );
+ widthSlider.value = initialW;
+ heightSlider.value = initialH;
+ widthValue.textContent = initialW;
+ heightValue.textContent = initialH;
+ zoomSlider.value = 1;
+ if (zoomValue) zoomValue.textContent = '100%';
+ paintWhiteToggle.checked = state.paintWhitePixels;
+ paintTransparentToggle.checked = state.paintTransparentPixels;
+
+ let _previewTimer = null;
+ let _previewJobId = 0;
+ let _isDraggingSize = false;
+ let _zoomLevel = 1;
+ let _ditherWorkBuf = null;
+ let _ditherEligibleBuf = null;
+ const ensureDitherBuffers = (n) => {
+ if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3)
+ _ditherWorkBuf = new Float32Array(n * 3);
+ if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n)
+ _ditherEligibleBuf = new Uint8Array(n);
+ return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf };
+ };
+ let _maskImageData = null;
+ let _maskData = null;
+ let _dirty = null;
+ const _resetDirty = () => {
+ _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 };
+ };
+ const _markDirty = (x, y) => {
+ if (!_dirty) _resetDirty();
+ if (x < _dirty.minX) _dirty.minX = x;
+ if (y < _dirty.minY) _dirty.minY = y;
+ if (x > _dirty.maxX) _dirty.maxX = x;
+ if (y > _dirty.maxY) _dirty.maxY = y;
+ };
+ const _flushDirty = () => {
+ if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return;
+ const x = Math.max(0, _dirty.minX);
+ const y = Math.max(0, _dirty.minY);
+ const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1);
+ const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1);
+ if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h);
+ _resetDirty();
+ };
+ const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => {
+ if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) {
+ _maskImageData = maskCtx.createImageData(w, h);
+ _maskData = _maskImageData.data;
+ rebuildFromMask = true;
+ }
+ if (rebuildFromMask) {
+ const m = state.resizeIgnoreMask;
+ const md = _maskData;
+ md.fill(0);
+ if (m) {
+ for (let i = 0; i < m.length; i++)
+ if (m[i]) {
+ const p = i * 4;
+ md[p] = 255;
+ md[p + 1] = 0;
+ md[p + 2] = 0;
+ md[p + 3] = 150;
+ }
+ }
+ maskCtx.putImageData(_maskImageData, 0, 0);
+ _resetDirty();
+ }
+ };
+ const ensureMaskSize = (w, h) => {
+ if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) {
+ state.resizeIgnoreMask = new Uint8Array(w * h);
+ }
+ baseCanvas.width = w;
+ baseCanvas.height = h;
+ maskCanvas.width = w;
+ maskCanvas.height = h;
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ // Ensure overlay buffers exist and rebuild from mask when dimensions change
+ _ensureMaskOverlayBuffers(w, h, true);
+ };
+ _updateResizePreview = async () => {
+ const jobId = ++_previewJobId;
+ const newWidth = parseInt(widthSlider.value, 10);
+ const newHeight = parseInt(heightSlider.value, 10);
+ _zoomLevel = parseFloat(zoomSlider.value);
+
+ widthValue.textContent = newWidth;
+ heightValue.textContent = newHeight;
+
+ ensureMaskSize(newWidth, newHeight);
+ canvasStack.style.width = newWidth + 'px';
+ canvasStack.style.height = newHeight + 'px';
+ baseCtx.imageSmoothingEnabled = false;
+ if (!state.availableColors || state.availableColors.length === 0) {
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ baseCtx.clearRect(0, 0, newWidth, newHeight);
+ baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ // Draw existing mask overlay buffer
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0);
+ updateZoomLayout();
+ return;
+ }
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ baseCtx.clearRect(0, 0, newWidth, newHeight);
+ baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight);
+ const data = imgData.data;
+
+ const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+
+ const applyFSDither = () => {
+ const w = newWidth,
+ h = newHeight;
+ const n = w * h;
+ const { work, eligible } = ensureDitherBuffers(n);
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ const i4 = idx * 4;
+ const r = data[i4],
+ g = data[i4 + 1],
+ b = data[i4 + 2],
+ a = data[i4 + 3];
+ const isEligible =
+ (state.paintTransparentPixels || a >= tThresh) &&
+ (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b));
+ eligible[idx] = isEligible ? 1 : 0;
+ work[idx * 3] = r;
+ work[idx * 3 + 1] = g;
+ work[idx * 3 + 2] = b;
+ if (!isEligible) {
+ data[i4 + 3] = 0; // transparent in preview overlay
+ }
+ }
+ }
+
+ const diffuse = (nx, ny, er, eg, eb, factor) => {
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h) return;
+ const nidx = ny * w + nx;
+ if (!eligible[nidx]) return;
+ const base = nidx * 3;
+ work[base] = Math.min(255, Math.max(0, work[base] + er * factor));
+ work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor));
+ work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor));
+ };
+
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ if (!eligible[idx]) continue;
+ const base = idx * 3;
+ const r0 = work[base],
+ g0 = work[base + 1],
+ b0 = work[base + 2];
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(
+ r0,
+ g0,
+ b0,
+ state.activeColorPalette
+ );
+ const i4 = idx * 4;
+ data[i4] = nr;
+ data[i4 + 1] = ng;
+ data[i4 + 2] = nb;
+ data[i4 + 3] = 255;
+
+ const er = r0 - nr;
+ const eg = g0 - ng;
+ const eb = b0 - nb;
+
+ diffuse(x + 1, y, er, eg, eb, 7 / 16);
+ diffuse(x - 1, y + 1, er, eg, eb, 3 / 16);
+ diffuse(x, y + 1, er, eg, eb, 5 / 16);
+ diffuse(x + 1, y + 1, er, eg, eb, 1 / 16);
+ }
+ }
+ };
+
+ // Skip expensive dithering while user is dragging sliders
+ if (state.ditheringEnabled && !_isDraggingSize) {
+ applyFSDither();
+ } else {
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i],
+ g = data[i + 1],
+ b = data[i + 2],
+ a = data[i + 3];
+ if (
+ (!state.paintTransparentPixels && a < tThresh) ||
+ (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))
+ ) {
+ data[i + 3] = 0;
+ continue;
+ }
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+ data[i] = nr;
+ data[i + 1] = ng;
+ data[i + 2] = nb;
+ data[i + 3] = 255;
+ }
+ }
+
+ if (jobId !== _previewJobId) return;
+ baseCtx.putImageData(imgData, 0, 0);
+ maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
+ if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0);
+ updateZoomLayout();
+ };
+
+ const onWidthInput = () => {
+ if (keepAspect.checked) {
+ heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio);
+ }
+ _updateResizePreview();
+ const curW = parseInt(widthSlider.value, 10);
+ const curH = parseInt(heightSlider.value, 10);
+ state.resizeSettings = {
+ baseWidth: width,
+ baseHeight: height,
+ width: curW,
+ height: curH,
+ };
+ saveBotSettings();
+ // Auto-fit after size changes
+ const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1;
+ if (!isNaN(fit) && isFinite(fit)) applyZoom(fit);
+ };
+
+ const onHeightInput = () => {
+ if (keepAspect.checked) {
+ widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio);
+ }
+ _updateResizePreview();
+ const curW = parseInt(widthSlider.value, 10);
+ const curH = parseInt(heightSlider.value, 10);
+ state.resizeSettings = {
+ baseWidth: width,
+ baseHeight: height,
+ width: curW,
+ height: curH,
+ };
+ saveBotSettings();
+ // Auto-fit after size changes
+ const fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1;
+ if (!isNaN(fit) && isFinite(fit)) applyZoom(fit);
+ };
+
+ paintWhiteToggle.onchange = (e) => {
+ state.paintWhitePixels = e.target.checked;
+ _updateResizePreview();
+ saveBotSettings();
+ };
+
+ paintTransparentToggle.onchange = (e) => {
+ state.paintTransparentPixels = e.target.checked;
+ _updateResizePreview();
+ saveBotSettings();
+ };
+
+ let panX = 0,
+ panY = 0;
+ const clampPan = () => {
+ const wrapRect = panStage?.getBoundingClientRect() || {
+ width: 0,
+ height: 0,
+ };
+ const w = (baseCanvas.width || 1) * _zoomLevel;
+ const h = (baseCanvas.height || 1) * _zoomLevel;
+ if (w <= wrapRect.width) {
+ panX = Math.floor((wrapRect.width - w) / 2);
+ } else {
+ const minX = wrapRect.width - w;
+ panX = Math.min(0, Math.max(minX, panX));
+ }
+ if (h <= wrapRect.height) {
+ panY = Math.floor((wrapRect.height - h) / 2);
+ } else {
+ const minY = wrapRect.height - h;
+ panY = Math.min(0, Math.max(minY, panY));
+ }
+ };
+ let _panRaf = 0;
+ const applyPan = () => {
+ if (_panRaf) return;
+ _panRaf = requestAnimationFrame(() => {
+ clampPan();
+ canvasStack.style.transform = `translate3d(${Math.round(
+ panX
+ )}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`;
+ _panRaf = 0;
+ });
+ };
+
+ const updateZoomLayout = () => {
+ const w = baseCanvas.width || 1,
+ h = baseCanvas.height || 1;
+ baseCanvas.style.width = w + 'px';
+ baseCanvas.style.height = h + 'px';
+ maskCanvas.style.width = w + 'px';
+ maskCanvas.style.height = h + 'px';
+ canvasStack.style.width = w + 'px';
+ canvasStack.style.height = h + 'px';
+ applyPan();
+ };
+ const applyZoom = (z) => {
+ _zoomLevel = Math.max(0.05, Math.min(20, z || 1));
+ zoomSlider.value = _zoomLevel;
+ updateZoomLayout();
+ if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`;
+ };
+ zoomSlider.addEventListener('input', () => {
+ applyZoom(parseFloat(zoomSlider.value));
+ });
+ if (zoomInBtn)
+ zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1));
+ if (zoomOutBtn)
+ zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1));
+ const computeFitZoom = () => {
+ const wrapRect = panStage?.getBoundingClientRect();
+ if (!wrapRect) return 1;
+ const w = baseCanvas.width || 1;
+ const h = baseCanvas.height || 1;
+ const margin = 10;
+ const scaleX = (wrapRect.width - margin) / w;
+ const scaleY = (wrapRect.height - margin) / h;
+ return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY)));
+ };
+ if (zoomFitBtn)
+ zoomFitBtn.addEventListener('click', () => {
+ applyZoom(computeFitZoom());
+ centerInView();
+ });
+ if (zoomActualBtn)
+ zoomActualBtn.addEventListener('click', () => {
+ applyZoom(1);
+ centerInView();
+ });
+
+ const centerInView = () => {
+ if (!panStage) return;
+ const rect = panStage.getBoundingClientRect();
+ const w = (baseCanvas.width || 1) * _zoomLevel;
+ const h = (baseCanvas.height || 1) * _zoomLevel;
+ panX = Math.floor((rect.width - w) / 2);
+ panY = Math.floor((rect.height - h) / 2);
+ applyPan();
+ };
+
+ let isPanning = false;
+ let startX = 0,
+ startY = 0,
+ startPanX = 0,
+ startPanY = 0;
+ let allowPan = false; // Space key
+ let panMode = false; // Explicit pan mode toggle for touch/one-button mice
+ const isPanMouseButton = (e) => e.button === 1 || e.button === 2;
+ const setCursor = (val) => {
+ if (panStage) panStage.style.cursor = val;
+ };
+ const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e);
+ const updatePanModeBtn = () => {
+ if (!panModeBtn) return;
+ panModeBtn.classList.toggle('active', panMode);
+ panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false');
+ };
+ if (panModeBtn) {
+ updatePanModeBtn();
+ panModeBtn.addEventListener('click', () => {
+ panMode = !panMode;
+ updatePanModeBtn();
+ setCursor(panMode ? 'grab' : '');
+ });
+ }
+ if (panStage) {
+ panStage.addEventListener('contextmenu', (e) => {
+ if (allowPan) e.preventDefault();
+ });
+ window.addEventListener('keydown', (e) => {
+ if (e.code === 'Space') {
+ allowPan = true;
+ setCursor('grab');
+ }
+ });
+ window.addEventListener('keyup', (e) => {
+ if (e.code === 'Space') {
+ allowPan = false;
+ if (!isPanning) setCursor('');
+ }
+ });
+ panStage.addEventListener('mousedown', (e) => {
+ if (!isPanActive(e)) return;
+ e.preventDefault();
+ isPanning = true;
+ startX = e.clientX;
+ startY = e.clientY;
+ startPanX = panX;
+ startPanY = panY;
+ setCursor('grabbing');
+ });
+ window.addEventListener('mousemove', (e) => {
+ if (!isPanning) return;
+ const dx = e.clientX - startX;
+ const dy = e.clientY - startY;
+ panX = startPanX + dx;
+ panY = startPanY + dy;
+ applyPan();
+ });
+ window.addEventListener('mouseup', () => {
+ if (isPanning) {
+ isPanning = false;
+ setCursor(allowPan ? 'grab' : '');
+ }
+ });
+ panStage.addEventListener(
+ 'wheel',
+ (e) => {
+ if (!e.ctrlKey && !e.metaKey) return;
+ e.preventDefault();
+ const rect = panStage.getBoundingClientRect();
+ const cx = e.clientX - rect.left - panX;
+ const cy = e.clientY - rect.top - panY;
+ const before = _zoomLevel;
+ const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1));
+ const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step)));
+ if (next === before) return;
+ const scale = next / before;
+ panX = panX - cx * (scale - 1);
+ panY = panY - cy * (scale - 1);
+ applyZoom(next);
+ },
+ { passive: false }
+ );
+ let lastTouchDist = null;
+ let touchStartTime = 0;
+ let doubleTapTimer = null;
+ panStage.addEventListener(
+ 'touchstart',
+ (e) => {
+ if (e.touches.length === 1) {
+ const t = e.touches[0];
+ isPanning = true;
+ startX = t.clientX;
+ startY = t.clientY;
+ startPanX = panX;
+ startPanY = panY;
+ setCursor('grabbing');
+ const now = Date.now();
+ if (now - touchStartTime < 300) {
+ // double tap -> toggle 100%/fit
+ const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1;
+ applyZoom(z);
+ centerInView();
+ if (doubleTapTimer) clearTimeout(doubleTapTimer);
+ } else {
+ touchStartTime = now;
+ doubleTapTimer = setTimeout(() => {
+ doubleTapTimer = null;
+ }, 320);
+ }
+ } else if (e.touches.length === 2) {
+ // Pinch start
+ const [a, b] = e.touches;
+ lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
+ }
+ },
+ { passive: true }
+ );
+ panStage.addEventListener(
+ 'touchmove',
+ (e) => {
+ if (e.touches.length === 1 && isPanning) {
+ const t = e.touches[0];
+ const dx = t.clientX - startX;
+ const dy = t.clientY - startY;
+ panX = startPanX + dx;
+ panY = startPanY + dy;
+ applyPan();
+ } else if (e.touches.length === 2 && lastTouchDist != null) {
+ e.preventDefault();
+ const [a, b] = e.touches;
+ const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY);
+ const rect = panStage.getBoundingClientRect();
+ const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX;
+ const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY;
+ const before = _zoomLevel;
+ const scale = dist / (lastTouchDist || dist);
+ const next = Math.max(0.05, Math.min(20, before * scale));
+ if (next !== before) {
+ panX = panX - centerX * (next / before - 1);
+ panY = panY - centerY * (next / before - 1);
+ applyZoom(next);
+ }
+ lastTouchDist = dist;
+ }
+ },
+ { passive: false }
+ );
+ panStage.addEventListener('touchend', () => {
+ isPanning = false;
+ lastTouchDist = null;
+ setCursor(panMode || allowPan ? 'grab' : '');
+ });
+ }
+ const schedulePreview = () => {
+ if (_previewTimer) clearTimeout(_previewTimer);
+ const run = () => {
+ _previewTimer = null;
+ _updateResizePreview();
+ };
+ if (window.requestIdleCallback) {
+ _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50);
+ } else {
+ _previewTimer = setTimeout(() => requestAnimationFrame(run), 50);
+ }
+ };
+ // Track dragging to reduce work and skip dithering during drag
+ const markDragStart = () => {
+ _isDraggingSize = true;
+ };
+ const markDragEnd = () => {
+ _isDraggingSize = false;
+ schedulePreview();
+ };
+ widthSlider.addEventListener('pointerdown', markDragStart);
+ heightSlider.addEventListener('pointerdown', markDragStart);
+ widthSlider.addEventListener('pointerup', markDragEnd);
+ heightSlider.addEventListener('pointerup', markDragEnd);
+ widthSlider.addEventListener('input', () => {
+ onWidthInput();
+ schedulePreview();
+ });
+ heightSlider.addEventListener('input', () => {
+ onHeightInput();
+ schedulePreview();
+ });
+
+ // Mask painting UX: brush size, modes, row/column fills, and precise coords
+ let draggingMask = false;
+ let lastPaintX = -1,
+ lastPaintY = -1;
+ let brushSize = 1;
+ let rowColSize = 1;
+ let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle'
+ const brushEl = resizeContainer.querySelector('#maskBrushSize');
+ const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue');
+ const btnIgnore = resizeContainer.querySelector('#maskModeIgnore');
+ const btnUnignore = resizeContainer.querySelector('#maskModeUnignore');
+ const btnToggle = resizeContainer.querySelector('#maskModeToggle');
+ const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn');
+ const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn');
+ const rowColSizeEl = resizeContainer.querySelector('#rowColSize');
+ const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue');
+
+ const updateModeButtons = () => {
+ const map = [
+ [btnIgnore, 'ignore'],
+ [btnUnignore, 'unignore'],
+ [btnToggle, 'toggle'],
+ ];
+ for (const [el, m] of map) {
+ if (!el) continue;
+ const active = maskMode === m;
+ el.classList.toggle('active', active);
+ el.setAttribute('aria-pressed', active ? 'true' : 'false');
+ }
+ };
+ const setMode = (mode) => {
+ maskMode = mode;
+ updateModeButtons();
+ };
+ if (brushEl && brushValEl) {
+ brushEl.addEventListener('input', () => {
+ brushSize = parseInt(brushEl.value, 10) || 1;
+ brushValEl.textContent = brushSize;
+ });
+ brushValEl.textContent = brushEl.value;
+ brushSize = parseInt(brushEl.value, 10) || 1;
+ }
+ if (rowColSizeEl && rowColSizeValEl) {
+ rowColSizeEl.addEventListener('input', () => {
+ rowColSize = parseInt(rowColSizeEl.value, 10) || 1;
+ rowColSizeValEl.textContent = rowColSize;
+ });
+ rowColSizeValEl.textContent = rowColSizeEl.value;
+ rowColSize = parseInt(rowColSizeEl.value, 10) || 1;
+ }
+ if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore'));
+ if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore'));
+ if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle'));
+ // Initialize button state (default to toggle mode)
+ updateModeButtons();
+
+ const mapClientToPixel = (clientX, clientY) => {
+ // Compute without rounding until final step to avoid drift at higher zoom
+ const rect = baseCanvas.getBoundingClientRect();
+ const scaleX = rect.width / baseCanvas.width;
+ const scaleY = rect.height / baseCanvas.height;
+ const dx = (clientX - rect.left) / scaleX;
+ const dy = (clientY - rect.top) / scaleY;
+ const x = Math.floor(dx);
+ const y = Math.floor(dy);
+ return { x, y };
+ };
+
+ const ensureMask = (w, h) => {
+ if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) {
+ state.resizeIgnoreMask = new Uint8Array(w * h);
+ }
+ };
+
+ const paintCircle = (cx, cy, radius, value) => {
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ ensureMask(w, h);
+ const r2 = radius * radius;
+ for (let yy = cy - radius; yy <= cy + radius; yy++) {
+ if (yy < 0 || yy >= h) continue;
+ for (let xx = cx - radius; xx <= cx + radius; xx++) {
+ if (xx < 0 || xx >= w) continue;
+ const dx = xx - cx,
+ dy = yy - cy;
+ if (dx * dx + dy * dy <= r2) {
+ const idx = yy * w + xx;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) {
+ _maskData[p] = 255;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 150;
+ } else {
+ _maskData[p] = 0;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 0;
+ }
+ _markDirty(xx, yy);
+ }
+ }
+ }
+ }
+ };
+
+ const paintRow = (y, value) => {
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ ensureMask(w, h);
+ if (y < 0 || y >= h) return;
+
+ // Paint multiple rows based on rowColSize
+ const halfSize = Math.floor(rowColSize / 2);
+ const startY = Math.max(0, y - halfSize);
+ const endY = Math.min(h - 1, y + halfSize);
+
+ for (let rowY = startY; rowY <= endY; rowY++) {
+ for (let x = 0; x < w; x++) {
+ const idx = rowY * w + x;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) {
+ _maskData[p] = 255;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 150;
+ } else {
+ _maskData[p] = 0;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 0;
+ }
+ }
+ }
+ if (_maskData) {
+ _markDirty(0, rowY);
+ _markDirty(w - 1, rowY);
+ }
+ }
+ };
+
+ const paintColumn = (x, value) => {
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ ensureMask(w, h);
+ if (x < 0 || x >= w) return;
+
+ // Paint multiple columns based on rowColSize
+ const halfSize = Math.floor(rowColSize / 2);
+ const startX = Math.max(0, x - halfSize);
+ const endX = Math.min(w - 1, x + halfSize);
+
+ for (let colX = startX; colX <= endX; colX++) {
+ for (let y = 0; y < h; y++) {
+ const idx = y * w + colX;
+ let val = state.resizeIgnoreMask[idx];
+ if (maskMode === 'toggle') {
+ val = val ? 0 : 1;
+ } else if (maskMode === 'ignore') {
+ val = 1;
+ } else {
+ val = 0;
+ }
+ state.resizeIgnoreMask[idx] = val;
+ if (_maskData) {
+ const p = idx * 4;
+ if (val) {
+ _maskData[p] = 255;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 150;
+ } else {
+ _maskData[p] = 0;
+ _maskData[p + 1] = 0;
+ _maskData[p + 2] = 0;
+ _maskData[p + 3] = 0;
+ }
+ }
+ }
+ if (_maskData) {
+ _markDirty(colX, 0);
+ _markDirty(colX, h - 1);
+ }
+ }
+ };
+
+ const redrawMaskOverlay = () => {
+ // Only flush the dirty region; full rebuild happens on size change
+ _flushDirty();
+ };
+
+ const handlePaint = (e) => {
+ // Suppress painting while panning
+ if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return;
+ const { x, y } = mapClientToPixel(e.clientX, e.clientY);
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ if (x < 0 || y < 0 || x >= w || y >= h) return;
+ const radius = Math.max(1, Math.floor(brushSize / 2));
+ if (e.shiftKey) {
+ paintRow(y);
+ } else if (e.altKey) {
+ paintColumn(x);
+ } else {
+ paintCircle(x, y, radius);
+ }
+ lastPaintX = x;
+ lastPaintY = y;
+ redrawMaskOverlay();
+ };
+
+ maskCanvas.addEventListener('mousedown', (e) => {
+ if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage
+ draggingMask = true;
+ handlePaint(e);
+ });
+ // Avoid hijacking touch gestures for panning/zooming
+ maskCanvas.addEventListener(
+ 'touchstart',
+ (e) => {
+ /* let panStage handle */
+ },
+ { passive: true }
+ );
+ maskCanvas.addEventListener(
+ 'touchmove',
+ (e) => {
+ /* let panStage handle */
+ },
+ { passive: true }
+ );
+ maskCanvas.addEventListener(
+ 'touchend',
+ (e) => {
+ /* let panStage handle */
+ },
+ { passive: true }
+ );
+ window.addEventListener('mousemove', (e) => {
+ if (draggingMask) handlePaint(e);
+ });
+ window.addEventListener('mouseup', () => {
+ if (draggingMask) {
+ draggingMask = false;
+ saveBotSettings();
+ }
+ });
+
+ if (clearIgnoredBtnEl)
+ clearIgnoredBtnEl.addEventListener('click', () => {
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0);
+ _ensureMaskOverlayBuffers(w, h, true);
+ _updateResizePreview();
+ saveBotSettings();
+ });
+
+ if (invertMaskBtn)
+ invertMaskBtn.addEventListener('click', () => {
+ if (!state.resizeIgnoreMask) return;
+ for (let i = 0; i < state.resizeIgnoreMask.length; i++)
+ state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1;
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ _ensureMaskOverlayBuffers(w, h, true);
+ _updateResizePreview();
+ saveBotSettings();
+ });
+
+ confirmResize.onclick = async () => {
+ const newWidth = parseInt(widthSlider.value, 10);
+ const newHeight = parseInt(heightSlider.value, 10);
+
+ // Generate the final paletted image data
+ const tempCanvas = document.createElement('canvas');
+ const tempCtx = tempCanvas.getContext('2d');
+ tempCanvas.width = newWidth;
+ tempCanvas.height = newHeight;
+ tempCtx.imageSmoothingEnabled = false;
+ if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) {
+ await baseProcessor.load();
+ }
+ tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight);
+ const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight);
+ const data = imgData.data;
+ const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+ let totalValidPixels = 0;
+ const mask =
+ state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight
+ ? state.resizeIgnoreMask
+ : null;
+
+ const applyFSDitherFinal = async () => {
+ const w = newWidth,
+ h = newHeight;
+ const n = w * h;
+ const { work, eligible } = ensureDitherBuffers(n);
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ const i4 = idx * 4;
+ const r = data[i4],
+ g = data[i4 + 1],
+ b = data[i4 + 2],
+ a = data[i4 + 3];
+ const masked = mask && mask[idx];
+ const isEligible =
+ !masked &&
+ (state.paintTransparentPixels || a >= tThresh2) &&
+ (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b));
+ eligible[idx] = isEligible ? 1 : 0;
+ work[idx * 3] = r;
+ work[idx * 3 + 1] = g;
+ work[idx * 3 + 2] = b;
+ if (!isEligible) {
+ data[i4 + 3] = 0;
+ }
+ }
+ // Yield to keep UI responsive
+ if ((y & 15) === 0) await Promise.resolve();
+ }
+
+ const diffuse = (nx, ny, er, eg, eb, factor) => {
+ if (nx < 0 || nx >= w || ny < 0 || ny >= h) return;
+ const nidx = ny * w + nx;
+ if (!eligible[nidx]) return;
+ const base = nidx * 3;
+ work[base] = Math.min(255, Math.max(0, work[base] + er * factor));
+ work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor));
+ work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor));
+ };
+
+ for (let y = 0; y < h; y++) {
+ for (let x = 0; x < w; x++) {
+ const idx = y * w + x;
+ if (!eligible[idx]) continue;
+ const base = idx * 3;
+ const r0 = work[base],
+ g0 = work[base + 1],
+ b0 = work[base + 2];
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(
+ r0,
+ g0,
+ b0,
+ state.activeColorPalette
+ );
+ const i4 = idx * 4;
+ data[i4] = nr;
+ data[i4 + 1] = ng;
+ data[i4 + 2] = nb;
+ data[i4 + 3] = 255;
+ totalValidPixels++;
+
+ const er = r0 - nr;
+ const eg = g0 - ng;
+ const eb = b0 - nb;
+
+ diffuse(x + 1, y, er, eg, eb, 7 / 16);
+ diffuse(x - 1, y + 1, er, eg, eb, 3 / 16);
+ diffuse(x, y + 1, er, eg, eb, 5 / 16);
+ diffuse(x + 1, y + 1, er, eg, eb, 1 / 16);
+ }
+ // Yield every row to reduce jank
+ await Promise.resolve();
+ }
+ };
+
+ if (state.ditheringEnabled) {
+ await applyFSDitherFinal();
+ } else {
+ for (let i = 0; i < data.length; i += 4) {
+ const r = data[i],
+ g = data[i + 1],
+ b = data[i + 2],
+ a = data[i + 3];
+ const masked = mask && mask[i >> 2];
+ const isTransparent = (!state.paintTransparentPixels && a < tThresh2) || masked;
+ const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b);
+ if (isTransparent || isWhiteAndSkipped) {
+ data[i + 3] = 0; // overlay transparency
+ continue;
+ }
+ totalValidPixels++;
+ const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+ data[i] = nr;
+ data[i + 1] = ng;
+ data[i + 2] = nb;
+ data[i + 3] = 255;
+ }
+ }
+ tempCtx.putImageData(imgData, 0, 0);
+
+ // Save the final pixel data for painting
+ // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay
+ const palettedPixels = new Uint8ClampedArray(imgData.data);
+ state.imageData.pixels = palettedPixels;
+ state.imageData.width = newWidth;
+ state.imageData.height = newHeight;
+ state.imageData.totalPixels = totalValidPixels;
+ state.totalPixels = totalValidPixels;
+ state.paintedPixels = 0;
+
+ state.resizeSettings = {
+ baseWidth: width,
+ baseHeight: height,
+ width: newWidth,
+ height: newHeight,
+ };
+ saveBotSettings();
+
+ const finalImageBitmap = await createImageBitmap(tempCanvas);
+ await overlayManager.setImage(finalImageBitmap);
+ overlayManager.enable();
+ toggleOverlayBtn.classList.add('active');
+ toggleOverlayBtn.setAttribute('aria-pressed', 'true');
+
+ // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored
+
+ updateStats();
+ updateUI('resizeSuccess', 'success', {
+ width: newWidth,
+ height: newHeight,
+ });
+ closeResizeDialog();
+ };
+
+ downloadPreviewBtn.onclick = () => {
+ try {
+ const w = baseCanvas.width,
+ h = baseCanvas.height;
+ const out = document.createElement('canvas');
+ out.width = w;
+ out.height = h;
+ const octx = out.getContext('2d');
+ octx.imageSmoothingEnabled = false;
+ octx.drawImage(baseCanvas, 0, 0);
+ octx.drawImage(maskCanvas, 0, 0);
+ const link = document.createElement('a');
+ link.download = 'wplace-preview.png';
+ link.href = out.toDataURL();
+ link.click();
+ } catch (e) {
+ console.warn('Failed to download preview:', e);
+ }
+ };
+
+ cancelResize.onclick = closeResizeDialog;
+
+ resizeOverlay.style.display = 'block';
+ resizeContainer.style.display = 'block';
+
+ // Reinitialize color palette with current available colors
+ initializeColorPalette(resizeContainer);
+
+ _updateResizePreview();
+ _resizeDialogCleanup = () => {
+ try {
+ zoomSlider.replaceWith(zoomSlider.cloneNode(true));
+ } catch { }
+ try {
+ if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true));
+ } catch { }
+ try {
+ if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true));
+ } catch { }
+ };
+ setTimeout(() => {
+ if (typeof computeFitZoom === 'function') {
+ const z = computeFitZoom();
+ if (!isNaN(z) && isFinite(z)) {
+ applyZoom(z);
+ centerInView();
+ }
+ } else {
+ centerInView();
+ }
+ }, 0);
+ }
+
+ function closeResizeDialog() {
+ try {
+ if (typeof _resizeDialogCleanup === 'function') {
+ _resizeDialogCleanup();
+ }
+ } catch { }
+ resizeOverlay.style.display = 'none';
+ resizeContainer.style.display = 'none';
+ _updateResizePreview = () => { };
+ try {
+ if (typeof cancelAnimationFrame === 'function' && _panRaf) {
+ cancelAnimationFrame(_panRaf);
+ }
+ } catch { }
+ try {
+ if (_previewTimer) {
+ clearTimeout(_previewTimer);
+ _previewTimer = null;
+ }
+ } catch { }
+ _maskImageData = null;
+ _maskData = null;
+ _dirty = null;
+ _ditherWorkBuf = null;
+ _ditherEligibleBuf = null;
+ _resizeDialogCleanup = null;
+ }
+
+ if (uploadBtn) {
+ uploadBtn.addEventListener('click', async () => {
+ const availableColors = Utils.extractAvailableColors();
+ if (availableColors === null || availableColors.length < 10) {
+ updateUI('noColorsFound', 'error');
+ Utils.showAlert(Utils.t('noColorsFound'), 'error');
+ return;
+ }
+
+ if (!state.colorsChecked) {
+ state.availableColors = availableColors;
+ state.colorsChecked = true;
+ updateUI('colorsFound', 'success', { count: availableColors.length });
+ updateStats();
+ selectPosBtn.disabled = false;
+ // Only enable resize button if image is also loaded
+ if (state.imageLoaded) {
+ resizeBtn.disabled = false;
+ }
+ }
+
+ try {
+ updateUI('loadingImage', 'default');
+ const imageSrc = await Utils.createImageUploader();
+ if (!imageSrc) {
+ updateUI('colorsFound', 'success', {
+ count: state.availableColors.length,
+ });
+ return;
+ }
+
+ const processor = new ImageProcessor(imageSrc);
+ await processor.load();
+
+ const { width, height } = processor.getDimensions();
+ const pixels = processor.getPixelData();
+
+ let totalValidPixels = 0;
+ for (let i = 0; i < pixels.length; i += 4) {
+ const isTransparent =
+ !state.paintTransparentPixels &&
+ pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD);
+ const isWhiteAndSkipped =
+ !state.paintWhitePixels &&
+ Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]);
+ if (!isTransparent && !isWhiteAndSkipped) {
+ totalValidPixels++;
+ }
+ }
+
+ state.imageData = {
+ width,
+ height,
+ pixels,
+ totalPixels: totalValidPixels,
+ processor,
+ };
+
+ state.totalPixels = totalValidPixels;
+ state.paintedPixels = 0;
+ state.imageLoaded = true;
+ state.lastPosition = { x: 0, y: 0 };
+
+ // Initialize painted map for tracking
+ Utils.initializePaintedMap(width, height);
+
+ // New image: clear previous resize settings
+ state.resizeSettings = null;
+ // Also clear any previous ignore mask
+ state.resizeIgnoreMask = null;
+ // Save original image for this browser (dataUrl + dims)
+ state.originalImage = { dataUrl: imageSrc, width, height };
+ saveBotSettings();
+
+ // Use the original image for the overlay initially
+ const imageBitmap = await createImageBitmap(processor.img);
+ await overlayManager.setImage(imageBitmap);
+ overlayManager.enable();
+ toggleOverlayBtn.disabled = false;
+ toggleOverlayBtn.classList.add('active');
+ toggleOverlayBtn.setAttribute('aria-pressed', 'true');
+
+ // Only enable resize button if colors have also been captured
+ if (state.colorsChecked) {
+ resizeBtn.disabled = false;
+ }
+ saveBtn.disabled = false;
+
+ if (state.startPosition) {
+ startBtn.disabled = false;
+ }
+
+ updateStats();
+ updateDataButtons();
+ updateUI('imageLoaded', 'success', { count: totalValidPixels });
+ } catch {
+ updateUI('imageError', 'error');
+ }
+ });
+ }
+
+ if (resizeBtn) {
+ resizeBtn.addEventListener('click', () => {
+ if (state.imageLoaded && state.imageData.processor && state.colorsChecked) {
+ showResizeDialog(state.imageData.processor);
+ } else if (!state.colorsChecked) {
+ Utils.showAlert(Utils.t('uploadImageFirstColors'), 'warning');
+ }
+ });
+ }
+
+ if (selectPosBtn) {
+ selectPosBtn.addEventListener('click', async () => {
+ if (state.selectingPosition) return;
+
+ state.selectingPosition = true;
+ state.startPosition = null;
+ state.region = null;
+ startBtn.disabled = true;
+
+ Utils.showAlert(Utils.t('selectPositionAlert'), 'info');
+ updateUI('waitingPosition', 'default');
+
+ const tempFetch = async (url, options) => {
+ if (
+ typeof url === 'string' &&
+ url.includes('https://backend.wplace.live/s0/pixel/') &&
+ options?.method?.toUpperCase() === 'POST'
+ ) {
+ try {
+ const response = await originalFetch(url, options);
+ const clonedResponse = response.clone();
+ const data = await clonedResponse.json();
+
+ if (data?.painted === 1) {
+ const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/);
+ if (regionMatch && regionMatch.length >= 3) {
+ state.region = {
+ x: Number.parseInt(regionMatch[1]),
+ y: Number.parseInt(regionMatch[2]),
+ };
+ }
+
+ const payload = JSON.parse(options.body);
+ if (payload?.coords && Array.isArray(payload.coords)) {
+ state.startPosition = {
+ x: payload.coords[0],
+ y: payload.coords[1],
+ };
+ state.lastPosition = { x: 0, y: 0 };
+
+ await overlayManager.setPosition(state.startPosition, state.region);
+
+ if (state.imageLoaded) {
+ startBtn.disabled = false;
+ }
+
+ window.fetch = originalFetch;
+ state.selectingPosition = false;
+ updateUI('positionSet', 'success');
+ }
+ }
+
+ return response;
+ } catch {
+ return originalFetch(url, options);
+ }
+ }
+ return originalFetch(url, options);
+ };
+
+ const originalFetch = window.fetch;
+ window.fetch = tempFetch;
+
+ setTimeout(() => {
+ if (state.selectingPosition) {
+ window.fetch = originalFetch;
+ state.selectingPosition = false;
+ updateUI('positionTimeout', 'error');
+ Utils.showAlert(Utils.t('positionTimeout'), 'error');
+ }
+ }, 120000);
+ });
+ }
+
+ async function startPainting() {
+ if (!state.imageLoaded || !state.startPosition || !state.region) {
+ updateUI('missingRequirements', 'error');
+ return;
+ }
+ await ensureToken();
+ if (!turnstileToken) return;
+
+ state.running = true;
+ state.stopFlag = false;
+ startBtn.disabled = true;
+ stopBtn.disabled = false;
+ uploadBtn.disabled = true;
+ selectPosBtn.disabled = true;
+ resizeBtn.disabled = true;
+ saveBtn.disabled = true;
+ toggleOverlayBtn.disabled = true;
+
+ updateUI('startPaintingMsg', 'success');
+
+ try {
+ await getAccounts();
+ await processImage();
+ } catch (e) {
+ console.error('Unexpected error:', e);
+ updateUI('paintingError', 'error');
+ } finally {
+ state.running = false;
+ stopBtn.disabled = true;
+ saveBtn.disabled = false;
+
+ if (state.stopFlag) {
+ startBtn.disabled = false;
+ } else {
+ startBtn.disabled = true;
+ uploadBtn.disabled = false;
+ selectPosBtn.disabled = false;
+ resizeBtn.disabled = false;
+ }
+ toggleOverlayBtn.disabled = false;
+ }
+ }
+
+ if (startBtn) {
+ startBtn.addEventListener('click', startPainting);
+ }
+
+ if (stopBtn) {
+ stopBtn.addEventListener('click', () => {
+ state.stopFlag = true;
+ state.running = false;
+ stopBtn.disabled = true;
+ updateUI('paintingStoppedByUser', 'warning');
+
+ if (state.imageLoaded && state.paintedPixels > 0) {
+ Utils.saveProgress();
+ Utils.showAlert(Utils.t('autoSaved'), 'success');
+ }
+ });
+ }
+
+ const checkSavedProgress = () => {
+ const savedData = Utils.loadProgress();
+ if (savedData && savedData.state.paintedPixels > 0) {
+ const savedDate = new Date(savedData.timestamp).toLocaleString();
+ const progress = Math.round(
+ (savedData.state.paintedPixels / savedData.state.totalPixels) * 100
+ );
+
+ Utils.showAlert(
+ `${Utils.t('savedDataFound')}\n\n` +
+ `Saved: ${savedDate}\n` +
+ `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` +
+ `${Utils.t('clickLoadToContinue')}`,
+ 'info'
+ );
+ }
+ };
+
+ setTimeout(checkSavedProgress, 1000);
+
+ if (cooldownSlider && cooldownInput && cooldownValue && cooldownDecrease && cooldownIncrease) {
+ const updateCooldown = (newValue) => {
+ const threshold = Math.max(1, Math.min(state.maxCharges || 999, parseInt(newValue)));
+ state.cooldownChargeThreshold = threshold;
+
+ // Update both controls (value shows in input, label shows unit only)
+ cooldownSlider.value = threshold;
+ cooldownInput.value = threshold;
+ cooldownValue.textContent = `${Utils.t('charges')}`;
+
+ saveBotSettings();
+ NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change
+ };
+
+ // Slider event listener
+ cooldownSlider.addEventListener('input', (e) => {
+ updateCooldown(e.target.value);
+ });
+
+ // Number input event listener
+ cooldownInput.addEventListener('input', (e) => {
+ updateCooldown(e.target.value);
+ });
+
+ // Decrease button
+ cooldownDecrease.addEventListener('click', () => {
+ updateCooldown(parseInt(cooldownInput.value) - 1);
+ });
+
+ // Increase button
+ cooldownIncrease.addEventListener('click', () => {
+ updateCooldown(parseInt(cooldownInput.value) + 1);
+ });
+
+ // Add scroll-to-adjust for cooldown slider
+ Utils.createScrollToAdjust(cooldownSlider, updateCooldown, 1, state.maxCharges, 1);
+ }
+
+ loadBotSettings();
+ // Ensure notification poller reflects current settings
+ NotificationManager.syncFromState();
+ }
+
+ function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) {
+ const remainingCharges = target - current;
+ return Math.max(0, remainingCharges * cooldown - intervalMs);
+ }
+
+ function updateChargesThresholdUI(intervalMs) {
+ if (state.stopFlag) return;
+
+ const threshold = state.cooldownChargeThreshold;
+ const remainingMs = getMsToTargetCharges(
+ state.preciseCurrentCharges,
+ threshold,
+ state.cooldown,
+ intervalMs
+ );
+ const timeText = Utils.msToTimeText(remainingMs);
+
+ updateUI(
+ 'noChargesThreshold',
+ 'warning',
+ {
+ threshold,
+ current: state.displayCharges,
+ time: timeText,
+ },
+ true
+ );
+ }
+
+ function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight) {
+ const coords = [];
+ console.log(
+ 'Generating coordinates with \n mode:',
+ mode,
+ '\n direction:',
+ direction,
+ '\n snake:',
+ snake,
+ '\n blockWidth:',
+ blockWidth,
+ '\n blockHeight:',
+ blockHeight
+ );
+ // --------- Standard 4 corners traversal ----------
+ let xStart, xEnd, xStep;
+ let yStart, yEnd, yStep;
+ switch (direction) {
+ case 'top-left':
+ xStart = 0;
+ xEnd = width;
+ xStep = 1;
+ yStart = 0;
+ yEnd = height;
+ yStep = 1;
+ break;
+ case 'top-right':
+ xStart = width - 1;
+ xEnd = -1;
+ xStep = -1;
+ yStart = 0;
+ yEnd = height;
+ yStep = 1;
+ break;
+ case 'bottom-left':
+ xStart = 0;
+ xEnd = width;
+ xStep = 1;
+ yStart = height - 1;
+ yEnd = -1;
+ yStep = -1;
+ break;
+ case 'bottom-right':
+ xStart = width - 1;
+ xEnd = -1;
+ xStep = -1;
+ yStart = height - 1;
+ yEnd = -1;
+ yStep = -1;
+ break;
+ default:
+ throw new Error(`Unknown direction: ${direction}`);
+ }
+
+ // --------- Traversal modes ----------
+ if (mode === 'rows') {
+ for (let y = yStart; y !== yEnd; y += yStep) {
+ if (snake && (y - yStart) % 2 !== 0) {
+ for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) {
+ coords.push([x, y]);
+ }
+ } else {
+ for (let x = xStart; x !== xEnd; x += xStep) {
+ coords.push([x, y]);
+ }
+ }
+ }
+ } else if (mode === 'columns') {
+ for (let x = xStart; x !== xEnd; x += xStep) {
+ if (snake && (x - xStart) % 2 !== 0) {
+ for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) {
+ coords.push([x, y]);
+ }
+ } else {
+ for (let y = yStart; y !== yEnd; y += yStep) {
+ coords.push([x, y]);
+ }
+ }
+ }
+ } else if (mode === 'circle-out') {
+ const cx = Math.floor(width / 2);
+ const cy = Math.floor(height / 2);
+ const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy));
+
+ for (let r = 0; r <= maxRadius; r++) {
+ for (let y = cy - r; y <= cy + r; y++) {
+ for (let x = cx - r; x <= cx + r; x++) {
+ if (x >= 0 && x < width && y >= 0 && y < height) {
+ const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy));
+ if (dist === r) coords.push([x, y]);
+ }
+ }
+ }
+ }
+ } else if (mode === 'circle-in') {
+ const cx = Math.floor(width / 2);
+ const cy = Math.floor(height / 2);
+ const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy));
+
+ for (let r = maxRadius; r >= 0; r--) {
+ for (let y = cy - r; y <= cy + r; y++) {
+ for (let x = cx - r; x <= cx + r; x++) {
+ if (x >= 0 && x < width && y >= 0 && y < height) {
+ const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy));
+ if (dist === r) coords.push([x, y]);
+ }
+ }
+ }
+ }
+ } else if (mode === 'blocks' || mode === 'shuffle-blocks') {
+ const blocks = [];
+ for (let by = 0; by < height; by += blockHeight) {
+ for (let bx = 0; bx < width; bx += blockWidth) {
+ const block = [];
+ for (let y = by; y < Math.min(by + blockHeight, height); y++) {
+ for (let x = bx; x < Math.min(bx + blockWidth, width); x++) {
+ block.push([x, y]);
+ }
+ }
+ blocks.push(block);
+ }
+ }
+
+ if (mode === 'shuffle-blocks') {
+ // Simple Fisher-Yates shuffle
+ for (let i = blocks.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [blocks[i], blocks[j]] = [blocks[j], blocks[i]];
+ }
+ }
+
+ // Concatenate all blocks
+ for (const block of blocks) {
+ coords.push(...block);
+ }
+ } else {
+ throw new Error(`Unknown mode: ${mode}`);
+ }
+
+ return coords;
+ }
+
+ async function flushPixelBatch(pixelBatch) {
+ if (!pixelBatch || pixelBatch.pixels.length === 0) return true;
+
+ const batchSize = pixelBatch.pixels.length;
+ console.log(
+ `📦 Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})`
+ );
+ const success = await sendBatchWithRetry(
+ pixelBatch.pixels,
+ pixelBatch.regionX,
+ pixelBatch.regionY
+ );
+ if (success) {
+ pixelBatch.pixels.forEach((p) => {
+ state.paintedPixels++;
+ Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY);
+ });
+ state.fullChargeData = {
+ ...state.fullChargeData,
+ spentSinceShot: state.fullChargeData.spentSinceShot + batchSize,
+ };
+ updateStats();
+ updateUI('paintingProgress', 'default', {
+ painted: state.paintedPixels,
+ total: state.totalPixels,
+ });
+ Utils.performSmartSave();
+
+ if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) {
+ const delayPerPixel = 1000 / state.paintingSpeed;
+ const totalDelay = Math.max(100, delayPerPixel * batchSize);
+ await Utils.sleep(totalDelay);
+ }
+ } else {
+ console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
+ state.stopFlag = true;
+ updateUI('paintingBatchFailed', 'error');
+ }
+
+ pixelBatch.pixels = [];
+ return success;
+ }
+
+ async function processImage() {
+ const { width, height, pixels } = state.imageData;
+ const { x: startX, y: startY } = state.startPosition;
+ const { x: regionX, y: regionY } = state.region;
+
+ // todo force load tiles
+ const tilesReady = await overlayManager.waitForTiles(
+ regionX,
+ regionY,
+ width,
+ height,
+ startX,
+ startY,
+ 10000 // timeout 10s
+ );
+
+ if (!tilesReady) {
+ updateUI('overlayTilesNotLoaded', 'error');
+ state.stopFlag = true;
+ return;
+ }
+
+ let pixelBatch = null;
+ let skippedPixels = {
+ transparent: 0,
+ white: 0,
+ alreadyPainted: 0,
+ colorUnavailable: 0,
+ };
+
+ const transparencyThreshold =
+ state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+
+ function checkPixelEligibility(x, y) {
+ const idx = (y * width + x) * 4;
+ const r = pixels[idx],
+ g = pixels[idx + 1],
+ b = pixels[idx + 2],
+ a = pixels[idx + 3];
+
+ if (!state.paintTransparentPixels && a < transparencyThreshold)
+ return {
+ eligible: false,
+ reason: 'transparent',
+ };
+ if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))
+ return {
+ eligible: false,
+ reason: 'white',
+ };
+
+ let targetRgb = Utils.isWhitePixel(r, g, b)
+ ? [255, 255, 255]
+ : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette);
+
+ // Template color ID, normalized/mapped to the nearest available color in our palette.
+ // Example: template requires "Slate", but we only have "Dark Gray" available
+ // → mappedTargetColorId = ID of Dark Gray.
+ //
+ // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier
+ // because "Slate" was not found (null returned).
+ //
+ // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"),
+ // and we proceed with painting using that mapped color.
+ //
+ // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray),
+ // we skip painting, since template and canvas both resolve to the same available color (Dark Gray).
+ const mappedTargetColorId = Utils.resolveColor(
+ targetRgb,
+ state.availableColors,
+ !state.paintUnavailablePixels
+ );
+
+ // Technically, checking only `!mappedTargetColorId.id` would be enough,
+ // but combined with `state.paintUnavailablePixels` it makes the logic explicit:
+ // we only skip when the template color cannot be mapped AND strict mode is on.
+ if (!state.paintUnavailablePixels && !mappedTargetColorId.id) {
+ return {
+ eligible: false,
+ reason: 'colorUnavailable',
+ r,
+ g,
+ b,
+ a,
+ mappedColorId: mappedTargetColorId.id,
+ };
+ }
+ return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id };
+ }
+
+ function skipPixel(reason, id, rgb, x, y) {
+ if (reason !== 'transparent') {
+ console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`);
+ }
+ skippedPixels[reason]++;
+ }
+
+ try {
+ const coords = generateCoordinates(
+ width,
+ height,
+ state.coordinateMode,
+ state.coordinateDirection,
+ state.coordinateSnake,
+ state.blockWidth,
+ state.blockHeight
+ );
+
+ outerLoop: for (const [x, y] of coords) {
+ if (state.stopFlag) {
+ if (pixelBatch && pixelBatch.pixels.length > 0) {
+ console.log(
+ `🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels`
+ );
+ await flushPixelBatch(pixelBatch);
+ }
+ state.lastPosition = { x, y };
+ updateUI('paintingPaused', 'warning', { x, y });
+ // noinspection UnnecessaryLabelOnBreakStatementJS
+ break outerLoop;
+ }
+
+ const targetPixelInfo = checkPixelEligibility(x, y);
+ let absX = startX + x;
+ let absY = startY + y;
+
+ let adderX = Math.floor(absX / 1000);
+ let adderY = Math.floor(absY / 1000);
+ let pixelX = absX % 1000;
+ let pixelY = absY % 1000;
+
+ // Template color ID, normalized/mapped to the nearest available color in our palette.
+ // Example: template requires "Slate", but we only have "Dark Gray" available
+ // → mappedTargetColorId = ID of Dark Gray.
+ //
+ // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier
+ // because "Slate" was not found (null returned).
+ //
+ // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"),
+ // and we proceed with painting using that mapped color.
+ //
+ // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray),
+ // we skip painting, since template and canvas both resolve to the same available color (Dark Gray).
+ const targetMappedColorId = targetPixelInfo.mappedColorId;
+
+ if (!targetPixelInfo.eligible) {
+ skipPixel(
+ targetPixelInfo.reason,
+ targetMappedColorId,
+ [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b],
+ pixelX,
+ pixelY
+ );
+ continue;
+ }
+
+ // console.log(`[DEBUG] Pixel at (${pixelX}, ${pixelY}) eligible: RGB=${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b},
+ // alpha=${targetPixelInfo.a}, mappedColorId=${targetMappedColorId}`);
+
+ if (
+ !pixelBatch ||
+ pixelBatch.regionX !== regionX + adderX ||
+ pixelBatch.regionY !== regionY + adderY
+ ) {
+ if (pixelBatch && pixelBatch.pixels.length > 0) {
+ console.log(
+ `🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${regionX + adderX
+ },${regionY + adderY})`
+ );
+ const success = await flushPixelBatch(pixelBatch);
+
+ if (success) {
+ if (
+ CONFIG.PAINTING_SPEED_ENABLED &&
+ state.paintingSpeed > 0 &&
+ pixelBatch.pixels.length > 0
+ ) {
+ const batchDelayFactor = Math.max(1, 100 / state.paintingSpeed);
+ const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length);
+ await Utils.sleep(totalDelay);
+ }
+ updateStats();
+ } else {
+ console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
+ state.stopFlag = true;
+ updateUI('paintingBatchFailed', 'error');
+ // noinspection UnnecessaryLabelOnBreakStatementJS
+ break outerLoop;
+ }
+ }
+
+ pixelBatch = {
+ regionX: regionX + adderX,
+ regionY: regionY + adderY,
+ pixels: [],
+ };
+ }
+
+ try {
+ const tileKeyParts = [pixelBatch.regionX, pixelBatch.regionY];
+
+ const tilePixelRGBA = await overlayManager.getTilePixelColor(
+ tileKeyParts[0],
+ tileKeyParts[1],
+ pixelX,
+ pixelY
+ );
+
+ if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) {
+ // Resolve the actual canvas pixel color to the closest available color.
+ // (The raw canvas RGB [er, eg, eb] is mapped into state.availableColors)
+ // so that comparison is consistent with targetMappedColorId.
+ const mappedCanvasColor = Utils.resolveColor(
+ tilePixelRGBA.slice(0, 3),
+ state.availableColors
+ );
+ const isMatch = mappedCanvasColor.id === targetMappedColorId;
+ if (isMatch) {
+ skipPixel(
+ 'alreadyPainted',
+ targetMappedColorId,
+ [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b],
+ pixelX,
+ pixelY
+ );
+ continue;
+ }
+ console.debug(
+ `[COMPARE] Pixel at 📍 (${pixelX}, ${pixelY}) in region (${regionX + adderX
+ }, ${regionY + adderY})\n` +
+ ` ├── Current color: rgb(${tilePixelRGBA.slice(0, 3).join(', ')}) (id: ${mappedCanvasColor.id})\n` +
+ ` ├── Target color: rgb(${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b}) (id: ${targetMappedColorId})\n` +
+ ` └── Status: ${isMatch ? '✅ Already painted → SKIP' : '🔴 Needs paint → PAINT'
+ }\n`
+ );
+ }
+ } catch (e) {
+ console.error(`[DEBUG] Error checking existing pixel at (${pixelX}, ${pixelY}):`, e);
+ updateUI('paintingPixelCheckFailed', 'error', { x: pixelX, y: pixelY });
+ state.stopFlag = true;
+ // noinspection UnnecessaryLabelOnBreakStatementJS
+ break outerLoop;
+ }
+
+ pixelBatch.pixels.push({
+ x: pixelX,
+ y: pixelY,
+ color: targetMappedColorId,
+ localX: x,
+ localY: y,
+ });
+
+ const maxBatchSize = calculateBatchSize();
+ if (pixelBatch.pixels.length >= maxBatchSize) {
+ const modeText =
+ state.batchMode === 'random'
+ ? `random (${state.randomBatchMin}-${state.randomBatchMax})`
+ : 'normal';
+ console.log(
+ `📦 Sending batch with ${pixelBatch.pixels.length} pixels (mode: ${modeText}, target: ${maxBatchSize})`
+ );
+ const success = await flushPixelBatch(pixelBatch);
+ if (!success) {
+ console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
+ state.stopFlag = true;
+ updateUI('paintingBatchFailed', 'error');
+ // noinspection UnnecessaryLabelOnBreakStatementJS
+ break outerLoop;
+ }
+
+ pixelBatch.pixels = [];
+ }
+ if (!CONFIG.autoSwap) {
+ if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ await Utils.dynamicSleep(() => {
+ if (state.displayCharges >= state.cooldownChargeThreshold) {
+ NotificationManager.maybeNotifyChargesReached(true);
+ return 0;
+ }
+ if (state.stopFlag) return 0;
+ return getMsToTargetCharges(
+ state.preciseCurrentCharges,
+ state.cooldownChargeThreshold,
+ state.cooldown
+ );
+ });
+ }
+ }
+ else {
+ if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) {
+ console.log("⚠️ Charges too low, swapping to next account...");
+
+ const accounts = JSON.parse(localStorage.getItem("accounts")) || [];
+ if (accounts.length === 0) {
+ console.warn("❌ No accounts available, stopping painting.");
+ state.stopFlag = true;
+ return;
+ }
+
+ state.accountIndex = (state.accountIndex + 1) % accounts.length;
+ console.log("🔄 Switching to account index:", state.accountIndex);
+
+ const nextToken = accounts[state.accountIndex];
+ console.log("🔑 Next token:", nextToken);
+
+ if (!nextToken) {
+ console.warn("⚠️ Invalid token, skipping...");
+ return;
+ }
+
+ swapAccountTrigger(nextToken);
+
+ let maxRetries = 20;
+ let retryCount = 0;
+ let swapSuccess = false;
+
+ while (!swapSuccess && retryCount < maxRetries) {
+ console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`);
+
+ // Wait for a short period before checking.
+ await new Promise(resolve => setTimeout(resolve, 1000));
+
+ try {
+ await fetchAccount();
+
+ console.log("✅ Account swap confirmed.");
+ swapSuccess = true;
+ } catch (error) {
+ console.warn("❌ Account swap not yet successful. Retrying...", error);
+ retryCount++;
+ }
+ }
+
+ if (swapSuccess) {
+
+ const { charges, cooldown } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.cooldown = cooldown;
+ Utils.performSmartSave();
+ updateStats();
+ } else {
+ console.error("❌ Failed to swap account after multiple retries. Stopping loop.");
+ state.stopFlag = true;
+ }
+ }
+ }
+
+ if (state.stopFlag) {
+ // noinspection UnnecessaryLabelOnBreakStatementJS
+ break outerLoop;
+ }
+ }
+
+ if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) {
+ console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`);
+ const success = await flushPixelBatch(pixelBatch);
+ if (!success) {
+ console.warn(
+ `⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`
+ );
+ }
+ }
+ } finally {
+ if (window._chargesInterval) clearInterval(window._chargesInterval);
+ window._chargesInterval = null;
+ }
+
+ if (state.stopFlag) {
+ // Save progress when stopped to preserve painted map
+ Utils.saveProgress();
+ } else {
+ updateUI('paintingComplete', 'success', { count: state.paintedPixels });
+ state.lastPosition = { x: 0, y: 0 };
+ // Keep painted map until user starts new project
+ // state.paintedMap = null // Commented out to preserve data
+ Utils.saveProgress(); // Save final complete state
+ overlayManager.clear();
+ const toggleOverlayBtn = document.getElementById('toggleOverlayBtn');
+ if (toggleOverlayBtn) {
+ toggleOverlayBtn.classList.remove('active');
+ toggleOverlayBtn.disabled = true;
+ }
+ }
+
+ // Log skip statistics
+ console.log(`📊 Pixel Statistics:`);
+ console.log(` Painted: ${state.paintedPixels}`);
+ console.log(` Skipped - Transparent: ${skippedPixels.transparent}`);
+ console.log(` Skipped - White (disabled): ${skippedPixels.white}`);
+ console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`);
+ console.log(` Skipped - Color Unavailable: ${skippedPixels.colorUnavailable}`);
+ console.log(
+ ` Total processed: ${state.paintedPixels +
+ skippedPixels.transparent +
+ skippedPixels.white +
+ skippedPixels.alreadyPainted +
+ skippedPixels.colorUnavailable
+ }`
+ );
+
+ updateStats();
+ }
+
+ // Helper function to calculate batch size based on mode
+ function calculateBatchSize() {
+ let targetBatchSize;
+
+ if (state.batchMode === 'random') {
+ // Generate random batch size within the specified range
+ const min = Math.max(1, state.randomBatchMin);
+ const max = Math.max(min, state.randomBatchMax);
+ targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min;
+ console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`);
+ } else {
+ // Normal mode - use the fixed paintingSpeed value
+ targetBatchSize = state.paintingSpeed;
+ }
+
+ // Always limit by available charges
+ const maxAllowed = state.displayCharges;
+ const finalBatchSize = Math.min(targetBatchSize, maxAllowed);
+
+ return finalBatchSize;
+ }
+
+ // Helper function to retry batch until success with exponential backoff
+ async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) {
+ let attempt = 0;
+ while (attempt < maxRetries && !state.stopFlag) {
+ attempt++;
+ console.log(
+ `🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels`
+ );
+
+ const result = await sendPixelBatch(pixels, regionX, regionY);
+
+ if (result === true) {
+ console.log(`✅ Batch succeeded on attempt ${attempt}`);
+ return true;
+ } else if (result === 'token_error') {
+ console.log(`🔑 Token error on attempt ${attempt}, regenerating...`);
+ updateUI('captchaSolving', 'warning');
+ try {
+ await handleCaptcha();
+ // Don't count token regeneration as a failed attempt
+ attempt--;
+ continue;
+ } catch (e) {
+ console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e);
+ updateUI('captchaFailed', 'error');
+ // Wait longer before retrying after token failure
+ await Utils.sleep(5000);
+ }
+ } else {
+ console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`);
+ // Exponential backoff with jitter
+ const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s
+ const jitter = Math.random() * 1000; // Add up to 1s random delay
+ await Utils.sleep(baseDelay + jitter);
+ }
+ }
+
+ if (attempt >= maxRetries) {
+ console.error(
+ `❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.`
+ );
+ updateUI('paintingError', 'error');
+ return false;
+ }
+
+ return false;
+ }
+
+ async function sendPixelBatch(pixelBatch, regionX, regionY) {
+ let token = turnstileToken;
+
+ // Generate new token if we don't have one
+ if (!token) {
+ try {
+ console.log('🔑 Generating Turnstile token for pixel batch...');
+ token = await handleCaptcha();
+ turnstileToken = token; // Store for potential reuse
+ } catch (error) {
+ console.error('❌ Failed to generate Turnstile token:', error);
+ tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ return 'token_error';
+ }
+ }
+
+ const coords = new Array(pixelBatch.length * 2);
+ const colors = new Array(pixelBatch.length);
+ for (let i = 0; i < pixelBatch.length; i++) {
+ const pixel = pixelBatch[i];
+ coords[i * 2] = pixel.x;
+ coords[i * 2 + 1] = pixel.y;
+ colors[i] = pixel.color;
+ }
+
+ try {
+ const payload = { coords, colors, t: token, fp: fpStr32 };
+ var wasmtoken = await createWasmToken(regionX, regionY, payload);
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken },
+ credentials: 'include',
+ body: JSON.stringify(payload),
+ });
+
+ if (res.status === 403) {
+ let data = null;
+ try {
+ data = await res.json();
+ } catch (_) { }
+ console.error('❌ 403 Forbidden. Turnstile token might be invalid or expired.');
+
+ // Try to generate a new token and retry once
+ try {
+ console.log('🔄 Regenerating Turnstile token after 403...');
+ token = await handleCaptcha();
+ turnstileToken = token;
+
+ // Retry the request with new token
+ const retryPayload = { coords, colors, t: token, fp: fpStr32 };
+ var wasmtoken = await createWasmToken(regionX, regionY, retryPayload);
+ const retryRes = await fetch(
+ `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken },
+ credentials: 'include',
+ body: JSON.stringify(retryPayload),
+ }
+ );
+
+ if (retryRes.status === 403) {
+ turnstileToken = null;
+ tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ return 'token_error';
+ }
+
+ const retryData = await retryRes.json();
+ return retryData?.painted === pixelBatch.length;
+ } catch (retryError) {
+ console.error('❌ Token regeneration failed:', retryError);
+ turnstileToken = null;
+ tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ return 'token_error';
+ }
+ }
+
+ const data = await res.json();
+ return data?.painted === pixelBatch.length;
+ } catch (e) {
+ console.error('Batch paint request failed:', e);
+ return false;
+ }
+ }
+
+ function saveBotSettings() {
+ try {
+ const settings = {
+ paintingSpeed: state.paintingSpeed,
+ paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked,
+ batchMode: state.batchMode, // "normal" or "random"
+ randomBatchMin: state.randomBatchMin,
+ randomBatchMax: state.randomBatchMax,
+ cooldownChargeThreshold: state.cooldownChargeThreshold,
+ tokenSource: state.tokenSource, // "generator", "hybrid", or "manual"
+ minimized: state.minimized,
+ overlayOpacity: state.overlayOpacity,
+ blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked,
+ ditheringEnabled: state.ditheringEnabled,
+ colorMatchingAlgorithm: state.colorMatchingAlgorithm,
+ enableChromaPenalty: state.enableChromaPenalty,
+ chromaPenaltyWeight: state.chromaPenaltyWeight,
+ customTransparencyThreshold: state.customTransparencyThreshold,
+ customWhiteThreshold: state.customWhiteThreshold,
+ paintWhitePixels: state.paintWhitePixels,
+ paintTransparentPixels: state.paintTransparentPixels,
+ resizeSettings: state.resizeSettings,
+ paintUnavailablePixels: state.paintUnavailablePixels,
+ coordinateMode: state.coordinateMode,
+ coordinateDirection: state.coordinateDirection,
+ coordinateSnake: state.coordinateSnake,
+ blockWidth: state.blockWidth,
+ blockHeight: state.blockHeight, // Save ignore mask (as base64) with its dimensions
+ resizeIgnoreMask:
+ state.resizeIgnoreMask &&
+ state.resizeSettings &&
+ state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length
+ ? {
+ w: state.resizeSettings.width,
+ h: state.resizeSettings.height,
+ data: btoa(String.fromCharCode(...state.resizeIgnoreMask)),
+ }
+ : null, // Notifications
+ notificationsEnabled: state.notificationsEnabled,
+ notifyOnChargesReached: state.notifyOnChargesReached,
+ notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused,
+ notificationIntervalMinutes: state.notificationIntervalMinutes,
+ originalImage: state.originalImage,
+ };
+ CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled;
+ // AUTO_CAPTCHA_ENABLED is always true - no need to save/load
+
+ localStorage.setItem('wplace-bot-settings', JSON.stringify(settings));
+ } catch (e) {
+ console.warn('Could not save bot settings:', e);
+ }
+ }
+
+ function loadBotSettings() {
+ try {
+ const saved = localStorage.getItem('wplace-bot-settings');
+ if (!saved) return;
+ const settings = JSON.parse(saved);
+
+ state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT;
+ state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal"
+ state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN;
+ state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX;
+ state.cooldownChargeThreshold =
+ settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD;
+ state.tokenSource = settings.tokenSource || CONFIG.TOKEN_SOURCE; // Default to "generator"
+ state.minimized = settings.minimized ?? false;
+ CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled ?? false;
+ CONFIG.AUTO_CAPTCHA_ENABLED = settings.autoCaptchaEnabled ?? false;
+ state.overlayOpacity = settings.overlayOpacity ?? CONFIG.OVERLAY.OPACITY_DEFAULT;
+ state.blueMarbleEnabled = settings.blueMarbleEnabled ?? CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT;
+ state.ditheringEnabled = settings.ditheringEnabled ?? false;
+ state.colorMatchingAlgorithm = settings.colorMatchingAlgorithm || 'lab';
+ state.enableChromaPenalty = settings.enableChromaPenalty ?? true;
+ state.chromaPenaltyWeight = settings.chromaPenaltyWeight ?? 0.15;
+ state.customTransparencyThreshold =
+ settings.customTransparencyThreshold ?? CONFIG.TRANSPARENCY_THRESHOLD;
+ state.customWhiteThreshold = settings.customWhiteThreshold ?? CONFIG.WHITE_THRESHOLD;
+ state.paintWhitePixels = settings.paintWhitePixels ?? true;
+ state.paintTransparentPixels = settings.paintTransparentPixels ?? false;
+ state.resizeSettings = settings.resizeSettings ?? null;
+ state.originalImage = settings.originalImage ?? null;
+ state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE;
+ state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE;
+ state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION;
+ state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE;
+ state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH;
+ state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT;
+ // Notifications
+ state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED;
+ state.notifyOnChargesReached =
+ settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED;
+ state.notifyOnlyWhenUnfocused =
+ settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED;
+ state.notificationIntervalMinutes =
+ settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES;
+ // Restore ignore mask if dims match current resizeSettings
+ if (
+ settings.resizeIgnoreMask &&
+ settings.resizeIgnoreMask.data &&
+ state.resizeSettings &&
+ settings.resizeIgnoreMask.w === state.resizeSettings.width &&
+ settings.resizeIgnoreMask.h === state.resizeSettings.height
+ ) {
+ try {
+ const bin = atob(settings.resizeIgnoreMask.data);
+ const arr = new Uint8Array(bin.length);
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
+ state.resizeIgnoreMask = arr;
+ } catch {
+ state.resizeIgnoreMask = null;
+ }
+ } else {
+ state.resizeIgnoreMask = null;
+ }
+ // Initialize coordinate generation UI
+ const coordinateModeSelect = document.getElementById('coordinateModeSelect');
+ if (coordinateModeSelect) coordinateModeSelect.value = state.coordinateMode;
+
+ const coordinateDirectionSelect = document.getElementById('coordinateDirectionSelect');
+ if (coordinateDirectionSelect) coordinateDirectionSelect.value = state.coordinateDirection;
+
+ const coordinateSnakeToggle = document.getElementById('coordinateSnakeToggle');
+ if (coordinateSnakeToggle) coordinateSnakeToggle.checked = state.coordinateSnake;
+
+ const settingsContainer = document.getElementById('wplace-settings-container');
+ const directionControls = settingsContainer.querySelector('#directionControls');
+ const snakeControls = settingsContainer.querySelector('#snakeControls');
+ const blockControls = settingsContainer.querySelector('#blockControls');
+ Utils.updateCoordinateUI({
+ mode: state.coordinateMode,
+ directionControls,
+ snakeControls,
+ blockControls,
+ });
+
+ const paintUnavailablePixelsToggle = document.getElementById('paintUnavailablePixelsToggle');
+ if (paintUnavailablePixelsToggle) {
+ paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels;
+ }
+
+ const settingsPaintWhiteToggle = settingsContainer.querySelector('#settingsPaintWhiteToggle');
+ if (settingsPaintWhiteToggle) {
+ settingsPaintWhiteToggle.checked = state.paintWhitePixels;
+ }
+
+ const settingsPaintTransparentToggle = settingsContainer.querySelector(
+ '#settingsPaintTransparentToggle'
+ );
+ if (settingsPaintTransparentToggle) {
+ settingsPaintTransparentToggle.checked = state.paintTransparentPixels;
+ }
+
+ const speedSlider = document.getElementById('speedSlider');
+ const speedInput = document.getElementById('speedInput');
+ const speedValue = document.getElementById('speedValue');
+ if (speedSlider) speedSlider.value = state.paintingSpeed;
+ if (speedInput) speedInput.value = state.paintingSpeed;
+ if (speedValue) speedValue.textContent = `pixels`;
+
+ const enableSpeedToggle = document.getElementById('enableSpeedToggle');
+ if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED;
+
+ // Batch mode UI initialization
+ const batchModeSelect = document.getElementById('batchModeSelect');
+ if (batchModeSelect) batchModeSelect.value = state.batchMode;
+
+ const normalBatchControls = document.getElementById('normalBatchControls');
+ const randomBatchControls = document.getElementById('randomBatchControls');
+
+ // Show/hide appropriate controls based on batch mode
+ if (normalBatchControls && randomBatchControls) {
+ if (state.batchMode === 'random') {
+ normalBatchControls.style.display = 'none';
+ randomBatchControls.style.display = 'block';
+ } else {
+ normalBatchControls.style.display = 'block';
+ randomBatchControls.style.display = 'none';
+ }
+ }
+
+ const randomBatchMin = document.getElementById('randomBatchMin');
+ if (randomBatchMin) randomBatchMin.value = state.randomBatchMin;
+
+ const randomBatchMax = document.getElementById('randomBatchMax');
+ if (randomBatchMax) randomBatchMax.value = state.randomBatchMax;
+
+ // AUTO_CAPTCHA_ENABLED is always true - no toggle to set
+
+ const cooldownSlider = document.getElementById('cooldownSlider');
+ const cooldownInput = document.getElementById('cooldownInput');
+ const cooldownValue = document.getElementById('cooldownValue');
+ if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold;
+ if (cooldownInput) cooldownInput.value = state.cooldownChargeThreshold;
+ if (cooldownValue) cooldownValue.textContent = `${Utils.t('charges')}`;
+
+ const overlayOpacitySlider = document.getElementById('overlayOpacitySlider');
+ if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity;
+ const overlayOpacityValue = document.getElementById('overlayOpacityValue');
+ if (overlayOpacityValue)
+ overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`;
+ const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle');
+ if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled;
+
+ const tokenSourceSelect = document.getElementById('tokenSourceSelect');
+ if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource;
+
+ const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect');
+ if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm;
+ const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle');
+ if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty;
+ const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider');
+ if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight;
+ const chromaWeightValue = document.getElementById('chromaWeightValue');
+ if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight;
+ const transparencyThresholdInput = document.getElementById('transparencyThresholdInput');
+ if (transparencyThresholdInput)
+ transparencyThresholdInput.value = state.customTransparencyThreshold;
+ const whiteThresholdInput = document.getElementById('whiteThresholdInput');
+ if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold;
+ // Notifications UI
+ const notifEnabledToggle = document.getElementById('notifEnabledToggle');
+ if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled;
+ const notifOnChargesToggle = document.getElementById('notifOnChargesToggle');
+ if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached;
+ const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle');
+ if (notifOnlyUnfocusedToggle)
+ notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused;
+ const notifIntervalInput = document.getElementById('notifIntervalInput');
+ if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes;
+ NotificationManager.resetEdgeTracking();
+ } catch (e) {
+ console.warn('Could not load bot settings:', e);
+ }
+ }
+
+ // Initialize Turnstile generator integration
+ console.log('🚀 WPlace Auto-Image with Turnstile Token Generator loaded');
+ console.log('🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)');
+ console.log('🎯 Manual pixel captcha solving: Available as fallback/alternative');
+ console.log('📱 Turnstile widgets: DISABLED - pure background token generation only!');
+
+ // Function to enable file operations after initial startup setup is complete
+ function enableFileOperations() {
+ state.initialSetupComplete = true;
+
+ const loadBtn = document.querySelector('#loadBtn');
+ const loadFromFileBtn = document.querySelector('#loadFromFileBtn');
+ const uploadBtn = document.querySelector('#uploadBtn');
+
+ if (loadBtn) {
+ loadBtn.disabled = false;
+ loadBtn.title = '';
+ // Add a subtle animation to indicate the button is now available
+ loadBtn.style.animation = 'pulse 0.6s ease-in-out';
+ setTimeout(() => {
+ if (loadBtn) loadBtn.style.animation = '';
+ }, 600);
+ console.log('✅ Load Progress button enabled after initial setup');
+ }
+
+ if (loadFromFileBtn) {
+ loadFromFileBtn.disabled = false;
+ loadFromFileBtn.title = '';
+ // Add a subtle animation to indicate the button is now available
+ loadFromFileBtn.style.animation = 'pulse 0.6s ease-in-out';
+ setTimeout(() => {
+ if (loadFromFileBtn) loadFromFileBtn.style.animation = '';
+ }, 600);
+ console.log('✅ Load from File button enabled after initial setup');
+ }
+
+ if (uploadBtn) {
+ uploadBtn.disabled = false;
+ uploadBtn.title = '';
+ // Add a subtle animation to indicate the button is now available
+ uploadBtn.style.animation = 'pulse 0.6s ease-in-out';
+ setTimeout(() => {
+ if (uploadBtn) uploadBtn.style.animation = '';
+ }, 600);
+ console.log('✅ Upload Image button enabled after initial setup');
+ }
+
+ // Show a notification that file operations are now available
+ Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success');
+ }
+
+ // Optimized token initialization with better timing and error handling
+ async function initializeTokenGenerator() {
+ // Skip if already have valid token
+ if (isTokenValid()) {
+ console.log('✅ Valid token already available, skipping initialization');
+ updateUI('tokenReady', 'success');
+ enableFileOperations(); // Enable file operations since initial setup is complete
+ return;
+ }
+
+ try {
+ console.log('🔧 Initializing Turnstile token generator...');
+ updateUI('initializingToken', 'default');
+
+ console.log('Attempting to load Turnstile script...');
+ await Utils.loadTurnstile();
+ console.log('Turnstile script loaded. Attempting to generate token...');
+
+ const token = await handleCaptchaWithRetry();
+ if (token) {
+ setTurnstileToken(token);
+ console.log('✅ Startup token generated successfully');
+ updateUI('tokenReady', 'success');
+ Utils.showAlert(Utils.t('tokenGeneratorReady'), 'success');
+ enableFileOperations(); // Enable file operations since initial setup is complete
+ } else {
+ console.warn(
+ '⚠️ Startup token generation failed (no token received), will retry when needed'
+ );
+ updateUI('tokenRetryLater', 'warning');
+ // Still enable file operations even if initial token generation fails
+ // Users can load progress and use manual/hybrid modes
+ enableFileOperations();
+ }
+ } catch (error) {
+ console.error('❌ Critical error during Turnstile initialization:', error); // More specific error
+ updateUI('tokenRetryLater', 'warning');
+ // Still enable file operations even if initial setup fails
+ // Users can load progress and use manual/hybrid modes
+ enableFileOperations();
+ // Don't show error alert for initialization failures, just log them
+ }
+ }
+
+ // Load theme preference immediately on startup before creating UI
+ loadThemePreference();
+ applyTheme();
+
+ var pawtect_chunk = null;
+
+ //find module if pawtect_chunk is null
+ pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm");
+
+ async function createWasmToken(regionX, regionY, payload) {
+ try {
+ // Load the Pawtect module and WASM
+ const mod = await import(new URL('/_app/immutable/chunks/' + pawtect_chunk, location.origin).href);
+ let wasm;
+ try {
+ wasm = await mod._();
+ console.log('✅ WASM initialized successfully');
+ } catch (wasmError) {
+ console.error('❌ WASM initialization failed:', wasmError);
+ return null;
+ }
+ try {
+ try {
+ const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null);
+ if (me?.id) {
+ mod.i(me.id);
+ console.log('✅ user ID set:', me.id);
+ }
+ } catch { }
+ } catch (userIdError) {
+ console.log('⚠️ Error setting user ID:', userIdError.message);
+ }
+ try {
+ const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`;
+ if (mod.r) {
+ mod.r(testUrl);
+ console.log('✅ Request URL set:', testUrl);
+ } else {
+ console.log('⚠️ request_url function (mod.r) not available');
+ }
+ } catch (urlError) {
+ console.log('⚠️ Error setting request URL:', urlError.message);
+ }
+
+ // Create test payload
+
+ console.log('📝 payload:', payload);
+
+ // Encode payload
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const bodyStr = JSON.stringify(payload);
+ const bytes = enc.encode(bodyStr);
+ console.log('📏 Payload size:', bytes.length, 'bytes');
+ console.log('📄 Payload string:', bodyStr);
+
+ // Allocate WASM memory with validation
+ let inPtr;
+ try {
+ if (!wasm.__wbindgen_malloc) {
+ console.error('❌ __wbindgen_malloc function not found');
+ return null;
+ }
+
+ inPtr = wasm.__wbindgen_malloc(bytes.length, 1);
+ console.log('✅ WASM memory allocated, pointer:', inPtr);
+
+ // Copy data to WASM memory
+ const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length);
+ wasmBuffer.set(bytes);
+ console.log('✅ Data copied to WASM memory');
+ } catch (memError) {
+ console.error('❌ Memory allocation error:', memError);
+ return null;
+ }
+
+ // Call the WASM function
+ console.log('🚀 Calling get_pawtected_endpoint_payload...');
+ let outPtr, outLen, token;
+ try {
+ const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length);
+ console.log('✅ Function called, result type:', typeof result, result);
+
+ if (Array.isArray(result) && result.length === 2) {
+ [outPtr, outLen] = result;
+ console.log('✅ Got output pointer:', outPtr, 'length:', outLen);
+
+ // Decode the result
+ const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen);
+ token = dec.decode(outputBuffer);
+ console.log('✅ Token decoded successfully');
+ } else {
+ console.error('❌ Unexpected function result format:', result);
+ return null;
+ }
+ } catch (funcError) {
+ console.error('❌ Function call error:', funcError);
+ console.error('Stack trace:', funcError.stack);
+ return null;
+ }
+
+ // Cleanup memory
+ try {
+ if (wasm.__wbindgen_free && outPtr && outLen) {
+ wasm.__wbindgen_free(outPtr, outLen, 1);
+ console.log('✅ Output memory freed');
+ }
+ if (wasm.__wbindgen_free && inPtr) {
+ wasm.__wbindgen_free(inPtr, bytes.length, 1);
+ console.log('✅ Input memory freed');
+ }
+ } catch (cleanupError) {
+ console.log('⚠️ Cleanup warning:', cleanupError.message);
+ }
+
+ // Display results
+ console.log('');
+ console.log('🎉 SUCCESS!');
+ console.log('🔑 Full token:');
+ console.log(token);
+ return token;
+ } catch (error) {
+ console.error('❌ Failed to generate fp parameter:', error);
+ return null;
+ }
+ }
+
+ async function findTokenModule(str) {
+ console.log('🔎 Searching for wasm Module...');
+ const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]'));
+
+ for (const link of links) {
+ try {
+ const url = new URL(link.getAttribute("href"), location.origin).href;
+ const code = await fetch(url).then(r => r.text());
+ if (code.includes(str)) {
+ console.log('✅ Found wasm Module...');
+ return url.split('/').pop();
+ }
+ } catch { }
+ }
+ console.error(`❌ Could not find Pawtect chunk: `, error);
+ }
+
+ async function purchase(type) {
+ // loadThemePreference()
+ let id;
+ let chargeMultiplier;
+ if (type === "max_charges") {
+ id = 70;
+ chargeMultiplier = 5;
+ } else if (type === "paint_charges") {
+ id = 80;
+ chargeMultiplier = 30;
+ } else {
+ console.error("Error: Invalid purchase type provided.");
+ return;
+ }
+ const { droplets } = await WPlaceService.getCharges();
+ console.log("There are currently : ", droplets, "droplets.");
+ try {
+ const amounts = Math.floor(droplets / 500);
+ if (amounts < 1) {
+ console.log("Not enough droplets to purchase.");
+ return;
+ }
+ const payload = {
+ "product": {
+ "id": id,
+ "amount": amounts
+ }
+ };
+ const res = fetch("https://backend.wplace.live/purchase", {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/plain;charset=UTF-8"
+ },
+ body: JSON.stringify(payload),
+ credentials: "include"
+ });
+ console.log("Fetch POST return :", res);
+ const { droplets: newDroplets } = await WPlaceService.getCharges();
+ if (droplets != newDroplets) {
+ console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), ".");
+ }
+ else {
+ console.log("Failed to buy charges");
+ }
+ } catch (e) {
+ console.error("An error occurred during the purchase:", e);
+ }
+ }
+ function swapAccountTrigger(token) {
+ localStorage.removeItem("lp");
+ if (!token) return;
+ console.log("Sending token to extension...");
+ window.postMessage({
+ source: 'my-userscript',
+ type: 'setCookie',
+ value: token
+ }, '*');
+ }
+ async function getAccounts() {
+ return new Promise((resolve, reject) => {
+ console.log("Requesting accounts from extension...");
+ // Ask extension for accounts
+ window.postMessage({
+ source: "my-userscript",
+ type: "getAccounts"
+ }, "*");
+ function handler(event) {
+ if (event.source !== window) return;
+ if (event.data.source !== "extension") return;
+ if (event.data.type === "accountsData") {
+ window.removeEventListener("message", handler);
+ // Save to localStorage
+ try {
+ localStorage.setItem("accounts", JSON.stringify(event.data.accounts));
+ console.log("✅ Accounts saved to localStorage:", event.data.accounts);
+ } catch (e) {
+ console.error("❌ Failed to save accounts:", e);
+ }
+ resolve(event.data.accounts);
+ }
+ }
+ window.addEventListener("message", handler);
+ });
+ }
+ async function fetchAccount() {
+ const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck();
+ console.log("User's ID :", ID);
+ console.log("User's Charges :", Charges, "/", Max);
+ console.log("User's Droplets :", Droplets);
+ }
+
+ createUI().then(() => {
+ // Generate token automatically after UI is ready
+ setTimeout(initializeTokenGenerator, 1000);
+
+ // Attach advanced color matching listeners (resize dialog)
+ const advancedInit = () => {
+ const chromaSlider = document.getElementById('chromaPenaltyWeightSlider');
+ const chromaValue = document.getElementById('chromaWeightValue');
+ const resetBtn = document.getElementById('resetAdvancedColorBtn');
+ const algoSelect = document.getElementById('colorAlgorithmSelect');
+ const chromaToggle = document.getElementById('enableChromaPenaltyToggle');
+ const transInput = document.getElementById('transparencyThresholdInput');
+ const whiteInput = document.getElementById('whiteThresholdInput');
+ const ditherToggle = document.getElementById('enableDitheringToggle');
+ if (algoSelect)
+ algoSelect.addEventListener('change', (e) => {
+ state.colorMatchingAlgorithm = e.target.value;
+ saveBotSettings();
+ _updateResizePreview();
+ });
+ if (chromaToggle)
+ chromaToggle.addEventListener('change', (e) => {
+ state.enableChromaPenalty = e.target.checked;
+ saveBotSettings();
+ _updateResizePreview();
+ });
+ if (chromaSlider && chromaValue)
+ chromaSlider.addEventListener('input', (e) => {
+ state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15;
+ chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2);
+ saveBotSettings();
+ _updateResizePreview();
+ });
+ if (transInput)
+ transInput.addEventListener('change', (e) => {
+ const v = parseInt(e.target.value, 10);
+ if (!isNaN(v) && v >= 0 && v <= 255) {
+ state.customTransparencyThreshold = v;
+ CONFIG.TRANSPARENCY_THRESHOLD = v;
+ saveBotSettings();
+ _updateResizePreview();
+ }
+ });
+ if (whiteInput)
+ whiteInput.addEventListener('change', (e) => {
+ const v = parseInt(e.target.value, 10);
+ if (!isNaN(v) && v >= 200 && v <= 255) {
+ state.customWhiteThreshold = v;
+ CONFIG.WHITE_THRESHOLD = v;
+ saveBotSettings();
+ _updateResizePreview();
+ }
+ });
+ if (ditherToggle)
+ ditherToggle.addEventListener('change', (e) => {
+ state.ditheringEnabled = e.target.checked;
+ saveBotSettings();
+ _updateResizePreview();
+ });
+ if (resetBtn)
+ resetBtn.addEventListener('click', () => {
+ state.colorMatchingAlgorithm = 'lab';
+ state.enableChromaPenalty = true;
+ state.chromaPenaltyWeight = 0.15;
+ state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100;
+ state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250;
+ saveBotSettings();
+ const a = document.getElementById('colorAlgorithmSelect');
+ if (a) a.value = 'lab';
+ const ct = document.getElementById('enableChromaPenaltyToggle');
+ if (ct) ct.checked = true;
+ if (chromaSlider) chromaSlider.value = 0.15;
+ if (chromaValue) chromaValue.textContent = '0.15';
+ if (transInput) transInput.value = 100;
+ if (whiteInput) whiteInput.value = 250;
+ _updateResizePreview();
+ Utils.showAlert(Utils.t('advancedColorSettingsReset'), 'success');
+ });
+ };
+ // Delay to ensure resize UI built
+ setTimeout(advancedInit, 500);
+
+ // Add cleanup on page unload
+ window.addEventListener('beforeunload', () => {
+ Utils.cleanupTurnstile();
+ });
+ });
+})();
diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js
new file mode 100644
index 0000000..7cd9677
--- /dev/null
+++ b/Extension/scripts/Auto-Repair.js
@@ -0,0 +1,3316 @@
+// eslint-disable-next-line prettier/prettier
+; (async () => {
+ // CONFIGURATION CONSTANTS
+ const CONFIG = {
+ COOLDOWN_DEFAULT: 31000,
+ TRANSPARENCY_THRESHOLD: 100,
+ WHITE_THRESHOLD: 250,
+ LOG_INTERVAL: 10,
+ PAINTING_SPEED: {
+ MIN: 1,
+ MAX: 1000,
+ DEFAULT: 5,
+ },
+ TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid"
+ AUTONOMOUS_MODE: true, // Enable autonomous operation
+ AUTO_TOKEN_REFRESH: true, // Automatically refresh tokens
+ TOKEN_PRELOAD_BUFFER: 60000, // Preload tokens 1 minute before expiry
+ MAX_RETRIES: 10,
+ RETRY_DELAY_BASE: 1000,
+ // Auto-batch configuration
+ AUTO_BATCH_ENABLED: true, // Enable auto pixel batch sizing
+ MAX_BATCH_SIZE: 500, // Maximum pixels per batch
+ MIN_BATCH_SIZE: 1, // Minimum pixels per batch
+ BATCH_OPTIMIZATION: true, // Optimize batch size based on detected damage
+ COLOR_MAP: {
+ 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } },
+ 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } },
+ 2: { id: 3, name: 'Gray', rgb: { r: 120, g: 120, b: 120 } },
+ 3: { id: 4, name: 'Light Gray', rgb: { r: 210, g: 210, b: 210 } },
+ 4: { id: 5, name: 'White', rgb: { r: 255, g: 255, b: 255 } },
+ 5: { id: 6, name: 'Deep Red', rgb: { r: 96, g: 0, b: 24 } },
+ 6: { id: 7, name: 'Red', rgb: { r: 237, g: 28, b: 36 } },
+ 7: { id: 8, name: 'Orange', rgb: { r: 255, g: 127, b: 39 } },
+ 8: { id: 9, name: 'Gold', rgb: { r: 246, g: 170, b: 9 } },
+ 9: { id: 10, name: 'Yellow', rgb: { r: 249, g: 221, b: 59 } },
+ 10: { id: 11, name: 'Light Yellow', rgb: { r: 255, g: 250, b: 188 } },
+ 11: { id: 12, name: 'Dark Green', rgb: { r: 14, g: 185, b: 104 } },
+ 12: { id: 13, name: 'Green', rgb: { r: 19, g: 230, b: 123 } },
+ 13: { id: 14, name: 'Light Green', rgb: { r: 135, g: 255, b: 94 } },
+ 14: { id: 15, name: 'Dark Teal', rgb: { r: 12, g: 129, b: 110 } },
+ 15: { id: 16, name: 'Teal', rgb: { r: 16, g: 174, b: 166 } },
+ 16: { id: 17, name: 'Light Teal', rgb: { r: 19, g: 225, b: 190 } },
+ 17: { id: 20, name: 'Cyan', rgb: { r: 96, g: 247, b: 242 } },
+ 18: { id: 44, name: 'Light Cyan', rgb: { r: 187, g: 250, b: 242 } },
+ 19: { id: 18, name: 'Dark Blue', rgb: { r: 40, g: 80, b: 158 } },
+ 20: { id: 19, name: 'Blue', rgb: { r: 64, g: 147, b: 228 } },
+ 21: { id: 21, name: 'Indigo', rgb: { r: 107, g: 80, b: 246 } },
+ 22: { id: 22, name: 'Light Indigo', rgb: { r: 153, g: 177, b: 251 } },
+ 23: { id: 23, name: 'Dark Purple', rgb: { r: 120, g: 12, b: 153 } },
+ 24: { id: 24, name: 'Purple', rgb: { r: 170, g: 56, b: 185 } },
+ 25: { id: 25, name: 'Light Purple', rgb: { r: 224, g: 159, b: 249 } },
+ 26: { id: 26, name: 'Dark Pink', rgb: { r: 203, g: 0, b: 122 } },
+ 27: { id: 27, name: 'Pink', rgb: { r: 236, g: 31, b: 128 } },
+ 28: { id: 28, name: 'Light Pink', rgb: { r: 243, g: 141, b: 169 } },
+ 29: { id: 29, name: 'Dark Brown', rgb: { r: 104, g: 70, b: 52 } },
+ 30: { id: 30, name: 'Brown', rgb: { r: 149, g: 104, b: 42 } },
+ 31: { id: 31, name: 'Beige', rgb: { r: 248, g: 178, b: 119 } },
+ 32: { id: 52, name: 'Light Beige', rgb: { r: 255, g: 197, b: 165 } },
+ 33: { id: 32, name: 'Medium Gray', rgb: { r: 170, g: 170, b: 170 } },
+ 34: { id: 33, name: 'Dark Red', rgb: { r: 165, g: 14, b: 30 } },
+ 35: { id: 34, name: 'Light Red', rgb: { r: 250, g: 128, b: 114 } },
+ 36: { id: 35, name: 'Dark Orange', rgb: { r: 228, g: 92, b: 26 } },
+ 37: { id: 37, name: 'Dark Goldenrod', rgb: { r: 156, g: 132, b: 49 } },
+ 38: { id: 38, name: 'Goldenrod', rgb: { r: 197, g: 173, b: 49 } },
+ 39: { id: 39, name: 'Light Goldenrod', rgb: { r: 232, g: 212, b: 95 } },
+ 40: { id: 40, name: 'Dark Olive', rgb: { r: 74, g: 107, b: 58 } },
+ 41: { id: 41, name: 'Olive', rgb: { r: 90, g: 148, b: 74 } },
+ 42: { id: 42, name: 'Light Olive', rgb: { r: 132, g: 197, b: 115 } },
+ 43: { id: 43, name: 'Dark Cyan', rgb: { r: 15, g: 121, b: 159 } },
+ 44: { id: 45, name: 'Light Blue', rgb: { r: 125, g: 199, b: 255 } },
+ 45: { id: 46, name: 'Dark Indigo', rgb: { r: 77, g: 49, b: 184 } },
+ 46: { id: 47, name: 'Dark Slate Blue', rgb: { r: 74, g: 66, b: 132 } },
+ 47: { id: 48, name: 'Slate Blue', rgb: { r: 122, g: 113, b: 196 } },
+ 48: { id: 49, name: 'Light Slate Blue', rgb: { r: 181, g: 174, b: 241 } },
+ 49: { id: 53, name: 'Dark Peach', rgb: { r: 155, g: 82, b: 73 } },
+ 50: { id: 54, name: 'Peach', rgb: { r: 209, g: 128, b: 120 } },
+ 51: { id: 55, name: 'Light Peach', rgb: { r: 250, g: 182, b: 164 } },
+ 52: { id: 50, name: 'Light Brown', rgb: { r: 219, g: 164, b: 99 } },
+ 53: { id: 56, name: 'Dark Tan', rgb: { r: 123, g: 99, b: 82 } },
+ 54: { id: 57, name: 'Tan', rgb: { r: 156, g: 132, b: 107 } },
+ 55: { id: 36, name: 'Light Tan', rgb: { r: 214, g: 181, b: 148 } },
+ 56: { id: 51, name: 'Dark Beige', rgb: { r: 209, g: 128, b: 81 } },
+ 57: { id: 61, name: 'Dark Stone', rgb: { r: 109, g: 100, b: 63 } },
+ 58: { id: 62, name: 'Stone', rgb: { r: 148, g: 140, b: 107 } },
+ 59: { id: 63, name: 'Light Stone', rgb: { r: 205, g: 197, b: 158 } },
+ 60: { id: 58, name: 'Dark Slate', rgb: { r: 51, g: 57, b: 65 } },
+ 61: { id: 59, name: 'Slate', rgb: { r: 109, g: 117, b: 141 } },
+ 62: { id: 60, name: 'Light Slate', rgb: { r: 179, g: 185, b: 209 } },
+ 63: { id: 0, name: 'Transparent', rgb: null },
+ },
+ };
+
+ // GLOBAL STATE
+ const state = {
+ running: false,
+ imageLoaded: false,
+ totalPixels: 0,
+ paintedPixels: 0,
+ availableColors: [],
+ displayCharges: 0,
+ maxCharges: 1,
+ cooldown: CONFIG.COOLDOWN_DEFAULT,
+ imageData: null,
+ stopFlag: false,
+ startPosition: null,
+ region: null,
+ paintWhitePixels: true,
+ paintTransparentPixels: true, // Changed to true to fix transparent pixel detection
+ autoRepairEnabled: false,
+ autoRepairInterval: 30,
+ autoRepairTimer: null,
+ debugLogs: [],
+ customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD,
+ customWhiteThreshold: CONFIG.WHITE_THRESHOLD,
+ tokenSource: CONFIG.TOKEN_SOURCE,
+ autonomousMode: CONFIG.AUTONOMOUS_MODE,
+ autoTokenRefresh: CONFIG.AUTO_TOKEN_REFRESH,
+ tokenPreloadBuffer: CONFIG.TOKEN_PRELOAD_BUFFER,
+ retryCount: 0,
+ tokenRetryTimer: null,
+ tokenPreloadTimer: null,
+ windowMinimized: false,
+ lastAttackState: null,
+ initialSetupComplete: false,
+ // Auto-batch state
+ autoBatchEnabled: CONFIG.AUTO_BATCH_ENABLED,
+ currentBatchSize: CONFIG.MIN_BATCH_SIZE,
+ batchOptimization: CONFIG.BATCH_OPTIMIZATION,
+ };
+
+ // Random string generator
+ const randStr = (len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') =>
+ [...Array(len)].map(() => chars[(crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) || Math.floor(Math.random() * chars.length)]).join('');
+
+ const fpStr32 = randStr(32);
+
+ // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt lol
+ let turnstileToken = null;
+ let tokenExpiryTime = 0;
+ let tokenGenerationInProgress = false;
+ let _resolveToken = null;
+ let tokenPromise = new Promise((resolve) => {
+ _resolveToken = resolve;
+ });
+ let retryCount = 0;
+ const MAX_RETRIES = 10;
+ const MAX_BATCH_RETRIES = 10;
+ const TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety)
+
+ function setTurnstileToken(token) {
+ if (_resolveToken) {
+ _resolveToken(token);
+ _resolveToken = null;
+ }
+ turnstileToken = token;
+ tokenExpiryTime = Date.now() + TOKEN_LIFETIME;
+ console.log('✅ Turnstile token set successfully');
+ Utils.addDebugLog('Token cached successfully', 'success');
+ }
+
+ function isTokenValid() {
+ return turnstileToken && Date.now() < tokenExpiryTime;
+ }
+
+ function invalidateToken() {
+ turnstileToken = null;
+ tokenExpiryTime = 0;
+ console.log('🗑️ Token invalidated, will force fresh generation');
+ Utils.addDebugLog('Token invalidated, will force fresh generation', 'warning');
+ }
+
+ async function ensureToken(forceRefresh = false) {
+ // Return cached token if still valid and not forcing refresh
+ if (isTokenValid() && !forceRefresh) {
+ return turnstileToken;
+ }
+
+ // Invalidate token if forcing refresh
+ if (forceRefresh) invalidateToken();
+
+ // Avoid multiple simultaneous token generations
+ if (tokenGenerationInProgress) {
+ console.log('🔄 Token generation already in progress, waiting...');
+ await Utils.sleep(2000);
+ return isTokenValid() ? turnstileToken : null;
+ }
+
+ tokenGenerationInProgress = true;
+
+ try {
+ console.log('🔄 Token expired or missing, generating new one...');
+ const token = await handleCaptchaWithRetry();
+ if (token && token.length > 20) {
+ setTurnstileToken(token);
+ console.log('✅ Token captured and cached successfully');
+ return token;
+ }
+
+ console.log('⚠️ Invisible Turnstile failed, forcing browser automation...');
+ const fallbackToken = await handleCaptchaFallback();
+ if (fallbackToken && fallbackToken.length > 20) {
+ setTurnstileToken(fallbackToken);
+ console.log('✅ Fallback token captured successfully');
+ return fallbackToken;
+ }
+
+ console.log('❌ All token generation methods failed');
+ return null;
+ } finally {
+ tokenGenerationInProgress = false;
+ }
+ }
+
+ async function handleCaptchaWithRetry() {
+ const startTime = performance.now();
+
+ try {
+ const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken();
+
+ if (!sitekey) {
+ throw new Error('No valid sitekey found');
+ }
+
+ console.log('🔑 Using sitekey:', sitekey);
+
+ if (typeof window !== 'undefined' && window.navigator) {
+ console.log(
+ '🧭 UA:',
+ window.navigator.userAgent.substring(0, 50) + '...',
+ 'Platform:',
+ window.navigator.platform
+ );
+ }
+
+ let token = null;
+
+ if (
+ preGeneratedToken &&
+ typeof preGeneratedToken === 'string' &&
+ preGeneratedToken.length > 20
+ ) {
+ console.log('♻️ Reusing pre-generated Turnstile token');
+ token = preGeneratedToken;
+ } else {
+ if (isTokenValid()) {
+ console.log('♻️ Using existing cached token (from previous session)');
+ token = turnstileToken;
+ } else {
+ console.log('🔍 Generating new token with executeTurnstile...');
+ token = await Utils.executeTurnstile(sitekey, 'paint');
+ if (token) setTurnstileToken(token);
+ }
+ }
+
+ if (token && typeof token === 'string' && token.length > 20) {
+ const elapsed = Math.round(performance.now() - startTime);
+ console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`);
+ return token;
+ } else {
+ throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`);
+ }
+ } catch (error) {
+ const elapsed = Math.round(performance.now() - startTime);
+ console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error);
+ throw error;
+ }
+ }
+
+ async function handleCaptchaFallback() {
+ return new Promise(async (resolve, reject) => {
+ try {
+ // Ensure we have a fresh promise to await for a new token capture
+ if (!_resolveToken) {
+ tokenPromise = new Promise((res) => {
+ _resolveToken = res;
+ });
+ }
+ 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);
+
+ // 800ms delay before sending confirmation
+ await Utils.sleep(800);
+
+ // Keep confirming until token is captured
+ const confirmLoop = async () => {
+ while (!turnstileToken) {
+ 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) {
+ confirmBtn.click();
+ }
+ await Utils.sleep(500); // 500ms delay between confirmation attempts
+ }
+ };
+
+ // Start confirmation loop and wait for token
+ confirmLoop();
+ const token = await tokenPromise;
+ await Utils.sleep(300); // small delay after token is captured
+ resolve(token);
+ })();
+
+ await Promise.race([solvePromise, timeoutPromise]);
+ } catch (error) {
+ console.error('Auto-CAPTCHA process failed:', error);
+ reject(error);
+ }
+ });
+ }
+
+ // NUEVA LÓGICA DE INYECCIÓN - ACTUALIZADA DESDE new.txt
+ 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) {
+ // 📊 Debug log
+ console.log(
+ `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${payload.t
+ ? typeof payload.t === 'string'
+ ? payload.t.length > 50
+ ? payload.t.substring(0, 50) + '...'
+ : payload.t
+ : JSON.stringify(payload.t)
+ : 'null/undefined'
+ }, Length: ${payload.t?.length || 0}`
+ );
+ 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);
+ Utils.addDebugLog('Token captured from injection system', 'success');
+ updateTokenStatus();
+ }
+ });
+
+ // NUEVO SISTEMA WASM - ACTUALIZADO DESDE new.txt
+ var pawtect_chunk = null;
+
+ // Find module if pawtect_chunk is null
+ pawtect_chunk ??= await findTokenModule("pawtect_wasm_bg.wasm");
+
+ async function createWasmToken(regionX, regionY, payload) {
+ try {
+ // Load the Pawtect module and WASM
+ const mod = await import(new URL('/_app/immutable/chunks/'+pawtect_chunk, location.origin).href);
+ let wasm;
+ try {
+ wasm = await mod._();
+ console.log('✅ WASM initialized successfully');
+ } catch (wasmError) {
+ console.error('❌ WASM initialization failed:', wasmError);
+ return null;
+ }
+ try {
+ try {
+ const me = await fetch(`https://backend.wplace.live/me`, { credentials: 'include' }).then(r => r.ok ? r.json() : null);
+ if (me?.id) {
+ mod.i(me.id);
+ console.log('✅ user ID set:', me.id);
+ }
+ } catch { }
+ } catch (userIdError) {
+ console.log('⚠️ Error setting user ID:', userIdError.message);
+ }
+ try {
+ const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`;
+ if (mod.r) {
+ mod.r(testUrl);
+ console.log('✅ Request URL set:', testUrl);
+ } else {
+ console.log('⚠️ request_url function (mod.r) not available');
+ }
+ } catch (urlError) {
+ console.log('⚠️ Error setting request URL:', urlError.message);
+ }
+
+ console.log('🔍 payload:', payload);
+
+ // Encode payload
+ const enc = new TextEncoder();
+ const dec = new TextDecoder();
+ const bodyStr = JSON.stringify(payload);
+ const bytes = enc.encode(bodyStr);
+ console.log('🔍 Payload size:', bytes.length, 'bytes');
+ console.log('🔄 Payload string:', bodyStr);
+
+ // Allocate WASM memory with validation
+ let inPtr;
+ try {
+ if (!wasm.__wbindgen_malloc) {
+ console.error('❌ __wbindgen_malloc function not found');
+ return null;
+ }
+
+ inPtr = wasm.__wbindgen_malloc(bytes.length, 1);
+ console.log('✅ WASM memory allocated, pointer:', inPtr);
+
+ // Copy data to WASM memory
+ const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length);
+ wasmBuffer.set(bytes);
+ console.log('✅ Data copied to WASM memory');
+ } catch (memError) {
+ console.error('❌ Memory allocation error:', memError);
+ return null;
+ }
+
+ // Call the WASM function
+ console.log('🚀 Calling get_pawtected_endpoint_payload...');
+ let outPtr, outLen, token;
+ try {
+ const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length);
+ console.log('✅ Function called, result type:', typeof result, result);
+
+ if (Array.isArray(result) && result.length === 2) {
+ [outPtr, outLen] = result;
+ console.log('✅ Got output pointer:', outPtr, 'length:', outLen);
+
+ // Decode the result
+ const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen);
+ token = dec.decode(outputBuffer);
+ console.log('✅ Token decoded successfully');
+ } else {
+ console.error('❌ Unexpected function result format:', result);
+ return null;
+ }
+ } catch (funcError) {
+ console.error('❌ Function call error:', funcError);
+ console.error('Stack trace:', funcError.stack);
+ return null;
+ }
+
+ // Cleanup memory
+ try {
+ if (wasm.__wbindgen_free && outPtr && outLen) {
+ wasm.__wbindgen_free(outPtr, outLen, 1);
+ console.log('✅ Output memory freed');
+ }
+ if (wasm.__wbindgen_free && inPtr) {
+ wasm.__wbindgen_free(inPtr, bytes.length, 1);
+ console.log('✅ Input memory freed');
+ }
+ } catch (cleanupError) {
+ console.log('⚠️ Cleanup warning:', cleanupError.message);
+ }
+
+ console.log('🎉 SUCCESS!');
+ console.log('🔑 Full token:', token);
+ return token;
+ } catch (error) {
+ console.error('❌ Failed to generate fp parameter:', error);
+ return null;
+ }
+ }
+
+ async function findTokenModule(str) {
+ console.log('🔎 Searching for wasm Module...');
+ const links = Array.from(document.querySelectorAll('link[rel="modulepreload"][href$=".js"]'));
+
+ for (const link of links) {
+ try {
+ const url = new URL(link.getAttribute("href"), location.origin).href;
+ const code = await fetch(url).then(r => r.text());
+ if (code.includes(str)) {
+ console.log('✅ Found wasm Module...');
+ return url.split('/').pop();
+ }
+ } catch { }
+ }
+ console.error(`❌ Could not find Pawtect chunk: `, error);
+ }
+
+ // Audio notification system
+ function playNotificationSound() {
+ try {
+ const audio = new Audio('https://cdn.pixabay.com/download/audio/2025/03/21/audio_9bec51b17f.mp3?filename=glass-break-316720.mp3');
+ audio.volume = 0.5;
+ audio.play().catch(() => {
+ console.warn('Could not play notification sound');
+ });
+ } catch (error) {
+ console.warn('Audio notification failed:', error);
+ }
+ }
+
+ // Notification system for attacks and repairs
+ function showAttackNotification(type, count = 0) {
+ const existing = document.getElementById('attack-notification');
+ if (existing) existing.remove();
+
+ const notification = document.createElement('div');
+ notification.id = 'attack-notification';
+ notification.style.cssText = `
+ position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
+ z-index: 15000; background: rgba(0,0,0,0.9); color: white;
+ border-radius: 15px; padding: 20px; text-align: center;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.8);
+ border: 2px solid ${type === 'attack' ? '#ff4444' : '#44ff44'};
+ min-width: 350px; max-width: 450px;
+ `;
+
+ if (type === 'attack') {
+ notification.innerHTML = `
+
+
+
+
+ PIXEL ART ATTACKED!
+
+
+ Detected ${count} damaged pixels!
+
+ `;
+ } else if (type === 'repaired') {
+ notification.innerHTML = `
+
+
+
+
+ COMPLETELY REPAIRED!
+
+
+ All pixels have been restored!
+
+ `;
+ }
+
+ document.body.appendChild(notification);
+
+ setTimeout(() => {
+ if (notification.parentNode) {
+ notification.style.transition = 'opacity 0.5s ease';
+ notification.style.opacity = '0';
+ setTimeout(() => {
+ if (notification.parentNode) notification.remove();
+ }, 500);
+ }
+ }, 3000);
+ }
+
+ // Peaceful state display in main window
+ function showPeacefulState() {
+ const statusDiv = document.getElementById('status');
+ if (statusDiv && !state.running) {
+ statusDiv.innerHTML = `
+
+
+
No attacks detected - All secure
+
+ `;
+ }
+ }
+
+ // Typewriter effect for title
+ function createTypewriterTitle() {
+ const titleText = "WPlace Autonomous Repair Tool";
+ let currentIndex = 0;
+ const titleElement = document.getElementById('main-title');
+
+ if (!titleElement) return;
+
+ function typeNext() {
+ if (currentIndex < titleText.length) {
+ titleElement.textContent = titleText.substring(0, currentIndex + 1);
+ currentIndex++;
+ setTimeout(typeNext, 100);
+ } else {
+ setTimeout(() => {
+ currentIndex = 0;
+ titleElement.textContent = '';
+ setTimeout(typeNext, 1000);
+ }, 3000);
+ }
+ }
+
+ typeNext();
+ }
+
+ // Fallback translations
+ const TEXTS = {
+ title: 'WPlace Autonomous Repair Tool',
+ loadFromFile: 'Load Progress File',
+ repairPixels: 'Repair Pixels',
+ enableAutoRepair: 'Enable Auto Repair',
+ repairInterval: 'Check Interval (seconds)',
+ debug: 'Debug Console',
+ clearDebug: 'Clear Debug',
+ scanningForDamage: 'Scanning for damaged pixels...',
+ damageDetected: 'Damage detected: {count} pixels',
+ noDamageDetected: 'No damage found',
+ repairingPixels: 'Repairing {count} damaged pixels...',
+ repairComplete: 'Repair completed: {repaired} pixels fixed',
+ autoRepairStarted: 'Auto repair started (every {interval}s)',
+ autoRepairStopped: 'Auto repair stopped',
+ fileLoaded: 'Progress file loaded successfully',
+ invalidFile: 'Invalid file format',
+ noImageData: 'No image data found in file',
+ turnstileInstructions: 'Complete the verification',
+ hideTurnstileBtn: 'Hide',
+ tokenCapturedSuccess: 'Token captured successfully',
+ autonomousModeActive: 'Autonomous mode active',
+ tokenSystemReady: 'Advanced token system ready',
+ fileOperationsAvailable: 'File operations available',
+ initializingToken: 'Initializing token system...',
+ tokenReady: 'Token system ready',
+ };
+
+ // UTILIDADES ACTUALIZADAS CON NUEVA IMPLEMENTACIÓN TURNSTILE
+ const Utils = {
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
+
+ t: (key, params = {}) => {
+ let text = TEXTS[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; right: 20px; z-index: 10000;
+ padding: 12px 16px; border-radius: 8px; color: white; font-weight: 500;
+ background: ${type === 'success' ? '#28a745' : type === 'error' ? '#dc3545' : type === 'warning' ? '#ffc107' : '#17a2b8'};
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
+ max-width: 300px; word-wrap: break-word;
+ `;
+ alertDiv.textContent = message;
+ document.body.appendChild(alertDiv);
+
+ setTimeout(() => {
+ alertDiv.style.transition = 'opacity 0.3s ease';
+ alertDiv.style.opacity = '0';
+ setTimeout(() => {
+ if (alertDiv.parentNode) alertDiv.remove();
+ }, 300);
+ }, 4000);
+ },
+
+ addDebugLog: (message, type = 'info') => {
+ const timestamp = new Date().toLocaleTimeString();
+ state.debugLogs.push({ timestamp, message, type });
+
+ if (state.debugLogs.length > 150) {
+ state.debugLogs = state.debugLogs.slice(-150);
+ }
+
+ updateDebugConsole();
+ console.log(`[${timestamp}] ${message}`);
+ },
+
+ 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;
+ },
+
+ 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();
+ }),
+
+ isWhitePixel: (r, g, b) => {
+ const wt = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD;
+ return r >= wt && g >= wt && b >= wt;
+ },
+
+ resolveColor(targetRgb, availableColors) {
+ if (!availableColors || availableColors.length === 0) {
+ return { id: null, rgb: targetRgb };
+ }
+
+ let bestId = availableColors[0].id;
+ let bestRgb = [...availableColors[0].rgb];
+ let bestScore = Infinity;
+
+ for (let i = 0; i < availableColors.length; i++) {
+ const c = availableColors[i];
+ const [r, g, b] = c.rgb;
+ const rmean = (r + targetRgb[0]) / 2;
+ const rdiff = r - targetRgb[0];
+ const gdiff = g - targetRgb[1];
+ const bdiff = b - targetRgb[2];
+ const dist = Math.sqrt(
+ (((512 + rmean) * rdiff * rdiff) >> 8) +
+ 4 * gdiff * gdiff +
+ (((767 - rmean) * bdiff * bdiff) >> 8)
+ );
+ if (dist < bestScore) {
+ bestScore = dist;
+ bestId = c.id;
+ bestRgb = [...c.rgb];
+ if (dist === 0) break;
+ }
+ }
+
+ return { id: bestId, rgb: bestRgb };
+ },
+
+ calculateTileRange(
+ startRegionX,
+ startRegionY,
+ startPixelX,
+ startPixelY,
+ width,
+ height,
+ tileSize = 1000
+ ) {
+ const endPixelX = startPixelX + width;
+ const endPixelY = startPixelY + height;
+
+ return {
+ startTileX: startRegionX + Math.floor(startPixelX / tileSize),
+ startTileY: startRegionY + Math.floor(startPixelY / tileSize),
+ endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize),
+ endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize),
+ };
+ },
+
+ dynamicSleep: async function (tickAndGetRemainingMs) {
+ let remaining = Math.max(0, await tickAndGetRemainingMs());
+ while (remaining > 0) {
+ const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100;
+ await this.sleep(Math.min(interval, remaining));
+ remaining = Math.max(0, await tickAndGetRemainingMs());
+ }
+ },
+
+ // NUEVA IMPLEMENTACIÓN TURNSTILE COMPLETA - ACTUALIZADA DESDE new.txt
+ turnstileLoaded: false,
+ _turnstileContainer: null,
+ _turnstileOverlay: null,
+ _turnstileWidgetId: null,
+ _lastSitekey: null,
+ _cachedSitekey: null,
+
+ async loadTurnstile() {
+ if (window.turnstile) {
+ this.turnstileLoaded = true;
+ return Promise.resolve();
+ }
+
+ return new Promise((resolve, reject) => {
+ if (document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) {
+ const checkReady = () => {
+ if (window.turnstile) {
+ this.turnstileLoaded = true;
+ resolve();
+ } else {
+ setTimeout(checkReady, 100);
+ }
+ };
+ return checkReady();
+ }
+
+ const script = document.createElement('script');
+ script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
+ script.async = true;
+ script.defer = true;
+ script.onload = () => {
+ this.turnstileLoaded = true;
+ Utils.addDebugLog('Turnstile script loaded successfully', 'success');
+ resolve();
+ };
+ script.onerror = () => {
+ Utils.addDebugLog('Failed to load Turnstile script', 'error');
+ reject(new Error('Failed to load Turnstile'));
+ };
+ document.head.appendChild(script);
+ });
+ },
+
+ ensureTurnstileContainer() {
+ if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) {
+ if (this._turnstileContainer) {
+ this._turnstileContainer.remove();
+ }
+
+ this._turnstileContainer = document.createElement('div');
+ this._turnstileContainer.className = 'wplace-turnstile-hidden';
+ this._turnstileContainer.setAttribute('aria-hidden', 'true');
+ this._turnstileContainer.id = 'turnstile-widget-container';
+ document.body.appendChild(this._turnstileContainer);
+ }
+ return this._turnstileContainer;
+ },
+
+ ensureTurnstileOverlayContainer() {
+ if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
+ return this._turnstileOverlay;
+ }
+
+ const overlay = document.createElement('div');
+ overlay.id = 'turnstile-overlay-container';
+ overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden';
+
+ const title = document.createElement('div');
+ title.textContent = Utils.t('turnstileInstructions');
+ title.className = 'wplace-turnstile-title';
+
+ const host = document.createElement('div');
+ host.id = 'turnstile-overlay-host';
+ host.className = 'wplace-turnstile-host';
+
+ const hideBtn = document.createElement('button');
+ hideBtn.textContent = Utils.t('hideTurnstileBtn');
+ hideBtn.className = 'wplace-turnstile-hide-btn';
+ hideBtn.addEventListener('click', () => overlay.remove());
+
+ overlay.appendChild(title);
+ overlay.appendChild(host);
+ overlay.appendChild(hideBtn);
+ document.body.appendChild(overlay);
+
+ this._turnstileOverlay = overlay;
+ return overlay;
+ },
+
+ async executeTurnstile(sitekey, action = 'paint') {
+ await this.loadTurnstile();
+
+ // Try reusing existing widget first if sitekey matches
+ if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) {
+ try {
+ console.log('🔄 Reusing existing Turnstile widget...');
+ const token = await Promise.race([
+ window.turnstile.execute(this._turnstileWidgetId, { action }),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Execute timeout')), 15000)
+ ),
+ ]);
+ if (token && token.length > 20) {
+ console.log('✅ Token generated via widget reuse');
+ return token;
+ }
+ } catch (error) {
+ console.log(' Widget reuse failed, will create a fresh widget:', error.message);
+ }
+ }
+
+ // Try invisible widget first
+ const invisibleToken = await this.createTurnstileWidget(sitekey, action);
+ if (invisibleToken && invisibleToken.length > 20) {
+ return invisibleToken;
+ }
+
+ console.log(' Falling back to interactive Turnstile (visible).');
+ return await this.createTurnstileWidgetInteractive(sitekey, action);
+ },
+
+ async createTurnstileWidget(sitekey, action) {
+ return new Promise((resolve) => {
+ try {
+ // Force cleanup of any existing widget
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ console.log('🧹 Cleaned up existing Turnstile widget');
+ } catch (e) {
+ console.warn('⚠️ Widget cleanup warning:', e.message);
+ }
+ }
+
+ const container = this.ensureTurnstileContainer();
+ container.innerHTML = '';
+
+ // Verify Turnstile is available
+ if (!window.turnstile?.render) {
+ console.error('❌ Turnstile not available for rendering');
+ resolve(null);
+ return;
+ }
+
+ console.log('🔧 Creating invisible Turnstile widget...');
+ const widgetId = window.turnstile.render(container, {
+ sitekey,
+ action,
+ size: 'invisible',
+ retry: 'auto',
+ 'retry-interval': 8000,
+ callback: (token) => {
+ console.log('✅ Invisible Turnstile callback');
+ resolve(token);
+ },
+ 'error-callback': () => resolve(null),
+ 'timeout-callback': () => resolve(null),
+ });
+
+ this._turnstileWidgetId = widgetId;
+ this._lastSitekey = sitekey;
+
+ if (!widgetId) {
+ return resolve(null);
+ }
+
+ // Execute the widget and race with timeout
+ Promise.race([
+ window.turnstile.execute(widgetId, { action }),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error('Invisible execute timeout')), 12000)
+ ),
+ ])
+ .then(resolve)
+ .catch(() => resolve(null));
+ } catch (e) {
+ console.error('❌ Invisible Turnstile creation failed:', e);
+ resolve(null);
+ }
+ });
+ },
+
+ async createTurnstileWidgetInteractive(sitekey, action) {
+ // Create a visible widget that users can interact with if needed
+ console.log('🔄 Creating interactive Turnstile widget (visible)');
+
+ return new Promise((resolve) => {
+ try {
+ // Force cleanup of any existing widget
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ } catch (e) {
+ console.warn('⚠️ Widget cleanup warning:', e.message);
+ }
+ }
+
+ const overlay = this.ensureTurnstileOverlayContainer();
+ overlay.classList.remove('wplace-overlay-hidden');
+ overlay.style.display = 'block';
+
+ const host = overlay.querySelector('#turnstile-overlay-host');
+ host.innerHTML = '';
+
+ // Set a timeout for interactive mode
+ const timeout = setTimeout(() => {
+ console.warn('⏰ Interactive Turnstile widget timeout');
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ resolve(null);
+ }, 60000); // 60 seconds for user interaction
+
+ const widgetId = window.turnstile.render(host, {
+ sitekey,
+ action,
+ size: 'normal',
+ theme: 'light',
+ callback: (token) => {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.log('✅ Interactive Turnstile completed successfully');
+
+ if (typeof token === 'string' && token.length > 20) {
+ resolve(token);
+ } else {
+ console.warn('❌ Invalid token from interactive widget');
+ resolve(null);
+ }
+ },
+ 'error-callback': (error) => {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.warn('❌ Interactive Turnstile error:', error);
+ resolve(null);
+ },
+ });
+
+ this._turnstileWidgetId = widgetId;
+ this._lastSitekey = sitekey;
+
+ if (!widgetId) {
+ clearTimeout(timeout);
+ overlay.classList.add('wplace-overlay-hidden');
+ overlay.style.display = 'none';
+ console.warn('❌ Failed to create interactive Turnstile widget');
+ resolve(null);
+ } else {
+ console.log('✅ Interactive Turnstile widget created, waiting for user interaction...');
+ }
+ } catch (e) {
+ console.error('❌ Interactive Turnstile creation failed:', e);
+ resolve(null);
+ }
+ });
+ },
+
+ cleanupTurnstile() {
+ if (this._turnstileWidgetId && window.turnstile?.remove) {
+ try {
+ window.turnstile.remove(this._turnstileWidgetId);
+ } catch (e) {
+ console.warn('Failed to cleanup Turnstile widget:', e);
+ }
+ }
+
+ if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) {
+ this._turnstileContainer.remove();
+ }
+
+ if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
+ this._turnstileOverlay.remove();
+ }
+
+ this._turnstileWidgetId = null;
+ this._turnstileContainer = null;
+ this._turnstileOverlay = null;
+ this._lastSitekey = null;
+ },
+
+ // DETECCIÓN DE SITEKEY MEJORADA - ACTUALIZADA DESDE new.txt
+ async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') {
+ // Cache sitekey to avoid repeated DOM queries
+ if (this._cachedSitekey) {
+ console.log('🔍 Using cached sitekey:', this._cachedSitekey);
+
+ return isTokenValid()
+ ? {
+ sitekey: this._cachedSitekey,
+ token: turnstileToken,
+ }
+ : { sitekey: this._cachedSitekey, token: null };
+ }
+
+ // List of potential sitekeys to try
+ const potentialSitekeys = [
+ '0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey
+ '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey
+ '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey
+ ];
+ const trySitekey = async (sitekey, source) => {
+ if (!sitekey || sitekey.length < 10) return null;
+
+ console.log(`🔍 Testing sitekey from ${source}:`, sitekey);
+ const token = await this.executeTurnstile(sitekey);
+
+ if (token && token.length >= 20) {
+ console.log(`✅ Valid token generated from ${source} sitekey`);
+ setTurnstileToken(token);
+ this._cachedSitekey = sitekey;
+ return { sitekey, token };
+ } else {
+ console.log(`❌ Failed to get token from ${source} sitekey`);
+ return null;
+ }
+ };
+
+ try {
+ // 1️⃣ data-sitekey attribute
+ const sitekeySel = document.querySelector('[data-sitekey]');
+ if (sitekeySel) {
+ const sitekey = sitekeySel.getAttribute('data-sitekey');
+ const result = await trySitekey(sitekey, 'data attribute');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 2️⃣ Turnstile element
+ const turnstileEl = document.querySelector('.cf-turnstile');
+ if (turnstileEl?.dataset?.sitekey) {
+ const sitekey = turnstileEl.dataset.sitekey;
+ const result = await trySitekey(sitekey, 'turnstile element');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 3️⃣ Meta tags
+ const metaTags = document.querySelectorAll(
+ 'meta[name*="turnstile"], meta[property*="turnstile"]'
+ );
+ for (const meta of metaTags) {
+ const content = meta.getAttribute('content');
+ const result = await trySitekey(content, 'meta tag');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 4️⃣ Global variable
+ if (window.__TURNSTILE_SITEKEY) {
+ const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable');
+ if (result) {
+ return result;
+ }
+ }
+
+ // 5️⃣ Script tags
+ const scripts = document.querySelectorAll('script');
+ for (const script of scripts) {
+ const content = script.textContent || script.innerHTML;
+ const match = content.match(
+ /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i
+ );
+ if (match && match[1]) {
+ const extracted = match[1].replace(/['"]/g, '');
+ const result = await trySitekey(extracted, 'script content');
+ if (result) {
+ return result;
+ }
+ }
+ }
+
+ // 6️⃣ Known potential sitekeys
+ console.log('🔍 Testing known potential sitekeys...');
+ for (const testSitekey of potentialSitekeys) {
+ const result = await trySitekey(testSitekey, 'known list');
+ if (result) {
+ return result;
+ }
+ }
+ } catch (error) {
+ console.warn('⚠️ Error during sitekey detection:', error);
+ }
+
+ // 7️⃣ Fallback
+ console.log('🔧 Trying fallback sitekey:', fallback);
+ const fallbackResult = await trySitekey(fallback, 'fallback');
+ if (fallbackResult) {
+ return fallbackResult;
+ }
+
+ console.error('❌ No working sitekey or token found.');
+ return { sitekey: null, token: null };
+ },
+ };
+
+ // Enhanced Overlay Manager for pixel detection with autonomous capabilities
+ class OverlayManager {
+ constructor() {
+ this.originalTiles = new Map();
+ this.originalTilesData = new Map();
+ this.tileSize = 1000;
+ this.loadingPromises = new Map();
+ this.autonomousMode = state.autonomousMode;
+ }
+
+ async processAndRespondToTileRequest(eventData) {
+ const { endpoint, blobID, blobData } = eventData;
+
+ 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}`;
+
+ try {
+ const originalBitmap = await createImageBitmap(blobData);
+ this.originalTiles.set(tileKey, originalBitmap);
+
+ try {
+ let canvas, ctx;
+ if (typeof OffscreenCanvas !== 'undefined') {
+ canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
+ ctx = canvas.getContext('2d');
+ } else {
+ canvas = document.createElement('canvas');
+ canvas.width = originalBitmap.width;
+ canvas.height = originalBitmap.height;
+ ctx = canvas.getContext('2d');
+ }
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(originalBitmap, 0, 0);
+ const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height);
+
+ this.originalTilesData.set(tileKey, {
+ w: originalBitmap.width,
+ h: originalBitmap.height,
+ data: new Uint8ClampedArray(imgData.data),
+ });
+
+ if (this.autonomousMode) {
+ Utils.addDebugLog(`Auto-cached tile: ${tileKey} (${originalBitmap.width}x${originalBitmap.height})`, 'info');
+ }
+ } catch (e) {
+ Utils.addDebugLog(`Failed to cache tile ImageData: ${tileKey} - ${e.message}`, 'warning');
+ }
+ } catch (e) {
+ Utils.addDebugLog(`Failed to create tile bitmap: ${tileKey} - ${e.message}`, 'error');
+ }
+ }
+
+ window.postMessage(
+ {
+ source: 'auto-image-overlay',
+ blobID: blobID,
+ blobData: blobData,
+ },
+ '*'
+ );
+ }
+
+ async getTilePixelColor(tileX, tileY, pixelX, pixelY) {
+ const tileKey = `${tileX},${tileY}`;
+ const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD;
+
+ const cached = this.originalTilesData.get(tileKey);
+ if (cached && cached.data && cached.w > 0 && cached.h > 0) {
+ const x = Math.max(0, Math.min(cached.w - 1, pixelX));
+ const y = Math.max(0, Math.min(cached.h - 1, pixelY));
+ const idx = (y * cached.w + x) * 4;
+ const d = cached.data;
+ const a = d[idx + 3];
+
+ // Always return pixel data, let caller decide about transparency
+ return [d[idx], d[idx + 1], d[idx + 2], a];
+ }
+
+ const bitmap = this.originalTiles.get(tileKey);
+ if (!bitmap) {
+ if (this.autonomousMode) {
+ Utils.addDebugLog(`Tile ${tileKey} not available, requesting load...`, 'warning');
+ }
+ return null;
+ }
+
+ try {
+ let canvas, ctx;
+ if (typeof OffscreenCanvas !== 'undefined') {
+ canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
+ ctx = canvas.getContext('2d');
+ } else {
+ canvas = document.createElement('canvas');
+ canvas.width = bitmap.width;
+ canvas.height = bitmap.height;
+ ctx = canvas.getContext('2d');
+ }
+ ctx.imageSmoothingEnabled = false;
+ ctx.drawImage(bitmap, 0, 0);
+
+ const x = Math.max(0, Math.min(bitmap.width - 1, pixelX));
+ const y = Math.max(0, Math.min(bitmap.height - 1, pixelY));
+ const data = ctx.getImageData(x, y, 1, 1).data;
+ const a = data[3];
+
+ // Always return pixel data, let caller decide about transparency
+ return [data[0], data[1], data[2], a];
+ } catch (e) {
+ Utils.addDebugLog(`Error reading pixel from tile ${tileKey}: ${e.message}`, 'error');
+ return null;
+ }
+ }
+
+ async waitForTiles(startRegionX, startRegionY, pixelWidth, pixelHeight, startPixelX = 0, startPixelY = 0, timeoutMs = 15000) {
+ const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange(
+ startRegionX,
+ startRegionY,
+ startPixelX,
+ startPixelY,
+ pixelWidth,
+ pixelHeight,
+ this.tileSize
+ );
+
+ const requiredTiles = [];
+ for (let ty = startTileY; ty <= endTileY; ty++) {
+ for (let tx = startTileX; tx <= endTileX; tx++) {
+ requiredTiles.push(`${tx},${ty}`);
+ }
+ }
+
+ if (requiredTiles.length === 0) return true;
+
+ Utils.addDebugLog(`Waiting for ${requiredTiles.length} tiles (autonomous: ${this.autonomousMode})...`, 'info');
+
+ const startTime = Date.now();
+ let lastProgress = 0;
+
+ while (Date.now() - startTime < timeoutMs) {
+ if (state.stopFlag) {
+ Utils.addDebugLog('waitForTiles: stopped by user', 'warning');
+ return false;
+ }
+
+ const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length;
+ const progress = Math.round((loaded / requiredTiles.length) * 100);
+
+ if (progress !== lastProgress && progress % 20 === 0) {
+ Utils.addDebugLog(`Tile loading progress: ${loaded}/${requiredTiles.length} (${progress}%)`, 'info');
+ lastProgress = progress;
+ }
+
+ if (loaded === requiredTiles.length) {
+ Utils.addDebugLog(`All ${requiredTiles.length} required tiles are loaded`, 'success');
+ return true;
+ }
+
+ await Utils.sleep(this.autonomousMode ? 500 : 1000);
+ }
+
+ const loaded = requiredTiles.filter((k) => this.originalTiles.has(k)).length;
+ Utils.addDebugLog(`Timeout waiting for tiles: ${loaded}/${requiredTiles.length} loaded`, 'warning');
+
+ if (this.autonomousMode && loaded > requiredTiles.length * 0.8) {
+ Utils.addDebugLog(`Autonomous mode: proceeding with ${loaded}/${requiredTiles.length} tiles (80%+ loaded)`, 'warning');
+ return true;
+ }
+
+ return loaded > 0;
+ }
+ }
+
+ const overlayManager = new OverlayManager();
+
+ // Enhanced WPlace API Service with auto-batch functionality
+ const WPlaceService = {
+ async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount = 0) {
+ try {
+ await ensureToken();
+ if (!turnstileToken) {
+ Utils.addDebugLog('No valid token available for paint request', 'error');
+ return 'token_error';
+ }
+
+ const payload = {
+ coords: [pixelX, pixelY],
+ colors: [color],
+ t: turnstileToken,
+ fp: fpStr32,
+ };
+
+ const wasmToken = await createWasmToken(regionX, regionY, payload);
+ if (!wasmToken) {
+ Utils.addDebugLog('Failed to generate WASM token', 'error');
+ return false;
+ }
+
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'x-pawtect-token': wasmToken
+ },
+ credentials: 'include',
+ body: JSON.stringify(payload),
+ });
+
+ if (res.status === 403) {
+ Utils.addDebugLog(`403 Forbidden for pixel (${pixelX},${pixelY}). Token invalid/expired.`, 'error');
+ invalidateToken();
+
+ if (retryCount < 2 && state.autonomousMode) {
+ Utils.addDebugLog(`Autonomous retry ${retryCount + 1}/2 for pixel (${pixelX},${pixelY})`, 'warning');
+ await Utils.sleep(1000);
+ return await this.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount + 1);
+ }
+
+ return 'token_error';
+ }
+
+ if (!res.ok) {
+ Utils.addDebugLog(`Paint request failed with status ${res.status}`, 'error');
+ return false;
+ }
+
+ const data = await res.json();
+ const success = data?.painted === 1;
+
+ if (success) {
+ Utils.addDebugLog(`Paint SUCCESS for (${pixelX},${pixelY}) with color ${color}`, 'success');
+ } else {
+ Utils.addDebugLog(`Paint FAILED for (${pixelX},${pixelY}) - server response: ${JSON.stringify(data)}`, 'error');
+ }
+
+ return success;
+ } catch (e) {
+ Utils.addDebugLog(`Paint request error for (${pixelX},${pixelY}): ${e.message}`, 'error');
+ return false;
+ }
+ },
+
+ // NEW: Auto-batch pixel painting functionality
+ async paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount = 0) {
+ try {
+ await ensureToken();
+ if (!turnstileToken) {
+ Utils.addDebugLog('No valid token available for batch paint request', 'error');
+ return 'token_error';
+ }
+
+ // Prepare batch coordinates and colors
+ const coords = [];
+ const colors = [];
+
+ for (const pixel of pixelBatch) {
+ coords.push(pixel.pixelX, pixel.pixelY);
+ colors.push(pixel.color);
+ }
+
+ const payload = {
+ coords: coords,
+ colors: colors,
+ t: turnstileToken,
+ fp: fpStr32,
+ };
+
+ const wasmToken = await createWasmToken(regionX, regionY, payload);
+ if (!wasmToken) {
+ Utils.addDebugLog('Failed to generate WASM token for batch', 'error');
+ return false;
+ }
+
+ const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'text/plain;charset=UTF-8',
+ 'x-pawtect-token': wasmToken
+ },
+ credentials: 'include',
+ body: JSON.stringify(payload),
+ });
+
+ if (res.status === 403) {
+ Utils.addDebugLog(`403 Forbidden for batch (${pixelBatch.length} pixels). Token invalid/expired.`, 'error');
+ invalidateToken();
+
+ if (retryCount < 2 && state.autonomousMode) {
+ Utils.addDebugLog(`Autonomous batch retry ${retryCount + 1}/2 for ${pixelBatch.length} pixels`, 'warning');
+ await Utils.sleep(1000);
+ return await this.paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount + 1);
+ }
+
+ return 'token_error';
+ }
+
+ if (!res.ok) {
+ Utils.addDebugLog(`Batch paint request failed with status ${res.status}`, 'error');
+ return false;
+ }
+
+ const data = await res.json();
+ const successCount = data?.painted || 0;
+
+ Utils.addDebugLog(`Batch paint result: ${successCount}/${pixelBatch.length} pixels painted successfully`,
+ successCount === pixelBatch.length ? 'success' : 'warning');
+
+ return {
+ success: successCount > 0,
+ painted: successCount,
+ total: pixelBatch.length
+ };
+
+ } catch (e) {
+ Utils.addDebugLog(`Batch paint request error for ${pixelBatch.length} pixels: ${e.message}`, 'error');
+ return false;
+ }
+ },
+
+ async getCharges() {
+ try {
+ const res = await fetch('https://backend.wplace.live/me', {
+ credentials: 'include',
+ });
+ if (!res.ok) return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT };
+ const data = await res.json();
+ return {
+ charges: data.charges?.count ?? 0,
+ max: data.charges?.max ?? 1,
+ cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT,
+ };
+ } catch (e) {
+ Utils.addDebugLog(`Error fetching charges: ${e.message}`, 'warning');
+ return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT };
+ }
+ },
+ };
+
+ // Enhanced anti-grief repair system with improved transparent pixel detection
+ async function scanForDamage() {
+ if (!state.imageData || !state.startPosition || !state.region) {
+ Utils.addDebugLog('No image data or position for scanning', 'warning');
+ return [];
+ }
+
+ Utils.addDebugLog('Starting enhanced damage scan with transparent pixel detection...', 'info');
+ updateStatus(Utils.t('scanningForDamage'));
+
+ const damagedPixels = [];
+ const { width, height, pixels } = state.imageData;
+
+ const ready = await overlayManager.waitForTiles(
+ state.region.x,
+ state.region.y,
+ width,
+ height,
+ state.startPosition.x,
+ state.startPosition.y,
+ state.autonomousMode ? 20000 : 15000
+ );
+
+ if (!ready) {
+ Utils.addDebugLog('Failed to load required tiles for scanning', 'error');
+ if (state.autonomousMode) {
+ Utils.addDebugLog('Autonomous mode: will retry scan in 30 seconds', 'warning');
+ setTimeout(() => {
+ if (state.autoRepairEnabled && !state.stopFlag) {
+ scanForDamage();
+ }
+ }, 30000);
+ }
+ return [];
+ }
+
+ Utils.addDebugLog(`Scanning ${width}x${height} image for damage (transparent detection: ${state.paintTransparentPixels})...`, 'info');
+
+ let scannedPixels = 0;
+ let transparentPixelsDetected = 0;
+ let wrongColorPixelsDetected = 0;
+ let lastProgressUpdate = Date.now();
+
+ for (let y = 0; y < height; y++) {
+ for (let x = 0; x < width; x++) {
+ if (state.stopFlag) break;
+
+ const idx = (y * width + x) * 4;
+ const originalR = pixels[idx];
+ const originalG = pixels[idx + 1];
+ const originalB = pixels[idx + 2];
+ const originalA = pixels[idx + 3];
+
+ // Enhanced transparent pixel detection
+ const isOriginalTransparent = originalA < state.customTransparencyThreshold;
+
+ // Skip white pixels if configured
+ if (!state.paintWhitePixels && Utils.isWhitePixel(originalR, originalG, originalB)) {
+ continue;
+ }
+
+ scannedPixels++;
+
+ const absX = state.startPosition.x + x;
+ const absY = state.startPosition.y + y;
+ const tileX = state.region.x + Math.floor(absX / 1000);
+ const tileY = state.region.y + Math.floor(absY / 1000);
+ const pixelX = absX % 1000;
+ const pixelY = absY % 1000;
+
+ try {
+ const currentPixel = await overlayManager.getTilePixelColor(tileX, tileY, pixelX, pixelY);
+
+ // Handle missing current pixel data (could indicate transparent area)
+ if (!currentPixel) {
+ if (state.paintTransparentPixels && !isOriginalTransparent) {
+ // Original pixel is not transparent but current is missing/transparent
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: targetColor,
+ currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] }, // Transparent color
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: [0, 0, 0],
+ isDamagedTransparent: true
+ });
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Missing/Transparent pixel at (${x},${y}): expected color ${targetColor.id}, found transparent/missing`, 'warning');
+ }
+ }
+ continue;
+ }
+
+ const currentIsTransparent = currentPixel[3] < state.customTransparencyThreshold;
+
+ // Case 1: Original is not transparent, but current is transparent (damaged)
+ if (!isOriginalTransparent && currentIsTransparent && state.paintTransparentPixels) {
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: targetColor,
+ currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] },
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: true
+ });
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Transparent damage at (${x},${y}): expected color ${targetColor.id}, found transparent`, 'warning');
+ }
+ continue;
+ }
+
+ // Case 2: Original is transparent, current is not transparent (wrong placement)
+ if (isOriginalTransparent && !currentIsTransparent && state.paintTransparentPixels) {
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: { id: 63, name: 'Transparent', rgb: null },
+ currentColor: Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors),
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: true
+ });
+ transparentPixelsDetected++;
+
+ if (!state.autonomousMode || transparentPixelsDetected <= 10) {
+ Utils.addDebugLog(`Wrong placement at (${x},${y}): expected transparent, found color`, 'warning');
+ }
+ continue;
+ }
+
+ // Case 3: Both are not transparent, check color match (normal damage detection)
+ if (!isOriginalTransparent && !currentIsTransparent) {
+ const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors);
+ const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors);
+
+ if (targetColor.id !== currentColor.id) {
+ damagedPixels.push({
+ x,
+ y,
+ originalColor: targetColor,
+ currentColor: currentColor,
+ originalRgb: [originalR, originalG, originalB],
+ currentRgb: currentPixel.slice(0, 3),
+ isDamagedTransparent: false
+ });
+ wrongColorPixelsDetected++;
+
+ if (!state.autonomousMode || wrongColorPixelsDetected <= 10) {
+ Utils.addDebugLog(`Color damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning');
+ }
+ }
+ }
+
+ } catch (e) {
+ if (!state.autonomousMode) {
+ Utils.addDebugLog(`Error checking pixel (${x},${y}): ${e.message}`, 'error');
+ }
+ }
+ }
+
+ if (state.autonomousMode && Date.now() - lastProgressUpdate > 5000) {
+ Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent, ${wrongColorPixelsDetected} wrong color)`, 'info');
+ lastProgressUpdate = Date.now();
+ } else if (!state.autonomousMode && y % 10 === 0) {
+ Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%)`, 'info');
+ }
+ }
+
+ const logLevel = damagedPixels.length > 0 ? 'warning' : 'success';
+ Utils.addDebugLog(`Enhanced scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent issues, ${wrongColorPixelsDetected} wrong colors)`, logLevel);
+
+ // Smart auto-adjust batch size: if 6 pixels detected, batch size = 6
+ if (state.autoBatchEnabled && state.batchOptimization && damagedPixels.length > 0) {
+ const newBatchSize = Math.min(CONFIG.MAX_BATCH_SIZE, Math.max(CONFIG.MIN_BATCH_SIZE, damagedPixels.length));
+ if (newBatchSize !== state.currentBatchSize) {
+ state.currentBatchSize = newBatchSize;
+ Utils.addDebugLog(`Smart batch: Auto-adjusted batch size to ${newBatchSize} to match ${damagedPixels.length} detected damaged pixels`, 'info');
+ updateBatchInfo(); // Update UI display
+ }
+ }
+
+ return damagedPixels;
+ }
+
+ async function repairDamagedPixels(damagedPixels) {
+ if (damagedPixels.length === 0) {
+ updateStatus(Utils.t('noDamageDetected'));
+ return 0;
+ }
+
+ Utils.addDebugLog(`Starting enhanced repair of ${damagedPixels.length} pixels with auto-batch (batch size: ${state.currentBatchSize})`, 'info');
+ updateStatus(Utils.t('repairingPixels', { count: damagedPixels.length }));
+
+ let repairedCount = 0;
+ let consecutiveFailures = 0;
+ const maxConsecutiveFailures = state.autonomousMode ? 5 : 3;
+
+ // Group pixels by region for batch processing
+ const pixelsByRegion = new Map();
+
+ for (const pixel of damagedPixels) {
+ const absX = state.startPosition.x + pixel.x;
+ const absY = state.startPosition.y + pixel.y;
+ const regionX = state.region.x + Math.floor(absX / 1000);
+ const regionY = state.region.y + Math.floor(absY / 1000);
+ const regionKey = `${regionX},${regionY}`;
+
+ if (!pixelsByRegion.has(regionKey)) {
+ pixelsByRegion.set(regionKey, []);
+ }
+
+ pixelsByRegion.get(regionKey).push({
+ ...pixel,
+ regionX,
+ regionY,
+ pixelX: absX % 1000,
+ pixelY: absY % 1000,
+ color: pixel.isDamagedTransparent && pixel.originalColor.id === 63 ? 63 : pixel.originalColor.id
+ });
+ }
+
+ Utils.addDebugLog(`Grouped ${damagedPixels.length} pixels into ${pixelsByRegion.size} regions for batch processing`, 'info');
+
+ // Process each region
+ for (const [regionKey, regionPixels] of pixelsByRegion) {
+ if (state.stopFlag) {
+ Utils.addDebugLog('Repair stopped by user request', 'warning');
+ break;
+ }
+
+ const [regionX, regionY] = regionKey.split(',').map(Number);
+ Utils.addDebugLog(`Processing region ${regionKey} with ${regionPixels.length} pixels`, 'info');
+
+ // Process pixels in batches
+ for (let i = 0; i < regionPixels.length; i += state.currentBatchSize) {
+ if (state.stopFlag) break;
+
+ const batch = regionPixels.slice(i, i + state.currentBatchSize);
+ const actualBatchSize = Math.min(batch.length, state.currentBatchSize);
+
+ // Enhanced charge management for batch processing
+ await updateCharges();
+ let chargeWaitAttempts = 0;
+ const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10;
+
+ while (state.displayCharges < actualBatchSize && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) {
+ chargeWaitAttempts++;
+ const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown;
+
+ if (chargeWaitAttempts === 1) {
+ Utils.addDebugLog(`Waiting for charges... (need ${actualBatchSize}, have ${state.displayCharges}/${state.maxCharges})`, 'info');
+ }
+
+ await Utils.dynamicSleep(() => {
+ if (state.displayCharges >= actualBatchSize) return 0;
+ if (state.stopFlag) return 0;
+ return waitTime;
+ });
+ await updateCharges();
+ }
+
+ if (state.stopFlag) break;
+
+ if (state.displayCharges < actualBatchSize) {
+ Utils.addDebugLog(`Insufficient charges for batch (need ${actualBatchSize}, have ${state.displayCharges}), falling back to single pixel repair`, 'warning');
+
+ // Fall back to single pixel repair
+ for (const pixel of batch) {
+ if (state.stopFlag) break;
+
+ await updateCharges();
+ if (state.displayCharges < 1) {
+ Utils.addDebugLog(`No charges available, skipping pixel (${pixel.x},${pixel.y})`, 'warning');
+ continue;
+ }
+
+ const success = await repairSinglePixel(pixel);
+ if (success) {
+ repairedCount++;
+ consecutiveFailures = 0;
+ } else {
+ consecutiveFailures++;
+ }
+
+ if (consecutiveFailures >= maxConsecutiveFailures) break;
+ await Utils.sleep(100);
+ }
+ } else {
+ // Use batch repair
+ const batchResult = await repairPixelBatch(regionX, regionY, batch);
+
+ if (batchResult && batchResult.success) {
+ repairedCount += batchResult.painted;
+ consecutiveFailures = 0;
+
+ Utils.addDebugLog(`Batch repair: ${batchResult.painted}/${batchResult.total} pixels repaired successfully [Total: ${repairedCount}/${damagedPixels.length}]`, 'success');
+ } else {
+ consecutiveFailures++;
+ Utils.addDebugLog(`Batch repair failed for ${batch.length} pixels [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error');
+
+ if (consecutiveFailures >= maxConsecutiveFailures) {
+ Utils.addDebugLog(`Too many consecutive batch failures (${consecutiveFailures}), pausing repair`, 'error');
+ if (state.autonomousMode) {
+ Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning');
+ setTimeout(() => {
+ if (state.autoRepairEnabled && !state.stopFlag) {
+ performRepairCheck();
+ }
+ }, 60000);
+ }
+ break;
+ }
+ }
+ }
+
+ await updateCharges();
+
+ // Dynamic delay based on mode and success rate
+ const baseDelay = state.autonomousMode ? 100 : 200;
+ const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay;
+ await Utils.sleep(adaptiveDelay);
+ }
+
+ if (consecutiveFailures >= maxConsecutiveFailures) break;
+ }
+
+ const message = Utils.t('repairComplete', { repaired: repairedCount });
+ updateStatus(message);
+ Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining) - Used auto-batch with size ${state.currentBatchSize}`, 'success');
+
+ return repairedCount;
+ }
+
+ async function repairPixelBatch(regionX, regionY, pixelBatch) {
+ try {
+ Utils.addDebugLog(`Attempting batch repair of ${pixelBatch.length} pixels in region ${regionX},${regionY}`, 'info');
+
+ const result = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch);
+
+ if (result === 'token_error') {
+ Utils.addDebugLog('Token error during batch repair, refreshing token...', 'warning');
+ await ensureToken(true);
+ await Utils.sleep(state.autonomousMode ? 500 : 1000);
+
+ // Retry once with new token
+ const retryResult = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch);
+ return retryResult;
+ }
+
+ return result;
+ } catch (e) {
+ Utils.addDebugLog(`Error during batch repair: ${e.message}`, 'error');
+ return false;
+ }
+ }
+
+ async function repairSinglePixel(pixel) {
+ const { x, y, originalColor, regionX, regionY, pixelX, pixelY, color } = pixel;
+
+ try {
+ const result = await WPlaceService.paintPixelInRegion(
+ regionX,
+ regionY,
+ pixelX,
+ pixelY,
+ color || originalColor.id
+ );
+
+ if (result === 'token_error') {
+ Utils.addDebugLog('Token error during repair, refreshing token...', 'warning');
+ await ensureToken(true);
+ await Utils.sleep(state.autonomousMode ? 500 : 1000);
+
+ // Retry once with new token
+ const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color || originalColor.id);
+ return retryResult === true;
+ }
+
+ if (result === true) {
+ if (pixel.isDamagedTransparent) {
+ Utils.addDebugLog(`Repaired transparent pixel (${x},${y}) with color ${color || originalColor.id}`, 'success');
+ }
+ }
+
+ return result === true;
+ } catch (e) {
+ Utils.addDebugLog(`Error repairing pixel (${x},${y}): ${e.message}`, 'error');
+ return false;
+ }
+ }
+
+ async function performRepairCheck() {
+ if (state.running) {
+ Utils.addDebugLog('Repair check skipped - manual repair in progress', 'info');
+ return;
+ }
+
+ try {
+ Utils.addDebugLog('Autonomous repair check triggered', 'info');
+
+ // Ensure we have a valid token before starting
+ if (!isTokenValid()) {
+ Utils.addDebugLog('No valid token for autonomous repair, generating...', 'warning');
+ await ensureToken(true);
+ if (!isTokenValid()) {
+ Utils.addDebugLog('Failed to generate token for autonomous repair, will retry next cycle', 'error');
+ return;
+ }
+ }
+
+ const damagedPixels = await scanForDamage();
+
+ if (damagedPixels.length > 0) {
+ Utils.addDebugLog(`Autonomous repair: Found ${damagedPixels.length} damaged pixels, starting repair`, 'warning');
+ updateStatus(Utils.t('damageDetected', { count: damagedPixels.length }));
+
+ // Show attack notification
+ showAttackNotification('attack', damagedPixels.length);
+ state.lastAttackState = 'attacked';
+
+ const repairedCount = await repairDamagedPixels(damagedPixels);
+
+ // Show repair complete notification if all pixels were repaired
+ if (repairedCount === damagedPixels.length) {
+ showAttackNotification('repaired');
+ state.lastAttackState = 'repaired';
+ }
+ } else {
+ updateStatus(Utils.t('noDamageDetected'));
+
+ // Show peaceful state if we're not under attack
+ if (state.lastAttackState !== 'peaceful') {
+ showPeacefulState();
+ state.lastAttackState = 'peaceful';
+ }
+
+ if (!state.autonomousMode) {
+ Utils.addDebugLog('Autonomous repair: No damage detected', 'success');
+ }
+ }
+ } catch (error) {
+ Utils.addDebugLog(`Autonomous repair error: ${error.message}`, 'error');
+
+ if (state.autonomousMode) {
+ Utils.addDebugLog('Autonomous mode: will retry repair check in 120 seconds due to error', 'warning');
+ setTimeout(() => {
+ if (state.autoRepairEnabled && !state.stopFlag) {
+ performRepairCheck();
+ }
+ }, 120000);
+ }
+ }
+ }
+
+ function startAutoRepair() {
+ if (state.autoRepairTimer) {
+ clearInterval(state.autoRepairTimer);
+ }
+
+ const intervalMs = state.autoRepairInterval * 1000;
+ state.autoRepairTimer = setInterval(performRepairCheck, intervalMs);
+
+ const message = Utils.t('autoRepairStarted', { interval: state.autoRepairInterval });
+ Utils.addDebugLog(`${message} (autonomous: ${state.autonomousMode})`, 'info');
+ Utils.showAlert(message, 'success');
+
+ // Perform first check immediately
+ setTimeout(performRepairCheck, 2000);
+ }
+
+ function stopAutoRepair() {
+ if (state.autoRepairTimer) {
+ clearInterval(state.autoRepairTimer);
+ state.autoRepairTimer = null;
+ }
+
+ const message = Utils.t('autoRepairStopped');
+ Utils.addDebugLog(`${message}`, 'info');
+ Utils.showAlert(message, 'info');
+ }
+
+ // FUNCIONES DE UI MEJORADAS CON GESTIÓN DE CONFIGURACIÓN INICIAL
+ function enableFileOperations() {
+ state.initialSetupComplete = true;
+
+ const loadBtn = document.querySelector('#loadFileBtn');
+ if (loadBtn) {
+ loadBtn.disabled = false;
+ loadBtn.title = '';
+ loadBtn.style.animation = 'pulse 0.6s ease-in-out';
+ setTimeout(() => {
+ if (loadBtn) loadBtn.style.animation = '';
+ }, 600);
+ Utils.addDebugLog('Load Progress button enabled after initial setup', 'success');
+ }
+
+ Utils.showAlert(Utils.t('fileOperationsAvailable'), 'success');
+ }
+
+ async function initializeTokenGenerator() {
+ if (isTokenValid()) {
+ Utils.addDebugLog('Valid token already available, skipping initialization', 'success');
+ updateTokenStatus();
+ enableFileOperations();
+ return;
+ }
+
+ try {
+ Utils.addDebugLog('Initializing Turnstile token generator...', 'info');
+ updateStatus(Utils.t('initializingToken'));
+
+ await Utils.loadTurnstile();
+ Utils.addDebugLog('Turnstile script loaded successfully', 'success');
+
+ if (state.autoTokenRefresh) {
+ Utils.addDebugLog('Pre-generating token for autonomous operations...', 'info');
+ await ensureToken();
+ }
+
+ state.initialSetupComplete = true;
+ Utils.addDebugLog('Enhanced token generator initialization complete', 'success');
+ updateStatus(Utils.t('tokenReady'));
+ enableFileOperations();
+
+ } catch (error) {
+ Utils.addDebugLog('Token system initialization failed: ' + error.message, 'warning');
+ Utils.addDebugLog('Manual token generation will be required', 'info');
+ enableFileOperations();
+ }
+ }
+
+ function updateStatus(message) {
+ const statusEl = document.getElementById('status');
+ if (statusEl) {
+ if (typeof message === 'string') {
+ statusEl.innerHTML = message;
+ } else {
+ statusEl.textContent = message;
+ }
+ }
+ }
+
+ function updateDebugConsole() {
+ const debugConsole = document.getElementById('debugConsole');
+ if (!debugConsole) return;
+
+ const logsHtml = state.debugLogs.map(log => {
+ const colorClass = log.type === 'error' ? 'neon-text danger' :
+ log.type === 'warning' ? 'neon-text warning' :
+ log.type === 'success' ? 'neon-text success' :
+ log.type === 'info' ? 'neon-text info' : 'neon-text';
+
+ return `
+ [${log.timestamp}] ${log.message}
+
`;
+ }).join('');
+
+ debugConsole.innerHTML = logsHtml;
+ debugConsole.scrollTop = debugConsole.scrollHeight;
+ }
+
+ async function updateCharges() {
+ try {
+ const { charges, max, cooldown } = await WPlaceService.getCharges();
+ state.displayCharges = Math.floor(charges);
+ state.maxCharges = Math.max(1, Math.floor(max));
+ state.cooldown = cooldown;
+
+ updateChargesDisplay();
+ } catch (error) {
+ if (!state.autonomousMode) {
+ Utils.addDebugLog(`Error updating charges: ${error.message}`, 'error');
+ }
+ }
+ }
+
+ function updateChargesDisplay() {
+ const chargesEl = document.getElementById('chargesInfo');
+ if (chargesEl) {
+ chargesEl.textContent = `Charges: ${state.displayCharges}/${state.maxCharges} (cooldown: ${Math.round(state.cooldown / 1000)}s)`;
+ }
+ }
+
+ function updateTokenStatus() {
+ const tokenEl = document.getElementById('tokenInfo');
+ if (tokenEl) {
+ // Remove existing classes first
+ tokenEl.className = '';
+
+ if (isTokenValid()) {
+ const remaining = Math.round((tokenExpiryTime - Date.now()) / 1000);
+ tokenEl.textContent = `Token: Valid (expires in ${remaining}s)`;
+ tokenEl.className = 'neon-text success';
+ } else {
+ tokenEl.textContent = 'Token: Not generated or expired';
+ tokenEl.className = 'neon-text warning';
+ }
+ }
+ }
+
+ // Update batch information display for new UI
+ function updateBatchInfo() {
+ const batchSizeEl = document.getElementById('batchSize');
+ const batchStatusEl = document.getElementById('batchStatus');
+ const smartModeEl = document.getElementById('smartBatchMode');
+ const manualModeEl = document.getElementById('manualBatchMode');
+ const manualControlsEl = document.getElementById('manualBatchControls');
+
+ // Update radio button states
+ if (smartModeEl && manualModeEl) {
+ smartModeEl.checked = state.autoBatchEnabled && state.batchOptimization;
+ manualModeEl.checked = !state.autoBatchEnabled || !state.batchOptimization;
+ }
+
+ // Update manual controls visibility
+ if (manualControlsEl) {
+ manualControlsEl.style.display = (!state.autoBatchEnabled || !state.batchOptimization) ? 'flex' : 'none';
+ }
+
+ // Update batch size input
+ if (batchSizeEl) {
+ batchSizeEl.value = state.currentBatchSize;
+ }
+
+ // Update status display
+ if (batchStatusEl) {
+ if (state.autoBatchEnabled && state.batchOptimization) {
+ batchStatusEl.textContent = `🧠 Smart Batch: AUTO (Current: ${state.currentBatchSize})`;
+ batchStatusEl.className = 'status-info';
+ } else {
+ batchStatusEl.textContent = `✋ Manual Batch: ${state.currentBatchSize} pixels fixed`;
+ batchStatusEl.className = 'status-warning';
+ }
+ }
+ }
+
+ function updateSystemStatus() {
+ const systemEl = document.getElementById('systemInfo');
+ if (systemEl) {
+ const autonomousStatus = state.autonomousMode ? 'AUTONOMOUS' : 'MANUAL';
+ const tokenMode = state.tokenSource.toUpperCase();
+ systemEl.textContent = `System: ${autonomousStatus} | Token: ${tokenMode} | Auto-refresh: ${state.autoTokenRefresh ? 'ON' : 'OFF'}`;
+ }
+ }
+
+ // Window dragging functionality
+ function makeWindowDraggable() {
+ const container = document.getElementById('wplace-repair-tool');
+ const header = document.getElementById('window-header');
+
+ let isDragging = false;
+ let currentX;
+ let currentY;
+ let initialX;
+ let initialY;
+ let xOffset = 0;
+ let yOffset = 0;
+
+ function dragStart(e) {
+ if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') {
+ return;
+ }
+
+ initialX = e.clientX - xOffset;
+ initialY = e.clientY - yOffset;
+
+ if (e.target === header || header.contains(e.target)) {
+ isDragging = true;
+ container.style.cursor = 'grabbing';
+ }
+ }
+
+ function dragEnd() {
+ initialX = currentX;
+ initialY = currentY;
+ isDragging = false;
+ container.style.cursor = 'default';
+ }
+
+ function drag(e) {
+ if (isDragging) {
+ e.preventDefault();
+ currentX = e.clientX - initialX;
+ currentY = e.clientY - initialY;
+ xOffset = currentX;
+ yOffset = currentY;
+
+ container.style.transform = `translate(${currentX}px, ${currentY}px)`;
+ }
+ }
+
+ header.addEventListener('mousedown', dragStart);
+ document.addEventListener('mouseup', dragEnd);
+ document.addEventListener('mousemove', drag);
+ }
+
+ // Enhanced UI Creation with NEW IMPLEMENTATION
+ function createUI() {
+ const existing = document.getElementById('wplace-repair-tool');
+ if (existing) existing.remove();
+
+ const container = document.createElement('div');
+ container.id = 'wplace-repair-tool';
+ container.style.cssText = `
+ position: fixed; top: 20px; left: 20px; z-index: 10000;
+ background: #1a1a2e;
+ border: 3px solid #00ff41;
+ border-radius: 0;
+ min-width: 600px; max-width: 700px;
+ box-shadow:
+ 0 0 30px rgb(0 255 65 / 50%),
+ inset 0 0 30px rgb(0 255 65 / 10%),
+ 0 0 0 1px #00ff41;
+ color: #00ff41;
+ font-family: 'Press Start 2P', monospace, 'Courier New';
+ resize: both; overflow: hidden;
+ animation: neon-pulse 2s ease-in-out infinite alternate;
+ `;
+
+ // Add Neon theme styles
+ const neonStyles = document.createElement('style');
+ neonStyles.id = 'auto-repair-neon-styles';
+ neonStyles.textContent = `
+ @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
+
+ #wplace-repair-tool::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, #00ff41, transparent);
+ z-index: 1;
+ pointer-events: none;
+ animation: scanline 3s linear infinite;
+ opacity: 0.7;
+ }
+
+ .neon-btn {
+ background: #16213e !important;
+ border: 2px solid #00ff41 !important;
+ border-radius: 0 !important;
+ color: #00ff41 !important;
+ padding: 8px 15px !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 8px !important;
+ text-transform: uppercase !important;
+ cursor: pointer !important;
+ transition: all 0.3s ease !important;
+ text-shadow: 0 0 8px #00ff41 !important;
+ letter-spacing: 1px !important;
+ margin: 4px !important;
+ }
+
+ .neon-btn:hover {
+ background: rgba(0,255,65,0.1) !important;
+ box-shadow: 0 0 20px rgb(0 255 65 / 60%) !important;
+ animation: pixel-blink 0.5s infinite !important;
+ }
+
+ .neon-btn:disabled {
+ background: #0d1117 !important;
+ border-color: #555 !important;
+ color: #555 !important;
+ text-shadow: none !important;
+ cursor: not-allowed !important;
+ }
+
+ .neon-btn.danger {
+ border-color: #ff073a !important;
+ color: #ff073a !important;
+ text-shadow: 0 0 8px #ff073a !important;
+ }
+
+ .neon-btn.danger:hover {
+ background: rgba(255, 7, 58, 0.1) !important;
+ box-shadow: 0 0 20px rgb(255 7 58 / 60%) !important;
+ }
+
+ .neon-btn.warning {
+ border-color: #ff6b35 !important;
+ color: #ff6b35 !important;
+ text-shadow: 0 0 8px #ff6b35 !important;
+ }
+
+ .neon-btn.warning:hover {
+ background: rgba(255, 107, 53, 0.1) !important;
+ box-shadow: 0 0 20px rgb(255 107 53 / 60%) !important;
+ }
+
+ .neon-input {
+ background: #16213e !important;
+ border: 2px solid #00ff41 !important;
+ border-radius: 0 !important;
+ color: #00ff41 !important;
+ padding: 6px 10px !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 8px !important;
+ text-shadow: 0 0 5px #00ff41 !important;
+ }
+
+ .neon-input:focus {
+ outline: none !important;
+ box-shadow: 0 0 15px rgb(0 255 65 / 60%) !important;
+ animation: pixel-blink 0.5s infinite !important;
+ }
+
+ .neon-select {
+ background: #16213e !important;
+ border: 2px solid #00ff41 !important;
+ border-radius: 0 !important;
+ color: #00ff41 !important;
+ padding: 4px 8px !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 7px !important;
+ text-shadow: 0 0 5px #00ff41 !important;
+ }
+
+ .neon-checkbox {
+ accent-color: #00ff41 !important;
+ transform: scale(1.2) !important;
+ margin-right: 8px !important;
+ }
+
+ /* Enhanced Batch Control Styles */
+ .batch-control-group {
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+ margin: 5px 0 !important;
+ padding: 8px !important;
+ background: rgba(0, 255, 65, 0.05) !important;
+ border: 1px solid rgba(0, 255, 65, 0.3) !important;
+ border-radius: 3px !important;
+ }
+
+ .batch-control-header {
+ text-align: center !important;
+ margin-bottom: 10px !important;
+ padding: 5px !important;
+ border-bottom: 1px solid rgba(0, 255, 65, 0.3) !important;
+ }
+
+ .batch-control-label {
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ white-space: nowrap !important;
+ }
+
+ .batch-mode-selection {
+ margin: 10px 0 !important;
+ }
+
+ .batch-mode-option {
+ margin: 8px 0 !important;
+ padding: 6px !important;
+ background: rgba(0, 255, 65, 0.03) !important;
+ border: 1px solid rgba(0, 255, 65, 0.2) !important;
+ border-radius: 3px !important;
+ transition: all 0.3s ease !important;
+ }
+
+ .batch-mode-option:hover {
+ background: rgba(0, 255, 65, 0.08) !important;
+ border-color: rgba(0, 255, 65, 0.4) !important;
+ }
+
+ .batch-mode-label {
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ margin-left: 8px !important;
+ }
+
+ .neon-radio {
+ accent-color: #00ff41 !important;
+ transform: scale(1.2) !important;
+ margin-right: 8px !important;
+ }
+
+ .batch-manual-controls {
+ display: flex !important;
+ align-items: center !important;
+ gap: 8px !important;
+ margin: 8px 0 !important;
+ padding: 8px !important;
+ background: rgba(0, 255, 65, 0.05) !important;
+ border: 1px solid rgba(0, 255, 65, 0.3) !important;
+ border-radius: 3px !important;
+ }
+
+ .batch-size-control {
+ display: flex !important;
+ align-items: center !important;
+ gap: 2px !important;
+ border: 1px solid #00ff41 !important;
+ border-radius: 3px !important;
+ background: #16213e !important;
+ overflow: hidden !important;
+ }
+
+ .batch-adjust-btn {
+ width: 20px !important;
+ height: 24px !important;
+ background: #16213e !important;
+ border: none !important;
+ color: #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 8px !important;
+ cursor: pointer !important;
+ transition: all 0.3s ease !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ }
+
+ .batch-adjust-btn:hover {
+ background: rgba(0, 255, 65, 0.1) !important;
+ box-shadow: 0 0 5px rgba(0, 255, 65, 0.3) !important;
+ }
+
+ .batch-adjust-btn:active {
+ background: rgba(0, 255, 65, 0.2) !important;
+ transform: scale(0.95) !important;
+ }
+
+ .batch-size-input {
+ width: 50px !important;
+ height: 24px !important;
+ background: #16213e !important;
+ border: none !important;
+ color: #00ff41 !important;
+ padding: 0 4px !important;
+ font-family: 'Press Start 2P', monospace !important;
+ font-size: 6px !important;
+ text-align: center !important;
+ text-shadow: 0 0 3px #00ff41 !important;
+ outline: none !important;
+ /* Hide browser default spinner buttons */
+ -webkit-appearance: none !important;
+ -moz-appearance: textfield !important;
+ }
+
+ /* Hide Chrome spinner buttons */
+ .batch-size-input::-webkit-outer-spin-button,
+ .batch-size-input::-webkit-inner-spin-button {
+ -webkit-appearance: none !important;
+ margin: 0 !important;
+ }
+
+ /* Hide Firefox spinner buttons */
+ .batch-size-input[type=number] {
+ -moz-appearance: textfield !important;
+ }
+
+ .status-info {
+ color: #00ff41 !important;
+ }
+
+ .status-warning {
+ color: #ff6b35 !important;
+ }
+
+ .neon-panel {
+ background: rgba(22, 33, 62, 0.3) !important;
+ border: 1px solid #00ff41 !important;
+ border-radius: 0 !important;
+ padding: 15px !important;
+ margin: 10px 0 !important;
+ box-shadow: inset 0 0 15px rgba(0, 255, 65, 0.1) !important;
+ }
+
+ .neon-text {
+ color: #00ff41 !important;
+ text-shadow: 0 0 5px #00ff41 !important;
+ font-family: 'Press Start 2P', monospace !important;
+ }
+
+ .neon-text.success {
+ color: #39ff14 !important;
+ text-shadow: 0 0 5px #39ff14 !important;
+ }
+
+ .neon-text.warning {
+ color: #ff6b35 !important;
+ text-shadow: 0 0 5px #ff6b35 !important;
+ }
+
+ .neon-text.danger {
+ color: #ff073a !important;
+ text-shadow: 0 0 5px #ff073a !important;
+ }
+
+ .neon-text.info {
+ color: #00ccff !important;
+ text-shadow: 0 0 5px #00ccff !important;
+ }
+
+ /* Custom scrollbar for debug console */
+ #debugConsole::-webkit-scrollbar {
+ width: 12px;
+ }
+
+ #debugConsole::-webkit-scrollbar-track {
+ background: #16213e;
+ border: 1px solid #00ff41;
+ }
+
+ #debugConsole::-webkit-scrollbar-thumb {
+ background: #00ff41;
+ border-radius: 0;
+ box-shadow: 0 0 10px #00ff41;
+ }
+
+ #debugConsole::-webkit-scrollbar-thumb:hover {
+ background: #39ff14;
+ box-shadow: 0 0 15px #39ff14;
+ }
+
+ /* Animations */
+ @keyframes neon-pulse {
+ 0% { box-shadow: 0 0 30px rgb(0 255 65 / 50%), inset 0 0 30px rgb(0 255 65 / 10%), 0 0 0 1px #00ff41; }
+ 100% { box-shadow: 0 0 40px rgb(0 255 65 / 70%), inset 0 0 40px rgb(0 255 65 / 15%), 0 0 0 1px #00ff41; }
+ }
+
+ @keyframes pixel-blink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0.7; }
+ }
+
+ @keyframes scanline {
+ 0% { transform: translateY(-100%); }
+ 100% { transform: translateY(400px); }
+ }
+
+ @keyframes text-glow {
+ 0% { text-shadow: 0 0 15px #00ff41; }
+ 100% { text-shadow: 0 0 25px #00ff41, 0 0 35px #00ff41; }
+ }
+ `;
+
+ if (!document.getElementById('auto-repair-neon-styles')) {
+ document.head.appendChild(neonStyles);
+ }
+
+ container.innerHTML = `
+
+
+
+
+
+ ${Utils.t('autonomousModeActive')}
+
+
+ Charges: 0/1 (cooldown: 31s)
+
+
+ Token: Initializing...
+
+
+ System: AUTONOMOUS | Token: GENERATOR | Auto-refresh: ON
+
+
+ Auto-Batch: ON | Size: 5 | Opt: ON
+
+
+
+
+
+ 📁 ${Utils.t('loadFromFile')}
+
+
+
+ 🔧 ${Utils.t('repairPixels')}
+
+
+
+ 🎯 Force Token
+
+
+
+
+
+
+
+ ${Utils.t('enableAutoRepair')} (Enhanced)
+
+
+
+
+ ${Utils.t('repairInterval')}:
+
+ seconds
+
+
+
+
+
+ Auto Token Refresh
+
+
+ Generator
+ Hybrid
+ Manual
+
+
+
+
+
+
+
+
+
+
+
+
+
Batch Size:
+
+ -
+
+ +
+
+
pixels
+
+
+
+
+ 🔧 Repair Transparent Pixels
+
+
+
+
+
+
Image: Not loaded
+
Position: Not set
+
Colors: 0 available
+
Batch Mode: Auto (${state.currentBatchSize} pixels)
+
+
+
+
+
+
+ 📋 ${Utils.t('debug')} (Enhanced System)
+
+
+ ${Utils.t('clearDebug')}
+
+
+
+
+
[Ready] WPlace Autonomous Repair Tool v3.0
+
[Info] NEW Turnstile implementation active
+
[Info] Enhanced token management with preloading
+
[Info] ${Utils.t('tokenSystemReady')}
+
+
+
+ `;
+
+ document.body.appendChild(container);
+ makeWindowDraggable();
+ setupEventListeners();
+
+ // Start typewriter effect
+ setTimeout(() => {
+ createTypewriterTitle();
+ }, 1000);
+
+ // Start autonomous systems
+ initializeAutonomousSystems();
+ }
+
+ function initializeAutonomousSystems() {
+ // Update charges periodically
+ updateCharges();
+ setInterval(updateCharges, 10000);
+
+ // Update token status
+ updateTokenStatus();
+ setInterval(updateTokenStatus, 5000);
+
+ // Update system status
+ updateSystemStatus();
+ setInterval(updateSystemStatus, 15000);
+
+ // Update batch information
+ updateBatchInfo();
+
+ // Initialize token generator
+ initializeTokenGenerator();
+ }
+
+ function setupEventListeners() {
+ // Window controls
+ document.getElementById('minimizeBtn').addEventListener('click', () => {
+ const content = document.getElementById('window-content');
+ state.windowMinimized = !state.windowMinimized;
+
+ if (state.windowMinimized) {
+ content.style.display = 'none';
+ document.getElementById('minimizeBtn').textContent = '+';
+ } else {
+ content.style.display = 'block';
+ document.getElementById('minimizeBtn').textContent = '−';
+ }
+ });
+
+ document.getElementById('closeBtn').addEventListener('click', () => {
+ const container = document.getElementById('wplace-repair-tool');
+ if (container) {
+ container.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
+ container.style.opacity = '0';
+ container.style.transform = 'scale(0.8)';
+ setTimeout(() => {
+ container.remove();
+ cleanup();
+ }, 300);
+ }
+ });
+
+ // Load file
+ document.getElementById('loadFileBtn').addEventListener('click', async () => {
+ try {
+ Utils.addDebugLog('Loading progress file...', 'info');
+ const data = await Utils.createFileUploader();
+
+ if (!data || !data.state || !data.imageData) {
+ throw new Error(Utils.t('invalidFile'));
+ }
+
+ const migratedData = migrateProgressData(data);
+
+ Object.assign(state, migratedData.state);
+ state.imageData = {
+ ...migratedData.imageData,
+ pixels: new Uint8ClampedArray(migratedData.imageData.pixels),
+ };
+
+ state.imageLoaded = true;
+ document.getElementById('repairBtn').disabled = false;
+
+ // Update info displays
+ document.getElementById('imageInfo').textContent = `${state.imageData.width}x${state.imageData.height}`;
+ document.getElementById('positionInfo').textContent = state.startPosition ?
+ `(${state.startPosition.x}, ${state.startPosition.y}) in region (${state.region.x}, ${state.region.y})` : 'Not set';
+ document.getElementById('colorsInfo').textContent = `${state.availableColors?.length || 0} available`;
+
+ updateStatus(Utils.t('fileLoaded'));
+ Utils.addDebugLog(`Loaded image: ${state.imageData.width}x${state.imageData.height}`, 'success');
+ Utils.addDebugLog(`Position: (${state.startPosition?.x}, ${state.startPosition?.y})`, 'info');
+ Utils.addDebugLog(`Region: (${state.region?.x}, ${state.region?.y})`, 'info');
+ Utils.addDebugLog(`Available colors: ${state.availableColors?.length || 0}`, 'info');
+
+ if (migratedData.state.paintWhitePixels !== undefined) {
+ state.paintWhitePixels = migratedData.state.paintWhitePixels;
+ Utils.addDebugLog(`Paint white pixels: ${state.paintWhitePixels}`, 'info');
+ }
+ if (migratedData.state.paintTransparentPixels !== undefined) {
+ state.paintTransparentPixels = migratedData.state.paintTransparentPixels;
+ Utils.addDebugLog(`Paint transparent pixels: ${state.paintTransparentPixels}`, 'info');
+ }
+
+ Utils.showAlert(Utils.t('fileLoaded'), 'success');
+
+ // If autonomous mode and auto repair is enabled, start it
+ if (state.autonomousMode && document.getElementById('autoRepairEnabled').checked) {
+ Utils.addDebugLog('Autonomous mode: starting auto repair after file load', 'info');
+ setTimeout(() => {
+ state.autoRepairEnabled = true;
+ startAutoRepair();
+ }, 5000);
+ }
+
+ } catch (error) {
+ Utils.addDebugLog(`Load error: ${error.message}`, 'error');
+ Utils.showAlert(error.message, 'error');
+ }
+ });
+
+ // Generate Token button
+ document.getElementById('generateTokenBtn').addEventListener('click', async () => {
+ const btn = document.getElementById('generateTokenBtn');
+ const originalText = btn.textContent;
+
+ try {
+ btn.disabled = true;
+ btn.textContent = '🔄 Generating...';
+ btn.style.background = 'linear-gradient(135deg, #ffa726 0%, #ff9800 100%)';
+
+ Utils.addDebugLog('Manual token generation requested', 'info');
+
+ const token = await ensureToken(true);
+
+ if (token) {
+ Utils.addDebugLog('Token generation succeeded!', 'success');
+ Utils.showAlert('Token generated successfully!', 'success');
+ updateTokenStatus();
+ } else {
+ throw new Error('Token generation failed');
+ }
+ } catch (error) {
+ Utils.addDebugLog(`Token generation failed: ${error.message}`, 'error');
+ Utils.showAlert('Token generation failed', 'error');
+ playNotificationSound();
+ } finally {
+ btn.disabled = false;
+ btn.textContent = originalText;
+ btn.style.background = 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)';
+ }
+ });
+
+ // Manual repair
+ let repairRunning = false;
+ document.getElementById('repairBtn').addEventListener('click', async () => {
+ if (!state.imageLoaded) {
+ Utils.showAlert('Please load a progress file first', 'warning');
+ return;
+ }
+
+ if (repairRunning) {
+ state.stopFlag = true;
+ Utils.addDebugLog('Manual repair stop requested', 'warning');
+ return;
+ }
+
+ repairRunning = true;
+ state.running = true;
+ state.stopFlag = false;
+ const repairBtn = document.getElementById('repairBtn');
+ repairBtn.textContent = '⏸️ Stop Repair';
+ repairBtn.style.background = 'linear-gradient(135deg, #ff6b35 0%, #f7931e 100%)';
+
+ try {
+ Utils.addDebugLog('Starting manual repair with enhanced autonomous system...', 'info');
+
+ if (!isTokenValid()) {
+ Utils.addDebugLog('No valid token, generating...', 'info');
+ await ensureToken(true);
+ }
+
+ const damagedPixels = await scanForDamage();
+
+ if (damagedPixels.length > 0) {
+ updateStatus(Utils.t('damageDetected', { count: damagedPixels.length }));
+ showAttackNotification('attack', damagedPixels.length);
+
+ const repairedCount = await repairDamagedPixels(damagedPixels);
+
+ if (repairedCount === damagedPixels.length) {
+ showAttackNotification('repaired');
+ }
+ } else {
+ updateStatus(Utils.t('noDamageDetected'));
+ showPeacefulState();
+ }
+ } catch (error) {
+ Utils.addDebugLog(`Repair error: ${error.message}`, 'error');
+ Utils.showAlert('Repair failed', 'error');
+ } finally {
+ repairRunning = false;
+ state.running = false;
+ state.stopFlag = false;
+ repairBtn.textContent = '🔧 ' + Utils.t('repairPixels');
+ repairBtn.style.background = 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)';
+ }
+ });
+
+ // Auto repair toggle
+ document.getElementById('autoRepairEnabled').addEventListener('change', (e) => {
+ state.autoRepairEnabled = e.target.checked;
+
+ if (state.autoRepairEnabled) {
+ if (!state.imageLoaded) {
+ Utils.showAlert('Please load a progress file first', 'warning');
+ e.target.checked = false;
+ state.autoRepairEnabled = false;
+ return;
+ }
+ startAutoRepair();
+ } else {
+ stopAutoRepair();
+ }
+ });
+
+ // Repair interval change
+ document.getElementById('repairInterval').addEventListener('change', (e) => {
+ const value = parseInt(e.target.value) || 30;
+ state.autoRepairInterval = Math.max(10, Math.min(3600, value));
+ e.target.value = state.autoRepairInterval;
+
+ Utils.addDebugLog(`Repair interval changed to ${state.autoRepairInterval} seconds`, 'info');
+
+ if (state.autoRepairEnabled && state.imageLoaded) {
+ Utils.addDebugLog('Restarting auto repair with new interval', 'info');
+ startAutoRepair();
+ }
+ });
+
+ // Auto token refresh toggle
+ document.getElementById('autoTokenRefresh').addEventListener('change', (e) => {
+ state.autoTokenRefresh = e.target.checked;
+ Utils.addDebugLog(`Auto token refresh ${state.autoTokenRefresh ? 'enabled' : 'disabled'}`, 'info');
+ updateSystemStatus();
+
+ if (state.autoTokenRefresh && state.autonomousMode && !isTokenValid()) {
+ ensureToken();
+ }
+ });
+
+ // Token source selection
+ document.getElementById('tokenSourceSelect').addEventListener('change', (e) => {
+ state.tokenSource = e.target.value;
+ Utils.addDebugLog(`Token source changed to: ${state.tokenSource}`, 'info');
+ updateSystemStatus();
+
+ // Invalidate current token to force new generation with new method
+ if (isTokenValid()) {
+ Utils.addDebugLog('Invalidating current token due to source change', 'info');
+ invalidateToken();
+ }
+ });
+
+ // NEW: Enhanced batch mode controls
+ document.getElementById('smartBatchMode').addEventListener('change', (e) => {
+ if (e.target.checked) {
+ state.autoBatchEnabled = true;
+ state.batchOptimization = true;
+ document.getElementById('manualBatchControls').style.display = 'none';
+ Utils.addDebugLog('Smart batch mode enabled - will auto-adjust to damage count', 'info');
+ updateBatchInfo();
+ }
+ });
+
+ document.getElementById('manualBatchMode').addEventListener('change', (e) => {
+ if (e.target.checked) {
+ state.autoBatchEnabled = false;
+ state.batchOptimization = false;
+ document.getElementById('manualBatchControls').style.display = 'flex';
+ Utils.addDebugLog('Manual batch mode enabled - using fixed batch size', 'info');
+ updateBatchInfo();
+ }
+ });
+
+ // Manual batch size controls with +/- buttons
+ document.getElementById('batchSizeIncrease').addEventListener('click', () => {
+ const newSize = Math.min(CONFIG.MAX_BATCH_SIZE, state.currentBatchSize + 1);
+ if (newSize !== state.currentBatchSize) {
+ state.currentBatchSize = newSize;
+ document.getElementById('batchSize').value = state.currentBatchSize;
+ Utils.addDebugLog(`Increased batch size to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ });
+
+ document.getElementById('batchSizeDecrease').addEventListener('click', () => {
+ const newSize = Math.max(CONFIG.MIN_BATCH_SIZE, state.currentBatchSize - 1);
+ if (newSize !== state.currentBatchSize) {
+ state.currentBatchSize = newSize;
+ document.getElementById('batchSize').value = state.currentBatchSize;
+ Utils.addDebugLog(`Decreased batch size to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ });
+
+ // Manual input allows direct typing for precise control
+ document.getElementById('batchSize').addEventListener('input', (e) => {
+ const value = parseInt(e.target.value);
+ if (!isNaN(value)) {
+ const clampedValue = Math.max(CONFIG.MIN_BATCH_SIZE, Math.min(CONFIG.MAX_BATCH_SIZE, value));
+ if (clampedValue !== state.currentBatchSize) {
+ state.currentBatchSize = clampedValue;
+ if (clampedValue !== value) {
+ e.target.value = state.currentBatchSize;
+ }
+ Utils.addDebugLog(`Manual batch size set to ${state.currentBatchSize} pixels`, 'info');
+ updateBatchInfo();
+ }
+ }
+ });
+
+ document.getElementById('paintTransparentPixels').addEventListener('change', (e) => {
+ state.paintTransparentPixels = e.target.checked;
+ Utils.addDebugLog(`Transparent pixel repair ${state.paintTransparentPixels ? 'enabled' : 'disabled'}`, 'info');
+ });
+
+ // Clear debug
+ document.getElementById('clearDebugBtn').addEventListener('click', () => {
+ state.debugLogs = [];
+ updateDebugConsole();
+ Utils.addDebugLog('Debug console cleared', 'info');
+ Utils.addDebugLog('Enhanced WPlace Autonomous Repair Tool ready', 'info');
+ });
+ }
+
+ // Enhanced data migration helper
+ function migrateProgressData(data) {
+ if (!data.version || data.version < '2.0') {
+ Utils.addDebugLog('Migrating old progress data format...', 'info');
+
+ if (!data.state) data.state = {};
+ if (!data.imageData) data.imageData = {};
+
+ data.state.paintWhitePixels = data.state.paintWhitePixels ?? true;
+ data.state.paintTransparentPixels = data.state.paintTransparentPixels ?? false;
+
+ data.version = '2.0';
+ }
+
+ // Additional migrations for autonomous features
+ if (data.version === '2.0') {
+ data.state.autonomousMode = data.state.autonomousMode ?? CONFIG.AUTONOMOUS_MODE;
+ data.state.autoTokenRefresh = data.state.autoTokenRefresh ?? CONFIG.AUTO_TOKEN_REFRESH;
+ data.state.tokenSource = data.state.tokenSource ?? CONFIG.TOKEN_SOURCE;
+ data.version = '3.0';
+ Utils.addDebugLog('Migrated to autonomous features v3.0', 'info');
+ }
+
+ return data;
+ }
+
+ // Main initialization function
+ async function initialize() {
+ Utils.addDebugLog('Starting WPlace Autonomous Repair Tool v3.0...', 'info');
+
+ // Create UI first
+ createUI();
+
+ Utils.addDebugLog('NEW Enhanced Features Initialized:', 'info');
+ Utils.addDebugLog('• NEW Turnstile implementation with widget reuse', 'info');
+ Utils.addDebugLog('• Enhanced sitekey detection from multiple sources', 'info');
+ Utils.addDebugLog('• Sophisticated token management with preloading', 'info');
+ Utils.addDebugLog('• Autonomous operation with smart retry logic', 'info');
+ Utils.addDebugLog('• Enhanced tile interception and caching', 'info');
+ Utils.addDebugLog('• Adaptive repair algorithms with failure handling', 'info');
+ Utils.addDebugLog('• Visual notifications and audio alerts', 'info');
+ Utils.addDebugLog('• Draggable window with minimize/close controls', 'info');
+
+ if (state.autonomousMode) {
+ Utils.addDebugLog('AUTONOMOUS MODE ACTIVE - Tool will operate independently', 'success');
+ Utils.addDebugLog('Instructions for autonomous operation:', 'info');
+ Utils.addDebugLog('1. Wait for initial setup to complete', 'info');
+ Utils.addDebugLog('2. Load a progress file using "Load Progress File"', 'info');
+ Utils.addDebugLog('3. Enable "Auto Repair" to start autonomous monitoring', 'info');
+ Utils.addDebugLog('4. Tool will automatically scan and repair damage', 'info');
+ Utils.addDebugLog('5. Tokens will be generated and refreshed automatically', 'info');
+ Utils.addDebugLog('6. Monitor debug console for autonomous operation logs', 'info');
+ } else {
+ Utils.addDebugLog('Manual mode active - use controls for manual operation', 'info');
+ }
+
+ // Show autonomous mode status
+ updateStatus(state.autonomousMode ? Utils.t('autonomousModeActive') : 'Manual mode ready - Load file and enable auto repair to start');
+
+ Utils.addDebugLog('WPlace Autonomous Repair Tool ready for operation', 'success');
+ }
+
+ // Enhanced cleanup function
+ function cleanup() {
+ Utils.addDebugLog('Cleaning up autonomous systems...', 'info');
+
+ // Clear all timers
+ if (state.autoRepairTimer) {
+ clearInterval(state.autoRepairTimer);
+ state.autoRepairTimer = null;
+ }
+
+ if (state.tokenRetryTimer) {
+ clearTimeout(state.tokenRetryTimer);
+ state.tokenRetryTimer = null;
+ }
+
+ if (state.tokenPreloadTimer) {
+ clearTimeout(state.tokenPreloadTimer);
+ state.tokenPreloadTimer = null;
+ }
+
+ // Cleanup Turnstile
+ Utils.cleanupTurnstile();
+
+ // Set stop flag
+ state.stopFlag = true;
+
+ Utils.addDebugLog('Cleanup completed', 'info');
+ }
+
+ // Add cleanup on page unload
+ window.addEventListener('beforeunload', cleanup);
+
+ // Handle page visibility changes for autonomous mode
+ document.addEventListener('visibilitychange', () => {
+ if (state.autonomousMode) {
+ if (document.hidden) {
+ Utils.addDebugLog('Page hidden - autonomous systems continue running', 'info');
+ } else {
+ Utils.addDebugLog('Page visible - autonomous systems active', 'info');
+ // Refresh status when page becomes visible
+ updateCharges();
+ updateTokenStatus();
+ }
+ }
+ });
+
+ // Enhanced error handling for autonomous mode
+ window.addEventListener('error', (event) => {
+ if (state.autonomousMode) {
+ Utils.addDebugLog(`Global error in autonomous mode: ${event.error?.message || 'Unknown error'}`, 'error');
+ }
+ });
+
+ window.addEventListener('unhandledrejection', (event) => {
+ if (state.autonomousMode) {
+ Utils.addDebugLog(`Unhandled promise rejection in autonomous mode: ${event.reason}`, 'error');
+ }
+ });
+
+ // Start the enhanced autonomous application
+ initialize().catch(error => {
+ console.error('Failed to initialize autonomous repair tool:', error);
+ Utils.addDebugLog(`Initialization failed: ${error.message}`, 'error');
+ });
+
+})();
diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css
new file mode 100644
index 0000000..10995a5
--- /dev/null
+++ b/Extension/themes/acrylic.css
@@ -0,0 +1,629 @@
+/* WPlace Auto-Image Bot - Acrylic Theme */
+/* Beautiful theme with translucent blurred panels */
+
+@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
+
+@font-face {
+ font-family: 'Apple Color Emoji';
+ src: url('https://raw.githubusercontent.com/GORAlexComp/AppleColorEmojiFont/refs/heads/main/fonts/AppleColorEmoji.ttf');
+}
+
+/* Multi-account panel styling */
+.wplace-theme-acrylic #wplace-accounts-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: none;
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style)
+ var(--wplace-border-color);
+ min-width: 250px;
+}
+
+.wplace-theme-acrylic #wplace-accounts-container * {
+ font-size: 15px;
+ line-height: normal;
+}
+
+/* Active account styles */
+.wplace-theme-acrylic .wplace-accounts-container .active-account {
+ --active-bg: color-mix(in srgb, white 15%, transparent);
+ --active-color: var(--wplace-success);
+ --inactive-color: var(--wplace-text);
+ --progress-bg: rgb(255 255 255 / 10%);
+ --progress-fill: white;
+}
+
+/* Main Container */
+.wplace-theme-acrylic #wplace-image-bot-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: none;
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style)
+ var(--wplace-border-color);
+ min-width: 300px;
+}
+
+.wplace-theme-acrylic #wplace-image-bot-container * {
+ font-size: 15px;
+ line-height: normal;
+}
+
+.wplace-theme-acrylic .wplace-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding: 10px;
+}
+
+#wplace-image-bot-container.wplace-dragging {
+ transition: none !important;
+ box-shadow: none !important;
+ transform: none !important;
+}
+
+/* Stats */
+.wplace-theme-acrylic #wplace-stats-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: none;
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style)
+ var(--wplace-border-color);
+}
+
+.wplace-theme-acrylic .wplace-stats {
+ margin-bottom: 0;
+}
+
+.wplace-theme-acrylic .wplace-colors-section {
+ border-top: none;
+ margin: 0;
+}
+
+/* Settings Container */
+.wplace-theme-acrylic #wplace-settings-container {
+ background: var(--wplace-primary) !important;
+ color: var(--wplace-text) !important;
+ border-radius: var(--wplace-radius) !important;
+ box-shadow: none !important;
+ backdrop-filter: var(--wplace-backdrop) !important;
+ border: var(--wplace-border-width) var(--wplace-border-style)
+ var(--wplace-border-color) !important;
+ max-width: 400px !important;
+}
+
+.wplace-theme-acrylic .wplace-settings-header {
+ background: transparent;
+ padding: 0;
+}
+
+.wplace-theme-acrylic .wplace-settings-header:hover {
+ background: transparent !important;
+}
+
+.wplace-theme-acrylic .wplace-settings-icon {
+ animation: none;
+ font-size: 15px;
+}
+
+.wplace-theme-acrylic .wplace-settings-content {
+ padding: 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.wplace-theme-acrylic .wplace-settings-footer {
+ border-top: none;
+ padding: 10px;
+}
+
+.wplace-theme-acrylic .wplace-settings-section {
+ margin: 0;
+}
+
+.wplace-theme-acrylic .wplace-settings-section-wrapper,
+.wplace-theme-acrylic .wplace-overlay-wrapper,
+.wplace-theme-acrylic .wplace-batch-controls {
+ padding: 0 !important;
+ background: transparent !important;
+ border: none !important;
+}
+
+/* Headers */
+.wplace-theme-acrylic .wplace-header,
+.wplace-theme-acrylic .wplace-settings-title-wrapper {
+ height: 40px;
+ padding: 7px 7px 7px 10px;
+ font-weight: 600;
+ background: var(--wplace-accent);
+ color: var(--wplace-highlight);
+}
+
+.wplace-theme-acrylic .wplace-header span,
+.wplace-theme-acrylic .wplace-settings-title {
+ max-width: 150px;
+ font-size: 15px;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.wplace-theme-acrylic .wplace-header-controls #compactBtn {
+ display: none;
+}
+
+.wplace-theme-acrylic .wplace-header-btn,
+.wplace-theme-acrylic .wplace-settings-close-btn {
+ background: #2e2e2e;
+ color: var(--wplace-highlight);
+ border: none;
+ font-size: 12px;
+ border-radius: 4px;
+ width: 24px;
+ height: 24px;
+}
+
+.wplace-theme-acrylic .wplace-header-btn i {
+ font-size: 12px !important;
+}
+
+.wplace-theme-acrylic .wplace-header-btn:hover,
+.wplace-theme-acrylic .wplace-settings-close-btn:hover {
+ background: transparent;
+ transform: none;
+}
+
+/* Sections */
+.wplace-theme-acrylic .wplace-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ background: transparent;
+ padding: 0;
+ border: none;
+ margin: 0;
+}
+
+.wplace-theme-acrylic .wplace-section-title {
+ color: var(--wplace-highlight);
+ text-transform: capitalize;
+ font-size: 13px;
+ font-weight: 600;
+ pointer-events: none;
+ letter-spacing: unset;
+}
+
+.wplace-theme-acrylic .wplace-section-title i {
+ display: none;
+}
+
+.wplace-theme-acrylic .wplace-status-section {
+ background: transparent;
+ padding: 0;
+ margin: 0;
+ border: none;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+/* Status Styling */
+.wplace-theme-acrylic .wplace-stats {
+ background: transparent;
+ padding: 0;
+ border: none;
+}
+
+.wplace-theme-acrylic .wplace-stat-value {
+ color: var(--wplace-highlight);
+}
+
+.wplace-theme-acrylic .wplace-status {
+ border-radius: var(--wplace-radius);
+ border: none;
+ box-shadow: none;
+ padding: 10px;
+ font-weight: 500;
+}
+
+.wplace-theme-acrylic .status-default {
+ background: color-mix(in srgb, white 15%, transparent) !important;
+}
+
+.wplace-theme-acrylic .status-success {
+ background: color-mix(in srgb, var(--wplace-success) 15%, transparent);
+ color: var(--wplace-success);
+}
+
+.wplace-theme-acrylic .status-error {
+ background: color-mix(in srgb, var(--wplace-error) 15%, transparent);
+ color: var(--wplace-error);
+}
+
+.wplace-theme-acrylic .status-warning {
+ background: color-mix(in srgb, var(--wplace-warning) 15%, transparent);
+ color: var(--wplace-warning);
+}
+
+/* Buttons */
+.wplace-theme-acrylic .wplace-btn {
+ border-radius: 8px;
+ border: none;
+ height: max-content;
+ padding: 10px;
+ gap: 5px;
+ flex-direction: column;
+ align-items: flex-start;
+ line-height: normal;
+ text-align: left;
+ font-size: 15px !important;
+ box-shadow: none;
+}
+
+.wplace-theme-acrylic .wplace-btn::before {
+ display: none;
+}
+
+.wplace-theme-acrylic .wplace-btn:disabled {
+ box-shadow: none !important;
+}
+
+.wplace-theme-acrylic .wplace-btn:hover:not(:disabled) {
+ transform: none;
+ animation: none;
+ box-shadow: none !important;
+ filter: brightness(0.9);
+}
+
+.wplace-theme-acrylic .wplace-btn-primary,
+.wplace-theme-acrylic .wplace-btn-upload {
+ background: white !important;
+ color: black !important;
+ border: none !important;
+}
+
+.wplace-theme-acrylic .wplace-btn-upload:hover:not(:disabled) {
+ background: revert !important;
+}
+
+.wplace-theme-acrylic .wplace-btn-secondary,
+.wplace-theme-acrylic .wplace-btn-select,
+.wplace-theme-acrylic .wplace-btn-file,
+.wplace-theme-acrylic .wplace-btn-overlay {
+ background: var(--wplace-secondary);
+}
+
+.wplace-theme-acrylic .wplace-btn-start {
+ background: var(--wplace-success);
+ color: white;
+}
+
+.wplace-theme-acrylic .wplace-btn-stop {
+ background: var(--wplace-error);
+ color: white;
+}
+
+.wplace-theme-acrylic .wplace-btn.active {
+ background: white;
+ color: black;
+ box-shadow: none !important;
+}
+
+.wplace-theme-acrylic .wplace-btn.active i {
+ filter: none;
+}
+
+.wplace-theme-acrylic .wplace-settings-apply-btn {
+ background: white;
+ color: black;
+ border-radius: 8px;
+}
+
+/* Compact Input Buttons */
+.wplace-theme-acrylic .wplace-input-btn-small,
+.wplace-theme-acrylic .wplace-input-btn-small:hover,
+.wplace-theme-acrylic .wplace-input-btn-small:active {
+ background: white;
+ color: black;
+ border: none;
+ transform: none;
+ border-radius: 4px;
+ box-shadow: none;
+ font-weight: 500;
+ width: 25px;
+ height: 25px;
+}
+
+/* Alerts */
+.wplace-theme-acrylic .wplace-alert-base {
+ border-radius: var(--wplace-radius);
+ backdrop-filter: var(--wplace-backdrop);
+ border: none;
+ box-shadow: none;
+}
+
+.wplace-theme-acrylic .wplace-alert-info {
+ background: color-mix(in srgb, #0000e5 50%, transparent);
+}
+
+.wplace-theme-acrylic .wplace-alert-success {
+ background: color-mix(in srgb, var(--wplace-success) 50%, transparent);
+}
+
+.wplace-theme-acrylic .wplace-alert-error {
+ background: color-mix(in srgb, var(--wplace-error) 50%, transparent);
+}
+
+.wplace-theme-acrylic .wplace-alert-warning {
+ background: color-mix(in srgb, var(--wplace-warning) 50%, transparent);
+}
+
+/* Progress Bar */
+.wplace-theme-acrylic .wplace-progress {
+ background: var(--wplace-secondary);
+ border-radius: 4px;
+ margin: 0;
+ border: none;
+}
+
+.wplace-theme-acrylic .wplace-progress-bar {
+ background: white;
+}
+
+.wplace-theme-acrylic .wplace-progress-bar::after {
+ display: none;
+}
+
+/* Form Controls */
+.wplace-theme-acrylic .wplace-settings-checkbox,
+.wplace-theme-acrylic .wplace-notification-checkbox,
+.wplace-theme-acrylic .wplace-speed-checkbox {
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ accent-color: var(--wplace-highlight);
+}
+
+.wplace-theme-acrylic .wplace-settings-select,
+.wplace-theme-acrylic .wplace-settings-number-input,
+.wplace-theme-acrylic .wplace-cooldown-input,
+.wplace-theme-acrylic .wplace-notification-interval-input {
+ background: rgb(255 255 255 / 10%);
+ color: white;
+ border: 1px solid rgb(255 255 255 / 25%);
+ border-radius: 8px;
+ box-shadow: none;
+ transition-duration: 0.3s;
+}
+
+.wplace-theme-acrylic .wplace-settings-select:hover,
+.wplace-theme-acrylic .wplace-settings-number-input:hover,
+.wplace-theme-acrylic .wplace-cooldown-input:hover,
+.wplace-theme-acrylic .wplace-notification-interval-input:hover {
+ border-color: rgb(255 255 255 / 50%);
+}
+
+.wplace-theme-acrylic .wplace-settings-select:focus,
+.wplace-theme-acrylic .wplace-settings-number-input:focus,
+.wplace-theme-acrylic .wplace-cooldown-input:focus,
+.wplace-theme-acrylic .wplace-notification-interval-input:focus {
+ border-color: white;
+}
+
+.wplace-theme-acrylic .wplace-settings-option {
+ background: #2d3748;
+ color: white;
+}
+
+.wplace-theme-acrylic .wplace-settings-select option {
+ background: #2d3748;
+ color: white;
+}
+
+.wplace-theme-acrylic .wplace-cooldown-input {
+ border-radius: 4px !important;
+}
+
+.wplace-theme-acrylic .wplace-cooldown-input:focus {
+ background: rgb(255 255 255 / 10%);
+ box-shadow: none;
+}
+
+.wplace-theme-acrylic .wplace-overlay-opacity-value {
+ background-color: var(--wplace-secondary) !important;
+ border: none !important;
+}
+
+/* Sliders */
+.wplace-theme-acrylic .wplace-slider,
+.wplace-theme-acrylic .wplace-speed-slider,
+.wplace-theme-acrylic .wplace-overlay-opacity-slider {
+ background: var(--wplace-secondary) !important;
+ border-radius: 3px !important;
+ height: 6px !important;
+}
+
+.wplace-theme-acrylic .wplace-slider::-webkit-slider-thumb,
+.wplace-theme-acrylic .resize-slider::-webkit-slider-thumb,
+.wplace-theme-acrylic .wplace-speed-slider::-webkit-slider-thumb,
+.wplace-theme-acrylic .wplace-overlay-opacity-slider::-webkit-slider-thumb {
+ width: 16px !important;
+ height: 16px !important;
+ border-radius: 4px !important;
+ background: white !important;
+ box-shadow: none !important;
+ transform: none !important;
+}
+
+.wplace-theme-acrylic .wplace-speed-value {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ border-radius: 8px;
+ color: white;
+ box-shadow: 0 3px 10px rgb(79 172 254 / 30%);
+ border: 1px solid rgb(255 255 255 / 20%);
+}
+
+.wplace-theme-acrylic .resize-slider {
+ -webkit-appearance: revert;
+}
+
+/* Layout Controls */
+.wplace-theme-acrylic .wplace-slider-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ margin: 0;
+}
+
+.wplace-theme-acrylic .wplace-input-label-compact {
+ color: rgba(255, 255, 255, 0.75);
+ margin-left: 4px;
+ text-transform: lowercase;
+ white-space: nowrap;
+}
+
+.wplace-theme-acrylic .wplace-cooldown-control {
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.wplace-theme-acrylic .wplace-cooldown-control label {
+ font-weight: 400;
+ margin-bottom: 0;
+ display: block;
+}
+
+.wplace-theme-acrylic .wplace-cooldown-input-group {
+ margin: 0;
+ background: transparent;
+}
+
+/* Color Elements */
+.wplace-theme-acrylic .wplace-color-item-name {
+ color: #ccc;
+}
+
+.wplace-theme-acrylic .wplace-color-swatch {
+ border: 1px solid rgb(255 255 255 / 20%);
+ border-radius: 4px;
+}
+
+.wplace-theme-acrylic .wplace-color-swatch:not(.active) {
+ filter: grayscale(80%);
+}
+
+.wplace-theme-acrylic .wplace-color-swatch.unavailable {
+ border-color: #666;
+}
+
+.wplace-theme-acrylic .wplace-color-swatch.unavailable:not(.active) {
+ filter: grayscale(90%);
+}
+
+.wplace-theme-acrylic .wplace-color-swatch.active::after {
+ text-shadow: 0 0 3px black;
+}
+
+.wplace-theme-acrylic .wplace-stat-color-swatch {
+ border-radius: 3px;
+ border: 1px solid rgb(255 255 255 / 10%);
+ box-shadow: inset 0 0 2px rgb(0 0 0 / 50%);
+}
+
+.wplace-theme-acrylic .wplace-stat-colors-grid {
+ background: var(--wplace-secondary);
+ border-radius: 8px;
+}
+
+.wplace-theme-acrylic .wplace-color-divider {
+ background: rgb(255 255 255 / 10%);
+}
+
+/* Icons */
+.wplace-theme-acrylic .wplace-icon-key,
+.wplace-theme-acrylic .wplace-icon-robot,
+.wplace-theme-acrylic .wplace-icon-speed,
+.wplace-theme-acrylic .wplace-icon-bell,
+.wplace-theme-acrylic .wplace-icon-palette,
+.wplace-theme-acrylic .wplace-icon-globe,
+.wplace-theme-acrylic .wplace-icon-paint,
+.wplace-theme-acrylic .wplace-icon-eye,
+.wplace-theme-acrylic .wplace-icon-compass,
+.wplace-theme-acrylic .wplace-icon-route,
+.wplace-theme-acrylic .wplace-icon-table,
+.wplace-theme-acrylic .wplace-icon-dice {
+ color: white;
+}
+
+/* Typography */
+.wplace-theme-acrylic .wplace-cooldown-unit {
+ display: none;
+}
+
+.wplace-theme-acrylic .wplace-settings-toggle-title,
+.wplace-theme-acrylic .wplace-overlay-opacity-label {
+ font-size: 15px !important;
+}
+
+.wplace-theme-acrylic .wplace-settings-description,
+.wplace-theme-acrylic .wplace-random-batch-description,
+.wplace-theme-acrylic .wplace-settings-toggle-description {
+ font-size: 12px;
+ color: rgb(255 255 255 / 75%) !important;
+}
+
+/* Turnstile Overlay */
+.wplace-theme-acrylic .wplace-turnstile-overlay {
+ background: var(--wplace-primary);
+ border-radius: var(--wplace-radius);
+ box-shadow: none;
+ backdrop-filter: var(--wplace-backdrop);
+ border: none;
+ padding: 16px !important;
+ color: var(--wplace-text) !important;
+}
+
+.wplace-theme-acrylic .wplace-turnstile-hide-btn {
+ border-radius: 8px !important;
+ backdrop-filter: var(--wplace-backdrop) !important;
+ background: var(--wplace-primary) !important;
+ top: -25px !important;
+ right: 0 !important;
+ transition: all 0.3s ease !important;
+}
+
+.wplace-theme-acrylic .wplace-turnstile-hide-btn:hover {
+ background: white !important;
+ color: black !important;
+}
+
+/* Resize Tools */
+.wplace-theme-acrylic .resize-tools button {
+ border-radius: 6px;
+ border: 1px solid rgb(255 255 255 / 20%);
+ background: rgb(255 255 255 / 6%);
+ color: #fff;
+}
+
+.wplace-theme-acrylic .resize-overlay {
+ background: rgb(0 0 0 / 50%);
+}
+
+/* TODO: More resize menu styles */
+.wplace-theme-acrylic .resize-container {
+ background: var(--wplace-secondary);
+ backdrop-filter: var(--wplace-backdrop);
+ border: none;
+ border-radius: var(--wplace-radius);
+ color: var(--wplace-text);
+}
diff --git a/themes/classic-light.css b/Extension/themes/classic-light.css
similarity index 93%
rename from themes/classic-light.css
rename to Extension/themes/classic-light.css
index b8079fc..b4d78b1 100644
--- a/themes/classic-light.css
+++ b/Extension/themes/classic-light.css
@@ -1,598 +1,618 @@
-/* WPlace Auto-Image Bot - Classic Light Theme */
-
-/* Clean, bright theme based on classic design with light backgrounds */
-
-.wplace-theme-classic-light {
- /* === CORE COLORS === */
- --wplace-primary: #fff; /* Clean white */
- --wplace-secondary: #f8f9fa; /* Light gray */
- --wplace-accent: #e9ecef; /* Lighter gray */
- --wplace-text: #212529; /* Dark text */
- --wplace-highlight: #6f42c1; /* Purple highlight */
- --wplace-success: #28a745; /* Bootstrap green */
- --wplace-error: #dc3545; /* Bootstrap red */
- --wplace-warning: #ffc107; /* Bootstrap yellow */
-
- /* === UI PROPERTIES === */
- --wplace-radius: 8px;
- --wplace-btn-radius: 12px;
- --wplace-border-style: solid;
- --wplace-border-width: 1px;
- --wplace-border-color: #415a77;
- --wplace-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%);
- --wplace-backdrop: blur(10px);
- --wplace-font: 'Segoe UI', roboto, sans-serif;
-
- /* === EXTENDED COLOR PALETTE === */
-
- /* Icon colors */
- --wplace-icon-primary: #4facfe; /* Light blue */
- --wplace-icon-secondary: #00f2fe; /* Cyan */
- --wplace-icon-palette: #f093fb; /* Pink */
-
- /* Additional UI colors */
- --wplace-danger: #dc3545; /* Bootstrap red */
- --wplace-danger-dark: #c82333; /* Darker red */
- --wplace-muted-text: rgb(33 37 41 / 70%);
- --wplace-highlight-secondary: #d3a4ff;
-
- /* Text variants */
- --wplace-text-secondary: rgb(33 37 41 / 90%);
- --wplace-text-muted: rgb(33 37 41 / 70%);
- --wplace-text-dim: rgb(33 37 41 / 60%);
- --wplace-text-faded: rgb(33 37 41 / 80%);
-
- /* Background variants */
- --wplace-bg-input: #f3f3f3; /* neutral-100 */
- --wplace-bg-subtle: #f3f3f3; /* neutral-100 — hover */
- --wplace-bg-faint: #e0e0e0; /* neutral-200 */
- --wplace-bg-ghost: #d1d1d1; /* neutral-300 */
- --wplace-bg-whisper: #e0e0e0; /* can be used as whisper on hover */
-
- /* Border variants */
- --wplace-border-subtle: #d1d1d1; /* neutral-300 */
- --wplace-border-faint: #e0e0e0; /* neutral-200 */
- --wplace-border-ghost: #e0e0e0; /* neutral-200 */
- --wplace-border-ultra-faint: #e0e0e0; /* neutral-200 — very faint */
-
- /* Shadow variants */
- --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 2px rgb(111 66 193 / 30%);
- --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 15%);
- --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 20%), 0 0 0 2px var(--wplace-icon-primary);
- --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 25%), 0 0 0 3px var(--wplace-icon-primary);
-
- /* Animation colors */
- --wplace-pulse-start: rgb(40 167 69 / 70%);
- --wplace-pulse-mid: rgb(40 167 69 / 0%);
- --wplace-pulse-end: rgb(40 167 69 / 0%);
-
- /* Slider colors */
- --wplace-slider-thumb-bg: #6f42c1;
- --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
-}
-
-/* === COMPONENT STYLING === */
-
-/* Main container with clean light styling */
-.wplace-theme-classic-light #wplace-image-bot-container {
- background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
- color: var(--wplace-text) !important;
- border: 1px solid rgb(0 0 0 / 15%) !important;
- border-radius: 12px !important;
- box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- backdrop-filter: blur(10px) !important;
-}
-
-
-/* Stats container with proper contrast */
-.wplace-theme-classic-light #wplace-stats-container {
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 12px !important;
- box-shadow: 0 8px 32px rgb(0 0 0 / 15%) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- color: var(--wplace-text) !important;
- position: fixed !important;
- overflow: hidden !important;
- z-index: 9998 !important;
-}
-
-/* Headers with light gradient */
-.wplace-theme-classic-light .wplace-header {
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
- color: var(--wplace-highlight) !important;
- border-bottom: 1px solid rgb(0 0 0 / 15%) !important;
- text-shadow: none !important;
-}
-
-/* Stats header styling */
-.wplace-theme-classic-light #wplace-stats-container .wplace-header {
- background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
- color: var(--wplace-text) !important;
- border-bottom: 1px solid rgb(0 0 0 / 20%) !important;
- text-shadow: none !important;
- font-weight: 600 !important;
-}
-
-/* Stats title specific styling */
-.wplace-theme-classic-light #wplace-stats-container .wplace-header .wplace-stats-title {
- color: var(--wplace-text) !important;
- text-shadow: none !important;
-}
-
-/* Comprehensive text and element styling for light theme - scoped to bot containers only */
-
-.wplace-theme-classic-light .wplace-status,
-.wplace-theme-classic-light .wplace-stats,
-.wplace-theme-classic-light .wplace-section,
-.wplace-theme-classic-light .wplace-controls,
-.wplace-theme-classic-light .wplace-data-management,
-.wplace-theme-classic-light .wplace-cooldown-settings {
- color: var(--wplace-text) !important;
-}
-
-/* Buttons with light styling */
-.wplace-theme-classic-light .wplace-btn {
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 12px !important;
- color: var(--wplace-text) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- font-weight: 500 !important;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
-}
-
-/* Data management section buttons */
-.wplace-theme-classic-light .wplace-data-management .wplace-btn {
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- color: #212529 !important;
- font-weight: 500 !important;
-}
-
-.wplace-theme-classic-light .wplace-btn:hover:not(:disabled) {
- background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important;
- transform: translateY(-1px) !important;
-}
-
-.wplace-theme-classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) {
- background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
- box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important;
-}
-
-/* Settings dialog */
-.wplace-theme-classic-light #wplace-settings-container {
- background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
- border: 1px solid rgb(0 0 0 / 15%) !important;
- border-radius: 12px !important;
- box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- backdrop-filter: blur(10px) !important;
- color: var(--wplace-text) !important;
-}
-
-/* Settings dialog text elements */
-.wplace-theme-classic-light .wplace-stat-colors-grid * {
- color: var(--wplace-text) !important;
- font-weight: 500 !important;
-}
-
-.wplace-theme-classic-light #wplace-settings-container * {
- color: var(--wplace-text) !important;
-}
-
-.wplace-theme-classic-light #wplace-settings-container .wplace-settings-section-wrapper * {
- color: var(--wplace-text) !important;
-}
-
-/* Color palette text labels */
-.wplace-theme-classic-light .wplace-color-label {
- color: var(--wplace-text) !important;
- font-weight: 500 !important;
- text-shadow: none !important;
-}
-
-.wplace-theme-classic-light .wplace-color-name {
- color: var(--wplace-text) !important;
- font-weight: 500 !important;
-}
-
-.wplace-theme-classic-light .wplace-color-item-name {
- color: #000 !important;
- font-weight: 600 !important;
- text-shadow: none !important;
-}
-
-/* Resize dialog */
-.wplace-theme-classic-light .resize-container {
- background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
- border: 1px solid rgb(0 0 0 / 15%) !important;
- border-radius: 12px !important;
- box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- backdrop-filter: blur(10px) !important;
-}
-
-/* Settings header */
-.wplace-theme-classic-light .wplace-settings-header {
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
- border-bottom: 1px solid rgb(0 0 0 / 15%) !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-title {
- color: var(--wplace-text) !important;
- text-shadow: none !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-close-btn {
- background: rgb(0 0 0 / 5%) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 50% !important;
- color: var(--wplace-text) !important;
- transition: all 0.3s ease !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-close-btn:hover {
- background: rgb(220 53 69 / 10%) !important;
- border-color: var(--wplace-error) !important;
- box-shadow: 0 0 12px rgb(220 53 69 / 30%) !important;
-}
-
-/* Section titles */
-.wplace-theme-classic-light .wplace-section-title {
- color: var(--wplace-highlight) !important;
- text-shadow: none !important;
- font-weight: 600 !important;
-}
-
-/* Button variants with classic light colors */
-.wplace-theme-classic-light .wplace-btn-start {
- background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
- color: white !important;
- font-weight: 600 !important;
-}
-
-.wplace-theme-classic-light .wplace-btn-start:hover:not(:disabled) {
- box-shadow: 0 4px 12px rgb(40 167 69 / 40%) !important;
-}
-
-.wplace-theme-classic-light .wplace-btn-stop {
- background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
- color: white !important;
- font-weight: 600 !important;
-}
-
-.wplace-theme-classic-light .wplace-btn-stop:hover:not(:disabled) {
- box-shadow: 0 4px 12px rgb(220 53 69 / 40%) !important;
-}
-
-.wplace-theme-classic-light .wplace-btn-upload {
- background: rgb(111 66 193 / 10%) !important;
- border: 2px dashed var(--wplace-highlight) !important;
- color: var(--wplace-highlight) !important;
-}
-
-.wplace-theme-classic-light .wplace-btn-upload:hover:not(:disabled) {
- background: rgb(111 66 193 / 15%) !important;
- box-shadow: 0 4px 12px rgb(111 66 193 / 20%) !important;
-}
-
-/* Progress bars with clean light styling */
-.wplace-theme-classic-light .wplace-progress {
- background: rgb(0 0 0 / 10%) !important;
- border: 1px solid rgb(0 0 0 / 15%) !important;
- border-radius: 12px !important;
-}
-
-.wplace-theme-classic-light .wplace-progress-bar {
- background: linear-gradient(135deg, #6f42c1 0%, #9370db 100%) !important;
- box-shadow: none !important;
-}
-
-.wplace-theme-classic-light .wplace-progress-bar::after {
- background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent) !important;
-}
-
-/* Status indicators with light colors */
-.wplace-theme-classic-light .status-success {
- background: rgb(40 167 69 / 10%) !important;
- border-color: var(--wplace-success) !important;
- color: var(--wplace-success) !important;
- box-shadow: 0 0 15px rgb(40 167 69 / 20%) !important;
- text-shadow: none !important;
-}
-
-.wplace-theme-classic-light .status-error {
- background: rgb(220 53 69 / 10%) !important;
- border-color: var(--wplace-error) !important;
- color: var(--wplace-error) !important;
- box-shadow: 0 0 15px rgb(220 53 69 / 20%) !important;
- text-shadow: none !important;
-}
-
-.wplace-theme-classic-light .status-default {
- background: rgb(111 66 193 / 10%) !important;
- border-color: var(--wplace-highlight) !important;
- color: var(--wplace-highlight) !important;
- text-shadow: none !important;
-}
-
-.wplace-theme-classic-light .wplace-stat-label {
- color: var(--wplace-text) !important;
- text-shadow: none !important;
- font-weight: 500 !important;
-}
-
-.wplace-theme-classic-light .wplace-stat-value {
- color: var(--wplace-highlight) !important;
- text-shadow: none !important;
- font-weight: 600 !important;
-}
-
-/* Sections with light styling */
-.wplace-theme-classic-light .wplace-section {
- background: rgb(0 0 0 / 3%) !important;
- border: 1px solid rgb(0 0 0 / 10%) !important;
- border-radius: 12px !important;
-}
-
-.wplace-theme-classic-light .wplace-status-section {
- background: rgb(0 0 0 / 3%) !important;
- border: 1px solid rgb(0 0 0 / 10%) !important;
- border-radius: 12px !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-section-wrapper {
- background: rgb(0 0 0 / 3%) !important;
- border: 1px solid rgb(0 0 0 / 10%) !important;
- border-radius: 12px !important;
-}
-
-/* Form controls with light styling */
-.wplace-theme-classic-light .wplace-settings-select {
- background: #fff !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 8px !important;
- color: var(--wplace-text) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-select:focus {
- border-color: var(--wplace-highlight) !important;
- box-shadow: 0 0 0 2px rgb(111 66 193 / 30%) !important;
-}
-
-/* Dropdown menu options */
-.wplace-theme-classic-light .wplace-settings-select option {
- background: #fff !important;
- color: var(--wplace-text) !important;
-}
-
-.wplace-theme-classic-light .wplace-settings-option {
- background: #fff !important;
- color: var(--wplace-text) !important;
-}
-
-/* Sliders with classic gradient */
-.wplace-theme-classic-light .wplace-speed-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
- border-radius: 4px !important;
-}
-
-.wplace-theme-classic-light .wplace-overlay-opacity-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
- border-radius: 4px !important;
-}
-
-.wplace-theme-classic-light .wplace-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
- border-radius: 4px !important;
-}
-
-.wplace-theme-classic-light .wplace-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: var(--wplace-slider-thumb-bg);
- border: 1px solid rgb(0 0 0 / 20%);
- cursor: pointer;
- transition: all 0.2s ease;
- box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
-}
-
-.wplace-theme-classic-light .wplace-speed-value {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important;
- border-radius: 8px !important;
- color: white !important;
- font-weight: 600 !important;
- box-shadow: 0 3px 10px rgb(79 172 254 / 30%) !important;
- border: 1px solid rgb(0 0 0 / 10%) !important;
-}
-
-/* Settings labels */
-.wplace-theme-classic-light .wplace-settings-section-label {
- color: var(--wplace-highlight) !important;
- text-shadow: none !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
- font-weight: 600 !important;
-}
-
-/* Icon colors for classic light theme */
-.wplace-theme-classic-light .wplace-icon-key { color: #4facfe; }
-.wplace-theme-classic-light .wplace-icon-robot { color: #4facfe; }
-.wplace-theme-classic-light .wplace-icon-speed { color: #4facfe; }
-.wplace-theme-classic-light .wplace-icon-bell { color: #ffc107; }
-.wplace-theme-classic-light .wplace-icon-palette { color: #f093fb; }
-.wplace-theme-classic-light .wplace-icon-globe { color: #ffeaa7; }
-.wplace-theme-classic-light .wplace-icon-paint { color: #4facfe; }
-.wplace-theme-classic-light .wplace-icon-eye { color: #6f42c1; }
-
-/* Clean light theme animations */
-@keyframes light-shimmer {
- 0% {
- transform: translateX(-100%);
- }
-
- 100% {
- transform: translateX(200%);
- }
-}
-
-/* Turnstile/CF checkbox overlay - CRITICAL FIX */
-.wplace-theme-classic-light .wplace-turnstile-overlay {
- background: rgb(255 255 255 / 98%) !important;
- border-radius: 12px !important;
- box-shadow: 0 8px 32px rgb(0 0 0 / 30%) !important;
- backdrop-filter: blur(10px) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- color: var(--wplace-text) !important;
- font-family: 'Segoe UI', Roboto, sans-serif !important;
-}
-
-.wplace-theme-classic-light .wplace-turnstile-title {
- color: var(--wplace-text) !important;
-}
-
-.wplace-theme-classic-light .wplace-turnstile-hide-btn {
- color: var(--wplace-text) !important;
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 6px !important;
- background: rgb(0 0 0 / 5%) !important;
-}
-
-.wplace-theme-classic-light .wplace-turnstile-hide-btn:hover {
- background: rgb(0 0 0 / 10%) !important;
-}
-
-/* Dual control layout styling for classic light theme */
-.wplace-theme-classic-light .wplace-dual-control-compact {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 12px;
- margin: 8px 0;
- flex-wrap: wrap;
-}
-
-.wplace-theme-classic-light .wplace-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 160px;
-}
-
-.wplace-theme-classic-light .wplace-speed-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 160px;
-}
-
-.wplace-theme-classic-light .wplace-input-group-compact {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-}
-
-.wplace-theme-classic-light .wplace-input-btn-compact {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- border-radius: 4px;
- width: 22px;
- height: 22px;
- cursor: pointer;
- font-weight: bold;
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
-}
-
-.wplace-theme-classic-light .wplace-input-btn-compact:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 8px rgba(0,0,0,0.3);
- background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
-}
-
-.wplace-theme-classic-light .wplace-input-btn-compact:active {
- transform: translateY(0);
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
-}
-
-.wplace-theme-classic-light .wplace-number-input-compact {
- background: #fff;
- border: 1px solid rgb(0 0 0 / 20%);
- border-radius: 4px;
- color: var(--wplace-text);
- padding: 4px 8px;
- font-size: 12px;
- width: 50px;
- text-align: center;
- transition: all 0.2s ease;
- height: 22px;
-}
-
-.wplace-theme-classic-light .wplace-number-input-compact:focus {
- outline: none;
- border-color: #4facfe;
- box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3);
- background: #f8f9fa;
-}
-
-.wplace-theme-classic-light .wplace-input-label-compact {
- color: var(--wplace-text);
- font-size: 11px;
- margin-left: 4px;
- white-space: nowrap;
-}
-
-.wplace-theme-classic-light .wplace-batch-size-header {
- margin-bottom: 8px;
-}
-
-.wplace-theme-classic-light .wplace-batch-size-label {
- color: var(--wplace-highlight);
- font-weight: 500;
- font-size: 13px;
-}
-
-/* Cooldown control styling for classic light theme */
-.wplace-theme-classic-light .wplace-cooldown-control {
- margin: 12px 0;
-}
-
-.wplace-theme-classic-light .wplace-cooldown-control label {
- color: var(--wplace-highlight);
- font-weight: 500;
- font-size: 13px;
- margin-bottom: 8px;
- display: block;
-}
-
-/* Color swatches with light styling */
-.wplace-theme-classic-light .wplace-color-swatch {
- border: 1px solid rgb(0 0 0 / 20%) !important;
- border-radius: 4px !important;
- box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important;
-}
-
-.wplace-theme-classic-light .wplace-color-swatch:hover {
- box-shadow: 0 4px 16px rgb(111 66 193 / 30%) !important;
- transform: translateY(-1px);
-}
-
-.wplace-theme-classic-light .wplace-stat-colors-grid {
- background: rgb(0 0 0 / 5%) !important;
- border-radius: 8px !important;
- border: 1px solid rgb(0 0 0 / 10%) !important;
-}
+/* WPlace Auto-Image Bot - Classic Light Theme */
+
+/* Clean, bright theme based on classic design with light backgrounds */
+
+.wplace-theme-classic-light {
+ /* === CORE COLORS === */
+ --wplace-primary: #fff; /* Clean white */
+ --wplace-secondary: #f8f9fa; /* Light gray */
+ --wplace-accent: #e9ecef; /* Lighter gray */
+ --wplace-text: #212529; /* Dark text */
+ --wplace-highlight: #6f42c1; /* Purple highlight */
+ --wplace-success: #28a745; /* Bootstrap green */
+ --wplace-error: #dc3545; /* Bootstrap red */
+ --wplace-warning: #ffc107; /* Bootstrap yellow */
+
+ /* === UI PROPERTIES === */
+ --wplace-radius: 8px;
+ --wplace-btn-radius: 12px;
+ --wplace-border-style: solid;
+ --wplace-border-width: 1px;
+ --wplace-border-color: #415a77;
+ --wplace-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%);
+ --wplace-backdrop: blur(10px);
+ --wplace-font: 'Segoe UI', roboto, sans-serif;
+
+ /* === EXTENDED COLOR PALETTE === */
+
+ /* Icon colors */
+ --wplace-icon-primary: #4facfe; /* Light blue */
+ --wplace-icon-secondary: #00f2fe; /* Cyan */
+ --wplace-icon-palette: #f093fb; /* Pink */
+
+ /* Additional UI colors */
+ --wplace-danger: #dc3545; /* Bootstrap red */
+ --wplace-danger-dark: #c82333; /* Darker red */
+ --wplace-muted-text: rgb(33 37 41 / 70%);
+ --wplace-highlight-secondary: #d3a4ff;
+
+ /* Text variants */
+ --wplace-text-secondary: rgb(33 37 41 / 90%);
+ --wplace-text-muted: rgb(33 37 41 / 70%);
+ --wplace-text-dim: rgb(33 37 41 / 60%);
+ --wplace-text-faded: rgb(33 37 41 / 80%);
+
+ /* Background variants */
+ --wplace-bg-input: #f3f3f3; /* neutral-100 */
+ --wplace-bg-subtle: #f3f3f3; /* neutral-100 — hover */
+ --wplace-bg-faint: #e0e0e0; /* neutral-200 */
+ --wplace-bg-ghost: #d1d1d1; /* neutral-300 */
+ --wplace-bg-whisper: #e0e0e0; /* can be used as whisper on hover */
+
+ /* Border variants */
+ --wplace-border-subtle: #d1d1d1; /* neutral-300 */
+ --wplace-border-faint: #e0e0e0; /* neutral-200 */
+ --wplace-border-ghost: #e0e0e0; /* neutral-200 */
+ --wplace-border-ultra-faint: #e0e0e0; /* neutral-200 — very faint */
+
+ /* Shadow variants */
+ --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 2px rgb(111 66 193 / 30%);
+ --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 15%);
+ --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 20%), 0 0 0 2px var(--wplace-icon-primary);
+ --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 25%), 0 0 0 3px var(--wplace-icon-primary);
+
+ /* Animation colors */
+ --wplace-pulse-start: rgb(40 167 69 / 70%);
+ --wplace-pulse-mid: rgb(40 167 69 / 0%);
+ --wplace-pulse-end: rgb(40 167 69 / 0%);
+
+ /* Slider colors */
+ --wplace-slider-thumb-bg: #6f42c1;
+ --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
+}
+
+/* === COMPONENT STYLING === */
+
+/* Multi-account panel styling */
+.wplace-theme-classic-light #wplace-accounts-container {
+ background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
+ color: var(--wplace-text) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ backdrop-filter: blur(10px) !important;
+}
+
+/* Active account styles */
+.wplace-theme-classic-light .wplace-accounts-container .active-account {
+ --active-bg: var(--wplace-secondary) !important;
+ --active-color: var(--wplace-success) !important;
+ --inactive-color: var(--wplace-text) !important;
+ --progress-bg: rgb(0 0 0 / 10%) !important;
+ --progress-fill: var(--wplace-highlight) !important;
+}
+
+/* Main container with clean light styling */
+.wplace-theme-classic-light #wplace-image-bot-container {
+ background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
+ color: var(--wplace-text) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ backdrop-filter: blur(10px) !important;
+}
+
+
+/* Stats container with proper contrast */
+.wplace-theme-classic-light #wplace-stats-container {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 15%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ color: var(--wplace-text) !important;
+ position: fixed !important;
+ overflow: hidden !important;
+ z-index: 9998 !important;
+}
+
+/* Headers with light gradient */
+.wplace-theme-classic-light .wplace-header {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
+ color: var(--wplace-highlight) !important;
+ border-bottom: 1px solid rgb(0 0 0 / 15%) !important;
+ text-shadow: none !important;
+}
+
+/* Stats header styling */
+.wplace-theme-classic-light #wplace-stats-container .wplace-header {
+ background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
+ color: var(--wplace-text) !important;
+ border-bottom: 1px solid rgb(0 0 0 / 20%) !important;
+ text-shadow: none !important;
+ font-weight: 600 !important;
+}
+
+/* Stats title specific styling */
+.wplace-theme-classic-light #wplace-stats-container .wplace-header .wplace-stats-title {
+ color: var(--wplace-text) !important;
+ text-shadow: none !important;
+}
+
+/* Comprehensive text and element styling for light theme - scoped to bot containers only */
+
+.wplace-theme-classic-light .wplace-status,
+.wplace-theme-classic-light .wplace-stats,
+.wplace-theme-classic-light .wplace-section,
+.wplace-theme-classic-light .wplace-controls,
+.wplace-theme-classic-light .wplace-data-management,
+.wplace-theme-classic-light .wplace-cooldown-settings {
+ color: var(--wplace-text) !important;
+}
+
+/* Buttons with light styling */
+.wplace-theme-classic-light .wplace-btn {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 12px !important;
+ color: var(--wplace-text) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ font-weight: 500 !important;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;
+}
+
+/* Data management section buttons */
+.wplace-theme-classic-light .wplace-data-management .wplace-btn {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ color: #212529 !important;
+ font-weight: 500 !important;
+}
+
+.wplace-theme-classic-light .wplace-btn:hover:not(:disabled) {
+ background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important;
+ transform: translateY(-1px) !important;
+}
+
+.wplace-theme-classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) {
+ background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important;
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important;
+}
+
+/* Settings dialog */
+.wplace-theme-classic-light #wplace-settings-container {
+ background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ backdrop-filter: blur(10px) !important;
+ color: var(--wplace-text) !important;
+}
+
+/* Settings dialog text elements */
+.wplace-theme-classic-light .wplace-stat-colors-grid * {
+ color: var(--wplace-text) !important;
+ font-weight: 500 !important;
+}
+
+.wplace-theme-classic-light #wplace-settings-container * {
+ color: var(--wplace-text) !important;
+}
+
+.wplace-theme-classic-light #wplace-settings-container .wplace-settings-section-wrapper * {
+ color: var(--wplace-text) !important;
+}
+
+/* Color palette text labels */
+.wplace-theme-classic-light .wplace-color-label {
+ color: var(--wplace-text) !important;
+ font-weight: 500 !important;
+ text-shadow: none !important;
+}
+
+.wplace-theme-classic-light .wplace-color-name {
+ color: var(--wplace-text) !important;
+ font-weight: 500 !important;
+}
+
+.wplace-theme-classic-light .wplace-color-item-name {
+ color: #000 !important;
+ font-weight: 600 !important;
+ text-shadow: none !important;
+}
+
+/* Resize dialog */
+.wplace-theme-classic-light .resize-container {
+ background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ backdrop-filter: blur(10px) !important;
+}
+
+/* Settings header */
+.wplace-theme-classic-light .wplace-settings-header {
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
+ border-bottom: 1px solid rgb(0 0 0 / 15%) !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-title {
+ color: var(--wplace-text) !important;
+ text-shadow: none !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-close-btn {
+ background: rgb(0 0 0 / 5%) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 50% !important;
+ color: var(--wplace-text) !important;
+ transition: all 0.3s ease !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-close-btn:hover {
+ background: rgb(220 53 69 / 10%) !important;
+ border-color: var(--wplace-error) !important;
+ box-shadow: 0 0 12px rgb(220 53 69 / 30%) !important;
+}
+
+/* Section titles */
+.wplace-theme-classic-light .wplace-section-title {
+ color: var(--wplace-highlight) !important;
+ text-shadow: none !important;
+ font-weight: 600 !important;
+}
+
+/* Button variants with classic light colors */
+.wplace-theme-classic-light .wplace-btn-start {
+ background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
+ color: white !important;
+ font-weight: 600 !important;
+}
+
+.wplace-theme-classic-light .wplace-btn-start:hover:not(:disabled) {
+ box-shadow: 0 4px 12px rgb(40 167 69 / 40%) !important;
+}
+
+.wplace-theme-classic-light .wplace-btn-stop {
+ background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important;
+ color: white !important;
+ font-weight: 600 !important;
+}
+
+.wplace-theme-classic-light .wplace-btn-stop:hover:not(:disabled) {
+ box-shadow: 0 4px 12px rgb(220 53 69 / 40%) !important;
+}
+
+.wplace-theme-classic-light .wplace-btn-upload {
+ background: rgb(111 66 193 / 10%) !important;
+ border: 2px dashed var(--wplace-highlight) !important;
+ color: var(--wplace-highlight) !important;
+}
+
+.wplace-theme-classic-light .wplace-btn-upload:hover:not(:disabled) {
+ background: rgb(111 66 193 / 15%) !important;
+ box-shadow: 0 4px 12px rgb(111 66 193 / 20%) !important;
+}
+
+/* Progress bars with clean light styling */
+.wplace-theme-classic-light .wplace-progress {
+ background: rgb(0 0 0 / 10%) !important;
+ border: 1px solid rgb(0 0 0 / 15%) !important;
+ border-radius: 12px !important;
+}
+
+.wplace-theme-classic-light .wplace-progress-bar {
+ background: linear-gradient(135deg, #6f42c1 0%, #9370db 100%) !important;
+ box-shadow: none !important;
+}
+
+.wplace-theme-classic-light .wplace-progress-bar::after {
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent) !important;
+}
+
+/* Status indicators with light colors */
+.wplace-theme-classic-light .status-success {
+ background: rgb(40 167 69 / 10%) !important;
+ border-color: var(--wplace-success) !important;
+ color: var(--wplace-success) !important;
+ box-shadow: 0 0 15px rgb(40 167 69 / 20%) !important;
+ text-shadow: none !important;
+}
+
+.wplace-theme-classic-light .status-error {
+ background: rgb(220 53 69 / 10%) !important;
+ border-color: var(--wplace-error) !important;
+ color: var(--wplace-error) !important;
+ box-shadow: 0 0 15px rgb(220 53 69 / 20%) !important;
+ text-shadow: none !important;
+}
+
+.wplace-theme-classic-light .status-default {
+ background: rgb(111 66 193 / 10%) !important;
+ border-color: var(--wplace-highlight) !important;
+ color: var(--wplace-highlight) !important;
+ text-shadow: none !important;
+}
+
+.wplace-theme-classic-light .wplace-stat-label {
+ color: var(--wplace-text) !important;
+ text-shadow: none !important;
+ font-weight: 500 !important;
+}
+
+.wplace-theme-classic-light .wplace-stat-value {
+ color: var(--wplace-highlight) !important;
+ text-shadow: none !important;
+ font-weight: 600 !important;
+}
+
+/* Sections with light styling */
+.wplace-theme-classic-light .wplace-section {
+ background: rgb(0 0 0 / 3%) !important;
+ border: 1px solid rgb(0 0 0 / 10%) !important;
+ border-radius: 12px !important;
+}
+
+.wplace-theme-classic-light .wplace-status-section {
+ background: rgb(0 0 0 / 3%) !important;
+ border: 1px solid rgb(0 0 0 / 10%) !important;
+ border-radius: 12px !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-section-wrapper {
+ background: rgb(0 0 0 / 3%) !important;
+ border: 1px solid rgb(0 0 0 / 10%) !important;
+ border-radius: 12px !important;
+}
+
+/* Form controls with light styling */
+.wplace-theme-classic-light .wplace-settings-select {
+ background: #fff !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 8px !important;
+ color: var(--wplace-text) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-select:focus {
+ border-color: var(--wplace-highlight) !important;
+ box-shadow: 0 0 0 2px rgb(111 66 193 / 30%) !important;
+}
+
+/* Dropdown menu options */
+.wplace-theme-classic-light .wplace-settings-select option {
+ background: #fff !important;
+ color: var(--wplace-text) !important;
+}
+
+.wplace-theme-classic-light .wplace-settings-option {
+ background: #fff !important;
+ color: var(--wplace-text) !important;
+}
+
+/* Sliders with classic gradient */
+.wplace-theme-classic-light .wplace-speed-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
+ border-radius: 4px !important;
+}
+
+.wplace-theme-classic-light .wplace-overlay-opacity-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
+ border-radius: 4px !important;
+}
+
+.wplace-theme-classic-light .wplace-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important;
+ border-radius: 4px !important;
+}
+
+.wplace-theme-classic-light .wplace-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--wplace-slider-thumb-bg);
+ border: 1px solid rgb(0 0 0 / 20%);
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgb(0 0 0 / 20%);
+}
+
+.wplace-theme-classic-light .wplace-speed-value {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important;
+ border-radius: 8px !important;
+ color: white !important;
+ font-weight: 600 !important;
+ box-shadow: 0 3px 10px rgb(79 172 254 / 30%) !important;
+ border: 1px solid rgb(0 0 0 / 10%) !important;
+}
+
+/* Settings labels */
+.wplace-theme-classic-light .wplace-settings-section-label {
+ color: var(--wplace-highlight) !important;
+ text-shadow: none !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+ font-weight: 600 !important;
+}
+
+/* Icon colors for classic light theme */
+.wplace-theme-classic-light .wplace-icon-key { color: #4facfe; }
+.wplace-theme-classic-light .wplace-icon-robot { color: #4facfe; }
+.wplace-theme-classic-light .wplace-icon-speed { color: #4facfe; }
+.wplace-theme-classic-light .wplace-icon-bell { color: #ffc107; }
+.wplace-theme-classic-light .wplace-icon-palette { color: #f093fb; }
+.wplace-theme-classic-light .wplace-icon-globe { color: #ffeaa7; }
+.wplace-theme-classic-light .wplace-icon-paint { color: #4facfe; }
+.wplace-theme-classic-light .wplace-icon-eye { color: #6f42c1; }
+
+/* Clean light theme animations */
+@keyframes light-shimmer {
+ 0% {
+ transform: translateX(-100%);
+ }
+
+ 100% {
+ transform: translateX(200%);
+ }
+}
+
+/* Turnstile/CF checkbox overlay - CRITICAL FIX */
+.wplace-theme-classic-light .wplace-turnstile-overlay {
+ background: rgb(255 255 255 / 98%) !important;
+ border-radius: 12px !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 30%) !important;
+ backdrop-filter: blur(10px) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ color: var(--wplace-text) !important;
+ font-family: 'Segoe UI', Roboto, sans-serif !important;
+}
+
+.wplace-theme-classic-light .wplace-turnstile-title {
+ color: var(--wplace-text) !important;
+}
+
+.wplace-theme-classic-light .wplace-turnstile-hide-btn {
+ color: var(--wplace-text) !important;
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 6px !important;
+ background: rgb(0 0 0 / 5%) !important;
+}
+
+.wplace-theme-classic-light .wplace-turnstile-hide-btn:hover {
+ background: rgb(0 0 0 / 10%) !important;
+}
+
+/* Dual control layout styling for classic light theme */
+.wplace-theme-classic-light .wplace-dual-control-compact {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 12px;
+ margin: 8px 0;
+ flex-wrap: wrap;
+}
+
+.wplace-theme-classic-light .wplace-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 160px;
+}
+
+.wplace-theme-classic-light .wplace-speed-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 160px;
+}
+
+.wplace-theme-classic-light .wplace-input-group-compact {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.wplace-theme-classic-light .wplace-input-btn-compact {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+.wplace-theme-classic-light .wplace-input-btn-compact:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
+ background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
+}
+
+.wplace-theme-classic-light .wplace-input-btn-compact:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+.wplace-theme-classic-light .wplace-number-input-compact {
+ background: #fff;
+ border: 1px solid rgb(0 0 0 / 20%);
+ border-radius: 4px;
+ color: var(--wplace-text);
+ padding: 4px 8px;
+ font-size: 12px;
+ width: 50px;
+ text-align: center;
+ transition: all 0.2s ease;
+ height: 22px;
+}
+
+.wplace-theme-classic-light .wplace-number-input-compact:focus {
+ outline: none;
+ border-color: #4facfe;
+ box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3);
+ background: #f8f9fa;
+}
+
+.wplace-theme-classic-light .wplace-input-label-compact {
+ color: var(--wplace-text);
+ font-size: 11px;
+ margin-left: 4px;
+ white-space: nowrap;
+}
+
+.wplace-theme-classic-light .wplace-batch-size-header {
+ margin-bottom: 8px;
+}
+
+.wplace-theme-classic-light .wplace-batch-size-label {
+ color: var(--wplace-highlight);
+ font-weight: 500;
+ font-size: 13px;
+}
+
+/* Cooldown control styling for classic light theme */
+.wplace-theme-classic-light .wplace-cooldown-control {
+ margin: 12px 0;
+}
+
+.wplace-theme-classic-light .wplace-cooldown-control label {
+ color: var(--wplace-highlight);
+ font-weight: 500;
+ font-size: 13px;
+ margin-bottom: 8px;
+ display: block;
+}
+
+/* Color swatches with light styling */
+.wplace-theme-classic-light .wplace-color-swatch {
+ border: 1px solid rgb(0 0 0 / 20%) !important;
+ border-radius: 4px !important;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important;
+}
+
+.wplace-theme-classic-light .wplace-color-swatch:hover {
+ box-shadow: 0 4px 16px rgb(111 66 193 / 30%) !important;
+ transform: translateY(-1px);
+}
+
+.wplace-theme-classic-light .wplace-stat-colors-grid {
+ background: rgb(0 0 0 / 5%) !important;
+ border-radius: 8px !important;
+ border: 1px solid rgb(0 0 0 / 10%) !important;
+}
diff --git a/themes/classic.css b/Extension/themes/classic.css
similarity index 92%
rename from themes/classic.css
rename to Extension/themes/classic.css
index 857ce6e..cd1fb39 100644
--- a/themes/classic.css
+++ b/Extension/themes/classic.css
@@ -1,725 +1,748 @@
-/* WPlace Auto-Image Bot - Classic Theme */
-/* Clean, modern UI with gradients and subtle effects */
-
-.wplace-theme-classic {
- --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%);
- --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%);
- --wplace-accent: #222;
- --wplace-text: #fff;
- --wplace-highlight: #775ce3;
- --wplace-success: #0f0;
- --wplace-error: #f00;
- --wplace-warning: #fa0;
- --wplace-radius: 12px;
- --wplace-btn-radius: 16px;
- --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%);
- --wplace-backdrop: blur(10px);
- --wplace-font: 'Segoe UI', roboto, sans-serif;
- --wplace-scanline: 0;
- --wplace-pixel-blink: 0;
-
- /* Icon colors */
- --wplace-icon-primary: #4facfe;
- --wplace-icon-secondary: #00f2fe;
- --wplace-icon-palette: #f093fb;
-
- /* Additional UI colors */
- --wplace-danger: #ff6a6a;
- --wplace-danger-dark: #ff4757;
- --wplace-muted-text: #fffB;
- --wplace-highlight-secondary: #d3a4ff;
-
- /* Slider colors */
- --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
-}
-
-/* Classic theme container styling */
-:root #wplace-image-bot-container,
-.wplace-theme-classic #wplace-image-bot-container {
- background: var(--wplace-primary);
- color: var(--wplace-text);
- border-radius: var(--wplace-radius);
- box-shadow: var(--wplace-shadow);
- font-family: var(--wplace-font);
- backdrop-filter: var(--wplace-backdrop);
- border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
-}
-
-:root #wplace-stats-container,
-.wplace-theme-classic #wplace-stats-container {
- background: var(--wplace-primary);
- color: var(--wplace-text);
- border-radius: var(--wplace-radius);
- box-shadow: var(--wplace-shadow);
- font-family: var(--wplace-font);
- backdrop-filter: var(--wplace-backdrop);
- border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
-}
-
-:root #wplace-settings-container,
-.wplace-theme-classic #wplace-settings-container {
- background: var(--wplace-primary);
- color: var(--wplace-text);
- border-radius: var(--wplace-radius);
- box-shadow: var(--wplace-shadow);
- font-family: var(--wplace-font);
- backdrop-filter: var(--wplace-backdrop);
- border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
-}
-
-:root .wplace-header,
-.wplace-theme-classic .wplace-header {
- background: var(--wplace-secondary);
- color: var(--wplace-highlight);
- text-shadow: 0 1px 2px rgb(0 0 0 / 50%);
-}
-
-:root .wplace-section-title,
-.wplace-theme-classic .wplace-section-title {
- color: var(--wplace-highlight);
-}
-
-:root .wplace-stat-value,
-.wplace-theme-classic .wplace-stat-value {
- color: var(--wplace-highlight);
-}
-
-:root .wplace-status-section,
-.wplace-theme-classic .wplace-status-section {
- border-radius: var(--wplace-radius);
- background: rgb(255 255 255 / 3%);
- border: 1px solid rgb(255 255 255 / 10%);
-}
-
-:root .wplace-section,
-.wplace-theme-classic .wplace-section {
- border-radius: var(--wplace-radius);
- background: rgb(255 255 255 / 3%);
-}
-
-:root .wplace-stats,
-.wplace-theme-classic .wplace-stats {
- border-radius: var(--wplace-radius);
- background: rgb(255 255 255 / 3%);
- border: 1px solid rgb(255 255 255 / 10%);
-}
-
-:root .wplace-status,
-.wplace-theme-classic .wplace-status {
- border-radius: var(--wplace-radius);
-}
-
-:root .wplace-alert-base,
-.wplace-theme-classic .wplace-alert-base {
- border-radius: var(--wplace-radius);
- font-family: var(--wplace-font);
-}
-
-:root .wplace-settings-container-base,
-.wplace-theme-classic .wplace-settings-container-base {
- font-family: var(--wplace-font);
- border-radius: var(--wplace-radius);
- background: var(--wplace-primary);
- color: var(--wplace-text);
- box-shadow: var(--wplace-shadow);
- backdrop-filter: var(--wplace-backdrop);
-}
-
-/* Button styling for classic theme */
-:root .wplace-btn,
-.wplace-theme-classic .wplace-btn {
- background: linear-gradient(135deg, #222 0%, #4a4a4a 100%);
- border: 1px solid rgb(255 255 255 / 10%);
- color: var(--wplace-text);
- border-radius: var(--wplace-btn-radius);
- font-family: var(--wplace-font);
-}
-
-:root .wplace-btn::before,
-.wplace-theme-classic .wplace-btn::before {
- background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent);
-}
-
-:root .wplace-btn:disabled,
-.wplace-theme-classic .wplace-btn:disabled {
- box-shadow: none !important;
-}
-
-:root .wplace-btn:hover:not(:disabled),
-.wplace-theme-classic .wplace-btn:hover:not(:disabled) {
- box-shadow: 0 4px 12px rgb(0 0 0 / 40%);
-}
-
-:root .wplace-btn-overlay.active,
-.wplace-theme-classic .wplace-btn-overlay.active {
- background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%);
- box-shadow: 0 0 15px #8e2de2;
-}
-
-/* Button variants - exact upstream main colors */
-:root .wplace-btn-primary,
-.wplace-theme-classic .wplace-btn-primary {
- background: linear-gradient(135deg, #222 0%, #6a5acd 100%);
- color: var(--wplace-text);
-}
-
-:root .wplace-btn-upload,
-.wplace-theme-classic .wplace-btn-upload {
- background: linear-gradient(135deg, #111 0%, #4a4a4a 100%);
- color: var(--wplace-text);
- border: 1px dashed var(--wplace-highlight) !important;
-}
-
-:root .wplace-btn-start,
-.wplace-theme-classic .wplace-btn-start {
- background: linear-gradient(135deg, var(--wplace-success) 0%, #228b22 100%);
- color: white;
-}
-
-:root .wplace-btn-stop,
-.wplace-theme-classic .wplace-btn-stop {
- background: linear-gradient(135deg, var(--wplace-error) 0%, #dc143c 100%);
- color: white;
-}
-
-:root .wplace-btn-select,
-.wplace-theme-classic .wplace-btn-select {
- background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%);
- color: white;
-}
-
-:root .wplace-btn-file,
-.wplace-theme-classic .wplace-btn-file {
- background: linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%);
- color: white;
-}
-
-:root .wplace-btn.active,
-:root .wplace-btn[aria-pressed="true"],
-.wplace-theme-classic .wplace-btn.active,
-.wplace-theme-classic .wplace-btn[aria-pressed="true"] {
- background: var(--wplace-highlight) !important;
- color: #000 !important;
- border-color: var(--wplace-text) !important;
- box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%) !important;
-}
-
-:root .wplace-btn.active i,
-:root .wplace-btn[aria-pressed="true"] i,
-.wplace-theme-classic .wplace-btn.active i,
-.wplace-theme-classic .wplace-btn[aria-pressed="true"] i {
- filter: drop-shadow(0 0 3px #000);
-}
-
-:root .mask-mode-group .wplace-btn.active,
-:root .mask-mode-group .wplace-btn[aria-pressed="true"],
-.wplace-theme-classic .mask-mode-group .wplace-btn.active,
-.wplace-theme-classic .mask-mode-group .wplace-btn[aria-pressed="true"] {
- background: var(--wplace-highlight);
- color: #000;
- border-color: var(--wplace-text);
- box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%);
-}
-
-/* Status styling for classic theme */
-:root .status-default,
-.wplace-theme-classic .status-default {
- background: rgb(255 255 255 / 10%);
- border-color: var(--wplace-text);
- color: var(--wplace-text);
-}
-
-:root .status-success,
-.wplace-theme-classic .status-success {
- background: rgb(0 255 0 / 10%);
- border-color: var(--wplace-success);
- color: var(--wplace-success);
- box-shadow: 0 0 15px var(--wplace-success);
-}
-
-:root .status-error,
-.wplace-theme-classic .status-error {
- background: rgb(255 0 0 / 10%);
- border-color: var(--wplace-error);
- color: var(--wplace-error);
- box-shadow: 0 0 15px var(--wplace-error);
-}
-
-:root .status-warning,
-.wplace-theme-classic .status-warning {
- background: rgb(255 165 0 / 10%);
- border-color: var(--wplace-warning);
- color: var(--wplace-warning);
- box-shadow: 0 0 15px var(--wplace-warning);
-}
-
-:root .wplace-status.status-default,
-.wplace-theme-classic .wplace-status.status-default {
- color: var(--wplace-text);
- background: rgb(255 255 255 / 10%);
- border-color: var(--wplace-text);
-}
-
-/* Alert styling for classic theme */
-:root .wplace-alert-info,
-.wplace-theme-classic .wplace-alert-info {
- background: linear-gradient(135deg, #3498db, #2980b9);
- box-shadow: 0 4px 12px rgb(52 152 219 / 30%);
-}
-
-:root .wplace-alert-success,
-.wplace-theme-classic .wplace-alert-success {
- background: linear-gradient(135deg, #27ae60, #229954);
- box-shadow: 0 4px 12px rgb(39 174 96 / 30%);
-}
-
-:root .wplace-alert-warning,
-.wplace-theme-classic .wplace-alert-warning {
- background: linear-gradient(135deg, #f39c12, #e67e22);
- box-shadow: 0 4px 12px rgb(243 156 18 / 30%);
-}
-
-:root .wplace-alert-error,
-.wplace-theme-classic .wplace-alert-error {
- background: linear-gradient(135deg, #e74c3c, #c0392b);
- box-shadow: 0 4px 12px rgb(231 76 60 / 30%);
-}
-
-/* Progress bar styling for classic theme */
-:root .wplace-progress,
-.wplace-theme-classic .wplace-progress {
- background: rgb(0 0 0 / 30%);
- border: 1px solid rgb(255 255 255 / 10%);
- border-radius: var(--wplace-radius);
-}
-
-:root .wplace-progress-bar,
-.wplace-theme-classic .wplace-progress-bar {
- background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%);
-}
-
-:root .wplace-progress-bar::after,
-.wplace-theme-classic .wplace-progress-bar::after {
- background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent);
-}
-
-/* Header and sections styling for classic theme */
-:root .wplace-header-btn,
-.wplace-theme-classic .wplace-header-btn {
- background: rgb(255 255 255 / 10%);
- color: var(--wplace-highlight);
- border-radius: 4px;
- font-family: var(--wplace-font);
-}
-
-:root .wplace-header-btn:hover,
-.wplace-theme-classic .wplace-header-btn:hover {
- background: #222;
- color: var(--wplace-text);
-}
-
-/* Font and typography for classic theme */
-:root .wplace-turnstile-overlay,
-.wplace-theme-classic .wplace-turnstile-overlay {
- background: rgb(0 0 0 / 90%) !important;
- border-radius: var(--wplace-radius) !important;
- box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important;
- backdrop-filter: var(--wplace-backdrop) !important;
- border: 1px solid rgb(255 255 255 / 20%) !important;
- color: var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
-}
-
-/* Auto light/dark support for classic theme */
-@media (prefers-color-scheme: light) {
- :root .theme-auto,
- .wplace-theme-classic .theme-auto {
- --wplace-primary: #fff;
- --wplace-secondary: #f5f5f5;
- --wplace-accent: #007acc;
- --wplace-text: #333;
- }
-}
-
-@media (prefers-color-scheme: dark) {
- :root .theme-auto,
- .wplace-theme-classic .theme-auto {
- --wplace-primary: #1e1e1e;
- --wplace-secondary: #2d2d30;
- --wplace-accent: var(--wplace-highlight);
- --wplace-text: #fff;
- }
-}
-
-/* Border and color styling for classic theme */
-:root .wplace-color-item-name,
-.wplace-theme-classic .wplace-color-item-name {
- color: #ccc;
-}
-
-:root .wplace-color-swatch,
-.wplace-theme-classic .wplace-color-swatch {
- border: 1px solid rgb(255 255 255 / 20%);
- border-radius: 4px;
-}
-
-:root .wplace-stat-color-swatch,
-.wplace-theme-classic .wplace-stat-color-swatch {
- border-radius: 3px;
- border: 1px solid rgb(255 255 255 / 10%);
- box-shadow: inset 0 0 2px rgb(0 0 0 / 50%);
-}
-
-:root .resize-tools button,
-.wplace-theme-classic .resize-tools button {
- border-radius: 6px;
- border: 1px solid rgb(255 255 255 / 20%);
- background: rgb(255 255 255 / 6%);
- color: #fff;
-}
-
-:root .resize-slider,
-.wplace-theme-classic .resize-slider {
- background: #ccc;
- border-radius: var(--wplace-radius);
-}
-
-/* Text effects and filters for classic theme */
-:root .wplace-color-swatch:not(.active),
-.wplace-theme-classic .wplace-color-swatch:not(.active) {
- filter: grayscale(80%);
-}
-
-:root .wplace-color-swatch.unavailable,
-.wplace-theme-classic .wplace-color-swatch.unavailable {
- border-color: #666;
-}
-
-:root .wplace-color-swatch.unavailable:not(.active),
-.wplace-theme-classic .wplace-color-swatch.unavailable:not(.active) {
- filter: grayscale(90%);
-}
-
-:root .wplace-color-swatch.active::after,
-.wplace-theme-classic .wplace-color-swatch.active::after {
- text-shadow: 0 0 3px black;
-}
-
-/* Icon colors for classic theme */
-:root .wplace-icon-key,
-.wplace-theme-classic .wplace-icon-key {
- color: #4facfe;
-}
-
-:root .wplace-icon-robot,
-.wplace-theme-classic .wplace-icon-robot {
- color: #4facfe;
-}
-
-:root .wplace-icon-speed,
-.wplace-theme-classic .wplace-icon-speed {
- color: #4facfe;
-}
-
-:root .wplace-icon-bell,
-.wplace-theme-classic .wplace-icon-bell {
- color: #ffd166;
-}
-
-:root .wplace-icon-palette,
-.wplace-theme-classic .wplace-icon-palette {
- color: #f093fb;
-}
-
-:root .wplace-icon-globe,
-.wplace-theme-classic .wplace-icon-globe {
- color: #ffeaa7;
-}
-
-:root .wplace-icon-paint,
-.wplace-theme-classic .wplace-icon-paint {
- color: #4facfe;
-}
-
-:root .wplace-icon-eye,
-.wplace-theme-classic .wplace-icon-eye {
- color: var(--wplace-highlight);
-}
-
-/* Form controls and sliders for classic theme */
-:root .wplace-slider,
-.wplace-theme-classic .wplace-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
- border-radius: 4px;
-}
-
-:root .wplace-slider::-webkit-slider-thumb,
-.wplace-theme-classic .wplace-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 16px;
- height: 16px;
- border-radius: 50%;
- background: var(--wplace-slider-thumb-bg);
- border: none;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-:root .wplace-speed-slider,
-.wplace-theme-classic .wplace-speed-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
- border-radius: 4px;
-}
-
-:root .wplace-speed-slider::-webkit-slider-thumb,
-.wplace-theme-classic .wplace-speed-slider::-webkit-slider-thumb {
- border-radius: 50%;
- border: 2px solid;
-}
-
-:root .wplace-speed-slider::-moz-range-thumb,
-.wplace-theme-classic .wplace-speed-slider::-moz-range-thumb {
- border-radius: 50%;
- border: 2px solid;
-}
-
-:root .wplace-turnstile-title,
-.wplace-theme-classic .wplace-turnstile-title {
- color: var(--wplace-text) !important;
-}
-
-:root .wplace-turnstile-hide-btn,
-.wplace-theme-classic .wplace-turnstile-hide-btn {
- color: var(--wplace-text) !important;
- border: 1px solid rgb(255 255 255 / 20%) !important;
- border-radius: 6px !important;
-}
-
-:root .wplace-turnstile-hide-btn:hover,
-.wplace-theme-classic .wplace-turnstile-hide-btn:hover {
- background: rgb(255 255 255 / 10%) !important;
-}
-
-:root .wplace-settings-select,
-.wplace-theme-classic .wplace-settings-select {
- background: rgb(255 255 255 / 15%);
- color: white;
- border: 1px solid rgb(255 255 255 / 20%);
- border-radius: 8px;
- box-shadow: 0 3px 10px rgb(0 0 0 / 10%);
-}
-
-:root .wplace-settings-option,
-.wplace-theme-classic .wplace-settings-option {
- background: #2d3748;
- color: white;
-}
-
-:root .wplace-stat-colors-grid,
-.wplace-theme-classic .wplace-stat-colors-grid {
- background: rgb(0 0 0 / 20%);
- border-radius: 4px;
-}
-
-:root .resize-overlay,
-.wplace-theme-classic .resize-overlay {
- background: rgb(0 0 0 / 80%);
-}
-
-:root .wplace-overlay,
-.wplace-theme-classic .wplace-overlay {
- background: rgb(0 0 0 / 80%);
-}
-
-:root .wplace-settings-error,
-.wplace-theme-classic .wplace-settings-error {
- background: rgb(255 0 0 / 40%) !important;
-}
-
-/* Settings sections and batch controls for classic theme */
-:root .wplace-settings-section-wrapper,
-.wplace-theme-classic .wplace-settings-section-wrapper {
- background: rgb(255 255 255 / 10%);
- border-radius: var(--wplace-radius);
- border: 1px solid rgb(255 255 255 / 10%);
-}
-
-:root .wplace-batch-controls,
-.wplace-theme-classic .wplace-batch-controls {
- background: rgb(255 255 255 / 10%);
- border-radius: var(--wplace-radius);
- border: 1px solid rgb(255 255 255 / 10%);
-}
-
-:root .wplace-overlay-opacity-slider,
-.wplace-theme-classic .wplace-overlay-opacity-slider {
- background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
- border-radius: 4px;
-}
-
-:root .wplace-speed-value,
-.wplace-theme-classic .wplace-speed-value {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- border-radius: 8px;
- color: white;
- box-shadow: 0 3px 10px rgb(79 172 254 / 30%);
- border: 1px solid rgb(255 255 255 / 20%);
-}
-
-/* Additional elements for classic theme */
-:root .wplace-color-divider,
-.wplace-theme-classic .wplace-color-divider {
- background: rgb(255 255 255 / 10%);
-}
-
-:root .wplace-settings-select option,
-.wplace-theme-classic .wplace-settings-select option {
- background: #2d3748;
- color: white;
-}
-
-:root .wplace-settings-description,
-.wplace-theme-classic .wplace-settings-description {
- color: rgb(255 255 255 / 70%);
-}
-
-:root .wplace-settings-apply-btn,
-.wplace-theme-classic .wplace-settings-apply-btn {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border-radius: 8px;
-}
-
-/* Dual control layout styling for classic theme */
-:root .wplace-dual-control-compact,
-.wplace-theme-classic .wplace-dual-control-compact {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 12px;
- margin: 8px 0;
- flex-wrap: wrap;
-}
-
-:root .wplace-slider-container-compact,
-.wplace-theme-classic .wplace-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 160px;
-}
-
-:root .wplace-speed-slider-container-compact,
-.wplace-theme-classic .wplace-speed-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 8px;
- flex: 1;
- min-width: 160px;
-}
-
-:root .wplace-input-group-compact,
-.wplace-theme-classic .wplace-input-group-compact {
- display: flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
-}
-
-:root .wplace-input-btn-compact,
-.wplace-theme-classic .wplace-input-btn-compact {
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
- color: white;
- border: none;
- border-radius: 4px;
- width: 22px;
- height: 22px;
- cursor: pointer;
- font-weight: bold;
- font-size: 12px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
-}
-
-:root .wplace-input-btn-compact:hover,
-.wplace-theme-classic .wplace-input-btn-compact:hover {
- transform: translateY(-1px);
- box-shadow: 0 4px 8px rgba(0,0,0,0.3);
- background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
-}
-
-:root .wplace-input-btn-compact:active,
-.wplace-theme-classic .wplace-input-btn-compact:active {
- transform: translateY(0);
- box-shadow: 0 2px 4px rgba(0,0,0,0.2);
-}
-
-:root .wplace-number-input-compact,
-.wplace-theme-classic .wplace-number-input-compact {
- background: rgba(255,255,255,0.1);
- border: 1px solid rgba(255,255,255,0.2);
- border-radius: 4px;
- color: white;
- padding: 4px 8px;
- font-size: 12px;
- width: 50px;
- text-align: center;
- transition: all 0.2s ease;
- height: 22px;
-}
-
-:root .wplace-number-input-compact:focus,
-.wplace-theme-classic .wplace-number-input-compact:focus {
- outline: none;
- border-color: #4facfe;
- box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3);
- background: rgba(255,255,255,0.15);
-}
-
-:root .wplace-input-label-compact,
-.wplace-theme-classic .wplace-input-label-compact {
- color: rgba(255,255,255,0.8);
- font-size: 11px;
- margin-left: 4px;
- white-space: nowrap;
-}
-
-:root .wplace-batch-size-header,
-.wplace-theme-classic .wplace-batch-size-header {
- margin-bottom: 8px;
-}
-
-:root .wplace-batch-size-label,
-.wplace-theme-classic .wplace-batch-size-label {
- color: var(--wplace-highlight);
- font-weight: 500;
- font-size: 13px;
-}
-
-/* Cooldown control styling for classic theme */
-:root .wplace-cooldown-control,
-.wplace-theme-classic .wplace-cooldown-control {
- margin: 12px 0;
-}
-
-:root .wplace-cooldown-control label,
-.wplace-theme-classic .wplace-cooldown-control label {
- color: var(--wplace-highlight);
- font-weight: 500;
- font-size: 13px;
- margin-bottom: 8px;
- display: block;
-}
-
-/* Resize dialog styling for classic theme */
-:root .resize-container,
-.wplace-theme-classic .resize-container {
- background: #000;
- border: 1px solid #fff;
- border-radius: 12px;
- box-shadow: 0 0 20px rgb(0 0 0 / 50%);
-}
+/* WPlace Auto-Image Bot - Classic Theme */
+/* Clean, modern UI with gradients and subtle effects */
+
+.wplace-theme-classic {
+ --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%);
+ --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%);
+ --wplace-accent: #222;
+ --wplace-text: #fff;
+ --wplace-highlight: #775ce3;
+ --wplace-success: #0f0;
+ --wplace-error: #f00;
+ --wplace-warning: #fa0;
+ --wplace-radius: 12px;
+ --wplace-btn-radius: 16px;
+ --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%);
+ --wplace-backdrop: blur(10px);
+ --wplace-font: 'Segoe UI', roboto, sans-serif;
+ --wplace-scanline: 0;
+ --wplace-pixel-blink: 0;
+
+ /* Icon colors */
+ --wplace-icon-primary: #4facfe;
+ --wplace-icon-secondary: #00f2fe;
+ --wplace-icon-palette: #f093fb;
+
+ /* Additional UI colors */
+ --wplace-danger: #ff6a6a;
+ --wplace-danger-dark: #ff4757;
+ --wplace-muted-text: #fffB;
+ --wplace-highlight-secondary: #d3a4ff;
+
+ /* Slider colors */
+ --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
+}
+
+/* Classic theme container styling */
+/* Multi-account panel styling */
+:root #wplace-accounts-container,
+.wplace-theme-classic #wplace-accounts-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: var(--wplace-shadow);
+ font-family: var(--wplace-font);
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
+}
+
+/* Active account styles */
+:root .wplace-accounts-container .active-account,
+.wplace-theme-classic .wplace-accounts-container .active-account {
+ --active-bg: var(--wplace-secondary);
+ --active-color: var(--wplace-success);
+ --inactive-color: var(--wplace-text);
+ --progress-bg: rgb(255 255 255 / 10%);
+ --progress-fill: var(--wplace-highlight);
+}
+
+/* Base container styling */
+:root #wplace-image-bot-container,
+.wplace-theme-classic #wplace-image-bot-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: var(--wplace-shadow);
+ font-family: var(--wplace-font);
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
+}
+
+:root #wplace-stats-container,
+.wplace-theme-classic #wplace-stats-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: var(--wplace-shadow);
+ font-family: var(--wplace-font);
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
+}
+
+:root #wplace-settings-container,
+.wplace-theme-classic #wplace-settings-container {
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-radius);
+ box-shadow: var(--wplace-shadow);
+ font-family: var(--wplace-font);
+ backdrop-filter: var(--wplace-backdrop);
+ border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color);
+}
+
+:root .wplace-header,
+.wplace-theme-classic .wplace-header {
+ background: var(--wplace-secondary);
+ color: var(--wplace-highlight);
+ text-shadow: 0 1px 2px rgb(0 0 0 / 50%);
+}
+
+:root .wplace-section-title,
+.wplace-theme-classic .wplace-section-title {
+ color: var(--wplace-highlight);
+}
+
+:root .wplace-stat-value,
+.wplace-theme-classic .wplace-stat-value {
+ color: var(--wplace-highlight);
+}
+
+:root .wplace-status-section,
+.wplace-theme-classic .wplace-status-section {
+ border-radius: var(--wplace-radius);
+ background: rgb(255 255 255 / 3%);
+ border: 1px solid rgb(255 255 255 / 10%);
+}
+
+:root .wplace-section,
+.wplace-theme-classic .wplace-section {
+ border-radius: var(--wplace-radius);
+ background: rgb(255 255 255 / 3%);
+}
+
+:root .wplace-stats,
+.wplace-theme-classic .wplace-stats {
+ border-radius: var(--wplace-radius);
+ background: rgb(255 255 255 / 3%);
+ border: 1px solid rgb(255 255 255 / 10%);
+}
+
+:root .wplace-status,
+.wplace-theme-classic .wplace-status {
+ border-radius: var(--wplace-radius);
+}
+
+:root .wplace-alert-base,
+.wplace-theme-classic .wplace-alert-base {
+ border-radius: var(--wplace-radius);
+ font-family: var(--wplace-font);
+}
+
+:root .wplace-settings-container-base,
+.wplace-theme-classic .wplace-settings-container-base {
+ font-family: var(--wplace-font);
+ border-radius: var(--wplace-radius);
+ background: var(--wplace-primary);
+ color: var(--wplace-text);
+ box-shadow: var(--wplace-shadow);
+ backdrop-filter: var(--wplace-backdrop);
+}
+
+/* Button styling for classic theme */
+:root .wplace-btn,
+.wplace-theme-classic .wplace-btn {
+ background: linear-gradient(135deg, #222 0%, #4a4a4a 100%);
+ border: 1px solid rgb(255 255 255 / 10%);
+ color: var(--wplace-text);
+ border-radius: var(--wplace-btn-radius);
+ font-family: var(--wplace-font);
+}
+
+:root .wplace-btn::before,
+.wplace-theme-classic .wplace-btn::before {
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent);
+}
+
+:root .wplace-btn:disabled,
+.wplace-theme-classic .wplace-btn:disabled {
+ box-shadow: none !important;
+}
+
+:root .wplace-btn:hover:not(:disabled),
+.wplace-theme-classic .wplace-btn:hover:not(:disabled) {
+ box-shadow: 0 4px 12px rgb(0 0 0 / 40%);
+}
+
+:root .wplace-btn-overlay.active,
+.wplace-theme-classic .wplace-btn-overlay.active {
+ background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%);
+ box-shadow: 0 0 15px #8e2de2;
+}
+
+/* Button variants - exact upstream main colors */
+:root .wplace-btn-primary,
+.wplace-theme-classic .wplace-btn-primary {
+ background: linear-gradient(135deg, #222 0%, #6a5acd 100%);
+ color: var(--wplace-text);
+}
+
+:root .wplace-btn-upload,
+.wplace-theme-classic .wplace-btn-upload {
+ background: linear-gradient(135deg, #111 0%, #4a4a4a 100%);
+ color: var(--wplace-text);
+ border: 1px dashed var(--wplace-highlight) !important;
+}
+
+:root .wplace-btn-start,
+.wplace-theme-classic .wplace-btn-start {
+ background: linear-gradient(135deg, var(--wplace-success) 0%, #228b22 100%);
+ color: white;
+}
+
+:root .wplace-btn-stop,
+.wplace-theme-classic .wplace-btn-stop {
+ background: linear-gradient(135deg, var(--wplace-error) 0%, #dc143c 100%);
+ color: white;
+}
+
+:root .wplace-btn-select,
+.wplace-theme-classic .wplace-btn-select {
+ background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%);
+ color: white;
+}
+
+:root .wplace-btn-file,
+.wplace-theme-classic .wplace-btn-file {
+ background: linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%);
+ color: white;
+}
+
+:root .wplace-btn.active,
+:root .wplace-btn[aria-pressed="true"],
+.wplace-theme-classic .wplace-btn.active,
+.wplace-theme-classic .wplace-btn[aria-pressed="true"] {
+ background: var(--wplace-highlight) !important;
+ color: #000 !important;
+ border-color: var(--wplace-text) !important;
+ box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%) !important;
+}
+
+:root .wplace-btn.active i,
+:root .wplace-btn[aria-pressed="true"] i,
+.wplace-theme-classic .wplace-btn.active i,
+.wplace-theme-classic .wplace-btn[aria-pressed="true"] i {
+ filter: drop-shadow(0 0 3px #000);
+}
+
+:root .mask-mode-group .wplace-btn.active,
+:root .mask-mode-group .wplace-btn[aria-pressed="true"],
+.wplace-theme-classic .mask-mode-group .wplace-btn.active,
+.wplace-theme-classic .mask-mode-group .wplace-btn[aria-pressed="true"] {
+ background: var(--wplace-highlight);
+ color: #000;
+ border-color: var(--wplace-text);
+ box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%);
+}
+
+/* Status styling for classic theme */
+:root .status-default,
+.wplace-theme-classic .status-default {
+ background: rgb(255 255 255 / 10%);
+ border-color: var(--wplace-text);
+ color: var(--wplace-text);
+}
+
+:root .status-success,
+.wplace-theme-classic .status-success {
+ background: rgb(0 255 0 / 10%);
+ border-color: var(--wplace-success);
+ color: var(--wplace-success);
+ box-shadow: 0 0 15px var(--wplace-success);
+}
+
+:root .status-error,
+.wplace-theme-classic .status-error {
+ background: rgb(255 0 0 / 10%);
+ border-color: var(--wplace-error);
+ color: var(--wplace-error);
+ box-shadow: 0 0 15px var(--wplace-error);
+}
+
+:root .status-warning,
+.wplace-theme-classic .status-warning {
+ background: rgb(255 165 0 / 10%);
+ border-color: var(--wplace-warning);
+ color: var(--wplace-warning);
+ box-shadow: 0 0 15px var(--wplace-warning);
+}
+
+:root .wplace-status.status-default,
+.wplace-theme-classic .wplace-status.status-default {
+ color: var(--wplace-text);
+ background: rgb(255 255 255 / 10%);
+ border-color: var(--wplace-text);
+}
+
+/* Alert styling for classic theme */
+:root .wplace-alert-info,
+.wplace-theme-classic .wplace-alert-info {
+ background: linear-gradient(135deg, #3498db, #2980b9);
+ box-shadow: 0 4px 12px rgb(52 152 219 / 30%);
+}
+
+:root .wplace-alert-success,
+.wplace-theme-classic .wplace-alert-success {
+ background: linear-gradient(135deg, #27ae60, #229954);
+ box-shadow: 0 4px 12px rgb(39 174 96 / 30%);
+}
+
+:root .wplace-alert-warning,
+.wplace-theme-classic .wplace-alert-warning {
+ background: linear-gradient(135deg, #f39c12, #e67e22);
+ box-shadow: 0 4px 12px rgb(243 156 18 / 30%);
+}
+
+:root .wplace-alert-error,
+.wplace-theme-classic .wplace-alert-error {
+ background: linear-gradient(135deg, #e74c3c, #c0392b);
+ box-shadow: 0 4px 12px rgb(231 76 60 / 30%);
+}
+
+/* Progress bar styling for classic theme */
+:root .wplace-progress,
+.wplace-theme-classic .wplace-progress {
+ background: rgb(0 0 0 / 30%);
+ border: 1px solid rgb(255 255 255 / 10%);
+ border-radius: var(--wplace-radius);
+}
+
+:root .wplace-progress-bar,
+.wplace-theme-classic .wplace-progress-bar {
+ background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%);
+}
+
+:root .wplace-progress-bar::after,
+.wplace-theme-classic .wplace-progress-bar::after {
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent);
+}
+
+/* Header and sections styling for classic theme */
+:root .wplace-header-btn,
+.wplace-theme-classic .wplace-header-btn {
+ background: rgb(255 255 255 / 10%);
+ color: var(--wplace-highlight);
+ border-radius: 4px;
+ font-family: var(--wplace-font);
+}
+
+:root .wplace-header-btn:hover,
+.wplace-theme-classic .wplace-header-btn:hover {
+ background: #222;
+ color: var(--wplace-text);
+}
+
+/* Font and typography for classic theme */
+:root .wplace-turnstile-overlay,
+.wplace-theme-classic .wplace-turnstile-overlay {
+ background: rgb(0 0 0 / 90%) !important;
+ border-radius: var(--wplace-radius) !important;
+ box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important;
+ backdrop-filter: var(--wplace-backdrop) !important;
+ border: 1px solid rgb(255 255 255 / 20%) !important;
+ color: var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+/* Auto light/dark support for classic theme */
+@media (prefers-color-scheme: light) {
+ :root .theme-auto,
+ .wplace-theme-classic .theme-auto {
+ --wplace-primary: #fff;
+ --wplace-secondary: #f5f5f5;
+ --wplace-accent: #007acc;
+ --wplace-text: #333;
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ :root .theme-auto,
+ .wplace-theme-classic .theme-auto {
+ --wplace-primary: #1e1e1e;
+ --wplace-secondary: #2d2d30;
+ --wplace-accent: var(--wplace-highlight);
+ --wplace-text: #fff;
+ }
+}
+
+/* Border and color styling for classic theme */
+:root .wplace-color-item-name,
+.wplace-theme-classic .wplace-color-item-name {
+ color: #ccc;
+}
+
+:root .wplace-color-swatch,
+.wplace-theme-classic .wplace-color-swatch {
+ border: 1px solid rgb(255 255 255 / 20%);
+ border-radius: 4px;
+}
+
+:root .wplace-stat-color-swatch,
+.wplace-theme-classic .wplace-stat-color-swatch {
+ border-radius: 3px;
+ border: 1px solid rgb(255 255 255 / 10%);
+ box-shadow: inset 0 0 2px rgb(0 0 0 / 50%);
+}
+
+:root .resize-tools button,
+.wplace-theme-classic .resize-tools button {
+ border-radius: 6px;
+ border: 1px solid rgb(255 255 255 / 20%);
+ background: rgb(255 255 255 / 6%);
+ color: #fff;
+}
+
+:root .resize-slider,
+.wplace-theme-classic .resize-slider {
+ background: #ccc;
+ border-radius: var(--wplace-radius);
+}
+
+/* Text effects and filters for classic theme */
+:root .wplace-color-swatch:not(.active),
+.wplace-theme-classic .wplace-color-swatch:not(.active) {
+ filter: grayscale(80%);
+}
+
+:root .wplace-color-swatch.unavailable,
+.wplace-theme-classic .wplace-color-swatch.unavailable {
+ border-color: #666;
+}
+
+:root .wplace-color-swatch.unavailable:not(.active),
+.wplace-theme-classic .wplace-color-swatch.unavailable:not(.active) {
+ filter: grayscale(90%);
+}
+
+:root .wplace-color-swatch.active::after,
+.wplace-theme-classic .wplace-color-swatch.active::after {
+ text-shadow: 0 0 3px black;
+}
+
+/* Icon colors for classic theme */
+:root .wplace-icon-key,
+.wplace-theme-classic .wplace-icon-key {
+ color: #4facfe;
+}
+
+:root .wplace-icon-robot,
+.wplace-theme-classic .wplace-icon-robot {
+ color: #4facfe;
+}
+
+:root .wplace-icon-speed,
+.wplace-theme-classic .wplace-icon-speed {
+ color: #4facfe;
+}
+
+:root .wplace-icon-bell,
+.wplace-theme-classic .wplace-icon-bell {
+ color: #ffd166;
+}
+
+:root .wplace-icon-palette,
+.wplace-theme-classic .wplace-icon-palette {
+ color: #f093fb;
+}
+
+:root .wplace-icon-globe,
+.wplace-theme-classic .wplace-icon-globe {
+ color: #ffeaa7;
+}
+
+:root .wplace-icon-paint,
+.wplace-theme-classic .wplace-icon-paint {
+ color: #4facfe;
+}
+
+:root .wplace-icon-eye,
+.wplace-theme-classic .wplace-icon-eye {
+ color: var(--wplace-highlight);
+}
+
+/* Form controls and sliders for classic theme */
+:root .wplace-slider,
+.wplace-theme-classic .wplace-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
+ border-radius: 4px;
+}
+
+:root .wplace-slider::-webkit-slider-thumb,
+.wplace-theme-classic .wplace-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--wplace-slider-thumb-bg);
+ border: none;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+:root .wplace-speed-slider,
+.wplace-theme-classic .wplace-speed-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
+ border-radius: 4px;
+}
+
+:root .wplace-speed-slider::-webkit-slider-thumb,
+.wplace-theme-classic .wplace-speed-slider::-webkit-slider-thumb {
+ border-radius: 50%;
+ border: 2px solid;
+}
+
+:root .wplace-speed-slider::-moz-range-thumb,
+.wplace-theme-classic .wplace-speed-slider::-moz-range-thumb {
+ border-radius: 50%;
+ border: 2px solid;
+}
+
+:root .wplace-turnstile-title,
+.wplace-theme-classic .wplace-turnstile-title {
+ color: var(--wplace-text) !important;
+}
+
+:root .wplace-turnstile-hide-btn,
+.wplace-theme-classic .wplace-turnstile-hide-btn {
+ color: var(--wplace-text) !important;
+ border: 1px solid rgb(255 255 255 / 20%) !important;
+ border-radius: 6px !important;
+}
+
+:root .wplace-turnstile-hide-btn:hover,
+.wplace-theme-classic .wplace-turnstile-hide-btn:hover {
+ background: rgb(255 255 255 / 10%) !important;
+}
+
+:root .wplace-settings-select,
+.wplace-theme-classic .wplace-settings-select {
+ background: rgb(255 255 255 / 15%);
+ color: white;
+ border: 1px solid rgb(255 255 255 / 20%);
+ border-radius: 8px;
+ box-shadow: 0 3px 10px rgb(0 0 0 / 10%);
+}
+
+:root .wplace-settings-option,
+.wplace-theme-classic .wplace-settings-option {
+ background: #2d3748;
+ color: white;
+}
+
+:root .wplace-stat-colors-grid,
+.wplace-theme-classic .wplace-stat-colors-grid {
+ background: rgb(0 0 0 / 20%);
+ border-radius: 4px;
+}
+
+:root .resize-overlay,
+.wplace-theme-classic .resize-overlay {
+ background: rgb(0 0 0 / 80%);
+}
+
+:root .wplace-overlay,
+.wplace-theme-classic .wplace-overlay {
+ background: rgb(0 0 0 / 80%);
+}
+
+:root .wplace-settings-error,
+.wplace-theme-classic .wplace-settings-error {
+ background: rgb(255 0 0 / 40%) !important;
+}
+
+/* Settings sections and batch controls for classic theme */
+:root .wplace-settings-section-wrapper,
+.wplace-theme-classic .wplace-settings-section-wrapper {
+ background: rgb(255 255 255 / 10%);
+ border-radius: var(--wplace-radius);
+ border: 1px solid rgb(255 255 255 / 10%);
+}
+
+:root .wplace-batch-controls,
+.wplace-theme-classic .wplace-batch-controls {
+ background: rgb(255 255 255 / 10%);
+ border-radius: var(--wplace-radius);
+ border: 1px solid rgb(255 255 255 / 10%);
+}
+
+:root .wplace-overlay-opacity-slider,
+.wplace-theme-classic .wplace-overlay-opacity-slider {
+ background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%);
+ border-radius: 4px;
+}
+
+:root .wplace-speed-value,
+.wplace-theme-classic .wplace-speed-value {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ border-radius: 8px;
+ color: white;
+ box-shadow: 0 3px 10px rgb(79 172 254 / 30%);
+ border: 1px solid rgb(255 255 255 / 20%);
+}
+
+/* Additional elements for classic theme */
+:root .wplace-color-divider,
+.wplace-theme-classic .wplace-color-divider {
+ background: rgb(255 255 255 / 10%);
+}
+
+:root .wplace-settings-select option,
+.wplace-theme-classic .wplace-settings-select option {
+ background: #2d3748;
+ color: white;
+}
+
+:root .wplace-settings-description,
+.wplace-theme-classic .wplace-settings-description {
+ color: rgb(255 255 255 / 70%);
+}
+
+:root .wplace-settings-apply-btn,
+.wplace-theme-classic .wplace-settings-apply-btn {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ color: white;
+ border-radius: 8px;
+}
+
+/* Dual control layout styling for classic theme */
+:root .wplace-dual-control-compact,
+.wplace-theme-classic .wplace-dual-control-compact {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 12px;
+ margin: 8px 0;
+ flex-wrap: wrap;
+}
+
+:root .wplace-slider-container-compact,
+.wplace-theme-classic .wplace-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 160px;
+}
+
+:root .wplace-speed-slider-container-compact,
+.wplace-theme-classic .wplace-speed-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex: 1;
+ min-width: 160px;
+}
+
+:root .wplace-input-group-compact,
+.wplace-theme-classic .wplace-input-group-compact {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+:root .wplace-input-btn-compact,
+.wplace-theme-classic .wplace-input-btn-compact {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ width: 22px;
+ height: 22px;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+:root .wplace-input-btn-compact:hover,
+.wplace-theme-classic .wplace-input-btn-compact:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.3);
+ background: linear-gradient(135deg, #00f2fe 0%, #4facfe 100%);
+}
+
+:root .wplace-input-btn-compact:active,
+.wplace-theme-classic .wplace-input-btn-compact:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
+}
+
+:root .wplace-number-input-compact,
+.wplace-theme-classic .wplace-number-input-compact {
+ background: rgba(255,255,255,0.1);
+ border: 1px solid rgba(255,255,255,0.2);
+ border-radius: 4px;
+ color: white;
+ padding: 4px 8px;
+ font-size: 12px;
+ width: 50px;
+ text-align: center;
+ transition: all 0.2s ease;
+ height: 22px;
+}
+
+:root .wplace-number-input-compact:focus,
+.wplace-theme-classic .wplace-number-input-compact:focus {
+ outline: none;
+ border-color: #4facfe;
+ box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.3);
+ background: rgba(255,255,255,0.15);
+}
+
+:root .wplace-input-label-compact,
+.wplace-theme-classic .wplace-input-label-compact {
+ color: rgba(255,255,255,0.8);
+ font-size: 11px;
+ margin-left: 4px;
+ white-space: nowrap;
+}
+
+:root .wplace-batch-size-header,
+.wplace-theme-classic .wplace-batch-size-header {
+ margin-bottom: 8px;
+}
+
+:root .wplace-batch-size-label,
+.wplace-theme-classic .wplace-batch-size-label {
+ color: var(--wplace-highlight);
+ font-weight: 500;
+ font-size: 13px;
+}
+
+/* Cooldown control styling for classic theme */
+:root .wplace-cooldown-control,
+.wplace-theme-classic .wplace-cooldown-control {
+ margin: 12px 0;
+}
+
+:root .wplace-cooldown-control label,
+.wplace-theme-classic .wplace-cooldown-control label {
+ color: var(--wplace-highlight);
+ font-weight: 500;
+ font-size: 13px;
+ margin-bottom: 8px;
+ display: block;
+}
+
+/* Resize dialog styling for classic theme */
+:root .resize-container,
+.wplace-theme-classic .resize-container {
+ background: #000;
+ border: 1px solid #fff;
+ border-radius: 12px;
+ box-shadow: 0 0 20px rgb(0 0 0 / 50%);
+}
diff --git a/themes/neon.css b/Extension/themes/neon.css
similarity index 78%
rename from themes/neon.css
rename to Extension/themes/neon.css
index 1daa02b..184cb64 100644
--- a/themes/neon.css
+++ b/Extension/themes/neon.css
@@ -1,519 +1,635 @@
-/* WPlace Auto-Image Bot - Neon Theme */
-/* Retro cyberpunk aesthetic with green neon glow effects */
-
-.wplace-theme-neon {
- /* Neon theme colors - matching upstream main neon theme */
- --wplace-primary: #1a1a2e;
- --wplace-secondary: #16213e;
- --wplace-accent: #0f3460;
- --wplace-text: #00ff41;
- --wplace-highlight: #ff6b35;
- --wplace-success: #39ff14;
- --wplace-error: #ff073a;
- --wplace-warning: #ff0;
-
- /* Neon UI properties */
- --wplace-radius: 0;
- --wplace-border-style: solid;
- --wplace-border-width: 2px;
- --wplace-border-color: #00ff41;
- --wplace-shadow: 0 0 20px rgb(0 255 65 / 30%), inset 0 0 20px rgb(0 255 65 / 10%);
- --wplace-backdrop: none;
- --wplace-font: 'Press Start 2P', monospace, 'Courier New';
-
- /* Z-index layers */
- --wplace-z-overlay: 10000;
- --wplace-z-alert: 10002;
- --wplace-z-settings: 10002;
-
- /* Feature toggles */
- --wplace-scanline: 1;
- --wplace-pixel-blink: 1;
-
- /* Icon colors - neon variants */
- --wplace-icon-primary: #00ff41;
- --wplace-icon-secondary: #39ff14;
- --wplace-icon-palette: #ff6b35;
-
- /* Additional UI colors - neon variants */
- --wplace-danger: #ff073a;
- --wplace-danger-dark: #cc0531;
- --wplace-muted-text: #00ff4180;
- --wplace-highlight-secondary: #fa0;
-
- /* Text variants - neon style */
- --wplace-text-secondary: #00ff41dd;
- --wplace-text-muted: #00ff41bb;
- --wplace-text-dim: #00ff4199;
- --wplace-text-faded: #00ff41cc;
-
- /* Background variants - neon style */
- --wplace-bg-input: rgb(0 255 65 / 15%);
- --wplace-bg-subtle: rgb(0 255 65 / 8%);
- --wplace-bg-faint: rgb(0 255 65 / 5%);
- --wplace-bg-ghost: rgb(0 255 65 / 3%);
- --wplace-bg-whisper: rgb(0 255 65 / 2%);
-
- /* Border variants - neon style */
- --wplace-border-subtle: rgb(0 255 65 / 40%);
- --wplace-border-faint: rgb(0 255 65 / 25%);
- --wplace-border-ghost: rgb(0 255 65 / 15%);
- --wplace-border-ultra-faint: rgb(0 255 65 / 8%);
-
- /* Shadow variants - neon style */
- --wplace-shadow-drag: 0 12px 40px rgb(0 255 65 / 60%), 0 0 0 2px rgb(0 255 65 / 80%), 0 0 20px rgb(0 255 65 / 30%);
- --wplace-shadow-notification: 0 4px 12px rgb(0 255 65 / 40%), 0 0 15px rgb(0 255 65 / 20%);
- --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 255 65 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(0 255 65 / 30%);
- --wplace-shadow-slider-hover: 0 4px 8px rgb(0 255 65 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(0 255 65 / 40%);
-
- /* Animation colors - neon style */
- --wplace-pulse-start: rgb(0 255 65 / 80%);
- --wplace-pulse-mid: rgb(0 255 65 / 0%);
- --wplace-pulse-end: rgb(0 255 65 / 0%);
-
- /* Slider colors - neon style */
- --wplace-slider-thumb-bg: #00ff41;
-}
-
-/* Neon-specific styling overrides */
-.wplace-theme-neon #wplace-image-bot-container {
- background: var(--wplace-primary) !important;
- border: 2px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
- font-family: var(--wplace-font) !important;
-}
-
-.wplace-theme-neon #wplace-stats-container {
- background: var(--wplace-primary) !important;
- border: 2px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
- font-family: var(--wplace-font) !important;
- position: fixed !important;
- overflow: hidden !important;
- z-index: 9998 !important;
-}
-
-.wplace-theme-neon .wplace-header {
- background: var(--wplace-secondary) !important;
- border-bottom: 1px solid var(--wplace-text) !important;
- color: var(--wplace-text) !important;
- text-shadow: 0 0 10px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
-}
-
-.wplace-theme-neon .wplace-section {
- background: rgb(22 33 62 / 50%) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
-}
-
-.wplace-theme-neon .wplace-status-section {
- background: rgb(22 33 62 / 50%) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
-}
-
-.wplace-theme-neon .wplace-section-title {
- color: var(--wplace-text) !important;
- text-shadow: 0 0 8px var(--wplace-text) !important;
- text-transform: uppercase !important;
- font-family: var(--wplace-font) !important;
- font-size: 10px !important;
-}
-
-.wplace-theme-neon .wplace-btn {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- text-shadow: 0 0 5px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- font-size: 9px !important;
- text-transform: uppercase !important;
-}
-
-/* Pixel blinking and neon glow animation for buttons */
-.wplace-theme-neon .wplace-btn:hover:not(:disabled) {
- box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(0 255 65 / 10%) !important;
- animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important;
-}
-
-.wplace-theme-neon .wplace-btn-start {
- background: var(--wplace-secondary) !important;
- border-color: var(--wplace-success) !important;
- color: var(--wplace-success) !important;
- text-shadow: 0 0 8px var(--wplace-success) !important;
-}
-
-.wplace-theme-neon .wplace-btn-stop {
- background: var(--wplace-secondary) !important;
- border-color: var(--wplace-error) !important;
- color: var(--wplace-error) !important;
- text-shadow: 0 0 8px var(--wplace-error) !important;
-}
-
-.wplace-theme-neon .wplace-btn-upload {
- background: var(--wplace-secondary) !important;
- border: 1px dashed var(--wplace-highlight) !important;
- color: var(--wplace-highlight) !important;
- text-shadow: 0 0 8px var(--wplace-highlight) !important;
-}
-
-.wplace-theme-neon .wplace-btn-select {
- background: var(--wplace-secondary) !important;
- border-color: var(--wplace-highlight) !important;
- color: var(--wplace-highlight) !important;
- text-shadow: 0 0 8px var(--wplace-highlight) !important;
-}
-
-.wplace-theme-neon .wplace-btn-file {
- background: var(--wplace-secondary) !important;
- border-color: var(--wplace-warning) !important;
- color: var(--wplace-warning) !important;
- text-shadow: 0 0 8px var(--wplace-warning) !important;
-}
-
-.wplace-theme-neon .wplace-progress {
- background: rgb(0 0 0 / 80%) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
-}
-
-.wplace-theme-neon .wplace-progress-bar {
- background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important;
- box-shadow: 0 0 10px var(--wplace-success) !important;
-}
-
-.wplace-theme-neon .wplace-stat-value {
- color: var(--wplace-text) !important;
- text-shadow: 0 0 5px var(--wplace-text) !important;
-}
-
-.wplace-theme-neon .status-success {
- background: rgb(57 255 20 / 10%) !important;
- border-color: var(--wplace-success) !important;
- color: var(--wplace-success) !important;
- box-shadow: 0 0 15px var(--wplace-success) !important;
- text-shadow: 0 0 8px var(--wplace-success) !important;
-}
-
-.wplace-theme-neon .status-error {
- background: rgb(255 7 58 / 10%) !important;
- border-color: var(--wplace-error) !important;
- color: var(--wplace-error) !important;
- box-shadow: 0 0 15px var(--wplace-error) !important;
- text-shadow: 0 0 8px var(--wplace-error) !important;
-}
-
-.wplace-theme-neon .status-default {
- background: rgb(0 255 65 / 10%) !important;
- border-color: var(--wplace-text) !important;
- color: var(--wplace-text) !important;
- text-shadow: 0 0 5px var(--wplace-text) !important;
-}
-
-/* Settings dialog neon styling */
-.wplace-theme-neon #wplace-settings-container {
- background: var(--wplace-primary) !important;
- border: 2px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(0 255 65 / 10%) !important;
- font-family: var(--wplace-font) !important;
-}
-
-.wplace-theme-neon .wplace-settings-header {
- background: var(--wplace-secondary) !important;
- border-bottom: 1px solid var(--wplace-text) !important;
-}
-
-.wplace-theme-neon .wplace-settings-header h3 {
- color: var(--wplace-text) !important;
- text-shadow: 0 0 10px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- font-size: 16px !important;
- text-transform: uppercase !important;
-}
-
-.wplace-theme-neon .wplace-settings-close-btn {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
-}
-
-.wplace-theme-neon .wplace-settings-close-btn:hover {
- background: var(--wplace-error) !important;
- box-shadow: 0 0 15px var(--wplace-error) !important;
-}
-
-.wplace-theme-neon .wplace-settings-section-wrapper {
- background: rgb(22 33 62 / 30%) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
-}
-
-.wplace-theme-neon .wplace-settings-select {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- font-size: 11px !important;
-}
-
-.wplace-theme-neon .wplace-settings-section-label {
- color: var(--wplace-text) !important;
- text-shadow: 0 0 8px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- font-size: 12px !important;
- text-transform: uppercase !important;
-}
-
-.wplace-theme-neon .wplace-speed-value {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- text-shadow: 0 0 8px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important;
-}
-
-.wplace-theme-neon .wplace-overlay-opacity-value {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- text-shadow: 0 0 5px var(--wplace-text) !important;
-}
-
-/* Neon slider styling */
-.wplace-theme-neon .wplace-slider,
-.wplace-theme-neon .wplace-speed-slider,
-.wplace-theme-neon .wplace-overlay-opacity-slider {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important;
-}
-
-.wplace-theme-neon .wplace-slider::-webkit-slider-thumb,
-.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb,
-.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb {
- -webkit-appearance: none;
- width: 16px;
- height: 16px;
- border-radius: 0 !important;
- background: var(--wplace-slider-thumb-bg) !important;
- border: 2px solid var(--wplace-text) !important;
- cursor: pointer;
- transition: all 0.2s ease;
- box-shadow: 0 0 8px rgb(0 255 65 / 50%) !important;
-}
-
-.wplace-theme-neon .wplace-slider::-webkit-slider-thumb:hover,
-.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb:hover,
-.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover {
- transform: scale(1.1);
- box-shadow: 0 0 15px var(--wplace-text) !important;
-}
-
-/* Scanline animation for neon theme */
-.wplace-theme-neon #wplace-image-bot-container::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, transparent, var(--wplace-text), transparent);
- z-index: 1;
- pointer-events: none;
- animation: scanline 3s linear infinite;
- opacity: 0.7;
-}
-
-
-.wplace-theme-neon #wplace-stats-container::before {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, transparent, var(--wplace-text), transparent);
- z-index: 1;
- pointer-events: none;
- animation: scanline-stats 4s linear infinite;
- opacity: 0.7;
-}
-
-
-/* Text glow animations */
-@keyframes neon-glow {
- 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;
- }
-}
-
-@keyframes pixel-blink {
- 0%, 50% {
- opacity: 1;
- }
-
- 51%, 100% {
- opacity: 0.7;
- }
-}
-
-@keyframes scanline {
- 0% {
- transform: translateY(-100%);
- }
-
- 100% {
- transform: translateY(400px);
- }
-}
-
-@keyframes scanline-stats {
- 0% {
- transform: translateY(-100%);
- }
-
- 100% {
- transform: translateY(300px);
- }
-}
-
-/* Dual control layout styling for neon theme */
-.wplace-theme-neon .wplace-dual-control-compact {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 8px;
- margin: 6px 0;
- flex-wrap: wrap;
-}
-
-.wplace-theme-neon .wplace-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 1;
- min-width: 160px;
-}
-
-.wplace-theme-neon .wplace-speed-slider-container-compact {
- display: flex;
- align-items: center;
- gap: 6px;
- flex: 1;
- min-width: 160px;
-}
-
-.wplace-theme-neon .wplace-input-group-compact {
- display: flex;
- align-items: center;
- gap: 3px;
- flex-shrink: 0;
-}
-
-.wplace-theme-neon .wplace-input-btn-compact {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- width: 18px;
- height: 18px;
- cursor: pointer;
- font-weight: bold;
- font-size: 10px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
- text-shadow: 0 0 5px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
-}
-
-.wplace-theme-neon .wplace-input-btn-compact:hover {
- box-shadow: 0 0 10px var(--wplace-text) !important;
- animation: pixel-blink 0.5s infinite !important;
-}
-
-.wplace-theme-neon .wplace-number-input-compact {
- background: var(--wplace-secondary) !important;
- border: 1px solid var(--wplace-text) !important;
- border-radius: 0 !important;
- color: var(--wplace-text) !important;
- padding: 2px 4px;
- font-size: 10px;
- width: 40px;
- text-align: center;
- transition: all 0.2s ease;
- height: 18px;
- font-family: var(--wplace-font) !important;
- text-shadow: 0 0 5px var(--wplace-text) !important;
-}
-
-.wplace-theme-neon .wplace-number-input-compact:focus {
- outline: none;
- box-shadow: 0 0 10px var(--wplace-text) !important;
- background: rgb(0 255 65 / 15%) !important;
-}
-
-.wplace-theme-neon .wplace-input-label-compact {
- color: var(--wplace-text) !important;
- font-size: 9px;
- margin-left: 3px;
- white-space: nowrap;
- text-shadow: 0 0 5px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- text-transform: uppercase;
-}
-
-.wplace-theme-neon .wplace-batch-size-header {
- margin-bottom: 6px;
-}
-
-.wplace-theme-neon .wplace-batch-size-label {
- color: var(--wplace-text) !important;
- font-weight: 500;
- font-size: 11px;
- text-shadow: 0 0 8px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- text-transform: uppercase;
-}
-
-/* Cooldown control styling for neon theme */
-.wplace-theme-neon .wplace-cooldown-control {
- margin: 8px 0;
-}
-
-.wplace-theme-neon .wplace-cooldown-control label {
- color: var(--wplace-text) !important;
- font-weight: 500;
- font-size: 11px;
- text-shadow: 0 0 8px var(--wplace-text) !important;
- font-family: var(--wplace-font) !important;
- text-transform: uppercase;
- margin-bottom: 6px;
- display: block;
-}
-
-/* Resize dialog styling for neon theme */
-.wplace-theme-neon .resize-container {
- background: #1a1a2e !important;
- border: 3px solid #00ff41 !important;
- border-radius: 0 !important;
- box-shadow: 0 0 30px rgb(0 255 65 / 50%) !important;
- font-family: 'Press Start 2P', monospace !important;
-}
-
+/* WPlace Auto-Image Bot - Neon Theme */
+/* Retro cyberpunk aesthetic with green neon glow effects */
+
+.wplace-theme-neon {
+ /* Neon theme colors - matching upstream main neon theme */
+ --wplace-primary: #1a1a2e;
+ --wplace-secondary: #16213e;
+ --wplace-accent: #0f3460;
+ --wplace-text: #00ff41;
+ --wplace-highlight: #ff6b35;
+ --wplace-success: #39ff14;
+ --wplace-error: #ff073a;
+ --wplace-warning: #ff0;
+
+ /* Neon UI properties */
+ --wplace-radius: 0;
+ --wplace-border-style: solid;
+ --wplace-border-width: 2px;
+ --wplace-border-color: #00ff41;
+ --wplace-shadow: 0 0 20px rgb(0 255 65 / 30%), inset 0 0 20px rgb(0 255 65 / 10%);
+ --wplace-backdrop: none;
+ --wplace-font: 'Press Start 2P', monospace, 'Courier New';
+
+ /* Z-index layers */
+ --wplace-z-overlay: 10000;
+ --wplace-z-alert: 10002;
+ --wplace-z-settings: 10002;
+
+ /* Feature toggles */
+ --wplace-scanline: 1;
+ --wplace-pixel-blink: 1;
+
+ /* Icon colors - neon variants */
+ --wplace-icon-primary: #00ff41;
+ --wplace-icon-secondary: #39ff14;
+ --wplace-icon-palette: #ff6b35;
+
+ /* Additional UI colors - neon variants */
+ --wplace-danger: #ff073a;
+ --wplace-danger-dark: #cc0531;
+ --wplace-muted-text: #00ff4180;
+ --wplace-highlight-secondary: #fa0;
+
+ /* Text variants - neon style */
+ --wplace-text-secondary: #00ff41dd;
+ --wplace-text-muted: #00ff41bb;
+ --wplace-text-dim: #00ff4199;
+ --wplace-text-faded: #00ff41cc;
+
+ /* Background variants - neon style */
+ --wplace-bg-input: rgb(0 255 65 / 15%);
+ --wplace-bg-subtle: rgb(0 255 65 / 8%);
+ --wplace-bg-faint: rgb(0 255 65 / 5%);
+ --wplace-bg-ghost: rgb(0 255 65 / 3%);
+ --wplace-bg-whisper: rgb(0 255 65 / 2%);
+
+ /* Border variants - neon style */
+ --wplace-border-subtle: rgb(0 255 65 / 40%);
+ --wplace-border-faint: rgb(0 255 65 / 25%);
+ --wplace-border-ghost: rgb(0 255 65 / 15%);
+ --wplace-border-ultra-faint: rgb(0 255 65 / 8%);
+
+ /* Shadow variants - neon style */
+ --wplace-shadow-drag: 0 12px 40px rgb(0 255 65 / 60%), 0 0 0 2px rgb(0 255 65 / 80%), 0 0 20px rgb(0 255 65 / 30%);
+ --wplace-shadow-notification: 0 4px 12px rgb(0 255 65 / 40%), 0 0 15px rgb(0 255 65 / 20%);
+ --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 255 65 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(0 255 65 / 30%);
+ --wplace-shadow-slider-hover: 0 4px 8px rgb(0 255 65 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(0 255 65 / 40%);
+
+ /* Animation colors - neon style */
+ --wplace-pulse-start: rgb(0 255 65 / 80%);
+ --wplace-pulse-mid: rgb(0 255 65 / 0%);
+ --wplace-pulse-end: rgb(0 255 65 / 0%);
+
+ /* Slider colors - neon style */
+ --wplace-slider-thumb-bg: #00ff41;
+}
+
+/* Neon-specific styling overrides */
+.wplace-theme-neon #wplace-image-bot-container {
+ background: var(--wplace-primary) !important;
+ border: 2px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+/* Multi-Account Panel Styles */
+.wplace-theme-neon #wplace-multi-account-container {
+ position: fixed !important;
+ top: 180px !important;
+ right: 20px !important;
+ width: 320px !important;
+ background: rgb(22 33 62 / 50%) !important;
+ border: 2px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
+ font-family: var(--wplace-font) !important;
+ z-index: 9997 !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-panel-header {
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+ padding: 10px !important;
+ background: var(--wplace-secondary) !important;
+ border-bottom: 1px solid var(--wplace-text) !important;
+ text-transform: uppercase !important;
+ font-size: 9px !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn {
+ background: transparent !important;
+ border: 1px solid var(--wplace-text) !important;
+ color: var(--wplace-text) !important;
+ font-size: 8px !important;
+ padding: 4px 8px !important;
+ cursor: pointer !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ text-transform: uppercase !important;
+}
+
+.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn:hover {
+ box-shadow: 0 0 15px var(--wplace-text) !important;
+ animation: pixel-blink 0.5s infinite !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-content {
+ padding: 10px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-info {
+ font-size: 9px !important;
+ text-transform: uppercase !important;
+ margin-bottom: 10px !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-current,
+.wplace-theme-neon .wplace-multi-account-stats {
+ margin-bottom: 8px !important;
+ font-size: 8px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-toggle {
+ margin-bottom: 12px !important;
+ display: flex !important;
+ align-items: center !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-switch {
+ margin-right: 8px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list {
+ max-height: 200px !important;
+ overflow-y: auto !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-item {
+ display: flex !important;
+ justify-content: space-between !important;
+ align-items: center !important;
+ padding: 8px !important;
+ margin-bottom: 4px !important;
+ background: rgb(22 33 62 / 50%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ font-size: 8px !important;
+ text-transform: uppercase !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-item.active {
+ background: rgb(0 255 65 / 10%) !important;
+ border-color: var(--wplace-success) !important;
+ box-shadow: 0 0 15px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar {
+ width: 6px !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-track {
+ background: rgb(22 33 62 / 50%) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb {
+ background: var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 10px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb:hover {
+ background: var(--wplace-success) !important;
+ box-shadow: 0 0 15px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon #wplace-stats-container {
+ background: var(--wplace-primary) !important;
+ border: 2px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important;
+ font-family: var(--wplace-font) !important;
+ position: fixed !important;
+ overflow: hidden !important;
+ z-index: 9998 !important;
+}
+
+.wplace-theme-neon .wplace-header {
+ background: var(--wplace-secondary) !important;
+ border-bottom: 1px solid var(--wplace-text) !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 10px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-section {
+ background: rgb(22 33 62 / 50%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+}
+
+.wplace-theme-neon .wplace-status-section {
+ background: rgb(22 33 62 / 50%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+}
+
+.wplace-theme-neon .wplace-section-title {
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 8px var(--wplace-text) !important;
+ text-transform: uppercase !important;
+ font-family: var(--wplace-font) !important;
+ font-size: 10px !important;
+}
+
+.wplace-theme-neon .wplace-btn {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ font-size: 9px !important;
+ text-transform: uppercase !important;
+}
+
+/* Pixel blinking and neon glow animation for buttons */
+.wplace-theme-neon .wplace-btn:hover:not(:disabled) {
+ box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(0 255 65 / 10%) !important;
+ animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important;
+}
+
+.wplace-theme-neon .wplace-btn-start {
+ background: var(--wplace-secondary) !important;
+ border-color: var(--wplace-success) !important;
+ color: var(--wplace-success) !important;
+ text-shadow: 0 0 8px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon .wplace-btn-stop {
+ background: var(--wplace-secondary) !important;
+ border-color: var(--wplace-error) !important;
+ color: var(--wplace-error) !important;
+ text-shadow: 0 0 8px var(--wplace-error) !important;
+}
+
+.wplace-theme-neon .wplace-btn-upload {
+ background: var(--wplace-secondary) !important;
+ border: 1px dashed var(--wplace-highlight) !important;
+ color: var(--wplace-highlight) !important;
+ text-shadow: 0 0 8px var(--wplace-highlight) !important;
+}
+
+.wplace-theme-neon .wplace-btn-select {
+ background: var(--wplace-secondary) !important;
+ border-color: var(--wplace-highlight) !important;
+ color: var(--wplace-highlight) !important;
+ text-shadow: 0 0 8px var(--wplace-highlight) !important;
+}
+
+.wplace-theme-neon .wplace-btn-file {
+ background: var(--wplace-secondary) !important;
+ border-color: var(--wplace-warning) !important;
+ color: var(--wplace-warning) !important;
+ text-shadow: 0 0 8px var(--wplace-warning) !important;
+}
+
+.wplace-theme-neon .wplace-progress {
+ background: rgb(0 0 0 / 80%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+}
+
+.wplace-theme-neon .wplace-progress-bar {
+ background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important;
+ box-shadow: 0 0 10px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon .wplace-stat-value {
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .status-success {
+ background: rgb(57 255 20 / 10%) !important;
+ border-color: var(--wplace-success) !important;
+ color: var(--wplace-success) !important;
+ box-shadow: 0 0 15px var(--wplace-success) !important;
+ text-shadow: 0 0 8px var(--wplace-success) !important;
+}
+
+.wplace-theme-neon .status-error {
+ background: rgb(255 7 58 / 10%) !important;
+ border-color: var(--wplace-error) !important;
+ color: var(--wplace-error) !important;
+ box-shadow: 0 0 15px var(--wplace-error) !important;
+ text-shadow: 0 0 8px var(--wplace-error) !important;
+}
+
+.wplace-theme-neon .status-default {
+ background: rgb(0 255 65 / 10%) !important;
+ border-color: var(--wplace-text) !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+/* Settings dialog neon styling */
+.wplace-theme-neon #wplace-settings-container {
+ background: var(--wplace-primary) !important;
+ border: 2px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(0 255 65 / 10%) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-settings-header {
+ background: var(--wplace-secondary) !important;
+ border-bottom: 1px solid var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .wplace-settings-header h3 {
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 10px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ font-size: 16px !important;
+ text-transform: uppercase !important;
+}
+
+.wplace-theme-neon .wplace-settings-close-btn {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .wplace-settings-close-btn:hover {
+ background: var(--wplace-error) !important;
+ box-shadow: 0 0 15px var(--wplace-error) !important;
+}
+
+.wplace-theme-neon .wplace-settings-section-wrapper {
+ background: rgb(22 33 62 / 30%) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+}
+
+.wplace-theme-neon .wplace-settings-select {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ font-size: 11px !important;
+}
+
+.wplace-theme-neon .wplace-settings-section-label {
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 8px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ font-size: 12px !important;
+ text-transform: uppercase !important;
+}
+
+.wplace-theme-neon .wplace-speed-value {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 8px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important;
+}
+
+.wplace-theme-neon .wplace-overlay-opacity-value {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+/* Neon slider styling */
+.wplace-theme-neon .wplace-slider,
+.wplace-theme-neon .wplace-speed-slider,
+.wplace-theme-neon .wplace-overlay-opacity-slider {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important;
+}
+
+.wplace-theme-neon .wplace-slider::-webkit-slider-thumb,
+.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb,
+.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 0 !important;
+ background: var(--wplace-slider-thumb-bg) !important;
+ border: 2px solid var(--wplace-text) !important;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ box-shadow: 0 0 8px rgb(0 255 65 / 50%) !important;
+}
+
+.wplace-theme-neon .wplace-slider::-webkit-slider-thumb:hover,
+.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb:hover,
+.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.1);
+ box-shadow: 0 0 15px var(--wplace-text) !important;
+}
+
+/* Scanline animation for neon theme */
+.wplace-theme-neon #wplace-image-bot-container::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--wplace-text), transparent);
+ z-index: 1;
+ pointer-events: none;
+ animation: scanline 3s linear infinite;
+ opacity: 0.7;
+}
+
+
+.wplace-theme-neon #wplace-stats-container::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, transparent, var(--wplace-text), transparent);
+ z-index: 1;
+ pointer-events: none;
+ animation: scanline-stats 4s linear infinite;
+ opacity: 0.7;
+}
+
+
+/* Text glow animations */
+@keyframes neon-glow {
+ 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;
+ }
+}
+
+@keyframes pixel-blink {
+ 0%, 50% {
+ opacity: 1;
+ }
+
+ 51%, 100% {
+ opacity: 0.7;
+ }
+}
+
+@keyframes scanline {
+ 0% {
+ transform: translateY(-100%);
+ }
+
+ 100% {
+ transform: translateY(400px);
+ }
+}
+
+@keyframes scanline-stats {
+ 0% {
+ transform: translateY(-100%);
+ }
+
+ 100% {
+ transform: translateY(300px);
+ }
+}
+
+/* Dual control layout styling for neon theme */
+.wplace-theme-neon .wplace-dual-control-compact {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ margin: 6px 0;
+ flex-wrap: wrap;
+}
+
+.wplace-theme-neon .wplace-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ min-width: 160px;
+}
+
+.wplace-theme-neon .wplace-speed-slider-container-compact {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+ min-width: 160px;
+}
+
+.wplace-theme-neon .wplace-input-group-compact {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ flex-shrink: 0;
+}
+
+.wplace-theme-neon .wplace-input-btn-compact {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ font-weight: bold;
+ font-size: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+}
+
+.wplace-theme-neon .wplace-input-btn-compact:hover {
+ box-shadow: 0 0 10px var(--wplace-text) !important;
+ animation: pixel-blink 0.5s infinite !important;
+}
+
+.wplace-theme-neon .wplace-number-input-compact {
+ background: var(--wplace-secondary) !important;
+ border: 1px solid var(--wplace-text) !important;
+ border-radius: 0 !important;
+ color: var(--wplace-text) !important;
+ padding: 2px 4px;
+ font-size: 10px;
+ width: 40px;
+ text-align: center;
+ transition: all 0.2s ease;
+ height: 18px;
+ font-family: var(--wplace-font) !important;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+}
+
+.wplace-theme-neon .wplace-number-input-compact:focus {
+ outline: none;
+ box-shadow: 0 0 10px var(--wplace-text) !important;
+ background: rgb(0 255 65 / 15%) !important;
+}
+
+.wplace-theme-neon .wplace-input-label-compact {
+ color: var(--wplace-text) !important;
+ font-size: 9px;
+ margin-left: 3px;
+ white-space: nowrap;
+ text-shadow: 0 0 5px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ text-transform: uppercase;
+}
+
+.wplace-theme-neon .wplace-batch-size-header {
+ margin-bottom: 6px;
+}
+
+.wplace-theme-neon .wplace-batch-size-label {
+ color: var(--wplace-text) !important;
+ font-weight: 500;
+ font-size: 11px;
+ text-shadow: 0 0 8px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ text-transform: uppercase;
+}
+
+/* Cooldown control styling for neon theme */
+.wplace-theme-neon .wplace-cooldown-control {
+ margin: 8px 0;
+}
+
+.wplace-theme-neon .wplace-cooldown-control label {
+ color: var(--wplace-text) !important;
+ font-weight: 500;
+ font-size: 11px;
+ text-shadow: 0 0 8px var(--wplace-text) !important;
+ font-family: var(--wplace-font) !important;
+ text-transform: uppercase;
+ margin-bottom: 6px;
+ display: block;
+}
+
+/* Resize dialog styling for neon theme */
+.wplace-theme-neon .resize-container {
+ background: #1a1a2e !important;
+ border: 3px solid #00ff41 !important;
+ border-radius: 0 !important;
+ box-shadow: 0 0 30px rgb(0 255 65 / 50%) !important;
+ font-family: 'Press Start 2P', monospace !important;
+}
+
diff --git a/README.md b/README.md
index 4e90937..7abb640 100644
--- a/README.md
+++ b/README.md
@@ -1,126 +1,119 @@
+WPlace AutoBOT Browser Extension
-
+
WPlace AutoBOT
-This project was fork from https://github.com/DarkModde/WPlace-AutoBOT
+This project was forked from https://github.com/DarkModde/WPlace-AutoBOT
- Consider checking out our discord: https://discord.gg/knkNRYyQcm
+Consider checking out our Discord: https://discord.gg/knkNRYyQcm
- Practical scripts with intuitive menus to make your life easier on WPlace !
- Perfect for those who want to level up automatically or build massive pixel arts — without wasting time.
-
- Warning: This is a purely educational script to demonstrate what can be done on wplace.live, it is not our responsibility of what happens if you use the script.
+Practical scripts with intuitive menus to make your life easier on WPlace !
+Perfect for those who want to level up automatically or build massive pixel arts — without wasting time.
+
+Warning: This is a purely educational script to demonstrate what can be done on wplace.live, it is not our responsibility of what happens if you use the script.
+
- If you're wondering—yes! These scripts were Developed by a team consisting of Brazilians, Vietnamese, and Indonesians. HUEHUE!
-
-
-
-
-
-
-
-
-
----
-
-🚀┃How to use the scripts:
-
+If you're wondering—yes! These scripts were Developed by a team consisting of Brazilians, Vietnamese, and Indonesians. HUEHUE!
- It’s super easy: copy one of the codes below, paste it into your browser’s bookmarks bar,
- and run it while you're on wplace.live.
+
+
+
+
+
+
-
+🚀┃Installation & Usage
+Since this is a browser extension that is not on the official web stores, you need to load it manually. Follow the instructions for your browser below.
-### 🎯┃Auto-Farm
-#### AUTOFARM USES CHARGES TO GET LEVELS, IT DOES NOT DRAW AN IMAGE FOR YOU. PLEASE USE AUTO-IMAGE FOR THAT
-```js
-javascript:(async()=>{ const u='https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Farm.js'; const c=await (await fetch(u,{cache:'no-store'})).text(); const blob=new Blob([c],{type:'application/javascript'}); const url=URL.createObjectURL(blob); const s=document.createElement('script'); s.src=url; document.body.appendChild(s); })();
-```
+> Download From here https://github.com/Wplace-AutoBot/WPlace-AutoBOT/releases/tag/Extension
-### 🖼️┃Auto-Image
+>Extract: Unzip the downloaded file (Extension.zip) to a permanent folder you won't delete.
-```js
-javascript:(async()=>{ const u='https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/Auto-Image.js'; const c=await (await fetch(u,{cache:'no-store'})).text(); const blob=new Blob([c],{type:'application/javascript'}); const url=URL.createObjectURL(blob); const s=document.createElement('script'); s.src=url; document.body.appendChild(s); })();;
-```
+>Open Extensions Page: Open your browser and go to the extensions page. You can usually do this by typing chrome://extensions (for Chrome) or edge://extensions (for Edge) in your address bar.
-
- 📖┃Tutorials
+>Enable Developer Mode: Find and turn on the Developer mode toggle, usually located in the top-right corner of the page.
----
+>Load the Extension: Click on the Load unpacked button that appears.
-
+>Select Folder: In the file dialog, navigate to the folder where you extracted the files and select the main folder (the one containing manifest.json).
----
+>Done! The WPlace AutoBOT extension should now appear in your extensions list and be active. Visit wplace.live and the bot menu will appear.
-
+For Firefox:
+> Download https://github.com/Wplace-AutoBot/WPlace-AutoBOT/releases/tag/Extension
----
+>Open Debugging Page: Open Firefox and type about:debugging into the address bar and press Enter.
-
+>Select "This Firefox": In the left-hand menu, click on This Firefox.
-
+>Load Temporary Add-on: Click on the Load Temporary Add-on... button.
+>Select File: Navigate into the folder you extracted and select the manifest.json file.
-
+>Done! The extension is now loaded for your current browser session. Visit wplace.live to use it. Note: For Firefox, you may need to repeat this process every time you restart the browser.
-> [!IMPORTANT]
-> 1. None of the scripts will work if you close the browser tab. You must keep the WPlace tab open, even if it's in the background.
-> 2. Never run both scripts on the same page — this can bug your WPlace. If it happens, just refresh the page!
-> 3. If the script doesn’t detect the available colors, just click the "Paint" button to show them — only then start the Auto-Imager.
+For Mobile (Android Only):
+You can use the extension on Android using a browser that supports Chrome extensions, such as Kiwi Browser.
-
+>Download and install Kiwi Browser from the Google Play Store.
-### ✨┃Features
+>Follow the exact same steps listed for Chrome above, using Kiwi Browser instead. The interface for loading unpacked extensions is identical.
+✨┃Features
- [x] Automatically farms droplets/levels
+
- [x] Correct Color Pixel Skip
-- [x] Overlay(you dont need bluemarble anymore)
+
+- [x] Overlay (you don't need Bluemarble anymore)
+
- [x] Paint over multiple tiles
+
- [x] Multi Account Auto Switcher Via Other Branch ; https://github.com/Wplace-AutoBot/WPlace-AutoBOT/tree/Acc-switch (Ram Efficient LOL)
+
- [x] AutoCaptcha Solver With Turnstile Token Generator
+
- [x] Wider Paint Control Options
+
- [x] Advance Color Processing
+
- [x] MultiLanguage Support
+
- [x] Multi account through save and load feature
-- [x] Works on mobile :P
-
----
-
-
----
+- [x] Works on mobile (via browsers like Kiwi) :P
-
+
----
+📋┃To-do
+[x] Making AutoFarm working
-### 📋┃To-do
-
-- [x] Making AutoFarm working
- [ ] Merge both scripts into one
+
- [x] Add Overlay system
+
- [x] Add Cross Tiles Support
+
- [x] Add AutoCaptcha Solver
+
- [x] Add Correct Color Pixel Skip
+
- [x] Separate Css
----
-
-- Contributors:
-
+Contributors:
+
-
+