diff --git a/Extension/Script-manager.js b/Extension/Script-manager.js index 98af2dc..a5fbf2a 100644 --- a/Extension/Script-manager.js +++ b/Extension/Script-manager.js @@ -34,6 +34,13 @@ description: 'Automated repair and maintenance tasks', icon: '🔧', category: 'utility' + }, + { + name: 'Acc-Switch.js', + displayName: '🔄 Account Switcher', + description: 'Temporary account switching script', + icon: '🔄', + category: 'automation' } ]; diff --git a/Extension/auto-image-styles.css b/Extension/auto-image-styles.css index 3e45a67..3cb715e 100644 --- a/Extension/auto-image-styles.css +++ b/Extension/auto-image-styles.css @@ -1920,3 +1920,162 @@ color: var(--wplace-icon-palette); margin-right: 6px; } + +/* Account Management Styles */ +.wplace-account-info { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 12px; + padding: 12px; + background: var(--wplace-secondary); + border: 1px solid var(--wplace-border-color); + border-radius: var(--wplace-radius); +} + +.wplace-account-current { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--wplace-text); +} + +.wplace-account-current i { + color: var(--wplace-highlight); + width: 16px; +} + +.wplace-account-current strong { + color: var(--wplace-success); +} + +.wplace-account-switch { + display: flex; + align-items: center; + gap: 8px; +} + +.wplace-toggle-switch { + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + user-select: none; +} + +.wplace-toggle-switch input[type="checkbox"] { + display: none; +} + +.wplace-toggle-slider { + position: relative; + width: 44px; + height: 24px; + background: var(--wplace-accent); + border: 1px solid var(--wplace-border-color); + border-radius: 12px; + transition: all 0.3s ease; +} + +.wplace-toggle-slider:before { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 18px; + height: 18px; + background: var(--wplace-text); + border-radius: 50%; + transition: all 0.3s ease; +} + +.wplace-toggle-switch input:checked + .wplace-toggle-slider { + background: var(--wplace-success); + border-color: var(--wplace-success); +} + +.wplace-toggle-switch input:checked + .wplace-toggle-slider:before { + transform: translateX(20px); + background: white; +} + +.wplace-toggle-label { + font-size: 14px; + color: var(--wplace-text); + font-weight: 500; +} + +.wplace-account-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid var(--wplace-border-color); + border-radius: var(--wplace-radius); + background: var(--wplace-secondary); +} + +.wplace-account-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 12px; + border-bottom: 1px solid var(--wplace-border-color); + transition: all 0.2s ease; +} + +.wplace-account-item:last-child { + border-bottom: none; +} + +.wplace-account-item:hover { + background: var(--wplace-accent); +} + +.wplace-account-item.current { + background: var(--wplace-success)20; + border-left: 3px solid var(--wplace-success); +} + +.wplace-account-info-item { + display: flex; + align-items: center; + gap: 8px; +} + +.wplace-account-number { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + background: var(--wplace-highlight); + color: white; + border-radius: 50%; + font-size: 12px; + font-weight: bold; +} + +.wplace-account-name { + font-size: 14px; + color: var(--wplace-text); + font-weight: 500; +} + +.wplace-account-status { + font-size: 12px; + color: var(--wplace-text-secondary); +} + +.wplace-account-placeholder { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 20px; + color: var(--wplace-text-secondary); + font-style: italic; +} + +.wplace-account-placeholder i { + color: var(--wplace-warning); +} diff --git a/Extension/background.js b/Extension/background.js index 2c904b1..c2dc166 100644 --- a/Extension/background.js +++ b/Extension/background.js @@ -48,7 +48,7 @@ async function executeLocalScript(scriptName, tabId) { } else { scriptUrl = chrome.runtime.getURL(`scripts/${scriptName}`); } - + const response = await fetch(scriptUrl); if (!response.ok) { @@ -67,38 +67,38 @@ async function executeLocalScript(scriptName, tabId) { world: "MAIN", // Key: executes in page context, not extension context func: (code, name, themeCSS, languages) => { console.log(`%c🚀 Executing ${name}...`, 'color: #4ade80; font-weight: bold; font-size: 14px;'); - + // Create detailed resource report console.group(`%c📊 WPlace AutoBOT Resource Report for ${name}`, 'color: #3b82f6; font-weight: bold; font-size: 16px;'); - + // Debug: Log what resources we received console.log(`%c📦 Raw Resources Received:`, 'color: #8b5cf6; font-weight: bold;'); console.log(` - Themes object:`, themeCSS); console.log(` - Languages object:`, languages); console.log(` - Theme count: ${Object.keys(themeCSS || {}).length}`); console.log(` - Language count: ${Object.keys(languages || {}).length}`); - + // Inject CSS themes if available if (themeCSS && Object.keys(themeCSS).length > 0) { console.group(`%c🎨 Theme Processing`, 'color: #8b5cf6; font-weight: bold;'); console.log(`%c📁 Loading ${Object.keys(themeCSS).length} theme files from extension local storage...`, 'color: #8b5cf6;'); - + // Create a global themes object window.AUTOBOT_THEMES = themeCSS; - + // Log detailed theme information Object.entries(themeCSS).forEach(([filename, content]) => { console.log(`%c📄 Theme File: ${filename}`, 'color: #8b5cf6; font-weight: bold;'); console.log(` 📏 Size: ${content.length.toLocaleString()} characters`); console.log(` 📍 Source: Extension local file (chrome-extension://)`); console.log(` 🔗 Full path: themes/${filename}`); - + // Show first few lines as preview const preview = content.substring(0, 200).split('\\n').slice(0, 3).join('\\n'); console.log(` 👀 Preview: ${preview}${content.length > 200 ? '...' : ''}`); console.log(` ✅ Status: Loaded successfully`); }); - + // Inject auto-image-styles.css if available if (themeCSS['auto-image-styles.css']) { const autoImageContent = themeCSS['auto-image-styles.css']; @@ -106,7 +106,7 @@ async function executeLocalScript(scriptName, tabId) { styleElement.id = 'autobot-auto-image-styles'; styleElement.textContent = autoImageContent; document.head.appendChild(styleElement); - + console.log(`%c✨ AUTO-INJECTED: auto-image-styles.css`, 'color: #10b981; font-weight: bold;'); console.log(` 📏 Injected size: ${autoImageContent.length.toLocaleString()} characters`); console.log(` 📍 Source: Extension local file`); @@ -122,14 +122,14 @@ async function executeLocalScript(scriptName, tabId) { console.log(` 📋 Expected files: auto-image-styles.css, acrylic.css, classic.css, etc.`); window.AUTOBOT_THEMES = {}; } - + // Inject language data if available if (languages && Object.keys(languages).length > 0) { console.group(`%c🌍 Language Processing`, 'color: #06b6d4; font-weight: bold;'); console.log(`%c📁 Loading ${Object.keys(languages).length} language files from extension local storage...`, 'color: #06b6d4;'); - + window.AUTOBOT_LANGUAGES = languages; - + // Log detailed language information Object.entries(languages).forEach(([filename, content]) => { console.log(`%c📄 Language File: ${filename}`, 'color: #06b6d4; font-weight: bold;'); @@ -137,18 +137,18 @@ async function executeLocalScript(scriptName, tabId) { console.log(` 📏 Keys count: ${Object.keys(content).length.toLocaleString()}`); console.log(` 📍 Source: Extension local file (chrome-extension://)`); console.log(` 🔗 Full path: lang/${filename}`); - + // Show some sample keys const sampleKeys = Object.keys(content).slice(0, 5); console.log(` 🔑 Sample keys: ${sampleKeys.join(', ')}${Object.keys(content).length > 5 ? '...' : ''}`); console.log(` ✅ Status: Loaded successfully`); }); - + // Helper function to get language data with detailed logging - window.getLanguage = function(lang = 'en') { + window.getLanguage = function (lang = 'en') { const langFile = lang + '.json'; const result = window.AUTOBOT_LANGUAGES[langFile] || window.AUTOBOT_LANGUAGES['en.json'] || {}; - + console.group(`%c🔤 Language Access: ${lang.toUpperCase()}`, 'color: #06b6d4; font-weight: bold;'); console.log(` 📋 Requested: ${lang}`); console.log(` 📄 File: ${langFile}`); @@ -157,10 +157,10 @@ async function executeLocalScript(scriptName, tabId) { console.log(` ✅ Success: ${window.AUTOBOT_LANGUAGES[langFile] ? 'Found exact match' : 'Fallback to English'}`); console.log(` 📝 Data preview:`, result); console.groupEnd(); - + return result; }; - + console.log(`%c🔤 Available languages: ${Object.keys(languages).map(f => f.replace('.json', '')).join(', ')}`, 'color: #06b6d4;'); console.groupEnd(); } else { @@ -168,16 +168,16 @@ async function executeLocalScript(scriptName, tabId) { console.log(` 📁 Expected source: Extension local files`); console.log(` 📋 Expected files: en.json, de.json, fr.json, etc.`); window.AUTOBOT_LANGUAGES = {}; - window.getLanguage = function() { + window.getLanguage = function () { console.warn(`%c⚠️ getLanguage() called but no languages available`, 'color: #f59e0b;'); - return {}; + return {}; }; } - + // Helper function to apply theme with detailed logging - window.applyTheme = function(themeName) { + window.applyTheme = function (themeName) { console.group(`%c🎨 Theme Application: ${themeName}`, 'color: #8b5cf6; font-weight: bold;'); - + if (!window.AUTOBOT_THEMES || Object.keys(window.AUTOBOT_THEMES).length === 0) { console.error(`%c❌ No themes available in extension`, 'color: #ef4444; font-weight: bold;'); console.log(` 📁 Expected source: Extension local files`); @@ -185,38 +185,38 @@ async function executeLocalScript(scriptName, tabId) { console.groupEnd(); return false; } - + const themeFile = themeName + '.css'; console.log(` 📋 Requested theme: ${themeName}`); console.log(` 📄 Looking for file: ${themeFile}`); console.log(` 📁 Available themes: ${Object.keys(window.AUTOBOT_THEMES).join(', ')}`); - + if (window.AUTOBOT_THEMES[themeFile]) { const themeContent = window.AUTOBOT_THEMES[themeFile]; - + // Remove existing theme const existing = document.getElementById('autobot-theme'); if (existing) { console.log(` 🗑️ Removing previous theme element`); existing.remove(); } - + // Apply new theme const styleElement = document.createElement('style'); styleElement.id = 'autobot-theme'; styleElement.textContent = themeContent; document.head.appendChild(styleElement); - + console.log(`%c✅ Theme applied successfully: ${themeName}`, 'color: #10b981; font-weight: bold;'); console.log(` 📏 Content size: ${themeContent.length.toLocaleString()} characters`); console.log(` 📍 Source: Extension local file`); console.log(` 🎯 Target: as + ` + + const resizeContainer = document.createElement("div") + resizeContainer.className = "resize-container" + resizeContainer.innerHTML = ` +

${Utils.t("resizeImage")}

+
+ + + + +
+ + + + + + + 100% +
+ Drag to pan • Pinch to zoom • Double‑tap to zoom +
+
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ +
+ + 1 +
+
+
+ +
+ + 1 +
+
+
+
+ +
+ + + +
+
+ + + Shift = Row • Alt = Column +
+
+ +
+
+  Color Palette +
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+  Advanced Color Matching +
+
+ + +
+
+ Chroma Weight + ${state.chromaPenaltyWeight} +
+ +
+ +
+ + +
+ +
+
+ +
+ + + +
+ ` + + 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) + + 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..."; + } + if (loadFromFileBtn) { + loadFromFileBtn.disabled = !state.initialSetupComplete; + loadFromFileBtn.title = state.initialSetupComplete ? "" : "🔄 Waiting for initial setup to complete..."; + } + if (uploadBtn) { + uploadBtn.disabled = !state.initialSetupComplete; + uploadBtn.title = state.initialSetupComplete ? "" : "🔄 Waiting for initial setup to complete..."; + } + + const minimizeBtn = container.querySelector("#minimizeBtn") + const compactBtn = container.querySelector("#compactBtn") + const statsBtn = container.querySelector("#statsBtn") + const toggleOverlayBtn = container.querySelector("#toggleOverlayBtn"); + const statusText = container.querySelector("#statusText") + const progressBar = container.querySelector("#progressBar") + const statsArea = statsContainer.querySelector("#statsArea") + const content = container.querySelector(".wplace-content") + const closeStatsBtn = statsContainer.querySelector("#closeStatsBtn") + const refreshChargesBtn = statsContainer.querySelector("#refreshChargesBtn") + const cooldownSlider = container.querySelector("#cooldownSlider"); + const cooldownValue = container.querySelector("#cooldownValue"); + + if (!uploadBtn || !selectPosBtn || !startBtn || !stopBtn) { + console.error("Some UI elements not found:", { + uploadBtn: !!uploadBtn, + selectPosBtn: !!selectPosBtn, + startBtn: !!startBtn, + stopBtn: !!stopBtn, + }) + } + + if (!statsContainer || !statsArea || !closeStatsBtn) { + console.error("Stats UI elements not found:", { + statsContainer: !!statsContainer, + statsArea: !!statsArea, + closeStatsBtn: !!closeStatsBtn, + }) + } + + const header = container.querySelector(".wplace-header") + + makeDraggable(container) + + function makeDraggable(element) { + let pos1 = 0, + pos2 = 0, + pos3 = 0, + pos4 = 0 + let isDragging = false + const header = element.querySelector(".wplace-header") || element.querySelector(".wplace-settings-header") + + if (!header) { + console.warn("No draggable header found for element:", element) + return + } + + header.onmousedown = dragMouseDown + + function dragMouseDown(e) { + if (e.target.closest(".wplace-header-btn") || e.target.closest("button")) return + + e.preventDefault() + isDragging = true + + const rect = element.getBoundingClientRect() + + element.style.transform = "none" + element.style.top = rect.top + "px" + element.style.left = rect.left + "px" + + pos3 = e.clientX + pos4 = e.clientY + element.classList.add("wplace-dragging") + document.onmouseup = closeDragElement + document.onmousemove = elementDrag + + document.body.style.userSelect = "none" + } + + function elementDrag(e) { + if (!isDragging) return + + e.preventDefault() + pos1 = pos3 - e.clientX + pos2 = pos4 - e.clientY + pos3 = e.clientX + pos4 = e.clientY + + let newTop = element.offsetTop - pos2 + let newLeft = element.offsetLeft - pos1 + + const rect = element.getBoundingClientRect() + const maxTop = window.innerHeight - rect.height + const maxLeft = window.innerWidth - rect.width + + newTop = Math.max(0, Math.min(newTop, maxTop)) + newLeft = Math.max(0, Math.min(newLeft, maxLeft)) + + element.style.top = newTop + "px" + element.style.left = newLeft + "px" + } + + function closeDragElement() { + isDragging = false + element.classList.remove("wplace-dragging") + document.onmouseup = null + document.onmousemove = null + document.body.style.userSelect = "" + } + } + + makeDraggable(statsContainer) + makeDraggable(container) + + if (statsBtn && closeStatsBtn) { + statsBtn.addEventListener("click", () => { + const isVisible = statsContainer.style.display !== "none" + if (isVisible) { + statsContainer.style.display = "none" + statsBtn.innerHTML = '' + statsBtn.title = "Show Stats" + } else { + statsContainer.style.display = "block" + statsBtn.innerHTML = '' + statsBtn.title = "Hide Stats" + } + }) + + closeStatsBtn.addEventListener("click", () => { + statsContainer.style.display = "none" + statsBtn.innerHTML = '' + statsBtn.title = "Show Stats" + }) + + const refreshAllAccountsBtn = statsContainer.querySelector("#refreshAllAccountsBtn"); + if (refreshAllAccountsBtn) { + refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails); + } + + const autoSwapToggle = statsContainer.querySelector("#autoSwapToggle"); + if (autoSwapToggle) { + autoSwapToggle.checked = CONFIG.autoSwap; + autoSwapToggle.addEventListener('change', (e) => { + CONFIG.autoSwap = e.target.checked; + }); + } + + if (refreshChargesBtn) { + refreshChargesBtn.addEventListener("click", async () => { + refreshChargesBtn.innerHTML = '' + refreshChargesBtn.disabled = true + + try { + await updateStats() + } catch (error) { + console.error("Error refreshing charges:", error) + } finally { + refreshChargesBtn.innerHTML = '' + refreshChargesBtn.disabled = false + } + }) + } + } + if (statsContainer && statsBtn) { + statsContainer.style.display = "block"; + statsBtn.innerHTML = ''; + statsBtn.title = "Hide Stats"; + } + + 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.style.display !== "none" + if (isVisible) { + settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards" + setTimeout(() => { + settingsContainer.style.display = "none" + settingsContainer.style.animation = "" + }, 300) + } else { + settingsContainer.style.top = "50%" + settingsContainer.style.left = "50%" + settingsContainer.style.transform = "translate(-50%, -50%)" + settingsContainer.style.display = "block" + settingsContainer.style.animation = "settingsSlideIn 0.4s ease-out" + } + }) + + closeSettingsBtn.addEventListener("click", () => { + settingsContainer.style.animation = "settingsFadeOut 0.3s ease-out forwards" + setTimeout(() => { + settingsContainer.style.display = "none" + settingsContainer.style.animation = "" + settingsContainer.style.top = "50%" + settingsContainer.style.left = "50%" + settingsContainer.style.transform = "translate(-50%, -50%)" + }, 300) + }) + + applySettingsBtn.addEventListener("click", () => { + // Sync advanced settings before save + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) state.colorMatchingAlgorithm = colorAlgorithmSelect.value; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) state.enableChromaPenalty = enableChromaPenaltyToggle.checked; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) state.chromaPenaltyWeight = parseFloat(chromaPenaltyWeightSlider.value) || 0.15; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) { + const v = parseInt(transparencyThresholdInput.value, 10); if (!isNaN(v) && v >= 0 && v <= 255) state.customTransparencyThreshold = v; + } + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) { + const v = parseInt(whiteThresholdInput.value, 10); if (!isNaN(v) && v >= 200 && v <= 255) state.customWhiteThreshold = v; + } + // Update functional thresholds + CONFIG.TRANSPARENCY_THRESHOLD = state.customTransparencyThreshold; + CONFIG.WHITE_THRESHOLD = state.customWhiteThreshold; + // Notifications + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifEnabledToggle) state.notificationsEnabled = !!notifEnabledToggle.checked; + if (notifOnChargesToggle) state.notifyOnChargesReached = !!notifOnChargesToggle.checked; + if (notifOnlyUnfocusedToggle) state.notifyOnlyWhenUnfocused = !!notifOnlyUnfocusedToggle.checked; + if (notifIntervalInput) { + const v = parseInt(notifIntervalInput.value, 10); + if (!isNaN(v) && v >= 1 && v <= 60) state.notificationIntervalMinutes = v; + } + saveBotSettings(); + Utils.showAlert(Utils.t("settingsSaved"), "success"); + closeSettingsBtn.click(); + NotificationManager.syncFromState(); + }); + + makeDraggable(settingsContainer) + + const tokenSourceSelect = settingsContainer.querySelector("#tokenSourceSelect") + if (tokenSourceSelect) { + tokenSourceSelect.addEventListener("change", (e) => { + state.tokenSource = e.target.value + saveBotSettings() + console.log(`🔑 Token source changed to: ${state.tokenSource}`) + const sourceNames = { + 'generator': 'Automatic Generator', + 'hybrid': 'Generator + Auto Fallback', + 'manual': 'Manual Pixel Placement' + } + Utils.showAlert(`Token source set to: ${sourceNames[state.tokenSource]}`, "success") + }) + } + + // Batch mode controls + const batchModeSelect = settingsContainer.querySelector("#batchModeSelect") + const normalBatchControls = settingsContainer.querySelector("#normalBatchControls") + const randomBatchControls = settingsContainer.querySelector("#randomBatchControls") + const randomBatchMin = settingsContainer.querySelector("#randomBatchMin") + const randomBatchMax = settingsContainer.querySelector("#randomBatchMax") + + if (batchModeSelect) { + batchModeSelect.addEventListener("change", (e) => { + state.batchMode = e.target.value + + // Switch between normal and random controls + if (normalBatchControls && randomBatchControls) { + if (e.target.value === 'random') { + normalBatchControls.style.display = 'none' + randomBatchControls.style.display = 'block' + } else { + normalBatchControls.style.display = 'block' + randomBatchControls.style.display = 'none' + } + } + + saveBotSettings() + console.log(`📦 Batch mode changed to: ${state.batchMode}`) + Utils.showAlert(`Batch mode set to: ${state.batchMode === 'random' ? 'Random Range' : 'Normal Fixed Size'}`, "success") + }) + } + + if (randomBatchMin) { + randomBatchMin.addEventListener("input", (e) => { + const min = parseInt(e.target.value) + if (min >= 1 && min <= 1000) { + state.randomBatchMin = min + // Ensure min doesn't exceed max + if (randomBatchMax && min > state.randomBatchMax) { + state.randomBatchMax = min + randomBatchMax.value = min + } + saveBotSettings() + } + }) + } + + if (randomBatchMax) { + randomBatchMax.addEventListener("input", (e) => { + const max = parseInt(e.target.value) + if (max >= 1 && max <= 1000) { + state.randomBatchMax = max + // Ensure max doesn't go below min + if (randomBatchMin && max < state.randomBatchMin) { + state.randomBatchMin = max + randomBatchMin.value = max + } + saveBotSettings() + } + }) + } + + const languageSelect = settingsContainer.querySelector("#languageSelect") + if (languageSelect) { + languageSelect.addEventListener("change", (e) => { + const newLanguage = e.target.value + state.language = newLanguage + localStorage.setItem('wplace_language', newLanguage) + + setTimeout(() => { + settingsContainer.style.display = "none" + createUI() + }, 100) + }) + } + + const themeSelect = settingsContainer.querySelector("#themeSelect") + if (themeSelect) { + themeSelect.addEventListener("change", (e) => { + const newTheme = e.target.value + switchTheme(newTheme) + }) + } + + const overlayOpacitySlider = settingsContainer.querySelector("#overlayOpacitySlider"); + const overlayOpacityValue = settingsContainer.querySelector("#overlayOpacityValue"); + const enableBlueMarbleToggle = settingsContainer.querySelector("#enableBlueMarbleToggle"); + + if (overlayOpacitySlider && overlayOpacityValue) { + overlayOpacitySlider.addEventListener('input', (e) => { + const opacity = parseFloat(e.target.value); + state.overlayOpacity = opacity; + overlayOpacityValue.textContent = `${Math.round(opacity * 100)}%`; + }); + } + + // Speed slider event listener + const speedSlider = settingsContainer.querySelector("#speedSlider"); + const speedValue = settingsContainer.querySelector("#speedValue"); + if (speedSlider && speedValue) { + speedSlider.addEventListener('input', (e) => { + const speed = parseInt(e.target.value, 10); + state.paintingSpeed = speed; + speedValue.textContent = `${speed} (batch size)`; + saveBotSettings(); + }); + } + + if (enableBlueMarbleToggle) { + enableBlueMarbleToggle.addEventListener('click', async () => { + state.blueMarbleEnabled = enableBlueMarbleToggle.checked; + if (state.imageLoaded && overlayManager.imageBitmap) { + Utils.showAlert("Re-processing overlay...", "info"); + await overlayManager.processImageIntoChunks(); + Utils.showAlert("Overlay updated!", "success"); + } + }); + } + + // (Advanced color listeners moved outside to work with resize dialog) + // (Advanced color listeners moved outside to work with resize dialog) + // Notifications listeners + const notifPermBtn = settingsContainer.querySelector("#notifRequestPermBtn"); + const notifTestBtn = settingsContainer.querySelector("#notifTestBtn"); + if (notifPermBtn) { + notifPermBtn.addEventListener("click", async () => { + const perm = await NotificationManager.requestPermission(); + if (perm === "granted") Utils.showAlert("Notifications enabled.", "success"); + else Utils.showAlert("Notifications permission denied.", "warning"); + }); + } + if (notifTestBtn) { + notifTestBtn.addEventListener("click", () => { + NotificationManager.notify("WPlace — Test", "This is a test notification.", "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 zoomSlider = resizeContainer.querySelector("#zoomSlider"); + const zoomValue = resizeContainer.querySelector('#zoomValue'); + const zoomInBtn = resizeContainer.querySelector('#zoomInBtn'); + const zoomOutBtn = resizeContainer.querySelector('#zoomOutBtn'); + const zoomFitBtn = resizeContainer.querySelector('#zoomFitBtn'); + const zoomActualBtn = resizeContainer.querySelector('#zoomActualBtn'); + const panModeBtn = resizeContainer.querySelector('#panModeBtn'); + const panStage = resizeContainer.querySelector('#resizePanStage'); + const canvasStack = resizeContainer.querySelector('#resizeCanvasStack'); + const baseCanvas = resizeContainer.querySelector('#resizeCanvas'); + const maskCanvas = resizeContainer.querySelector('#maskCanvas'); + const baseCtx = baseCanvas.getContext('2d'); + const maskCtx = maskCanvas.getContext('2d'); + const confirmResize = resizeContainer.querySelector("#confirmResize") + const cancelResize = resizeContainer.querySelector("#cancelResize") + const downloadPreviewBtn = resizeContainer.querySelector("#downloadPreviewBtn"); + const clearIgnoredBtn = resizeContainer.querySelector('#clearIgnoredBtn'); + + if (compactBtn) { + compactBtn.addEventListener("click", () => { + container.classList.toggle("wplace-compact") + const isCompact = container.classList.contains("wplace-compact") + + if (isCompact) { + compactBtn.innerHTML = '' + compactBtn.title = "Expand Mode" + } else { + compactBtn.innerHTML = '' + compactBtn.title = "Compact Mode" + } + }) + } + + if (minimizeBtn) { + minimizeBtn.addEventListener("click", () => { + state.minimized = !state.minimized + if (state.minimized) { + container.classList.add("wplace-minimized") + content.classList.add("wplace-hidden") + minimizeBtn.innerHTML = '' + minimizeBtn.title = "Restore" + } else { + container.classList.remove("wplace-minimized") + content.classList.remove("wplace-hidden") + minimizeBtn.innerHTML = '' + minimizeBtn.title = "Minimize" + } + saveBotSettings() + }) + } + + if (toggleOverlayBtn) { + toggleOverlayBtn.addEventListener('click', () => { + const isEnabled = overlayManager.toggle(); + toggleOverlayBtn.classList.toggle('active', isEnabled); + toggleOverlayBtn.setAttribute('aria-pressed', isEnabled ? 'true' : 'false'); + Utils.showAlert(`Overlay ${isEnabled ? 'enabled' : 'disabled'}.`, 'info'); + }); + } + + if (state.minimized) { + container.classList.add("wplace-minimized") + content.classList.add("wplace-hidden") + if (minimizeBtn) { + minimizeBtn.innerHTML = '' + minimizeBtn.title = "Restore" + } + } else { + container.classList.remove("wplace-minimized") + content.classList.remove("wplace-hidden") + if (minimizeBtn) { + minimizeBtn.innerHTML = '' + minimizeBtn.title = "Minimize" + } + } + + if (saveBtn) { + saveBtn.addEventListener("click", () => { + if (!state.imageLoaded) { + Utils.showAlert(Utils.t("missingRequirements"), "error") + return + } + + const success = Utils.saveProgress() + if (success) { + updateUI("autoSaved", "success") + Utils.showAlert(Utils.t("autoSaved"), "success") + } else { + Utils.showAlert("❌ Erro ao salvar progresso", "error") + } + }) + } + + if (loadBtn) { + loadBtn.addEventListener("click", () => { + // Check if initial setup is complete + if (!state.initialSetupComplete) { + Utils.showAlert("🔄 Please wait for the initial setup to complete before loading progress.", "warning"); + return; + } + + const savedData = Utils.loadProgress() + if (!savedData) { + updateUI("noSavedData", "warning") + Utils.showAlert(Utils.t("noSavedData"), "warning") + return + } + + const confirmLoad = confirm( + `${Utils.t("savedDataFound")}\n\n` + + `Saved: ${new Date(savedData.timestamp).toLocaleString()}\n` + + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels`, + ) + + if (confirmLoad) { + const success = Utils.restoreProgress(savedData) + if (success) { + updateUI("dataLoaded", "success") + Utils.showAlert(Utils.t("dataLoaded"), "success") + updateDataButtons() + + updateStats() + + // Restore overlay if image data was loaded from localStorage + Utils.restoreOverlayFromData().catch(error => { + console.error('Failed to restore overlay from localStorage:', error); + }); + + if (!state.colorsChecked) { + uploadBtn.disabled = false; + } else { + uploadBtn.disabled = false; + selectPosBtn.disabled = false; + } + + if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + startBtn.disabled = false + } + } else { + Utils.showAlert("❌ Erro ao carregar progresso", "error") + } + } + }) + } + + if (saveToFileBtn) { + saveToFileBtn.addEventListener("click", () => { + const success = Utils.saveProgressToFile() + if (success) { + updateUI("fileSaved", "success") + Utils.showAlert(Utils.t("fileSaved"), "success") + } else { + Utils.showAlert(Utils.t("fileError"), "error") + } + }) + } + + if (loadFromFileBtn) { + loadFromFileBtn.addEventListener("click", async () => { + // Check if initial setup is complete + if (!state.initialSetupComplete) { + Utils.showAlert("🔄 Please wait for the initial setup to complete before loading from file.", "warning"); + return; + } + + try { + const success = await Utils.loadProgressFromFile() + if (success) { + updateUI("fileLoaded", "success") + Utils.showAlert(Utils.t("fileLoaded"), "success") + updateDataButtons() + + await updateStats() + + // Restore overlay if image data was loaded from file + await Utils.restoreOverlayFromData().catch(error => { + console.error('Failed to restore overlay from file:', error); + }); + + if (state.colorsChecked) { + uploadBtn.disabled = false + selectPosBtn.disabled = false + resizeBtn.disabled = false + } else { + uploadBtn.disabled = false; + } + + if (state.imageLoaded && state.startPosition && state.region && state.colorsChecked) { + startBtn.disabled = false + } + } + } catch (error) { + if (error.message === "Invalid JSON file") { + Utils.showAlert(Utils.t("invalidFileFormat"), "error") + } else { + Utils.showAlert(Utils.t("fileError"), "error") + } + } + }) + } + + updateUI = (messageKey, type = "default", params = {}) => { + const message = Utils.t(messageKey, params) + statusText.textContent = message + statusText.className = `wplace-status status-${type}` + statusText.style.animation = "none" + void statusText.offsetWidth + statusText.style.animation = "slideIn 0.3s ease-out" + } + + updateStats = async () => { + localStorage.removeItem("lp"); + const { id, charges, cooldown, max, droplets } = 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(); + + 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 remainingPixels = state.totalPixels - state.paintedPixels; + state.estimatedTime = Utils.calculateEstimatedTime(remainingPixels, state.currentCharges, state.cooldown); + if (progressBar) progressBar.style.width = `${progress}%`; + + imageStatsHTML = ` +
+
${Utils.t("progress")}
+
${progress}%
+
+
+
${Utils.t("pixels")}
+
${state.paintedPixels}/${state.totalPixels}
+
+
+
${Utils.t("estimatedTime")}
+
${Utils.formatTime(state.estimatedTime)}
+
+ `; + } + + let colorSwatchesHTML = ''; + if (state.colorsChecked) { + colorSwatchesHTML = state.availableColors.map(color => { + const rgbString = `rgb(${color.rgb.join(',')})`; + return `
`; + }).join(''); + } + + let totalAllCharges = 0; + if (state.allAccountsInfo.length > 0) { + totalAllCharges = state.allAccountsInfo.reduce((sum, acc) => sum + Math.floor(acc.Charges || 0), 0); + totalMaxCharges = state.allAccountsInfo.reduce((sum, acc) => sum + Math.floor(acc.Max || 0), 0); + } + + if (statsArea) statsArea.innerHTML = ` + ${imageStatsHTML} +
+
Total All Accounts Charges
+
${totalAllCharges}/${totalMaxCharges}
+
+
+
${Utils.t("charges")}
+
${Math.floor(state.currentCharges)} / ${state.maxCharges}
+
+ ${state.colorsChecked ? ` +
+
Available Colors (${state.availableColors.length})
+
+ ${colorSwatchesHTML} +
+
+ ` : ''} + `; + + // Sync with the accounts list + if (state.allAccountsInfo.length > 0 && id) { + const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === id); + if (currentAccountInList) { + currentAccountInList.Charges = state.currentCharges; + currentAccountInList.Max = state.maxCharges; + currentAccountInList.Droplets = droplets; + // Re-render the list to show the updated charge count + renderAccountsList(); + } + } + } + + updateDataButtons = () => { + const hasImageData = state.imageLoaded && state.imageData + saveBtn.disabled = !hasImageData + saveToFileBtn.disabled = !hasImageData + } + + updateDataButtons() + + function showResizeDialog(processor) { + let baseProcessor = processor; + let width, height; + if (state.originalImage?.dataUrl) { + baseProcessor = new ImageProcessor(state.originalImage.dataUrl); + width = state.originalImage.width; + height = state.originalImage.height; + } else { + const dims = processor.getDimensions(); + width = dims.width; + height = dims.height; + } + const aspectRatio = width / height; + + const rs = state.resizeSettings; + widthSlider.max = width * 2; + heightSlider.max = height * 2; + let initialW = width; + let initialH = height; + if (rs && Number.isFinite(rs.width) && Number.isFinite(rs.height) && rs.width > 0 && rs.height > 0) { + initialW = rs.width; + initialH = rs.height; + } + // Clamp to slider ranges + initialW = Math.max(parseInt(widthSlider.min, 10) || 10, Math.min(initialW, parseInt(widthSlider.max, 10))); + initialH = Math.max(parseInt(heightSlider.min, 10) || 10, Math.min(initialH, parseInt(heightSlider.max, 10))); + widthSlider.value = initialW; + heightSlider.value = initialH; + widthValue.textContent = initialW; + heightValue.textContent = initialH; + zoomSlider.value = 1; + if (zoomValue) zoomValue.textContent = '100%'; + paintWhiteToggle.checked = state.paintWhitePixels; + + let _previewTimer = null; + let _previewJobId = 0; + let _isDraggingSize = false; + let _zoomLevel = 1; + let _ditherWorkBuf = null; + let _ditherEligibleBuf = null; + const ensureDitherBuffers = (n) => { + if (!_ditherWorkBuf || _ditherWorkBuf.length !== n * 3) _ditherWorkBuf = new Float32Array(n * 3); + if (!_ditherEligibleBuf || _ditherEligibleBuf.length !== n) _ditherEligibleBuf = new Uint8Array(n); + return { work: _ditherWorkBuf, eligible: _ditherEligibleBuf }; + }; + let _maskImageData = null; + let _maskData = null; + let _dirty = null; + const _resetDirty = () => { _dirty = { minX: Infinity, minY: Infinity, maxX: -1, maxY: -1 }; }; + const _markDirty = (x, y) => { + if (!_dirty) _resetDirty(); + if (x < _dirty.minX) _dirty.minX = x; + if (y < _dirty.minY) _dirty.minY = y; + if (x > _dirty.maxX) _dirty.maxX = x; + if (y > _dirty.maxY) _dirty.maxY = y; + }; + const _flushDirty = () => { + if (!_dirty || _dirty.maxX < _dirty.minX || _dirty.maxY < _dirty.minY) return; + const x = Math.max(0, _dirty.minX); + const y = Math.max(0, _dirty.minY); + const w = Math.min(maskCanvas.width - x, _dirty.maxX - x + 1); + const h = Math.min(maskCanvas.height - y, _dirty.maxY - y + 1); + if (w > 0 && h > 0) maskCtx.putImageData(_maskImageData, 0, 0, x, y, w, h); + _resetDirty(); + }; + const _ensureMaskOverlayBuffers = (w, h, rebuildFromMask = false) => { + if (!_maskImageData || _maskImageData.width !== w || _maskImageData.height !== h) { + _maskImageData = maskCtx.createImageData(w, h); + _maskData = _maskImageData.data; + rebuildFromMask = true; + } + if (rebuildFromMask) { + const m = state.resizeIgnoreMask; + const md = _maskData; + md.fill(0); + if (m) { + for (let i = 0; i < m.length; i++) if (m[i]) { const p = i * 4; md[p] = 255; md[p + 1] = 0; md[p + 2] = 0; md[p + 3] = 150; } + } + maskCtx.putImageData(_maskImageData, 0, 0); + _resetDirty(); + } + }; + const ensureMaskSize = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + baseCanvas.width = w; baseCanvas.height = h; + maskCanvas.width = w; maskCanvas.height = h; + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + // Ensure overlay buffers exist and rebuild from mask when dimensions change + _ensureMaskOverlayBuffers(w, h, true); + }; + _updateResizePreview = async () => { + const jobId = ++_previewJobId; + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + _zoomLevel = parseFloat(zoomSlider.value); + + widthValue.textContent = newWidth; + heightValue.textContent = newHeight; + + ensureMaskSize(newWidth, newHeight); + canvasStack.style.width = newWidth + 'px'; + canvasStack.style.height = newHeight + 'px'; + baseCtx.imageSmoothingEnabled = false; + if (!state.availableColors || state.availableColors.length === 0) { + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + // Draw existing mask overlay buffer + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + return; + } + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + baseCtx.clearRect(0, 0, newWidth, newHeight); + baseCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = baseCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + + const tThresh = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + const applyFSDither = () => { + const w = newWidth, h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], g = data[i4 + 1], b = data[i4 + 2], a = data[i4 + 3]; + const isEligible = a >= tThresh && (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; // transparent in preview overlay + } + } + } + + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], g0 = work[base + 1], b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor(r0, g0, b0, state.activeColorPalette); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + } + }; + + // Skip expensive dithering while user is dragging sliders + if (state.ditheringEnabled && !_isDraggingSize) { + applyFSDither(); + } else { + for (let i = 0; i < data.length; i += 4) { + const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3]; + if (a < tThresh || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) { + data[i + 3] = 0; + continue; + } + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + } + } + + if (jobId !== _previewJobId) return; + baseCtx.putImageData(imgData, 0, 0); + maskCtx.clearRect(0, 0, maskCanvas.width, maskCanvas.height); + if (_maskImageData) maskCtx.putImageData(_maskImageData, 0, 0); + updateZoomLayout(); + }; + + const onWidthInput = () => { + if (keepAspect.checked) { + heightSlider.value = Math.round(parseInt(widthSlider.value, 10) / aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { baseWidth: width, baseHeight: height, width: curW, height: curH }; + saveBotSettings(); + // Auto-fit after size changes + const fit = (typeof computeFitZoom === 'function') ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + const onHeightInput = () => { + if (keepAspect.checked) { + widthSlider.value = Math.round(parseInt(heightSlider.value, 10) * aspectRatio); + } + _updateResizePreview(); + const curW = parseInt(widthSlider.value, 10); + const curH = parseInt(heightSlider.value, 10); + state.resizeSettings = { baseWidth: width, baseHeight: height, width: curW, height: curH }; + saveBotSettings(); + // Auto-fit after size changes + const fit = (typeof computeFitZoom === 'function') ? computeFitZoom() : 1; + if (!isNaN(fit) && isFinite(fit)) applyZoom(fit); + }; + + paintWhiteToggle.onchange = (e) => { + state.paintWhitePixels = e.target.checked; + _updateResizePreview(); + }; + + let panX = 0, panY = 0; + const clampPan = () => { + const wrapRect = panStage?.getBoundingClientRect() || { width: 0, height: 0 }; + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + if (w <= wrapRect.width) { + panX = Math.floor((wrapRect.width - w) / 2); + } else { + const minX = wrapRect.width - w; + panX = Math.min(0, Math.max(minX, panX)); + } + if (h <= wrapRect.height) { + panY = Math.floor((wrapRect.height - h) / 2); + } else { + const minY = wrapRect.height - h; + panY = Math.min(0, Math.max(minY, panY)); + } + }; + let _panRaf = 0; + const applyPan = () => { + if (_panRaf) return; + _panRaf = requestAnimationFrame(() => { + clampPan(); + canvasStack.style.transform = `translate3d(${Math.round(panX)}px, ${Math.round(panY)}px, 0) scale(${_zoomLevel})`; + _panRaf = 0; + }); + }; + + const updateZoomLayout = () => { + const w = baseCanvas.width || 1, h = baseCanvas.height || 1; + baseCanvas.style.width = w + 'px'; + baseCanvas.style.height = h + 'px'; + maskCanvas.style.width = w + 'px'; + maskCanvas.style.height = h + 'px'; + canvasStack.style.width = w + 'px'; + canvasStack.style.height = h + 'px'; + applyPan(); + }; + const applyZoom = (z) => { + _zoomLevel = Math.max(0.05, Math.min(20, z || 1)); + zoomSlider.value = _zoomLevel; + updateZoomLayout(); + if (zoomValue) zoomValue.textContent = `${Math.round(_zoomLevel * 100)}%`; + }; + zoomSlider.addEventListener('input', () => { + applyZoom(parseFloat(zoomSlider.value)); + }); + if (zoomInBtn) zoomInBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) + 0.1)); + if (zoomOutBtn) zoomOutBtn.addEventListener('click', () => applyZoom(parseFloat(zoomSlider.value) - 0.1)); + const computeFitZoom = () => { + const wrapRect = panStage?.getBoundingClientRect(); + if (!wrapRect) return 1; + const w = baseCanvas.width || 1; + const h = baseCanvas.height || 1; + const margin = 10; + const scaleX = (wrapRect.width - margin) / w; + const scaleY = (wrapRect.height - margin) / h; + return Math.max(0.05, Math.min(20, Math.min(scaleX, scaleY))); + }; + if (zoomFitBtn) zoomFitBtn.addEventListener('click', () => { applyZoom(computeFitZoom()); centerInView(); }); + if (zoomActualBtn) zoomActualBtn.addEventListener('click', () => { applyZoom(1); centerInView(); }); + + const centerInView = () => { + if (!panStage) return; + const rect = panStage.getBoundingClientRect(); + const w = (baseCanvas.width || 1) * _zoomLevel; + const h = (baseCanvas.height || 1) * _zoomLevel; + panX = Math.floor((rect.width - w) / 2); + panY = Math.floor((rect.height - h) / 2); + applyPan(); + }; + + let isPanning = false; let startX = 0, startY = 0, startPanX = 0, startPanY = 0; + let allowPan = false; // Space key + let panMode = false; // Explicit pan mode toggle for touch/one-button mice + const isPanMouseButton = (e) => e.button === 1 || e.button === 2; + const setCursor = (val) => { if (panStage) panStage.style.cursor = val; }; + const isPanActive = (e) => panMode || allowPan || isPanMouseButton(e); + const updatePanModeBtn = () => { + if (!panModeBtn) return; + panModeBtn.classList.toggle('active', panMode); + panModeBtn.setAttribute('aria-pressed', panMode ? 'true' : 'false'); + }; + if (panModeBtn) { + updatePanModeBtn(); + panModeBtn.addEventListener('click', () => { panMode = !panMode; updatePanModeBtn(); setCursor(panMode ? 'grab' : ''); }); + } + if (panStage) { + panStage.addEventListener('contextmenu', (e) => { if (allowPan) e.preventDefault(); }); + window.addEventListener('keydown', (e) => { if (e.code === 'Space') { allowPan = true; setCursor('grab'); } }); + window.addEventListener('keyup', (e) => { if (e.code === 'Space') { allowPan = false; if (!isPanning) setCursor(''); } }); + panStage.addEventListener('mousedown', (e) => { + if (!isPanActive(e)) return; + e.preventDefault(); + isPanning = true; startX = e.clientX; startY = e.clientY; startPanX = panX; startPanY = panY; + setCursor('grabbing'); + }); + window.addEventListener('mousemove', (e) => { + if (!isPanning) return; + const dx = e.clientX - startX; const dy = e.clientY - startY; + panX = startPanX + dx; panY = startPanY + dy; applyPan(); + }); + window.addEventListener('mouseup', () => { if (isPanning) { isPanning = false; setCursor(allowPan ? 'grab' : ''); } }); + panStage.addEventListener('wheel', (e) => { + if (!e.ctrlKey && !e.metaKey) return; + e.preventDefault(); + const rect = panStage.getBoundingClientRect(); + const cx = e.clientX - rect.left - panX; + const cy = e.clientY - rect.top - panY; + const before = _zoomLevel; + const step = Math.max(0.05, Math.min(0.5, Math.abs(e.deltaY) > 20 ? 0.2 : 0.1)); + const next = Math.max(0.05, Math.min(20, before + (e.deltaY > 0 ? -step : step))); + if (next === before) return; + const scale = next / before; + panX = panX - cx * (scale - 1); + panY = panY - cy * (scale - 1); + applyZoom(next); + }, { passive: false }); + let lastTouchDist = null; + let touchStartTime = 0; + let doubleTapTimer = null; + panStage.addEventListener('touchstart', (e) => { + if (e.touches.length === 1) { + const t = e.touches[0]; + isPanning = true; startX = t.clientX; startY = t.clientY; startPanX = panX; startPanY = panY; + setCursor('grabbing'); + const now = Date.now(); + if (now - touchStartTime < 300) { + // double tap -> toggle 100%/fit + const z = Math.abs(_zoomLevel - 1) < 0.01 ? computeFitZoom() : 1; + applyZoom(z); + centerInView(); + if (doubleTapTimer) clearTimeout(doubleTapTimer); + } else { + touchStartTime = now; + doubleTapTimer = setTimeout(() => { doubleTapTimer = null; }, 320); + } + } else if (e.touches.length === 2) { + // Pinch start + const [a, b] = e.touches; + lastTouchDist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + } + }, { passive: true }); + panStage.addEventListener('touchmove', (e) => { + if (e.touches.length === 1 && isPanning) { + const t = e.touches[0]; + const dx = t.clientX - startX; const dy = t.clientY - startY; + panX = startPanX + dx; panY = startPanY + dy; applyPan(); + } else if (e.touches.length === 2 && lastTouchDist != null) { + e.preventDefault(); + const [a, b] = e.touches; + const dist = Math.hypot(b.clientX - a.clientX, b.clientY - a.clientY); + const rect = panStage.getBoundingClientRect(); + const centerX = (a.clientX + b.clientX) / 2 - rect.left - panX; + const centerY = (a.clientY + b.clientY) / 2 - rect.top - panY; + const before = _zoomLevel; + const scale = dist / (lastTouchDist || dist); + const next = Math.max(0.05, Math.min(20, before * scale)); + if (next !== before) { + panX = panX - centerX * (next / before - 1); + panY = panY - centerY * (next / before - 1); + applyZoom(next); + } + lastTouchDist = dist; + } + }, { passive: false }); + panStage.addEventListener('touchend', () => { + isPanning = false; lastTouchDist = null; setCursor(panMode || allowPan ? 'grab' : ''); + }); + } + const schedulePreview = () => { + if (_previewTimer) clearTimeout(_previewTimer); + const run = () => { + _previewTimer = null; + _updateResizePreview(); + }; + if (window.requestIdleCallback) { + _previewTimer = setTimeout(() => requestIdleCallback(run, { timeout: 150 }), 50); + } else { + _previewTimer = setTimeout(() => requestAnimationFrame(run), 50); + } + }; + // Track dragging to reduce work and skip dithering during drag + const markDragStart = () => { _isDraggingSize = true; }; + const markDragEnd = () => { _isDraggingSize = false; schedulePreview(); }; + widthSlider.addEventListener('pointerdown', markDragStart); + heightSlider.addEventListener('pointerdown', markDragStart); + widthSlider.addEventListener('pointerup', markDragEnd); + heightSlider.addEventListener('pointerup', markDragEnd); + widthSlider.addEventListener("input", () => { onWidthInput(); schedulePreview(); }); + heightSlider.addEventListener("input", () => { onHeightInput(); schedulePreview(); }); + + // Mask painting UX: brush size, modes, row/column fills, and precise coords + let draggingMask = false; + let lastPaintX = -1, lastPaintY = -1; + let brushSize = 1; + let rowColSize = 1; + let maskMode = 'ignore'; // 'ignore' | 'unignore' | 'toggle' + const brushEl = resizeContainer.querySelector('#maskBrushSize'); + const brushValEl = resizeContainer.querySelector('#maskBrushSizeValue'); + const btnIgnore = resizeContainer.querySelector('#maskModeIgnore'); + const btnUnignore = resizeContainer.querySelector('#maskModeUnignore'); + const btnToggle = resizeContainer.querySelector('#maskModeToggle'); + const clearIgnoredBtnEl = resizeContainer.querySelector('#clearIgnoredBtn'); + const invertMaskBtn = resizeContainer.querySelector('#invertMaskBtn'); + const rowColSizeEl = resizeContainer.querySelector('#rowColSize'); + const rowColSizeValEl = resizeContainer.querySelector('#rowColSizeValue'); + + const updateModeButtons = () => { + const map = [ + [btnIgnore, 'ignore'], + [btnUnignore, 'unignore'], + [btnToggle, 'toggle'] + ]; + for (const [el, m] of map) { + if (!el) continue; + const active = maskMode === m; + el.classList.toggle('active', active); + el.setAttribute('aria-pressed', active ? 'true' : 'false'); + } + }; + const setMode = (mode) => { maskMode = mode; updateModeButtons(); }; + if (brushEl && brushValEl) { + brushEl.addEventListener('input', () => { brushSize = parseInt(brushEl.value, 10) || 1; brushValEl.textContent = brushSize; }); + brushValEl.textContent = brushEl.value; + brushSize = parseInt(brushEl.value, 10) || 1; + } + if (rowColSizeEl && rowColSizeValEl) { + rowColSizeEl.addEventListener('input', () => { rowColSize = parseInt(rowColSizeEl.value, 10) || 1; rowColSizeValEl.textContent = rowColSize; }); + rowColSizeValEl.textContent = rowColSizeEl.value; + rowColSize = parseInt(rowColSizeEl.value, 10) || 1; + } + if (btnIgnore) btnIgnore.addEventListener('click', () => setMode('ignore')); + if (btnUnignore) btnUnignore.addEventListener('click', () => setMode('unignore')); + if (btnToggle) btnToggle.addEventListener('click', () => setMode('toggle')); + // Initialize button state (default to toggle mode) + updateModeButtons(); + + const mapClientToPixel = (clientX, clientY) => { + // Compute without rounding until final step to avoid drift at higher zoom + const rect = baseCanvas.getBoundingClientRect(); + const scaleX = rect.width / baseCanvas.width; + const scaleY = rect.height / baseCanvas.height; + const dx = (clientX - rect.left) / scaleX; + const dy = (clientY - rect.top) / scaleY; + const x = Math.floor(dx); + const y = Math.floor(dy); + return { x, y }; + }; + + const ensureMask = (w, h) => { + if (!state.resizeIgnoreMask || state.resizeIgnoreMask.length !== w * h) { + state.resizeIgnoreMask = new Uint8Array(w * h); + } + }; + + const paintCircle = (cx, cy, radius, value) => { + const w = baseCanvas.width, h = baseCanvas.height; + ensureMask(w, h); + const r2 = radius * radius; + for (let yy = cy - radius; yy <= cy + radius; yy++) { + if (yy < 0 || yy >= h) continue; + for (let xx = cx - radius; xx <= cx + radius; xx++) { + if (xx < 0 || xx >= w) continue; + const dx = xx - cx, dy = yy - cy; + if (dx * dx + dy * dy <= r2) { + const idx = yy * w + xx; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; } + else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; } + _markDirty(xx, yy); + } + } + } + } + }; + + const paintRow = (y, value) => { + const w = baseCanvas.width, h = baseCanvas.height; + ensureMask(w, h); + if (y < 0 || y >= h) return; + + // Paint multiple rows based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startY = Math.max(0, y - halfSize); + const endY = Math.min(h - 1, y + halfSize); + + for (let rowY = startY; rowY <= endY; rowY++) { + for (let x = 0; x < w; x++) { + const idx = rowY * w + x; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; } + else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; } + } + } + if (_maskData) { _markDirty(0, rowY); _markDirty(w - 1, rowY); } + } + }; + + const paintColumn = (x, value) => { + const w = baseCanvas.width, h = baseCanvas.height; + ensureMask(w, h); + if (x < 0 || x >= w) return; + + // Paint multiple columns based on rowColSize + const halfSize = Math.floor(rowColSize / 2); + const startX = Math.max(0, x - halfSize); + const endX = Math.min(w - 1, x + halfSize); + + for (let colX = startX; colX <= endX; colX++) { + for (let y = 0; y < h; y++) { + const idx = y * w + colX; + let val = state.resizeIgnoreMask[idx]; + if (maskMode === 'toggle') { + val = val ? 0 : 1; + } else if (maskMode === 'ignore') { + val = 1; + } else { + val = 0; + } + state.resizeIgnoreMask[idx] = val; + if (_maskData) { + const p = idx * 4; + if (val) { _maskData[p] = 255; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 150; } + else { _maskData[p] = 0; _maskData[p + 1] = 0; _maskData[p + 2] = 0; _maskData[p + 3] = 0; } + } + } + if (_maskData) { _markDirty(colX, 0); _markDirty(colX, h - 1); } + } + }; + + const redrawMaskOverlay = () => { + // Only flush the dirty region; full rebuild happens on size change + _flushDirty(); + }; + + const handlePaint = (e) => { + // Suppress painting while panning + if ((e.buttons & 4) === 4 || (e.buttons & 2) === 2 || allowPan) return; + const { x, y } = mapClientToPixel(e.clientX, e.clientY); + const w = baseCanvas.width, h = baseCanvas.height; + if (x < 0 || y < 0 || x >= w || y >= h) return; + const radius = Math.max(1, Math.floor(brushSize / 2)); + if (e.shiftKey) { + paintRow(y); + } else if (e.altKey) { + paintColumn(x); + } else { + paintCircle(x, y, radius); + } + lastPaintX = x; lastPaintY = y; + redrawMaskOverlay(); + }; + + maskCanvas.addEventListener('mousedown', (e) => { + if (e.button === 1 || e.button === 2 || allowPan) return; // let pan handler manage + draggingMask = true; handlePaint(e); + }); + // Avoid hijacking touch gestures for panning/zooming + maskCanvas.addEventListener('touchstart', (e) => { /* let panStage handle */ }, { passive: true }); + maskCanvas.addEventListener('touchmove', (e) => { /* let panStage handle */ }, { passive: true }); + maskCanvas.addEventListener('touchend', (e) => { /* let panStage handle */ }, { passive: true }); + window.addEventListener('mousemove', (e) => { if (draggingMask) handlePaint(e); }); + window.addEventListener('mouseup', () => { if (draggingMask) { draggingMask = false; saveBotSettings(); } }); + + if (clearIgnoredBtnEl) clearIgnoredBtnEl.addEventListener('click', () => { + const w = baseCanvas.width, h = baseCanvas.height; + if (state.resizeIgnoreMask) state.resizeIgnoreMask.fill(0); + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + if (invertMaskBtn) invertMaskBtn.addEventListener('click', () => { + if (!state.resizeIgnoreMask) return; + for (let i = 0; i < state.resizeIgnoreMask.length; i++) state.resizeIgnoreMask[i] = state.resizeIgnoreMask[i] ? 0 : 1; + const w = baseCanvas.width, h = baseCanvas.height; + _ensureMaskOverlayBuffers(w, h, true); + _updateResizePreview(); + saveBotSettings(); + }); + + confirmResize.onclick = async () => { + const newWidth = parseInt(widthSlider.value, 10); + const newHeight = parseInt(heightSlider.value, 10); + + // Generate the final paletted image data + const tempCanvas = document.createElement('canvas'); + const tempCtx = tempCanvas.getContext('2d'); + tempCanvas.width = newWidth; + tempCanvas.height = newHeight; + tempCtx.imageSmoothingEnabled = false; + if (baseProcessor !== processor && (!baseProcessor.img || !baseProcessor.canvas)) { + await baseProcessor.load(); + } + tempCtx.drawImage(baseProcessor.img, 0, 0, newWidth, newHeight); + const imgData = tempCtx.getImageData(0, 0, newWidth, newHeight); + const data = imgData.data; + const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + let totalValidPixels = 0; + const mask = (state.resizeIgnoreMask && state.resizeIgnoreMask.length === newWidth * newHeight) ? state.resizeIgnoreMask : null; + + const applyFSDitherFinal = async () => { + const w = newWidth, h = newHeight; + const n = w * h; + const { work, eligible } = ensureDitherBuffers(n); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + const i4 = idx * 4; + const r = data[i4], g = data[i4 + 1], b = data[i4 + 2], a = data[i4 + 3]; + const masked = mask && mask[idx]; + const isEligible = !masked && a >= tThresh2 && (state.paintWhitePixels || !Utils.isWhitePixel(r, g, b)); + eligible[idx] = isEligible ? 1 : 0; + work[idx * 3] = r; + work[idx * 3 + 1] = g; + work[idx * 3 + 2] = b; + if (!isEligible) { + data[i4 + 3] = 0; + } + } + // Yield to keep UI responsive + if ((y & 15) === 0) await Promise.resolve(); + } + + const diffuse = (nx, ny, er, eg, eb, factor) => { + if (nx < 0 || nx >= w || ny < 0 || ny >= h) return; + const nidx = ny * w + nx; + if (!eligible[nidx]) return; + const base = nidx * 3; + work[base] = Math.min(255, Math.max(0, work[base] + er * factor)); + work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor)); + work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor)); + }; + + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const idx = y * w + x; + if (!eligible[idx]) continue; + const base = idx * 3; + const r0 = work[base], g0 = work[base + 1], b0 = work[base + 2]; + const [nr, ng, nb] = Utils.findClosestPaletteColor(r0, g0, b0, state.activeColorPalette); + const i4 = idx * 4; + data[i4] = nr; + data[i4 + 1] = ng; + data[i4 + 2] = nb; + data[i4 + 3] = 255; + totalValidPixels++; + + const er = r0 - nr; + const eg = g0 - ng; + const eb = b0 - nb; + + diffuse(x + 1, y, er, eg, eb, 7 / 16); + diffuse(x - 1, y + 1, er, eg, eb, 3 / 16); + diffuse(x, y + 1, er, eg, eb, 5 / 16); + diffuse(x + 1, y + 1, er, eg, eb, 1 / 16); + } + // Yield every row to reduce jank + await Promise.resolve(); + } + }; + + if (state.ditheringEnabled) { + await applyFSDitherFinal(); + } else { + for (let i = 0; i < data.length; i += 4) { + const r = data[i], g = data[i + 1], b = data[i + 2], a = data[i + 3]; + const masked = mask && mask[(i >> 2)]; + const isTransparent = a < tThresh2 || masked; + const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(r, g, b); + if (isTransparent || isWhiteAndSkipped) { + data[i + 3] = 0; // overlay transparency + continue; + } + totalValidPixels++; + const [nr, ng, nb] = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + data[i] = nr; + data[i + 1] = ng; + data[i + 2] = nb; + data[i + 3] = 255; + } + } + tempCtx.putImageData(imgData, 0, 0); + + // Save the final pixel data for painting + // Persist the paletted (and possibly dithered) pixels so painting uses the same output seen in overlay + const palettedPixels = new Uint8ClampedArray(imgData.data); + state.imageData.pixels = palettedPixels; + state.imageData.width = newWidth; + state.imageData.height = newHeight; + state.imageData.totalPixels = totalValidPixels; + state.totalPixels = totalValidPixels; + state.paintedPixels = 0; + + state.resizeSettings = { baseWidth: width, baseHeight: height, width: newWidth, height: newHeight }; + saveBotSettings(); + + const finalImageBitmap = await createImageBitmap(tempCanvas); + await overlayManager.setImage(finalImageBitmap); + overlayManager.enable(); + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); + + // Keep state.imageData.processor as the original-based source; painting uses paletted pixels already stored + + updateStats(); + updateUI("resizeSuccess", "success", { width: newWidth, height: newHeight }); + closeResizeDialog(); + }; + + downloadPreviewBtn.onclick = () => { + try { + const w = baseCanvas.width, h = baseCanvas.height; + const out = document.createElement('canvas'); + out.width = w; out.height = h; + const octx = out.getContext('2d'); + octx.imageSmoothingEnabled = false; + octx.drawImage(baseCanvas, 0, 0); + octx.drawImage(maskCanvas, 0, 0); + const link = document.createElement('a'); + link.download = 'wplace-preview.png'; + link.href = out.toDataURL(); + link.click(); + } catch (e) { console.warn('Failed to download preview:', e); } + }; + + cancelResize.onclick = closeResizeDialog; + + resizeOverlay.style.display = "block"; + resizeContainer.style.display = "block"; + + // Reinitialize color palette with current available colors + initializeColorPalette(resizeContainer); + + _updateResizePreview(); + _resizeDialogCleanup = () => { + try { zoomSlider.replaceWith(zoomSlider.cloneNode(true)); } catch { } + try { if (zoomInBtn) zoomInBtn.replaceWith(zoomInBtn.cloneNode(true)); } catch { } + try { if (zoomOutBtn) zoomOutBtn.replaceWith(zoomOutBtn.cloneNode(true)); } catch { } + }; + setTimeout(() => { + if (typeof computeFitZoom === 'function') { + const z = computeFitZoom(); + if (!isNaN(z) && isFinite(z)) { + applyZoom(z); + centerInView(); + } + } else { + centerInView(); + } + }, 0); + } + + function closeResizeDialog() { + try { if (typeof _resizeDialogCleanup === 'function') { _resizeDialogCleanup(); } } catch { } + resizeOverlay.style.display = "none"; + resizeContainer.style.display = "none"; + _updateResizePreview = () => { }; + try { if (typeof cancelAnimationFrame === 'function' && _panRaf) { cancelAnimationFrame(_panRaf); } } catch { } + try { if (_previewTimer) { clearTimeout(_previewTimer); _previewTimer = null; } } catch { } + _maskImageData = null; _maskData = null; _dirty = null; + _ditherWorkBuf = null; _ditherEligibleBuf = null; + _resizeDialogCleanup = null; + } + + if (uploadBtn) { + uploadBtn.addEventListener("click", async () => { + const availableColors = Utils.extractAvailableColors(); + if (availableColors.length < 10) { + updateUI("noColorsFound", "error"); + Utils.showAlert(Utils.t("noColorsFound"), "error"); + return; + } + + if (!state.colorsChecked) { + state.availableColors = availableColors; + state.colorsChecked = true; + updateUI("colorsFound", "success", { count: availableColors.length }); + updateStats(); + selectPosBtn.disabled = false; + // Only enable resize button if image is also loaded + if (state.imageLoaded) { + resizeBtn.disabled = false; + } + } + + try { + updateUI("loadingImage", "default") + const imageSrc = await Utils.createImageUploader() + if (!imageSrc) { + updateUI("colorsFound", "success", { count: state.availableColors.length }); + return; + } + + const processor = new ImageProcessor(imageSrc) + await processor.load() + + const { width, height } = processor.getDimensions() + const pixels = processor.getPixelData() + + let totalValidPixels = 0; + for (let i = 0; i < pixels.length; i += 4) { + const isTransparent = pixels[i + 3] < (state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD); + const isWhiteAndSkipped = !state.paintWhitePixels && Utils.isWhitePixel(pixels[i], pixels[i + 1], pixels[i + 2]); + if (!isTransparent && !isWhiteAndSkipped) { + totalValidPixels++; + } + } + + state.imageData = { + width, + height, + pixels, + totalPixels: totalValidPixels, + processor, + } + + state.totalPixels = totalValidPixels + state.paintedPixels = 0 + state.imageLoaded = true + state.lastPosition = { x: 0, y: 0 } + + // Initialize painted map for tracking + Utils.initializePaintedMap(width, height); + + // New image: clear previous resize settings + state.resizeSettings = null; + // Also clear any previous ignore mask + state.resizeIgnoreMask = null; + // Save original image for this browser (dataUrl + dims) + state.originalImage = { dataUrl: imageSrc, width, height }; + saveBotSettings(); + + // Use the original image for the overlay initially + const imageBitmap = await createImageBitmap(processor.img); + await overlayManager.setImage(imageBitmap); + overlayManager.enable(); + toggleOverlayBtn.disabled = false; + toggleOverlayBtn.classList.add('active'); + toggleOverlayBtn.setAttribute('aria-pressed', 'true'); + + // Only enable resize button if colors have also been captured + if (state.colorsChecked) { + resizeBtn.disabled = false; + } + saveBtn.disabled = false + + if (state.startPosition) { + startBtn.disabled = false + } + + updateStats() + updateDataButtons() + updateUI("imageLoaded", "success", { count: totalValidPixels }) + } catch { + updateUI("imageError", "error") + } + }) + } + + if (resizeBtn) { + resizeBtn.addEventListener("click", () => { + if (state.imageLoaded && state.imageData.processor && state.colorsChecked) { + showResizeDialog(state.imageData.processor) + } else if (!state.colorsChecked) { + Utils.showAlert("Please upload an image first to capture available colors", "warning") + } + }) + } + + if (selectPosBtn) { + selectPosBtn.addEventListener("click", async () => { + if (state.selectingPosition) return + + state.selectingPosition = true + state.startPosition = null + state.region = null + startBtn.disabled = true + + Utils.showAlert(Utils.t("selectPositionAlert"), "info") + updateUI("waitingPosition", "default") + + const tempFetch = async (url, options) => { + if ( + typeof url === "string" && + url.includes("https://backend.wplace.live/s0/pixel/") && + options?.method?.toUpperCase() === "POST" + ) { + try { + const response = await originalFetch(url, options) + const clonedResponse = response.clone() + const data = await clonedResponse.json() + + if (data?.painted === 1) { + const regionMatch = url.match(/\/pixel\/(\d+)\/(\d+)/) + if (regionMatch && regionMatch.length >= 3) { + state.region = { + x: Number.parseInt(regionMatch[1]), + y: Number.parseInt(regionMatch[2]), + } + } + + const payload = JSON.parse(options.body) + if (payload?.coords && Array.isArray(payload.coords)) { + state.startPosition = { + x: payload.coords[0], + y: payload.coords[1], + } + state.lastPosition = { x: 0, y: 0 } + + await overlayManager.setPosition(state.startPosition, state.region); + + if (state.imageLoaded) { + startBtn.disabled = false + } + + window.fetch = originalFetch + state.selectingPosition = false + updateUI("positionSet", "success") + } + } + + return response + } catch { + return originalFetch(url, options) + } + } + return originalFetch(url, options) + } + + const originalFetch = window.fetch; + window.fetch = tempFetch; + + setTimeout(() => { + if (state.selectingPosition) { + window.fetch = originalFetch + state.selectingPosition = false + updateUI("positionTimeout", "error") + Utils.showAlert(Utils.t("positionTimeout"), "error") + } + }, 120000) + }) + } + + async function startPainting() { + if (!state.imageLoaded || !state.startPosition || !state.region) { + updateUI("missingRequirements", "error") + return false + } + await ensureToken() + if (!turnstileToken) return false + + state.running = true + state.stopFlag = false + startBtn.disabled = true + stopBtn.disabled = false + uploadBtn.disabled = true + selectPosBtn.disabled = true + resizeBtn.disabled = true + saveBtn.disabled = true + toggleOverlayBtn.disabled = true; + + updateUI("startPaintingMsg", "success") + + try { + await getAccounts(); + await processImage(); + return true + } catch { + updateUI("paintingError", "error") + return false + } finally { + state.running = false + stopBtn.disabled = true + saveBtn.disabled = false + + if (!state.stopFlag) { + startBtn.disabled = true + uploadBtn.disabled = false + selectPosBtn.disabled = false + resizeBtn.disabled = false + } else { + startBtn.disabled = false + } + toggleOverlayBtn.disabled = false; + } + } + + if (startBtn) { + startBtn.addEventListener("click", startPainting) + } + + if (stopBtn) { + stopBtn.addEventListener("click", () => { + state.stopFlag = true + state.running = false + stopBtn.disabled = true + updateUI("paintingStopped", "warning") + + if (state.imageLoaded && state.paintedPixels > 0) { + Utils.saveProgress() + Utils.showAlert(Utils.t("autoSaved"), "success") + } + }) + } + + const checkSavedProgress = () => { + const savedData = Utils.loadProgress() + if (savedData && savedData.state.paintedPixels > 0) { + const savedDate = new Date(savedData.timestamp).toLocaleString() + const progress = Math.round((savedData.state.paintedPixels / savedData.state.totalPixels) * 100) + + Utils.showAlert( + `${Utils.t("savedDataFound")}\n\n` + + `Saved: ${savedDate}\n` + + `Progress: ${savedData.state.paintedPixels}/${savedData.state.totalPixels} pixels (${progress}%)\n` + + `${Utils.t("clickLoadToContinue")}`, + "info", + ) + } + } + + setTimeout(checkSavedProgress, 1000) + + if (cooldownSlider && cooldownValue) { + cooldownSlider.addEventListener("input", (e) => { + const threshold = parseInt(e.target.value); + state.cooldownChargeThreshold = threshold; + cooldownValue.textContent = threshold; + saveBotSettings(); + NotificationManager.resetEdgeTracking(); // prevent spurious notify after threshold change + }); + } + + loadBotSettings(); + // Ensure notification poller reflects current settings + 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 + + 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 (a < tThresh2) return false; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) return false; + return true; + }; + + 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++; + } + } + if (!foundStart) { startRow = height; startCol = 0; } + + let pixelBatch = null; + let skippedPixels = { transparent: 0, white: 0, alreadyPainted: 0 }; + + 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 idx = (y * width + x) * 4 + const r = pixels[idx] + const g = pixels[idx + 1] + const b = pixels[idx + 2] + const alpha = pixels[idx + 3] + + const tThresh2 = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + if (alpha < tThresh2 || (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b))) { + if (alpha < tThresh2) { + skippedPixels.transparent++; + } else { + skippedPixels.white++; + } + continue; + } + + let targetRgb; + if (Utils.isWhitePixel(r, g, b)) { + targetRgb = [255, 255, 255]; + } else { + targetRgb = Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + } + + const colorId = findClosestColor([r, g, b], state.availableColors); + + let absX = startX + x; + let absY = startY + y; + + let adderX = Math.floor(absX / 1000); + let adderY = Math.floor(absY / 1000); + let pixelX = absX % 1000; + let pixelY = absY % 1000; + + 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 (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) + } + } 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.pixels = []; + } + if (!CONFIG.autoSwap) { + while (state.currentCharges < state.cooldownChargeThreshold && !state.stopFlag) { + const { charges, cooldown } = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(charges); + state.cooldown = cooldown; + + if (state.currentCharges >= state.cooldownChargeThreshold) { + // Edge-trigger a notification the instant threshold is crossed + NotificationManager.maybeNotifyChargesReached(true); + updateStats(); + break; + } + + // 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); + } + } + else { + if (state.currentCharges < state.cooldownChargeThreshold && !state.stopFlag) { + console.log("⚠️ Charges too low, swapping to next account..."); + + const accounts = JSON.parse(localStorage.getItem("accounts")) || []; + if (accounts.length === 0) { + console.warn("❌ No accounts available, stopping painting."); + state.stopFlag = true; + return; + } + + state.accountIndex = (state.accountIndex + 1) % accounts.length; + console.log("🔄 Switching to account index:", state.accountIndex); + + const nextToken = accounts[state.accountIndex]; + console.log("🔑 Next token:", nextToken); + + if (!nextToken) { + console.warn("⚠️ Invalid token, skipping..."); + return; + } + + swapAccountTrigger(nextToken); + + let maxRetries = 20; + let retryCount = 0; + let swapSuccess = false; + + while (!swapSuccess && retryCount < maxRetries) { + console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); + + // Wait for a short period before checking. + await new Promise(resolve => setTimeout(resolve, 1000)); + + try { + await fetchAccount(); + + console.log("✅ Account swap confirmed."); + swapSuccess = true; + } catch (error) { + console.warn("❌ Account swap not yet successful. Retrying...", error); + retryCount++; + } + } + + if (swapSuccess) { + + const { charges, cooldown } = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(charges); + state.cooldown = cooldown; + Utils.performSmartSave(); + updateStats(); + } else { + console.error("❌ Failed to swap account after multiple retries. Stopping loop."); + state.stopFlag = true; + } + } + } + + // Disable save button again after exiting wait loop (back to normal painting) + if (!state.stopFlag) { + saveBtn.disabled = true; + } + if (state.stopFlag) break outerLoop; + + } + } + + if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { + console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); + const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY); + 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.`); + } + } + } finally { + 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() + } else { + updateUI("paintingComplete", "success", { count: state.paintedPixels }) + state.lastPosition = { x: 0, y: 0 } + // Keep painted map until user starts new project + // state.paintedMap = null // Commented out to preserve data + Utils.saveProgress() // Save final complete state + overlayManager.clear(); + const toggleOverlayBtn = document.getElementById('toggleOverlayBtn'); + if (toggleOverlayBtn) { + toggleOverlayBtn.classList.remove('active'); + toggleOverlayBtn.disabled = true; + } + } + + // Log skip statistics + console.log(`📊 Pixel Statistics:`); + console.log(` Painted: ${state.paintedPixels}`); + console.log(` Skipped - Transparent: ${skippedPixels.transparent}`); + console.log(` Skipped - White (disabled): ${skippedPixels.white}`); + console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`); + console.log(` Total processed: ${state.paintedPixels + skippedPixels.transparent + skippedPixels.white + skippedPixels.alreadyPainted}`); + + updateStats() + } + + const loadAccountNames = () => { + try { + return JSON.parse(localStorage.getItem("wplace-account-names")) || {}; + } catch (e) { + return {}; + } + }; + + const saveAccountNames = (names) => { + try { + localStorage.setItem("wplace-account-names", JSON.stringify(names)); + } catch (e) { + console.warn("Could not save account names:", e); + } + }; + + async function editAccountName(token, currentName) { + const newName = prompt("Enter a new name for this account:", currentName); + if (newName === null) return; // User cancelled + + if (newName.trim() === "") { + Utils.showAlert("Account name cannot be empty.", "warning"); + return; + } + + const names = loadAccountNames(); + names[token] = newName.trim(); + saveAccountNames(names); + + const accountInfo = state.allAccountsInfo.find(acc => acc.token === token); + if (accountInfo) { + accountInfo.displayName = newName.trim(); + } + + renderAccountsList(); + Utils.showAlert("Account name updated.", "success"); + } + + async function deleteAccount(token, name) { + if (!confirm(`Are you sure you want to delete the account "${name}"?\nThis action cannot be undone.`)) { + return; + } + + let accounts = JSON.parse(localStorage.getItem("accounts")) || []; + const initialCount = accounts.length; + accounts = accounts.filter(t => t !== token); + if (accounts.length < initialCount) { + localStorage.setItem("accounts", JSON.stringify(accounts)); + } + + const names = loadAccountNames(); + delete names[token]; + saveAccountNames(names); + + state.allAccountsInfo = state.allAccountsInfo.filter(acc => acc.token !== token); + renderAccountsList(); + Utils.showAlert(`Account "${name}" has been deleted.`, "success"); + } + + function renderAccountsList() { + const accountsListArea = document.getElementById('accountsListArea'); + if (!accountsListArea) return; + + accountsListArea.innerHTML = ''; + if (state.allAccountsInfo.length === 0) { + accountsListArea.innerHTML = `
No account data. Click to refresh.
`; + return; + } + + const theme = getCurrentTheme(); + state.allAccountsInfo.forEach((info, index) => { + const item = Utils.createElement('div', { + className: `wplace-account-item ${info.isCurrent ? 'is-current' : ''}` + }); + + const displayName = info.displayName || `Account ${index + 1}`; // this line is fine + + const details = Utils.createElement('div', { className: 'wplace-account-details' }); + details.appendChild(Utils.createElement('div', { className: 'wplace-account-name', title: displayName }, displayName)); + // details.appendChild(Utils.createElement('div', { className: 'wplace-account-id' }, `ID: ${info.ID}`)); + + let stats; + if (info.error) { + stats = Utils.createElement('div', { className: 'wplace-account-stats', style: { color: theme.error } }, 'Error'); + } else { + stats = Utils.createElement('div', { + className: 'wplace-account-stats', + innerHTML: ` + ${Math.floor(info.Charges || 0)}/${Math.floor(info.Max || 0)} + ${Math.floor(info.Droplets || 0)} + ` + }); + } + + const controls = Utils.createElement('div', { className: 'wplace-account-controls' }); + const editBtn = Utils.createElement('button', { + className: 'wplace-account-btn edit-btn', + title: 'Edit Name', + innerHTML: '' + }); + editBtn.addEventListener('click', (e) => { + e.stopPropagation(); + editAccountName(info.token, displayName); + }); + + const deleteBtn = Utils.createElement('button', { + className: 'wplace-account-btn delete-btn', + title: 'Delete Account', + innerHTML: '' + }); + deleteBtn.addEventListener('click', (e) => { + e.stopPropagation(); + deleteAccount(info.token, displayName); + }); + + controls.appendChild(editBtn); + controls.appendChild(deleteBtn); + + item.appendChild(details); + item.appendChild(stats); + item.appendChild(controls); + + accountsListArea.appendChild(item); + }); + } + + async function fetchAllAccountDetails() { + if (state.isFetchingAllAccounts) { + Utils.showAlert("Already fetching account details.", "warning"); + return; + } + state.isFetchingAllAccounts = true; + + const refreshBtn = document.getElementById('refreshAllAccountsBtn'); + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = true; + } + + const accountsListArea = document.getElementById('accountsListArea'); + if (accountsListArea) { + accountsListArea.innerHTML = `
Initializing...
`; + } + + let originalToken = null; + + try { + await getAccounts(); + const accountsTokens = JSON.parse(localStorage.getItem("accounts")) || []; + if (accountsTokens.length === 0) { + if (accountsListArea) accountsListArea.innerHTML = `
No accounts found.
`; + return; + } + + const { id: originalId } = await WPlaceService.getCharges(); + state.allAccountsInfo = []; + renderAccountsList(); + + const accountNames = loadAccountNames(); + for (let i = 0; i < accountsTokens.length; i++) { + const token = accountsTokens[i]; + swapAccountTrigger(token); + + let retries = 0; + let swapped = false; + let fetchedInfo = null; + while (retries < 5 && !swapped) { + await Utils.sleep(1000); + try { + fetchedInfo = await WPlaceService.fetchCheck(); + if (fetchedInfo.ID) swapped = true; + } catch (e) { retries++; } + } + + if (swapped) { + await fetchAccount(); + // await purchase("max_charges"); + const displayName = accountNames[token] || `Account ${i + 1}`; + if (fetchedInfo.ID === originalId) originalToken = token; + state.allAccountsInfo.push({ ...fetchedInfo, token, displayName, isCurrent: fetchedInfo.ID === originalId }); + } else { + const displayName = accountNames[token] || `Account ${i + 1}`; + state.allAccountsInfo.push({ token, ID: `...${token.slice(-4)}`, displayName, error: 'Failed to fetch' }); + } + renderAccountsList(); + } + } catch (error) { + console.error("Error fetching all account details:", error); + if (accountsListArea) accountsListArea.innerHTML = `
Error loading accounts.
`; + } finally { + if (originalToken) swapAccountTrigger(originalToken); + await Utils.sleep(1000); + + // After switching back, update stats and sync the list + const meData = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(meData.charges); + state.cooldown = meData.cooldown; + state.maxCharges = Math.floor(meData.max) > 1 ? Math.floor(meData.max) : state.maxCharges; + const currentAccountInList = state.allAccountsInfo.find(acc => acc.ID === meData.id); + if (currentAccountInList) { + currentAccountInList.Charges = state.currentCharges; + currentAccountInList.Max = state.maxCharges; + currentAccountInList.Droplets = meData.droplets; + } + await updateStats(); // This will re-render the main stats + renderAccountsList(); // This will re-render the list with synced data + + state.isFetchingAllAccounts = false; + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = false; + } + } + } + + // Helper function to calculate batch size based on mode + function calculateBatchSize() { + let targetBatchSize; + + if (state.batchMode === 'random') { + // Generate random batch size within the specified range + const min = Math.max(1, state.randomBatchMin); + const max = Math.max(min, state.randomBatchMax); + targetBatchSize = Math.floor(Math.random() * (max - min + 1)) + min; + console.log(`🎲 Random batch size generated: ${targetBatchSize} (range: ${min}-${max})`); + } else { + // Normal mode - use the fixed paintingSpeed value + targetBatchSize = state.paintingSpeed; + } + + // Always limit by available charges + const maxAllowed = Math.floor(state.currentCharges); + const finalBatchSize = Math.min(targetBatchSize, maxAllowed); + + return finalBatchSize; + } + + // Helper function to retry batch until success with exponential backoff + async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) { + let attempt = 0; + while (attempt < maxRetries && !state.stopFlag) { + attempt++; + console.log(`🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels`); + + const result = await sendPixelBatch(pixels, regionX, regionY); + + if (result === true) { + console.log(`✅ Batch succeeded on attempt ${attempt}`); + return true; + } else if (result === "token_error") { + console.log(`🔑 Token error on attempt ${attempt}, regenerating...`); + updateUI("captchaSolving", "warning"); + try { + await handleCaptcha(); + // Don't count token regeneration as a failed attempt + attempt--; + continue; + } catch (e) { + console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e); + updateUI("captchaFailed", "error"); + // Wait longer before retrying after token failure + await Utils.sleep(5000); + } + } else { + console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`); + // Exponential backoff with jitter + const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s + const jitter = Math.random() * 1000; // Add up to 1s random delay + await Utils.sleep(baseDelay + jitter); + } + } + + if (attempt >= maxRetries) { + console.error(`❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.`); + updateUI("paintingError", "error"); + return false; + } + + return false; + } + + async function sendPixelBatch(pixelBatch, regionX, regionY) { + let token = turnstileToken; + + // Generate new token if we don't have one + if (!token) { + try { + console.log("🔑 Generating Turnstile token for pixel batch..."); + token = await handleCaptcha(); + turnstileToken = token; // Store for potential reuse + } catch (error) { + console.error("❌ Failed to generate Turnstile token:", error); + tokenPromise = new Promise((resolve) => { _resolveToken = resolve }); + return "token_error"; + } + } + + const coords = new Array(pixelBatch.length * 2) + const colors = new Array(pixelBatch.length) + for (let i = 0; i < pixelBatch.length; i++) { + const pixel = pixelBatch[i] + coords[i * 2] = pixel.x + coords[i * 2 + 1] = pixel.y + colors[i] = pixel.color + } + + try { + const payload = { coords, colors, t: token, fp: randStr(10) }; + var wasmtoken = await createWasmToken(regionX, regionY, payload); + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token":wasmtoken }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (res.status === 403) { + let data = null + try { data = await res.json() } catch (_) { } + console.error("❌ 403 Forbidden. Turnstile token might be invalid or expired.") + + // Try to generate a new token and retry once + try { + console.log("🔄 Regenerating Turnstile token after 403..."); + token = await handleCaptcha(); + turnstileToken = token; + + // Retry the request with new token + const retryPayload = { coords, colors, t: token, fp: randStr(10) }; + var wasmtoken = await createWasmToken(regionX, regionY, retryPayload); + const retryRes = await fetch( + `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, + { + method: 'POST', + headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token":wasmtoken }, + credentials: 'include', + body: JSON.stringify(retryPayload), + } + ); + + if (retryRes.status === 403) { + turnstileToken = null; + tokenPromise = new Promise((resolve) => { _resolveToken = resolve }); + return "token_error"; + } + + const retryData = await retryRes.json(); + return retryData?.painted === pixelBatch.length; + + } catch (retryError) { + console.error("❌ Token regeneration failed:", retryError); + turnstileToken = null; + tokenPromise = new Promise((resolve) => { _resolveToken = resolve }); + return "token_error"; + } + } + + const data = await res.json() + return data?.painted === pixelBatch.length + } catch (e) { + console.error("Batch paint request failed:", e) + return false + } + } + + function saveBotSettings() { + try { + const settings = { + paintingSpeed: state.paintingSpeed, + paintingSpeedEnabled: document.getElementById('enableSpeedToggle')?.checked, + batchMode: state.batchMode, // "normal" or "random" + randomBatchMin: state.randomBatchMin, + randomBatchMax: state.randomBatchMax, + cooldownChargeThreshold: state.cooldownChargeThreshold, + autoSwap: CONFIG.autoSwap, + tokenSource: state.tokenSource, // "generator", "hybrid", or "manual" + minimized: state.minimized, + overlayOpacity: state.overlayOpacity, + blueMarbleEnabled: document.getElementById('enableBlueMarbleToggle')?.checked, + ditheringEnabled: state.ditheringEnabled, + colorMatchingAlgorithm: state.colorMatchingAlgorithm, + enableChromaPenalty: state.enableChromaPenalty, + chromaPenaltyWeight: state.chromaPenaltyWeight, + customTransparencyThreshold: state.customTransparencyThreshold, + customWhiteThreshold: state.customWhiteThreshold, + paintWhitePixels: state.paintWhitePixels, + resizeSettings: state.resizeSettings, + originalImage: state.originalImage, + // Save ignore mask (as base64) with its dimensions + resizeIgnoreMask: (state.resizeIgnoreMask && state.resizeSettings && state.resizeSettings.width * state.resizeSettings.height === state.resizeIgnoreMask.length) + ? { w: state.resizeSettings.width, h: state.resizeSettings.height, data: btoa(String.fromCharCode(...state.resizeIgnoreMask)) } + : null, + // Notifications + notificationsEnabled: state.notificationsEnabled, + notifyOnChargesReached: state.notifyOnChargesReached, + notifyOnlyWhenUnfocused: state.notifyOnlyWhenUnfocused, + notificationIntervalMinutes: state.notificationIntervalMinutes, + }; + CONFIG.PAINTING_SPEED_ENABLED = settings.paintingSpeedEnabled; + // AUTO_CAPTCHA_ENABLED is always true - no need to save/load + + localStorage.setItem("wplace-bot-settings", JSON.stringify(settings)); + } catch (e) { + console.warn("Could not save bot settings:", e); + } + } + + function loadBotSettings() { + try { + const saved = localStorage.getItem("wplace-bot-settings"); + if (!saved) return; + const settings = JSON.parse(saved); + + state.paintingSpeed = settings.paintingSpeed || CONFIG.PAINTING_SPEED.DEFAULT; + state.batchMode = settings.batchMode || CONFIG.BATCH_MODE; // Default to "normal" + state.randomBatchMin = settings.randomBatchMin || CONFIG.RANDOM_BATCH_RANGE.MIN; + state.randomBatchMax = settings.randomBatchMax || CONFIG.RANDOM_BATCH_RANGE.MAX; + state.cooldownChargeThreshold = settings.cooldownChargeThreshold || CONFIG.COOLDOWN_CHARGE_THRESHOLD; + CONFIG.autoSwap = settings.autoSwap ?? false; + 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.resizeSettings = settings.resizeSettings ?? null; + state.originalImage = settings.originalImage ?? null; + // Notifications + state.notificationsEnabled = settings.notificationsEnabled ?? CONFIG.NOTIFICATIONS.ENABLED; + state.notifyOnChargesReached = settings.notifyOnChargesReached ?? CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED; + state.notifyOnlyWhenUnfocused = settings.notifyOnlyWhenUnfocused ?? CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED; + state.notificationIntervalMinutes = settings.notificationIntervalMinutes ?? CONFIG.NOTIFICATIONS.REPEAT_MINUTES; + // Restore ignore mask if dims match current resizeSettings + if (settings.resizeIgnoreMask && settings.resizeIgnoreMask.data && state.resizeSettings && settings.resizeIgnoreMask.w === state.resizeSettings.width && settings.resizeIgnoreMask.h === state.resizeSettings.height) { + try { + const bin = atob(settings.resizeIgnoreMask.data); + const arr = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i); + state.resizeIgnoreMask = arr; + } catch { state.resizeIgnoreMask = null; } + } else { + state.resizeIgnoreMask = null; + } + + const speedSlider = document.getElementById('speedSlider'); + if (speedSlider) speedSlider.value = state.paintingSpeed; + const speedValue = document.getElementById('speedValue'); + if (speedValue) speedValue.textContent = `${state.paintingSpeed} (batch size)`; + + const enableSpeedToggle = document.getElementById('enableSpeedToggle'); + if (enableSpeedToggle) enableSpeedToggle.checked = CONFIG.PAINTING_SPEED_ENABLED; + + // Batch mode UI initialization + const batchModeSelect = document.getElementById('batchModeSelect'); + if (batchModeSelect) batchModeSelect.value = state.batchMode; + + const normalBatchControls = document.getElementById('normalBatchControls'); + const randomBatchControls = document.getElementById('randomBatchControls'); + + // Show/hide appropriate controls based on batch mode + if (normalBatchControls && randomBatchControls) { + if (state.batchMode === 'random') { + normalBatchControls.style.display = 'none'; + randomBatchControls.style.display = 'block'; + } else { + normalBatchControls.style.display = 'block'; + randomBatchControls.style.display = 'none'; + } + } + + const randomBatchMin = document.getElementById('randomBatchMin'); + if (randomBatchMin) randomBatchMin.value = state.randomBatchMin; + + const randomBatchMax = document.getElementById('randomBatchMax'); + if (randomBatchMax) randomBatchMax.value = state.randomBatchMax; + + // AUTO_CAPTCHA_ENABLED is always true - no toggle to set + + const cooldownSlider = document.getElementById('cooldownSlider'); + if (cooldownSlider) cooldownSlider.value = state.cooldownChargeThreshold; + const cooldownValue = document.getElementById('cooldownValue'); + if (cooldownValue) cooldownValue.textContent = state.cooldownChargeThreshold; + + const autoSwapToggle = document.getElementById('autoSwapToggle'); + if (autoSwapToggle) autoSwapToggle.checked = CONFIG.autoSwap; + + const overlayOpacitySlider = document.getElementById('overlayOpacitySlider'); + if (overlayOpacitySlider) overlayOpacitySlider.value = state.overlayOpacity; + const overlayOpacityValue = document.getElementById('overlayOpacityValue'); + if (overlayOpacityValue) overlayOpacityValue.textContent = `${Math.round(state.overlayOpacity * 100)}%`; + const enableBlueMarbleToggle = document.getElementById('enableBlueMarbleToggle'); + if (enableBlueMarbleToggle) enableBlueMarbleToggle.checked = state.blueMarbleEnabled; + + const tokenSourceSelect = document.getElementById('tokenSourceSelect'); + if (tokenSourceSelect) tokenSourceSelect.value = state.tokenSource; + + const colorAlgorithmSelect = document.getElementById('colorAlgorithmSelect'); + if (colorAlgorithmSelect) colorAlgorithmSelect.value = state.colorMatchingAlgorithm; + const enableChromaPenaltyToggle = document.getElementById('enableChromaPenaltyToggle'); + if (enableChromaPenaltyToggle) enableChromaPenaltyToggle.checked = state.enableChromaPenalty; + const chromaPenaltyWeightSlider = document.getElementById('chromaPenaltyWeightSlider'); + if (chromaPenaltyWeightSlider) chromaPenaltyWeightSlider.value = state.chromaPenaltyWeight; + const chromaWeightValue = document.getElementById('chromaWeightValue'); + if (chromaWeightValue) chromaWeightValue.textContent = state.chromaPenaltyWeight; + const transparencyThresholdInput = document.getElementById('transparencyThresholdInput'); + if (transparencyThresholdInput) transparencyThresholdInput.value = state.customTransparencyThreshold; + const whiteThresholdInput = document.getElementById('whiteThresholdInput'); + if (whiteThresholdInput) whiteThresholdInput.value = state.customWhiteThreshold; + // Notifications UI + const notifEnabledToggle = document.getElementById('notifEnabledToggle'); + if (notifEnabledToggle) notifEnabledToggle.checked = state.notificationsEnabled; + const notifOnChargesToggle = document.getElementById('notifOnChargesToggle'); + if (notifOnChargesToggle) notifOnChargesToggle.checked = state.notifyOnChargesReached; + const notifOnlyUnfocusedToggle = document.getElementById('notifOnlyUnfocusedToggle'); + if (notifOnlyUnfocusedToggle) notifOnlyUnfocusedToggle.checked = state.notifyOnlyWhenUnfocused; + const notifIntervalInput = document.getElementById('notifIntervalInput'); + if (notifIntervalInput) notifIntervalInput.value = state.notificationIntervalMinutes; + NotificationManager.resetEdgeTracking(); + + } catch (e) { + console.warn("Could not load bot settings:", e); + } + } + + // Initialize Turnstile generator integration + console.log("🚀 WPlace Auto-Image with Turnstile Token Generator loaded"); + console.log("🔑 Turnstile token generator: ALWAYS ENABLED (Background mode)"); + console.log("🎯 Manual pixel captcha solving: Available as fallback/alternative"); + console.log("📱 Turnstile widgets: DISABLED - pure background token generation only!"); + + // Function to enable file operations after initial startup setup is complete + function enableFileOperations() { + state.initialSetupComplete = true; + + const loadBtn = document.querySelector("#loadBtn"); + const loadFromFileBtn = document.querySelector("#loadFromFileBtn"); + const uploadBtn = document.querySelector("#uploadBtn"); + + if (loadBtn) { + loadBtn.disabled = false; + loadBtn.title = ""; + // Add a subtle animation to indicate the button is now available + loadBtn.style.animation = "pulse 0.6s ease-in-out"; + setTimeout(() => { + if (loadBtn) loadBtn.style.animation = ""; + }, 600); + console.log("✅ Load Progress button enabled after initial setup"); + } + + if (loadFromFileBtn) { + loadFromFileBtn.disabled = false; + loadFromFileBtn.title = ""; + // Add a subtle animation to indicate the button is now available + loadFromFileBtn.style.animation = "pulse 0.6s ease-in-out"; + setTimeout(() => { + if (loadFromFileBtn) loadFromFileBtn.style.animation = ""; + }, 600); + console.log("✅ Load from File button enabled after initial setup"); + } + + if (uploadBtn) { + uploadBtn.disabled = false; + uploadBtn.title = ""; + // Add a subtle animation to indicate the button is now available + uploadBtn.style.animation = "pulse 0.6s ease-in-out"; + setTimeout(() => { + if (uploadBtn) uploadBtn.style.animation = ""; + }, 600); + console.log("✅ Upload Image button enabled after initial setup"); + } + + // Show a notification that file operations are now available + Utils.showAlert("📂 File operations (Load/Upload) are now available!", "success"); + } + + // Optimized token initialization with better timing and error handling + async function initializeTokenGenerator() { + // Skip if already have valid token + if (isTokenValid()) { + console.log("✅ Valid token already available, skipping initialization"); + updateUI("tokenReady", "success"); + enableFileOperations(); // Enable file operations since initial setup is complete + return; + } + + try { + console.log("🔧 Initializing Turnstile token generator..."); + updateUI("initializingToken", "default"); + + // Pre-load Turnstile script first to avoid delays later + await Utils.loadTurnstile(); + + const token = await handleCaptchaWithRetry(); + if (token) { + setTurnstileToken(token); + console.log("✅ Startup token generated successfully"); + updateUI("tokenReady", "success"); + Utils.showAlert("🔑 Token generator ready!", "success"); + enableFileOperations(); // Enable file operations since initial setup is complete + } else { + console.warn("⚠️ Startup token generation failed, 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.warn("⚠️ Startup token generation failed:", 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(); + async function createWasmToken(regionX,regionY, payload) { + try { + // Load the Pawtect module and WASM + + async function getModURL(update) { + if (update === true || localStorage.getItem('pawtect-wasm-location') === null) { + const rootHTML = (await (await fetch(window.location.href)).text()).split('\n') + + for (const line of rootHTML) { + if (line.includes(' r.ok ? r.json() : null); + if (me?.id) + { + mod.i(me.id); + console.log('✅ user ID set:', me.id); + } + } catch { } + } catch (userIdError) { + console.log('⚠️ Error setting user ID:', userIdError.message); + } + try { + const testUrl = `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`; + if (mod.r) { + mod.r(testUrl); + console.log('✅ Request URL set:', testUrl); + } else { + console.log('⚠️ request_url function (mod.r) not available'); + } + } catch (urlError) { + console.log('⚠️ Error setting request URL:', urlError.message); + } + + // Create test payload + + console.log('📝 payload:', payload); + + // Encode payload + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const bodyStr = JSON.stringify(payload); + const bytes = enc.encode(bodyStr); + console.log('📏 Payload size:', bytes.length, 'bytes'); + console.log('📄 Payload string:', bodyStr); + + // Allocate WASM memory with validation + let inPtr; + try { + if (!wasm.__wbindgen_malloc) { + console.error('❌ __wbindgen_malloc function not found'); + return null; + } + + inPtr = wasm.__wbindgen_malloc(bytes.length, 1); + console.log('✅ WASM memory allocated, pointer:', inPtr); + + // Copy data to WASM memory + const wasmBuffer = new Uint8Array(wasm.memory.buffer, inPtr, bytes.length); + wasmBuffer.set(bytes); + console.log('✅ Data copied to WASM memory'); + } catch (memError) { + console.error('❌ Memory allocation error:', memError); + return null; + } + + // Call the WASM function + console.log('🚀 Calling get_pawtected_endpoint_payload...'); + let outPtr, outLen, token; + try { + const result = wasm.get_pawtected_endpoint_payload(inPtr, bytes.length); + console.log('✅ Function called, result type:', typeof result, result); + + if (Array.isArray(result) && result.length === 2) { + [outPtr, outLen] = result; + console.log('✅ Got output pointer:', outPtr, 'length:', outLen); + + // Decode the result + const outputBuffer = new Uint8Array(wasm.memory.buffer, outPtr, outLen); + token = dec.decode(outputBuffer); + console.log('✅ Token decoded successfully'); + } else { + console.error('❌ Unexpected function result format:', result); + return null; + } + } catch (funcError) { + console.error('❌ Function call error:', funcError); + console.error('Stack trace:', funcError.stack); + return null; + } + + // Cleanup memory + try { + if (wasm.__wbindgen_free && outPtr && outLen) { + wasm.__wbindgen_free(outPtr, outLen, 1); + console.log('✅ Output memory freed'); + } + if (wasm.__wbindgen_free && inPtr) { + wasm.__wbindgen_free(inPtr, bytes.length, 1); + console.log('✅ Input memory freed'); + } + } catch (cleanupError) { + console.log('⚠️ Cleanup warning:', cleanupError.message); + } + + // Display results + console.log(''); + console.log('🎉 SUCCESS!'); + console.log('📊 Results:'); + console.log(' Input coords: [1245984, 1088]'); + console.log(' Token length:', token?.length || 0); + console.log(' Token preview:', token?.substring(0, 50) + '...'); + console.log(''); + console.log('🔑 Full token:'); + console.log(token); + return token; + } catch (error) { + console.error('❌ Failed to generate fp parameter:', error); + return null; + } + return null; + } + + async function purchase(type) { + // loadThemePreference() + let id; + let chargeMultiplier; + if (type === "max_charges") { + id = 70; + chargeMultiplier = 5; + } else if (type === "paint_charges") { + id = 80; + chargeMultiplier = 30; + } else { + console.error("Error: Invalid purchase type provided."); + return; + } + const { droplets } = await WPlaceService.getCharges(); + console.log("There are currently : ", droplets, "droplets."); + try { + const amounts = Math.floor(droplets / 500); + if (amounts < 1) { + console.log("Not enough droplets to purchase."); + return; + } + const payload = { + "product": { + "id": id, + "amount": amounts + } + }; + const res = fetch("https://backend.wplace.live/purchase", { + method: "POST", + headers: { + "Content-Type": "text/plain;charset=UTF-8" + }, + body: JSON.stringify(payload), + credentials: "include" + }); + console.log("Fetch POST return :", res); + const { droplets: newDroplets } = await WPlaceService.getCharges(); + if (droplets != newDroplets) { + console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), "."); + } + else { + console.log("Failed to buy charges"); + } + } catch (e) { + console.error("An error occurred during the purchase:", e); + } + } + function swapAccountTrigger(token) { + localStorage.removeItem("lp"); + if (!token) return; + console.log("Sending token to extension..."); + window.postMessage({ + source: 'my-userscript', + type: 'setCookie', + value: token + }, '*'); + } + async function getAccounts() { + return new Promise((resolve, reject) => { + console.log("Requesting accounts from extension..."); + // Ask extension for accounts + window.postMessage({ + source: "my-userscript", + type: "getAccounts" + }, "*"); + function handler(event) { + if (event.source !== window) return; + if (event.data.source !== "extension") return; + if (event.data.type === "accountsData") { + window.removeEventListener("message", handler); + // Save to localStorage + try { + localStorage.setItem("accounts", JSON.stringify(event.data.accounts)); + console.log("✅ Accounts saved to localStorage:", event.data.accounts); + } catch (e) { + console.error("❌ Failed to save accounts:", e); + } + resolve(event.data.accounts); + } + } + window.addEventListener("message", handler); + }); + } + async function fetchAccount() { + const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck(); + console.log("User's ID :", ID); + console.log("User's Charges :", Charges, "/", Max); + console.log("User's Droplets :", Droplets); + } + createUI().then(() => { + // Generate token automatically after UI is ready + setTimeout(initializeTokenGenerator, 1000); + + // Attach advanced color matching listeners (resize dialog) + const advancedInit = () => { + const chromaSlider = document.getElementById('chromaPenaltyWeightSlider'); + const chromaValue = document.getElementById('chromaWeightValue'); + const resetBtn = document.getElementById('resetAdvancedColorBtn'); + const algoSelect = document.getElementById('colorAlgorithmSelect'); + const chromaToggle = document.getElementById('enableChromaPenaltyToggle'); + const transInput = document.getElementById('transparencyThresholdInput'); + const whiteInput = document.getElementById('whiteThresholdInput'); + const ditherToggle = document.getElementById('enableDitheringToggle'); + if (algoSelect) algoSelect.addEventListener('change', e => { state.colorMatchingAlgorithm = e.target.value; saveBotSettings(); _updateResizePreview(); }); + if (chromaToggle) chromaToggle.addEventListener('change', e => { state.enableChromaPenalty = e.target.checked; saveBotSettings(); _updateResizePreview(); }); + if (chromaSlider && chromaValue) chromaSlider.addEventListener('input', e => { state.chromaPenaltyWeight = parseFloat(e.target.value) || 0.15; chromaValue.textContent = state.chromaPenaltyWeight.toFixed(2); saveBotSettings(); _updateResizePreview(); }); + if (transInput) transInput.addEventListener('change', e => { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 0 && v <= 255) { state.customTransparencyThreshold = v; CONFIG.TRANSPARENCY_THRESHOLD = v; saveBotSettings(); _updateResizePreview(); } }); + if (whiteInput) whiteInput.addEventListener('change', e => { const v = parseInt(e.target.value, 10); if (!isNaN(v) && v >= 200 && v <= 255) { state.customWhiteThreshold = v; CONFIG.WHITE_THRESHOLD = v; saveBotSettings(); _updateResizePreview(); } }); + if (ditherToggle) ditherToggle.addEventListener('change', e => { state.ditheringEnabled = e.target.checked; saveBotSettings(); _updateResizePreview(); }); + if (resetBtn) resetBtn.addEventListener('click', () => { + state.colorMatchingAlgorithm = 'lab'; state.enableChromaPenalty = true; state.chromaPenaltyWeight = 0.15; state.customTransparencyThreshold = CONFIG.TRANSPARENCY_THRESHOLD = 100; state.customWhiteThreshold = CONFIG.WHITE_THRESHOLD = 250; saveBotSettings(); const a = document.getElementById('colorAlgorithmSelect'); if (a) a.value = 'lab'; const ct = document.getElementById('enableChromaPenaltyToggle'); if (ct) ct.checked = true; if (chromaSlider) chromaSlider.value = 0.15; if (chromaValue) chromaValue.textContent = '0.15'; if (transInput) transInput.value = 100; if (whiteInput) whiteInput.value = 250; _updateResizePreview(); Utils.showAlert('Advanced color settings reset.', 'success'); + }); + }; + // Delay to ensure resize UI built + setTimeout(advancedInit, 500); + + // Add cleanup on page unload + window.addEventListener('beforeunload', () => { + Utils.cleanupTurnstile(); + }); + }) +})() \ No newline at end of file diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index a379d53..7cef737 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -38,7 +38,7 @@ localStorage.removeItem("lp"); REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds }, OVERLAY: { - OPACITY_DEFAULT: 0.2, + OPACITY_DEFAULT: 0.6, BLUE_MARBLE_DEFAULT: false, ditheringEnabled: false, }, // --- START: Color data from colour-converter.js --- @@ -229,6 +229,7 @@ localStorage.removeItem("lp"); COORDINATE_SNAKE: true, COORDINATE_BLOCK_WIDTH: 6, COORDINATE_BLOCK_HEIGHT: 2, + autoSwap: true, }; const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme]; @@ -251,7 +252,7 @@ localStorage.removeItem("lp"); const theme = getCurrentTheme(); console.group('%c🎨 Applying Theme in Auto-Image Script', 'color: #8b5cf6; font-weight: bold;'); console.log(`%c🎯 Target theme: ${CONFIG.currentTheme}`, 'color: #8b5cf6;'); - + // Toggle theme class on documentElement so CSS vars cascade to our UI document.documentElement.classList.remove( 'wplace-theme-classic', @@ -262,7 +263,7 @@ localStorage.removeItem("lp"); let themeClass = 'wplace-theme-classic'; // default let themeFileName = 'classic'; // corresponding file name - + if (CONFIG.currentTheme === 'Neon Retro') { themeClass = 'wplace-theme-neon'; themeFileName = 'neon'; @@ -320,7 +321,7 @@ localStorage.removeItem("lp"); setVar('--wplace-border-width', '' + (theme.borderWidth || '1px')); setVar('--wplace-backdrop', '' + (theme.backdropFilter || 'blur(10px)')); setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); - + console.log(`%c🎨 Theme application complete`, 'color: #8b5cf6; font-weight: bold;'); console.groupEnd(); } @@ -377,11 +378,11 @@ localStorage.removeItem("lp"); // First try: Check if extension has loaded local resources if (window.AUTOBOT_LANGUAGES && Object.keys(window.AUTOBOT_LANGUAGES).length > 0) { console.log(`%c🔍 Checking extension local resources...`, 'color: #06b6d4;'); - + const langFile = language + '.json'; if (window.AUTOBOT_LANGUAGES[langFile]) { const translations = window.AUTOBOT_LANGUAGES[langFile]; - + // Validate that translations is an object with keys if ( typeof translations === 'object' && @@ -721,6 +722,7 @@ localStorage.removeItem("lp"); _lastSaveTime: 0, _saveInProgress: false, paintedMap: null, + accountIndex: -1, }; let _updateResizePreview = () => { }; @@ -2976,34 +2978,49 @@ localStorage.removeItem("lp"); }, async getCharges() { - const defaultResult = { - charges: 0, - max: 1, - cooldown: CONFIG.COOLDOWN_DEFAULT, - }; - try { - const res = await fetch('https://backend.wplace.live/me', { - credentials: 'include', - }); - - if (!res.ok) { - console.error(`Failed to get charges: HTTP ${res.status}`); - return defaultResult; - } - - const data = await res.json(); - + 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?.cooldownMs ?? CONFIG.COOLDOWN_DEFAULT, - }; + id: data.id, + charges: data.charges?.count || 0, + max: data.charges?.max || 1, + cooldown: data.charges?.next || CONFIG.COOLDOWN_DEFAULT, + droplets: data.droplets || 0, + } } catch (e) { - console.error('Failed to get charges:', e); - return defaultResult; + console.error("Failed to get charges:", e) + return { + id: null, + charges: 0, + max: 1, + cooldown: CONFIG.COOLDOWN_DEFAULT, + droplets: 0, + } } }, + + async fetchCheck() { + try { + const res = await fetch("https://backend.wplace.live/me", { + credentials: "include", + }) + const data = await res.json() + return { + ID: data.id, + Charges: data.charges.count, + Max: data.charges.max, + Droplets: data.droplets + } + } catch (e) { + console.error("Failed to get ID:", e) + return { + + } + } + } }; // Desktop Notification Manager @@ -3515,13 +3532,13 @@ localStorage.removeItem("lp"); // Load auto-image-styles.css - prioritize extension local resources console.group('%c🎨 Loading Auto-Image Styles', 'color: #8b5cf6; font-weight: bold;'); - + let stylesLoaded = false; - + // First try: Check if extension has loaded local CSS resources if (window.AUTOBOT_THEMES && window.AUTOBOT_THEMES['auto-image-styles.css']) { console.log('%c🔍 Found auto-image-styles.css in extension local resources!', 'color: #10b981; font-weight: bold;'); - + // Check if it's already injected by the extension const existingStyle = document.getElementById('autobot-auto-image-styles'); if (existingStyle) { @@ -3536,7 +3553,7 @@ localStorage.removeItem("lp"); styleElement.setAttribute('data-wplace-theme', 'true'); styleElement.textContent = window.AUTOBOT_THEMES['auto-image-styles.css']; document.head.appendChild(styleElement); - + console.log('%c✅ Injected auto-image-styles.css from extension local resources', 'color: #10b981; font-weight: bold;'); console.log(' 📍 Source: Extension local file'); console.log(' 📏 Size: ' + window.AUTOBOT_THEMES['auto-image-styles.css'].length + ' characters'); @@ -3550,7 +3567,7 @@ localStorage.removeItem("lp"); console.log(' 📋 Available themes:', Object.keys(window.AUTOBOT_THEMES)); } } - + // Fallback: Load from CDN if local resources not available if (!stylesLoaded) { console.log('%c🌐 Falling back to CDN loading...', 'color: #8b5cf6;'); @@ -3563,13 +3580,13 @@ localStorage.removeItem("lp"); console.log(' 🌐 URL: https://wplace-autobot.github.io/WPlace-AutoBOT/main/auto-image-styles.css'); console.log(' ⚠️ Performance: Network request required'); } - + console.groupEnd(); // Ensure default theme is loaded from extension resources if (window.applyTheme && typeof window.applyTheme === 'function') { console.group('%c🎨 Loading Default Theme from Extension', 'color: #8b5cf6; font-weight: bold;'); - + // Determine the current theme file name let defaultTheme = 'classic'; // fallback if (CONFIG.currentTheme === 'Neon Retro') { @@ -3579,10 +3596,10 @@ localStorage.removeItem("lp"); } else if (CONFIG.currentTheme === 'Acrylic') { defaultTheme = 'acrylic'; } - + console.log(`%c🎯 Loading theme: ${defaultTheme} (${CONFIG.currentTheme})`, 'color: #8b5cf6;'); const success = window.applyTheme(defaultTheme); - + if (success) { console.log(`%c✅ Default theme loaded from extension local resources`, 'color: #10b981; font-weight: bold;'); console.log(` 📍 Source: Extension local file (themes/${defaultTheme}.css)`); @@ -3592,7 +3609,7 @@ localStorage.removeItem("lp"); console.warn(`%c⚠️ Failed to load theme ${defaultTheme} from extension`, 'color: #f59e0b;'); console.log(` 📝 Note: Theme CSS classes will still work`); } - + console.groupEnd(); } else { console.warn(`%c⚠️ Extension applyTheme() function not available`, 'color: #f59e0b; font-weight: bold;'); @@ -6834,6 +6851,7 @@ localStorage.removeItem("lp"); updateUI('startPaintingMsg', 'success'); try { + await getAccounts(); await processImage(); } catch (e) { console.error('Unexpected error:', e); @@ -7422,20 +7440,79 @@ localStorage.removeItem("lp"); pixelBatch.pixels = []; } + if (!CONFIG.autoSwap) { + if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { + await Utils.dynamicSleep(() => { + if (state.displayCharges >= state.cooldownChargeThreshold) { + NotificationManager.maybeNotifyChargesReached(true); + return 0; + } + if (state.stopFlag) return 0; + return getMsToTargetCharges( + state.preciseCurrentCharges, + state.cooldownChargeThreshold, + state.cooldown + ); + }); + } + } + else { + if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { + console.log("⚠️ Charges too low, swapping to next account..."); - if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { - await Utils.dynamicSleep(() => { - if (state.displayCharges >= state.cooldownChargeThreshold) { - NotificationManager.maybeNotifyChargesReached(true); - return 0; + const accounts = JSON.parse(localStorage.getItem("accounts")) || []; + if (accounts.length === 0) { + console.warn("❌ No accounts available, stopping painting."); + state.stopFlag = true; + return; } - if (state.stopFlag) return 0; - return getMsToTargetCharges( - state.preciseCurrentCharges, - state.cooldownChargeThreshold, - state.cooldown - ); - }); + + state.accountIndex = (state.accountIndex + 1) % accounts.length; + console.log("🔄 Switching to account index:", state.accountIndex); + + const nextToken = accounts[state.accountIndex]; + console.log("🔑 Next token:", nextToken); + + if (!nextToken) { + console.warn("⚠️ Invalid token, skipping..."); + return; + } + + swapAccountTrigger(nextToken); + + let maxRetries = 20; + let retryCount = 0; + let swapSuccess = false; + + while (!swapSuccess && retryCount < maxRetries) { + console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); + + // Wait for a short period before checking. + await new Promise(resolve => setTimeout(resolve, 1000)); + + try { + await fetchAccount(); + + console.log("✅ Account swap confirmed."); + swapSuccess = true; + } catch (error) { + console.warn("❌ Account swap not yet successful. Retrying...", error); + retryCount++; + } + } + + if (swapSuccess) { + + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.cooldown = cooldown; + Utils.performSmartSave(); + updateStats(); + } else { + console.error("❌ Failed to swap account after multiple retries. Stopping loop."); + state.stopFlag = true; + } + } } if (state.stopFlag) { @@ -7998,7 +8075,7 @@ localStorage.removeItem("lp"); async function createWasmToken(regionX, regionY, payload) { try { // Load the Pawtect module and WASM - const mod = await import(new URL('/_app/immutable/chunks/'+pawtect_chunk, location.origin).href); + const mod = await import(new URL('/_app/immutable/chunks/' + pawtect_chunk, location.origin).href); let wasm; try { wasm = await mod._(); @@ -8130,6 +8207,97 @@ localStorage.removeItem("lp"); console.error(`❌ Could not find Pawtect chunk: `, error); } + async function purchase(type) { + // loadThemePreference() + let id; + let chargeMultiplier; + if (type === "max_charges") { + id = 70; + chargeMultiplier = 5; + } else if (type === "paint_charges") { + id = 80; + chargeMultiplier = 30; + } else { + console.error("Error: Invalid purchase type provided."); + return; + } + const { droplets } = await WPlaceService.getCharges(); + console.log("There are currently : ", droplets, "droplets."); + try { + const amounts = Math.floor(droplets / 500); + if (amounts < 1) { + console.log("Not enough droplets to purchase."); + return; + } + const payload = { + "product": { + "id": id, + "amount": amounts + } + }; + const res = fetch("https://backend.wplace.live/purchase", { + method: "POST", + headers: { + "Content-Type": "text/plain;charset=UTF-8" + }, + body: JSON.stringify(payload), + credentials: "include" + }); + console.log("Fetch POST return :", res); + const { droplets: newDroplets } = await WPlaceService.getCharges(); + if (droplets != newDroplets) { + console.log("Successfully bought", amounts * chargeMultiplier, type.replace('_', ' '), "."); + } + else { + console.log("Failed to buy charges"); + } + } catch (e) { + console.error("An error occurred during the purchase:", e); + } + } + function swapAccountTrigger(token) { + localStorage.removeItem("lp"); + if (!token) return; + console.log("Sending token to extension..."); + window.postMessage({ + source: 'my-userscript', + type: 'setCookie', + value: token + }, '*'); + } + async function getAccounts() { + return new Promise((resolve, reject) => { + console.log("Requesting accounts from extension..."); + // Ask extension for accounts + window.postMessage({ + source: "my-userscript", + type: "getAccounts" + }, "*"); + function handler(event) { + if (event.source !== window) return; + if (event.data.source !== "extension") return; + if (event.data.type === "accountsData") { + window.removeEventListener("message", handler); + // Save to localStorage + try { + localStorage.setItem("accounts", JSON.stringify(event.data.accounts)); + console.log("✅ Accounts saved to localStorage:", event.data.accounts); + } catch (e) { + console.error("❌ Failed to save accounts:", e); + } + resolve(event.data.accounts); + } + } + window.addEventListener("message", handler); + }); + } + async function fetchAccount() { + const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck(); + console.log("User's ID :", ID); + console.log("User's Charges :", Charges, "/", Max); + console.log("User's Droplets :", Droplets); + } + createUI().then(() => { // Generate token automatically after UI is ready setTimeout(initializeTokenGenerator, 1000); diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js index bbcbd92..7cd9677 100644 --- a/Extension/scripts/Auto-Repair.js +++ b/Extension/scripts/Auto-Repair.js @@ -17,6 +17,11 @@ TOKEN_PRELOAD_BUFFER: 60000, // Preload tokens 1 minute before expiry MAX_RETRIES: 10, RETRY_DELAY_BASE: 1000, + // Auto-batch configuration + AUTO_BATCH_ENABLED: true, // Enable auto pixel batch sizing + MAX_BATCH_SIZE: 500, // Maximum pixels per batch + MIN_BATCH_SIZE: 1, // Minimum pixels per batch + BATCH_OPTIMIZATION: true, // Optimize batch size based on detected damage COLOR_MAP: { 0: { id: 1, name: 'Black', rgb: { r: 0, g: 0, b: 0 } }, 1: { id: 2, name: 'Dark Gray', rgb: { r: 60, g: 60, b: 60 } }, @@ -100,7 +105,7 @@ startPosition: null, region: null, paintWhitePixels: true, - paintTransparentPixels: false, + paintTransparentPixels: true, // Changed to true to fix transparent pixel detection autoRepairEnabled: false, autoRepairInterval: 30, autoRepairTimer: null, @@ -117,6 +122,10 @@ windowMinimized: false, lastAttackState: null, initialSetupComplete: false, + // Auto-batch state + autoBatchEnabled: CONFIG.AUTO_BATCH_ENABLED, + currentBatchSize: CONFIG.MIN_BATCH_SIZE, + batchOptimization: CONFIG.BATCH_OPTIMIZATION, }; // Random string generator @@ -125,7 +134,7 @@ const fpStr32 = randStr(32); - // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt + // Enhanced Turnstile token handling - Actualizado con la lógica de new.txt lol let turnstileToken = null; let tokenExpiryTime = 0; let tokenGenerationInProgress = false; @@ -1352,9 +1361,7 @@ const d = cached.data; const a = d[idx + 3]; - if (!state.paintTransparentPixels && a < alphaThresh) { - return null; - } + // Always return pixel data, let caller decide about transparency return [d[idx], d[idx + 1], d[idx + 2], a]; } @@ -1385,10 +1392,7 @@ const data = ctx.getImageData(x, y, 1, 1).data; const a = data[3]; - if (!state.paintTransparentPixels && a < alphaThresh) { - return null; - } - + // Always return pixel data, let caller decide about transparency return [data[0], data[1], data[2], a]; } catch (e) { Utils.addDebugLog(`Error reading pixel from tile ${tileKey}: ${e.message}`, 'error'); @@ -1457,7 +1461,7 @@ const overlayManager = new OverlayManager(); - // Enhanced WPlace API Service with autonomous capabilities - ACTUALIZADO CON NUEVA LÓGICA + // Enhanced WPlace API Service with auto-batch functionality const WPlaceService = { async paintPixelInRegion(regionX, regionY, pixelX, pixelY, color, retryCount = 0) { try { @@ -1524,6 +1528,83 @@ } }, + // NEW: Auto-batch pixel painting functionality + async paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount = 0) { + try { + await ensureToken(); + if (!turnstileToken) { + Utils.addDebugLog('No valid token available for batch paint request', 'error'); + return 'token_error'; + } + + // Prepare batch coordinates and colors + const coords = []; + const colors = []; + + for (const pixel of pixelBatch) { + coords.push(pixel.pixelX, pixel.pixelY); + colors.push(pixel.color); + } + + const payload = { + coords: coords, + colors: colors, + t: turnstileToken, + fp: fpStr32, + }; + + const wasmToken = await createWasmToken(regionX, regionY, payload); + if (!wasmToken) { + Utils.addDebugLog('Failed to generate WASM token for batch', 'error'); + return false; + } + + const res = await fetch(`https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain;charset=UTF-8', + 'x-pawtect-token': wasmToken + }, + credentials: 'include', + body: JSON.stringify(payload), + }); + + if (res.status === 403) { + Utils.addDebugLog(`403 Forbidden for batch (${pixelBatch.length} pixels). Token invalid/expired.`, 'error'); + invalidateToken(); + + if (retryCount < 2 && state.autonomousMode) { + Utils.addDebugLog(`Autonomous batch retry ${retryCount + 1}/2 for ${pixelBatch.length} pixels`, 'warning'); + await Utils.sleep(1000); + return await this.paintPixelBatchInRegion(regionX, regionY, pixelBatch, retryCount + 1); + } + + return 'token_error'; + } + + if (!res.ok) { + Utils.addDebugLog(`Batch paint request failed with status ${res.status}`, 'error'); + return false; + } + + const data = await res.json(); + const successCount = data?.painted || 0; + + Utils.addDebugLog(`Batch paint result: ${successCount}/${pixelBatch.length} pixels painted successfully`, + successCount === pixelBatch.length ? 'success' : 'warning'); + + return { + success: successCount > 0, + painted: successCount, + total: pixelBatch.length + }; + + } catch (e) { + Utils.addDebugLog(`Batch paint request error for ${pixelBatch.length} pixels: ${e.message}`, 'error'); + return false; + } + }, + async getCharges() { try { const res = await fetch('https://backend.wplace.live/me', { @@ -1543,14 +1624,14 @@ }, }; - // Anti-grief repair system with autonomous capabilities + // Enhanced anti-grief repair system with improved transparent pixel detection async function scanForDamage() { if (!state.imageData || !state.startPosition || !state.region) { Utils.addDebugLog('No image data or position for scanning', 'warning'); return []; } - Utils.addDebugLog('Starting autonomous damage scan...', 'info'); + Utils.addDebugLog('Starting enhanced damage scan with transparent pixel detection...', 'info'); updateStatus(Utils.t('scanningForDamage')); const damagedPixels = []; @@ -1579,9 +1660,11 @@ return []; } - Utils.addDebugLog(`Scanning ${width}x${height} image for damage (autonomous: ${state.autonomousMode})...`, 'info'); + Utils.addDebugLog(`Scanning ${width}x${height} image for damage (transparent detection: ${state.paintTransparentPixels})...`, 'info'); let scannedPixels = 0; + let transparentPixelsDetected = 0; + let wrongColorPixelsDetected = 0; let lastProgressUpdate = Date.now(); for (let y = 0; y < height; y++) { @@ -1594,10 +1677,10 @@ const originalB = pixels[idx + 2]; const originalA = pixels[idx + 3]; - if (!state.paintTransparentPixels && originalA < CONFIG.TRANSPARENCY_THRESHOLD) { - continue; - } - + // Enhanced transparent pixel detection + const isOriginalTransparent = originalA < state.customTransparencyThreshold; + + // Skip white pixels if configured if (!state.paintWhitePixels && Utils.isWhitePixel(originalR, originalG, originalB)) { continue; } @@ -1614,30 +1697,93 @@ try { const currentPixel = await overlayManager.getTilePixelColor(tileX, tileY, pixelX, pixelY); + // Handle missing current pixel data (could indicate transparent area) if (!currentPixel) { - if (!state.autonomousMode) { - Utils.addDebugLog(`No current pixel data for (${x},${y}) - tile ${tileX},${tileY}`, 'warning'); + if (state.paintTransparentPixels && !isOriginalTransparent) { + // Original pixel is not transparent but current is missing/transparent + const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors); + damagedPixels.push({ + x, + y, + originalColor: targetColor, + currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] }, // Transparent color + originalRgb: [originalR, originalG, originalB], + currentRgb: [0, 0, 0], + isDamagedTransparent: true + }); + transparentPixelsDetected++; + + if (!state.autonomousMode || transparentPixelsDetected <= 10) { + Utils.addDebugLog(`Missing/Transparent pixel at (${x},${y}): expected color ${targetColor.id}, found transparent/missing`, 'warning'); + } } continue; } - const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors); - const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors); + const currentIsTransparent = currentPixel[3] < state.customTransparencyThreshold; - if (targetColor.id !== currentColor.id) { + // Case 1: Original is not transparent, but current is transparent (damaged) + if (!isOriginalTransparent && currentIsTransparent && state.paintTransparentPixels) { + const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors); damagedPixels.push({ x, y, originalColor: targetColor, - currentColor: currentColor, + currentColor: { id: 63, name: 'Transparent', rgb: [0, 0, 0] }, originalRgb: [originalR, originalG, originalB], - currentRgb: currentPixel.slice(0, 3) + currentRgb: currentPixel.slice(0, 3), + isDamagedTransparent: true }); + transparentPixelsDetected++; + + if (!state.autonomousMode || transparentPixelsDetected <= 10) { + Utils.addDebugLog(`Transparent damage at (${x},${y}): expected color ${targetColor.id}, found transparent`, 'warning'); + } + continue; + } - if (!state.autonomousMode || damagedPixels.length <= 10) { - Utils.addDebugLog(`Damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning'); + // Case 2: Original is transparent, current is not transparent (wrong placement) + if (isOriginalTransparent && !currentIsTransparent && state.paintTransparentPixels) { + damagedPixels.push({ + x, + y, + originalColor: { id: 63, name: 'Transparent', rgb: null }, + currentColor: Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors), + originalRgb: [originalR, originalG, originalB], + currentRgb: currentPixel.slice(0, 3), + isDamagedTransparent: true + }); + transparentPixelsDetected++; + + if (!state.autonomousMode || transparentPixelsDetected <= 10) { + Utils.addDebugLog(`Wrong placement at (${x},${y}): expected transparent, found color`, 'warning'); + } + continue; + } + + // Case 3: Both are not transparent, check color match (normal damage detection) + if (!isOriginalTransparent && !currentIsTransparent) { + const targetColor = Utils.resolveColor([originalR, originalG, originalB], state.availableColors); + const currentColor = Utils.resolveColor(currentPixel.slice(0, 3), state.availableColors); + + if (targetColor.id !== currentColor.id) { + damagedPixels.push({ + x, + y, + originalColor: targetColor, + currentColor: currentColor, + originalRgb: [originalR, originalG, originalB], + currentRgb: currentPixel.slice(0, 3), + isDamagedTransparent: false + }); + wrongColorPixelsDetected++; + + if (!state.autonomousMode || wrongColorPixelsDetected <= 10) { + Utils.addDebugLog(`Color damage at (${x},${y}): expected color ${targetColor.id}, found color ${currentColor.id}`, 'warning'); + } } } + } catch (e) { if (!state.autonomousMode) { Utils.addDebugLog(`Error checking pixel (${x},${y}): ${e.message}`, 'error'); @@ -1646,7 +1792,7 @@ } if (state.autonomousMode && Date.now() - lastProgressUpdate > 5000) { - Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged`, 'info'); + Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%), found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent, ${wrongColorPixelsDetected} wrong color)`, 'info'); lastProgressUpdate = Date.now(); } else if (!state.autonomousMode && y % 10 === 0) { Utils.addDebugLog(`Scan progress: ${y}/${height} rows (${Math.round((y / height) * 100)}%)`, 'info'); @@ -1654,7 +1800,17 @@ } const logLevel = damagedPixels.length > 0 ? 'warning' : 'success'; - Utils.addDebugLog(`Scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged`, logLevel); + Utils.addDebugLog(`Enhanced scan complete. Checked ${scannedPixels} pixels, found ${damagedPixels.length} damaged (${transparentPixelsDetected} transparent issues, ${wrongColorPixelsDetected} wrong colors)`, logLevel); + + // Smart auto-adjust batch size: if 6 pixels detected, batch size = 6 + if (state.autoBatchEnabled && state.batchOptimization && damagedPixels.length > 0) { + const newBatchSize = Math.min(CONFIG.MAX_BATCH_SIZE, Math.max(CONFIG.MIN_BATCH_SIZE, damagedPixels.length)); + if (newBatchSize !== state.currentBatchSize) { + state.currentBatchSize = newBatchSize; + Utils.addDebugLog(`Smart batch: Auto-adjusted batch size to ${newBatchSize} to match ${damagedPixels.length} detected damaged pixels`, 'info'); + updateBatchInfo(); // Update UI display + } + } return damagedPixels; } @@ -1665,98 +1821,174 @@ return 0; } - Utils.addDebugLog(`Starting autonomous repair of ${damagedPixels.length} pixels`, 'info'); + Utils.addDebugLog(`Starting enhanced repair of ${damagedPixels.length} pixels with auto-batch (batch size: ${state.currentBatchSize})`, 'info'); updateStatus(Utils.t('repairingPixels', { count: damagedPixels.length })); let repairedCount = 0; let consecutiveFailures = 0; const maxConsecutiveFailures = state.autonomousMode ? 5 : 3; - for (let i = 0; i < damagedPixels.length; i++) { - const pixel = damagedPixels[i]; + // Group pixels by region for batch processing + const pixelsByRegion = new Map(); + + for (const pixel of damagedPixels) { + const absX = state.startPosition.x + pixel.x; + const absY = state.startPosition.y + pixel.y; + const regionX = state.region.x + Math.floor(absX / 1000); + const regionY = state.region.y + Math.floor(absY / 1000); + const regionKey = `${regionX},${regionY}`; + if (!pixelsByRegion.has(regionKey)) { + pixelsByRegion.set(regionKey, []); + } + + pixelsByRegion.get(regionKey).push({ + ...pixel, + regionX, + regionY, + pixelX: absX % 1000, + pixelY: absY % 1000, + color: pixel.isDamagedTransparent && pixel.originalColor.id === 63 ? 63 : pixel.originalColor.id + }); + } + + Utils.addDebugLog(`Grouped ${damagedPixels.length} pixels into ${pixelsByRegion.size} regions for batch processing`, 'info'); + + // Process each region + for (const [regionKey, regionPixels] of pixelsByRegion) { if (state.stopFlag) { Utils.addDebugLog('Repair stopped by user request', 'warning'); break; } - // Enhanced charge management for autonomous mode - await updateCharges(); - let chargeWaitAttempts = 0; - const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10; - - while (state.displayCharges < 1 && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) { - chargeWaitAttempts++; - const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown; + const [regionX, regionY] = regionKey.split(',').map(Number); + Utils.addDebugLog(`Processing region ${regionKey} with ${regionPixels.length} pixels`, 'info'); + + // Process pixels in batches + for (let i = 0; i < regionPixels.length; i += state.currentBatchSize) { + if (state.stopFlag) break; + + const batch = regionPixels.slice(i, i + state.currentBatchSize); + const actualBatchSize = Math.min(batch.length, state.currentBatchSize); - if (chargeWaitAttempts === 1) { - Utils.addDebugLog(`Waiting for charges... (${state.displayCharges}/${state.maxCharges})`, 'info'); - } - - await Utils.dynamicSleep(() => { - if (state.displayCharges >= 1) return 0; - if (state.stopFlag) return 0; - return waitTime; - }); + // Enhanced charge management for batch processing await updateCharges(); - } - - if (state.stopFlag) break; - - if (state.displayCharges < 1) { - Utils.addDebugLog(`No charges available after waiting, skipping pixel (${pixel.x},${pixel.y})`, 'warning'); - continue; - } - - const success = await repairSinglePixel(pixel); - if (success) { - repairedCount++; - consecutiveFailures = 0; - if (!state.autonomousMode || repairedCount <= 10 || repairedCount % 10 === 0) { - Utils.addDebugLog(`Repaired pixel (${pixel.x},${pixel.y}) with color ${pixel.originalColor.id} [${repairedCount}/${damagedPixels.length}]`, 'success'); - } - } else { - consecutiveFailures++; - Utils.addDebugLog(`Failed to repair pixel (${pixel.x},${pixel.y}) [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error'); + let chargeWaitAttempts = 0; + const maxChargeWaitAttempts = state.autonomousMode ? 20 : 10; - if (consecutiveFailures >= maxConsecutiveFailures) { - Utils.addDebugLog(`Too many consecutive failures (${consecutiveFailures}), pausing repair`, 'error'); - if (state.autonomousMode) { - Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning'); - setTimeout(() => { - if (state.autoRepairEnabled && !state.stopFlag) { - performRepairCheck(); - } - }, 60000); + while (state.displayCharges < actualBatchSize && !state.stopFlag && chargeWaitAttempts < maxChargeWaitAttempts) { + chargeWaitAttempts++; + const waitTime = state.autonomousMode ? Math.min(state.cooldown, 10000) : state.cooldown; + + if (chargeWaitAttempts === 1) { + Utils.addDebugLog(`Waiting for charges... (need ${actualBatchSize}, have ${state.displayCharges}/${state.maxCharges})`, 'info'); } - break; + + await Utils.dynamicSleep(() => { + if (state.displayCharges >= actualBatchSize) return 0; + if (state.stopFlag) return 0; + return waitTime; + }); + await updateCharges(); } - } - await updateCharges(); + if (state.stopFlag) break; + + if (state.displayCharges < actualBatchSize) { + Utils.addDebugLog(`Insufficient charges for batch (need ${actualBatchSize}, have ${state.displayCharges}), falling back to single pixel repair`, 'warning'); + + // Fall back to single pixel repair + for (const pixel of batch) { + if (state.stopFlag) break; + + await updateCharges(); + if (state.displayCharges < 1) { + Utils.addDebugLog(`No charges available, skipping pixel (${pixel.x},${pixel.y})`, 'warning'); + continue; + } + + const success = await repairSinglePixel(pixel); + if (success) { + repairedCount++; + consecutiveFailures = 0; + } else { + consecutiveFailures++; + } + + if (consecutiveFailures >= maxConsecutiveFailures) break; + await Utils.sleep(100); + } + } else { + // Use batch repair + const batchResult = await repairPixelBatch(regionX, regionY, batch); + + if (batchResult && batchResult.success) { + repairedCount += batchResult.painted; + consecutiveFailures = 0; + + Utils.addDebugLog(`Batch repair: ${batchResult.painted}/${batchResult.total} pixels repaired successfully [Total: ${repairedCount}/${damagedPixels.length}]`, 'success'); + } else { + consecutiveFailures++; + Utils.addDebugLog(`Batch repair failed for ${batch.length} pixels [${consecutiveFailures}/${maxConsecutiveFailures} consecutive failures]`, 'error'); + + if (consecutiveFailures >= maxConsecutiveFailures) { + Utils.addDebugLog(`Too many consecutive batch failures (${consecutiveFailures}), pausing repair`, 'error'); + if (state.autonomousMode) { + Utils.addDebugLog('Autonomous mode: will retry repair in 60 seconds', 'warning'); + setTimeout(() => { + if (state.autoRepairEnabled && !state.stopFlag) { + performRepairCheck(); + } + }, 60000); + } + break; + } + } + } + + await updateCharges(); + + // Dynamic delay based on mode and success rate + const baseDelay = state.autonomousMode ? 100 : 200; + const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay; + await Utils.sleep(adaptiveDelay); + } - // Dynamic delay based on mode and success rate - const baseDelay = state.autonomousMode ? 100 : 200; - const adaptiveDelay = consecutiveFailures > 0 ? baseDelay * (consecutiveFailures + 1) : baseDelay; - await Utils.sleep(adaptiveDelay); + if (consecutiveFailures >= maxConsecutiveFailures) break; } const message = Utils.t('repairComplete', { repaired: repairedCount }); updateStatus(message); - Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining)`, 'success'); + Utils.addDebugLog(`${message} (${damagedPixels.length - repairedCount} remaining) - Used auto-batch with size ${state.currentBatchSize}`, 'success'); return repairedCount; } - async function repairSinglePixel(pixel) { - const { x, y, originalColor } = pixel; + async function repairPixelBatch(regionX, regionY, pixelBatch) { + try { + Utils.addDebugLog(`Attempting batch repair of ${pixelBatch.length} pixels in region ${regionX},${regionY}`, 'info'); + + const result = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch); - const absX = state.startPosition.x + x; - const absY = state.startPosition.y + y; - const regionX = state.region.x + Math.floor(absX / 1000); - const regionY = state.region.y + Math.floor(absY / 1000); - const pixelX = absX % 1000; - const pixelY = absY % 1000; + if (result === 'token_error') { + Utils.addDebugLog('Token error during batch repair, refreshing token...', 'warning'); + await ensureToken(true); + await Utils.sleep(state.autonomousMode ? 500 : 1000); + + // Retry once with new token + const retryResult = await WPlaceService.paintPixelBatchInRegion(regionX, regionY, pixelBatch); + return retryResult; + } + + return result; + } catch (e) { + Utils.addDebugLog(`Error during batch repair: ${e.message}`, 'error'); + return false; + } + } + + async function repairSinglePixel(pixel) { + const { x, y, originalColor, regionX, regionY, pixelX, pixelY, color } = pixel; try { const result = await WPlaceService.paintPixelInRegion( @@ -1764,7 +1996,7 @@ regionY, pixelX, pixelY, - originalColor.id + color || originalColor.id ); if (result === 'token_error') { @@ -1773,10 +2005,16 @@ await Utils.sleep(state.autonomousMode ? 500 : 1000); // Retry once with new token - const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, originalColor.id); + const retryResult = await WPlaceService.paintPixelInRegion(regionX, regionY, pixelX, pixelY, color || originalColor.id); return retryResult === true; } + if (result === true) { + if (pixel.isDamagedTransparent) { + Utils.addDebugLog(`Repaired transparent pixel (${x},${y}) with color ${color || originalColor.id}`, 'success'); + } + } + return result === true; } catch (e) { Utils.addDebugLog(`Error repairing pixel (${x},${y}): ${e.message}`, 'error'); @@ -1993,6 +2231,42 @@ } } + // Update batch information display for new UI + function updateBatchInfo() { + const batchSizeEl = document.getElementById('batchSize'); + const batchStatusEl = document.getElementById('batchStatus'); + const smartModeEl = document.getElementById('smartBatchMode'); + const manualModeEl = document.getElementById('manualBatchMode'); + const manualControlsEl = document.getElementById('manualBatchControls'); + + // Update radio button states + if (smartModeEl && manualModeEl) { + smartModeEl.checked = state.autoBatchEnabled && state.batchOptimization; + manualModeEl.checked = !state.autoBatchEnabled || !state.batchOptimization; + } + + // Update manual controls visibility + if (manualControlsEl) { + manualControlsEl.style.display = (!state.autoBatchEnabled || !state.batchOptimization) ? 'flex' : 'none'; + } + + // Update batch size input + if (batchSizeEl) { + batchSizeEl.value = state.currentBatchSize; + } + + // Update status display + if (batchStatusEl) { + if (state.autoBatchEnabled && state.batchOptimization) { + batchStatusEl.textContent = `🧠 Smart Batch: AUTO (Current: ${state.currentBatchSize})`; + batchStatusEl.className = 'status-info'; + } else { + batchStatusEl.textContent = `✋ Manual Batch: ${state.currentBatchSize} pixels fixed`; + batchStatusEl.className = 'status-warning'; + } + } + } + function updateSystemStatus() { const systemEl = document.getElementById('systemInfo'); if (systemEl) { @@ -2182,6 +2456,149 @@ margin-right: 8px !important; } + /* Enhanced Batch Control Styles */ + .batch-control-group { + display: flex !important; + align-items: center !important; + gap: 8px !important; + margin: 5px 0 !important; + padding: 8px !important; + background: rgba(0, 255, 65, 0.05) !important; + border: 1px solid rgba(0, 255, 65, 0.3) !important; + border-radius: 3px !important; + } + + .batch-control-header { + text-align: center !important; + margin-bottom: 10px !important; + padding: 5px !important; + border-bottom: 1px solid rgba(0, 255, 65, 0.3) !important; + } + + .batch-control-label { + color: #00ff41 !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 6px !important; + text-shadow: 0 0 3px #00ff41 !important; + white-space: nowrap !important; + } + + .batch-mode-selection { + margin: 10px 0 !important; + } + + .batch-mode-option { + margin: 8px 0 !important; + padding: 6px !important; + background: rgba(0, 255, 65, 0.03) !important; + border: 1px solid rgba(0, 255, 65, 0.2) !important; + border-radius: 3px !important; + transition: all 0.3s ease !important; + } + + .batch-mode-option:hover { + background: rgba(0, 255, 65, 0.08) !important; + border-color: rgba(0, 255, 65, 0.4) !important; + } + + .batch-mode-label { + color: #00ff41 !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 6px !important; + text-shadow: 0 0 3px #00ff41 !important; + margin-left: 8px !important; + } + + .neon-radio { + accent-color: #00ff41 !important; + transform: scale(1.2) !important; + margin-right: 8px !important; + } + + .batch-manual-controls { + display: flex !important; + align-items: center !important; + gap: 8px !important; + margin: 8px 0 !important; + padding: 8px !important; + background: rgba(0, 255, 65, 0.05) !important; + border: 1px solid rgba(0, 255, 65, 0.3) !important; + border-radius: 3px !important; + } + + .batch-size-control { + display: flex !important; + align-items: center !important; + gap: 2px !important; + border: 1px solid #00ff41 !important; + border-radius: 3px !important; + background: #16213e !important; + overflow: hidden !important; + } + + .batch-adjust-btn { + width: 20px !important; + height: 24px !important; + background: #16213e !important; + border: none !important; + color: #00ff41 !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 8px !important; + cursor: pointer !important; + transition: all 0.3s ease !important; + text-shadow: 0 0 3px #00ff41 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + } + + .batch-adjust-btn:hover { + background: rgba(0, 255, 65, 0.1) !important; + box-shadow: 0 0 5px rgba(0, 255, 65, 0.3) !important; + } + + .batch-adjust-btn:active { + background: rgba(0, 255, 65, 0.2) !important; + transform: scale(0.95) !important; + } + + .batch-size-input { + width: 50px !important; + height: 24px !important; + background: #16213e !important; + border: none !important; + color: #00ff41 !important; + padding: 0 4px !important; + font-family: 'Press Start 2P', monospace !important; + font-size: 6px !important; + text-align: center !important; + text-shadow: 0 0 3px #00ff41 !important; + outline: none !important; + /* Hide browser default spinner buttons */ + -webkit-appearance: none !important; + -moz-appearance: textfield !important; + } + + /* Hide Chrome spinner buttons */ + .batch-size-input::-webkit-outer-spin-button, + .batch-size-input::-webkit-inner-spin-button { + -webkit-appearance: none !important; + margin: 0 !important; + } + + /* Hide Firefox spinner buttons */ + .batch-size-input[type=number] { + -moz-appearance: textfield !important; + } + + .status-info { + color: #00ff41 !important; + } + + .status-warning { + color: #ff6b35 !important; + } + .neon-panel { background: rgba(22, 33, 62, 0.3) !important; border: 1px solid #00ff41 !important; @@ -2312,6 +2729,9 @@
System: AUTONOMOUS | Token: GENERATOR | Auto-refresh: ON
+
+ Auto-Batch: ON | Size: 5 | Opt: ON +
@@ -2354,11 +2774,54 @@
+ + +
+
+ 🎯 Pixel Batch Mode +
+ + +
+
+ +
+ +
+ +
+
+ + +
+ Batch Size: +
+ + + +
+ pixels +
+
+ +
+
Image: Not loaded
Position: Not set
Colors: 0 available
+
Batch Mode: Auto (${state.currentBatchSize} pixels)
@@ -2414,6 +2877,9 @@ updateSystemStatus(); setInterval(updateSystemStatus, 15000); + // Update batch information + updateBatchInfo(); + // Initialize token generator initializeTokenGenerator(); } @@ -2648,6 +3114,69 @@ } }); + // NEW: Enhanced batch mode controls + document.getElementById('smartBatchMode').addEventListener('change', (e) => { + if (e.target.checked) { + state.autoBatchEnabled = true; + state.batchOptimization = true; + document.getElementById('manualBatchControls').style.display = 'none'; + Utils.addDebugLog('Smart batch mode enabled - will auto-adjust to damage count', 'info'); + updateBatchInfo(); + } + }); + + document.getElementById('manualBatchMode').addEventListener('change', (e) => { + if (e.target.checked) { + state.autoBatchEnabled = false; + state.batchOptimization = false; + document.getElementById('manualBatchControls').style.display = 'flex'; + Utils.addDebugLog('Manual batch mode enabled - using fixed batch size', 'info'); + updateBatchInfo(); + } + }); + + // Manual batch size controls with +/- buttons + document.getElementById('batchSizeIncrease').addEventListener('click', () => { + const newSize = Math.min(CONFIG.MAX_BATCH_SIZE, state.currentBatchSize + 1); + if (newSize !== state.currentBatchSize) { + state.currentBatchSize = newSize; + document.getElementById('batchSize').value = state.currentBatchSize; + Utils.addDebugLog(`Increased batch size to ${state.currentBatchSize} pixels`, 'info'); + updateBatchInfo(); + } + }); + + document.getElementById('batchSizeDecrease').addEventListener('click', () => { + const newSize = Math.max(CONFIG.MIN_BATCH_SIZE, state.currentBatchSize - 1); + if (newSize !== state.currentBatchSize) { + state.currentBatchSize = newSize; + document.getElementById('batchSize').value = state.currentBatchSize; + Utils.addDebugLog(`Decreased batch size to ${state.currentBatchSize} pixels`, 'info'); + updateBatchInfo(); + } + }); + + // Manual input allows direct typing for precise control + document.getElementById('batchSize').addEventListener('input', (e) => { + const value = parseInt(e.target.value); + if (!isNaN(value)) { + const clampedValue = Math.max(CONFIG.MIN_BATCH_SIZE, Math.min(CONFIG.MAX_BATCH_SIZE, value)); + if (clampedValue !== state.currentBatchSize) { + state.currentBatchSize = clampedValue; + if (clampedValue !== value) { + e.target.value = state.currentBatchSize; + } + Utils.addDebugLog(`Manual batch size set to ${state.currentBatchSize} pixels`, 'info'); + updateBatchInfo(); + } + } + }); + + document.getElementById('paintTransparentPixels').addEventListener('change', (e) => { + state.paintTransparentPixels = e.target.checked; + Utils.addDebugLog(`Transparent pixel repair ${state.paintTransparentPixels ? 'enabled' : 'disabled'}`, 'info'); + }); + // Clear debug document.getElementById('clearDebugBtn').addEventListener('click', () => { state.debugLogs = []; diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css index f94db2a..10995a5 100644 --- a/Extension/themes/acrylic.css +++ b/Extension/themes/acrylic.css @@ -8,6 +8,32 @@ src: url('https://raw.githubusercontent.com/GORAlexComp/AppleColorEmojiFont/refs/heads/main/fonts/AppleColorEmoji.ttf'); } +/* Multi-account panel styling */ +.wplace-theme-acrylic #wplace-accounts-container { + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: none; + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) + var(--wplace-border-color); + min-width: 250px; +} + +.wplace-theme-acrylic #wplace-accounts-container * { + font-size: 15px; + line-height: normal; +} + +/* Active account styles */ +.wplace-theme-acrylic .wplace-accounts-container .active-account { + --active-bg: color-mix(in srgb, white 15%, transparent); + --active-color: var(--wplace-success); + --inactive-color: var(--wplace-text); + --progress-bg: rgb(255 255 255 / 10%); + --progress-fill: white; +} + /* Main Container */ .wplace-theme-acrylic #wplace-image-bot-container { background: var(--wplace-primary); diff --git a/Extension/themes/classic-light.css b/Extension/themes/classic-light.css index 424ad36..b4d78b1 100644 --- a/Extension/themes/classic-light.css +++ b/Extension/themes/classic-light.css @@ -73,6 +73,26 @@ /* === COMPONENT STYLING === */ +/* Multi-account panel styling */ +.wplace-theme-classic-light #wplace-accounts-container { + background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; + color: var(--wplace-text) !important; + border: 1px solid rgb(0 0 0 / 15%) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgb(0 0 0 / 15%), 0 0 0 1px rgb(0 0 0 / 8%) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; + backdrop-filter: blur(10px) !important; +} + +/* Active account styles */ +.wplace-theme-classic-light .wplace-accounts-container .active-account { + --active-bg: var(--wplace-secondary) !important; + --active-color: var(--wplace-success) !important; + --inactive-color: var(--wplace-text) !important; + --progress-bg: rgb(0 0 0 / 10%) !important; + --progress-fill: var(--wplace-highlight) !important; +} + /* Main container with clean light styling */ .wplace-theme-classic-light #wplace-image-bot-container { background: linear-gradient(135deg, #fff 0%, #f8f9fa 100%) !important; diff --git a/Extension/themes/classic.css b/Extension/themes/classic.css index 634675e..cd1fb39 100644 --- a/Extension/themes/classic.css +++ b/Extension/themes/classic.css @@ -34,6 +34,29 @@ } /* Classic theme container styling */ +/* Multi-account panel styling */ +:root #wplace-accounts-container, +.wplace-theme-classic #wplace-accounts-container { + background: var(--wplace-primary); + color: var(--wplace-text); + border-radius: var(--wplace-radius); + box-shadow: var(--wplace-shadow); + font-family: var(--wplace-font); + backdrop-filter: var(--wplace-backdrop); + border: var(--wplace-border-width) var(--wplace-border-style) var(--wplace-border-color); +} + +/* Active account styles */ +:root .wplace-accounts-container .active-account, +.wplace-theme-classic .wplace-accounts-container .active-account { + --active-bg: var(--wplace-secondary); + --active-color: var(--wplace-success); + --inactive-color: var(--wplace-text); + --progress-bg: rgb(255 255 255 / 10%); + --progress-fill: var(--wplace-highlight); +} + +/* Base container styling */ :root #wplace-image-bot-container, .wplace-theme-classic #wplace-image-bot-container { background: var(--wplace-primary); diff --git a/Extension/themes/neon.css b/Extension/themes/neon.css index a6d5795..184cb64 100644 --- a/Extension/themes/neon.css +++ b/Extension/themes/neon.css @@ -84,6 +84,122 @@ font-family: var(--wplace-font) !important; } +/* Multi-Account Panel Styles */ +.wplace-theme-neon #wplace-multi-account-container { + position: fixed !important; + top: 180px !important; + right: 20px !important; + width: 320px !important; + background: rgb(22 33 62 / 50%) !important; + border: 2px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 20px var(--wplace-text), inset 0 0 20px rgb(0 255 65 / 10%) !important; + font-family: var(--wplace-font) !important; + z-index: 9997 !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; +} + +.wplace-theme-neon #wplace-multi-account-container .wplace-panel-header { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 10px !important; + background: var(--wplace-secondary) !important; + border-bottom: 1px solid var(--wplace-text) !important; + text-transform: uppercase !important; + font-size: 9px !important; +} + +.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn { + background: transparent !important; + border: 1px solid var(--wplace-text) !important; + color: var(--wplace-text) !important; + font-size: 8px !important; + padding: 4px 8px !important; + cursor: pointer !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; + text-transform: uppercase !important; +} + +.wplace-theme-neon #wplace-multi-account-container .wplace-header-btn:hover { + box-shadow: 0 0 15px var(--wplace-text) !important; + animation: pixel-blink 0.5s infinite !important; +} + +.wplace-theme-neon .wplace-multi-account-content { + padding: 10px !important; +} + +.wplace-theme-neon .wplace-multi-account-info { + font-size: 9px !important; + text-transform: uppercase !important; + margin-bottom: 10px !important; + color: var(--wplace-text) !important; + text-shadow: 0 0 5px var(--wplace-text) !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-multi-account-current, +.wplace-theme-neon .wplace-multi-account-stats { + margin-bottom: 8px !important; + font-size: 8px !important; +} + +.wplace-theme-neon .wplace-multi-account-toggle { + margin-bottom: 12px !important; + display: flex !important; + align-items: center !important; +} + +.wplace-theme-neon .wplace-multi-account-switch { + margin-right: 8px !important; +} + +.wplace-theme-neon .wplace-multi-account-list { + max-height: 200px !important; + overflow-y: auto !important; +} + +.wplace-theme-neon .wplace-multi-account-item { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 8px !important; + margin-bottom: 4px !important; + background: rgb(22 33 62 / 50%) !important; + border: 1px solid var(--wplace-text) !important; + font-size: 8px !important; + text-transform: uppercase !important; + font-family: var(--wplace-font) !important; +} + +.wplace-theme-neon .wplace-multi-account-item.active { + background: rgb(0 255 65 / 10%) !important; + border-color: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; +} + +.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar { + width: 6px !important; +} + +.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-track { + background: rgb(22 33 62 / 50%) !important; +} + +.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb { + background: var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 10px var(--wplace-text) !important; +} + +.wplace-theme-neon .wplace-multi-account-list::-webkit-scrollbar-thumb:hover { + background: var(--wplace-success) !important; + box-shadow: 0 0 15px var(--wplace-success) !important; +} + .wplace-theme-neon #wplace-stats-container { background: var(--wplace-primary) !important; border: 2px solid var(--wplace-text) !important;