diff --git a/.gitignore b/.gitignore index b512c09..7ec2b31 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,18 @@ -node_modules \ No newline at end of file +# Node modules +node_modules/ + +# NPM/Yarn lock +package-lock.json +package.json + +# Prettier +.prettierrc.json + +# ESLint +eslint.config.cjs + +# Staged changes +staged_changes.diff + +# IDE (WebStorm/IntelliJ IDEA) +.idea/ 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..6199f25 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'); @@ -283,7 +298,7 @@ 70% { box-shadow: 0 0 0 10px rgba(0, 255, 0, 0); } 100% { box-shadow: 0 0 0 0 rgba(0, 255, 0, 0); } } - @keyframes slideIn { + @keyframes slide-in { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @@ -300,7 +315,7 @@ z-index: 9999; font-family: 'Segoe UI', Roboto, sans-serif; color: ${CONFIG.THEME.text}; - animation: slideIn 0.4s ease-out; + animation: slide-in 0.4s ease-out; overflow: hidden; } .wplace-header { @@ -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; }); @@ -568,7 +594,7 @@ statusText.className = `wplace-status status-${type}`; statusText.style.animation = 'none'; void statusText.offsetWidth; - statusText.style.animation = 'slideIn 0.3s ease-out'; + statusText.style.animation = 'slide-in 0.3s ease-out'; } }; @@ -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..81a4264 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,32 +7,31 @@ 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 + 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 + 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: 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, - }, - // --- START: Color data from colour-converter.js --- + }, // --- START: Color data from colour-converter.js --- // New color structure with proper ID mapping COLOR_MAP: { 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, @@ -97,9 +97,8 @@ 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 } - }, - // --- END: Color data --- + 63: { id: 0, name: 'Transparent', rgb: null }, + }, // --- END: Color data --- // Optimized CSS Classes for reuse CSS_CLASSES: { BUTTON_PRIMARY: ` @@ -122,151 +121,163 @@ 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, + 'pixel-blink': 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, + 'pixel-blink': 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, - pixelBlink: true, + 'pixel-blink': true, }, }, }, - currentTheme: "Classic Autobot", - } + currentTheme: 'Classic Autobot', + PAINT_UNAVAILABLE: true, + COORDINATE_MODE: 'rows', + COORDINATE_DIRECTION: 'bottom-left', + COORDINATE_SNAKE: true, + COORDINATE_BLOCK_WIDTH: 6, + COORDINATE_BLOCK_HEIGHT: 2, + }; - 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 +286,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) => { @@ -284,55 +308,70 @@ function applyTheme() { } // Load translations from CDN - const url = `https://staninna.github.io/WPlace-AutoBOT/decoupled-translations/lang/${language}.json`; + const url = `https://skalsech.github.io/WPlace-AutoBOT/feat-external-lang/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 +382,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 +400,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 +430,7 @@ function applyTheme() { `; warning.textContent = message; document.body.appendChild(warning); - + // Auto-remove after 8 seconds setTimeout(() => { if (warning.parentNode) { @@ -398,79 +439,78 @@ 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', + toggleOverlay: 'Toggle Overlay', + scanColors: 'Scan Colors', + uploadImage: 'Upload Image', + resizeImage: 'Resize Image', + selectPosition: 'Select Position', + 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); @@ -487,8 +527,11 @@ function applyTheme() { availableColors: [], activeColorPalette: [], // User-selected colors for conversion paintWhitePixels: true, // Default to ON + fullChargeData: null, + fullChargeInterval: null, paintTransparentPixels: false, // Default to OFF - currentCharges: 0, + displayCharges: 0, + preciseCurrentCharges: 0, maxCharges: 1, // Default max charges cooldown: CONFIG.COOLDOWN_DEFAULT, imageData: null, @@ -500,41 +543,48 @@ 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 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, 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, + paintUnavailablePixels: CONFIG.PAINT_UNAVAILABLE, + // Coordinate generation settings + coordinateMode: CONFIG.COORDINATE_MODE, + coordinateDirection: CONFIG.COORDINATE_DIRECTION, + coordinateSnake: CONFIG.COORDINATE_SNAKE, + blockWidth: CONFIG.COORDINATE_BLOCK_WIDTH, + blockHeight: CONFIG.COORDINATE_BLOCK_HEIGHT, + 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 +608,20 @@ 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,18 +688,20 @@ 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; - const endPixelX = startPixelX + imageWidth; - const endPixelY = startPixelY + imageHeight; - - const startTileX = startRegionX + Math.floor(startPixelX / this.tileSize); - const startTileY = startRegionY + Math.floor(startPixelY / this.tileSize); - const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize); - const endTileY = startRegionY + Math.floor(endPixelY / this.tileSize); + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + imageWidth, + imageHeight, + this.tileSize + ); const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1); console.log(`🔄 Processing ${totalTiles} overlay tiles...`); @@ -661,26 +719,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 +789,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 +841,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 +858,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,17 +867,22 @@ 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) async getTilePixelColor(tileX, tileY, pixelX, pixelY) { const tileKey = `${tileX},${tileY}`; - // Prefer cached ImageData if available + const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + // 1. Prefer cached ImageData if available const cached = this.originalTilesData.get(tileKey); if (cached && cached.data && cached.w > 0 && cached.h > 0) { const x = Math.max(0, Math.min(cached.w - 1, pixelX)); @@ -801,55 +890,81 @@ function applyTheme() { const idx = (y * cached.w + x) * 4; const d = cached.data; const a = d[idx + 3]; - const alphaThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + 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('OverlayManager: pixel transparent (cached), skipping', tileKey, x, y, a); return null; } return [d[idx], d[idx + 1], d[idx + 2], a]; } - // Fallback: draw stored bitmap to canvas and read single pixel - const bitmap = this.originalTiles.get(tileKey); - if (!bitmap) return null; - - try { - let canvas, ctx; - if (typeof OffscreenCanvas !== 'undefined') { - canvas = new OffscreenCanvas(bitmap.width, bitmap.height); - ctx = canvas.getContext('2d'); - } else { - canvas = document.createElement('canvas'); - canvas.width = bitmap.width; - canvas.height = bitmap.height; - ctx = canvas.getContext('2d'); + // 2. Fallback: use bitmap, with retry + const maxRetries = 3; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + const bitmap = this.originalTiles.get(tileKey); + if (!bitmap) { + if (attempt === maxRetries) { + console.warn('OverlayManager: no bitmap for', tileKey, 'after', maxRetries, 'attempts'); + } else { + await Utils.sleep(50 * attempt); // exponential delay + } + continue; } - ctx.imageSmoothingEnabled = false; - ctx.drawImage(bitmap, 0, 0); - const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); - const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); - const data = ctx.getImageData(x, y, 1, 1).data; - const a = data[3]; - 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); - return null; + try { + let canvas, ctx; + if (typeof OffscreenCanvas !== 'undefined') { + canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + ctx = canvas.getContext('2d'); + } else { + canvas = document.createElement('canvas'); + canvas.width = bitmap.width; + canvas.height = bitmap.height; + ctx = canvas.getContext('2d'); + } + ctx.imageSmoothingEnabled = false; + ctx.drawImage(bitmap, 0, 0); + + const x = Math.max(0, Math.min(bitmap.width - 1, pixelX)); + const y = Math.max(0, Math.min(bitmap.height - 1, pixelY)); + const data = ctx.getImageData(x, y, 1, 1).data; + const a = data[3]; + + if (!state.paintTransparentPixels && a < alphaThresh) { + if (window._overlayDebug) + console.debug('OverlayManager: pixel transparent (fallback)', tileKey, x, y, a); + return null; + } + + return [data[0], data[1], data[2], a]; + } catch (e) { + console.warn('OverlayManager: failed to read pixel (attempt', attempt, ')', tileKey, e); + if (attempt < maxRetries) { + await Utils.sleep(50 * attempt); + } else { + console.error( + 'OverlayManager: failed to read pixel after', + maxRetries, + 'attempts', + tileKey + ); + } } - return [data[0], data[1], data[2], a]; - } catch (e) { - console.warn('OverlayManager.getTilePixelColor failed for', tileKey, pixelX, pixelY, e); - return null; } + + // 3. If everything fails — you can return null or [0,0,0,0] + // Prefer null — to avoid misleading + return null; } async _compositeTileOptimized(originalBlob, overlayBitmap) { 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,44 +977,107 @@ 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 }); } + + /** + * Wait until all required tiles are loaded and cached + * @param {number} startRegionX + * @param {number} startRegionY + * @param {number} pixelWidth + * @param {number} pixelHeight + * @param {number} startPixelX + * @param {number} startPixelY + * @param {number} timeoutMs + * @returns {Promise} true if tiles are ready + */ + async waitForTiles( + startRegionX, + startRegionY, + pixelWidth, + pixelHeight, + startPixelX = 0, + startPixelY = 0, + timeoutMs = 10000 + ) { + const { startTileX, startTileY, endTileX, endTileY } = Utils.calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + pixelWidth, + pixelHeight, + this.tileSize + ); + + const requiredTiles = []; + for (let ty = startTileY; ty <= endTileY; ty++) { + for (let tx = startTileX; tx <= endTileX; tx++) { + requiredTiles.push(`${tx},${ty}`); + } + } + + if (requiredTiles.length === 0) return true; + + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + if (state.stopFlag) { + console.log('waitForTiles: stopped by user'); + return false; + } + + const missing = requiredTiles.filter((key) => !this.originalTiles.has(key)); + if (missing.length === 0) { + console.log(`✅ All ${requiredTiles.length} required tiles are loaded`); + return true; + } + + await Utils.sleep(100); + } + + console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required, + ${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`); + return false; + } } const overlayManager = new OverlayManager(); // Optimized Turnstile token handling with improved caching and retry logic - let turnstileToken = null - let tokenExpiryTime = 0 - let tokenGenerationInProgress = false - let _resolveToken = null - let tokenPromise = new Promise((resolve) => { _resolveToken = resolve }) - let retryCount = 0 - const MAX_RETRIES = 10 - const MAX_BATCH_RETRIES = 10 // Maximum attempts for batch sending - const TOKEN_LIFETIME = 240000 // 4 minutes (tokens typically last 5 min, use 4 for safety) + 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 +1091,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; @@ -945,26 +1123,56 @@ function applyTheme() { } async function handleCaptchaWithRetry() { - const startTime = Date.now(); - try { - const sitekey = Utils.detectSitekey(); - console.log("🔑 Generating Turnstile token for sitekey:", sitekey); + const startTime = performance.now(); - if (typeof window !== "undefined" && window.navigator) { - console.log("🧭 UA:", window.navigator.userAgent, "Platform:", window.navigator.platform); + try { + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); } - const token = await Utils.generatePaintToken(sitekey); - if (token && token.length > 20) { - const elapsed = Math.round(Date.now() - startTime); + console.log('🔑 Using sitekey:', sitekey); + + if (typeof window !== 'undefined' && window.navigator) { + console.log( + '🧭 UA:', + window.navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + window.navigator.platform + ); + } + + let token = null; + + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated Turnstile token'); + token = preGeneratedToken; + } else { + if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous session)'); + token = turnstileToken; + } else { + console.log('🔐 Generating new token with executeTurnstile...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) setTurnstileToken(token); + } + } + + if (token && typeof token === 'string' && token.length > 20) { + const elapsed = Math.round(performance.now() - startTime); console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`); return token; } else { - throw new Error("Invalid or empty token received"); + throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`); } } catch (error) { - const elapsed = Math.round(Date.now() - startTime); - console.log(`❌ Turnstile token generation failed after ${elapsed}ms:`, error); + const elapsed = Math.round(performance.now() - startTime); + console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error); throw error; } } @@ -972,7 +1180,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 +1208,30 @@ 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); + // 📊 Debug log + console.log( + `🔍✅ Turnstile Token Captured - Type: ${typeof payload.t}, Value: ${ + payload.t + ? typeof payload.t === 'string' + ? payload.t.length > 50 + ? payload.t.substring(0, 50) + '...' + : payload.t + : JSON.stringify(payload.t) + : 'null/undefined' + }, Length: ${payload.t?.length || 0}` + ); window.postMessage({ source: 'turnstile-capture', token: payload.t }, '*'); } - } catch (_) { /* ignore */ } + } catch (_) { + /* ignore */ + } } const contentType = response.headers.get('content-type') || ''; @@ -1021,19 +1242,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 +1277,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'; } } @@ -1074,6 +1300,15 @@ function applyTheme() { const Utils = { sleep: (ms) => new Promise((r) => setTimeout(r, ms)), + dynamicSleep: async function (tickAndGetRemainingMs) { + let remaining = Math.max(0, await tickAndGetRemainingMs()); + while (remaining > 0) { + const interval = remaining > 5000 ? 2000 : remaining > 1000 ? 500 : 100; + await this.sleep(Math.min(interval, remaining)); + remaining = Math.max(0, await tickAndGetRemainingMs()); + } + }, + waitForSelector: async (selector, interval = 200, timeout = 5000) => { const start = Date.now(); while (Date.now() - start < timeout) { @@ -1084,7 +1319,47 @@ function applyTheme() { return null; }, - // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup + 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`; + }, + + /** + * Calculate the range of tile coordinates (in region space) that cover a given image area. + * @param {number} startRegionX - Base region X + * @param {number} startRegionY - Base region Y + * @param {number} startPixelX - Starting pixel X within the region grid + * @param {number} startPixelY - Starting pixel Y within the region grid + * @param {number} width - Image width in pixels + * @param {number} height - Image height in pixels + * @param {number} tileSize - Size of a tile (default 1000) + * @returns {{ startTileX: number, startTileY: number, endTileX: number, endTileY: number }} + */ + calculateTileRange( + startRegionX, + startRegionY, + startPixelX, + startPixelY, + width, + height, + tileSize = 1000 + ) { + const endPixelX = startPixelX + width; + const endPixelY = startPixelY + height; + + return { + startTileX: startRegionX + Math.floor(startPixelX / tileSize), + startTileY: startRegionY + Math.floor(startPixelY / tileSize), + endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize), + endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize), + }; + }, // Turnstile Generator Integration - Optimized with widget reuse and proper cleanup turnstileLoaded: false, _turnstileContainer: null, _turnstileOverlay: null, @@ -1097,10 +1372,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 +1390,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 +1437,7 @@ 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.className = 'wplace-turnstile-title'; const host = document.createElement('div'); host.id = 'turnstile-overlay-host'; @@ -1184,17 +1463,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 +1485,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 +1494,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 +1524,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 +1537,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 +1553,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 +1579,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 +1588,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 +1601,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'); @@ -1341,10 +1626,6 @@ function applyTheme() { }); }, - async generatePaintToken(sitekey) { - return this.executeTurnstile(sitekey, 'paint'); - }, - // Cleanup method for when the script is disabled/reloaded cleanupTurnstile() { if (this._turnstileWidgetId && window.turnstile?.remove) { @@ -1354,26 +1635,32 @@ 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; this._lastSitekey = null; }, - detectSitekey(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { + async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') { // Cache sitekey to avoid repeated DOM queries if (this._cachedSitekey) { - console.log("🔍 Using cached sitekey:", this._cachedSitekey); - return this._cachedSitekey; + console.log('🔍 Using cached sitekey:', this._cachedSitekey); + + return isTokenValid() + ? { + sitekey: this._cachedSitekey, + token: turnstileToken, + } + : { sitekey: this._cachedSitekey, token: null }; } // List of potential sitekeys to try @@ -1382,112 +1669,141 @@ function applyTheme() { '0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey '0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey ]; + const trySitekey = async (sitekey, source) => { + if (!sitekey || sitekey.length < 10) return null; + + console.log(`🔍 Testing sitekey from ${source}:`, sitekey); + const token = await this.executeTurnstile(sitekey); + + if (token && token.length >= 20) { + console.log(`✅ Valid token generated from ${source} sitekey`); + setTurnstileToken(token); + this._cachedSitekey = sitekey; + return { sitekey, token }; + } else { + console.log(`❌ Failed to get token from ${source} sitekey`); + return null; + } + }; try { - // Try to find sitekey in data attributes + // 1️⃣ data-sitekey attribute const sitekeySel = document.querySelector('[data-sitekey]'); if (sitekeySel) { const sitekey = sitekeySel.getAttribute('data-sitekey'); - if (sitekey && sitekey.length > 10) { - this._cachedSitekey = sitekey; - console.log("🔍 Sitekey detected from data attribute:", sitekey); - return sitekey; + const result = await trySitekey(sitekey, 'data attribute'); + if (result) { + return result; } } - // Try turnstile element + // 2️⃣ Turnstile element 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); - return this._cachedSitekey; + if (turnstileEl?.dataset?.sitekey) { + const sitekey = turnstileEl.dataset.sitekey; + const result = await trySitekey(sitekey, 'turnstile element'); + if (result) { + return result; + } } - // Try to find sitekey in meta tags - const metaTags = document.querySelectorAll('meta[name*="turnstile"], meta[property*="turnstile"]'); + // 3️⃣ Meta tags + 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); - return this._cachedSitekey; + const result = await trySitekey(content, 'meta tag'); + if (result) { + return result; } } - // Try global variable - 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); - return this._cachedSitekey; + // 4️⃣ Global variable + if (window.__TURNSTILE_SITEKEY) { + const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable'); + if (result) { + return result; + } } - // Try script tags for inline sitekey + // 5️⃣ Script tags const scripts = document.querySelectorAll('script'); for (const script of scripts) { const content = script.textContent || script.innerHTML; - 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); - return this._cachedSitekey; + const match = content.match( + /(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i + ); + if (match && match[1]) { + const extracted = match[1].replace(/['"]/g, ''); + const result = await trySitekey(extracted, 'script content'); + if (result) { + return result; + } } } - // If no sitekey found through detection, try the known working sitekeys - console.log("🔍 No sitekey detected, trying known working sitekeys..."); + // 6️⃣ Known potential sitekeys + console.log('🔍 Testing known potential sitekeys...'); for (const testSitekey of potentialSitekeys) { - console.log("🔍 Trying sitekey:", testSitekey); - this._cachedSitekey = testSitekey; - return testSitekey; + const result = await trySitekey(testSitekey, 'known list'); + if (result) { + return result; + } } - } catch (error) { - console.warn('Error detecting sitekey:', error); + console.warn('⚠️ Error during sitekey detection:', error); } - - console.log("🔍 Using fallback sitekey:", fallback); - this._cachedSitekey = fallback; - return fallback; + + // 7️⃣ Fallback + console.log('🔧 Trying fallback sitekey:', fallback); + const fallbackResult = await trySitekey(fallback, 'fallback'); + if (fallbackResult) { + return fallbackResult; + } + + console.error('❌ No working sitekey or token found.'); + return { sitekey: null, token: null }; }, createElement: (tag, props = {}, children = []) => { - const element = document.createElement(tag) + 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 +1843,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 = 'slide-down 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 +1883,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 +1907,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 +1919,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]; @@ -1641,129 +1964,257 @@ function applyTheme() { return r >= wt && g >= wt && b >= wt; }, + resolveColor(targetRgb, availableColors, exactMatch = false) { + if (!availableColors || availableColors.length === 0) { + return { + id: null, + rgb: targetRgb, + }; + } + + const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${state.colorMatchingAlgorithm}|${ + state.enableChromaPenalty ? 'c' : 'nc' + }|${state.chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`; + + if (colorCache.has(cacheKey)) return colorCache.get(cacheKey); + + // Check for an exact color match in availableColors. + // If found, return the matched color with its ID. + // If not found, return the target color with null ID. + // Cache the result for future lookups. + if (exactMatch) { + const match = availableColors.find( + (c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2] + ); + const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb }; + colorCache.set(cacheKey, result); + return result; + } + + // check for white using threshold + const whiteThreshold = state.customWhiteThreshold || CONFIG.WHITE_THRESHOLD; + if ( + targetRgb[0] >= whiteThreshold && + targetRgb[1] >= whiteThreshold && + targetRgb[2] >= whiteThreshold + ) { + const whiteEntry = availableColors.find( + (c) => + c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold + ); + if (whiteEntry) { + const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] }; + colorCache.set(cacheKey, result); + return result; + } + } + + // find nearest color + let bestId = availableColors[0].id; + let bestRgb = [...availableColors[0].rgb]; + let bestScore = Infinity; + + if (state.colorMatchingAlgorithm === 'legacy') { + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const rmean = (r + targetRgb[0]) / 2; + const rdiff = r - targetRgb[0]; + const gdiff = g - targetRgb[1]; + const bdiff = b - targetRgb[2]; + const dist = Math.sqrt( + (((512 + rmean) * rdiff * rdiff) >> 8) + + 4 * gdiff * gdiff + + (((767 - rmean) * bdiff * bdiff) >> 8) + ); + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } else { + const [Lt, at, bt] = Utils._lab(targetRgb[0], targetRgb[1], targetRgb[2]); + const targetChroma = Math.sqrt(at * at + bt * bt); + const penaltyWeight = state.enableChromaPenalty ? state.chromaPenaltyWeight || 0.15 : 0; + + for (let i = 0; i < availableColors.length; i++) { + const c = availableColors[i]; + const [r, g, b] = c.rgb; + const [L2, a2, b2] = Utils._lab(r, g, b); + const dL = Lt - L2, + da = at - a2, + db = bt - b2; + let dist = dL * dL + da * da + db * db; + + if (penaltyWeight > 0 && targetChroma > 20) { + const candChroma = Math.sqrt(a2 * a2 + b2 * b2); + if (candChroma < targetChroma) { + const cd = targetChroma - candChroma; + dist += cd * cd * penaltyWeight; + } + } + + if (dist < bestScore) { + bestScore = dist; + bestId = c.id; + bestRgb = [...c.rgb]; + if (dist === 0) break; + } + } + } + + const result = { id: bestId, rgb: bestRgb }; + colorCache.set(cacheKey, result); + + // limit the size of the cache + if (colorCache.size > 15000) { + const firstKey = colorCache.keys().next().value; + colorCache.delete(firstKey); + } + + return result; + }, + createImageUploader: () => new Promise((resolve) => { - const input = document.createElement("input") - input.type = "file" - input.accept = "image/png,image/jpeg" + 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('.tooltip button[id^="color-"]'); + if (colorElements.length === 0) { + console.log('❌ No color elements found on page'); + return null; + } // Separate available and unavailable colors - const availableColors = [] - const unavailableColors = [] + 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); + if (!rgbStr || rgbStr.length < 3) { + console.warn(`Skipping color element ${el.id} — cannot parse RGB`); + return; + } + const rgb = rgbStr.map(Number); // Find color name from COLOR_MAP - const colorInfo = Object.values(CONFIG.COLOR_MAP).find(color => color.id === id) - const name = colorInfo ? colorInfo.name : `Unknown Color ${id}` + const 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 +2226,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 +2236,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 +2250,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 +2267,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 +2305,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 +2326,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 +2344,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 +2372,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,112 +2398,201 @@ function applyTheme() { } }, - saveProgress: () => { + migrateProgressToV22: (data) => { 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); - if (data) { - paintedMapPacked = { - width: state.imageData.width, - height: state.imageData.height, - data: data - }; - } + const migrated = { ...data }; + migrated.version = '2.2'; + + // Add new fields with default values + if (!migrated.state.coordinateMode) { + migrated.state.coordinateMode = CONFIG.COORDINATE_MODE; + } + if (!migrated.state.coordinateDirection) { + migrated.state.coordinateDirection = CONFIG.COORDINATE_DIRECTION; + } + if (!migrated.state.coordinateSnake) { + migrated.state.coordinateSnake = CONFIG.COORDINATE_SNAKE; + } + if (!migrated.state.blockWidth) { + migrated.state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH; + } + if (!migrated.state.blockHeight) { + migrated.state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT; } - const progressData = { - timestamp: Date.now(), - version: "2.1", - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { + return migrated; + } catch (e) { + console.warn('Migration to v2.2 failed, using original data:', e); + return data; + } + }, + + buildPaintedMapPacked() { + if (state.paintedMap && state.imageData) { + const data = Utils.packPaintedMapToBase64( + state.paintedMap, + state.imageData.width, + state.imageData.height + ); + if (data) { + return { + width: state.imageData.width, + height: state.imageData.height, + data: data, + }; + } + } + return null; + }, + + buildProgressData() { + return { + timestamp: Date.now(), + version: '2.2', + state: { + totalPixels: state.totalPixels, + paintedPixels: state.paintedPixels, + lastPosition: state.lastPosition, + startPosition: state.startPosition, + region: state.region, + imageLoaded: state.imageLoaded, + colorsChecked: state.colorsChecked, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, + availableColors: state.availableColors, + }, + imageData: state.imageData + ? { width: state.imageData.width, height: state.imageData.height, pixels: Array.from(state.imageData.pixels), totalPixels: state.imageData.totalPixels, } - : null, - paintedMapPacked: paintedMapPacked, - } + : null, + paintedMapPacked: Utils.buildPaintedMapPacked(), + }; + }, - localStorage.setItem("wplace-bot-progress", JSON.stringify(progressData)) - return true + migrateProgress(saved) { + if (!saved) return null; + + let data = saved; + const ver = data.version; + + // If version is missing or ≤ 1.x → first migrate to v2 + if (!ver || ver === '1' || ver === '1.0' || ver === '1.1') { + data = Utils.migrateProgressToV2(data); + } + + // If still older than v2.1 → migrate to 2.1 + if (data.version === '2' || data.version === '2.0') { + data = Utils.migrateProgressToV21(data); + } + + // If still older than v2.2 → migrate to 2.2 + if (data.version === '2.1') { + data = Utils.migrateProgressToV22(data); + } + + // Now data is guaranteed to be the latest version + return data; + }, + + saveProgress: () => { + try { + const progressData = Utils.buildProgressData(state); + + localStorage.setItem('wplace-bot-progress', JSON.stringify(progressData)); + return true; } catch (error) { - console.error("Error saving progress:", error) - return false + 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; - let migrated = data; - if (ver === '2.1') { - // already latest - } else if (ver === '2' || ver === '2.0') { - migrated = Utils.migrateProgressToV21(data); - } else { - migrated = Utils.migrateProgressToV21(data); - } + const migrated = Utils.migrateProgress(data); + if (migrated && migrated !== data) { - try { localStorage.setItem("wplace-bot-progress", JSON.stringify(migrated)); } catch {} - data = migrated; + try { + localStorage.setItem('wplace-bot-progress', JSON.stringify(migrated)); + } catch {} } - return data; + return migrated; } 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 + // Reset coordinate generation settings to their default values + state.coordinateMode = CONFIG.COORDINATE_MODE; + state.coordinateDirection = CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = CONFIG.COORDINATE_SNAKE; + state.blockWidth = CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = CONFIG.COORDINATE_BLOCK_HEIGHT; + console.log('📋 Progress and painted map cleared'); + return true; } catch (error) { - console.error("Error clearing progress:", error) - return false + console.error('Error clearing progress:', error); + return false; } }, restoreProgress: (savedData) => { try { - Object.assign(state, savedData.state) + Object.assign(state, savedData.state); + + // Restore coordinate generation settings + if (savedData.state.coordinateMode) { + state.coordinateMode = savedData.state.coordinateMode; + } + if (savedData.state.coordinateDirection) { + state.coordinateDirection = savedData.state.coordinateDirection; + } + if (savedData.state.coordinateSnake !== undefined) { + state.coordinateSnake = savedData.state.coordinateSnake; + } + if (savedData.state.blockWidth) { + state.blockWidth = savedData.state.blockWidth; + } + if (savedData.state.blockHeight) { + state.blockHeight = savedData.state.blockHeight; + } if (savedData.imageData) { state.imageData = { ...savedData.imageData, pixels: new Uint8ClampedArray(savedData.imageData.pixels), - } + }; try { const canvas = document.createElement('canvas'); canvas.width = state.imageData.width; canvas.height = state.imageData.height; const ctx = canvas.getContext('2d'); - const imageData = new ImageData(state.imageData.pixels, state.imageData.width, state.imageData.height); + 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,83 +2606,44 @@ 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); - if (data) { - paintedMapPacked = { - width: state.imageData.width, - height: state.imageData.height, - data: data - }; - } - } - - const progressData = { - timestamp: Date.now(), - version: "2.1", - state: { - totalPixels: state.totalPixels, - paintedPixels: state.paintedPixels, - lastPosition: state.lastPosition, - startPosition: state.startPosition, - region: state.region, - imageLoaded: state.imageLoaded, - colorsChecked: state.colorsChecked, - availableColors: state.availableColors, - }, - imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, - 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 progressData = Utils.buildProgressData(); + const filename = `wplace-bot-progress-${new Date() + .toISOString() + .slice(0, 19) + .replace(/:/g, '-')}.json`; + Utils.createFileDownloader(JSON.stringify(progressData, null, 2), filename); + return true; } catch (error) { - console.error("Error saving to file:", error) - return false + 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; - if (ver === '2.1') { - } else if (ver === '2' || ver === '2.0') { - migrated = Utils.migrateProgressToV21(data) || data; - } else { - migrated = Utils.migrateProgressToV21(data) || data; - } - const success = Utils.restoreProgress(migrated) - return success + const migrated = Utils.migrateProgress(data); + + 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 +2685,83 @@ function applyTheme() { return false; } }, - } + + updateCoordinateUI({ mode, directionControls, snakeControls, blockControls }) { + const isLinear = mode === 'rows' || mode === 'columns'; + const isBlock = mode === 'blocks' || mode === 'shuffle-blocks'; + + if (directionControls) directionControls.style.display = isLinear ? 'block' : 'none'; + if (snakeControls) snakeControls.style.display = isLinear ? 'block' : 'none'; + if (blockControls) blockControls.style.display = isBlock ? 'block' : 'none'; + }, + }; // IMAGE PROCESSOR CLASS class ImageProcessor { constructor(imageSrc) { - this.imageSrc = imageSrc - this.img = null - this.canvas = null - this.ctx = null + 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 +2769,65 @@ 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() { + const defaultResult = { + charges: 0, + max: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + }; + try { - const res = await fetch("https://backend.wplace.live/me", { - credentials: "include", - }) - const data = await res.json() - return { - charges: data.charges?.count || 0, - max: data.charges?.max || 1, - cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, + 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?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, + }; } catch (e) { - console.error("Failed to get charges:", e) - return { - charges: 0, - max: 1, - cooldown: CONFIG.COOLDOWN_DEFAULT, - } + console.error('Failed to get charges:', e); + return defaultResult; } }, - } + }; // Desktop Notification Manager const NotificationManager = { @@ -2289,14 +2835,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 +2851,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,25 +2872,29 @@ function applyTheme() { return true; } catch { // Graceful fallback - Utils.showAlert(body, "info"); + Utils.showAlert(body, 'info'); return false; } }, resetEdgeTracking() { - state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold; + state._lastChargesBelow = state.displayCharges < state.cooldownChargeThreshold; state._lastChargesNotifyAt = 0; }, maybeNotifyChargesReached(force = false) { if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; - const reached = state.currentCharges >= state.cooldownChargeThreshold; + const reached = state.displayCharges >= state.cooldownChargeThreshold; const now = Date.now(); const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; if (reached) { const shouldEdge = state._lastChargesBelow || force; const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; if (shouldEdge || shouldRepeat) { - const msg = Utils.t("chargesReadyMessage", { current: 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: state.displayCharges, + max: state.maxCharges, + threshold: state.cooldownChargeThreshold, + }); + this.notify(Utils.t('chargesReadyNotification'), msg, 'wplace-notify-charges'); state._lastChargesNotifyAt = now; } state._lastChargesBelow = false; @@ -2357,11 +2909,13 @@ function applyTheme() { this.pollTimer = setInterval(async () => { try { const { charges, cooldown, max } = await WPlaceService.getCharges(); - state.currentCharges = Math.floor(charges); + state.displayCharges = Math.floor(charges); state.cooldown = cooldown; state.maxCharges = Math.max(1, Math.floor(max)); this.maybeNotifyChargesReached(); - } catch { /* ignore */ } + } catch { + /* ignore */ + } }, this.pollIntervalMs); }, stopPolling() { @@ -2378,69 +2932,18 @@ function applyTheme() { }; // COLOR MATCHING FUNCTION - Optimized with caching - 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) - - 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 - - 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 } - } - } 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 - if (penaltyWeight > 0 && targetChroma > 20) { - const candChroma = Math.sqrt(a2 * a2 + b2 * b2) - if (candChroma < targetChroma) { - const cd = targetChroma - candChroma - dist += cd * cd * penaltyWeight - } - } - if (dist < bestScore) { bestScore = dist; bestId = c.id; if (dist === 0) break } - } - } - - colorCache.set(cacheKey, bestId) - if (colorCache.size > 15000) { const firstKey = colorCache.keys().next().value; colorCache.delete(firstKey) } - return bestId - } + const colorCache = new Map(); // UI UPDATE FUNCTIONS (declared early to avoid reference errors) - let updateUI = () => { } - let updateStats = () => { } - let updateDataButtons = () => { } + let updateUI = () => {}; + let updateStats = (isManualRefresh) => {}; + 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 +2959,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 +2976,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'); @@ -2492,7 +2995,9 @@ function applyTheme() { // Don't re-fetch colors here, use what was captured when user clicked upload if (!state.availableColors || state.availableColors.length === 0) { // If no colors have been captured yet, show message - colorsContainer.innerHTML = `
${Utils.t('uploadImageFirst')}
`; + colorsContainer.innerHTML = `
${Utils.t( + 'uploadImageFirst' + )}
`; return; } @@ -2502,16 +3007,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 @@ -2521,7 +3026,9 @@ function applyTheme() { 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)' : ''}`, @@ -2540,10 +3047,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 +3082,107 @@ 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); - + const { sitekey, token: preGeneratedToken } = await Utils.obtainSitekeyAndToken(); + + if (!sitekey) { + throw new Error('No valid sitekey found'); + } + + console.log('🔑 Generating Turnstile token for sitekey:', sitekey); + console.log( + '🧭 UA:', + navigator.userAgent.substring(0, 50) + '...', + 'Platform:', + navigator.platform + ); + // Add additional checks before token generation if (!window.turnstile) { await Utils.loadTurnstile(); } - - 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}`); - + + let token = null; + + // ✅ Reuse pre-generated token if available and valid + if ( + preGeneratedToken && + typeof preGeneratedToken === 'string' && + preGeneratedToken.length > 20 + ) { + console.log('♻️ Reusing pre-generated token from sitekey detection phase'); + token = preGeneratedToken; + } + // ✅ Or use globally cached token if still valid + else if (isTokenValid()) { + console.log('♻️ Using existing cached token (from previous operation)'); + token = turnstileToken; + } + // ✅ Otherwise generate a new one + else { + console.log('🔐 No valid pre-generated or cached token, creating new one...'); + token = await Utils.executeTurnstile(sitekey, 'paint'); + if (token) { + setTurnstileToken(token); + } + } + + // 📊 Debug log + console.log( + `🔍 Token received - Type: ${typeof token}, Value: ${ + token + ? typeof token === 'string' + ? token.length > 50 + ? token.substring(0, 50) + '...' + : token + : JSON.stringify(token) + : 'null/undefined' + }, Length: ${token?.length || 0}` + ); + + // ✅ Final validation if (typeof token === 'string' && token.length > 20) { const duration = Math.round(performance.now() - startTime); console.log(`✅ Turnstile token generated successfully in ${duration}ms`); return token; } else { - throw new Error(`Invalid or empty token received - Type: ${typeof token}, Value: ${JSON.stringify(token)}, Length: ${token?.length || 0}`); + 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 +3198,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 +3230,28 @@ 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 +3260,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,65 +3282,76 @@ 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) + function appendLinkOnce(href, attributes = {}) { + // Check if a link with the same href already exists in the document head + const exists = Array.from(document.head.querySelectorAll('link')).some( + (link) => link.href === href + ); + if (exists) return; - 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) + // Create a new link element + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = href; + + // Add any additional attributes (e.g., data-* attributes) + for (const [key, value] of Object.entries(attributes)) { + link.setAttribute(key, value); + } + + // Append the link element to the document head + document.head.appendChild(link); } - // 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.setAttribute('data-wplace-theme', 'true'); - document.head.appendChild(cssLink); + appendLinkOnce('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'); - const container = document.createElement("div") - container.id = "wplace-image-bot-container" + if (theme.fontFamily.includes('Press Start 2P')) { + appendLinkOnce('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + } + + appendLinkOnce( + 'https://skalsech.github.io/WPlace-AutoBOT/feat-external-lang/auto-image-styles.css', + { 'data-wplace-theme': 'true' } + ); + + const container = document.createElement('div'); + container.id = 'wplace-image-bot-container'; container.innerHTML = `
- ${Utils.t("title")} + ${Utils.t('title')}
- -
@@ -2761,7 +3369,7 @@ function applyTheme() {
- ${Utils.t("initMessage")} + ${Utils.t('initMessage')}
@@ -2773,19 +3381,21 @@ function applyTheme() {
🖼️ Image Management
-
@@ -2798,17 +3408,17 @@ function applyTheme() {
@@ -2816,9 +3426,9 @@ function applyTheme() {
-
⏱️ ${Utils.t("cooldownSettings")}
+
⏱️ ${Utils.t('cooldownSettings')}
- +
${state.cooldownChargeThreshold} @@ -2833,32 +3443,36 @@ 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 = `
@@ -2866,7 +3480,9 @@ function applyTheme() { ${Utils.t('paintingStats')}
-
- ` + `; // 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; @@ -2904,22 +3522,29 @@ function applyTheme() { z-index: 99999; color: ${theme.text || 'white'}; font-family: ${theme.fontFamily || "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif"}; - box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)'}; + box-shadow: ${ + theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.1)' + }; backdrop-filter: ${theme.backdropFilter || 'blur(10px)'}; overflow: hidden; - animation: settingsSlideIn 0.4s ease-out; - ${theme.animations?.glow ? ` + animation: settings-slide-in 0.4s ease-out; + ${ + theme.animations?.glow + ? ` box-shadow: ${theme.boxShadow || '0 20px 40px rgba(0,0,0,0.3)'}, 0 0 30px ${theme.highlight || theme.neon || '#00ffff'}; - ` : ''} + ` + : '' + } `; + // noinspection CssInvalidFunction settingsContainer.innerHTML = `

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

@@ -2935,9 +3560,15 @@ function applyTheme() {

Generator mode creates tokens automatically. Hybrid mode falls back to manual when generator fails. Manual mode only uses pixel placement. @@ -2949,7 +3580,7 @@ function applyTheme() {

@@ -2957,40 +3588,59 @@ function applyTheme() {
- Overlay Opacity + Overlay Opacity
${Math.round(state.overlayOpacity * 100)}%
-
- - ` + `; - const resizeContainer = document.createElement("div") - resizeContainer.className = "resize-container" + const resizeContainer = document.createElement('div'); + resizeContainer.className = 'resize-container'; resizeContainer.innerHTML = ` -

${Utils.t("resizeImage")}

+

${Utils.t('resizeImage')}

- + - - - + + + @@ -3328,6 +4118,9 @@ function applyTheme() {
+ + + Shift = Row • Alt = Column
@@ -3359,8 +4152,12 @@ function applyTheme() {
- +
@@ -3382,7 +4181,9 @@ 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'); + + container.querySelectorAll('.wplace-section-title').forEach((title) => { + // Add a right-side arrow if it doesn't exist + if (!title.querySelector('i.arrow')) { + const arrow = document.createElement('i'); + arrow.className = 'fas fa-chevron-down arrow'; // FontAwesome down arrow + title.appendChild(arrow); + } + + // Click event to toggle collapse/expand of the section + title.addEventListener('click', () => { + const section = title.parentElement; + section.classList.toggle('collapsed'); + }); + }); - 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(true); } 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 +4416,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 = 'settings-fade-out 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 = 'settings-slide-in 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 = 'settings-fade-out 0.3s ease-out forwards'; + settingsContainer.classList.remove('show'); setTimeout(() => { - settingsContainer.style.animation = ""; - settingsContainer.style.top = "50%"; - settingsContainer.style.left = "50%"; - settingsContainer.style.transform = "translate(-50%, -50%)"; + 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 +4479,174 @@ 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.checked = state.paintWhitePixels; + settingsPaintWhiteToggle.addEventListener('change', (e) => { + state.paintWhitePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint white pixels: ${state.paintWhitePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintWhitePixels + ? 'White pixels in the template will be painted' + : 'White pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } - if (settingsPaintTransparentToggle) { - settingsPaintTransparentToggle.addEventListener('change', (e) => { - state.paintTransparentPixels = e.target.checked; - saveBotSettings(); - }); - } + if (settingsPaintTransparentToggle) { + settingsPaintTransparentToggle.checked = state.paintTransparentPixels; + settingsPaintTransparentToggle.addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + saveBotSettings(); + console.log( + `🎨 Paint transparent pixels: ${state.paintTransparentPixels ? 'ON' : 'OFF'}` + ); + const statusText = state.paintTransparentPixels + ? 'Transparent pixels in the template will be painted with the closest available color' + : 'Transparent pixels will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } // Speed 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 +4660,179 @@ 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'); + + // Coordinate generation controls with smart visibility + const coordinateModeSelect = settingsContainer.querySelector('#coordinateModeSelect'); + const coordinateDirectionSelect = settingsContainer.querySelector('#coordinateDirectionSelect'); + const coordinateSnakeToggle = settingsContainer.querySelector('#coordinateSnakeToggle'); + const directionControls = settingsContainer.querySelector('#directionControls'); + const snakeControls = settingsContainer.querySelector('#snakeControls'); + const blockControls = settingsContainer.querySelector('#blockControls'); + const blockWidthInput = settingsContainer.querySelector('#blockWidthInput'); + const blockHeightInput = settingsContainer.querySelector('#blockHeightInput'); + const paintUnavailablePixelsToggle = settingsContainer.querySelector( + '#paintUnavailablePixelsToggle' + ); + + if (paintUnavailablePixelsToggle) { + paintUnavailablePixelsToggle.checked = state.paintUnavailablePixels; + paintUnavailablePixelsToggle.addEventListener('change', (e) => { + state.paintUnavailablePixels = e.target.checked; + saveBotSettings(); + console.log(`🎨 Paint unavailable colors: ${state.paintUnavailablePixels ? 'ON' : 'OFF'}`); + const statusText = state.paintUnavailablePixels + ? 'Unavailable template colors will be painted with the closest available color' + : 'Unavailable template colors will be skipped'; + Utils.showAlert(statusText, 'success'); + }); + } + if (coordinateModeSelect) { + coordinateModeSelect.value = state.coordinateMode; + coordinateModeSelect.addEventListener('change', (e) => { + state.coordinateMode = e.target.value; + Utils.updateCoordinateUI({ + mode: state.coordinateMode, + directionControls, + snakeControls, + blockControls, + }); + saveBotSettings(); + console.log(`🔄 Coordinate mode changed to: ${state.coordinateMode}`); + Utils.showAlert(`Coordinate mode set to: ${state.coordinateMode}`, 'success'); + }); + } + + if (coordinateDirectionSelect) { + coordinateDirectionSelect.value = state.coordinateDirection; + coordinateDirectionSelect.addEventListener('change', (e) => { + state.coordinateDirection = e.target.value; + saveBotSettings(); + console.log(`🧭 Coordinate direction changed to: ${state.coordinateDirection}`); + Utils.showAlert(`Coordinate direction set to: ${state.coordinateDirection}`, 'success'); + }); + } + + if (coordinateSnakeToggle) { + coordinateSnakeToggle.checked = state.coordinateSnake; + coordinateSnakeToggle.addEventListener('change', (e) => { + state.coordinateSnake = e.target.checked; + saveBotSettings(); + console.log(`🐍 Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`); + Utils.showAlert( + `Snake pattern ${state.coordinateSnake ? 'enabled' : 'disabled'}`, + 'success' + ); + }); + } + + if (blockWidthInput) { + blockWidthInput.value = state.blockWidth; + blockWidthInput.addEventListener('input', (e) => { + const width = parseInt(e.target.value); + if (width >= 1 && width <= 50) { + state.blockWidth = width; + saveBotSettings(); + } + }); + } + + if (blockHeightInput) { + blockHeightInput.value = state.blockHeight; + blockHeightInput.addEventListener('change', (e) => { + const height = parseInt(e.target.value); + if (height >= 1 && height <= 50) { + state.blockHeight = height; + saveBotSettings(); + } + }); + } if (compactBtn) { - compactBtn.addEventListener("click", () => { - container.classList.toggle("wplace-compact") - const isCompact = container.classList.contains("wplace-compact") + 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 +4845,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,147 +4920,295 @@ 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 = {}, silent = false) => { + const message = Utils.t(messageKey, params); + statusText.textContent = message; + statusText.className = `wplace-status status-${type}`; + + if (!silent) { + // Trigger animation only when silent = false + statusText.style.animation = 'none'; + void statusText.offsetWidth; // trick to restart the animation + statusText.style.animation = 'slide-in 0.3s ease-out'; + } + }; + + function updateChargeStatsDisplay(intervalMs) { + const currentChargesEl = document.getElementById('wplace-stat-charges-value'); + const fullChargeEl = document.getElementById('wplace-stat-fullcharge-value'); + if (!fullChargeEl && !currentChargesEl) return; + if (!state.fullChargeData) { + fullChargeEl.textContent = '--:--:--'; + return; + } + + const { current, max, cooldownMs, startTime, spentSinceShot } = state.fullChargeData; + const elapsed = Date.now() - startTime; + + // total charges including elapsed time and spent during painting since snapshot + const chargesGained = elapsed / cooldownMs; + const rawCharges = current + chargesGained - spentSinceShot; + const cappedCharges = Math.min(rawCharges, max); + + // rounding with 0.95 threshold + let displayCharges; + const fraction = cappedCharges - Math.floor(cappedCharges); + if (fraction >= 0.95) { + displayCharges = Math.ceil(cappedCharges); + } else { + displayCharges = Math.floor(cappedCharges); + } + + state.displayCharges = Math.max(0, displayCharges); + state.preciseCurrentCharges = cappedCharges; + + const remainingMs = getMsToTargetCharges(cappedCharges, max, state.cooldown, intervalMs); + const timeText = Utils.msToTimeText(remainingMs); + + if (currentChargesEl) { + currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`; + } + + if ( + state.displayCharges < state.cooldownChargeThreshold && + !state.stopFlag && + state.running + ) { + updateChargesThresholdUI(intervalMs); + } + + if (fullChargeEl) { + if (state.displayCharges >= max) { + fullChargeEl.innerHTML = `FULL`; + } else { + fullChargeEl.innerHTML = ` + ${timeText} + `; + } + } } - updateStats = async () => { - const { charges, cooldown, max } = await WPlaceService.getCharges(); - state.currentCharges = Math.floor(charges); - state.cooldown = cooldown; - state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; - // Evaluate notifications every time we refresh server-side charges - NotificationManager.maybeNotifyChargesReached(); + updateStats = async (isManualRefresh = false) => { + const isForcedRefresh = isManualRefresh; + const isFirstCheck = !state.fullChargeData?.startTime; - if (cooldownSlider.max != state.maxCharges) { + const minUpdateInterval = 60_000; + const maxUpdateInterval = 90_000; + const randomUpdateThreshold = + minUpdateInterval + Math.random() * (maxUpdateInterval - minUpdateInterval); + const timeSinceLastUpdate = Date.now() - (state.fullChargeData?.startTime || 0); + const isTimeToUpdate = timeSinceLastUpdate >= randomUpdateThreshold; + + const shouldCallApi = isForcedRefresh || isFirstCheck || isTimeToUpdate; + + if (shouldCallApi) { + const { charges, max, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + state.maxCharges = Math.floor(max) > 1 ? Math.floor(max) : state.maxCharges; + + state.fullChargeData = { + current: charges, + max: max, + cooldownMs: cooldown, + startTime: Date.now(), + spentSinceShot: 0, + }; + // Evaluate notifications every time we refresh server-side charges + NotificationManager.maybeNotifyChargesReached(); + } + + if (state.fullChargeInterval) { + clearInterval(state.fullChargeInterval); + state.fullChargeInterval = null; + } + const intervalMs = 1000; + state.fullChargeInterval = setInterval( + () => updateChargeStatsDisplay(intervalMs), + intervalMs + ); + + if (cooldownSlider.max !== state.maxCharges) { cooldownSlider.max = state.maxCharges; } 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.displayCharges, + state.cooldown + ); progressBar.style.width = `${progress}%`; imageStatsHTML = ` -
-
${Utils.t("progress")}
-
${progress}%
-
-
-
${Utils.t("pixels")}
-
${state.paintedPixels}/${state.totalPixels}
-
-
-
${Utils.t("estimatedTime")}
-
${Utils.formatTime(state.estimatedTime)}
-
- `; +
+
${Utils.t('progress')}
+
${progress}%
+
+
+
${Utils.t( + 'pixels' + )}
+
${state.paintedPixels}/${state.totalPixels}
+
+
+
${Utils.t( + 'estimatedTime' + )}
+
${Utils.formatTime(state.estimatedTime)}
+
+ `; } let colorSwatchesHTML = ''; + state.availableColors = state.availableColors.filter( + (c) => c.name !== 'Unknown CoIor NaN' && c.id !== null + ); + + const availableColors = Utils.extractAvailableColors(); + const newCount = Array.isArray(availableColors) ? availableColors.length : 0; + + if (newCount === 0 && isManualRefresh) { + Utils.showAlert(Utils.t('noColorsFound'), 'warning'); + } else if (newCount > 0 && state.availableColors.length < newCount) { + const oldCount = state.availableColors.length; + + Utils.showAlert( + Utils.t('colorsUpdated', { + oldCount, + newCount: newCount, + diffCount: newCount - oldCount, + }), + 'success' + ); + state.availableColors = availableColors; + } if (state.colorsChecked) { - colorSwatchesHTML = state.availableColors.map(color => { - const rgbString = `rgb(${color.rgb.join(',')})`; - return `
`; - }).join(''); + colorSwatchesHTML = state.availableColors + .map((color) => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }) + .join(''); } statsArea.innerHTML = ` ${imageStatsHTML}
-
${Utils.t("charges")}
-
${Math.floor(state.currentCharges)} / ${state.maxCharges}
+
+ ${Utils.t('charges')} +
+
+ ${state.displayCharges} / ${state.maxCharges} +
- ${state.colorsChecked ? ` +
+
+ ${Utils.t('fullChargeIn')} +
+
--:--:--
+
+ ${ + state.colorsChecked + ? `
-
${Utils.t('availableColors', {count: state.availableColors.length})}
+
${Utils.t( + 'availableColors', + { count: state.availableColors.length } + )}
${colorSwatchesHTML}
- ` : ''} + ` + : '' + } `; - } + + // should be after statsArea.innerHTML = '...'. todo make full stats ui update partial + updateChargeStatsDisplay(intervalMs); + }; updateDataButtons = () => { - const hasImageData = state.imageLoaded && state.imageData - saveBtn.disabled = !hasImageData - saveToFileBtn.disabled = !hasImageData - } + const hasImageData = state.imageLoaded && state.imageData; + saveBtn.disabled = !hasImageData; + saveToFileBtn.disabled = !hasImageData; + }; - updateDataButtons() + updateDataButtons(); function showResizeDialog(processor) { let baseProcessor = processor; @@ -4134,24 +5224,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 +5261,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 +5303,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 +5320,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 +5337,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 +5404,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 +5436,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 +5457,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 +5467,18 @@ 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,30 +5486,39 @@ 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 wrapRect = panStage?.getBoundingClientRect() || { + width: 0, + height: 0, + }; const w = (baseCanvas.width || 1) * _zoomLevel; const h = (baseCanvas.height || 1) * _zoomLevel; if (w <= wrapRect.width) { @@ -4391,13 +5539,16 @@ function applyTheme() { if (_panRaf) return; _panRaf = requestAnimationFrame(() => { clampPan(); - canvasStack.style.transform = `translate3d(${Math.round(panX)}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; + canvasStack.style.transform = `translate3d(${Math.round( + panX + )}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; _panRaf = 0; }); }; const updateZoomLayout = () => { - const w = baseCanvas.width || 1, h = baseCanvas.height || 1; + 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 +5566,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 +5580,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 +5601,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 +5620,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 +5769,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 +5810,7 @@ function applyTheme() { const map = [ [btnIgnore, 'ignore'], [btnUnignore, 'unignore'], - [btnToggle, 'toggle'] + [btnToggle, 'toggle'], ]; for (const [el, m] of map) { if (!el) continue; @@ -4589,22 +5819,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 +5864,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 +5887,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 +5906,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 +5930,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 +5975,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 +6004,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 +6015,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 +6089,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 +6147,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 +6181,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 +6212,37 @@ 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 +6251,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 +6290,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"); + if (availableColors === null || availableColors.length < 10) { + updateUI('noColorsFound', 'error'); + Utils.showAlert(Utils.t('noColorsFound'), 'error'); return; } if (!state.colorsChecked) { state.availableColors = availableColors; state.colorsChecked = true; - updateUI("colorsFound", "success", { count: availableColors.length }); + updateUI('colorsFound', 'success', { count: availableColors.length }); updateStats(); selectPosBtn.disabled = false; // Only enable resize button if image is also loaded @@ -4976,23 +6339,29 @@ 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 +6373,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 +6403,187 @@ 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; } - await ensureToken() - if (!turnstileToken) return false + await ensureToken(); + if (!turnstileToken) return; - state.running = true - state.stopFlag = false - startBtn.disabled = true - stopBtn.disabled = false - uploadBtn.disabled = true - selectPosBtn.disabled = true - resizeBtn.disabled = true - saveBtn.disabled = true + 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 - } catch { - updateUI("paintingError", "error") - return false + await processImage(); + } catch (e) { + console.error('Unexpected error:', e); + updateUI('paintingError', 'error'); } 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 + if (state.stopFlag) { + startBtn.disabled = false; } else { - startBtn.disabled = false + startBtn.disabled = true; + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + resizeBtn.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('paintingStoppedByUser', '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; @@ -5227,276 +6597,540 @@ function applyTheme() { NotificationManager.syncFromState(); } - async function processImage() { - const { width, height, pixels } = state.imageData - const { x: startX, y: startY } = state.startPosition - const { x: regionX, y: regionY } = state.region + function getMsToTargetCharges(current, target, cooldown, intervalMs = 0) { + const remainingCharges = target - current; + return Math.max(0, remainingCharges * cooldown - intervalMs); + } - 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]; - if (!state.paintTransparentPixels && a < tThresh2) return false; - if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) return false; - return true; - }; + function updateChargesThresholdUI(intervalMs) { + if (state.stopFlag) return; - let startRow = 0; - let startCol = 0; - let foundStart = false; - let seen = 0; - const target = Math.max(0, Math.min(state.paintedPixels || 0, width * height)); - 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; } - seen++; + const threshold = state.cooldownChargeThreshold; + const remainingMs = getMsToTargetCharges( + state.preciseCurrentCharges, + threshold, + state.cooldown, + intervalMs + ); + const timeText = Utils.msToTimeText(remainingMs); + + updateUI( + 'noChargesThreshold', + 'warning', + { + threshold, + current: state.displayCharges, + time: timeText, + }, + true + ); + } + + function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight) { + const coords = []; + console.log( + 'Generating coordinates with \n mode:', + mode, + '\n direction:', + direction, + '\n snake:', + snake, + '\n blockWidth:', + blockWidth, + '\n blockHeight:', + blockHeight + ); + // --------- Standard 4 corners traversal ---------- + let xStart, xEnd, xStep; + let yStart, yEnd, yStep; + switch (direction) { + case 'top-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'top-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = 0; + yEnd = height; + yStep = 1; + break; + case 'bottom-left': + xStart = 0; + xEnd = width; + xStep = 1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + case 'bottom-right': + xStart = width - 1; + xEnd = -1; + xStep = -1; + yStart = height - 1; + yEnd = -1; + yStep = -1; + break; + default: + throw new Error(`Unknown direction: ${direction}`); + } + + // --------- Traversal modes ---------- + if (mode === 'rows') { + for (let y = yStart; y !== yEnd; y += yStep) { + if (snake && (y - yStart) % 2 !== 0) { + for (let x = xEnd - xStep; x !== xStart - xStep; x -= xStep) { + coords.push([x, y]); + } + } else { + for (let x = xStart; x !== xEnd; x += xStep) { + coords.push([x, y]); + } + } } + } else if (mode === 'columns') { + for (let x = xStart; x !== xEnd; x += xStep) { + if (snake && (x - xStart) % 2 !== 0) { + for (let y = yEnd - yStep; y !== yStart - yStep; y -= yStep) { + coords.push([x, y]); + } + } else { + for (let y = yStart; y !== yEnd; y += yStep) { + coords.push([x, y]); + } + } + } + } else if (mode === 'circle-out') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = 0; r <= maxRadius; r++) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'circle-in') { + const cx = Math.floor(width / 2); + const cy = Math.floor(height / 2); + const maxRadius = Math.ceil(Math.sqrt(cx * cx + cy * cy)); + + for (let r = maxRadius; r >= 0; r--) { + for (let y = cy - r; y <= cy + r; y++) { + for (let x = cx - r; x <= cx + r; x++) { + if (x >= 0 && x < width && y >= 0 && y < height) { + const dist = Math.max(Math.abs(x - cx), Math.abs(y - cy)); + if (dist === r) coords.push([x, y]); + } + } + } + } + } else if (mode === 'blocks' || mode === 'shuffle-blocks') { + const blocks = []; + for (let by = 0; by < height; by += blockHeight) { + for (let bx = 0; bx < width; bx += blockWidth) { + const block = []; + for (let y = by; y < Math.min(by + blockHeight, height); y++) { + for (let x = bx; x < Math.min(bx + blockWidth, width); x++) { + block.push([x, y]); + } + } + blocks.push(block); + } + } + + if (mode === 'shuffle-blocks') { + // Simple Fisher-Yates shuffle + for (let i = blocks.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; + } + } + + // Concatenate all blocks + for (const block of blocks) { + coords.push(...block); + } + } else { + throw new Error(`Unknown mode: ${mode}`); + } + + return coords; + } + + async function flushPixelBatch(pixelBatch) { + if (!pixelBatch || pixelBatch.pixels.length === 0) return true; + + const batchSize = pixelBatch.pixels.length; + console.log( + `📦 Sending batch with ${batchSize} pixels (region: ${pixelBatch.regionX},${pixelBatch.regionY})` + ); + const success = await sendBatchWithRetry( + pixelBatch.pixels, + pixelBatch.regionX, + pixelBatch.regionY + ); + if (success) { + pixelBatch.pixels.forEach((p) => { + state.paintedPixels++; + Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); + }); + state.fullChargeData = { + ...state.fullChargeData, + spentSinceShot: state.fullChargeData.spentSinceShot + batchSize, + }; + updateStats(); + updateUI('paintingProgress', 'default', { + painted: state.paintedPixels, + total: state.totalPixels, + }); + Utils.performSmartSave(); + + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { + const delayPerPixel = 1000 / state.paintingSpeed; + const totalDelay = Math.max(100, delayPerPixel * batchSize); + await Utils.sleep(totalDelay); + } + } else { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + } + + pixelBatch.pixels = []; + return success; + } + + async function processImage() { + const { width, height, pixels } = state.imageData; + const { x: startX, y: startY } = state.startPosition; + const { x: regionX, y: regionY } = state.region; + + // todo force load tiles + const tilesReady = await overlayManager.waitForTiles( + regionX, + regionY, + width, + height, + startX, + startY, + 10000 // timeout 10s + ); + + if (!tilesReady) { + updateUI('overlayTilesNotLoaded', 'error'); + state.stopFlag = true; + return; } - if (!foundStart) { startRow = height; startCol = 0; } let pixelBatch = null; - let skippedPixels = { transparent: 0, white: 0, alreadyPainted: 0 }; + let skippedPixels = { + transparent: 0, + white: 0, + alreadyPainted: 0, + colorUnavailable: 0, + }; + + const transparencyThreshold = + state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + function checkPixelEligibility(x, y) { + const idx = (y * width + x) * 4; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; + + if (!state.paintTransparentPixels && a < transparencyThreshold) + return { + eligible: false, + reason: 'transparent', + }; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + return { + eligible: false, + reason: 'white', + }; + + let targetRgb = Utils.isWhitePixel(r, g, b) + ? [255, 255, 255] + : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + + // Template color ID, normalized/mapped to the nearest available color in our palette. + // Example: template requires "Slate", but we only have "Dark Gray" available + // → mappedTargetColorId = ID of Dark Gray. + // + // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier + // because "Slate" was not found (null returned). + // + // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"), + // and we proceed with painting using that mapped color. + // + // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray), + // we skip painting, since template and canvas both resolve to the same available color (Dark Gray). + const mappedTargetColorId = Utils.resolveColor( + targetRgb, + state.availableColors, + !state.paintUnavailablePixels + ); + + // Technically, checking only `!mappedTargetColorId.id` would be enough, + // but combined with `state.paintUnavailablePixels` it makes the logic explicit: + // we only skip when the template color cannot be mapped AND strict mode is on. + if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { + return { + eligible: false, + reason: 'colorUnavailable', + r, + g, + b, + a, + mappedColorId: mappedTargetColorId.id, + }; + } + return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + } + + function skipPixel(reason, id, rgb, x, y) { + if (reason !== 'transparent') { + console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); + } + skippedPixels[reason]++; + } try { - outerLoop: for (let y = startRow; y < height; y++) { - 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); - if (success) { - pixelBatch.pixels.forEach(() => { state.paintedPixels++; }); - state.currentCharges -= pixelBatch.pixels.length; - updateStats(); - } - } - state.lastPosition = { x, y } - updateUI("paintingPaused", "warning", { x, y }) - break outerLoop + const coords = generateCoordinates( + width, + height, + state.coordinateMode, + state.coordinateDirection, + state.coordinateSnake, + state.blockWidth, + state.blockHeight + ); + + outerLoop: for (const [x, y] of coords) { + if (state.stopFlag) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log( + `🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels` + ); + await flushPixelBatch(pixelBatch); } + state.lastPosition = { x, y }; + updateUI('paintingPaused', 'warning', { x, y }); + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } - - const 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 targetPixelInfo = checkPixelEligibility(x, y); + let absX = startX + x; + let absY = startY + y; - const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; - if ((!state.paintTransparentPixels && alpha < tThresh2) || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) { - if (!state.paintTransparentPixels && alpha < tThresh2) { - skippedPixels.transparent++; - } else { - skippedPixels.white++; - } - continue; - } + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + let pixelX = absX % 1000; + let pixelY = absY % 1000; - let targetRgb; - if (Utils.isWhitePixel(r, g, b)) { - targetRgb = [255, 255, 255]; - } else { - targetRgb = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); - } + // Template color ID, normalized/mapped to the nearest available color in our palette. + // Example: template requires "Slate", but we only have "Dark Gray" available + // → mappedTargetColorId = ID of Dark Gray. + // + // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier + // because "Slate" was not found (null returned). + // + // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"), + // and we proceed with painting using that mapped color. + // + // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray), + // we skip painting, since template and canvas both resolve to the same available color (Dark Gray). + const targetMappedColorId = targetPixelInfo.mappedColorId; - const colorId = findClosestColor([r, g, b], state.availableColors); + if (!targetPixelInfo.eligible) { + skipPixel( + targetPixelInfo.reason, + targetMappedColorId, + [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b], + pixelX, + pixelY + ); + continue; + } - let absX = startX + x; - let absY = startY + y; + // console.log(`[DEBUG] Pixel at (${pixelX}, ${pixelY}) eligible: RGB=${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b}, + // alpha=${targetPixelInfo.a}, mappedColorId=${targetMappedColorId}`); - let adderX = Math.floor(absX / 1000); - let adderY = Math.floor(absY / 1000); - let pixelX = absX % 1000; - let pixelY = absY % 1000; - - if (!pixelBatch || - pixelBatch.regionX !== regionX + adderX || - pixelBatch.regionY !== regionY + adderY) { - - if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${regionX + adderX},${regionY + adderY})`); - const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); - - if (success) { - 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", { - 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) { - // 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) - } - updateStats(); - } else { - // If batch failed after all retries, stop painting to prevent infinite loops - console.error(`❌ Batch failed permanently after retries. Stopping painting.`); - state.stopFlag = true; - break outerLoop; - } - } - - pixelBatch = { - regionX: regionX + adderX, - regionY: regionY + adderY, - 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); - if (existingColorRGBA && Array.isArray(existingColorRGBA)) { - const [er, eg, eb] = existingColorRGBA; - const existingColorId = findClosestColor([er, eg, eb], state.availableColors); - // console.log(`pixel at (${pixelX}, ${pixelY}) has color ${existingColorId} it should be ${colorId}`); - if (existingColorId === colorId) { - skippedPixels.alreadyPainted++; - console.log(`Skipped already painted pixel at (${pixelX}, ${pixelY})`); - continue; // Skip - } - } - } catch (e) { - /* ignore */ - } - - pixelBatch.pixels.push({ - x: pixelX, - y: pixelY, - color: colorId, - localX: x, - localY: y, - }); - - // 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); + if ( + !pixelBatch || + pixelBatch.regionX !== regionX + adderX || + pixelBatch.regionY !== regionY + adderY + ) { + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log( + `🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels (switching to region ${ + regionX + adderX + },${regionY + adderY})` + ); + const success = await flushPixelBatch(pixelBatch); if (success) { - 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", { - 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 batchDelayFactor = Math.max(1, 100 / state.paintingSpeed); + const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length); + await Utils.sleep(totalDelay); } + updateStats(); } else { - // If batch failed after all retries, stop painting to prevent infinite loops console.error(`❌ Batch failed permanently after retries. Stopping painting.`); state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + // noinspection UnnecessaryLabelOnBreakStatementJS break outerLoop; } - - pixelBatch.pixels = []; } - while (state.currentCharges < state.cooldownChargeThreshold && !state.stopFlag) { - const { charges, cooldown } = await WPlaceService.getCharges(); - state.currentCharges = Math.floor(charges); - state.cooldown = cooldown; + pixelBatch = { + regionX: regionX + adderX, + regionY: regionY + adderY, + pixels: [], + }; + } - if (state.currentCharges >= state.cooldownChargeThreshold) { - // Edge-trigger a notification the instant threshold is crossed - NotificationManager.maybeNotifyChargesReached(true); - updateStats(); - break; + try { + const tileKeyParts = [pixelBatch.regionX, pixelBatch.regionY]; + + const tilePixelRGBA = await overlayManager.getTilePixelColor( + tileKeyParts[0], + tileKeyParts[1], + pixelX, + pixelY + ); + + if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { + // Resolve the actual canvas pixel color to the closest available color. + // (The raw canvas RGB [er, eg, eb] is mapped into state.availableColors) + // so that comparison is consistent with targetMappedColorId. + const mappedCanvasColor = Utils.resolveColor( + tilePixelRGBA.slice(0, 3), + state.availableColors + ); + const isMatch = mappedCanvasColor.id === targetMappedColorId; + if (isMatch) { + skipPixel( + 'alreadyPainted', + targetMappedColorId, + [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b], + pixelX, + pixelY + ); + continue; } - - // Enable save button during cooldown wait - saveBtn.disabled = false; - - updateUI("noChargesThreshold", "warning", { - time: Utils.formatTime(state.cooldown), - threshold: state.cooldownChargeThreshold, - current: state.currentCharges - }); - await updateStats(); - - // Allow auto save during cooldown - Utils.performSmartSave(); - - await Utils.sleep(state.cooldown); + console.debug( + `[COMPARE] Pixel at 📍 (${pixelX}, ${pixelY}) in region (${ + regionX + adderX + }, ${regionY + adderY})\n` + + ` ├── Current color: rgb(${tilePixelRGBA.slice(0, 3).join(', ')}) (id: ${mappedCanvasColor.id})\n` + + ` ├── Target color: rgb(${targetPixelInfo.r}, ${targetPixelInfo.g}, ${targetPixelInfo.b}) (id: ${targetMappedColorId})\n` + + ` └── Status: ${ + isMatch ? '✅ Already painted → SKIP' : '🔴 Needs paint → PAINT' + }\n` + ); } - - // Disable save button again after exiting wait loop (back to normal painting) - if (!state.stopFlag) { - saveBtn.disabled = true; - } - if (state.stopFlag) break outerLoop; + } catch (e) { + console.error(`[DEBUG] Error checking existing pixel at (${pixelX}, ${pixelY}):`, e); + updateUI('paintingPixelCheckFailed', 'error', { x: pixelX, y: pixelY }); + state.stopFlag = true; + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } + pixelBatch.pixels.push({ + x: pixelX, + y: pixelY, + color: targetMappedColorId, + localX: x, + localY: y, + }); + + const maxBatchSize = calculateBatchSize(); + if (pixelBatch.pixels.length >= maxBatchSize) { + const modeText = + state.batchMode === 'random' + ? `random (${state.randomBatchMin}-${state.randomBatchMax})` + : 'normal'; + console.log( + `📦 Sending batch with ${pixelBatch.pixels.length} pixels (mode: ${modeText}, target: ${maxBatchSize})` + ); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.error(`❌ Batch failed permanently after retries. Stopping painting.`); + state.stopFlag = true; + updateUI('paintingBatchFailed', 'error'); + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; + } + + pixelBatch.pixels = []; + } + + if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { + await Utils.dynamicSleep(() => { + if (state.displayCharges >= state.cooldownChargeThreshold) { + NotificationManager.maybeNotifyChargesReached(true); + return 0; + } + if (state.stopFlag) return 0; + return getMsToTargetCharges( + state.preciseCurrentCharges, + state.cooldownChargeThreshold, + state.cooldown + ); + }); + } + + if (state.stopFlag) { + // noinspection UnnecessaryLabelOnBreakStatementJS + break outerLoop; } } if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); - const success = await 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) - } - } else { - // If final batch failed after retries, log it - console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`); + const success = await flushPixelBatch(pixelBatch); + if (!success) { + console.warn( + `⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.` + ); } } } finally { - if (window._chargesInterval) clearInterval(window._chargesInterval) - window._chargesInterval = null + if (window._chargesInterval) clearInterval(window._chargesInterval); + window._chargesInterval = null; } if (state.stopFlag) { - 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 +7145,24 @@ 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(` Skipped - Color Unavailable: ${skippedPixels.colorUnavailable}`); + console.log( + ` Total processed: ${ + state.paintedPixels + + skippedPixels.transparent + + skippedPixels.white + + skippedPixels.alreadyPainted + + skippedPixels.colorUnavailable + }` + ); - 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 +7173,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 maxAllowed = state.displayCharges; const finalBatchSize = Math.min(targetBatchSize, maxAllowed); - + return finalBatchSize; } @@ -5543,16 +7186,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 +7205,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 +7217,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,38 +7332,49 @@ 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, - // Notifications + ditheringEnabled: state.ditheringEnabled, + colorMatchingAlgorithm: state.colorMatchingAlgorithm, + enableChromaPenalty: state.enableChromaPenalty, + chromaPenaltyWeight: state.chromaPenaltyWeight, + customTransparencyThreshold: state.customTransparencyThreshold, + customWhiteThreshold: state.customWhiteThreshold, + paintWhitePixels: state.paintWhitePixels, + paintTransparentPixels: state.paintTransparentPixels, + resizeSettings: state.resizeSettings, + paintUnavailablePixels: state.paintUnavailablePixels, + coordinateMode: state.coordinateMode, + coordinateDirection: state.coordinateDirection, + coordinateSnake: state.coordinateSnake, + blockWidth: state.blockWidth, + blockHeight: state.blockHeight, // Save ignore mask (as base64) with its dimensions + resizeIgnoreMask: + state.resizeIgnoreMask && + state.resizeSettings && + state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length + ? { + w: state.resizeSettings.width, + h: state.resizeSettings.height, + data: btoa(String.fromCharCode(...state.resizeIgnoreMask)), + } + : null, // Notifications notificationsEnabled: state.notificationsEnabled, notifyOnChargesReached: state.notifyOnChargesReached, notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, notificationIntervalMinutes: state.notificationIntervalMinutes, + originalImage: state.originalImage, }; CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; // AUTO_CAPTCHA_ENABLED is always true - no need to save/load - localStorage.setItem("wplace-bot-settings", JSON.stringify(settings)); + 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 +7382,95 @@ 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; + state.paintUnavailablePixels = settings.paintUnavailablePixels ?? CONFIG.PAINT_UNAVAILABLE; + state.coordinateMode = settings.coordinateMode ?? CONFIG.COORDINATE_MODE; + state.coordinateDirection = settings.coordinateDirection ?? CONFIG.COORDINATE_DIRECTION; + state.coordinateSnake = settings.coordinateSnake ?? CONFIG.COORDINATE_SNAKE; + state.blockWidth = settings.blockWidth ?? CONFIG.COORDINATE_BLOCK_WIDTH; + state.blockHeight = settings.blockHeight ?? CONFIG.COORDINATE_BLOCK_HEIGHT; // Notifications state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; - state.notifyOnChargesReached = settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; - state.notifyOnlyWhenUnfocused = settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; - state.notificationIntervalMinutes = settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; - // Restore ignore mask if dims match current resizeSettings - if (settings.resizeIgnoreMask && settings.resizeIgnoreMask.data && state.resizeSettings && settings.resizeIgnoreMask.w === state.resizeSettings.width && settings.resizeIgnoreMask.h === state.resizeSettings.height) { - try { - const bin = atob(settings.resizeIgnoreMask.data); - const arr = new Uint8Array(bin.length); - for (let i=0;i { - 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'); + // Still enable file operations even if initial token generation fails + // Users can load progress and use manual/hybrid modes enableFileOperations(); } } catch (error) { - console.error("❌ Critical error during Turnstile initialization:", error); // More specific error - updateUI("tokenRetryLater", "warning"); + console.error('❌ Critical error during Turnstile initialization:', error); // More specific error + updateUI('tokenRetryLater', 'warning'); + // Still enable file operations even if initial setup fails + // Users can load progress and use manual/hybrid modes enableFileOperations(); + // Don't show error alert for initialization failures, just log them } } // Load theme preference immediately on startup before creating UI - loadThemePreference() - applyTheme() + loadThemePreference(); + applyTheme(); createUI().then(() => { // Generate token automatically after UI is ready @@ -5928,24 +7661,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(); }); - }) -})() + }); +})(); diff --git a/auto-image-styles.css b/auto-image-styles.css index 5406b16..480c805 100644 --- a/auto-image-styles.css +++ b/auto-image-styles.css @@ -8,32 +8,33 @@ /* ========================= */ /* 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/feat-external-lang/themes/classic.css'); +@import url('https://skalsech.github.io/WPlace-AutoBOT/feat-external-lang/themes/classic-light.css'); +@import url('https://skalsech.github.io/WPlace-AutoBOT/feat-external-lang/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 */ :root { /* Classic theme colors - exact upstream main values */ - --wplace-primary: linear-gradient(135deg, #000000 0%, #1a1a1a 100%); - --wplace-secondary: linear-gradient(135deg, #111111 0%, #2a2a2a 100%); - --wplace-accent: #222222; - --wplace-text: #ffffff; + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; --wplace-highlight: #775ce3; --wplace-highlight-secondary: #d3a4ff; - --wplace-success: #00ff00; - --wplace-error: #ff0000; - --wplace-warning: #ffaa00; + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; /* UI properties */ --wplace-radius: 12px; + --wplace-btn-radius: 16px; --wplace-border-style: solid; --wplace-border-width: 1px; - --wplace-border-color: #222222; - --wplace-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1); + --wplace-border-color: #222; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); --wplace-backdrop: blur(10px); - --wplace-font: 'Segoe UI', Roboto, sans-serif; + --wplace-font: 'Segoe UI', roboto, sans-serif; /* Z-index layers */ --wplace-z-overlay: 10000; @@ -48,45 +49,49 @@ --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; - + --wplace-muted-text: #fffb; + /* 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); - + --wplace-text-secondary: rgb(255 255 255 / 90%); + --wplace-text-muted: rgb(255 255 255 / 70%); + --wplace-text-dim: rgb(255 255 255 / 60%); + --wplace-text-faded: rgb(255 255 255 / 80%); + /* 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); - + --wplace-bg-input: rgb(255 255 255 / 15%); + --wplace-bg-subtle: rgb(255 255 255 / 10%); + --wplace-bg-faint: rgb(255 255 255 / 8%); + --wplace-bg-ghost: rgb(255 255 255 / 6%); + --wplace-bg-whisper: rgb(255 255 255 / 5%); + /* 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); - + --wplace-border-subtle: rgb(255 255 255 / 20%); + --wplace-border-faint: rgb(255 255 255 / 15%); + --wplace-border-ghost: rgb(255 255 255 / 10%); + --wplace-border-ultra-faint: rgb(255 255 255 / 5%); + /* 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); - + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 80%), 0 0 0 2px rgb(255 255 255 / 20%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 30%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 30%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 40%), 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); - + --wplace-pulse-start: rgb(0 255 0 / 70%); + --wplace-pulse-mid: rgb(0 255 0 / 0%); + --wplace-pulse-end: rgb(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%); + --wplace-slider-track-bg: linear-gradient( + to right, + var(--wplace-icon-primary) 0%, + var(--wplace-icon-secondary) 100% + ); } /* Theme classes are now defined in separate files */ @@ -96,74 +101,88 @@ /* ========================= */ /* Core animations (shared) */ /* ========================= */ - @keyframes neonGlow { +@keyframes neon-glow { 0%, 100% { - text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor; + text-shadow: + 0 0 5px currentcolor, + 0 0 10px currentcolor, + 0 0 15px currentcolor; } + 50% { - text-shadow: 0 0 2px currentColor, 0 0 5px currentColor, 0 0 8px currentColor; + text-shadow: + 0 0 2px currentcolor, + 0 0 5px currentcolor, + 0 0 8px currentcolor; } } - @keyframes pixelBlink { +@keyframes pixel-blink { 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } } - @keyframes scanline { +@keyframes scanline { 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } } - @keyframes pulse { +@keyframes pulse { 0% { box-shadow: 0 0 0 0 var(--wplace-pulse-start); } + 70% { box-shadow: 0 0 0 10px var(--wplace-pulse-mid); } + 100% { box-shadow: 0 0 0 0 var(--wplace-pulse-end); } } - @keyframes slideIn { +@keyframes slide-in { from { transform: translateY(-10px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } } - @keyframes slideDown { +@keyframes slide-down { from { transform: translateX(-50%) translateY(-20px); opacity: 0; } + to { transform: translateX(-50%) translateY(0); opacity: 1; } } - @keyframes shimmer { +@keyframes shimmer { 0% { transform: translateX(-100%); } + 100% { transform: translateX(100%); } @@ -181,11 +200,11 @@ max-height: calc(100vh - 40px); padding: 0; z-index: 9998; - animation: slideIn 0.4s ease-out; - overflow-y: auto; - overflow-x: hidden; + animation: slide-in 0.4s ease-out; + overflow: hidden auto; transition: all 0.3s ease; user-select: none; + /* Default classic theme styling for 100% compliance */ background: var(--wplace-primary); color: var(--wplace-text); @@ -217,16 +236,17 @@ #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; - animation: slideIn 0.4s ease-out; + animation: slide-in 0.4s ease-out; overflow-y: auto; transition: all 0.3s ease; user-select: none; display: none; + /* Default classic theme styling for 100% compliance */ background: var(--wplace-primary); color: var(--wplace-text); @@ -265,10 +285,11 @@ transition: background 0.2s ease; position: relative; z-index: 2; + /* Default styling for 100% classic compliance */ background: var(--wplace-secondary); color: var(--wplace-highlight); - text-shadow: 0 1px 2px rgba(0,0,0,0.5); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); } .wplace-header-title { @@ -293,8 +314,9 @@ align-items: center; justify-content: center; transition: all 0.2s; + /* Default styling for 100% classic compliance */ - background: rgba(255,255,255,0.1); + background: rgb(255 255 255 / 10%); color: var(--wplace-text); border-radius: 4px; font-family: var(--wplace-font); @@ -302,6 +324,7 @@ .wplace-header-btn:hover { transform: scale(1.1); + /* Default styling for 100% classic compliance */ background: var(--wplace-accent); color: var(--wplace-text); @@ -320,14 +343,16 @@ /* 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 +361,24 @@ 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-section-title i.arrow { + transform: rotate(-90deg); +} + /* ========================= */ /* Controls and buttons */ /* ========================= */ @@ -352,6 +388,17 @@ gap: 8px; } +.wplace-cooldown-control { + margin-top: 8px; +} + +.wplace-section.collapsed .wplace-controls, +.wplace-section.collapsed .wplace-cooldown-control { + max-height: 0; + opacity: 0; + pointer-events: none; +} + .wplace-row { display: grid; grid-template-columns: 1fr 1fr; @@ -365,7 +412,6 @@ .wplace-btn { padding: 8px 12px; border: none; - font-weight: 500; cursor: pointer; display: flex; align-items: center; @@ -376,6 +422,15 @@ position: relative; overflow: hidden; font-weight: 500; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none !important; + /* Theme-specific styling applied via theme files */ } @@ -387,15 +442,21 @@ width: 100%; height: 100%; transition: left 0.5s ease; + /* Theme-specific styling applied via theme files */ } +.wplace-btn:disabled::before { + display: none; +} + .wplace-btn:hover:not(:disabled)::before { left: 100%; } .wplace-btn:hover:not(:disabled) { transform: translateY(-1px); + /* Theme-specific styling applied via theme files */ } @@ -403,17 +464,6 @@ transform: translateY(0); } -.wplace-btn:disabled { - opacity: 0.5; - cursor: not-allowed; - transform: none !important; - /* Theme-specific styling applied via theme files */ -} - -.wplace-btn:disabled::before { - display: none; -} - /* Button variants moved to theme files */ /* ========================= */ @@ -422,6 +472,7 @@ .wplace-stats { margin-bottom: 8px; padding: 8px; + /* Theme-specific styling applied via theme files */ } @@ -447,6 +498,7 @@ .wplace-stat-value { font-weight: 600; + /* Theme-specific styling applied via theme files */ } @@ -464,6 +516,7 @@ padding: 4px; max-height: 80px; overflow-y: auto; + /* Theme-specific styling applied via theme files */ } @@ -484,6 +537,7 @@ .wplace-stat-color-swatch { width: 16px; height: 16px; + /* Theme-specific styling applied via theme files */ } @@ -494,6 +548,7 @@ overflow: hidden; height: 6px; position: relative; + /* Theme-specific styling applied via theme files */ } @@ -501,17 +556,16 @@ height: 6px; transition: width 0.5s ease; position: relative; + /* Theme-specific styling applied via theme files */ } .wplace-progress-bar::after { content: ''; position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; animation: shimmer 2s infinite; + /* Theme-specific styling applied via theme files */ } @@ -525,6 +579,7 @@ padding: 6px; border: 1px solid; font-size: 11px; + /* Theme-specific styling applied via theme files */ } @@ -545,6 +600,7 @@ max-width: 700px; max-height: 90%; overflow: auto; + /* Theme-specific styling applied via theme files */ } @@ -555,6 +611,7 @@ margin: 15px 0; height: 300px; overflow: hidden; + /* Theme-specific styling applied via theme files */ } @@ -569,9 +626,8 @@ position: absolute; left: 0; top: 0; - image-rendering: pixelated; image-rendering: -moz-crisp-edges; - image-rendering: crisp-edges; + image-rendering: pixelated; } .resize-mask-canvas { @@ -586,9 +642,15 @@ font-size: 12px; } +/* Missing button hover styles */ .resize-tools button { padding: 6px 10px; + border-radius: 6px; + border: 1px solid var(--wplace-border-subtle); + background: var(--wplace-bg-ghost); + color: var(--wplace-text); cursor: pointer; + /* Theme-specific styling applied via theme files */ } @@ -607,6 +669,7 @@ border: none; outline: none; -webkit-appearance: none; + /* Theme-specific styling applied via theme files */ } @@ -614,8 +677,9 @@ grid-column: 1 / -1; display: flex; align-items: center; - gap: 10px; + gap: 8px; margin-top: 15px; + flex-wrap: wrap; } .resize-buttons { @@ -625,16 +689,6 @@ margin-top: 20px; } -.resize-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 9999; - display: none; - /* Theme-specific styling applied via theme files */ -} /* ========================= */ /* Color grid */ @@ -662,6 +716,7 @@ overflow: hidden; text-overflow: ellipsis; width: 100%; + /* Theme-specific styling applied via theme files */ } @@ -669,15 +724,19 @@ width: 22px; height: 22px; cursor: pointer; - transition: transform 0.1s ease, box-shadow 0.2s ease; + transition: + transform 0.1s ease, + box-shadow 0.2s ease; position: relative; margin: 0 auto; + /* Theme-specific styling applied via theme files */ } .wplace-color-swatch.unavailable { border-style: dashed; cursor: not-allowed; + /* Theme-specific styling applied via theme files */ } @@ -688,11 +747,13 @@ .wplace-color-swatch:not(.active) { opacity: 0.3; + /* Theme-specific styling applied via theme files */ } .wplace-color-swatch.unavailable:not(.active) { opacity: 0.2; + /* Theme-specific styling applied via theme files */ } @@ -705,6 +766,7 @@ color: var(--wplace-text); font-size: 12px; font-weight: bold; + /* Theme-specific styling applied via theme files */ } @@ -712,16 +774,13 @@ border: none; height: 1px; margin: 8px 0; + /* Theme-specific styling applied via theme files */ } /* ========================= */ /* Cooldown controls */ /* ========================= */ -.wplace-cooldown-control { - margin-top: 8px; -} - .wplace-cooldown-control label { font-size: 11px; margin-bottom: 4px; @@ -740,6 +799,7 @@ appearance: none; height: 4px; outline: none; + /* Theme-specific styling applied via theme files */ } @@ -748,6 +808,7 @@ width: 14px; height: 14px; cursor: pointer; + /* Theme-specific styling applied via theme files */ } @@ -759,12 +820,13 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - padding: 0px; + padding: 0; z-index: 10002; display: none; min-width: 420px; max-width: 480px; overflow: hidden; + /* Default classic theme styling for 100% compliance */ background: var(--wplace-primary); color: var(--wplace-text); @@ -777,25 +839,27 @@ #wplace-settings-container.show { display: block; - animation: settingsSlideIn 0.4s ease-out; + animation: settings-slide-in 0.4s ease-out; } -@keyframes settingsSlideIn { +@keyframes settings-slide-in { from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); } + to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } -@keyframes settingsFadeOut { +@keyframes settings-fade-out { from { opacity: 1; transform: translate(-50%, -50%) scale(1); } + to { opacity: 0; transform: translate(-50%, -50%) scale(0.9); @@ -851,29 +915,26 @@ margin-right: 6px; } -.wplace-speed-slider-container { - display: flex; - align-items: center; - gap: 12px; -} .wplace-speed-slider { flex: 1; - height: 6px; + height: 8px; outline: none; - cursor: pointer; -webkit-appearance: none; appearance: none; + cursor: pointer; background: var(--wplace-slider-track-bg); border-radius: 4px; } + .wplace-speed-slider::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; cursor: pointer; + /* Theme-specific styling applied via theme files */ } @@ -881,6 +942,7 @@ width: 18px; height: 18px; cursor: pointer; + /* Theme-specific styling applied via theme files */ } @@ -918,13 +980,17 @@ padding: 20px !important; min-width: 300px !important; max-width: 400px !important; + /* Theme-specific styling applied via theme files */ } .wplace-turnstile-title { - font: 600 12px/1.3 'Segoe UI', sans-serif !important; + font: + 600 12px/1.3 'Segoe UI', + sans-serif !important; margin-bottom: 8px !important; opacity: 0.9 !important; + /* Theme-specific styling applied via theme files */ } @@ -942,6 +1008,7 @@ padding: 2px 6px !important; cursor: pointer !important; transition: background 0.2s ease !important; + /* Theme-specific styling applied via theme files */ } @@ -963,8 +1030,9 @@ z-index: 10002; max-width: 400px; text-align: center; - animation: slideDown 0.3s ease-out; + animation: slide-down 0.3s ease-out; box-shadow: var(--wplace-shadow-notification); + /* Theme-specific styling applied via theme files */ } @@ -981,6 +1049,7 @@ height: 100%; z-index: 100002; display: none; + /* Theme-specific styling applied via theme files */ } @@ -1000,7 +1069,7 @@ /* ========================= */ /* Responsive tweaks */ /* ========================= */ - @media (max-width: 768px) { +@media (width <= 768px) { #wplace-image-bot-container { left: 10px; width: calc(100vw - 20px); @@ -1027,7 +1096,7 @@ /* Auto light/dark support moved to theme files */ /* ===================================== */ -/* Settings container variants (optional)*/ +/* Settings container variants (optional) */ /* ===================================== */ .wplace-settings-container-base { position: fixed; @@ -1038,11 +1107,12 @@ display: none; min-width: 400px; max-width: 500px; - max-height: 80vh; + max-height: 100vh; overflow-y: auto; - animation: slideIn 0.3s ease-out; + animation: slide-in 0.3s ease-out; border: 1px solid var(--wplace-border-ghost); padding: 0; + /* Theme-specific styling applied via theme files */ } @@ -1073,8 +1143,8 @@ /* Settings content container */ .wplace-settings-content { - padding: 25px; - max-height: 70vh; + padding: 25px 25px 0; + max-height: 67vh; overflow-y: auto; } @@ -1126,6 +1196,7 @@ /* Section wrapper styling */ .wplace-settings-section-wrapper { padding: 18px; + /* Theme-specific styling applied via theme files */ } @@ -1138,11 +1209,13 @@ cursor: pointer; transition: all 0.3s ease; font-family: inherit; + /* Theme-specific styling applied via theme files */ } .wplace-settings-option { padding: 10px; + /* Theme-specific styling applied via theme files */ } @@ -1150,43 +1223,14 @@ .wplace-settings-description { font-size: 12px; color: var(--wplace-text-muted); - margin: 8px 0 0 0; -} - -/* Batch Mode Controls */ -.wplace-batch-mode-selection { - margin-bottom: 15px; -} - -.wplace-batch-mode-label { - display: block; - margin-bottom: 8px; - color: var(--wplace-text-secondary); - font-weight: 500; - font-size: 14px; -} - -.wplace-icon-dice { - color: var(--wplace-icon-palette); - margin-right: 6px; -} - -.wplace-batch-mode-select { - width: 100%; - padding: 10px 12px; - background: var(--wplace-bg-input); - color: var(--wplace-text); - border: 1px solid var(--wplace-border-subtle); - border-radius: 8px; - font-size: 13px; - outline: none; - cursor: pointer; + margin: 8px 0 0; } /* Batch Controls */ .wplace-batch-controls { padding: 18px; margin-bottom: 15px; + /* Theme-specific styling applied via theme files */ } @@ -1202,15 +1246,6 @@ margin-bottom: 10px; } -.wplace-speed-slider { - flex: 1; - height: 8px; - outline: none; - -webkit-appearance: none; - cursor: pointer; - background: var(--wplace-slider-track-bg); - border-radius: 4px; -} .wplace-speed-value { min-width: 100px; @@ -1218,6 +1253,7 @@ padding: 8px 12px; font-weight: bold; font-size: 13px; + /* Theme-specific styling applied via theme files */ } @@ -1253,10 +1289,10 @@ margin-right: 4px; } -.wplace-random-batch-input { +.wplace-settings-number-input { width: 100%; padding: 10px 12px; - background: var(--wplace-bg-subtle); + background: var(--wplace-bg-input); color: var(--wplace-text); border: 1px solid var(--wplace-border-subtle); border-radius: 8px; @@ -1267,7 +1303,7 @@ .wplace-random-batch-description { font-size: 11px; color: var(--wplace-text-dim); - margin: 8px 0 0 0; + margin: 8px 0 0; text-align: center; } @@ -1303,8 +1339,12 @@ .wplace-overlay-opacity-value { min-width: 40px; text-align: center; + background: var(--wplace-accent); + color: var(--wplace-text); padding: 4px 8px; + border-radius: 6px; font-size: 12px; + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); } .wplace-overlay-opacity-slider { @@ -1315,26 +1355,29 @@ cursor: pointer; } -.wplace-blue-marble-toggle { +.wplace-settings-toggle { display: flex; align-items: center; justify-content: space-between; cursor: pointer; } -.wplace-blue-marble-title { +.wplace-settings-toggle-title { font-weight: 500; } -.wplace-blue-marble-description { +.wplace-settings-toggle-description { font-size: 12px; - margin: 4px 0 0 0; + margin: 4px 0 0; + color: var(--wplace-muted-text); } -.wplace-blue-marble-checkbox { +.wplace-settings-checkbox { cursor: pointer; width: 20px; height: 20px; + flex-shrink: 0; + accent-color: var(--wplace-highlight); } /* Notifications */ @@ -1384,12 +1427,28 @@ /* Settings Footer */ .wplace-settings-footer { border-top: 1px solid var(--wplace-border-ghost); - padding-top: 20px; - margin-top: 10px; + padding: 20px; + position: sticky; + bottom: 0; + background: var(--wplace-secondary); } +/* Settings select option styling moved to theme files */ +/* Settings description styling moved to theme files */ + .wplace-settings-apply-btn { width: 100%; + border: none; + padding: 10px 16px; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 8px; + justify-content: center; + + /* Theme-specific styling applied via theme files */ } /* ===================================== */ @@ -1399,6 +1458,7 @@ /* Resize dialog title */ .resize-dialog-title { margin-top: 0; + color: var(--wplace-text); } /* Resize control labels */ @@ -1417,13 +1477,6 @@ } /* Zoom controls */ -.resize-zoom-controls { - display: flex; - align-items: center; - gap: 8px; - margin-top: 15px; - flex-wrap: wrap; -} .resize-zoom-btn { padding: 4px 8px; @@ -1658,6 +1711,7 @@ height: 100%; z-index: 9999; display: none; + /* Theme-specific styling applied via theme files */ } @@ -1694,7 +1748,7 @@ } .wplace-settings-close-btn { - background: rgba(34, 34, 34, 0.4); + background: rgb(34 34 34 / 40%); color: var(--wplace-text); border: 1px solid var(--wplace-border-color); border-radius: 50%; @@ -1710,13 +1764,18 @@ } .wplace-settings-close-btn:hover { - background: rgba(255, 0, 0, 0.4); + background: rgb(255 0 0 / 40%); transform: scale(1.1); } @keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } } /* Font styling corrections */ @@ -1725,46 +1784,6 @@ color: var(--wplace-text); } -.resize-dialog-title { - margin-top: 0; - color: var(--wplace-text); -} - -/* Ensure proper z-indexing */ -.resize-container { - z-index: 10000; -} - -/* Missing button hover styles */ -.resize-tools button { - padding: 6px 10px; - border-radius: 6px; - border: 1px solid var(--wplace-border-subtle); - background: var(--wplace-bg-ghost); - color: var(--wplace-text); - cursor: pointer; -} - -/* Canvas styling */ -.resize-canvas-stack { - position: relative; - transform-origin: center center; - display: inline-block; -} - -.resize-base-canvas, -.resize-mask-canvas { - position: absolute; - left: 0; - top: 0; - image-rendering: pixelated; - image-rendering: -moz-crisp-edges; - image-rendering: crisp-edges; -} - -.resize-mask-canvas { - pointer-events: auto; -} /* Additional precision fixes for exact upstream main matching */ /* Status default styling moved to theme files */ @@ -1774,94 +1793,16 @@ /* Settings header styling moved to theme files */ /* All remaining settings styling moved to theme files */ -.wplace-settings-section-wrapper { - padding: 18px; - /* Theme-specific styling applied via theme files */ -} - -.wplace-settings-select { - width: 100%; - padding: 12px 16px; - font-size: 14px; - outline: none; - cursor: pointer; - transition: all 0.3s ease; - font-family: inherit; - /* Theme-specific styling applied via theme files */ -} - -/* Settings select option styling moved to theme files */ -/* Settings description styling moved to theme files */ - -.wplace-settings-footer { - border-top: 1px solid var(--wplace-border-ghost); - padding-top: 20px; - margin-top: 10px; -} - -.wplace-settings-apply-btn { - width: 100%; - border: none; - padding: 10px 16px; - cursor: pointer; - font-weight: 500; - transition: all 0.3s ease; - display: flex; - align-items: center; - gap: 8px; - justify-content: center; - /* Theme-specific styling applied via theme files */ -} /* Icon styling moved to theme files */ -.wplace-icon-key { font-size: 16px; } -.wplace-icon-robot { font-size: 16px; } -.wplace-icon-speed { font-size: 16px; } -.wplace-icon-bell { font-size: 16px; } -.wplace-icon-palette { font-size: 16px; } -.wplace-icon-globe { font-size: 16px; } -.wplace-icon-paint { font-size: 16px; } -.wplace-icon-eye { font-size: 16px; } +.wplace-icon-eye { + font-size: 16px; +} /* Overlay Settings Controls */ -.wplace-overlay-opacity-control { - margin-bottom: 15px; -} - -.wplace-overlay-opacity-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.wplace-overlay-opacity-label { - font-weight: 500; - font-size: 13px; -} - -.wplace-overlay-opacity-value { - min-width: 40px; - text-align: center; - background: var(--wplace-accent); - color: var(--wplace-text); - padding: 4px 8px; - border-radius: 6px; - font-size: 12px; - border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); -} - -.wplace-overlay-opacity-slider { - width: 100%; - -webkit-appearance: none; - height: 8px; - /* Theme-specific styling applied via theme files */ - outline: none; - cursor: pointer; -} - .wplace-overlay-opacity-slider::-webkit-slider-thumb { -webkit-appearance: none; + appearance: none; width: 18px; height: 18px; border-radius: 50%; @@ -1876,36 +1817,90 @@ box-shadow: var(--wplace-shadow-slider-hover); } -.wplace-blue-marble-toggle { +/* Coordinate Generation Controls */ +.wplace-icon-route { + color: var(--wplace-icon-primary); + font-size: 16px; +} + +.wplace-icon-table { + color: var(--wplace-icon-palette); + margin-right: 6px; +} + +.wplace-icon-compass { + color: var(--wplace-icon-secondary); + margin-right: 6px; +} + +/* Pixel Filter Controls */ +.wplace-pixel-filter-controls { + padding: 18px; + margin-bottom: 15px; display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; + flex-direction: column; + gap: 12px; + + /* Theme-specific styling applied via theme files */ } -.wplace-blue-marble-title { - font-weight: 500; +/* Snake Pattern Controls */ +.wplace-snake-pattern-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ } -.wplace-blue-marble-description { +/* Block Size Controls */ +.wplace-block-size-controls { + padding: 18px; + margin-bottom: 15px; + + /* Theme-specific styling applied via theme files */ +} + +.wplace-block-size-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 15px; +} + +.wplace-block-size-label { + display: block; + color: var(--wplace-text-faded); font-size: 12px; - color: var(--wplace-muted-text); - margin: 4px 0 0 0; + margin-bottom: 8px; } -.wplace-blue-marble-checkbox { - cursor: pointer; - width: 20px; - height: 20px; - accent-color: var(--wplace-highlight); +.wplace-icon-width { + color: var(--wplace-icon-primary); + margin-right: 4px; +} + +.wplace-icon-height { + color: var(--wplace-icon-secondary); + margin-right: 4px; +} + +.wplace-block-size-description { + font-size: 11px; + color: var(--wplace-text-dim); + margin: 8px 0 0; + text-align: center; +} + +/* Random Block Controls */ +.wplace-shuffle-block-size-controls { + display: none; } /* Batch Mode Controls */ -.wplace-batch-mode-selection { +.wplace-mode-selection { margin-bottom: 15px; } -.wplace-batch-mode-label { +.wplace-mode-label { display: block; margin-bottom: 8px; color: var(--wplace-text-secondary); @@ -1917,55 +1912,3 @@ color: var(--wplace-icon-palette); margin-right: 6px; } - -.wplace-batch-mode-select { - width: 100%; - padding: 10px 12px; - background: var(--wplace-bg-input); - color: var(--wplace-text); - border: 1px solid var(--wplace-border-subtle); - border-radius: 8px; - font-size: 13px; - outline: none; - cursor: pointer; -} - -.wplace-batch-controls { - padding: 18px; - margin-bottom: 15px; - /* Theme-specific styling applied via theme files */ -} - -.wplace-speed-slider-container { - display: flex; - align-items: center; - gap: 15px; - margin-bottom: 10px; -} - -.wplace-speed-slider { - flex: 1; - height: 8px; - outline: none; - -webkit-appearance: none; - cursor: pointer; - background: var(--wplace-slider-track-bg); - border-radius: 4px; -} - -.wplace-speed-value { - min-width: 100px; - text-align: center; - padding: 8px 12px; - font-weight: bold; - font-size: 13px; - /* Theme-specific styling applied via theme files */ -} - -.wplace-speed-labels { - display: flex; - justify-content: space-between; - color: var(--wplace-text-muted); - font-size: 11px; - margin-top: 8px; -} diff --git a/lang/en.json b/lang/en.json index cb50b95..d2c493d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,14 +1,15 @@ { - "title": "WPlace Auto-Image", + "title": "Auto-Image", "toggleOverlay": "Toggle Overlay", "scanColors": "Scan Colors", - "uploadImage": "Upload Image", - "resizeImage": "Resize Image", + "uploadImage": "Upload", + "resizeImage": "Resize", "selectPosition": "Select Position", - "startPainting": "Start Painting", - "stopPainting": "Stop Painting", + "startPainting": "Start", + "stopPainting": "Stop", "checkingColors": "🔍 Checking available colors...", - "noColorsFound": "❌ Open the color palette on the site and try again!", + "noColorsFound": "❌ To update the color swatch, open the color palette on the site and try again!", + "colorsUpdated": "✅ Available colors increased {oldCount} -> {newCount}, {diffCount} new colors found", "colorsFound": "✅ {count} available colors found. Ready to upload.", "loadingImage": "🖼️ Loading image...", "imageLoaded": "✅ Image loaded with {count} valid pixels", @@ -20,13 +21,18 @@ "startPaintingMsg": "🎨 Starting painting...", "paintingProgress": "🧱 Progress: {painted}/{total} pixels...", "noCharges": "⌛ No charges. Waiting {time}...", - "paintingStopped": "⏹️ Painting stopped by user", + "overlayTilesNotLoaded": "❌ Required map tiles not loaded. Check connection or retry.", + "paintingStoppedByUser": "⏹️ Painting stopped by user", + "paintingBatchFailed": "❌ Failed to send pixel batch after retries. Painting stopped.", + "paintingPixelCheckFailed": "❌ Failed to read pixel at ({x}, {y}). Painting stopped.", + "paintingFinalBatchFailed": "⚠️ Final batch of {count} pixels failed after retries.", "paintingComplete": "✅ Painting complete! {count} pixels painted.", - "paintingError": "❌ Error during painting", + "paintingError": "❌ Unexpected error during painting", "missingRequirements": "❌ Load an image and select a position first", "progress": "Progress", "pixels": "Pixels", "charges": "Charges", + "fullChargeIn": "Full Charge In", "estimatedTime": "Estimated time", "initMessage": "Click 'Upload Image' to begin", "waitingInit": "Waiting for initialization...", @@ -71,7 +77,8 @@ "waitCharges": "Wait until charges reach", "captchaSolving": "🔑 Generating Turnstile token...", "captchaFailed": "❌ Turnstile token generation failed. Trying fallback method...", - "automation": "Automation", "noChargesThreshold": "⌛ Waiting for charges to reach {threshold}. Currently {current}. Next in {time}...", + "automation": "Automation", + "noChargesThreshold": "⌛ Waiting to reach {threshold} charges. Currently {current}. Estimated time: {time}.", "tokenCapturedSuccess": "Token captured successfully! You can start the bot now.", "notificationsNotSupported": "Notifications are not supported in this browser.", "chargesReadyNotification": "WPlace — Charges Ready", @@ -138,6 +145,10 @@ "restore": "Restore", "hideStats": "Hide Stats", "paintOptions": "Paint Options", - "paintWhitePixels": "Paint White Pixels", - "paintTransparentPixels": "Paint Transparent Pixels" + "paintWhitePixels": "Paint White", + "paintWhitePixelsDescription": "If enabled, template white pixels will be painted.", + "paintTransparentPixels": "Paint Transparent", + "paintTransparentPixelsDescription": "If enabled, template transparent pixels will be painted", + "paintUnavailablePixels": "Paint Unavailable", + "paintUnavailablePixelsDescription": "If enabled, template colors that are unavailable will be painted using the closest available color" } \ No newline at end of file diff --git a/themes/classic-light.css b/themes/classic-light.css index 2322dcc..3761527 100644 --- a/themes/classic-light.css +++ b/themes/classic-light.css @@ -1,9 +1,10 @@ /* WPlace Auto-Image Bot - Classic Light Theme */ + /* Clean, bright theme based on classic design with light backgrounds */ -.wplace-theme--classic-light { +.wplace-theme-classic-light { /* === CORE COLORS === */ - --wplace-primary: #ffffff; /* Clean white */ + --wplace-primary: #fff; /* Clean white */ --wplace-secondary: #f8f9fa; /* Light gray */ --wplace-accent: #e9ecef; /* Lighter gray */ --wplace-text: #212529; /* Dark text */ @@ -14,14 +15,16 @@ /* === UI PROPERTIES === */ --wplace-radius: 8px; + --wplace-btn-radius: 12px; --wplace-border-style: solid; --wplace-border-width: 1px; --wplace-border-color: #415a77; - --wplace-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.08); + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%); --wplace-backdrop: blur(10px); - --wplace-font: 'Segoe UI', Roboto, sans-serif; + --wplace-font: 'Segoe UI', roboto, sans-serif; /* === EXTENDED COLOR PALETTE === */ + /* Icon colors */ --wplace-icon-primary: #4facfe; /* Light blue */ --wplace-icon-secondary: #00f2fe; /* Cyan */ @@ -30,38 +33,38 @@ /* Additional UI colors */ --wplace-danger: #dc3545; /* Bootstrap red */ --wplace-danger-dark: #c82333; /* Darker red */ - --wplace-muted-text: rgba(33, 37, 41, 0.7); + --wplace-muted-text: rgb(33 37 41 / 70%); --wplace-highlight-secondary: #d3a4ff; /* Text variants */ - --wplace-text-secondary: rgba(33, 37, 41, 0.9); - --wplace-text-muted: rgba(33, 37, 41, 0.7); - --wplace-text-dim: rgba(33, 37, 41, 0.6); - --wplace-text-faded: rgba(33, 37, 41, 0.8); - + --wplace-text-secondary: rgb(33 37 41 / 90%); + --wplace-text-muted: rgb(33 37 41 / 70%); + --wplace-text-dim: rgb(33 37 41 / 60%); + --wplace-text-faded: rgb(33 37 41 / 80%); + /* Background variants */ - --wplace-bg-input: rgba(0, 0, 0, 0.08); - --wplace-bg-subtle: rgba(0, 0, 0, 0.05); - --wplace-bg-faint: rgba(0, 0, 0, 0.03); - --wplace-bg-ghost: rgba(0, 0, 0, 0.02); - --wplace-bg-whisper: rgba(0, 0, 0, 0.01); - + --wplace-bg-input: #f3f3f3; /* neutral-100 */ + --wplace-bg-subtle: #f3f3f3; /* neutral-100 — hover */ + --wplace-bg-faint: #e0e0e0; /* neutral-200 */ + --wplace-bg-ghost: #d1d1d1; /* neutral-300 */ + --wplace-bg-whisper: #e0e0e0; /* can be used as whisper on hover */ + /* Border variants */ - --wplace-border-subtle: rgba(0, 0, 0, 0.2); - --wplace-border-faint: rgba(0, 0, 0, 0.15); - --wplace-border-ghost: rgba(0, 0, 0, 0.1); - --wplace-border-ultra-faint: rgba(0, 0, 0, 0.05); + --wplace-border-subtle: #d1d1d1; /* neutral-300 */ + --wplace-border-faint: #e0e0e0; /* neutral-200 */ + --wplace-border-ghost: #e0e0e0; /* neutral-200 */ + --wplace-border-ultra-faint: #e0e0e0; /* neutral-200 — very faint */ /* Shadow variants */ - --wplace-shadow-drag: 0 12px 40px rgba(0, 0, 0, 0.2), 0 0 0 2px rgba(111, 66, 193, 0.3); - --wplace-shadow-notification: 0 4px 12px rgba(0, 0, 0, 0.15); - --wplace-shadow-slider-thumb: 0 3px 6px rgba(0, 0, 0, 0.2), 0 0 0 2px var(--wplace-icon-primary); - --wplace-shadow-slider-hover: 0 4px 8px rgba(0, 0, 0, 0.25), 0 0 0 3px var(--wplace-icon-primary); + --wplace-shadow-drag: 0 12px 40px rgb(0 0 0 / 20%), 0 0 0 2px rgb(111 66 193 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 0 0 / 15%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 0 0 / 20%), 0 0 0 2px var(--wplace-icon-primary); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 0 0 / 25%), 0 0 0 3px var(--wplace-icon-primary); /* Animation colors */ - --wplace-pulse-start: rgba(40, 167, 69, 0.7); - --wplace-pulse-mid: rgba(40, 167, 69, 0); - --wplace-pulse-end: rgba(40, 167, 69, 0); + --wplace-pulse-start: rgb(40 167 69 / 70%); + --wplace-pulse-mid: rgb(40 167 69 / 0%); + --wplace-pulse-end: rgb(40 167 69 / 0%); /* Slider colors */ --wplace-slider-thumb-bg: #6f42c1; @@ -71,23 +74,23 @@ /* === COMPONENT STYLING === */ /* Main container with clean light styling */ -.wplace-theme--classic-light #wplace-image-bot-container { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important; +.wplace-theme-classic-light #wplace-image-bot-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; color: var(--wplace-text) !important; - border: 1px solid rgba(0, 0, 0, 0.15) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; border-radius: 12px !important; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15), 0 0 0 1px rgba(0, 0, 0, 0.08) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important; font-family: 'Segoe UI', Roboto, sans-serif !important; backdrop-filter: blur(10px) !important; } /* Stats container with proper contrast */ -.wplace-theme--classic-light #wplace-stats-container { +.wplace-theme-classic-light #wplace-stats-container { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; border-radius: 12px !important; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%) !important; font-family: 'Segoe UI', Roboto, sans-serif !important; color: var(--wplace-text) !important; position: fixed !important; @@ -95,142 +98,44 @@ z-index: 9998 !important; } +/* Headers with light gradient */ +.wplace-theme-classic-light .wplace-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + color: var(--wplace-highlight) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; + text-shadow: none !important; +} + /* Stats header styling */ -.wplace-theme--classic-light #wplace-stats-container .wplace-header { +.wplace-theme-classic-light #wplace-stats-container .wplace-header { background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; color: var(--wplace-text) !important; - border-bottom: 1px solid rgba(0, 0, 0, 0.2) !important; + border-bottom: 1px solid rgb(0 0 0 / 20%) !important; text-shadow: none !important; font-weight: 600 !important; } /* Stats title specific styling */ -.wplace-theme--classic-light #wplace-stats-container .wplace-header .wplace-stats-title { +.wplace-theme-classic-light #wplace-stats-container .wplace-header .wplace-stats-title { color: var(--wplace-text) !important; text-shadow: none !important; } /* Comprehensive text and element styling for light theme - scoped to bot containers only */ -.wplace-theme--classic-light .wplace-status, -.wplace-theme--classic-light .wplace-stats, -.wplace-theme--classic-light .wplace-section, -.wplace-theme--classic-light .wplace-controls, -.wplace-theme--classic-light .wplace-data-management, -.wplace-theme--classic-light .wplace-cooldown-settings { +.wplace-theme-classic-light .wplace-status, +.wplace-theme-classic-light .wplace-stats, +.wplace-theme-classic-light .wplace-section, +.wplace-theme-classic-light .wplace-controls, +.wplace-theme-classic-light .wplace-data-management, +.wplace-theme-classic-light .wplace-cooldown-settings { color: var(--wplace-text) !important; } -/* Data management section buttons */ -.wplace-theme--classic-light .wplace-data-management .wplace-btn { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; - color: #212529 !important; - font-weight: 500 !important; -} - -.wplace-theme--classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { - background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; -} - -/* Settings dialog */ -.wplace-theme--classic-light #wplace-settings-container { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important; - border: 1px solid rgba(0, 0, 0, 0.15) !important; - border-radius: 12px !important; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08) !important; - font-family: 'Segoe UI', Roboto, sans-serif !important; - backdrop-filter: blur(10px) !important; - color: var(--wplace-text) !important; -} - -/* Settings dialog text elements */ -.wplace-theme--classic-light #wplace-settings-container * { - color: var(--wplace-text) !important; -} - -.wplace-theme--classic-light #wplace-settings-container .wplace-settings-section-wrapper * { - color: var(--wplace-text) !important; -} - -/* Color palette text labels */ -.wplace-theme--classic-light .wplace-stat-colors-grid * { - color: var(--wplace-text) !important; - font-weight: 500 !important; -} - -.wplace-theme--classic-light .wplace-color-label { - color: var(--wplace-text) !important; - font-weight: 500 !important; - text-shadow: none !important; -} - -.wplace-theme--classic-light .wplace-color-name { - color: var(--wplace-text) !important; - font-weight: 500 !important; -} - -.wplace-theme--classic-light .wplace-color-item-name { - color: #000000 !important; - font-weight: 600 !important; - text-shadow: none !important; -} - -/* Resize dialog */ -.wplace-theme--classic-light .resize-container { - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important; - border: 1px solid rgba(0, 0, 0, 0.15) !important; - border-radius: 12px !important; - box-shadow: 0 16px 48px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08) !important; - font-family: 'Segoe UI', Roboto, sans-serif !important; - backdrop-filter: blur(10px) !important; -} - -/* Headers with light gradient */ -.wplace-theme--classic-light .wplace-header { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; - color: var(--wplace-highlight) !important; - border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important; - text-shadow: none !important; -} - -/* Settings header */ -.wplace-theme--classic-light .wplace-settings-header { - background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; - border-bottom: 1px solid rgba(0, 0, 0, 0.15) !important; -} - -.wplace-theme--classic-light .wplace-settings-title { - color: var(--wplace-text) !important; - text-shadow: none !important; -} - -.wplace-theme--classic-light .wplace-settings-close-btn { - background: rgba(0, 0, 0, 0.05) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; - border-radius: 50% !important; - color: var(--wplace-text) !important; - transition: all 0.3s ease !important; -} - -.wplace-theme--classic-light .wplace-settings-close-btn:hover { - background: rgba(220, 53, 69, 0.1) !important; - border-color: var(--wplace-error) !important; - box-shadow: 0 0 12px rgba(220, 53, 69, 0.3) !important; -} - -/* Section titles */ -.wplace-theme--classic-light .wplace-section-title { - color: var(--wplace-highlight) !important; - text-shadow: none !important; - font-weight: 600 !important; -} - /* Buttons with light styling */ -.wplace-theme--classic-light .wplace-btn { +.wplace-theme-classic-light .wplace-btn { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; border-radius: 12px !important; color: var(--wplace-text) !important; font-family: 'Segoe UI', Roboto, sans-serif !important; @@ -238,180 +143,278 @@ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; } -.wplace-theme--classic-light .wplace-btn:hover:not(:disabled) { +/* Data management section buttons */ +.wplace-theme-classic-light .wplace-data-management .wplace-btn { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + color: #212529 !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-btn:hover:not(:disabled) { background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; transform: translateY(-1px) !important; } +.wplace-theme-classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 15%) !important; +} + +/* Settings dialog */ +.wplace-theme-classic-light #wplace-settings-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; + color: var(--wplace-text) !important; +} + +/* Settings dialog text elements */ +.wplace-theme-classic-light .wplace-stat-colors-grid * { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light #wplace-settings-container * { + color: var(--wplace-text) !important; +} + +.wplace-theme-classic-light #wplace-settings-container .wplace-settings-section-wrapper * { + color: var(--wplace-text) !important; +} + +/* Color palette text labels */ +.wplace-theme-classic-light .wplace-color-label { + color: var(--wplace-text) !important; + font-weight: 500 !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-color-name { + color: var(--wplace-text) !important; + font-weight: 500 !important; +} + +.wplace-theme-classic-light .wplace-color-item-name { + color: #000 !important; + font-weight: 600 !important; + text-shadow: none !important; +} + +/* Resize dialog */ +.wplace-theme-classic-light .resize-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 16px 48px rgb(0 0 0 / 20%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; +} + +/* Settings header */ +.wplace-theme-classic-light .wplace-settings-header { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border-bottom: 1px solid rgb(0 0 0 / 15%) !important; +} + +.wplace-theme-classic-light .wplace-settings-title { + color: var(--wplace-text) !important; + text-shadow: none !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn { + background: rgb(0 0 0 / 5%) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; + border-radius: 50% !important; + color: var(--wplace-text) !important; + transition: all 0.3s ease !important; +} + +.wplace-theme-classic-light .wplace-settings-close-btn:hover { + background: rgb(220 53 69 / 10%) !important; + border-color: var(--wplace-error) !important; + box-shadow: 0 0 12px rgb(220 53 69 / 30%) !important; +} + +/* Section titles */ +.wplace-theme-classic-light .wplace-section-title { + color: var(--wplace-highlight) !important; + text-shadow: none !important; + font-weight: 600 !important; +} + /* Button variants with classic light colors */ -.wplace-theme--classic-light .wplace-btn-start { +.wplace-theme-classic-light .wplace-btn-start { background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important; color: white !important; font-weight: 600 !important; } -.wplace-theme--classic-light .wplace-btn-start:hover:not(:disabled) { - box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4) !important; +.wplace-theme-classic-light .wplace-btn-start:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(40 167 69 / 40%) !important; } -.wplace-theme--classic-light .wplace-btn-stop { +.wplace-theme-classic-light .wplace-btn-stop { background: linear-gradient(135deg, #dc3545 0%, #c82333 100%) !important; color: white !important; font-weight: 600 !important; } -.wplace-theme--classic-light .wplace-btn-stop:hover:not(:disabled) { - box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4) !important; +.wplace-theme-classic-light .wplace-btn-stop:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(220 53 69 / 40%) !important; } -.wplace-theme--classic-light .wplace-btn-upload { - background: rgba(111, 66, 193, 0.1) !important; +.wplace-theme-classic-light .wplace-btn-upload { + background: rgb(111 66 193 / 10%) !important; border: 2px dashed var(--wplace-highlight) !important; color: var(--wplace-highlight) !important; } -.wplace-theme--classic-light .wplace-btn-upload:hover:not(:disabled) { - background: rgba(111, 66, 193, 0.15) !important; - box-shadow: 0 4px 12px rgba(111, 66, 193, 0.2) !important; +.wplace-theme-classic-light .wplace-btn-upload:hover:not(:disabled) { + background: rgb(111 66 193 / 15%) !important; + box-shadow: 0 4px 12px rgb(111 66 193 / 20%) !important; } /* Progress bars with clean light styling */ -.wplace-theme--classic-light .wplace-progress { - background: rgba(0, 0, 0, 0.1) !important; - border: 1px solid rgba(0, 0, 0, 0.15) !important; +.wplace-theme-classic-light .wplace-progress { + background: rgb(0 0 0 / 10%) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; border-radius: 12px !important; } -.wplace-theme--classic-light .wplace-progress-bar { +.wplace-theme-classic-light .wplace-progress-bar { background: linear-gradient(135deg, #6f42c1 0%, #9370db 100%) !important; box-shadow: none !important; } -.wplace-theme--classic-light .wplace-progress-bar::after { - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent) !important; +.wplace-theme-classic-light .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent) !important; } /* Status indicators with light colors */ -.wplace-theme--classic-light .status-success { - background: rgba(40, 167, 69, 0.1) !important; +.wplace-theme-classic-light .status-success { + background: rgb(40 167 69 / 10%) !important; border-color: var(--wplace-success) !important; color: var(--wplace-success) !important; - box-shadow: 0 0 15px rgba(40, 167, 69, 0.2) !important; + box-shadow: 0 0 15px rgb(40 167 69 / 20%) !important; text-shadow: none !important; } -.wplace-theme--classic-light .status-error { - background: rgba(220, 53, 69, 0.1) !important; +.wplace-theme-classic-light .status-error { + background: rgb(220 53 69 / 10%) !important; border-color: var(--wplace-error) !important; color: var(--wplace-error) !important; - box-shadow: 0 0 15px rgba(220, 53, 69, 0.2) !important; + box-shadow: 0 0 15px rgb(220 53 69 / 20%) !important; text-shadow: none !important; } -.wplace-theme--classic-light .status-default { - background: rgba(111, 66, 193, 0.1) !important; +.wplace-theme-classic-light .status-default { + background: rgb(111 66 193 / 10%) !important; border-color: var(--wplace-highlight) !important; color: var(--wplace-highlight) !important; text-shadow: none !important; } -.wplace-theme--classic-light .wplace-stat-label { +.wplace-theme-classic-light .wplace-stat-label { color: var(--wplace-text) !important; text-shadow: none !important; font-weight: 500 !important; } -.wplace-theme--classic-light .wplace-stat-value { +.wplace-theme-classic-light .wplace-stat-value { color: var(--wplace-highlight) !important; text-shadow: none !important; font-weight: 600 !important; } /* Sections with light styling */ -.wplace-theme--classic-light .wplace-section { - background: rgba(0, 0, 0, 0.03) !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; +.wplace-theme-classic-light .wplace-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; border-radius: 12px !important; } -.wplace-theme--classic-light .wplace-status-section { - background: rgba(0, 0, 0, 0.03) !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; +.wplace-theme-classic-light .wplace-status-section { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; border-radius: 12px !important; } -.wplace-theme--classic-light .wplace-settings-section-wrapper { - background: rgba(0, 0, 0, 0.03) !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; +.wplace-theme-classic-light .wplace-settings-section-wrapper { + background: rgb(0 0 0 / 3%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; border-radius: 12px !important; } /* Form controls with light styling */ -.wplace-theme--classic-light .wplace-settings-select { - background: #ffffff !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; +.wplace-theme-classic-light .wplace-settings-select { + background: #fff !important; + border: 1px solid rgb(0 0 0 / 20%) !important; border-radius: 8px !important; color: var(--wplace-text) !important; font-family: 'Segoe UI', Roboto, sans-serif !important; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; } -.wplace-theme--classic-light .wplace-settings-select:focus { +.wplace-theme-classic-light .wplace-settings-select:focus { border-color: var(--wplace-highlight) !important; - box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.3) !important; + box-shadow: 0 0 0 2px rgb(111 66 193 / 30%) !important; } /* Dropdown menu options */ -.wplace-theme--classic-light .wplace-settings-select option { - background: #ffffff !important; +.wplace-theme-classic-light .wplace-settings-select option { + background: #fff !important; color: var(--wplace-text) !important; } -.wplace-theme--classic-light .wplace-settings-option { - background: #ffffff !important; +.wplace-theme-classic-light .wplace-settings-option { + background: #fff !important; color: var(--wplace-text) !important; } /* Sliders with classic gradient */ -.wplace-theme--classic-light .wplace-speed-slider { +.wplace-theme-classic-light .wplace-speed-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; border-radius: 4px !important; } -.wplace-theme--classic-light .wplace-overlay-opacity-slider { +.wplace-theme-classic-light .wplace-overlay-opacity-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; border-radius: 4px !important; } -.wplace-theme--classic-light .wplace-slider { +.wplace-theme-classic-light .wplace-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; border-radius: 4px !important; } -.wplace-theme--classic-light .wplace-slider::-webkit-slider-thumb { +.wplace-theme-classic-light .wplace-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--wplace-slider-thumb-bg); - border: 1px solid rgba(0, 0, 0, 0.2); + border: 1px solid rgb(0 0 0 / 20%); cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 4px rgb(0 0 0 / 20%); } -.wplace-theme--classic-light .wplace-speed-value { +.wplace-theme-classic-light .wplace-speed-value { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important; border-radius: 8px !important; color: white !important; font-weight: 600 !important; - box-shadow: 0 3px 10px rgba(79, 172, 254, 0.3) !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; + box-shadow: 0 3px 10px rgb(79 172 254 / 30%) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; } /* Settings labels */ -.wplace-theme--classic-light .wplace-settings-section-label { +.wplace-theme-classic-light .wplace-settings-section-label { color: var(--wplace-highlight) !important; text-shadow: none !important; font-family: 'Segoe UI', Roboto, sans-serif !important; @@ -419,65 +422,66 @@ } /* Icon colors for classic light theme */ -.wplace-theme--classic-light .wplace-icon-key { color: #4facfe; } -.wplace-theme--classic-light .wplace-icon-robot { color: #4facfe; } -.wplace-theme--classic-light .wplace-icon-speed { color: #4facfe; } -.wplace-theme--classic-light .wplace-icon-bell { color: #ffc107; } -.wplace-theme--classic-light .wplace-icon-palette { color: #f093fb; } -.wplace-theme--classic-light .wplace-icon-globe { color: #ffeaa7; } -.wplace-theme--classic-light .wplace-icon-paint { color: #4facfe; } -.wplace-theme--classic-light .wplace-icon-eye { color: #6f42c1; } +.wplace-theme-classic-light .wplace-icon-key { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-robot { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-speed { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-bell { color: #ffc107; } +.wplace-theme-classic-light .wplace-icon-palette { color: #f093fb; } +.wplace-theme-classic-light .wplace-icon-globe { color: #ffeaa7; } +.wplace-theme-classic-light .wplace-icon-paint { color: #4facfe; } +.wplace-theme-classic-light .wplace-icon-eye { color: #6f42c1; } /* Clean light theme animations */ -@keyframes lightShimmer { +@keyframes light-shimmer { 0% { transform: translateX(-100%); } + 100% { transform: translateX(200%); } } /* Turnstile/CF checkbox overlay - CRITICAL FIX */ -.wplace-theme--classic-light .wplace-turnstile-overlay { - background: rgba(255, 255, 255, 0.98) !important; +.wplace-theme-classic-light .wplace-turnstile-overlay { + background: rgb(255 255 255 / 98%) !important; border-radius: 12px !important; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 30%) !important; backdrop-filter: blur(10px) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; color: var(--wplace-text) !important; font-family: 'Segoe UI', Roboto, sans-serif !important; } -.wplace-theme--classic-light .wplace-turnstile-title { +.wplace-theme-classic-light .wplace-turnstile-title { color: var(--wplace-text) !important; } -.wplace-theme--classic-light .wplace-turnstile-hide-btn { +.wplace-theme-classic-light .wplace-turnstile-hide-btn { color: var(--wplace-text) !important; - border: 1px solid rgba(0, 0, 0, 0.2) !important; + border: 1px solid rgb(0 0 0 / 20%) !important; border-radius: 6px !important; - background: rgba(0, 0, 0, 0.05) !important; + background: rgb(0 0 0 / 5%) !important; } -.wplace-theme--classic-light .wplace-turnstile-hide-btn:hover { - background: rgba(0, 0, 0, 0.1) !important; +.wplace-theme-classic-light .wplace-turnstile-hide-btn:hover { + background: rgb(0 0 0 / 10%) !important; } /* Color swatches with light styling */ -.wplace-theme--classic-light .wplace-color-swatch { - border: 1px solid rgba(0, 0, 0, 0.2) !important; +.wplace-theme-classic-light .wplace-color-swatch { + border: 1px solid rgb(0 0 0 / 20%) !important; border-radius: 4px !important; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; + box-shadow: 0 2px 8px rgb(0 0 0 / 10%) !important; } -.wplace-theme--classic-light .wplace-color-swatch:hover { - box-shadow: 0 4px 16px rgba(111, 66, 193, 0.3) !important; +.wplace-theme-classic-light .wplace-color-swatch:hover { + box-shadow: 0 4px 16px rgb(111 66 193 / 30%) !important; transform: translateY(-1px); } -.wplace-theme--classic-light .wplace-stat-colors-grid { - background: rgba(0, 0, 0, 0.05) !important; +.wplace-theme-classic-light .wplace-stat-colors-grid { + background: rgb(0 0 0 / 5%) !important; border-radius: 8px !important; - border: 1px solid rgba(0, 0, 0, 0.1) !important; + border: 1px solid rgb(0 0 0 / 10%) !important; } diff --git a/themes/classic.css b/themes/classic.css index 910f449..f71efe3 100644 --- a/themes/classic.css +++ b/themes/classic.css @@ -1,21 +1,20 @@ /* WPlace Auto-Image Bot - Classic Theme */ /* Clean, modern UI with gradients and subtle effects */ -.wplace-theme--classic { - --wplace-primary: linear-gradient(135deg, #000000 0%, #1a1a1a 100%); - --wplace-secondary: linear-gradient(135deg, #111111 0%, #2a2a2a 100%); - --wplace-accent: #222222; - --wplace-text: #ffffff; +.wplace-theme-classic { + --wplace-primary: linear-gradient(135deg, #000 0%, #1a1a1a 100%); + --wplace-secondary: linear-gradient(135deg, #111 0%, #2a2a2a 100%); + --wplace-accent: #222; + --wplace-text: #fff; --wplace-highlight: #775ce3; - --wplace-success: #00ff00; - --wplace-error: #ff0000; - --wplace-warning: #ffaa00; - + --wplace-success: #0f0; + --wplace-error: #f00; + --wplace-warning: #fa0; --wplace-radius: 12px; - --wplace-shadow: 0 8px 32px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1); + --wplace-btn-radius: 16px; + --wplace-shadow: 0 8px 32px rgb(0 0 0 / 60%), 0 0 0 1px rgb(255 255 255 / 10%); --wplace-backdrop: blur(10px); - --wplace-font: 'Segoe UI', Roboto, sans-serif; - + --wplace-font: 'Segoe UI', roboto, sans-serif; --wplace-scanline: 0; --wplace-pixel-blink: 0; @@ -27,47 +26,16 @@ /* Additional UI colors */ --wplace-danger: #ff6a6a; --wplace-danger-dark: #ff4757; - --wplace-muted-text: #ffffffBB; + --wplace-muted-text: #fffB; --wplace-highlight-secondary: #d3a4ff; - /* 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 */ - --wplace-slider-thumb-bg: white; --wplace-slider-track-bg: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); } /* Classic theme container styling */ :root #wplace-image-bot-container, -.wplace-theme--classic #wplace-image-bot-container { +.wplace-theme-classic #wplace-image-bot-container { background: var(--wplace-primary); color: var(--wplace-text); border-radius: var(--wplace-radius); @@ -78,7 +46,7 @@ } :root #wplace-stats-container, -.wplace-theme--classic #wplace-stats-container { +.wplace-theme-classic #wplace-stats-container { background: var(--wplace-primary); color: var(--wplace-text); border-radius: var(--wplace-radius); @@ -89,7 +57,7 @@ } :root #wplace-settings-container, -.wplace-theme--classic #wplace-settings-container { +.wplace-theme-classic #wplace-settings-container { background: var(--wplace-primary); color: var(--wplace-text); border-radius: var(--wplace-radius); @@ -100,48 +68,55 @@ } :root .wplace-header, -.wplace-theme--classic .wplace-header { +.wplace-theme-classic .wplace-header { background: var(--wplace-secondary); color: var(--wplace-highlight); + text-shadow: 0 1px 2px rgb(0 0 0 / 50%); } :root .wplace-section-title, -.wplace-theme--classic .wplace-section-title { +.wplace-theme-classic .wplace-section-title { color: var(--wplace-highlight); } :root .wplace-stat-value, -.wplace-theme--classic .wplace-stat-value { +.wplace-theme-classic .wplace-stat-value { color: var(--wplace-highlight); } :root .wplace-status-section, -.wplace-theme--classic .wplace-status-section { +.wplace-theme-classic .wplace-status-section { border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); } :root .wplace-section, -.wplace-theme--classic .wplace-section { +.wplace-theme-classic .wplace-section { border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); } :root .wplace-stats, -.wplace-theme--classic .wplace-stats { +.wplace-theme-classic .wplace-stats { border-radius: var(--wplace-radius); + background: rgb(255 255 255 / 3%); + border: 1px solid rgb(255 255 255 / 10%); } :root .wplace-status, -.wplace-theme--classic .wplace-status { +.wplace-theme-classic .wplace-status { border-radius: var(--wplace-radius); } :root .wplace-alert-base, -.wplace-theme--classic .wplace-alert-base { +.wplace-theme-classic .wplace-alert-base { border-radius: var(--wplace-radius); + font-family: var(--wplace-font); } :root .wplace-settings-container-base, -.wplace-theme--classic .wplace-settings-container-base { +.wplace-theme-classic .wplace-settings-container-base { font-family: var(--wplace-font); border-radius: var(--wplace-radius); background: var(--wplace-primary); @@ -152,357 +127,334 @@ /* Button styling for classic theme */ :root .wplace-btn, -.wplace-theme--classic .wplace-btn { - background: linear-gradient(135deg, #222222 0%, #4a4a4a 100%); - border: 1px solid rgba(255,255,255,0.1); +.wplace-theme-classic .wplace-btn { + background: linear-gradient(135deg, #222 0%, #4a4a4a 100%); + border: 1px solid rgb(255 255 255 / 10%); color: var(--wplace-text); - border-radius: var(--wplace-radius); + border-radius: var(--wplace-btn-radius); font-family: var(--wplace-font); } :root .wplace-btn::before, -.wplace-theme--classic .wplace-btn::before { - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); -} - -:root .wplace-btn:hover:not(:disabled), -.wplace-theme--classic .wplace-btn:hover:not(:disabled) { - box-shadow: 0 4px 12px rgba(0,0,0,0.4); +.wplace-theme-classic .wplace-btn::before { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 10%), transparent); } :root .wplace-btn:disabled, -.wplace-theme--classic .wplace-btn:disabled { +.wplace-theme-classic .wplace-btn:disabled { box-shadow: none !important; } +:root .wplace-btn:hover:not(:disabled), +.wplace-theme-classic .wplace-btn:hover:not(:disabled) { + box-shadow: 0 4px 12px rgb(0 0 0 / 40%); +} + :root .wplace-btn-overlay.active, -.wplace-theme--classic .wplace-btn-overlay.active { +.wplace-theme-classic .wplace-btn-overlay.active { background: linear-gradient(135deg, #29b6f6 0%, #8e2de2 100%); box-shadow: 0 0 15px #8e2de2; } /* Button variants - exact upstream main colors */ :root .wplace-btn-primary, -.wplace-theme--classic .wplace-btn-primary { - background: linear-gradient(135deg, #222222 0%, #6a5acd 100%); +.wplace-theme-classic .wplace-btn-primary { + background: linear-gradient(135deg, #222 0%, #6a5acd 100%); color: var(--wplace-text); } :root .wplace-btn-upload, -.wplace-theme--classic .wplace-btn-upload { - background: linear-gradient(135deg, #111111 0%, #4a4a4a 100%); +.wplace-theme-classic .wplace-btn-upload { + background: linear-gradient(135deg, #111 0%, #4a4a4a 100%); color: var(--wplace-text); border: 1px dashed var(--wplace-highlight) !important; } :root .wplace-btn-start, -.wplace-theme--classic .wplace-btn-start { +.wplace-theme-classic .wplace-btn-start { background: linear-gradient(135deg, var(--wplace-success) 0%, #228b22 100%); color: white; } :root .wplace-btn-stop, -.wplace-theme--classic .wplace-btn-stop { +.wplace-theme-classic .wplace-btn-stop { background: linear-gradient(135deg, var(--wplace-error) 0%, #dc143c 100%); color: white; } :root .wplace-btn-select, -.wplace-theme--classic .wplace-btn-select { +.wplace-theme-classic .wplace-btn-select { background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); color: white; } :root .wplace-btn-file, -.wplace-theme--classic .wplace-btn-file { +.wplace-theme-classic .wplace-btn-file { background: linear-gradient(135deg, #ff8c00 0%, #ff7f50 100%); color: white; } :root .wplace-btn.active, :root .wplace-btn[aria-pressed="true"], -.wplace-theme--classic .wplace-btn.active, -.wplace-theme--classic .wplace-btn[aria-pressed="true"] { +.wplace-theme-classic .wplace-btn.active, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] { background: var(--wplace-highlight) !important; - color: #000000 !important; + color: #000 !important; border-color: var(--wplace-text) !important; - box-shadow: 0 0 8px rgba(0,0,0,0.25) inset, 0 0 6px rgba(0,0,0,0.2) !important; + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%) !important; } :root .wplace-btn.active i, :root .wplace-btn[aria-pressed="true"] i, -.wplace-theme--classic .wplace-btn.active i, -.wplace-theme--classic .wplace-btn[aria-pressed="true"] i { - filter: drop-shadow(0 0 3px #000000); +.wplace-theme-classic .wplace-btn.active i, +.wplace-theme-classic .wplace-btn[aria-pressed="true"] i { + filter: drop-shadow(0 0 3px #000); } :root .mask-mode-group .wplace-btn.active, :root .mask-mode-group .wplace-btn[aria-pressed="true"], -.wplace-theme--classic .mask-mode-group .wplace-btn.active, -.wplace-theme--classic .mask-mode-group .wplace-btn[aria-pressed="true"] { +.wplace-theme-classic .mask-mode-group .wplace-btn.active, +.wplace-theme-classic .mask-mode-group .wplace-btn[aria-pressed="true"] { background: var(--wplace-highlight); - color: #000000; + color: #000; border-color: var(--wplace-text); - box-shadow: 0 0 8px rgba(0,0,0,0.25) inset, 0 0 6px rgba(0,0,0,0.2); + box-shadow: 0 0 8px rgb(0 0 0 / 25%) inset, 0 0 6px rgb(0 0 0 / 20%); } /* Status styling for classic theme */ :root .status-default, -.wplace-theme--classic .status-default { - background: rgba(255,255,255,0.1); +.wplace-theme-classic .status-default { + background: rgb(255 255 255 / 10%); border-color: var(--wplace-text); color: var(--wplace-text); } :root .status-success, -.wplace-theme--classic .status-success { - background: rgba(0, 255, 0, 0.1); +.wplace-theme-classic .status-success { + background: rgb(0 255 0 / 10%); border-color: var(--wplace-success); color: var(--wplace-success); box-shadow: 0 0 15px var(--wplace-success); } :root .status-error, -.wplace-theme--classic .status-error { - background: rgba(255, 0, 0, 0.1); +.wplace-theme-classic .status-error { + background: rgb(255 0 0 / 10%); border-color: var(--wplace-error); color: var(--wplace-error); box-shadow: 0 0 15px var(--wplace-error); } :root .status-warning, -.wplace-theme--classic .status-warning { - background: rgba(255, 165, 0, 0.1); +.wplace-theme-classic .status-warning { + background: rgb(255 165 0 / 10%); border-color: var(--wplace-warning); color: var(--wplace-warning); box-shadow: 0 0 15px var(--wplace-warning); } :root .wplace-status.status-default, -.wplace-theme--classic .wplace-status.status-default { +.wplace-theme-classic .wplace-status.status-default { color: var(--wplace-text); - background: rgba(255,255,255,0.1); + background: rgb(255 255 255 / 10%); border-color: var(--wplace-text); } /* Alert styling for classic theme */ :root .wplace-alert-info, -.wplace-theme--classic .wplace-alert-info { +.wplace-theme-classic .wplace-alert-info { background: linear-gradient(135deg, #3498db, #2980b9); - box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3); + box-shadow: 0 4px 12px rgb(52 152 219 / 30%); } :root .wplace-alert-success, -.wplace-theme--classic .wplace-alert-success { +.wplace-theme-classic .wplace-alert-success { background: linear-gradient(135deg, #27ae60, #229954); - box-shadow: 0 4px 12px rgba(39, 174, 96, 0.3); + box-shadow: 0 4px 12px rgb(39 174 96 / 30%); } :root .wplace-alert-warning, -.wplace-theme--classic .wplace-alert-warning { +.wplace-theme-classic .wplace-alert-warning { background: linear-gradient(135deg, #f39c12, #e67e22); - box-shadow: 0 4px 12px rgba(243, 156, 18, 0.3); + box-shadow: 0 4px 12px rgb(243 156 18 / 30%); } :root .wplace-alert-error, -.wplace-theme--classic .wplace-alert-error { +.wplace-theme-classic .wplace-alert-error { background: linear-gradient(135deg, #e74c3c, #c0392b); - box-shadow: 0 4px 12px rgba(231, 76, 60, 0.3); + box-shadow: 0 4px 12px rgb(231 76 60 / 30%); } /* Progress bar styling for classic theme */ :root .wplace-progress, -.wplace-theme--classic .wplace-progress { - background: rgba(0,0,0,0.3); - border: 1px solid rgba(255,255,255,0.1); +.wplace-theme-classic .wplace-progress { + background: rgb(0 0 0 / 30%); + border: 1px solid rgb(255 255 255 / 10%); border-radius: var(--wplace-radius); } :root .wplace-progress-bar, -.wplace-theme--classic .wplace-progress-bar { +.wplace-theme-classic .wplace-progress-bar { background: linear-gradient(135deg, var(--wplace-highlight) 0%, #9370db 100%); } :root .wplace-progress-bar::after, -.wplace-theme--classic .wplace-progress-bar::after { - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent); +.wplace-theme-classic .wplace-progress-bar::after { + background: linear-gradient(90deg, transparent, rgb(255 255 255 / 30%), transparent); } /* Header and sections styling for classic theme */ :root .wplace-header-btn, -.wplace-theme--classic .wplace-header-btn { - background: rgba(255,255,255,0.1); +.wplace-theme-classic .wplace-header-btn { + background: rgb(255 255 255 / 10%); color: var(--wplace-highlight); border-radius: 4px; font-family: var(--wplace-font); } :root .wplace-header-btn:hover, -.wplace-theme--classic .wplace-header-btn:hover { - background: #222222; +.wplace-theme-classic .wplace-header-btn:hover { + background: #222; color: var(--wplace-text); } -:root .wplace-status-section, -.wplace-theme--classic .wplace-status-section { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); -} - -: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, -.wplace-theme--classic .wplace-stats { - background: rgba(255, 255, 255, 0.03); - border: 1px solid rgba(255, 255, 255, 0.1); -} - /* Font and typography for classic theme */ :root .wplace-turnstile-overlay, -.wplace-theme--classic .wplace-turnstile-overlay { - background: rgba(0, 0, 0, 0.9) !important; +.wplace-theme-classic .wplace-turnstile-overlay { + background: rgb(0 0 0 / 90%) !important; border-radius: var(--wplace-radius) !important; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4) !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 40%) !important; backdrop-filter: var(--wplace-backdrop) !important; - border: 1px solid rgba(255, 255, 255, 0.2) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; color: var(--wplace-text) !important; font-family: var(--wplace-font) !important; } -:root .wplace-alert-base, -.wplace-theme--classic .wplace-alert-base { - font-family: var(--wplace-font); -} - /* Auto light/dark support for classic theme */ @media (prefers-color-scheme: light) { :root .theme-auto, - .wplace-theme--classic .theme-auto { - --wplace-primary: #ffffff; + .wplace-theme-classic .theme-auto { + --wplace-primary: #fff; --wplace-secondary: #f5f5f5; --wplace-accent: #007acc; - --wplace-text: #333333; + --wplace-text: #333; } } @media (prefers-color-scheme: dark) { :root .theme-auto, - .wplace-theme--classic .theme-auto { + .wplace-theme-classic .theme-auto { --wplace-primary: #1e1e1e; --wplace-secondary: #2d2d30; --wplace-accent: var(--wplace-highlight); - --wplace-text: #ffffff; + --wplace-text: #fff; } } /* Border and color styling for classic theme */ :root .wplace-color-item-name, -.wplace-theme--classic .wplace-color-item-name { +.wplace-theme-classic .wplace-color-item-name { color: #ccc; } :root .wplace-color-swatch, -.wplace-theme--classic .wplace-color-swatch { - border: 1px solid rgba(255, 255, 255, 0.2); +.wplace-theme-classic .wplace-color-swatch { + border: 1px solid rgb(255 255 255 / 20%); border-radius: 4px; } :root .wplace-stat-color-swatch, -.wplace-theme--classic .wplace-stat-color-swatch { +.wplace-theme-classic .wplace-stat-color-swatch { border-radius: 3px; - border: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.5); + border: 1px solid rgb(255 255 255 / 10%); + box-shadow: inset 0 0 2px rgb(0 0 0 / 50%); } :root .resize-tools button, -.wplace-theme--classic .resize-tools button { +.wplace-theme-classic .resize-tools button { border-radius: 6px; - border: 1px solid rgba(255, 255, 255, 0.2); - background: rgba(255, 255, 255, 0.06); + border: 1px solid rgb(255 255 255 / 20%); + background: rgb(255 255 255 / 6%); color: #fff; } :root .resize-slider, -.wplace-theme--classic .resize-slider { +.wplace-theme-classic .resize-slider { background: #ccc; border-radius: var(--wplace-radius); } /* Text effects and filters for classic theme */ -:root .wplace-header, -.wplace-theme--classic .wplace-header { - text-shadow: 0 1px 2px rgba(0,0,0,0.5); -} - :root .wplace-color-swatch:not(.active), -.wplace-theme--classic .wplace-color-swatch:not(.active) { +.wplace-theme-classic .wplace-color-swatch:not(.active) { filter: grayscale(80%); } +:root .wplace-color-swatch.unavailable, +.wplace-theme-classic .wplace-color-swatch.unavailable { + border-color: #666; +} + :root .wplace-color-swatch.unavailable:not(.active), -.wplace-theme--classic .wplace-color-swatch.unavailable:not(.active) { +.wplace-theme-classic .wplace-color-swatch.unavailable:not(.active) { filter: grayscale(90%); } :root .wplace-color-swatch.active::after, -.wplace-theme--classic .wplace-color-swatch.active::after { +.wplace-theme-classic .wplace-color-swatch.active::after { text-shadow: 0 0 3px black; } /* Icon colors for classic theme */ :root .wplace-icon-key, -.wplace-theme--classic .wplace-icon-key { +.wplace-theme-classic .wplace-icon-key { color: #4facfe; } :root .wplace-icon-robot, -.wplace-theme--classic .wplace-icon-robot { +.wplace-theme-classic .wplace-icon-robot { color: #4facfe; } :root .wplace-icon-speed, -.wplace-theme--classic .wplace-icon-speed { +.wplace-theme-classic .wplace-icon-speed { color: #4facfe; } :root .wplace-icon-bell, -.wplace-theme--classic .wplace-icon-bell { +.wplace-theme-classic .wplace-icon-bell { color: #ffd166; } :root .wplace-icon-palette, -.wplace-theme--classic .wplace-icon-palette { +.wplace-theme-classic .wplace-icon-palette { color: #f093fb; } :root .wplace-icon-globe, -.wplace-theme--classic .wplace-icon-globe { +.wplace-theme-classic .wplace-icon-globe { color: #ffeaa7; } :root .wplace-icon-paint, -.wplace-theme--classic .wplace-icon-paint { +.wplace-theme-classic .wplace-icon-paint { color: #4facfe; } :root .wplace-icon-eye, -.wplace-theme--classic .wplace-icon-eye { +.wplace-theme-classic .wplace-icon-eye { color: var(--wplace-highlight); } /* Form controls and sliders for classic theme */ :root .wplace-slider, -.wplace-theme--classic .wplace-slider { +.wplace-theme-classic .wplace-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); border-radius: 4px; } :root .wplace-slider::-webkit-slider-thumb, -.wplace-theme--classic .wplace-slider::-webkit-slider-thumb { +.wplace-theme-classic .wplace-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; @@ -514,135 +466,125 @@ } :root .wplace-speed-slider, -.wplace-theme--classic .wplace-speed-slider { - border-radius: 3px; +.wplace-theme-classic .wplace-speed-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; } :root .wplace-speed-slider::-webkit-slider-thumb, -.wplace-theme--classic .wplace-speed-slider::-webkit-slider-thumb { +.wplace-theme-classic .wplace-speed-slider::-webkit-slider-thumb { border-radius: 50%; border: 2px solid; } :root .wplace-speed-slider::-moz-range-thumb, -.wplace-theme--classic .wplace-speed-slider::-moz-range-thumb { +.wplace-theme-classic .wplace-speed-slider::-moz-range-thumb { border-radius: 50%; border: 2px solid; } -:root .wplace-color-swatch.unavailable, -.wplace-theme--classic .wplace-color-swatch.unavailable { - border-color: #666; -} - :root .wplace-turnstile-title, -.wplace-theme--classic .wplace-turnstile-title { +.wplace-theme-classic .wplace-turnstile-title { color: var(--wplace-text) !important; } :root .wplace-turnstile-hide-btn, -.wplace-theme--classic .wplace-turnstile-hide-btn { +.wplace-theme-classic .wplace-turnstile-hide-btn { color: var(--wplace-text) !important; - border: 1px solid rgba(255, 255, 255, 0.2) !important; + border: 1px solid rgb(255 255 255 / 20%) !important; border-radius: 6px !important; } :root .wplace-turnstile-hide-btn:hover, -.wplace-theme--classic .wplace-turnstile-hide-btn:hover { - background: rgba(255, 255, 255, 0.1) !important; +.wplace-theme-classic .wplace-turnstile-hide-btn:hover { + background: rgb(255 255 255 / 10%) !important; } :root .wplace-settings-select, -.wplace-theme--classic .wplace-settings-select { - background: rgba(255,255,255,0.15); +.wplace-theme-classic .wplace-settings-select { + background: rgb(255 255 255 / 15%); color: white; - border: 1px solid rgba(255,255,255,0.2); + border: 1px solid rgb(255 255 255 / 20%); border-radius: 8px; - box-shadow: 0 3px 10px rgba(0,0,0,0.1); + box-shadow: 0 3px 10px rgb(0 0 0 / 10%); } :root .wplace-settings-option, -.wplace-theme--classic .wplace-settings-option { +.wplace-theme-classic .wplace-settings-option { background: #2d3748; color: white; } :root .wplace-stat-colors-grid, -.wplace-theme--classic .wplace-stat-colors-grid { - background: rgba(0, 0, 0, 0.2); +.wplace-theme-classic .wplace-stat-colors-grid { + background: rgb(0 0 0 / 20%); border-radius: 4px; } :root .resize-overlay, -.wplace-theme--classic .resize-overlay { - background: rgba(0, 0, 0, 0.8); +.wplace-theme-classic .resize-overlay { + background: rgb(0 0 0 / 80%); } :root .wplace-overlay, -.wplace-theme--classic .wplace-overlay { - background: rgba(0, 0, 0, 0.8); +.wplace-theme-classic .wplace-overlay { + background: rgb(0 0 0 / 80%); } :root .wplace-settings-error, -.wplace-theme--classic .wplace-settings-error { - background: rgba(255, 0, 0, 0.4) !important; +.wplace-theme-classic .wplace-settings-error { + background: rgb(255 0 0 / 40%) !important; } /* Settings sections and batch controls for classic theme */ :root .wplace-settings-section-wrapper, -.wplace-theme--classic .wplace-settings-section-wrapper { - background: rgba(255,255,255,0.1); +.wplace-theme-classic .wplace-settings-section-wrapper { + background: rgb(255 255 255 / 10%); border-radius: var(--wplace-radius); - border: 1px solid rgba(255,255,255,0.1); + border: 1px solid rgb(255 255 255 / 10%); } :root .wplace-batch-controls, -.wplace-theme--classic .wplace-batch-controls { - background: rgba(255,255,255,0.1); +.wplace-theme-classic .wplace-batch-controls { + background: rgb(255 255 255 / 10%); border-radius: var(--wplace-radius); - border: 1px solid rgba(255,255,255,0.1); -} - -:root .wplace-speed-slider, -.wplace-theme--classic .wplace-speed-slider { - background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); - border-radius: 4px; + border: 1px solid rgb(255 255 255 / 10%); } :root .wplace-overlay-opacity-slider, -.wplace-theme--classic .wplace-overlay-opacity-slider { +.wplace-theme-classic .wplace-overlay-opacity-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); border-radius: 4px; } :root .wplace-speed-value, -.wplace-theme--classic .wplace-speed-value { +.wplace-theme-classic .wplace-speed-value { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 8px; color: white; - box-shadow: 0 3px 10px rgba(79, 172, 254, 0.3); - border: 1px solid rgba(255,255,255,0.2); + box-shadow: 0 3px 10px rgb(79 172 254 / 30%); + border: 1px solid rgb(255 255 255 / 20%); } /* Additional elements for classic theme */ :root .wplace-color-divider, -.wplace-theme--classic .wplace-color-divider { - background: rgba(255, 255, 255, 0.1); +.wplace-theme-classic .wplace-color-divider { + background: rgb(255 255 255 / 10%); } :root .wplace-settings-select option, -.wplace-theme--classic .wplace-settings-select option { +.wplace-theme-classic .wplace-settings-select option { background: #2d3748; color: white; } :root .wplace-settings-description, -.wplace-theme--classic .wplace-settings-description { - color: rgba(255,255,255,0.7); +.wplace-theme-classic .wplace-settings-description { + color: rgb(255 255 255 / 70%); } :root .wplace-settings-apply-btn, -.wplace-theme--classic .wplace-settings-apply-btn { +.wplace-theme-classic .wplace-settings-apply-btn { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; border-radius: 8px; @@ -650,9 +592,9 @@ /* Resize dialog styling for classic theme */ :root .resize-container, -.wplace-theme--classic .resize-container { - background: #000000; - border: 1px solid #ffffff; +.wplace-theme-classic .resize-container { + background: #000; + border: 1px solid #fff; border-radius: 12px; - box-shadow: 0 0 20px rgba(0,0,0,0.5); + box-shadow: 0 0 20px rgb(0 0 0 / 50%); } diff --git a/themes/neon.css b/themes/neon.css index f3a96d1..9ce3c5c 100644 --- a/themes/neon.css +++ b/themes/neon.css @@ -1,7 +1,7 @@ /* WPlace Auto-Image Bot - Neon Theme */ /* Retro cyberpunk aesthetic with green neon glow effects */ -.wplace-theme--neon { +.wplace-theme-neon { /* Neon theme colors - matching upstream main neon theme */ --wplace-primary: #1a1a2e; --wplace-secondary: #16213e; @@ -10,14 +10,14 @@ --wplace-highlight: #ff6b35; --wplace-success: #39ff14; --wplace-error: #ff073a; - --wplace-warning: #ffff00; + --wplace-warning: #ff0; /* Neon UI properties */ --wplace-radius: 0; --wplace-border-style: solid; --wplace-border-width: 2px; --wplace-border-color: #00ff41; - --wplace-shadow: 0 0 20px rgba(0, 255, 65, 0.3), inset 0 0 20px rgba(0, 255, 65, 0.1); + --wplace-shadow: 0 0 20px rgb(0 255 65 / 30%), inset 0 0 20px rgb(0 255 65 / 10%); --wplace-backdrop: none; --wplace-font: 'Press Start 2P', monospace, 'Courier New'; @@ -39,7 +39,7 @@ --wplace-danger: #ff073a; --wplace-danger-dark: #cc0531; --wplace-muted-text: #00ff4180; - --wplace-highlight-secondary: #ffaa00; + --wplace-highlight-secondary: #fa0; /* Text variants - neon style */ --wplace-text-secondary: #00ff41dd; @@ -48,51 +48,54 @@ --wplace-text-faded: #00ff41cc; /* Background variants - neon style */ - --wplace-bg-input: rgba(0, 255, 65, 0.15); - --wplace-bg-subtle: rgba(0, 255, 65, 0.08); - --wplace-bg-faint: rgba(0, 255, 65, 0.05); - --wplace-bg-ghost: rgba(0, 255, 65, 0.03); - --wplace-bg-whisper: rgba(0, 255, 65, 0.02); + --wplace-bg-input: rgb(0 255 65 / 15%); + --wplace-bg-subtle: rgb(0 255 65 / 8%); + --wplace-bg-faint: rgb(0 255 65 / 5%); + --wplace-bg-ghost: rgb(0 255 65 / 3%); + --wplace-bg-whisper: rgb(0 255 65 / 2%); /* Border variants - neon style */ - --wplace-border-subtle: rgba(0, 255, 65, 0.4); - --wplace-border-faint: rgba(0, 255, 65, 0.25); - --wplace-border-ghost: rgba(0, 255, 65, 0.15); - --wplace-border-ultra-faint: rgba(0, 255, 65, 0.08); + --wplace-border-subtle: rgb(0 255 65 / 40%); + --wplace-border-faint: rgb(0 255 65 / 25%); + --wplace-border-ghost: rgb(0 255 65 / 15%); + --wplace-border-ultra-faint: rgb(0 255 65 / 8%); /* Shadow variants - neon style */ - --wplace-shadow-drag: 0 12px 40px rgba(0, 255, 65, 0.6), 0 0 0 2px rgba(0, 255, 65, 0.8), 0 0 20px rgba(0, 255, 65, 0.3); - --wplace-shadow-notification: 0 4px 12px rgba(0, 255, 65, 0.4), 0 0 15px rgba(0, 255, 65, 0.2); - --wplace-shadow-slider-thumb: 0 3px 6px rgba(0, 255, 65, 0.5), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgba(0, 255, 65, 0.3); - --wplace-shadow-slider-hover: 0 4px 8px rgba(0, 255, 65, 0.6), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgba(0, 255, 65, 0.4); + --wplace-shadow-drag: 0 12px 40px rgb(0 255 65 / 60%), 0 0 0 2px rgb(0 255 65 / 80%), 0 0 20px rgb(0 255 65 / 30%); + --wplace-shadow-notification: 0 4px 12px rgb(0 255 65 / 40%), 0 0 15px rgb(0 255 65 / 20%); + --wplace-shadow-slider-thumb: 0 3px 6px rgb(0 255 65 / 50%), 0 0 0 2px var(--wplace-icon-primary), 0 0 10px rgb(0 255 65 / 30%); + --wplace-shadow-slider-hover: 0 4px 8px rgb(0 255 65 / 60%), 0 0 0 3px var(--wplace-icon-primary), 0 0 15px rgb(0 255 65 / 40%); /* Animation colors - neon style */ - --wplace-pulse-start: rgba(0, 255, 65, 0.8); - --wplace-pulse-mid: rgba(0, 255, 65, 0); - --wplace-pulse-end: rgba(0, 255, 65, 0); + --wplace-pulse-start: rgb(0 255 65 / 80%); + --wplace-pulse-mid: rgb(0 255 65 / 0%); + --wplace-pulse-end: rgb(0 255 65 / 0%); /* Slider colors - neon style */ --wplace-slider-thumb-bg: #00ff41; } /* Neon-specific styling overrides */ -.wplace-theme--neon #wplace-image-bot-container { +.wplace-theme-neon #wplace-image-bot-container { background: var(--wplace-primary) !important; border: 2px solid var(--wplace-text) !important; border-radius: 0 !important; - box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgba(0, 255, 65, 0.1) !important; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; font-family: var(--wplace-font) !important; } -.wplace-theme--neon #wplace-stats-container { +.wplace-theme-neon #wplace-stats-container { background: var(--wplace-primary) !important; border: 2px solid var(--wplace-text) !important; border-radius: 0 !important; - box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgba(0, 255, 65, 0.1) !important; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; font-family: var(--wplace-font) !important; + position: fixed !important; + overflow: hidden !important; + z-index: 9998 !important; } -.wplace-theme--neon .wplace-header { +.wplace-theme-neon .wplace-header { background: var(--wplace-secondary) !important; border-bottom: 1px solid var(--wplace-text) !important; color: var(--wplace-text) !important; @@ -100,19 +103,19 @@ font-family: var(--wplace-font) !important; } -.wplace-theme--neon .wplace-section { - background: rgba(22, 33, 62, 0.5) !important; +.wplace-theme-neon .wplace-section { + background: rgb(22 33 62 / 50%) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; } -.wplace-theme--neon .wplace-status-section { - background: rgba(22, 33, 62, 0.5) !important; +.wplace-theme-neon .wplace-status-section { + background: rgb(22 33 62 / 50%) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; } -.wplace-theme--neon .wplace-section-title { +.wplace-theme-neon .wplace-section-title { color: var(--wplace-text) !important; text-shadow: 0 0 8px var(--wplace-text) !important; text-transform: uppercase !important; @@ -120,7 +123,7 @@ font-size: 10px !important; } -.wplace-theme--neon .wplace-btn { +.wplace-theme-neon .wplace-btn { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; @@ -131,100 +134,101 @@ text-transform: uppercase !important; } -.wplace-theme--neon .wplace-btn:hover:not(:disabled) { - box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgba(0, 255, 65, 0.1) !important; - animation: neonGlow 1s ease-in-out infinite alternate !important; +/* Pixel blinking and neon glow animation for buttons */ +.wplace-theme-neon .wplace-btn:hover:not(:disabled) { + box-shadow: 0 0 15px var(--wplace-text), inset 0 0 15px rgb(0 255 65 / 10%) !important; + animation: pixel-blink 0.5s infinite, neon-glow 1s ease-in-out infinite alternate !important; } -.wplace-theme--neon .wplace-btn-start { +.wplace-theme-neon .wplace-btn-start { background: var(--wplace-secondary) !important; border-color: var(--wplace-success) !important; color: var(--wplace-success) !important; text-shadow: 0 0 8px var(--wplace-success) !important; } -.wplace-theme--neon .wplace-btn-stop { +.wplace-theme-neon .wplace-btn-stop { background: var(--wplace-secondary) !important; border-color: var(--wplace-error) !important; color: var(--wplace-error) !important; text-shadow: 0 0 8px var(--wplace-error) !important; } -.wplace-theme--neon .wplace-btn-upload { +.wplace-theme-neon .wplace-btn-upload { background: var(--wplace-secondary) !important; border: 1px dashed var(--wplace-highlight) !important; color: var(--wplace-highlight) !important; text-shadow: 0 0 8px var(--wplace-highlight) !important; } -.wplace-theme--neon .wplace-btn-select { +.wplace-theme-neon .wplace-btn-select { background: var(--wplace-secondary) !important; border-color: var(--wplace-highlight) !important; color: var(--wplace-highlight) !important; text-shadow: 0 0 8px var(--wplace-highlight) !important; } -.wplace-theme--neon .wplace-btn-file { +.wplace-theme-neon .wplace-btn-file { background: var(--wplace-secondary) !important; border-color: var(--wplace-warning) !important; color: var(--wplace-warning) !important; text-shadow: 0 0 8px var(--wplace-warning) !important; } -.wplace-theme--neon .wplace-progress { - background: rgba(0, 0, 0, 0.8) !important; +.wplace-theme-neon .wplace-progress { + background: rgb(0 0 0 / 80%) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; } -.wplace-theme--neon .wplace-progress-bar { +.wplace-theme-neon .wplace-progress-bar { background: linear-gradient(90deg, var(--wplace-success) 0%, var(--wplace-text) 100%) !important; box-shadow: 0 0 10px var(--wplace-success) !important; } -.wplace-theme--neon .wplace-stat-value { +.wplace-theme-neon .wplace-stat-value { color: var(--wplace-text) !important; text-shadow: 0 0 5px var(--wplace-text) !important; } -.wplace-theme--neon .status-success { - background: rgba(57, 255, 20, 0.1) !important; +.wplace-theme-neon .status-success { + background: rgb(57 255 20 / 10%) !important; border-color: var(--wplace-success) !important; color: var(--wplace-success) !important; box-shadow: 0 0 15px var(--wplace-success) !important; text-shadow: 0 0 8px var(--wplace-success) !important; } -.wplace-theme--neon .status-error { - background: rgba(255, 7, 58, 0.1) !important; +.wplace-theme-neon .status-error { + background: rgb(255 7 58 / 10%) !important; border-color: var(--wplace-error) !important; color: var(--wplace-error) !important; box-shadow: 0 0 15px var(--wplace-error) !important; text-shadow: 0 0 8px var(--wplace-error) !important; } -.wplace-theme--neon .status-default { - background: rgba(0, 255, 65, 0.1) !important; +.wplace-theme-neon .status-default { + background: rgb(0 255 65 / 10%) !important; border-color: var(--wplace-text) !important; color: var(--wplace-text) !important; text-shadow: 0 0 5px var(--wplace-text) !important; } /* Settings dialog neon styling */ -.wplace-theme--neon #wplace-settings-container { +.wplace-theme-neon #wplace-settings-container { background: var(--wplace-primary) !important; border: 2px solid var(--wplace-text) !important; border-radius: 0 !important; - box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgba(0, 255, 65, 0.1) !important; + box-shadow: 0 0 30px var(--wplace-text), inset 0 0 30px rgb(0 255 65 / 10%) !important; font-family: var(--wplace-font) !important; } -.wplace-theme--neon .wplace-settings-header { +.wplace-theme-neon .wplace-settings-header { background: var(--wplace-secondary) !important; border-bottom: 1px solid var(--wplace-text) !important; } -.wplace-theme--neon .wplace-settings-header h3 { +.wplace-theme-neon .wplace-settings-header h3 { color: var(--wplace-text) !important; text-shadow: 0 0 10px var(--wplace-text) !important; font-family: var(--wplace-font) !important; @@ -232,25 +236,25 @@ text-transform: uppercase !important; } -.wplace-theme--neon .wplace-settings-close-btn { +.wplace-theme-neon .wplace-settings-close-btn { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; color: var(--wplace-text) !important; } -.wplace-theme--neon .wplace-settings-close-btn:hover { +.wplace-theme-neon .wplace-settings-close-btn:hover { background: var(--wplace-error) !important; box-shadow: 0 0 15px var(--wplace-error) !important; } -.wplace-theme--neon .wplace-settings-section-wrapper { - background: rgba(22, 33, 62, 0.3) !important; +.wplace-theme-neon .wplace-settings-section-wrapper { + background: rgb(22 33 62 / 30%) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; } -.wplace-theme--neon .wplace-settings-select { +.wplace-theme-neon .wplace-settings-select { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; @@ -259,7 +263,7 @@ font-size: 11px !important; } -.wplace-theme--neon .wplace-settings-section-label { +.wplace-theme-neon .wplace-settings-section-label { color: var(--wplace-text) !important; text-shadow: 0 0 8px var(--wplace-text) !important; font-family: var(--wplace-font) !important; @@ -267,17 +271,17 @@ text-transform: uppercase !important; } -.wplace-theme--neon .wplace-speed-value { +.wplace-theme-neon .wplace-speed-value { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; color: var(--wplace-text) !important; text-shadow: 0 0 8px var(--wplace-text) !important; font-family: var(--wplace-font) !important; - box-shadow: 0 0 10px rgba(0, 255, 65, 0.3) !important; + box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important; } -.wplace-theme--neon .wplace-overlay-opacity-value { +.wplace-theme-neon .wplace-overlay-opacity-value { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; @@ -286,18 +290,18 @@ } /* Neon slider styling */ -.wplace-theme--neon .wplace-slider, -.wplace-theme--neon .wplace-speed-slider, -.wplace-theme--neon .wplace-overlay-opacity-slider { +.wplace-theme-neon .wplace-slider, +.wplace-theme-neon .wplace-speed-slider, +.wplace-theme-neon .wplace-overlay-opacity-slider { background: var(--wplace-secondary) !important; border: 1px solid var(--wplace-text) !important; border-radius: 0 !important; - box-shadow: 0 0 10px rgba(0, 255, 65, 0.3) !important; + box-shadow: 0 0 10px rgb(0 255 65 / 30%) !important; } -.wplace-theme--neon .wplace-slider::-webkit-slider-thumb, -.wplace-theme--neon .wplace-speed-slider::-webkit-slider-thumb, -.wplace-theme--neon .wplace-overlay-opacity-slider::-webkit-slider-thumb { +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; @@ -306,18 +310,18 @@ border: 2px solid var(--wplace-text) !important; cursor: pointer; transition: all 0.2s ease; - box-shadow: 0 0 8px rgba(0, 255, 65, 0.5) !important; + box-shadow: 0 0 8px rgb(0 255 65 / 50%) !important; } -.wplace-theme--neon .wplace-slider::-webkit-slider-thumb:hover, -.wplace-theme--neon .wplace-speed-slider::-webkit-slider-thumb:hover, -.wplace-theme--neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { +.wplace-theme-neon .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme-neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { transform: scale(1.1); box-shadow: 0 0 15px var(--wplace-text) !important; } /* Scanline animation for neon theme */ -.wplace-theme--neon #wplace-image-bot-container::before { +.wplace-theme-neon #wplace-image-bot-container::before { content: ''; position: absolute; top: 0; @@ -331,13 +335,8 @@ opacity: 0.7; } -.wplace-theme--neon #wplace-stats-container { - position: fixed !important; - overflow: hidden !important; - z-index: 9998 !important; -} -.wplace-theme--neon #wplace-stats-container::before { +.wplace-theme-neon #wplace-stats-container::before { content: ''; position: absolute; top: 0; @@ -347,29 +346,27 @@ background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); z-index: 1; pointer-events: none; - animation: scanlineStats 4s linear infinite; + animation: scanline-stats 4s linear infinite; opacity: 0.7; } -/* Pixel blinking animation for buttons */ -.wplace-theme--neon .wplace-btn:hover:not(:disabled) { - animation: pixelBlink 0.5s infinite, neonGlow 1s ease-in-out infinite alternate; -} /* Text glow animations */ -@keyframes neonGlow { +@keyframes neon-glow { 0%, 100% { - text-shadow: 0 0 5px currentColor, 0 0 10px currentColor, 0 0 15px currentColor; + text-shadow: 0 0 5px currentcolor, 0 0 10px currentcolor, 0 0 15px currentcolor; } + 50% { - text-shadow: 0 0 2px currentColor, 0 0 5px currentColor, 0 0 8px currentColor; + text-shadow: 0 0 2px currentcolor, 0 0 5px currentcolor, 0 0 8px currentcolor; } } -@keyframes pixelBlink { +@keyframes pixel-blink { 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.7; } @@ -379,25 +376,27 @@ 0% { transform: translateY(-100%); } + 100% { transform: translateY(400px); } } -@keyframes scanlineStats { +@keyframes scanline-stats { 0% { transform: translateY(-100%); } + 100% { transform: translateY(300px); } } /* Resize dialog styling for neon theme */ -.wplace-theme--neon .resize-container { +.wplace-theme-neon .resize-container { background: #1a1a2e !important; border: 3px solid #00ff41 !important; border-radius: 0 !important; - box-shadow: 0 0 30px rgba(0, 255, 65, 0.5) !important; + box-shadow: 0 0 30px rgb(0 255 65 / 50%) !important; font-family: 'Press Start 2P', monospace !important; }