From 41d7fa8b5f91205d9e729d3064dc2e8ca9e5fedb Mon Sep 17 00:00:00 2001 From: kyuketski Date: Fri, 29 Aug 2025 19:18:27 +0300 Subject: [PATCH 01/24] 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(); }); - }) -})() + }); +})(); From 12d9d9cbbb3a330c16e8c81f1222a66a12e61bf4 Mon Sep 17 00:00:00 2001 From: kyuketski Date: Fri, 29 Aug 2025 21:16:54 +0300 Subject: [PATCH 02/24] style: update button radius and section layout in UI styles Updated CSS variables for button border-radius across themes and adjusted layout spacing for sections and settings footer. * Introduced `--wplace-btn-radius` for consistent button rounding in all themes. * Reduced margins and padding for `.wplace-section` and `.wplace-status-section` for tighter layout. * Added collapsible section behavior with arrow rotation and hidden controls. * Made settings footer sticky with proper background and padding. * Minor spacing and formatting fixes for better visual alignment. --- auto-image-styles.css | 53 ++++++++++++++++++++++++++++------------ themes/classic-light.css | 1 + themes/classic.css | 4 +-- 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/auto-image-styles.css b/auto-image-styles.css index 5406b16..2387556 100644 --- a/auto-image-styles.css +++ b/auto-image-styles.css @@ -8,9 +8,9 @@ /* ========================= */ /* Import theme files - classic is default */ -@import url('https://staninna.github.io/WPlace-AutoBOT/decoupled-css/themes/classic.css'); -@import url('https://staninna.github.io/WPlace-AutoBOT/decoupled-css/themes/classic-light.css'); -@import url('https://staninna.github.io/WPlace-AutoBOT/decoupled-css/themes/neon.css'); +@import url('https://skalsech.github.io/WPlace-AutoBOT/my_main/themes/classic.css'); +@import url('https://skalsech.github.io/WPlace-AutoBOT/my_main/themes/classic-light.css'); +@import url('https://skalsech.github.io/WPlace-AutoBOT/my_main/themes/neon.css'); /* Default :root CSS variables for 100% classic theme compliance */ /* These ensure the bot works perfectly even if theme files fail to load */ @@ -28,6 +28,7 @@ /* UI properties */ --wplace-radius: 12px; + --wplace-btn-radius: 16px; --wplace-border-style: solid; --wplace-border-width: 1px; --wplace-border-color: #222222; @@ -48,42 +49,42 @@ --wplace-icon-primary: #4facfe; --wplace-icon-secondary: #00f2fe; --wplace-icon-palette: #f093fb; - + /* Additional UI colors */ --wplace-danger: #ff6a6a; --wplace-danger-dark: #ff4757; --wplace-muted-text: #ffffffBB; - + /* Text variants */ --wplace-text-secondary: rgba(255,255,255,0.9); --wplace-text-muted: rgba(255,255,255,0.7); --wplace-text-dim: rgba(255,255,255,0.6); --wplace-text-faded: rgba(255,255,255,0.8); - + /* Background variants */ --wplace-bg-input: rgba(255,255,255,0.15); --wplace-bg-subtle: rgba(255,255,255,0.1); --wplace-bg-faint: rgba(255,255,255,0.08); --wplace-bg-ghost: rgba(255,255,255,0.06); --wplace-bg-whisper: rgba(255,255,255,0.05); - + /* Border variants */ --wplace-border-subtle: rgba(255,255,255,0.2); --wplace-border-faint: rgba(255,255,255,0.15); --wplace-border-ghost: rgba(255,255,255,0.1); --wplace-border-ultra-faint: rgba(255,255,255,0.05); - + /* Shadow variants */ --wplace-shadow-drag: 0 12px 40px rgba(0, 0, 0, 0.8), 0 0 0 2px rgba(255, 255, 255, 0.2); --wplace-shadow-notification: 0 4px 12px rgba(0, 0, 0, 0.3); --wplace-shadow-slider-thumb: 0 3px 6px rgba(0,0,0,0.3), 0 0 0 2px var(--wplace-icon-primary); --wplace-shadow-slider-hover: 0 4px 8px rgba(0,0,0,0.4), 0 0 0 3px var(--wplace-icon-primary); - + /* Animation colors */ --wplace-pulse-start: rgba(0, 255, 0, 0.7); --wplace-pulse-mid: rgba(0, 255, 0, 0); --wplace-pulse-end: rgba(0, 255, 0, 0); - + /* Slider colors - defaults for classic compatibility */ --wplace-slider-thumb-bg: white; --wplace-slider-track-bg: linear-gradient(to right, var(--wplace-icon-primary) 0%, var(--wplace-icon-secondary) 100%); @@ -217,8 +218,8 @@ #wplace-stats-container { position: fixed; top: 20px; - left: 330px; - width: 280px; + left: 310px; + width: 230px; max-height: calc(100vh - 40px); padding: 0; z-index: 9997; @@ -320,14 +321,14 @@ /* Sections */ .wplace-status-section { - margin-bottom: 12px; + margin-bottom: 6px; padding: 8px; /* Theme-specific styling applied via theme files */ } .wplace-section { - margin-bottom: 12px; - padding: 12px; + margin-bottom: 6px; + padding: 8px; /* Theme-specific styling applied via theme files */ } @@ -336,13 +337,30 @@ font-weight: 600; margin-bottom: 8px; display: flex; + justify-content: space-between; align-items: center; + cursor: pointer; gap: 6px; text-transform: uppercase; letter-spacing: 1px; /* Theme-specific styling applied via theme files */ } +.wplace-section-title i.arrow { + transition: transform 0.3s ease; +} + +.wplace-section.collapsed .wplace-controls, +.wplace-section.collapsed .wplace-cooldown-control { + max-height: 0; + opacity: 0; + pointer-events: none; +} + +.wplace-section.collapsed .wplace-section-title i.arrow { + transform: rotate(-90deg); +} + /* ========================= */ /* Controls and buttons */ /* ========================= */ @@ -1384,8 +1402,11 @@ /* Settings Footer */ .wplace-settings-footer { border-top: 1px solid var(--wplace-border-ghost); - padding-top: 20px; + padding: 20px; margin-top: 10px; + position: sticky; + bottom: 0; + background: var(--wplace-secondary); } .wplace-settings-apply-btn { diff --git a/themes/classic-light.css b/themes/classic-light.css index 2322dcc..daa45db 100644 --- a/themes/classic-light.css +++ b/themes/classic-light.css @@ -14,6 +14,7 @@ /* === UI PROPERTIES === */ --wplace-radius: 8px; + --wplace-btn-radius: 12px; --wplace-border-style: solid; --wplace-border-width: 1px; --wplace-border-color: #415a77; diff --git a/themes/classic.css b/themes/classic.css index 910f449..af84216 100644 --- a/themes/classic.css +++ b/themes/classic.css @@ -12,6 +12,7 @@ --wplace-warning: #ffaa00; --wplace-radius: 12px; + --wplace-btn-radius: 16px; --wplace-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1); --wplace-backdrop: blur(10px); --wplace-font: 'Segoe UI', Roboto, sans-serif; @@ -156,7 +157,7 @@ background: linear-gradient(135deg, #222222 0%, #4a4a4a 100%); border: 1px solid rgba(255,255,255,0.1); color: var(--wplace-text); - border-radius: var(--wplace-radius); + border-radius: var(--wplace-btn-radius); font-family: var(--wplace-font); } @@ -352,7 +353,6 @@ :root .wplace-section, .wplace-theme--classic .wplace-section { background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); } :root .wplace-stats, From cacff68f34e6ec90eb734b7616220867949bab82 Mon Sep 17 00:00:00 2001 From: kyuketski Date: Fri, 29 Aug 2025 21:48:17 +0300 Subject: [PATCH 03/24] refactor: enhance configuration, coordinate generation, and pixel painting * Enabled PAINTING\_SPEED\_ENABLED by default to improve painting performance. * Disabled desktop notifications by default to reduce interruptions. * Lowered overlay opacity from 0.6 to 0.2 for better visibility. * Commented out theme variable assignments for future cleanup or removal. * Added new state properties for coordinate generation (coordinateMode, coordinateDirection, etc.) to support advanced painting strategies. * Introduced dynamicSleep utility for more efficient sleep handling during cooldowns. * Added msToTimeText utility to format time durations in a human-readable way. * Implemented migration logic to version 2.2 for backward compatibility with saved progress data. * Enhanced saveProgress and loadProgress methods to include new coordinate generation settings. * Improved error handling in getCharges to provide default values when API calls fail. * Refactored notification messages for better clarity and consistency. * Added multiple coordinate generation modes including rows, columns, circle-out, circle-in, blocks, and shuffle-blocks. * Implemented UI controls for selecting generation modes, setting block sizes, toggling snake patterns, and choosing directions. * Enhanced settings panel with collapsible sections and improved styling for better user experience. * Added dynamic visibility logic to show/hide relevant controls based on the selected generation mode. * Integrated state management to persist user preferences for generation modes, directions, snake pattern toggling, and block dimensions. * Improved logging and user feedback with success alerts when changing coordinate-related settings. * Implemented functionality to track full charge progress using a dynamic timer. * Displays remaining time until full charge in the UI, updating every second. * Ensures charges are calculated accurately based on elapsed time and cooldown intervals. * Added support for pausing and resuming charge updates when the user interacts with the system. * Improved UI feedback for charge-related information, including current charges and full charge status. * Refactored the pixel batch processing logic to improve readability and maintainability. * Introduced a generateCoordinates function to handle coordinate generation for various modes (rows, columns, circle-out, etc.). * Added support for configurable block-based painting modes (blocks and shuffle-blocks) with customizable block dimensions. * Improved logic for handling region transitions during batch painting. * Updated delay calculations for batch processing to better align with painting speed settings. * Enhanced transparency and white pixel skipping logic with customizable thresholds. * Added detailed logging for debugging and monitoring purposes. * Ensured proper cleanup of intervals and state updates when stopping or pausing painting. * Refactored pixel painting logic to include smarter batch processing. * Added checks to skip already painted pixels, reducing redundant operations. * Improved batch size calculation and handling, ensuring batches are sent optimally. * Enhanced error handling for failed batches, preventing infinite loops in case of persistent failures. * Introduced dynamic sleep functionality during cooldown periods to optimize resource usage. * Updated UI interactions to reflect batch progress and cooldown states more accurately. * Added new coordinate mode and direction settings for greater flexibility in pixel painting. These changes improve the bot's configurability, performance, maintainability, and user experience while ensuring backward compatibility with existing progress data, accurate charge tracking, and efficient pixel painting across multiple modes. --- Auto-Image.js | 1180 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 924 insertions(+), 256 deletions(-) diff --git a/Auto-Image.js b/Auto-Image.js index 66de554..83bd5b0 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -16,19 +16,19 @@ MIN: 3, // Random range minimum MAX: 20, // Random range maximum }, - PAINTING_SPEED_ENABLED: false, // Off by default + PAINTING_SPEED_ENABLED: true, // On by default AUTO_CAPTCHA_ENABLED: true, // Turnstile generator enabled by default TOKEN_SOURCE: 'generator', // "generator", "manual", or "hybrid" - default to generator COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold // Desktop Notifications (defaults) NOTIFICATIONS: { - ENABLED: true, + ENABLED: false, ON_CHARGES_REACHED: true, ONLY_WHEN_UNFOCUSED: true, REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds }, OVERLAY: { - OPACITY_DEFAULT: 0.6, + OPACITY_DEFAULT: 0.2, BLUE_MARBLE_DEFAULT: false, ditheringEnabled: false, }, @@ -238,22 +238,22 @@ } 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)'); + // 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 = () => { @@ -470,12 +470,11 @@ const FALLBACK_TEXT = { en: { title: 'WPlace Auto-Image', - settings: 'Settings', - close: 'Close', - language: 'Language', - loadingError: 'Loading translations failed, using fallback', + toggleOverlay: 'Toggle Overlay', scanColors: 'Scan Colors', uploadImage: 'Upload Image', + resizeImage: 'Resize Image', + selectPosition: 'Select Position', startPainting: 'Start Painting', stopPainting: 'Stop Painting', progress: 'Progress', @@ -522,6 +521,8 @@ availableColors: [], activeColorPalette: [], // User-selected colors for conversion paintWhitePixels: true, // Default to ON + fullChargeData: null, + fullChargeInterval: null, paintTransparentPixels: false, // Default to OFF currentCharges: 0, maxCharges: 1, // Default max charges @@ -541,6 +542,7 @@ randomBatchMin: CONFIG.RANDOM_BATCH_RANGE.MIN, // Random range minimum randomBatchMax: CONFIG.RANDOM_BATCH_RANGE.MAX, // Random range maximum cooldownChargeThreshold: CONFIG.COOLDOWN_CHARGE_THRESHOLD, + chargesThresholdInterval: null, tokenSource: CONFIG.TOKEN_SOURCE, // "generator" or "manual" initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, @@ -555,6 +557,12 @@ resizeSettings: null, originalImage: null, resizeIgnoreMask: null, + // Coordinate generation settings + coordinateMode: 'rows', + coordinateDirection: 'bottom-left', + coordinateSnake: true, + blockWidth: 6, + blockHeight: 2, // Notification prefs and runtime bookkeeping notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, @@ -1159,6 +1167,14 @@ const Utils = { sleep: (ms) => new Promise((r) => setTimeout(r, ms)), + dynamicSleep: async function (getRemainingMsFn, interval = 500) { + let remaining = getRemainingMsFn(); + while (remaining > 0) { + await this.sleep(Math.min(interval, remaining)); + remaining = getRemainingMsFn(); + } + }, + waitForSelector: async (selector, interval = 200, timeout = 5000) => { const start = Date.now(); while (Date.now() - start < timeout) { @@ -1169,6 +1185,16 @@ return null; }, + msToTimeText(ms) { + const totalSeconds = Math.ceil(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; + if (minutes > 0) return `${minutes}m ${seconds}s`; + return `${seconds}s`; + }, // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup turnstileLoaded: false, _turnstileContainer: null, @@ -2059,6 +2085,35 @@ } }, + migrateProgressToV22: (data) => { + try { + const migrated = { ...data }; + migrated.version = '2.2'; + + // Добавляем новые поля с значениями по умолчанию + if (!migrated.state.coordinateMode) { + migrated.state.coordinateMode = 'rows'; + } + if (!migrated.state.coordinateDirection) { + migrated.state.coordinateDirection = 'bottom-left'; + } + if (!migrated.state.coordinateSnake) { + migrated.state.coordinateSnake = true; + } + if (!migrated.state.blockWidth) { + migrated.state.blockWidth = 6; + } + if (!migrated.state.blockHeight) { + migrated.state.blockHeight = 2; + } + + return migrated; + } catch (e) { + console.warn('Migration to v2.2 failed, using original data:', e); + return data; + } + }, + saveProgress: () => { try { // Pack painted map if available @@ -2080,7 +2135,7 @@ const progressData = { timestamp: Date.now(), - version: '2.1', + version: '2.2', state: { totalPixels: state.totalPixels, paintedPixels: state.paintedPixels, @@ -2090,6 +2145,12 @@ imageLoaded: state.imageLoaded, colorsChecked: state.colorsChecked, availableColors: state.availableColors, + // Добавляем настройки генерации координат + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, }, imageData: state.imageData ? { @@ -2117,13 +2178,18 @@ let data = JSON.parse(saved); const ver = data.version; let migrated = data; - if (ver === '2.1') { + + // Миграция для новых версий + if (ver === '2.2') { // already latest + } else if (ver === '2.1') { + migrated = Utils.migrateProgressToV22(data); } else if (ver === '2' || ver === '2.0') { - migrated = Utils.migrateProgressToV21(data); + migrated = Utils.migrateProgressToV22(Utils.migrateProgressToV21(data)); } else { - migrated = Utils.migrateProgressToV21(data); + migrated = Utils.migrateProgressToV22(Utils.migrateProgressToV21(data)); } + if (migrated && migrated !== data) { try { localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); @@ -2144,6 +2210,12 @@ state.paintedMap = null; state._lastSavePixelCount = 0; state._lastSaveTime = 0; + // Сбрасываем настройки генерации координат к значениям по умолчанию + state.coordinateMode = 'rows'; + state.coordinateDirection = 'bottom-left'; + state.coordinateSnake = true; + state.blockWidth = 6; + state.blockHeight = 2; console.log('📋 Progress and painted map cleared'); return true; } catch (error) { @@ -2156,6 +2228,23 @@ try { Object.assign(state, savedData.state); + // Восстанавливаем настройки генерации координат + if (savedData.state.coordinateMode) { + state.coordinateMode = savedData.state.coordinateMode; + } + if (savedData.state.coordinateDirection) { + state.coordinateDirection = savedData.state.coordinateDirection; + } + if (savedData.state.coordinateSnake !== undefined) { + state.coordinateSnake = savedData.state.coordinateSnake; + } + if (savedData.state.blockWidth) { + state.blockWidth = savedData.state.blockWidth; + } + if (savedData.state.blockHeight) { + state.blockHeight = savedData.state.blockHeight; + } + if (savedData.imageData) { state.imageData = { ...savedData.imageData, @@ -2241,7 +2330,10 @@ paintedMapPacked: paintedMapPacked, }; - const filename = `wplace-bot-progress-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; + 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) { @@ -2387,7 +2479,11 @@ try { await ensureToken(); if (!turnstileToken) return 'token_error'; - const payload = { coords: [pixelX, pixelY], colors: [color], t: turnstileToken }; + 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' }, @@ -2411,23 +2507,32 @@ }, async getCharges() { + const defaultResult = { + charges: 0, + max: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + }; + try { const res = await fetch('https://backend.wplace.live/me', { credentials: 'include', }); + + if (!res.ok) { + console.error(`Failed to get charges: HTTP ${res.status}`); + return defaultResult; + } + const data = await res.json(); + return { - charges: data.charges?.count || 0, - max: data.charges?.max || 1, - cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, + charges: data.charges?.count ?? 0, + max: data.charges?.max ?? 1, + cooldown: data.charges?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, }; } catch (e) { console.error('Failed to get charges:', e); - return { - charges: 0, - max: 1, - cooldown: CONFIG.COOLDOWN_DEFAULT, - }; + return defaultResult; } }, }; @@ -2539,7 +2644,9 @@ 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}`; + 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; @@ -2706,7 +2813,9 @@ if (isAvailable) availableCount++; - const colorItem = Utils.createElement('div', { className: 'wplace-color-item' }); + const colorItem = Utils.createElement('div', { + className: 'wplace-color-item', + }); const swatch = Utils.createElement('button', { className: `wplace-color-swatch ${!isAvailable ? 'unavailable' : ''}`, title: `${name} (ID: ${id})${!isAvailable ? ' (Unavailable)' : ''}`, @@ -2799,7 +2908,15 @@ 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}` + `🔍 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) { @@ -2808,7 +2925,9 @@ return token; } else { throw new Error( - `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify(token)}, Length: ${token?.length || 0}` + `Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify( + token + )}, Length: ${token?.length || 0}` ); } } catch (error) { @@ -2868,14 +2987,26 @@ const centerY = Math.round(rect.top + rect.height / 2); canvas.dispatchEvent( - new MouseEvent('mousemove', { clientX: centerX, clientY: centerY, bubbles: true }) + new MouseEvent('mousemove', { + clientX: centerX, + clientY: centerY, + bubbles: true, + }) ); canvas.dispatchEvent( - new KeyboardEvent('keydown', { key: ' ', code: 'Space', bubbles: true }) + new KeyboardEvent('keydown', { + key: ' ', + code: 'Space', + bubbles: true, + }) ); await Utils.sleep(50); canvas.dispatchEvent( - new KeyboardEvent('keyup', { key: ' ', code: 'Space', bubbles: true }) + new KeyboardEvent('keyup', { + key: ' ', + code: 'Space', + bubbles: true, + }) ); await Utils.sleep(500); @@ -2950,7 +3081,7 @@ // Link external CSS files const cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; - cssLink.href = 'https://staninna.github.io/WPlace-AutoBOT/decoupled-css/auto-image-styles.css'; // TODO: Before merge change to https://raw.githubusercontent.com/Wplace-AutoBot/WPlace-AutoBOT/refs/heads/main/auto-image-styles.css + cssLink.href = 'https://skalsech.github.io/WPlace-AutoBOT/my_main/auto-image-styles.css'; cssLink.setAttribute('data-wplace-theme', 'true'); document.head.appendChild(cssLink); @@ -3098,7 +3229,9 @@
-
${Utils.t('initMessage')}
+
${Utils.t( + 'initMessage' + )}
@@ -3187,6 +3320,7 @@
${Math.round(state.overlayOpacity * 100)}%
@@ -3305,6 +3441,121 @@ ${Utils.t('enablePaintingSpeedLimit')}
+ + +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +
+ + + +
@@ -3378,18 +3629,17 @@
+
-
-