From 41d7fa8b5f91205d9e729d3064dc2e8ca9e5fedb Mon Sep 17 00:00:00 2001 From: kyuketski Date: Fri, 29 Aug 2025 19:18:27 +0300 Subject: [PATCH] chore: add local linter and format files * Set up a linter for local use only; changes are not pushed to the branch. * Reformatted project files according to local linter rules. * Improves local code consistency and readability without affecting the shared codebase. --- AccountSwapper/background.js | 169 +- AccountSwapper/content.js | 58 +- Auto-Farm.js | 168 +- Auto-Image.js | 4060 ++++++++++++++++++++-------------- 4 files changed, 2584 insertions(+), 1871 deletions(-) diff --git a/AccountSwapper/background.js b/AccountSwapper/background.js index d6f1850..500060e 100644 --- a/AccountSwapper/background.js +++ b/AccountSwapper/background.js @@ -1,84 +1,85 @@ -const cookieDomain = ".backend.wplace.live"; - -async function preserveAndResetJ() { - let savedValue = null; - - try { - const oldJ = await chrome.cookies.get({ - url: "https://backend.wplace.live/", - name: "j" - }); - - if (oldJ && oldJ.value) { - savedValue = oldJ.value; - console.log("[bg] Saved j cookie:", savedValue); - } else { - console.warn("[bg] No 'j' cookie found → will only nuke cf_clearance"); - } - - setTimeout(async () => { - await new Promise((resolve) => { - chrome.browsingData.remove( - { origins: ["https://wplace.live"] }, - { cookies: true }, - () => { - console.log("[bg] Nuke done (delayed)"); - resolve(); - } - ); - }); - - if (savedValue) { - setCookie(savedValue); - } else { - console.log("[bg] No j to restore, leaving nuked"); - } - }, 2000); - - } catch (err) { - console.error("[bg] Error in preserveAndResetJ:", err); - } -} - -chrome.webNavigation.onCompleted.addListener((details) => { - if (details.url.includes("wplace.live")) { - console.log("[bg] Page load detected → nuking cookies with double pass"); - preserveAndResetJ(); - } -}, { url: [{ hostContains: "wplace.live" }] }); - -function setCookie(value) { - const cleaned = value.trim(); - console.log("[bg] setCookie CALLED with:", cleaned); - - chrome.cookies.set({ - url: "https://backend.wplace.live/", - name: "j", - value: cleaned, - domain: cookieDomain, - path: "/" - }, (cookie) => { - if (chrome.runtime.lastError) { - console.error("[bg] cookie set error:", chrome.runtime.lastError.message); - } else { - console.log("[bg] cookie set result:", cookie); - - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0) { - chrome.tabs.sendMessage(tabs[0].id, { - type: "cookieSet", - value: cleaned - }); - } - }); - } - }); -} - -chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => { - if (msg.type === "setCookie" && msg.value) { - await setCookie(msg.value); - sendResponse({ status: "ok" }); - } - return true; -}); +const cookieDomain = '.backend.wplace.live'; + +async function preserveAndResetJ() { + let savedValue = null; + + try { + const oldJ = await chrome.cookies.get({ + url: 'https://backend.wplace.live/', + name: 'j', + }); + + if (oldJ && oldJ.value) { + savedValue = oldJ.value; + console.log('[bg] Saved j cookie:', savedValue); + } else { + console.warn("[bg] No 'j' cookie found → will only nuke cf_clearance"); + } + + setTimeout(async () => { + await new Promise((resolve) => { + chrome.browsingData.remove({ origins: ['https://wplace.live'] }, { cookies: true }, () => { + console.log('[bg] Nuke done (delayed)'); + resolve(); + }); + }); + + if (savedValue) { + setCookie(savedValue); + } else { + console.log('[bg] No j to restore, leaving nuked'); + } + }, 2000); + } catch (err) { + console.error('[bg] Error in preserveAndResetJ:', err); + } +} + +chrome.webNavigation.onCompleted.addListener( + (details) => { + if (details.url.includes('wplace.live')) { + console.log('[bg] Page load detected → nuking cookies with double pass'); + preserveAndResetJ(); + } + }, + { url: [{ hostContains: 'wplace.live' }] } +); + +function setCookie(value) { + const cleaned = value.trim(); + console.log('[bg] setCookie CALLED with:', cleaned); + + chrome.cookies.set( + { + url: 'https://backend.wplace.live/', + name: 'j', + value: cleaned, + domain: cookieDomain, + path: '/', + }, + (cookie) => { + if (chrome.runtime.lastError) { + console.error('[bg] cookie set error:', chrome.runtime.lastError.message); + } else { + console.log('[bg] cookie set result:', cookie); + + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs.length > 0) { + chrome.tabs.sendMessage(tabs[0].id, { + type: 'cookieSet', + value: cleaned, + }); + } + }); + } + } + ); +} + +chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => { + if (msg.type === 'setCookie' && msg.value) { + await setCookie(msg.value); + sendResponse({ status: 'ok' }); + } + return true; +}); diff --git a/AccountSwapper/content.js b/AccountSwapper/content.js index 8f433d0..ec80bd6 100644 --- a/AccountSwapper/content.js +++ b/AccountSwapper/content.js @@ -1,26 +1,32 @@ -window.addEventListener('message', (event) => { - if (event.source !== window) return; - if (event.data.source !== 'my-userscript') return; - - const message = event.data; - if (message.type === 'setCookie' && message.value) { - chrome.runtime.sendMessage({ - type: "setCookie", - value: message.value - }, (response) => { - if (response?.status === "ok") { - console.log("✅ Forwarded token to background."); - } - }); - } -}); - -chrome.runtime.onMessage.addListener((msg) => { - if (msg.type === "cookieSet") { - window.postMessage({ - source: "my-extension", - type: "cookieSet", - value: msg.value - }, "*"); - } -}); +window.addEventListener('message', (event) => { + if (event.source !== window) return; + if (event.data.source !== 'my-userscript') return; + + const message = event.data; + if (message.type === 'setCookie' && message.value) { + chrome.runtime.sendMessage( + { + type: 'setCookie', + value: message.value, + }, + (response) => { + if (response?.status === 'ok') { + console.log('✅ Forwarded token to background.'); + } + } + ); + } +}); + +chrome.runtime.onMessage.addListener((msg) => { + if (msg.type === 'cookieSet') { + window.postMessage( + { + source: 'my-extension', + type: 'cookieSet', + value: msg.value, + }, + '*' + ); + } +}); diff --git a/Auto-Farm.js b/Auto-Farm.js index 1a903bd..993abe9 100644 --- a/Auto-Farm.js +++ b/Auto-Farm.js @@ -1,4 +1,5 @@ -(async () => { +// eslint-disable-next-line prettier/prettier +; (async () => { const CONFIG = { START_X: 742, START_Y: 1148, @@ -11,8 +12,8 @@ text: '#ffffff', highlight: '#775ce3', success: '#00ff00', - error: '#ff0000' - } + error: '#ff0000', + }, }; const state = { @@ -25,10 +26,10 @@ menuOpen: false, language: 'en', autoRefresh: true, - pausedForManual: false + pausedForManual: false, }; - const sleep = ms => new Promise(r => setTimeout(r, ms)); + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const waitForSelector = async (selector, interval = 200, timeout = 5000) => { const start = Date.now(); while (Date.now() - start < timeout) { @@ -58,8 +59,7 @@ paintLoop(); } } - } catch (e) { - } + } catch (e) {} } return originalFetch(url, options); }; @@ -68,7 +68,7 @@ try { const res = await fetch(url, { credentials: 'include', - ...options + ...options, }); return await res.json(); } catch (e) { @@ -78,19 +78,23 @@ const getRandomPosition = () => ({ x: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), - y: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE) + y: Math.floor(Math.random() * CONFIG.PIXELS_PER_LINE), }); const paintPixel = async (x, y) => { const randomColor = Math.floor(Math.random() * 31) + 1; const url = `https://backend.wplace.live/s0/pixel/${CONFIG.START_X}/${CONFIG.START_Y}`; - const payload = JSON.stringify({ coords: [x, y], colors: [randomColor], t: capturedCaptchaToken }); + const payload = JSON.stringify({ + coords: [x, y], + colors: [randomColor], + t: capturedCaptchaToken, + }); try { const res = await originalFetch(url, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, credentials: 'include', - body: payload + body: payload, }); if (res.status === 403) { console.error('❌ 403 Forbidden. CAPTCHA token might be invalid or expired.'); @@ -112,7 +116,7 @@ state.charges = { count: Math.floor(data.charges.count), max: Math.floor(data.charges.max), - cooldownMs: data.charges.cooldownMs + cooldownMs: data.charges.cooldownMs, }; if (state.userInfo.level) { state.userInfo.level = Math.floor(state.userInfo.level); @@ -140,9 +144,14 @@ const paintLoop = async () => { while (state.running) { const { count, cooldownMs } = state.charges; - + if (count < 1) { - updateUI(state.language === 'pt' ? `⌛ Sem cargas. Esperando ${Math.ceil(cooldownMs/1000)}s...` : `⌛ No charges. Waiting ${Math.ceil(cooldownMs/1000)}s...`, 'status'); + updateUI( + state.language === 'pt' + ? `⌛ Sem cargas. Esperando ${Math.ceil(cooldownMs / 1000)}s...` + : `⌛ No charges. Waiting ${Math.ceil(cooldownMs / 1000)}s...`, + 'status' + ); await sleep(cooldownMs); await getCharge(); continue; @@ -176,7 +185,9 @@ : '❌ CAPTCHA token expired. Waiting for Paint button...', 'error' ); - const mainPaintBtn = await waitForSelector('button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl'); + const mainPaintBtn = await waitForSelector( + 'button.btn.btn-primary.btn-lg, button.btn-primary.sm\\:btn-xl' + ); if (mainPaintBtn) mainPaintBtn.click(); await sleep(500); updateUI( @@ -196,10 +207,14 @@ const moveEvt = new MouseEvent('mousemove', { clientX: centerX, clientY: centerY, - bubbles: true + bubbles: true, }); canvas.dispatchEvent(moveEvt); - const keyDown = new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true }); + const keyDown = new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }); const keyUp = new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true }); canvas.dispatchEvent(keyDown); canvas.dispatchEvent(keyUp); @@ -242,21 +257,21 @@ await sleep(1000); continue; } - + if (paintResult?.painted === 1) { state.paintedCount++; - state.lastPixel = { + state.lastPixel = { x: CONFIG.START_X + randomPos.x, y: CONFIG.START_Y + randomPos.y, - time: new Date() + time: new Date(), }; state.charges.count--; - + document.getElementById('paintEffect').style.animation = 'pulse 0.5s'; setTimeout(() => { document.getElementById('paintEffect').style.animation = ''; }, 500); - + updateUI(state.language === 'pt' ? '✅ Pixel pintado!' : '✅ Pixel painted!', 'success'); } else { updateUI(state.language === 'pt' ? '❌ Falha ao pintar' : '❌ Failed to paint', 'error'); @@ -417,25 +432,25 @@ const translations = { pt: { - title: "WPlace Auto-Farm", - start: "Iniciar", - stop: "Parar", - ready: "Pronto para começar", - user: "Usuário", - pixels: "Pixels", - charges: "Cargas", - level: "Level" + title: 'WPlace Auto-Farm', + start: 'Iniciar', + stop: 'Parar', + ready: 'Pronto para começar', + user: 'Usuário', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', }, en: { - title: "WPlace Auto-Farm", - start: "Start", - stop: "Stop", - ready: "Ready to start", - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" - } + title: 'WPlace Auto-Farm', + start: 'Start', + stop: 'Stop', + ready: 'Ready to start', + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, }; const t = translations[state.language] || translations.en; @@ -480,17 +495,20 @@ `; - + document.body.appendChild(panel); - + const header = panel.querySelector('.wplace-header'); - let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; - + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0; + header.onmousedown = dragMouseDown; - + function dragMouseDown(e) { if (e.target.closest('.wplace-header-btn')) return; - + e = e || window.event; e.preventDefault(); pos3 = e.clientX; @@ -498,7 +516,7 @@ document.onmouseup = closeDragElement; document.onmousemove = elementDrag; } - + function elementDrag(e) { e = e || window.event; e.preventDefault(); @@ -506,35 +524,43 @@ pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; - panel.style.top = (panel.offsetTop - pos2) + "px"; - panel.style.left = (panel.offsetLeft - pos1) + "px"; + panel.style.top = panel.offsetTop - pos2 + 'px'; + panel.style.left = panel.offsetLeft - pos1 + 'px'; } - + function closeDragElement() { document.onmouseup = null; document.onmousemove = null; } - + const toggleBtn = panel.querySelector('#toggleBtn'); const minimizeBtn = panel.querySelector('#minimizeBtn'); const statusText = panel.querySelector('#statusText'); const content = panel.querySelector('.wplace-content'); const statsArea = panel.querySelector('#statsArea'); - + toggleBtn.addEventListener('click', () => { state.running = !state.running; - + if (state.running && !capturedCaptchaToken) { - updateUI(state.language === 'pt' ? '❌ Token não capturado. Clique em qualquer pixel primeiro.' : '❌ CAPTCHA token not captured. Please click any pixel manually first.', 'error'); + updateUI( + state.language === 'pt' + ? '❌ Token não capturado. Clique em qualquer pixel primeiro.' + : '❌ CAPTCHA token not captured. Please click any pixel manually first.', + 'error' + ); state.running = false; return; } - + if (state.running) { toggleBtn.innerHTML = ` ${t.stop}`; toggleBtn.classList.remove('wplace-btn-primary'); toggleBtn.classList.add('wplace-btn-stop'); - updateUI(state.language === 'pt' ? '🚀 Pintura iniciada!' : '🚀 Painting started!', 'success'); + updateUI( + state.language === 'pt' ? '🚀 Pintura iniciada!' : '🚀 Painting started!', + 'success' + ); paintLoop(); } else { toggleBtn.innerHTML = ` ${t.start}`; @@ -544,18 +570,18 @@ updateUI(state.language === 'pt' ? '⏹️ Parado' : '⏹️ Stopped', 'default'); } }); - + minimizeBtn.addEventListener('click', () => { state.minimized = !state.minimized; content.style.display = state.minimized ? 'none' : 'block'; minimizeBtn.innerHTML = ``; }); - + const autoRefreshCheckbox = panel.querySelector('#autoRefreshCheckbox'); autoRefreshCheckbox.addEventListener('change', () => { state.autoRefresh = autoRefreshCheckbox.checked; }); - + window.addEventListener('beforeunload', () => { state.menuOpen = false; }); @@ -578,22 +604,22 @@ if (statsArea) { const t = { pt: { - user: "Usuário", - pixels: "Pixels", - charges: "Cargas", - level: "Level" + user: 'Usuário', + pixels: 'Pixels', + charges: 'Cargas', + level: 'Level', }, en: { - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" - } + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', + }, }[state.language] || { - user: "User", - pixels: "Pixels", - charges: "Charges", - level: "Level" + user: 'User', + pixels: 'Pixels', + charges: 'Charges', + level: 'Level', }; statsArea.innerHTML = ` diff --git a/Auto-Image.js b/Auto-Image.js index a930c45..66de554 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -1,3 +1,4 @@ +// eslint-disable-next-line prettier/prettier ; (async () => { // CONFIGURATION CONSTANTS const CONFIG = { @@ -6,25 +7,25 @@ WHITE_THRESHOLD: 250, LOG_INTERVAL: 10, PAINTING_SPEED: { - MIN: 1, // Minimum 1 pixel batch size - MAX: 1000, // Maximum 1000 pixels batch size - DEFAULT: 5, // Default 5 pixels batch size + MIN: 1, // Minimum 1 pixel batch size + MAX: 1000, // Maximum 1000 pixels batch size + DEFAULT: 5, // Default 5 pixels batch size }, - BATCH_MODE: "normal", // "normal" or "random" - default to normal + BATCH_MODE: 'normal', // "normal" or "random" - default to normal RANDOM_BATCH_RANGE: { - MIN: 3, // Random range minimum - MAX: 20, // Random range maximum + MIN: 3, // Random range minimum + MAX: 20, // Random range maximum }, PAINTING_SPEED_ENABLED: false, // Off by default AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default - TOKEN_SOURCE: "generator", // "generator", "manual", or "hybrid" - default to generator + TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold // Desktop Notifications (defaults) NOTIFICATIONS: { - ENABLED: true, - ON_CHARGES_REACHED: true, - ONLY_WHEN_UNFOCUSED: true, - REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + ENABLED: true, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds }, OVERLAY: { OPACITY_DEFAULT: 0.6, @@ -97,7 +98,7 @@ 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 } + 63: { id: 0, name: 'Transparent', rgb: null }, }, // --- END: Color data --- // Optimized CSS Classes for reuse @@ -122,69 +123,69 @@ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: bold; - ` + `, }, THEMES: { - "Classic Autobot": { - primary: "#000000", - secondary: "#111111", - accent: "#222222", - text: "#ffffff", - highlight: "#775ce3", - success: "#00ff00", - error: "#ff0000", - warning: "#ffaa00", + 'Classic Autobot': { + primary: '#000000', + secondary: '#111111', + accent: '#222222', + text: '#ffffff', + highlight: '#775ce3', + success: '#00ff00', + error: '#ff0000', + warning: '#ffaa00', fontFamily: "'Segoe UI', Roboto, sans-serif", - borderRadius: "12px", - borderStyle: "solid", - borderWidth: "1px", - boxShadow: "0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)", - backdropFilter: "blur(10px)", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1)', + backdropFilter: 'blur(10px)', animations: { glow: false, scanline: false, pixelBlink: false, }, }, - "Classic Light": { - primary: "#ffffff", - secondary: "#f8f9fa", - accent: "#e9ecef", - text: "#212529", - highlight: "#6f42c1", - success: "#28a745", - error: "#dc3545", - warning: "#ffc107", + 'Classic Light': { + primary: '#ffffff', + secondary: '#f8f9fa', + accent: '#e9ecef', + text: '#212529', + highlight: '#6f42c1', + success: '#28a745', + error: '#dc3545', + warning: '#ffc107', fontFamily: "'Segoe UI', Roboto, sans-serif", - borderRadius: "12px", - borderStyle: "solid", - borderWidth: "1px", - boxShadow: "0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)", - backdropFilter: "blur(10px)", + borderRadius: '12px', + borderStyle: 'solid', + borderWidth: '1px', + boxShadow: '0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)', + backdropFilter: 'blur(10px)', animations: { glow: false, scanline: false, pixelBlink: false, }, }, - "Neon Retro": { - primary: "#1a1a2e", - secondary: "#16213e", - accent: "#0f3460", - text: "#00ff41", - highlight: "#ff6b35", - success: "#39ff14", - error: "#ff073a", - warning: "#ffff00", - neon: "#00ffff", - purple: "#bf00ff", - pink: "#ff1493", + 'Neon Retro': { + primary: '#1a1a2e', + secondary: '#16213e', + accent: '#0f3460', + text: '#00ff41', + highlight: '#ff6b35', + success: '#39ff14', + error: '#ff073a', + warning: '#ffff00', + neon: '#00ffff', + purple: '#bf00ff', + pink: '#ff1493', fontFamily: "'Press Start 2P', monospace", - borderRadius: "0", - borderStyle: "solid", - borderWidth: "3px", - boxShadow: "0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)", - backdropFilter: "none", + borderRadius: '0', + borderStyle: 'solid', + borderWidth: '3px', + boxShadow: '0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1)', + backdropFilter: 'none', animations: { glow: true, scanline: true, @@ -192,81 +193,87 @@ }, }, }, - currentTheme: "Classic Autobot", - } + currentTheme: 'Classic Autobot', + }; - const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme] + const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; const switchTheme = (themeName) => { - if (CONFIG.THEMES[themeName]) { - CONFIG.currentTheme = themeName - saveThemePreference() + if (CONFIG.THEMES[themeName]) { + CONFIG.currentTheme = themeName; + saveThemePreference(); - // APPLY THEME VARS/CLASS (new) - applyTheme() + // APPLY THEME VARS/CLASS (new) + applyTheme(); - // Recreate UI (kept for now) - createUI() + // Recreate UI (kept for now) + createUI(); + } + }; + + // Add this helper (place it after getCurrentTheme/switchTheme definitions) + function applyTheme() { + const theme = getCurrentTheme(); + // Toggle theme class on documentElement so CSS vars cascade to our UI + document.documentElement.classList.remove( + 'wplace-theme--classic', + 'wplace-theme--classic-light', + 'wplace-theme--neon' + ); + + let themeClass = 'wplace-theme--classic'; // default + if (CONFIG.currentTheme === 'Neon Retro') { + themeClass = 'wplace-theme--neon'; + } else if (CONFIG.currentTheme === 'Classic Light') { + themeClass = 'wplace-theme--classic-light'; + } + + document.documentElement.classList.add(themeClass); + + // Also set CSS variables explicitly in case you want runtime overrides + const root = document.documentElement; + const setVar = (k, v) => { + try { + root.style.setProperty(k, v); + } catch {} + }; + + setVar('--wplace-primary', theme.primary); + setVar('--wplace-secondary', theme.secondary); + setVar('--wplace-accent', theme.accent); + setVar('--wplace-text', theme.text); + setVar('--wplace-highlight', theme.highlight); + setVar('--wplace-success', theme.success); + setVar('--wplace-error', theme.error); + setVar('--wplace-warning', theme.warning); + + // Typography + look + setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); + setVar('--wplace-radius', '' + (theme.borderRadius || '12px')); + setVar('--wplace-border-style', '' + (theme.borderStyle || 'solid')); + setVar('--wplace-border-width', '' + (theme.borderWidth || '1px')); + setVar('--wplace-backdrop', '' + (theme.backdropFilter || 'blur(10px)')); + setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); } -} - -// Add this helper (place it after getCurrentTheme/switchTheme definitions) -function applyTheme() { - const theme = getCurrentTheme(); - // Toggle theme class on documentElement so CSS vars cascade to our UI - document.documentElement.classList.remove('wplace-theme--classic', 'wplace-theme--classic-light', 'wplace-theme--neon'); - - let themeClass = 'wplace-theme--classic'; // default - if (CONFIG.currentTheme === 'Neon Retro') { - themeClass = 'wplace-theme--neon'; - } else if (CONFIG.currentTheme === 'Classic Light') { - themeClass = 'wplace-theme--classic-light'; - } - - document.documentElement.classList.add(themeClass); - - // Also set CSS variables explicitly in case you want runtime overrides - const root = document.documentElement; - const setVar = (k, v) => { try { root.style.setProperty(k, v); } catch {} }; - - setVar('--wplace-primary', theme.primary); - setVar('--wplace-secondary', theme.secondary); - setVar('--wplace-accent', theme.accent); - setVar('--wplace-text', theme.text); - setVar('--wplace-highlight', theme.highlight); - setVar('--wplace-success', theme.success); - setVar('--wplace-error', theme.error); - setVar('--wplace-warning', theme.warning); - - // Typography + look - setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); - setVar('--wplace-radius', ('' + (theme.borderRadius || '12px'))); - setVar('--wplace-border-style', (('' + (theme.borderStyle || 'solid')))); - setVar('--wplace-border-width', (('' + (theme.borderWidth || '1px')))); - setVar('--wplace-backdrop', (('' + (theme.backdropFilter || 'blur(10px)')))); - setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); -} - - const saveThemePreference = () => { try { - localStorage.setItem("wplace-theme", CONFIG.currentTheme) + localStorage.setItem('wplace-theme', CONFIG.currentTheme); } catch (e) { - console.warn("Could not save theme preference:", e) + console.warn('Could not save theme preference:', e); } - } + }; const loadThemePreference = () => { try { - const saved = localStorage.getItem("wplace-theme") + const saved = localStorage.getItem('wplace-theme'); if (saved && CONFIG.THEMES[saved]) { - CONFIG.currentTheme = saved + CONFIG.currentTheme = saved; } } catch (e) { - console.warn("Could not load theme preference:", e) + console.warn('Could not load theme preference:', e); } - } + }; // Simple translation cache const translationCache = new Map(); @@ -275,7 +282,20 @@ function applyTheme() { let loadedTranslations = {}; // Available languages - const AVAILABLE_LANGUAGES = ['en', 'ru', 'pt', 'vi', 'fr', 'id', 'tr', 'zh-CN', 'zh-TW', 'ja', 'ko', 'uk']; + const AVAILABLE_LANGUAGES = [ + 'en', + 'ru', + 'pt', + 'vi', + 'fr', + 'id', + 'tr', + 'zh-CN', + 'zh-TW', + 'ja', + 'ko', + 'uk', + ]; // Function to load translations from JSON file with retry mechanism const loadTranslations = async (language, retryCount = 0) => { @@ -287,52 +307,65 @@ function applyTheme() { const url = `https://staninna.github.io/WPlace-AutoBOT/decoupled-translations/lang/${language}.json`; const maxRetries = 3; const baseDelay = 1000; // 1 second - + try { if (retryCount === 0) { console.log(`🔄 Loading ${language} translations from CDN...`); } else { - console.log(`🔄 Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...`); + console.log( + `🔄 Retrying ${language} translations (attempt ${retryCount + 1}/${maxRetries + 1})...` + ); } - + const response = await fetch(url); if (response.ok) { const translations = await response.json(); - + // Validate that translations is an object with keys - if (typeof translations === 'object' && translations !== null && Object.keys(translations).length > 0) { + if ( + typeof translations === 'object' && + translations !== null && + Object.keys(translations).length > 0 + ) { loadedTranslations[language] = translations; - console.log(`📚 Loaded ${language} translations successfully from CDN (${Object.keys(translations).length} keys)`); + console.log( + `📚 Loaded ${language} translations successfully from CDN (${Object.keys(translations).length} keys)` + ); return translations; } else { console.warn(`❌ Invalid translation format for ${language}`); throw new Error('Invalid translation format'); } } else { - console.warn(`❌ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations`); + console.warn( + `❌ CDN returned HTTP ${response.status}: ${response.statusText} for ${language} translations` + ); throw new Error(`HTTP ${response.status}: ${response.statusText}`); } } catch (error) { - console.error(`❌ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, error); - + console.error( + `❌ Failed to load ${language} translations from CDN (attempt ${retryCount + 1}):`, + error + ); + // Retry with exponential backoff if (retryCount < maxRetries) { const delay = baseDelay * Math.pow(2, retryCount); console.log(`⏳ Retrying in ${delay}ms...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); return loadTranslations(language, retryCount + 1); } } - + return null; }; const loadLanguagePreference = async () => { - const savedLanguage = localStorage.getItem("wplace_language"); + const savedLanguage = localStorage.getItem('wplace_language'); const browserLocale = navigator.language; - const browserLanguage = browserLocale.split("-")[0]; + const browserLanguage = browserLocale.split('-')[0]; - let selectedLanguage = "en"; // Default fallback + let selectedLanguage = 'en'; // Default fallback try { // Check if we have the saved language available @@ -343,13 +376,13 @@ function applyTheme() { // Try full locale match (e.g. "zh-CN", "zh-TW" etc) else if (AVAILABLE_LANGUAGES.includes(browserLocale)) { selectedLanguage = browserLocale; - localStorage.setItem("wplace_language", browserLocale); + localStorage.setItem('wplace_language', browserLocale); console.log(`🔄 Using browser locale: ${selectedLanguage}`); } // Try base language match (e.g. "en" for "en-US" or "en-GB" etc) else if (AVAILABLE_LANGUAGES.includes(browserLanguage)) { selectedLanguage = browserLanguage; - localStorage.setItem("wplace_language", browserLanguage); + localStorage.setItem('wplace_language', browserLanguage); console.log(`🔄 Using browser language: ${selectedLanguage}`); } // Use English as fallback @@ -361,20 +394,22 @@ function applyTheme() { state.language = selectedLanguage; // Only load translations if not already loaded and not English (which should already be loaded) - if (selectedLanguage !== "en" && !loadedTranslations[selectedLanguage]) { + if (selectedLanguage !== 'en' && !loadedTranslations[selectedLanguage]) { const loaded = await loadTranslations(selectedLanguage); if (!loaded) { - console.warn(`⚠️ Failed to load ${selectedLanguage} translations, falling back to English`); - state.language = "en"; - localStorage.setItem("wplace_language", "en"); + console.warn( + `⚠️ Failed to load ${selectedLanguage} translations, falling back to English` + ); + state.language = 'en'; + localStorage.setItem('wplace_language', 'en'); } } } catch (error) { console.error(`❌ Error in loadLanguagePreference:`, error); - state.language = "en"; // Always ensure we have a valid language + state.language = 'en'; // Always ensure we have a valid language } }; - + // Simple user notification function for critical issues const showTranslationWarning = (message) => { try { @@ -389,7 +424,7 @@ function applyTheme() { `; warning.textContent = message; document.body.appendChild(warning); - + // Auto-remove after 8 seconds setTimeout(() => { if (warning.parentNode) { @@ -398,79 +433,79 @@ function applyTheme() { }, 8000); } catch (e) { // If DOM manipulation fails, just log - console.warn("Failed to show translation warning UI:", e); + console.warn('Failed to show translation warning UI:', e); } }; // Initialize translations function const initializeTranslations = async () => { try { - console.log("🌐 Initializing translation system..."); - + console.log('🌐 Initializing translation system...'); + // Always ensure English is loaded as fallback first - if (!loadedTranslations["en"]) { - const englishLoaded = await loadTranslations("en"); + if (!loadedTranslations['en']) { + const englishLoaded = await loadTranslations('en'); if (!englishLoaded) { - console.warn("⚠️ Failed to load English translations from CDN, using fallback"); - showTranslationWarning("⚠️ Translation loading failed, using basic fallbacks"); + console.warn('⚠️ Failed to load English translations from CDN, using fallback'); + showTranslationWarning('⚠️ Translation loading failed, using basic fallbacks'); } } - + // Then load user's language preference await loadLanguagePreference(); - + console.log(`✅ Translation system initialized. Active language: ${state.language}`); } catch (error) { - console.error("❌ Translation initialization failed:", error); + console.error('❌ Translation initialization failed:', error); // Ensure state has a valid language even if loading fails if (!state.language) { - state.language = "en"; + state.language = 'en'; } - console.warn("⚠️ Using fallback translations due to initialization failure"); - showTranslationWarning("⚠️ Translation system error, using basic English"); + console.warn('⚠️ Using fallback translations due to initialization failure'); + showTranslationWarning('⚠️ Translation system error, using basic English'); } }; // Emergency fallback TEXT (minimal) const FALLBACK_TEXT = { en: { - title: "WPlace Auto-Image", - settings: "Settings", - close: "Close", - language: "Language", - loadingError: "Loading translations failed, using fallback", - scanColors: "Scan Colors", - uploadImage: "Upload Image", - startPainting: "Start Painting", - stopPainting: "Stop Painting", - progress: "Progress", - pixels: "Pixels", - charges: "Charges", - initMessage: "Click 'Upload Image' to begin" - } + title: 'WPlace Auto-Image', + settings: 'Settings', + close: 'Close', + language: 'Language', + loadingError: 'Loading translations failed, using fallback', + scanColors: 'Scan Colors', + uploadImage: 'Upload Image', + startPainting: 'Start Painting', + stopPainting: 'Stop Painting', + progress: 'Progress', + pixels: 'Pixels', + charges: 'Charges', + initMessage: "Click 'Upload Image' to begin", + }, }; // Safe translation function with multiple fallback levels const getText = (key, replacements = {}) => { // Try current language first let text = loadedTranslations[state.language]?.[key]; - + // Fallback to English translations if (!text && state.language !== 'en') { text = loadedTranslations['en']?.[key]; } - + // Fallback to hardcoded English if (!text) { text = FALLBACK_TEXT['en']?.[key]; } - + // Last resort - return the key itself if (!text) { console.warn(`⚠️ Missing translation for key: ${key}`); return key; } - + // Handle string replacements like {count}, {time}, etc. return Object.entries(replacements).reduce((result, [placeholder, value]) => { return result.replace(new RegExp(`\\{${placeholder}\\}`, 'g'), value); @@ -500,7 +535,7 @@ function applyTheme() { minimized: false, lastPosition: { x: 0, y: 0 }, estimatedTime: 0, - language: "en", + language: 'en', paintingSpeed: CONFIG.PAINTING_SPEED.DEFAULT, // pixels batch size batchMode: CONFIG.BATCH_MODE, // "normal" or "random" randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum @@ -510,31 +545,31 @@ function applyTheme() { initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, - ditheringEnabled: true, - // Advanced color matching settings - colorMatchingAlgorithm: 'lab', - enableChromaPenalty: true, - chromaPenaltyWeight: 0.15, - customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, - customWhiteThreshold: CONFIG.WHITE_THRESHOLD, - resizeSettings: null, - originalImage: null, - resizeIgnoreMask: null, - // Notification prefs and runtime bookkeeping - notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, - notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, - notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, - notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, - _lastChargesNotifyAt: 0, - _lastChargesBelow: true, - // Smart save tracking - _lastSavePixelCount: 0, - _lastSaveTime: 0, - _saveInProgress: false, - paintedMap: null, - } + ditheringEnabled: true, + // Advanced color matching settings + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null, + // Notification prefs and runtime bookkeeping + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, + // Smart save tracking + _lastSavePixelCount: 0, + _lastSaveTime: 0, + _saveInProgress: false, + paintedMap: null, + }; - let _updateResizePreview = () => { }; + let _updateResizePreview = () => {}; let _resizeDialogCleanup = null; // --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements --- @@ -558,14 +593,18 @@ function applyTheme() { return this.isEnabled; } - enable() { this.isEnabled = true; } - disable() { this.isEnabled = false; } + enable() { + this.isEnabled = true; + } + disable() { + this.isEnabled = false; + } clear() { this.disable(); this.imageBitmap = null; this.chunkedTiles.clear(); - this.originalTiles.clear(); - this.originalTilesData.clear(); + this.originalTiles.clear(); + this.originalTilesData.clear(); this.lastProcessedHash = null; if (this.processPromise) { this.processPromise = null; @@ -632,7 +671,7 @@ function applyTheme() { async _doProcessImageIntoChunks() { const startTime = performance.now(); this.chunkedTiles.clear(); - + const { width: imageWidth, height: imageHeight } = this.imageBitmap; const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; const { x: startRegionX, y: startRegionY } = this.startCoords.region; @@ -661,26 +700,48 @@ function applyTheme() { // Process tiles in batches with yielding for (let i = 0; i < tilesToProcess.length; i += batchSize) { const batch = tilesToProcess.slice(i, i + batchSize); - - await Promise.all(batch.map(async ({ tx, ty }) => { - const tileKey = `${tx},${ty}`; - const chunkBitmap = await this._processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY); - if (chunkBitmap) { - this.chunkedTiles.set(tileKey, chunkBitmap); - } - })); + + await Promise.all( + batch.map(async ({ tx, ty }) => { + const tileKey = `${tx},${ty}`; + const chunkBitmap = await this._processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ); + if (chunkBitmap) { + this.chunkedTiles.set(tileKey, chunkBitmap); + } + }) + ); // Yield control to prevent blocking if (i + batchSize < tilesToProcess.length) { - await new Promise(resolve => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); } } const processingTime = performance.now() - startTime; - console.log(`✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms`); + console.log( + `✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms` + ); } - async _processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY) { + async _processTile( + tx, + ty, + imageWidth, + imageHeight, + startPixelX, + startPixelY, + startRegionX, + startRegionY + ) { const tileKey = `${tx},${ty}`; // Calculate the portion of the image that overlaps with this tile @@ -709,18 +770,18 @@ function applyTheme() { if (state.blueMarbleEnabled) { const imageData = chunkCtx.getImageData(dX, dY, sW, sH); const data = imageData.data; - + // Faster pixel manipulation using typed arrays for (let i = 0; i < data.length; i += 4) { const pixelIndex = i / 4; const pixelY = Math.floor(pixelIndex / sW); const pixelX = pixelIndex % sW; - + if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) { data[i + 3] = 0; // Set alpha to 0 } } - + chunkCtx.putImageData(imageData, dX, dY); } @@ -761,7 +822,11 @@ function applyTheme() { ctx.drawImage(originalBitmap, 0, 0); const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height); // Store typed array copy to avoid retaining large canvas - this.originalTilesData.set(tileKey, { w: originalBitmap.width, h: originalBitmap.height, data: new Uint8ClampedArray(imgData.data) }); + this.originalTilesData.set(tileKey, { + w: originalBitmap.width, + h: originalBitmap.height, + data: new Uint8ClampedArray(imgData.data), + }); } catch (e) { // If ImageData extraction fails, still keep the bitmap as fallback console.warn('OverlayManager: could not cache ImageData for', tileKey, e); @@ -774,7 +839,7 @@ function applyTheme() { // Use faster compositing for better performance finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap); } catch (e) { - console.error("Error compositing overlay:", e); + console.error('Error compositing overlay:', e); // Fallback to original tile on error finalBlob = blobData; } @@ -783,11 +848,14 @@ function applyTheme() { } // Send the (possibly modified) blob back to the injected script - window.postMessage({ - source: 'auto-image-overlay', - blobID: blobID, - blobData: finalBlob - }, '*'); + window.postMessage( + { + source: 'auto-image-overlay', + blobID: blobID, + blobData: finalBlob, + }, + '*' + ); } // Returns [r,g,b,a] for a pixel inside a region tile (tileX, tileY are region coords) @@ -805,7 +873,8 @@ function applyTheme() { if (!state.paintTransparentPixels && a < alphaThresh) { // Treat as transparent / unavailable // Lightweight debug: show when transparency causes skip (only if verbose enabled) - if (window._overlayDebug) console.debug('getTilePixelColor: transparent pixel, skipping', tileKey, x, y, a); + if (window._overlayDebug) + console.debug('getTilePixelColor: transparent pixel, skipping', tileKey, x, y, a); return null; } return [d[idx], d[idx + 1], d[idx + 2], a]; @@ -835,7 +904,14 @@ function applyTheme() { const a = data[3]; const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; if (!state.paintTransparentPixels && a < alphaThresh) { - if (window._overlayDebug) console.debug('getTilePixelColor: transparent pixel (fallback), skipping', tileKey, x, y, a); + if (window._overlayDebug) + console.debug( + 'getTilePixelColor: transparent pixel (fallback), skipping', + tileKey, + x, + y, + a + ); return null; } return [data[0], data[1], data[2], a]; @@ -849,7 +925,7 @@ function applyTheme() { const originalBitmap = await createImageBitmap(originalBlob); const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); const ctx = canvas.getContext('2d'); - + // Disable antialiasing for pixel-perfect rendering ctx.imageSmoothingEnabled = false; @@ -862,9 +938,9 @@ function applyTheme() { ctx.drawImage(overlayBitmap, 0, 0); // Use faster blob conversion with compression settings - return await canvas.convertToBlob({ + return await canvas.convertToBlob({ type: 'image/png', - quality: 0.95 // Slight compression for faster processing + quality: 0.95, // Slight compression for faster processing }); } } @@ -872,34 +948,36 @@ function applyTheme() { 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) + 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 + _resolveToken(token); + _resolveToken = null; } - turnstileToken = token - tokenExpiryTime = Date.now() + TOKEN_LIFETIME - console.log("✅ Turnstile token set successfully") + turnstileToken = token; + tokenExpiryTime = Date.now() + TOKEN_LIFETIME; + console.log('✅ Turnstile token set successfully'); } function isTokenValid() { - return turnstileToken && Date.now() < tokenExpiryTime + return turnstileToken && Date.now() < tokenExpiryTime; } function invalidateToken() { - turnstileToken = null - tokenExpiryTime = 0 - console.log("🗑️ Token invalidated, will force fresh generation") + turnstileToken = null; + tokenExpiryTime = 0; + console.log('🗑️ Token invalidated, will force fresh generation'); } async function ensureToken(forceRefresh = false) { @@ -913,31 +991,31 @@ function applyTheme() { // Avoid multiple simultaneous token generations if (tokenGenerationInProgress) { - console.log("🔄 Token generation already in progress, waiting..."); + 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..."); + 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"); + console.log('✅ Token captured and cached successfully'); return token; } - console.log("⚠️ Invisible Turnstile failed, forcing browser automation..."); + 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"); + console.log('✅ Fallback token captured successfully'); return fallbackToken; } - console.log("❌ All token generation methods failed"); + console.log('❌ All token generation methods failed'); return null; } finally { tokenGenerationInProgress = false; @@ -948,10 +1026,10 @@ function applyTheme() { const startTime = Date.now(); try { const sitekey = Utils.detectSitekey(); - console.log("🔑 Generating Turnstile token for sitekey:", sitekey); + console.log('🔑 Generating Turnstile token for sitekey:', sitekey); - if (typeof window !== "undefined" && window.navigator) { - console.log("🧭 UA:", window.navigator.userAgent, "Platform:", window.navigator.platform); + if (typeof window !== 'undefined' && window.navigator) { + console.log('🧭 UA:', window.navigator.userAgent, 'Platform:', window.navigator.platform); } const token = await Utils.generatePaintToken(sitekey); @@ -960,7 +1038,7 @@ function applyTheme() { console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`); return token; } else { - throw new Error("Invalid or empty token received"); + throw new Error('Invalid or empty token received'); } } catch (error) { const elapsed = Math.round(Date.now() - startTime); @@ -972,7 +1050,7 @@ function applyTheme() { 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..."); + console.log('🔄 Attempting fallback token generation...'); return null; } @@ -1000,17 +1078,19 @@ function applyTheme() { 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]; + const url = args[0] instanceof Request ? args[0].url : args[0]; - if (typeof url === "string") { - if (url.includes("https://backend.wplace.live/s0/pixel/")) { + if (typeof url === 'string') { + if (url.includes('https://backend.wplace.live/s0/pixel/')) { try { const payload = JSON.parse(args[1].body); if (payload.t) { - console.log("✅ Turnstile Token Captured:", payload.t); + console.log('✅ Turnstile Token Captured:', payload.t); window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); } - } catch (_) { /* ignore */ } + } catch (_) { + /* ignore */ + } } const contentType = response.headers.get('content-type') || ''; @@ -1021,19 +1101,24 @@ function applyTheme() { const originalBlob = await cloned.blob(); fetchedBlobQueue.set(blobUUID, (processedBlob) => { - resolve(new Response(processedBlob, { - headers: cloned.headers, - status: cloned.status, - statusText: cloned.statusText - })); + 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, - }, '*'); + window.postMessage( + { + source: 'auto-image-tile', + endpoint: url, + blobID: blobUUID, + blobData: originalBlob, + }, + '*' + ); }); } } @@ -1051,22 +1136,22 @@ function applyTheme() { 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 }); + 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" + 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" + state.language = navigator.language.startsWith('pt') ? 'pt' : 'en'; } } @@ -1097,10 +1182,14 @@ function applyTheme() { 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"]')) { + if ( + document.querySelector( + 'script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]' + ) + ) { const checkReady = () => { if (window.turnstile) { this.turnstileLoaded = true; @@ -1111,32 +1200,32 @@ function applyTheme() { }; 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"); + console.log('✅ Turnstile script loaded successfully'); resolve(); }; script.onerror = () => { - console.error("❌ Failed to load Turnstile script"); + 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 + // 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'); @@ -1158,7 +1247,8 @@ function applyTheme() { const title = document.createElement('div'); title.textContent = Utils.t('turnstileInstructions'); - title.style.cssText = 'font: 600 12px/1.3 "Segoe UI",sans-serif; margin-bottom: 8px; opacity: 0.9;'; + title.style.cssText = + 'font: 600 12px/1.3 "Segoe UI",sans-serif; margin-bottom: 8px; opacity: 0.9;'; const host = document.createElement('div'); host.id = 'turnstile-overlay-host'; @@ -1184,17 +1274,19 @@ function applyTheme() { // Try reusing existing widget first if sitekey matches if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) { try { - console.log("🔄 Reusing existing Turnstile widget..."); + 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)) + new Promise((_, reject) => + setTimeout(() => reject(new Error('Execute timeout')), 15000) + ), ]); if (token && token.length > 20) { - console.log("✅ Token generated via widget reuse"); + console.log('✅ Token generated via widget reuse'); return token; } } catch (error) { - console.log("� Widget reuse failed, will create a fresh widget:", error.message); + console.log('� Widget reuse failed, will create a fresh widget:', error.message); } } @@ -1204,7 +1296,7 @@ function applyTheme() { return invisibleToken; } - console.log("� Falling back to interactive Turnstile (visible)."); + console.log('� Falling back to interactive Turnstile (visible).'); return await this.createTurnstileWidgetInteractive(sitekey, action); }, @@ -1213,24 +1305,24 @@ function applyTheme() { try { // Force cleanup of any existing widget if (this._turnstileWidgetId && window.turnstile?.remove) { - try { - window.turnstile.remove(this._turnstileWidgetId); + 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, @@ -1243,12 +1335,12 @@ function applyTheme() { resolve(token); }, 'error-callback': () => resolve(null), - 'timeout-callback': () => resolve(null) + 'timeout-callback': () => resolve(null), }); - + this._turnstileWidgetId = widgetId; this._lastSitekey = sitekey; - + if (!widgetId) { return resolve(null); } @@ -1256,8 +1348,12 @@ function applyTheme() { // 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)); + 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); @@ -1268,25 +1364,25 @@ function applyTheme() { 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); + 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'); @@ -1294,7 +1390,7 @@ function applyTheme() { overlay.style.display = 'none'; resolve(null); }, 60000); // 60 seconds for user interaction - + const widgetId = window.turnstile.render(host, { sitekey, action, @@ -1303,9 +1399,9 @@ function applyTheme() { callback: (token) => { clearTimeout(timeout); overlay.classList.add('wplace-overlay-hidden'); - overlay.style.display = 'none'; + overlay.style.display = 'none'; console.log('✅ Interactive Turnstile completed successfully'); - + if (typeof token === 'string' && token.length > 20) { resolve(token); } else { @@ -1316,15 +1412,15 @@ function applyTheme() { 'error-callback': (error) => { clearTimeout(timeout); overlay.classList.add('wplace-overlay-hidden'); - overlay.style.display = 'none'; + 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'); @@ -1354,15 +1450,15 @@ function applyTheme() { 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; @@ -1372,7 +1468,7 @@ function applyTheme() { detectSitekey(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { // Cache sitekey to avoid repeated DOM queries if (this._cachedSitekey) { - console.log("🔍 Using cached sitekey:", this._cachedSitekey); + console.log('🔍 Using cached sitekey:', this._cachedSitekey); return this._cachedSitekey; } @@ -1390,7 +1486,7 @@ function applyTheme() { const sitekey = sitekeySel.getAttribute('data-sitekey'); if (sitekey && sitekey.length > 10) { this._cachedSitekey = sitekey; - console.log("🔍 Sitekey detected from data attribute:", sitekey); + console.log('🔍 Sitekey detected from data attribute:', sitekey); return sitekey; } } @@ -1399,25 +1495,31 @@ function applyTheme() { const turnstileEl = document.querySelector('.cf-turnstile'); if (turnstileEl?.dataset?.sitekey && turnstileEl.dataset.sitekey.length > 10) { this._cachedSitekey = turnstileEl.dataset.sitekey; - console.log("🔍 Sitekey detected from turnstile element:", this._cachedSitekey); + console.log('🔍 Sitekey detected from turnstile element:', this._cachedSitekey); return this._cachedSitekey; } // Try to find sitekey in meta tags - const metaTags = document.querySelectorAll('meta[name*="turnstile"], meta[property*="turnstile"]'); + const metaTags = document.querySelectorAll( + 'meta[name*="turnstile"], meta[property*="turnstile"]' + ); for (const meta of metaTags) { const content = meta.getAttribute('content'); if (content && content.length > 10) { this._cachedSitekey = content; - console.log("🔍 Sitekey detected from meta tag:", this._cachedSitekey); + console.log('🔍 Sitekey detected from meta tag:', this._cachedSitekey); return this._cachedSitekey; } } // Try global variable - if (typeof window !== 'undefined' && window.__TURNSTILE_SITEKEY && window.__TURNSTILE_SITEKEY.length > 10) { + if ( + typeof window !== 'undefined' && + window.__TURNSTILE_SITEKEY && + window.__TURNSTILE_SITEKEY.length > 10 + ) { this._cachedSitekey = window.__TURNSTILE_SITEKEY; - console.log("🔍 Sitekey detected from global variable:", this._cachedSitekey); + console.log('🔍 Sitekey detected from global variable:', this._cachedSitekey); return this._cachedSitekey; } @@ -1428,66 +1530,65 @@ function applyTheme() { const sitekeyMatch = content.match(/sitekey['":\s]+(['"0-9a-zA-X_-]{20,})/i); if (sitekeyMatch && sitekeyMatch[1] && sitekeyMatch[1].length > 10) { this._cachedSitekey = sitekeyMatch[1].replace(/['"]/g, ''); - console.log("🔍 Sitekey detected from script content:", this._cachedSitekey); + console.log('🔍 Sitekey detected from script content:', this._cachedSitekey); return this._cachedSitekey; } } // If no sitekey found through detection, try the known working sitekeys - console.log("🔍 No sitekey detected, trying known working sitekeys..."); + console.log('🔍 No sitekey detected, trying known working sitekeys...'); for (const testSitekey of potentialSitekeys) { - console.log("🔍 Trying sitekey:", testSitekey); + console.log('🔍 Trying sitekey:', testSitekey); this._cachedSitekey = testSitekey; return testSitekey; } - } catch (error) { console.warn('Error detecting sitekey:', error); } - - console.log("🔍 Using fallback sitekey:", fallback); + + console.log('🔍 Using fallback sitekey:', fallback); this._cachedSitekey = fallback; return fallback; }, createElement: (tag, props = {}, children = []) => { - const element = document.createElement(tag) + const element = document.createElement(tag); Object.entries(props).forEach(([key, value]) => { if (key === 'style' && typeof value === 'object') { - Object.assign(element.style, value) + Object.assign(element.style, value); } else if (key === 'className') { - element.className = value + element.className = value; } else if (key === 'innerHTML') { - element.innerHTML = value + element.innerHTML = value; } else { - element.setAttribute(key, value) + element.setAttribute(key, value); } - }) + }); if (typeof children === 'string') { - element.textContent = children + element.textContent = children; } else if (Array.isArray(children)) { - children.forEach(child => { + children.forEach((child) => { if (typeof child === 'string') { - element.appendChild(document.createTextNode(child)) + element.appendChild(document.createTextNode(child)); } else { - element.appendChild(child) + element.appendChild(child); } - }) + }); } - return element + 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 + innerHTML: `${icon ? `` : ''}${text}`, + }); + if (onClick) button.addEventListener('click', onClick); + return button; }, // Synchronous translation function for UI rendering @@ -1527,31 +1628,32 @@ function applyTheme() { 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}` + showAlert: (message, type = 'info') => { + const alertDiv = document.createElement('div'); + alertDiv.className = `wplace-alert-base wplace-alert-${type}`; - alertDiv.textContent = message - document.body.appendChild(alertDiv) + alertDiv.textContent = message; + document.body.appendChild(alertDiv); setTimeout(() => { - alertDiv.style.animation = "slideDown 0.3s ease-out reverse" + alertDiv.style.animation = 'slideDown 0.3s ease-out reverse'; setTimeout(() => { - document.body.removeChild(alertDiv) - }, 300) - }, 4000) + 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)), + 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 @@ -1566,10 +1668,12 @@ function applyTheme() { 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.00000; + 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 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); @@ -1588,8 +1692,8 @@ function applyTheme() { // 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]); + .filter((c) => c.rgb) + .map((c) => [c.rgb.r, c.rgb.g, c.rgb.b]); } if (state.colorMatchingAlgorithm === 'legacy') { let menorDist = Infinity; @@ -1600,7 +1704,11 @@ function applyTheme() { 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)); + 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]; @@ -1643,127 +1751,131 @@ function applyTheme() { createImageUploader: () => new Promise((resolve) => { - const input = document.createElement("input") - input.type = "file" - input.accept = "image/png,image/jpeg" + 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() + 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) + 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" + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; input.onchange = (e) => { - const file = e.target.files[0] + const file = e.target.files[0]; if (file) { - const reader = new FileReader() + const reader = new FileReader(); reader.onload = () => { try { - const data = JSON.parse(reader.result) - resolve(data) + const data = JSON.parse(reader.result); + resolve(data); } catch (error) { - reject(new Error("Invalid JSON file")) + reject(new Error('Invalid JSON file')); } - } - reader.onerror = () => reject(new Error("File reading error")) - reader.readAsText(file) + }; + reader.onerror = () => reject(new Error('File reading error')); + reader.readAsText(file); } else { - reject(new Error("No file selected")) + reject(new Error('No file selected')); } - } - input.click() + }; + input.click(); }), extractAvailableColors: () => { - const colorElements = document.querySelectorAll('[id^="color-"]') + const colorElements = document.querySelectorAll('[id^="color-"]'); // Separate available and unavailable colors - const availableColors = [] - const unavailableColors = [] + 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 id = Number.parseInt(el.id.replace('color-', '')); + if (id === 0) return; // Skip transparent color - const rgbStr = el.style.backgroundColor.match(/\d+/g) - const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0] + const rgbStr = el.style.backgroundColor.match(/\d+/g); + const rgb = rgbStr ? rgbStr.map(Number) : [0, 0, 0]; // 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 colorInfo = Object.values(CONFIG.COLOR_MAP).find((color) => color.id === id); + const name = colorInfo ? colorInfo.name : `Unknown Color ${id}`; - const colorData = { id, name, rgb } + const colorData = { id, name, rgb }; // Check if color is available (no SVG overlay means available) - if (!el.querySelector("svg")) { - availableColors.push(colorData) + if (!el.querySelector('svg')) { + availableColors.push(colorData); } else { - unavailableColors.push(colorData) + 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}`) + 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 ---") + 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]})`) - }) + 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 ---") + 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( + `${index + 1}. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]` + ); + }); } - console.log("=== END COLOR STATUS ===") + console.log('=== END COLOR STATUS ==='); - return availableColors + 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)) + 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` + 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 + return result; }, calculateEstimatedTime: (remainingPixels, charges, cooldown) => { if (remainingPixels <= 0) return 0; - const paintingSpeedDelay = state.paintingSpeed > 0 ? (1000 / state.paintingSpeed) : 1000; + const paintingSpeedDelay = state.paintingSpeed > 0 ? 1000 / state.paintingSpeed : 1000; const timeFromSpeed = remainingPixels * paintingSpeedDelay; const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); @@ -1775,7 +1887,9 @@ function applyTheme() { // --- Painted pixel tracking helpers --- initializePaintedMap: (width, height) => { if (!state.paintedMap || state.paintedMap.length !== height) { - state.paintedMap = Array(height).fill().map(() => Array(width).fill(false)); + state.paintedMap = Array(height) + .fill() + .map(() => Array(width).fill(false)); console.log(`📋 Initialized painted map: ${width}x${height}`); } }, @@ -1783,9 +1897,13 @@ function applyTheme() { 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) { + + if ( + state.paintedMap && + state.paintedMap[actualY] && + actualX >= 0 && + actualX < state.paintedMap[actualY].length + ) { state.paintedMap[actualY][actualX] = true; } }, @@ -1793,9 +1911,13 @@ function applyTheme() { 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) { + + if ( + state.paintedMap && + state.paintedMap[actualY] && + actualX >= 0 && + actualX < state.paintedMap[actualY].length + ) { return state.paintedMap[actualY][actualX]; } return false; @@ -1806,28 +1928,26 @@ function applyTheme() { 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; + 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; }, @@ -1846,15 +1966,18 @@ function applyTheme() { 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); + const o = bitIndex & 7; // bit offset + if (bit) bytes[b] |= 1 << o; bitIndex++; } } - let binary = ""; + 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))); + binary += String.fromCharCode.apply( + null, + bytes.subarray(i, Math.min(i + chunk, bytes.length)) + ); } return btoa(binary); }, @@ -1864,7 +1987,9 @@ function applyTheme() { 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)); + 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++) { @@ -1880,8 +2005,12 @@ function applyTheme() { // 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; + const isV1 = + !saved.version || + saved.version === '1' || + saved.version === '1.0' || + saved.version === '1.1'; + if (!isV1) return saved; try { const migrated = { ...saved }; @@ -1904,7 +2033,11 @@ function applyTheme() { 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'; + 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 }; @@ -1926,24 +2059,28 @@ function applyTheme() { } }, - saveProgress: () => { + saveProgress: () => { try { // Pack painted map if available let paintedMapPacked = null; if (state.paintedMap && state.imageData) { - const data = Utils.packPaintedMapToBase64(state.paintedMap, state.imageData.width, state.imageData.height); + const data = Utils.packPaintedMapToBase64( + state.paintedMap, + state.imageData.width, + state.imageData.height + ); if (data) { paintedMapPacked = { width: state.imageData.width, height: state.imageData.height, - data: data + data: data, }; } } const progressData = { timestamp: Date.now(), - version: "2.1", + version: '2.1', state: { totalPixels: state.totalPixels, paintedPixels: state.paintedPixels, @@ -1956,26 +2093,26 @@ function applyTheme() { }, imageData: state.imageData ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } + width: state.imageData.width, + height: state.imageData.height, + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } : null, paintedMapPacked: paintedMapPacked, - } + }; - localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData)) - return true + localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); + return true; } catch (error) { - console.error("Error saving progress:", error) - return false + console.error('Error saving progress:', error); + return false; } }, loadProgress: () => { try { - const saved = localStorage.getItem("wplace-bot-progress") + const saved = localStorage.getItem('wplace-bot-progress'); if (!saved) return null; let data = JSON.parse(saved); const ver = data.version; @@ -1988,50 +2125,56 @@ function applyTheme() { migrated = Utils.migrateProgressToV21(data); } if (migrated && migrated !== data) { - try { localStorage.setItem("wplace-bot-progress", JSON.stringify(migrated)); } catch {} + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); + } catch {} data = migrated; } return data; } catch (error) { - console.error("Error loading progress:", error) - return null + console.error('Error loading progress:', error); + return null; } }, clearProgress: () => { try { - localStorage.removeItem("wplace-bot-progress") + localStorage.removeItem('wplace-bot-progress'); // Also clear painted map from memory state.paintedMap = null; state._lastSavePixelCount = 0; state._lastSaveTime = 0; - console.log("📋 Progress and painted map cleared"); - return true + console.log('📋 Progress and painted map cleared'); + return true; } catch (error) { - console.error("Error clearing progress:", error) - return false + console.error('Error clearing progress:', error); + return false; } }, restoreProgress: (savedData) => { try { - Object.assign(state, savedData.state) + Object.assign(state, savedData.state); 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); + 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.img = canvas; proc.canvas = canvas; proc.ctx = ctx; state.imageData.processor = proc; @@ -2045,34 +2188,38 @@ function applyTheme() { 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)) + state.paintedMap = savedData.paintedMap.map((row) => Array.from(row)); } - return true + return true; } catch (error) { - console.error("Error restoring progress:", error) - return false + console.error('Error restoring progress:', error); + return false; } }, - saveProgressToFile: () => { + saveProgressToFile: () => { try { // Pack painted map if available let paintedMapPacked = null; if (state.paintedMap && state.imageData) { - const data = Utils.packPaintedMapToBase64(state.paintedMap, state.imageData.width, state.imageData.height); + const data = Utils.packPaintedMapToBase64( + state.paintedMap, + state.imageData.width, + state.imageData.height + ); if (data) { paintedMapPacked = { width: state.imageData.width, height: state.imageData.height, - data: data + data: data, }; } } const progressData = { timestamp: Date.now(), - version: "2.1", + version: '2.1', state: { totalPixels: state.totalPixels, paintedPixels: state.paintedPixels, @@ -2085,29 +2232,29 @@ function applyTheme() { }, imageData: state.imageData ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } + width: state.imageData.width, + height: state.imageData.height, + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } : null, paintedMapPacked: paintedMapPacked, - } + }; - const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, "-")}.json` - Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename) - return true + 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 + console.error('Error saving to file:', error); + return false; } }, - loadProgressFromFile: async () => { + loadProgressFromFile: async () => { try { - const data = await Utils.createFileUploader() + const data = await Utils.createFileUploader(); if (!data || !data.state) { - throw new Error("Invalid file format") + throw new Error('Invalid file format'); } const ver = data.version; let migrated = data; @@ -2117,11 +2264,11 @@ function applyTheme() { } else { migrated = Utils.migrateProgressToV21(data) || data; } - const success = Utils.restoreProgress(migrated) - return success + const success = Utils.restoreProgress(migrated); + return success; } catch (error) { - console.error("Error loading from file:", error) - throw error + console.error('Error loading from file:', error); + throw error; } }, @@ -2163,74 +2310,74 @@ function applyTheme() { return false; } }, - } + }; // IMAGE PROCESSOR CLASS class ImageProcessor { constructor(imageSrc) { - this.imageSrc = imageSrc - this.img = null - this.canvas = null - this.ctx = null + 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 = 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 - }) + 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 + 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") + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); - tempCanvas.width = newWidth - tempCanvas.height = newHeight + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; - tempCtx.imageSmoothingEnabled = false - tempCtx.drawImage(this.canvas, 0, 0, newWidth, 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) + 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 + return this.ctx.getImageData(0, 0, newWidth, newHeight).data; } generatePreview(width, height) { - const previewCanvas = document.createElement("canvas") - const previewCtx = previewCanvas.getContext("2d") + const previewCanvas = document.createElement('canvas'); + const previewCtx = previewCanvas.getContext('2d'); - previewCanvas.width = width - previewCanvas.height = height + previewCanvas.width = width; + previewCanvas.height = height; - previewCtx.imageSmoothingEnabled = false - previewCtx.drawImage(this.img, 0, 0, width, height) + previewCtx.imageSmoothingEnabled = false; + previewCtx.drawImage(this.img, 0, 0, width, height); - return previewCanvas.toDataURL() + return previewCanvas.toDataURL(); } } @@ -2238,50 +2385,52 @@ function applyTheme() { 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 } + await ensureToken(); + if (!turnstileToken) return 'token_error'; + const payload = { coords: [pixelX, pixelY], colors: [color], t: turnstileToken }; const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', body: JSON.stringify(payload), - }) + }); if (res.status === 403) { - console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.") - turnstileToken = null - tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - return "token_error" + 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 + const data = await res.json(); + return data?.painted === 1; } catch (e) { - console.error("Paint request failed:", e) - return false + 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() + const res = await fetch('https://backend.wplace.live/me', { + credentials: 'include', + }); + const data = await res.json(); return { charges: data.charges?.count || 0, max: data.charges?.max || 1, cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, - } + }; } catch (e) { - console.error("Failed to get charges:", e) + console.error('Failed to get charges:', e); return { charges: 0, max: 1, cooldown: CONFIG.COOLDOWN_DEFAULT, - } + }; } }, - } + }; // Desktop Notification Manager const NotificationManager = { @@ -2289,14 +2438,14 @@ function applyTheme() { pollIntervalMs: 60_000, icon() { const link = document.querySelector("link[rel~='icon']"); - return link?.href || (location.origin + "/favicon.ico"); + return link?.href || location.origin + '/favicon.ico'; }, async requestPermission() { - if (!("Notification" in window)) { - Utils.showAlert(Utils.t("notificationsNotSupported"), "warning"); - return "denied"; + if (!('Notification' in window)) { + Utils.showAlert(Utils.t('notificationsNotSupported'), 'warning'); + return 'denied'; } - if (Notification.permission === "granted") return "granted"; + if (Notification.permission === 'granted') return 'granted'; try { const perm = await Notification.requestPermission(); return perm; @@ -2305,11 +2454,13 @@ function applyTheme() { } }, canNotify() { - return state.notificationsEnabled && - typeof Notification !== "undefined" && - Notification.permission === "granted"; + return ( + state.notificationsEnabled && + typeof Notification !== 'undefined' && + Notification.permission === 'granted' + ); }, - notify(title, body, tag = "wplace-charges", force = false) { + notify(title, body, tag = 'wplace-charges', force = false) { if (!this.canNotify()) return false; if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; try { @@ -2324,7 +2475,7 @@ function applyTheme() { return true; } catch { // Graceful fallback - Utils.showAlert(body, "info"); + Utils.showAlert(body, 'info'); return false; } }, @@ -2341,8 +2492,12 @@ function applyTheme() { const shouldEdge = state._lastChargesBelow || force; const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; if (shouldEdge || shouldRepeat) { - const msg = Utils.t("chargesReadyMessage", { current: Math.floor(state.currentCharges), max: state.maxCharges, threshold: state.cooldownChargeThreshold }); - this.notify(Utils.t("chargesReadyNotification"), msg, "wplace-notify-charges"); + const msg = Utils.t('chargesReadyMessage', { + current: Math.floor(state.currentCharges), + max: state.maxCharges, + threshold: state.cooldownChargeThreshold, + }); + this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); state._lastChargesNotifyAt = now; } state._lastChargesBelow = false; @@ -2361,7 +2516,9 @@ function applyTheme() { state.cooldown = cooldown; state.maxCharges = Math.max(1, Math.floor(max)); this.maybeNotifyChargesReached(); - } catch { /* ignore */ } + } catch { + /* ignore */ + } }, this.pollIntervalMs); }, stopPolling() { @@ -2378,69 +2535,97 @@ function applyTheme() { }; // COLOR MATCHING FUNCTION - Optimized with caching - const colorCache = new Map() + const colorCache = new Map(); function findClosestColor(targetRgb, availableColors) { - if (!availableColors || availableColors.length === 0) return 1 - const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${state.enableChromaPenalty?'c':'nc'}|${state.chromaPenaltyWeight}` - if (colorCache.has(cacheKey)) return colorCache.get(cacheKey) + if (!availableColors || availableColors.length === 0) return 1; + const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${state.enableChromaPenalty ? 'c' : 'nc'}|${state.chromaPenaltyWeight}`; + if (colorCache.has(cacheKey)) return colorCache.get(cacheKey); - 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) { colorCache.set(cacheKey, whiteEntry.id); return whiteEntry.id } + 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) { + colorCache.set(cacheKey, whiteEntry.id); + return whiteEntry.id; + } } - let bestId = availableColors[0].id - let bestScore = Infinity + let bestId = availableColors[0].id; + 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; if (dist === 0) break } + 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; + if (dist === 0) break; + } } - } else { // lab - 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 + } else { + // lab + 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 + 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) + const candChroma = Math.sqrt(a2 * a2 + b2 * b2); if (candChroma < targetChroma) { - const cd = targetChroma - candChroma - dist += cd * cd * penaltyWeight + const cd = targetChroma - candChroma; + dist += cd * cd * penaltyWeight; } } - if (dist < bestScore) { bestScore = dist; bestId = c.id; if (dist === 0) break } + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + if (dist === 0) break; + } } } - colorCache.set(cacheKey, bestId) - if (colorCache.size > 15000) { const firstKey = colorCache.keys().next().value; colorCache.delete(firstKey) } - return bestId + colorCache.set(cacheKey, bestId); + if (colorCache.size > 15000) { + const firstKey = colorCache.keys().next().value; + colorCache.delete(firstKey); + } + return bestId; } // UI UPDATE FUNCTIONS (declared early to avoid reference errors) - let updateUI = () => { } - let updateStats = () => { } - let updateDataButtons = () => { } + let updateUI = () => {}; + let updateStats = () => {}; + let updateDataButtons = () => {}; function updateActiveColorPalette() { state.activeColorPalette = []; const activeSwatches = document.querySelectorAll('.wplace-color-swatch.active'); if (activeSwatches) { - activeSwatches.forEach(swatch => { + activeSwatches.forEach((swatch) => { const rgbStr = swatch.getAttribute('data-rgb'); if (rgbStr) { const rgb = rgbStr.split(',').map(Number); @@ -2456,7 +2641,7 @@ function applyTheme() { function toggleAllColors(select, showingUnavailable = false) { const swatches = document.querySelectorAll('.wplace-color-swatch'); if (swatches) { - swatches.forEach(swatch => { + 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) { @@ -2473,16 +2658,16 @@ function applyTheme() { function unselectAllPaidColors() { const swatches = document.querySelectorAll('.wplace-color-swatch'); if (swatches) { - swatches.forEach(swatch => { + swatches.forEach((swatch) => { const colorId = parseInt(swatch.getAttribute('data-color-id'), 10); - if (!isNaN(colorId) && colorId>= 32) { + 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'); @@ -2502,16 +2687,16 @@ function applyTheme() { let totalCount = 0; // Convert COLOR_MAP to array and filter out transparent - const allColors = Object.values(CONFIG.COLOR_MAP).filter(color => color.rgb !== null); + const allColors = Object.values(CONFIG.COLOR_MAP).filter((color) => color.rgb !== null); - allColors.forEach(colorData => { + 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 + 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 @@ -2540,10 +2725,14 @@ function applyTheme() { 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)' : '')); + 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) { @@ -2571,49 +2760,66 @@ function applyTheme() { }); } - container.querySelector('#selectAllBtn')?.addEventListener('click', () => toggleAllColors(true, showAllToggle?.checked)); - container.querySelector('#unselectAllBtn')?.addEventListener('click', () => toggleAllColors(false, showAllToggle?.checked)); - container.querySelector('#unselectPaidBtn')?.addEventListener('click', () => unselectAllPaidColors()); + 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"); + 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 = Utils.detectSitekey(); - console.log("🔑 Generating Turnstile token for sitekey:", sitekey); - console.log('🧭 UA:', navigator.userAgent.substring(0, 50) + '...', 'Platform:', navigator.platform); - + 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(); } - + const token = await Utils.generatePaintToken(sitekey); - - 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}`); - + + 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}` + ); + 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}`); + 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..."); + if (state.tokenSource === 'hybrid') { + console.log( + '🔄 Hybrid mode: Generator failed, automatically switching to manual pixel placement...' + ); const fbToken = await handleCaptchaFallback(); return fbToken; } else { @@ -2629,23 +2835,31 @@ function applyTheme() { try { // Ensure we have a fresh promise to await for a new token capture if (!_resolveToken) { - tokenPromise = new Promise((res) => { _resolveToken = res; }); + tokenPromise = new Promise((res) => { + _resolveToken = res; + }); } - const timeoutPromise = Utils.sleep(20000).then(() => reject(new Error("Auto-CAPTCHA timed out."))); + 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."); + 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."); + 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."); + if (!canvas) throw new Error('Could not find the canvas element.'); canvas.setAttribute('tabindex', '0'); canvas.focus(); @@ -2653,10 +2867,16 @@ function applyTheme() { 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 })); + 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 })); + canvas.dispatchEvent( + new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true }) + ); await Utils.sleep(500); // 800ms delay before sending confirmation @@ -2665,7 +2885,9 @@ function applyTheme() { // 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'); + 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; @@ -2685,46 +2907,44 @@ function applyTheme() { })(); await Promise.race([solvePromise, timeoutPromise]); - } catch (error) { - console.error("Auto-CAPTCHA process failed:", error); + console.error('Auto-CAPTCHA process failed:', error); reject(error); } }); } - async function createUI() { - await detectLanguage() + 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") + 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() + if (existingContainer) existingContainer.remove(); + if (existingStats) existingStats.remove(); + if (existingSettings) existingSettings.remove(); + if (existingResizeContainer) existingResizeContainer.remove(); + if (existingResizeOverlay) existingResizeOverlay.remove(); - loadThemePreference() - await initializeTranslations() + loadThemePreference(); + await initializeTranslations(); - const theme = getCurrentTheme() - applyTheme() // <- new: set CSS vars and theme class before building UI + const theme = getCurrentTheme(); + applyTheme(); // <- new: set CSS vars and theme class before building UI - const fontAwesome = document.createElement("link") - fontAwesome.rel = "stylesheet" - fontAwesome.href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" - document.head.appendChild(fontAwesome) + const fontAwesome = document.createElement('link'); + fontAwesome.rel = 'stylesheet'; + fontAwesome.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'; + document.head.appendChild(fontAwesome); - if (theme.fontFamily.includes("Press Start 2P")) { - const googleFonts = document.createElement("link") - googleFonts.rel = "stylesheet" - googleFonts.href = "https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" - document.head.appendChild(googleFonts) + if (theme.fontFamily.includes('Press Start 2P')) { + const googleFonts = document.createElement('link'); + googleFonts.rel = 'stylesheet'; + googleFonts.href = 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'; + document.head.appendChild(googleFonts); } // Link external CSS files @@ -2734,16 +2954,16 @@ function applyTheme() { cssLink.setAttribute('data-wplace-theme', 'true'); document.head.appendChild(cssLink); - const container = document.createElement("div") - container.id = "wplace-image-bot-container" + const container = document.createElement('div'); + container.id = 'wplace-image-bot-container'; container.innerHTML = `
- ${Utils.t("title")} + ${Utils.t('title')}
- -
@@ -2761,7 +2981,7 @@ function applyTheme() {
- ${Utils.t("initMessage")} + ${Utils.t('initMessage')}
@@ -2775,17 +2995,17 @@ function applyTheme() {
@@ -2798,17 +3018,17 @@ function applyTheme() {
@@ -2816,9 +3036,9 @@ function applyTheme() {
-
⏱️ ${Utils.t("cooldownSettings")}
+
⏱️ ${Utils.t('cooldownSettings')}
- +
${state.cooldownChargeThreshold} @@ -2833,32 +3053,32 @@ function applyTheme() {
- ` + `; // Stats Window - Separate UI - const statsContainer = document.createElement("div") - statsContainer.id = "wplace-stats-container" - statsContainer.style.display = "none" + const statsContainer = document.createElement('div'); + statsContainer.id = 'wplace-stats-container'; + statsContainer.style.display = 'none'; statsContainer.innerHTML = `
@@ -2878,23 +3098,23 @@ function applyTheme() {
-
${Utils.t("initMessage")}
+
${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" - + 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%)` - + 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; @@ -2908,10 +3128,14 @@ function applyTheme() { backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; overflow: hidden; animation: settingsSlideIn 0.4s ease-out; - ${theme.animations?.glow ? ` + ${ + theme.animations?.glow + ? ` box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; - ` : ''} + ` + : '' + } `; settingsContainer.innerHTML = ` @@ -2919,7 +3143,7 @@ function applyTheme() {

- ${Utils.t("settings")} + ${Utils.t('settings')}

@@ -2949,7 +3173,7 @@ function applyTheme() {
@@ -2964,9 +3188,13 @@ function applyTheme() { background: ${theme.accent ? `${theme.accent}20` : 'rgba(255,255,255,0.1)'}; border-radius: ${theme.borderRadius || '12px'}; border: 1px solid ${theme.accent || 'rgba(255,255,255,0.1)'}; - ${theme.animations?.glow ? ` + ${ + theme.animations?.glow + ? ` box-shadow: 0 0 15px ${theme.accent || 'rgba(255,255,255,0.1)'}33; - ` : ''} + ` + : '' + } ">
@@ -3001,17 +3229,17 @@ function applyTheme() {
@@ -3021,7 +3249,7 @@ function applyTheme() {
@@ -3113,13 +3341,16 @@ function applyTheme() {
@@ -3128,7 +3359,7 @@ function applyTheme() {
- ${Utils.t("keepAspectRatio")} + ${Utils.t('keepAspectRatio')}
@@ -3359,8 +3590,8 @@ function applyTheme() {
- +
@@ -3382,7 +3613,7 @@ function applyTheme() { Enable Dithering
Floyd–Steinberg error diffusion in preview and applied output
- +
- ` + `; - const resizeOverlay = document.createElement("div") - resizeOverlay.className = "resize-overlay" + 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) + 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" + 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'); - 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") - // 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..."; + 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..."; + 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..."; + 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 cooldownValue = container.querySelector("#cooldownValue"); + + 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 cooldownValue = container.querySelector('#cooldownValue'); if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { - console.error("Some UI elements not found:", { + 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") + const header = container.querySelector('.wplace-header'); - makeDraggable(container) + 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") + 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 + console.warn('No draggable header found for element:', element); + return; } - header.onmousedown = dragMouseDown + header.onmousedown = dragMouseDown; function dragMouseDown(e) { - if (e.target.closest(".wplace-header-btn") || e.target.closest("button")) return + if (e.target.closest('.wplace-header-btn') || e.target.closest('button')) return; - e.preventDefault() - isDragging = true + e.preventDefault(); + isDragging = true; - const rect = element.getBoundingClientRect() + const rect = element.getBoundingClientRect(); - element.style.transform = "none" - element.style.top = rect.top + "px" - element.style.left = rect.left + "px" + 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 + pos3 = e.clientX; + pos4 = e.clientY; + element.classList.add('wplace-dragging'); + document.onmouseup = closeDragElement; + document.onmousemove = elementDrag; - document.body.style.userSelect = "none" + document.body.style.userSelect = 'none'; } function elementDrag(e) { - if (!isDragging) return + if (!isDragging) return; - e.preventDefault() - pos1 = pos3 - e.clientX - pos2 = pos4 - e.clientY - pos3 = e.clientX - pos4 = e.clientY + 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 + 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 + 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)) + 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" + 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 = "" + isDragging = false; + element.classList.remove('wplace-dragging'); + document.onmouseup = null; + document.onmousemove = null; + document.body.style.userSelect = ''; } } - makeDraggable(statsContainer) - makeDraggable(container) + makeDraggable(statsContainer); + makeDraggable(container); if (statsBtn && closeStatsBtn) { - statsBtn.addEventListener("click", () => { - const isVisible = statsContainer.style.display !== "none" + statsBtn.addEventListener('click', () => { + const isVisible = statsContainer.style.display !== 'none'; if (isVisible) { - statsContainer.style.display = "none" - statsBtn.innerHTML = '' - statsBtn.title = Utils.t('showStats') + statsContainer.style.display = 'none'; + statsBtn.innerHTML = ''; + statsBtn.title = Utils.t('showStats'); } else { - statsContainer.style.display = "block" - statsBtn.innerHTML = '' - statsBtn.title = Utils.t('hideStats') + 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') - }) + 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 + refreshChargesBtn.addEventListener('click', async () => { + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = true; try { - await updateStats() + await updateStats(); } catch (error) { - console.error("Error refreshing charges:", error) + console.error('Error refreshing charges:', error); } finally { - refreshChargesBtn.innerHTML = '' - refreshChargesBtn.disabled = false + refreshChargesBtn.innerHTML = ''; + refreshChargesBtn.disabled = false; } - }) + }); } } if (statsContainer && statsBtn) { @@ -3593,55 +3831,58 @@ function applyTheme() { statsBtn.title = Utils.t('showStats'); } - const settingsBtn = container.querySelector("#settingsBtn") - const closeSettingsBtn = settingsContainer.querySelector("#closeSettingsBtn") - const applySettingsBtn = settingsContainer.querySelector("#applySettingsBtn"); - + 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"); + settingsBtn.addEventListener('click', () => { + const isVisible = settingsContainer.classList.contains('show'); if (isVisible) { - settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"; - settingsContainer.classList.remove("show"); + settingsContainer.style.animation = 'settingsFadeOut 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); setTimeout(() => { - settingsContainer.style.animation = ""; + 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 = "settingsSlideIn 0.4s ease-out"; + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; + settingsContainer.classList.add('show'); + settingsContainer.style.animation = 'settingsSlideIn 0.4s ease-out'; } - }) + }); - closeSettingsBtn.addEventListener("click", () => { - settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards"; - settingsContainer.classList.remove("show"); + closeSettingsBtn.addEventListener('click', () => { + settingsContainer.style.animation = 'settingsFadeOut 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%)"; + settingsContainer.style.animation = ''; + settingsContainer.style.top = '50%'; + settingsContainer.style.left = '50%'; + settingsContainer.style.transform = 'translate(-50%, -50%)'; }, 300); }); - applySettingsBtn.addEventListener("click", () => { + 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; + if (enableChromaPenaltyToggle) + state.enableChromaPenalty = enableChromaPenaltyToggle.checked; const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); - if (chromaPenaltyWeightSlider) state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15; + 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 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; + const v = parseInt(whiteThresholdInput.value, 10); + if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v; } // Update functional thresholds CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; @@ -3653,148 +3894,160 @@ function applyTheme() { const notifIntervalInput = document.getElementById('notifIntervalInput'); if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; - if (notifOnlyUnfocusedToggle) state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.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; + const v = parseInt(notifIntervalInput.value, 10); + if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; } saveBotSettings(); - Utils.showAlert(Utils.t("settingsSaved"), "success"); + Utils.showAlert(Utils.t('settingsSaved'), 'success'); closeSettingsBtn.click(); NotificationManager.syncFromState(); }); - makeDraggable(settingsContainer) + makeDraggable(settingsContainer); - const tokenSourceSelect = settingsContainer.querySelector("#tokenSourceSelect") + 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}`) + 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") - }) + 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") - + 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 - + 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' + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; } else { - normalBatchControls.style.display = 'block' - randomBatchControls.style.display = 'none' + 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") - }) + + 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) + randomBatchMin.addEventListener('input', (e) => { + const min = parseInt(e.target.value); if (min >= 1 && min <= 1000) { - state.randomBatchMin = min + state.randomBatchMin = min; // Ensure min doesn't exceed max if (randomBatchMax && min > state.randomBatchMax) { - state.randomBatchMax = min - randomBatchMax.value = min + state.randomBatchMax = min; + randomBatchMax.value = min; } - saveBotSettings() + saveBotSettings(); } - }) + }); } - + if (randomBatchMax) { - randomBatchMax.addEventListener("input", (e) => { - const max = parseInt(e.target.value) + randomBatchMax.addEventListener('input', (e) => { + const max = parseInt(e.target.value); if (max >= 1 && max <= 1000) { - state.randomBatchMax = max + state.randomBatchMax = max; // Ensure max doesn't go below min if (randomBatchMin && max < state.randomBatchMin) { - state.randomBatchMin = max - randomBatchMin.value = max + state.randomBatchMin = max; + randomBatchMin.value = max; } - saveBotSettings() + saveBotSettings(); } - }) + }); } - const languageSelect = settingsContainer.querySelector("#languageSelect") + 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) + 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) + await loadTranslations(newLanguage); setTimeout(() => { - settingsContainer.style.display = "none" - createUI() - }, 100) - }) + settingsContainer.style.display = 'none'; + createUI(); + }, 100); + }); } - const themeSelect = settingsContainer.querySelector("#themeSelect") + const themeSelect = settingsContainer.querySelector('#themeSelect'); if (themeSelect) { - themeSelect.addEventListener("change", (e) => { - const newTheme = e.target.value - switchTheme(newTheme) - }) + 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"); + 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) { - overlayOpacitySlider.addEventListener('input', (e) => { - const opacity = parseFloat(e.target.value); - state.overlayOpacity = opacity; - overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; - }); - } + if (overlayOpacitySlider && overlayOpacityValue) { + overlayOpacitySlider.addEventListener('input', (e) => { + const opacity = parseFloat(e.target.value); + state.overlayOpacity = opacity; + overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; + }); + } - if (settingsPaintWhiteToggle) { - settingsPaintWhiteToggle.addEventListener('change', (e) => { - state.paintWhitePixels = e.target.checked; - saveBotSettings(); - }); - } + if (settingsPaintWhiteToggle) { + settingsPaintWhiteToggle.addEventListener('change', (e) => { + state.paintWhitePixels = e.target.checked; + saveBotSettings(); + }); + } - if (settingsPaintTransparentToggle) { - settingsPaintTransparentToggle.addEventListener('change', (e) => { - state.paintTransparentPixels = e.target.checked; - saveBotSettings(); - }); - } + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + saveBotSettings(); + }); + } // Speed slider event listener - const speedSlider = settingsContainer.querySelector("#speedSlider"); - const speedValue = settingsContainer.querySelector("#speedValue"); + const speedSlider = settingsContainer.querySelector('#speedSlider'); + const speedValue = settingsContainer.querySelector('#speedValue'); if (speedSlider && speedValue) { speedSlider.addEventListener('input', (e) => { const speed = parseInt(e.target.value, 10); @@ -3808,89 +4061,93 @@ function applyTheme() { enableBlueMarbleToggle.addEventListener('click', async () => { state.blueMarbleEnabled = enableBlueMarbleToggle.checked; if (state.imageLoaded && overlayManager.imageBitmap) { - Utils.showAlert(Utils.t('reprocessingOverlay'), "info"); + Utils.showAlert(Utils.t('reprocessingOverlay'), 'info'); await overlayManager.processImageIntoChunks(); - Utils.showAlert(Utils.t('overlayUpdated'), "success"); + 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) // (Advanced color listeners moved outside to work with resize dialog) // Notifications listeners - const notifPermBtn = settingsContainer.querySelector("#notifRequestPermBtn"); - const notifTestBtn = settingsContainer.querySelector("#notifTestBtn"); + const notifPermBtn = settingsContainer.querySelector('#notifRequestPermBtn'); + const notifTestBtn = settingsContainer.querySelector('#notifTestBtn'); if (notifPermBtn) { - notifPermBtn.addEventListener("click", async () => { + 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 (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); + 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'); + 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'); if (compactBtn) { - compactBtn.addEventListener("click", () => { - container.classList.toggle("wplace-compact") - const isCompact = container.classList.contains("wplace-compact") + compactBtn.addEventListener('click', () => { + container.classList.toggle('wplace-compact'); + const isCompact = container.classList.contains('wplace-compact'); if (isCompact) { - compactBtn.innerHTML = '' - compactBtn.title = Utils.t('expandMode') + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('expandMode'); } else { - compactBtn.innerHTML = '' - compactBtn.title = Utils.t('compactMode') + compactBtn.innerHTML = ''; + compactBtn.title = Utils.t('compactMode'); } - }) + }); } if (minimizeBtn) { - minimizeBtn.addEventListener("click", () => { - state.minimized = !state.minimized + 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') + 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') + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); } - saveBotSettings() - }) + saveBotSettings(); + }); } if (toggleOverlayBtn) { @@ -3903,70 +4160,70 @@ function applyTheme() { } if (state.minimized) { - container.classList.add("wplace-minimized") - content.classList.add("wplace-hidden") + container.classList.add('wplace-minimized'); + content.classList.add('wplace-hidden'); if (minimizeBtn) { - minimizeBtn.innerHTML = '' - minimizeBtn.title = Utils.t('restore') + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('restore'); } } else { - container.classList.remove("wplace-minimized") - content.classList.remove("wplace-hidden") + container.classList.remove('wplace-minimized'); + content.classList.remove('wplace-hidden'); if (minimizeBtn) { - minimizeBtn.innerHTML = '' - minimizeBtn.title = Utils.t('minimize') + minimizeBtn.innerHTML = ''; + minimizeBtn.title = Utils.t('minimize'); } } if (saveBtn) { - saveBtn.addEventListener("click", () => { + saveBtn.addEventListener('click', () => { if (!state.imageLoaded) { - Utils.showAlert(Utils.t("missingRequirements"), "error") - return + Utils.showAlert(Utils.t('missingRequirements'), 'error'); + return; } - const success = Utils.saveProgress() + const success = Utils.saveProgress(); if (success) { - updateUI("autoSaved", "success") - Utils.showAlert(Utils.t("autoSaved"), "success") + updateUI('autoSaved', 'success'); + Utils.showAlert(Utils.t('autoSaved'), 'success'); } else { - Utils.showAlert(Utils.t('errorSavingProgress'), "error") + Utils.showAlert(Utils.t('errorSavingProgress'), 'error'); } - }) + }); } if (loadBtn) { - loadBtn.addEventListener("click", () => { + loadBtn.addEventListener('click', () => { // Check if initial setup is complete if (!state.initialSetupComplete) { - Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), "warning"); + Utils.showAlert(Utils.t('pleaseWaitInitialSetup'), 'warning'); return; } - - const savedData = Utils.loadProgress() + + const savedData = Utils.loadProgress(); if (!savedData) { - updateUI("noSavedData", "warning") - Utils.showAlert(Utils.t("noSavedData"), "warning") - return + 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`, - ) + `${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) + const success = Utils.restoreProgress(savedData); if (success) { - updateUI("dataLoaded", "success") - Utils.showAlert(Utils.t("dataLoaded"), "success") - updateDataButtons() + updateUI('dataLoaded', 'success'); + Utils.showAlert(Utils.t('dataLoaded'), 'success'); + updateDataButtons(); - updateStats() + updateStats(); // Restore overlay if image data was loaded from localStorage - Utils.restoreOverlayFromData().catch(error => { + Utils.restoreOverlayFromData().catch((error) => { console.error('Failed to restore overlay from localStorage:', error); }); @@ -3978,79 +4235,79 @@ function applyTheme() { } if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { - startBtn.disabled = false + startBtn.disabled = false; } } else { - Utils.showAlert(Utils.t('errorLoadingProgress'), "error") + Utils.showAlert(Utils.t('errorLoadingProgress'), 'error'); } } - }) + }); } if (saveToFileBtn) { - saveToFileBtn.addEventListener("click", () => { - const success = Utils.saveProgressToFile() + saveToFileBtn.addEventListener('click', () => { + const success = Utils.saveProgressToFile(); if (success) { - updateUI("fileSaved", "success") - Utils.showAlert(Utils.t("fileSaved"), "success") + updateUI('fileSaved', 'success'); + Utils.showAlert(Utils.t('fileSaved'), 'success'); } else { - Utils.showAlert(Utils.t("fileError"), "error") + Utils.showAlert(Utils.t('fileError'), 'error'); } - }) + }); } if (loadFromFileBtn) { - loadFromFileBtn.addEventListener("click", async () => { + loadFromFileBtn.addEventListener('click', async () => { // Check if initial setup is complete if (!state.initialSetupComplete) { - Utils.showAlert(Utils.t('pleaseWaitFileSetup'), "warning"); + 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() + 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 => { + 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 + 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 + startBtn.disabled = false; } } } catch (error) { - if (error.message === "Invalid JSON file") { - Utils.showAlert(Utils.t("invalidFileFormat"), "error") + if (error.message === 'Invalid JSON file') { + Utils.showAlert(Utils.t('invalidFileFormat'), 'error'); } else { - Utils.showAlert(Utils.t("fileError"), "error") + Utils.showAlert(Utils.t('fileError'), 'error'); } } - }) + }); } - updateUI = (messageKey, type = "default", params = {}) => { - const message = Utils.t(messageKey, params) - statusText.textContent = message - statusText.className = `wplace-status status-${type}` - statusText.style.animation = "none" - void statusText.offsetWidth - statusText.style.animation = "slideIn 0.3s ease-out" - } + updateUI = (messageKey, type = 'default', params = {}) => { + const message = Utils.t(messageKey, params); + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + statusText.style.animation = 'none'; + void statusText.offsetWidth; + statusText.style.animation = 'slideIn 0.3s ease-out'; + }; updateStats = async () => { const { charges, cooldown, max } = await WPlaceService.getCharges(); @@ -4066,22 +4323,27 @@ function applyTheme() { let imageStatsHTML = ''; if (state.imageLoaded) { - const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0; + 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.currentCharges, state.cooldown); + state.estimatedTime = Utils.calculateEstimatedTime( + remainingPixels, + state.currentCharges, + state.cooldown + ); progressBar.style.width = `${progress}%`; imageStatsHTML = `
-
${Utils.t("progress")}
+
${Utils.t('progress')}
${progress}%
-
${Utils.t("pixels")}
+
${Utils.t('pixels')}
${state.paintedPixels}/${state.totalPixels}
-
${Utils.t("estimatedTime")}
+
${Utils.t('estimatedTime')}
${Utils.formatTime(state.estimatedTime)}
`; @@ -4089,36 +4351,42 @@ function applyTheme() { let colorSwatchesHTML = ''; if (state.colorsChecked) { - colorSwatchesHTML = state.availableColors.map(color => { - const rgbString = `rgb(${color.rgb.join(',')})`; - return `
`; - }).join(''); + colorSwatchesHTML = state.availableColors + .map((color) => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }) + .join(''); } statsArea.innerHTML = ` ${imageStatsHTML}
-
${Utils.t("charges")}
+
${Utils.t('charges')}
${Math.floor(state.currentCharges)} / ${state.maxCharges}
- ${state.colorsChecked ? ` + ${ + state.colorsChecked + ? `
-
${Utils.t('availableColors', {count: state.availableColors.length})}
+
${Utils.t('availableColors', { count: state.availableColors.length })}
${colorSwatchesHTML}
- ` : ''} + ` + : '' + } `; - } + }; updateDataButtons = () => { - const hasImageData = state.imageLoaded && state.imageData - saveBtn.disabled = !hasImageData - saveToFileBtn.disabled = !hasImageData - } + const hasImageData = state.imageLoaded && state.imageData; + saveBtn.disabled = !hasImageData; + saveToFileBtn.disabled = !hasImageData; + }; - updateDataButtons() + updateDataButtons(); function showResizeDialog(processor) { let baseProcessor = processor; @@ -4134,24 +4402,36 @@ function applyTheme() { } 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%'; + 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; @@ -4159,17 +4439,21 @@ function applyTheme() { let _previewJobId = 0; let _isDraggingSize = false; let _zoomLevel = 1; - let _ditherWorkBuf = null; + 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); + 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 _resetDirty = () => { + _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; + }; const _markDirty = (x, y) => { if (!_dirty) _resetDirty(); if (x < _dirty.minX) _dirty.minX = x; @@ -4197,7 +4481,14 @@ function applyTheme() { 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; } + 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(); @@ -4207,8 +4498,10 @@ function applyTheme() { 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; + 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); @@ -4222,42 +4515,48 @@ function applyTheme() { widthValue.textContent = newWidth; heightValue.textContent = newHeight; - ensureMaskSize(newWidth, newHeight); - canvasStack.style.width = newWidth + 'px'; - canvasStack.style.height = newHeight + 'px'; + 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.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); + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); updateZoomLayout(); return; } - if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { await baseProcessor.load(); } - baseCtx.clearRect(0,0,newWidth,newHeight); + 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 tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; const applyFSDither = () => { - const w = newWidth, h = newHeight; + 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)); + 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; @@ -4283,8 +4582,15 @@ function applyTheme() { 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 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; @@ -4308,8 +4614,14 @@ function applyTheme() { 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))) { + 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; } @@ -4323,9 +4635,9 @@ function applyTheme() { 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(); + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); }; const onWidthInput = () => { @@ -4333,13 +4645,13 @@ function applyTheme() { 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(); + 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 fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); }; const onHeightInput = () => { @@ -4347,28 +4659,29 @@ function applyTheme() { 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(); + 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 fit = typeof computeFitZoom === 'function' ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); }; - paintWhiteToggle.onchange = (e) => { - state.paintWhitePixels = e.target.checked; - _updateResizePreview(); - saveBotSettings(); - }; + paintWhiteToggle.onchange = (e) => { + state.paintWhitePixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; - paintTransparentToggle.onchange = (e) => { - state.paintTransparentPixels = e.target.checked; - _updateResizePreview(); - saveBotSettings(); - }; + paintTransparentToggle.onchange = (e) => { + state.paintTransparentPixels = e.target.checked; + _updateResizePreview(); + saveBotSettings(); + }; - let panX = 0, panY = 0; + let panX = 0, + panY = 0; const clampPan = () => { const wrapRect = panStage?.getBoundingClientRect() || { width: 0, height: 0 }; const w = (baseCanvas.width || 1) * _zoomLevel; @@ -4397,7 +4710,8 @@ function applyTheme() { }; const updateZoomLayout = () => { - const w = baseCanvas.width || 1, h = baseCanvas.height || 1; + const w = baseCanvas.width || 1, + h = baseCanvas.height || 1; baseCanvas.style.width = w + 'px'; baseCanvas.style.height = h + 'px'; maskCanvas.style.width = w + 'px'; @@ -4415,8 +4729,10 @@ function applyTheme() { 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)); + 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; @@ -4427,8 +4743,16 @@ function applyTheme() { 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(); }); + if (zoomFitBtn) + zoomFitBtn.addEventListener('click', () => { + applyZoom(computeFitZoom()); + centerInView(); + }); + if (zoomActualBtn) + zoomActualBtn.addEventListener('click', () => { + applyZoom(1); + centerInView(); + }); const centerInView = () => { if (!panStage) return; @@ -4440,12 +4764,18 @@ function applyTheme() { 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 + 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 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); @@ -4453,89 +4783,140 @@ function applyTheme() { }; if (panModeBtn) { updatePanModeBtn(); - panModeBtn.addEventListener('click', () => { panMode = !panMode; updatePanModeBtn(); setCursor(panMode ? 'grab' : ''); }); + 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('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; + 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(); + 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 }); + 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); + 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); } - } 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); + }, + { 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; } - lastTouchDist = dist; - } - }, { passive: false }); + }, + { passive: false } + ); panStage.addEventListener('touchend', () => { - isPanning = false; lastTouchDist = null; setCursor(panMode || allowPan ? 'grab' : ''); + isPanning = false; + lastTouchDist = null; + setCursor(panMode || allowPan ? 'grab' : ''); }); } const schedulePreview = () => { @@ -4551,18 +4932,30 @@ function applyTheme() { } }; // Track dragging to reduce work and skip dithering during drag - const markDragStart = () => { _isDraggingSize = true; }; - const markDragEnd = () => { _isDraggingSize = false; schedulePreview(); }; + 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(); }); + 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 lastPaintX = -1, + lastPaintY = -1; let brushSize = 1; let rowColSize = 1; let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle' @@ -4580,7 +4973,7 @@ function applyTheme() { const map = [ [btnIgnore, 'ignore'], [btnUnignore, 'unignore'], - [btnToggle, 'toggle'] + [btnToggle, 'toggle'], ]; for (const [el, m] of map) { if (!el) continue; @@ -4589,22 +4982,31 @@ function applyTheme() { el.setAttribute('aria-pressed', active ? 'true' : 'false'); } }; - const setMode = (mode) => { maskMode = mode; updateModeButtons(); }; + const setMode = (mode) => { + maskMode = mode; + updateModeButtons(); + }; if (brushEl && brushValEl) { - brushEl.addEventListener('input', () => { brushSize = parseInt(brushEl.value, 10) || 1; brushValEl.textContent = brushSize; }); + 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; }); + 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(); + 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 @@ -4625,14 +5027,16 @@ function applyTheme() { }; const paintCircle = (cx, cy, radius, value) => { - const w = baseCanvas.width, h = baseCanvas.height; + 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; + const dx = xx - cx, + dy = yy - cy; if (dx * dx + dy * dy <= r2) { const idx = yy * w + xx; let val = state.resizeIgnoreMask[idx]; @@ -4646,8 +5050,17 @@ function applyTheme() { 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 (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); } } @@ -4656,15 +5069,16 @@ function applyTheme() { }; const paintRow = (y, value) => { - const w = baseCanvas.width, h = baseCanvas.height; + 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; @@ -4679,24 +5093,37 @@ function applyTheme() { 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 (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); } + if (_maskData) { + _markDirty(0, rowY); + _markDirty(w - 1, rowY); + } } }; const paintColumn = (x, value) => { - const w = baseCanvas.width, h = baseCanvas.height; + 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; @@ -4711,11 +5138,23 @@ function applyTheme() { 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 (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); } + if (_maskData) { + _markDirty(colX, 0); + _markDirty(colX, h - 1); + } } }; @@ -4728,7 +5167,8 @@ function applyTheme() { // 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; + 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) { @@ -4738,37 +5178,69 @@ function applyTheme() { } else { paintCircle(x, y, radius); } - lastPaintX = x; lastPaintY = y; + lastPaintX = x; + lastPaintY = y; redrawMaskOverlay(); }; - maskCanvas.addEventListener('mousedown', (e) => { + maskCanvas.addEventListener('mousedown', (e) => { if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage - draggingMask = true; handlePaint(e); + 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(); + // 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 (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(); - }); + 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); @@ -4780,27 +5252,37 @@ function applyTheme() { tempCanvas.width = newWidth; tempCanvas.height = newHeight; tempCtx.imageSmoothingEnabled = false; - if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + 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 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 mask = + state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight + ? state.resizeIgnoreMask + : null; const applyFSDitherFinal = async () => { - const w = newWidth, h = newHeight; + 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 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)); + 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; @@ -4828,8 +5310,15 @@ function applyTheme() { 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 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; @@ -4855,8 +5344,11 @@ function applyTheme() { 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 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) { @@ -4883,27 +5375,34 @@ function applyTheme() { state.totalPixels = totalValidPixels; state.paintedPixels = 0; - state.resizeSettings = { baseWidth: width, baseHeight: height, width: newWidth, height: newHeight }; + 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'); + 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 + // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored updateStats(); - updateUI("resizeSuccess", "success", { width: newWidth, height: newHeight }); + updateUI('resizeSuccess', 'success', { width: newWidth, height: newHeight }); closeResizeDialog(); }; downloadPreviewBtn.onclick = () => { try { - const w = baseCanvas.width, h = baseCanvas.height; + const w = baseCanvas.width, + h = baseCanvas.height; const out = document.createElement('canvas'); - out.width = w; out.height = h; + out.width = w; + out.height = h; const octx = out.getContext('2d'); octx.imageSmoothingEnabled = false; octx.drawImage(baseCanvas, 0, 0); @@ -4912,22 +5411,30 @@ function applyTheme() { link.download = 'wplace-preview.png'; link.href = out.toDataURL(); link.click(); - } catch (e) { console.warn('Failed to download preview:', e); } + } catch (e) { + console.warn('Failed to download preview:', e); + } }; cancelResize.onclick = closeResizeDialog; - resizeOverlay.style.display = "block"; - resizeContainer.style.display = "block"; + 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 {} + 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') { @@ -4943,30 +5450,46 @@ function applyTheme() { } 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; + 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 () => { + uploadBtn.addEventListener('click', async () => { const availableColors = Utils.extractAvailableColors(); if (availableColors.length < 10) { - updateUI("noColorsFound", "error"); - Utils.showAlert(Utils.t("noColorsFound"), "error"); + 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 }); + updateUI('colorsFound', 'success', { count: availableColors.length }); updateStats(); selectPosBtn.disabled = false; // Only enable resize button if image is also loaded @@ -4976,23 +5499,27 @@ function applyTheme() { } try { - updateUI("loadingImage", "default") - const imageSrc = await Utils.createImageUploader() + updateUI('loadingImage', 'default'); + const imageSrc = await Utils.createImageUploader(); if (!imageSrc) { - updateUI("colorsFound", "success", { count: state.availableColors.length }); + updateUI('colorsFound', 'success', { count: state.availableColors.length }); return; } - const processor = new ImageProcessor(imageSrc) - await processor.load() + const processor = new ImageProcessor(imageSrc); + await processor.load(); - const { width, height } = processor.getDimensions() - const pixels = processor.getPixelData() + 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]); + 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++; } @@ -5004,16 +5531,16 @@ function applyTheme() { pixels, totalPixels: totalValidPixels, processor, - } + }; + + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + state.imageLoaded = true; + state.lastPosition = { x: 0, y: 0 }; - 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 @@ -5034,186 +5561,188 @@ function applyTheme() { if (state.colorsChecked) { resizeBtn.disabled = false; } - saveBtn.disabled = false + saveBtn.disabled = false; if (state.startPosition) { - startBtn.disabled = false + startBtn.disabled = false; } - updateStats() - updateDataButtons() - updateUI("imageLoaded", "success", { count: totalValidPixels }) + updateStats(); + updateDataButtons(); + updateUI('imageLoaded', 'success', { count: totalValidPixels }); } catch { - updateUI("imageError", "error") + updateUI('imageError', 'error'); } - }) + }); } if (resizeBtn) { - resizeBtn.addEventListener("click", () => { + resizeBtn.addEventListener('click', () => { if (state.imageLoaded && state.imageData.processor && state.colorsChecked) { - showResizeDialog(state.imageData.processor) + showResizeDialog(state.imageData.processor); } else if (!state.colorsChecked) { - Utils.showAlert(Utils.t('uploadImageFirstColors'), "warning") + Utils.showAlert(Utils.t('uploadImageFirstColors'), 'warning'); } - }) + }); } if (selectPosBtn) { - selectPosBtn.addEventListener("click", async () => { - if (state.selectingPosition) return + selectPosBtn.addEventListener('click', async () => { + if (state.selectingPosition) return; - state.selectingPosition = true - state.startPosition = null - state.region = null - startBtn.disabled = true + state.selectingPosition = true; + state.startPosition = null; + state.region = null; + startBtn.disabled = true; - Utils.showAlert(Utils.t("selectPositionAlert"), "info") - updateUI("waitingPosition", "default") + 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" + 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() + 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+)/) + 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) + 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 } + }; + state.lastPosition = { x: 0, y: 0 }; await overlayManager.setPosition(state.startPosition, state.region); if (state.imageLoaded) { - startBtn.disabled = false + startBtn.disabled = false; } - window.fetch = originalFetch - state.selectingPosition = false - updateUI("positionSet", "success") + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionSet', 'success'); } } - return response + return response; } catch { - return originalFetch(url, options) + return originalFetch(url, options); } } - 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") + window.fetch = originalFetch; + state.selectingPosition = false; + updateUI('positionTimeout', 'error'); + Utils.showAlert(Utils.t('positionTimeout'), 'error'); } - }, 120000) - }) + }, 120000); + }); } async function startPainting() { if (!state.imageLoaded || !state.startPosition || !state.region) { - updateUI("missingRequirements", "error") - return false + updateUI('missingRequirements', 'error'); + return false; } - await ensureToken() - if (!turnstileToken) return false + await ensureToken(); + if (!turnstileToken) return false; - state.running = true - state.stopFlag = false - startBtn.disabled = true - stopBtn.disabled = false - uploadBtn.disabled = true - selectPosBtn.disabled = true - resizeBtn.disabled = true - saveBtn.disabled = true + 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") + updateUI('startPaintingMsg', 'success'); try { - await processImage() - return true + await processImage(); + return true; } catch { - updateUI("paintingError", "error") - return false + updateUI('paintingError', 'error'); + return false; } finally { - state.running = false - stopBtn.disabled = true - saveBtn.disabled = false + state.running = false; + stopBtn.disabled = true; + saveBtn.disabled = false; if (!state.stopFlag) { - startBtn.disabled = true - uploadBtn.disabled = false - selectPosBtn.disabled = false - resizeBtn.disabled = false + startBtn.disabled = true; + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.disabled = false; } else { - startBtn.disabled = false + startBtn.disabled = false; } toggleOverlayBtn.disabled = false; } } if (startBtn) { - startBtn.addEventListener("click", startPainting) + startBtn.addEventListener('click', startPainting); } if (stopBtn) { - stopBtn.addEventListener("click", () => { - state.stopFlag = true - state.running = false - stopBtn.disabled = true - updateUI("paintingStopped", "warning") + stopBtn.addEventListener('click', () => { + state.stopFlag = true; + state.running = false; + stopBtn.disabled = true; + updateUI('paintingStopped', 'warning'); if (state.imageLoaded && state.paintedPixels > 0) { - Utils.saveProgress() - Utils.showAlert(Utils.t("autoSaved"), "success") + Utils.saveProgress(); + Utils.showAlert(Utils.t('autoSaved'), 'success'); } - }) + }); } const checkSavedProgress = () => { - const savedData = Utils.loadProgress() + 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) + 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", - ) + `${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) + setTimeout(checkSavedProgress, 1000); if (cooldownSlider && cooldownValue) { - cooldownSlider.addEventListener("input", (e) => { + cooldownSlider.addEventListener('input', (e) => { const threshold = parseInt(e.target.value); state.cooldownChargeThreshold = threshold; cooldownValue.textContent = threshold; @@ -5228,14 +5757,17 @@ function applyTheme() { } async function processImage() { - const { width, height, pixels } = state.imageData - const { x: startX, y: startY } = state.startPosition - const { x: regionX, y: regionY } = state.region + const { width, height, pixels } = state.imageData; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; const isEligibleAt = (x, y) => { const idx = (y * width + x) * 4; - const r = pixels[idx], g = pixels[idx + 1], b = pixels[idx + 2], a = pixels[idx + 3]; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; if (!state.paintTransparentPixels && a < tThresh2) return false; if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) return false; return true; @@ -5249,11 +5781,19 @@ function applyTheme() { for (let y = 0; y < height && !foundStart; y++) { for (let x = 0; x < width; x++) { if (!isEligibleAt(x, y)) continue; - if (seen === target) { startRow = y; startCol = x; foundStart = true; break; } + if (seen === target) { + startRow = y; + startCol = x; + foundStart = true; + break; + } seen++; } } - if (!foundStart) { startRow = height; startCol = 0; } + if (!foundStart) { + startRow = height; + startCol = 0; + } let pixelBatch = null; let skippedPixels = { transparent: 0, white: 0, alreadyPainted: 0 }; @@ -5263,28 +5803,38 @@ function applyTheme() { for (let x = y === startRow ? startCol : 0; x < width; x++) { if (state.stopFlag) { if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log(`🎯 Sending final batch before stop with ${pixelBatch.pixels.length} pixels`); - const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + console.log( + `🎯 Sending final batch before stop with ${pixelBatch.pixels.length} pixels` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); if (success) { - pixelBatch.pixels.forEach(() => { state.paintedPixels++; }); + pixelBatch.pixels.forEach(() => { + state.paintedPixels++; + }); state.currentCharges -= pixelBatch.pixels.length; updateStats(); } } - state.lastPosition = { x, y } - updateUI("paintingPaused", "warning", { x, y }) - break outerLoop + state.lastPosition = { x, y }; + updateUI('paintingPaused', 'warning', { x, y }); + break outerLoop; } - - const idx = (y * width + x) * 4 - const r = pixels[idx] - const g = pixels[idx + 1] - const b = pixels[idx + 2] - const alpha = pixels[idx + 3] + const idx = (y * width + x) * 4; + const r = pixels[idx]; + const g = pixels[idx + 1]; + const b = pixels[idx + 2]; + const alpha = pixels[idx + 3]; const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - if ((!state.paintTransparentPixels && alpha < tThresh2) || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) { + if ( + (!state.paintTransparentPixels && alpha < tThresh2) || + (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + ) { if (!state.paintTransparentPixels && alpha < tThresh2) { skippedPixels.transparent++; } else { @@ -5310,34 +5860,45 @@ function applyTheme() { let pixelX = absX % 1000; let pixelY = absY % 1000; - if (!pixelBatch || + if ( + !pixelBatch || pixelBatch.regionX !== regionX + adderX || - pixelBatch.regionY !== regionY + adderY) { - + 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 sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - + console.log( + `🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${regionX + adderX},${regionY + adderY})` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); + if (success) { - pixelBatch.pixels.forEach((p) => { + pixelBatch.pixels.forEach((p) => { state.paintedPixels++; // Mark pixel as painted in map Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); }); state.currentCharges -= pixelBatch.pixels.length; - updateUI("paintingProgress", "default", { + updateUI('paintingProgress', 'default', { painted: state.paintedPixels, total: state.totalPixels, - }) + }); // Use smart save instead of fixed interval Utils.performSmartSave(); - if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) { + if ( + CONFIG.PAINTING_SPEED_ENABLED && + state.paintingSpeed > 0 && + pixelBatch.pixels.length > 0 + ) { // paintingSpeed now represents batch size, so add a small delay based on batch size const batchDelayFactor = Math.max(1, 100 / state.paintingSpeed); // Larger batches = less delay const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length); - await Utils.sleep(totalDelay) + await Utils.sleep(totalDelay); } updateStats(); } else { @@ -5351,16 +5912,17 @@ function applyTheme() { pixelBatch = { regionX: regionX + adderX, regionY: regionY + adderY, - pixels: [] + pixels: [], }; } - try { - const tileRegionX = pixelBatch ? (pixelBatch.regionX) : (regionX + adderX); - const tileRegionY = pixelBatch ? (pixelBatch.regionY) : (regionY + adderY); - const tileKeyParts = [(regionX + adderX), (regionY + adderY)]; - const existingColorRGBA = await overlayManager.getTilePixelColor(tileKeyParts[0], tileKeyParts[1], pixelX, pixelY).catch(() => null); + const tileRegionX = pixelBatch ? pixelBatch.regionX : regionX + adderX; + const tileRegionY = pixelBatch ? pixelBatch.regionY : regionY + adderY; + const tileKeyParts = [regionX + adderX, regionY + adderY]; + const existingColorRGBA = await overlayManager + .getTilePixelColor(tileKeyParts[0], tileKeyParts[1], pixelX, pixelY) + .catch(() => null); if (existingColorRGBA && Array.isArray(existingColorRGBA)) { const [er, eg, eb] = existingColorRGBA; const existingColorId = findClosestColor([er, eg, eb], state.availableColors); @@ -5386,31 +5948,44 @@ function applyTheme() { // Calculate batch size based on mode (normal/random) 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 sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + 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 sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); if (success) { pixelBatch.pixels.forEach((pixel) => { state.paintedPixels++; // Mark pixel as painted in map Utils.markPixelPainted(pixel.x, pixel.y, pixelBatch.regionX, pixelBatch.regionY); - }) + }); state.currentCharges -= pixelBatch.pixels.length; - updateStats() - updateUI("paintingProgress", "default", { + updateStats(); + updateUI('paintingProgress', 'default', { painted: state.paintedPixels, total: state.totalPixels, - }) + }); // Use smart save instead of fixed interval Utils.performSmartSave(); - if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) { - const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel - const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length) // minimum 100ms - await Utils.sleep(totalDelay) + if ( + CONFIG.PAINTING_SPEED_ENABLED && + state.paintingSpeed > 0 && + pixelBatch.pixels.length > 0 + ) { + const delayPerPixel = 1000 / state.paintingSpeed; // ms per pixel + const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length); // minimum 100ms + await Utils.sleep(totalDelay); } } else { // If batch failed after all retries, stop painting to prevent infinite loops @@ -5437,66 +6012,75 @@ function applyTheme() { // Enable save button during cooldown wait saveBtn.disabled = false; - updateUI("noChargesThreshold", "warning", { + updateUI('noChargesThreshold', 'warning', { time: Utils.formatTime(state.cooldown), threshold: state.cooldownChargeThreshold, - current: state.currentCharges + current: state.currentCharges, }); await updateStats(); - + // Allow auto save during cooldown Utils.performSmartSave(); - + await Utils.sleep(state.cooldown); } - + // Disable save button again after exiting wait loop (back to normal painting) if (!state.stopFlag) { saveBtn.disabled = true; } if (state.stopFlag) break outerLoop; - } } if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); - const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); if (success) { pixelBatch.pixels.forEach((pixel) => { state.paintedPixels++; // Mark pixel as painted in map Utils.markPixelPainted(pixel.x, pixel.y, pixelBatch.regionX, pixelBatch.regionY); - }) + }); state.currentCharges -= pixelBatch.pixels.length; // Final save with painted map Utils.saveProgress(); - - if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && pixelBatch.pixels.length > 0) { - const delayPerPixel = 1000 / state.paintingSpeed // ms per pixel - const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length) // minimum 100ms - await Utils.sleep(totalDelay) + + if ( + CONFIG.PAINTING_SPEED_ENABLED && + state.paintingSpeed > 0 && + pixelBatch.pixels.length > 0 + ) { + const delayPerPixel = 1000 / state.paintingSpeed; // ms per pixel + const totalDelay = Math.max(100, delayPerPixel * pixelBatch.pixels.length); // minimum 100ms + await Utils.sleep(totalDelay); } } else { // If final batch failed after retries, log it - console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`); + console.warn( + `⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.` + ); } } } finally { - if (window._chargesInterval) clearInterval(window._chargesInterval) - window._chargesInterval = null + if (window._chargesInterval) clearInterval(window._chargesInterval); + window._chargesInterval = null; } if (state.stopFlag) { - updateUI("paintingStopped", "warning") + updateUI('paintingStopped', 'warning'); // Save progress when stopped to preserve painted map - Utils.saveProgress() + Utils.saveProgress(); } else { - updateUI("paintingComplete", "success", { count: state.paintedPixels }) - state.lastPosition = { x: 0, y: 0 } + 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 + Utils.saveProgress(); // Save final complete state overlayManager.clear(); const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); if (toggleOverlayBtn) { @@ -5511,15 +6095,17 @@ function applyTheme() { console.log(` Skipped - Transparent: ${skippedPixels.transparent}`); console.log(` Skipped - White (disabled): ${skippedPixels.white}`); console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`); - console.log(` Total processed: ${state.paintedPixels + skippedPixels.transparent + skippedPixels.white + skippedPixels.alreadyPainted}`); + console.log( + ` Total processed: ${state.paintedPixels + skippedPixels.transparent + skippedPixels.white + skippedPixels.alreadyPainted}` + ); - updateStats() + 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); @@ -5530,11 +6116,11 @@ function applyTheme() { // Normal mode - use the fixed paintingSpeed value targetBatchSize = state.paintingSpeed; } - + // Always limit by available charges const maxAllowed = Math.floor(state.currentCharges); const finalBatchSize = Math.min(targetBatchSize, maxAllowed); - + return finalBatchSize; } @@ -5543,16 +6129,18 @@ function applyTheme() { 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`); - + 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") { + } else if (result === 'token_error') { console.log(`🔑 Token error on attempt ${attempt}, regenerating...`); - updateUI("captchaSolving", "warning"); + updateUI('captchaSolving', 'warning'); try { await handleCaptcha(); // Don't count token regeneration as a failed attempt @@ -5560,7 +6148,7 @@ function applyTheme() { continue; } catch (e) { console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e); - updateUI("captchaFailed", "error"); + updateUI('captchaFailed', 'error'); // Wait longer before retrying after token failure await Utils.sleep(5000); } @@ -5572,93 +6160,105 @@ function applyTheme() { 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"); + 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..."); + 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"; + 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) + 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 + 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 } + const payload = { coords, colors, t: token }; const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + 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.") - + 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..."); + console.log('🔄 Regenerating Turnstile token after 403...'); token = await handleCaptcha(); turnstileToken = token; - + // Retry the request with new token const retryPayload = { coords, colors, t: token }; - const retryRes = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { - method: "POST", - headers: { "Content-Type": "text/plain;charset=UTF-8" }, - credentials: "include", - body: JSON.stringify(retryPayload), - }); - + const retryRes = await fetch( + `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, + { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8' }, + credentials: 'include', + body: JSON.stringify(retryPayload), + } + ); + if (retryRes.status === 403) { turnstileToken = null; - tokenPromise = new Promise((resolve) => { _resolveToken = resolve }); - return "token_error"; + 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); + console.error('❌ Token regeneration failed:', retryError); turnstileToken = null; - tokenPromise = new Promise((resolve) => { _resolveToken = resolve }); - return "token_error"; + tokenPromise = new Promise((resolve) => { + _resolveToken = resolve; + }); + return 'token_error'; } } - - const data = await res.json() - return data?.painted === pixelBatch.length + + const data = await res.json(); + return data?.painted === pixelBatch.length; } catch (e) { - console.error("Batch paint request failed:", e) - return false + console.error('Batch paint request failed:', e); + return false; } } @@ -5675,20 +6275,27 @@ function applyTheme() { 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, - originalImage: state.originalImage, - // 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, + 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, + originalImage: state.originalImage, + // 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, @@ -5698,15 +6305,15 @@ function applyTheme() { 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)); + localStorage.setItem('wplace-bot-settings', JSON.stringify(settings)); } catch (e) { - console.warn("Could not save bot settings:", e); + console.warn('Could not save bot settings:', e); } } function loadBotSettings() { try { - const saved = localStorage.getItem("wplace-bot-settings"); + const saved = localStorage.getItem('wplace-bot-settings'); if (!saved) return; const settings = JSON.parse(saved); @@ -5714,39 +6321,52 @@ function applyTheme() { 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.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.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; // 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 { - if (loadBtn) loadBtn.style.animation = ""; + if (loadBtn) loadBtn.style.animation = ''; }, 600); - console.log("✅ Load Progress button enabled after initial setup"); + console.log('✅ Load Progress button enabled after initial setup'); } - + if (loadFromFileBtn) { loadFromFileBtn.disabled = false; - loadFromFileBtn.title = ""; + loadFromFileBtn.title = ''; // Add a subtle animation to indicate the button is now available - loadFromFileBtn.style.animation = "pulse 0.6s ease-in-out"; + loadFromFileBtn.style.animation = 'pulse 0.6s ease-in-out'; setTimeout(() => { - if (loadFromFileBtn) loadFromFileBtn.style.animation = ""; + if (loadFromFileBtn) loadFromFileBtn.style.animation = ''; }, 600); - console.log("✅ Load from File button enabled after initial setup"); + console.log('✅ Load from File button enabled after initial setup'); } - + if (uploadBtn) { uploadBtn.disabled = false; - uploadBtn.title = ""; + uploadBtn.title = ''; // Add a subtle animation to indicate the button is now available - uploadBtn.style.animation = "pulse 0.6s ease-in-out"; + uploadBtn.style.animation = 'pulse 0.6s ease-in-out'; setTimeout(() => { - if (uploadBtn) uploadBtn.style.animation = ""; + if (uploadBtn) uploadBtn.style.animation = ''; }, 600); - console.log("✅ Upload Image button enabled after initial setup"); + console.log('✅ Upload Image button enabled after initial setup'); } - + // Show a notification that file operations are now available - Utils.showAlert(Utils.t('fileOperationsAvailable'), "success"); + 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"); + 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('🔧 Initializing Turnstile token generator...'); + updateUI('initializingToken', 'default'); - console.log("Attempting to load Turnstile script..."); + console.log('Attempting to load Turnstile script...'); await Utils.loadTurnstile(); - console.log("Turnstile script loaded. Attempting to generate token..."); + 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"); + 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"); + console.warn( + '⚠️ Startup token generation failed (no token received), will retry when needed' + ); + updateUI('tokenRetryLater', 'warning'); enableFileOperations(); } } catch (error) { - console.error("❌ Critical error during Turnstile initialization:", error); // More specific error - updateUI("tokenRetryLater", "warning"); + console.error('❌ Critical error during Turnstile initialization:', error); // More specific error + updateUI('tokenRetryLater', 'warning'); enableFileOperations(); } } // Load theme preference immediately on startup before creating UI - loadThemePreference() - applyTheme() + loadThemePreference(); + applyTheme(); createUI().then(() => { // Generate token automatically after UI is ready @@ -5928,24 +6552,80 @@ function applyTheme() { const resetBtn = document.getElementById('resetAdvancedColorBtn'); const algoSelect = document.getElementById('colorAlgorithmSelect'); const chromaToggle = document.getElementById('enableChromaPenaltyToggle'); - const transInput = document.getElementById('transparencyThresholdInput'); + 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'); }); + 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(); }); - }) -})() + }); +})();