From 954eed9d98ecb39ae7470645afdabbe02f086e89 Mon Sep 17 00:00:00 2001 From: MiraclePatrickLumowa Date: Sat, 20 Sep 2025 00:22:09 +0800 Subject: [PATCH] feat: Fully implemented Auto-Switch feature --- Extension/Script-manager.js | 19 +- Extension/auto-image-styles.css | 91 +- Extension/background.js | 6 +- Extension/content-script.js | 4 +- Extension/lang/tr.json | 16 +- Extension/manifest.json | 2 +- Extension/popup/popup.html | 2 +- Extension/popup/popup.js | 32 +- Extension/scripts/Auto-Image.js | 1202 ++++++++++++++++++-------- Extension/scripts/Auto-Repair.js | 25 +- Extension/scripts/image-processor.js | 2 +- Extension/scripts/overlay-manager.js | 2 +- Extension/scripts/token-manager.js | 4 +- Extension/scripts/utils-manager.js | 42 +- Extension/themes/acrylic.css | 153 ++++ Extension/themes/classic-light.css | 160 +++- Extension/themes/classic.css | 176 +++- Extension/themes/neon-cyan.css | 187 +++- Extension/themes/neon-light.css | 188 +++- Extension/themes/neon.css | 187 +++- 20 files changed, 2051 insertions(+), 449 deletions(-) diff --git a/Extension/Script-manager.js b/Extension/Script-manager.js index a5fbf2a..4a93851 100644 --- a/Extension/Script-manager.js +++ b/Extension/Script-manager.js @@ -7,7 +7,7 @@ // @match https://wplace.live/* // @grant none // @icon -// ==/UserScript== +// ==/UserScript==l ; (async () => { console.log('%c🚀 WPlace AutoBOT Script Manager Loading...', 'color: #00ff41; font-weight: bold; font-size: 16px;'); @@ -28,23 +28,14 @@ icon: '🖼️', category: 'automation' }, - { - name: 'Auto-Repair.js', - displayName: '🔧 Auto Repair', + { + name: 'Auto-Repair.js', + displayName: '🔧 Auto Repair', description: 'Automated repair and maintenance tasks', icon: '🔧', category: 'utility' - }, - { - name: 'Acc-Switch.js', - displayName: '🔄 Account Switcher', - description: 'Temporary account switching script', - icon: '🔄', - category: 'automation' } - ]; - - // Neon theme styling + ]; // Neon theme styling const NEON_STYLES = ` @import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); diff --git a/Extension/auto-image-styles.css b/Extension/auto-image-styles.css index 28d7d3c..74a89c6 100644 --- a/Extension/auto-image-styles.css +++ b/Extension/auto-image-styles.css @@ -206,12 +206,12 @@ position: fixed; top: 20px; left: 20px; - width: 280px; + width: 340px; max-height: calc(100vh - 40px); padding: 0; z-index: 9998; animation: slide-in 0.4s ease-out; - overflow: hidden auto; + overflow: hidden; transition: all 0.3s ease; user-select: none; @@ -233,21 +233,21 @@ } #wplace-image-bot-container.wplace-compact { - width: 240px; + width: 300px; } #wplace-image-bot-container.wplace-minimized { - width: 200px; + width: 260px; height: auto; overflow: hidden; } -/* Stats container sits to the right of main (280 + 30 = 330) */ +/* Stats container sits to the right of main (340 + 30 = 370) */ #wplace-stats-container { position: fixed; top: 20px; - left: 310px; - width: 230px; + left: 370px; + width: 300px; max-height: calc(100vh - 40px); padding: 0; z-index: 9997; @@ -2081,3 +2081,80 @@ .wplace-account-placeholder i { color: var(--wplace-warning); } + +/* ===================================== */ +/* Hide browser default spinner buttons */ +/* ===================================== */ + +/* Hide spinner buttons for all number inputs in the extension */ +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +/* Firefox support */ +input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Specifically target extension number inputs */ +.wplace-settings-number-input::-webkit-outer-spin-button, +.wplace-settings-number-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-settings-number-input { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on batch control inputs */ +.wplace-random-batch-grid input[type="number"]::-webkit-outer-spin-button, +.wplace-random-batch-grid input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-random-batch-grid input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on block size inputs */ +.wplace-block-size-grid input[type="number"]::-webkit-outer-spin-button, +.wplace-block-size-grid input[type="number"]::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-block-size-grid input[type="number"] { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on threshold inputs */ +.resize-threshold-input::-webkit-outer-spin-button, +.resize-threshold-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.resize-threshold-input { + -moz-appearance: textfield; + appearance: textfield; +} + +/* Hide spinners on notification interval inputs */ +.wplace-notification-interval-input::-webkit-outer-spin-button, +.wplace-notification-interval-input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.wplace-notification-interval-input { + -moz-appearance: textfield; + appearance: textfield; +} diff --git a/Extension/background.js b/Extension/background.js index cb8f587..2120fe3 100644 --- a/Extension/background.js +++ b/Extension/background.js @@ -1,6 +1,6 @@ // Background service worker for WPlace AutoBOT Extension -// Test resource loading on extension start +// Test resource loading on extension starto chrome.runtime.onStartup.addListener(async () => { console.log('🚀 WPlace AutoBOT Extension started'); const resources = await loadExtensionResources(); @@ -15,7 +15,7 @@ chrome.runtime.onInstalled.addListener(async () => { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.action === 'executeScript') { - // Get tabId from sender or request + // Get tabId from sender or requests const tabId = request.tabId || sender.tab?.id; if (!tabId) { @@ -23,7 +23,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { return; } - // Use IIFE for async handling + // Use IIFE for async handlings (async () => { try { await executeLocalScript(request.scriptName, tabId); diff --git a/Extension/content-script.js b/Extension/content-script.js index e596abf..dc478e5 100644 --- a/Extension/content-script.js +++ b/Extension/content-script.js @@ -1,6 +1,6 @@ // Content script for WPlace AutoBOT - Creates in-page UI -// Check if we're on wplace.live +// Check if we're on wplace.lives if (window.location.hostname === 'wplace.live') { // Control variables @@ -59,7 +59,7 @@ if (window.location.hostname === 'wplace.live') { } } - // Execute script function + // Execute script functions async function executeScript(scriptName) { if (!autobotButton || currentScript) return; diff --git a/Extension/lang/tr.json b/Extension/lang/tr.json index 8557ddf..558b31f 100644 --- a/Extension/lang/tr.json +++ b/Extension/lang/tr.json @@ -9,7 +9,6 @@ "stopPainting": "Boyamayı Durdur", "checkingColors": "🔍 Uygun renkler kontrol ediliyor...", "noColorsFound": "❌ Sitede renk paletini açın ve tekrar deneyin!", - "colorsUpdated": "✅ Mevcut renk sayısı arttı {oldCount} -> {newCount}, {diffCount} yeni renk bulundu.", "colorsFound": "✅ {count} uygun renk bulundu. Yüklemeye hazır.", "loadingImage": "🖼️ Resim yükleniyor...", "imageLoaded": "✅ Resim {count} geçerli piksel ile yüklendi", @@ -21,12 +20,8 @@ "startPaintingMsg": "🎨 Boyama başlatılıyor...", "paintingProgress": "🧱 İlerleme: {painted}/{total} piksel...", "noCharges": "⌛ Yeterli hak yok. Bekleniyor {time}...", - "overlayTilesNotLoaded": "❌ Gerekli harita döşemeleri yüklenmedi. Bağlantıyı kontrol edin veya tekrar deneyin.", + "paintingStopped": "⏹️ Boyama kullanıcı tarafından durduruldu", "paintingComplete": "✅ Boyama tamamlandı! {count} piksel boyandı.", - "paintingBatchFailed": "❌ Yeniden denemelerden sonra piksel grubu gönderilemedi. Boyama durduruldu.", - "paintingPixelCheckFailed": "❌ ({x}, {y}) konumundaki piksel okunamadı. Boyama durduruldu.", - "paintingFinalBatchFailed": "⚠️ {count} pikselden oluşan son parti, yeniden denemelerden sonra başarısız oldu.", - "paintingStoppedByUser": "⏹️ Boyama kullanıcı tarafından durduruldu.", "paintingError": "❌ Boyama sırasında hata oluştu", "missingRequirements": "❌ Önce resim yükleyip konum seçmelisiniz", "progress": "İlerleme", @@ -35,7 +30,6 @@ "estimatedTime": "Tahmini süre", "initMessage": "Başlamak için 'Resim Yükle'ye tıklayın", "waitingInit": "Başlatma bekleniyor...", - "fullChargeIn": "Tam Şarj", "initializingToken": "🔧 Turnstile token üreticisi başlatılıyor...", "tokenReady": "✅ Token üreteci hazır - artık boyamaya başlayabilirsiniz!", "tokenRetryLater": "⚠️ Token üreteci gerektiğinde yeniden deneyecek", @@ -146,9 +140,5 @@ "hideStats": "İstatistikleri Gizle", "paintOptions": "Boya Seçenekleri", "paintWhitePixels": "Beyaz pikselleri boya", - "paintWhitePixelsDescription": "Etkinleştirildiğinde, şablonun beyaz pikselleri boyanacaktır.", - "paintTransparentPixels": "Şeffaf pikselleri boya", - "paintTransparentPixelsDescription": "Etkinleştirildiğinde, şablonun şeffaf pikselleri boyanır.", - "paintUnavailablePixels": "Boyama Yapılamıyor.", - "paintUnavailablePixelsDescription": "Etkinleştirildiğinde, kullanılamayan şablon renkleri, mevcut en yakın renk kullanılarak boyanır." -} \ No newline at end of file + "paintTransparentPixels": "Şeffaf pikselleri boya" +} diff --git a/Extension/manifest.json b/Extension/manifest.json index 1d0966e..2209b6c 100644 --- a/Extension/manifest.json +++ b/Extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "WPlace AutoBOT Script Launcher", - "version": "1.0.0", + "version": "2.0.0", "description": "Launch and manage automation scripts for WPlace", "icons": { "16": "icons/icon16.png", diff --git a/Extension/popup/popup.html b/Extension/popup/popup.html index af58ec9..a09f2ab 100644 --- a/Extension/popup/popup.html +++ b/Extension/popup/popup.html @@ -105,7 +105,7 @@ diff --git a/Extension/popup/popup.js b/Extension/popup/popup.js index 6b65a44..3dcb07f 100644 --- a/Extension/popup/popup.js +++ b/Extension/popup/popup.js @@ -18,7 +18,7 @@ const AVAILABLE_SCRIPTS = [ { name: 'Auto-Image.js', displayName: '🖼️ Auto Image', - description: 'Automated image processing and placement', + description: 'Automated image processing and placement with account switching', filename: 'Auto-Image.js' }, { @@ -26,12 +26,6 @@ const AVAILABLE_SCRIPTS = [ displayName: '🔧 Auto Repair', description: 'Automated repair and maintenance tasks', filename: 'Auto-Repair.js' - }, - { - name: 'Acc-Switch.js', - displayName: '🔄 Account Switcher', - description: 'Temporary account switching script', - filename: 'Acc-Switch.js' } ]; @@ -612,9 +606,6 @@ function showNotification(message, type = 'info') { } }, 3000); - // Log to console as well - // console.log(`📢 ${type.toUpperCase()}: ${message}`); - // Update status indicator if (type === 'error') { statusDot.className = 'status-dot error'; @@ -630,10 +621,12 @@ function showNotification(message, type = 'info') { statusText.textContent = 'Info'; } - // Reset status after 3 seconds - setTimeout(() => { - checkCurrentTab(); - }, 3000); + // Only reset status if it's not the initial "Ready to launch" message + if (message !== 'Ready to launch scripts on wplace.live') { + setTimeout(() => { + checkCurrentTab(); + }, 3000); + } } function escapeHtml(text) { @@ -664,17 +657,6 @@ async function loadStartupScript() { } } - - -// Load startup script selection -async function loadStartupScript() { - const result = await chrome.storage.local.get('startupScript'); - const select = document.getElementById('startupScriptSelect'); - if (select) { - select.value = result.startupScript || ''; - } -} - // Enhanced account validation async function validateAccountToken(token) { try { diff --git a/Extension/scripts/Auto-Image.js b/Extension/scripts/Auto-Image.js index d6c3daa..a206132 100644 --- a/Extension/scripts/Auto-Image.js +++ b/Extension/scripts/Auto-Image.js @@ -761,6 +761,7 @@ function getText(key, params) { processing: false, totalPixels: 0, paintedPixels: 0, + preFilteringDone: false, // Track if pre-filtering detection has been done this session availableColors: [], activeColorPalette: [], // User-selected colors for conversion paintWhitePixels: true, // Default to ON @@ -819,7 +820,11 @@ function getText(key, params) { _lastSaveTime: 0, _saveInProgress: false, paintedMap: null, - accountIndex: -1, + accountIndex: 0, + // Account switching state + allAccountsInfo: [], + isFetchingAllAccounts: false, + cooldownUsedThisCycle: false, }; // Expose state globally for the utils manager and other modules @@ -913,10 +918,7 @@ function getText(key, params) { const response = await originalFetch.apply(this, args); const url = args[0] instanceof Request ? args[0].url : args[0]; - // Debug: Log ALL tile requests to see if fetch interception is working - if (typeof url === 'string' && url.includes('.png') && url.match(/\/\d+\/\d+\.png/)) { - console.log(`🔍 TILE REQUEST: ${url} at ${Date.now()}`); - } + // TILE REQUEST logging removed to reduce console spam if (typeof url === 'string') { if (url.includes('https://backend.wplace.live/s0/pixel/')) { @@ -1417,6 +1419,7 @@ function getText(key, params) { const data = await res.json() return { ID: data.id, + Username: data.username || data.name || null, Charges: data.charges.count, Max: data.charges.max, Droplets: data.droplets @@ -1694,12 +1697,16 @@ function getText(key, params) { ?.addEventListener('click', () => unselectAllPaidColors()); } - async function handleCaptcha() { + async function handleCaptcha(allowGeneration = true) { const startTime = performance.now(); // Check user's token source preference if (state.tokenSource === 'manual') { console.log('🎯 Manual token source selected - using pixel placement automation'); + if (!allowGeneration) { + console.log('❌ Token generation disabled during processing'); + return null; + } return await tokenManager.handleCaptchaFallback(); } @@ -1741,8 +1748,12 @@ function getText(key, params) { console.log('♻️ Using existing cached token (from previous operation)'); token = getTurnstileToken(); } - // ✅ Otherwise generate a new one + // ✅ Otherwise generate a new one (only if allowed) else { + if (!allowGeneration) { + console.log('❌ Token expired/missing but generation disabled during processing'); + return null; + } console.log('🔐 No valid pre-generated or cached token, creating new one...'); token = await Utils.executeTurnstile(sitekey, 'paint'); if (token) { @@ -2094,6 +2105,32 @@ function getText(key, params) { + +
+
+
+ + Account Swapper +
+ +
+
+ +
+
+ + All Accounts + +
+
+
Click the icon to load accounts.
+
+
`; @@ -3008,6 +3045,22 @@ function getText(key, params) { } }); } + + // Account-related event handlers + const autoSwapToggle = statsContainer.querySelector('#autoSwapToggle'); + const refreshAllAccountsBtn = statsContainer.querySelector('#refreshAllAccountsBtn'); + + if (autoSwapToggle) { + autoSwapToggle.checked = CONFIG.autoSwap; + autoSwapToggle.addEventListener('change', (e) => { + CONFIG.autoSwap = e.target.checked; + console.log(`🔄 Auto-swap ${CONFIG.autoSwap ? 'enabled' : 'disabled'}`); + }); + } + + if (refreshAllAccountsBtn) { + refreshAllAccountsBtn.addEventListener('click', fetchAllAccountDetails); + } } if (statsContainer && statsBtn) { // Stats container starts hidden - user clicks button to show @@ -3775,16 +3828,67 @@ function getText(key, params) { cooldownInput.max = state.maxCharges; } + // Update current account charges in the account list + updateCurrentAccountInList(); + let imageStatsHTML = ''; if (state.imageLoaded) { const progress = state.totalPixels > 0 ? Math.round((state.paintedPixels / state.totalPixels) * 100) : 0; const remainingPixels = state.totalPixels - state.paintedPixels; - state.estimatedTime = Utils.calculateEstimatedTime( - remainingPixels, - state.displayCharges, - state.cooldown - ); + + // Updated estimation calculation for new cooldown logic + // Now we wait for cooldownChargeThreshold before processing pixels + + // Calculate batch size (with fallback if function not yet defined) + let batchSize; + try { + batchSize = typeof calculateBatchSize === 'function' ? calculateBatchSize() : state.paintingSpeed || 5; + } catch (e) { + batchSize = state.paintingSpeed || 5; + } + + // Ensure batchSize is valid and not zero + batchSize = Math.max(1, Math.min(batchSize, remainingPixels)); + const batchesNeeded = Math.ceil(remainingPixels / batchSize); + + // Calculate time accounting for cooldown threshold behavior with safety checks + let estimatedMs = 0; + if (remainingPixels > 0 && Number.isFinite(state.cooldown) && state.cooldown > 0) { + // Current charges available check + const currentCharges = Math.max(0, state.displayCharges); + const thresholdCharges = Math.max(1, state.cooldownChargeThreshold || 1); + + if (currentCharges < thresholdCharges) { + // Need to wait for charges to reach threshold first + const chargesNeeded = thresholdCharges - currentCharges; + estimatedMs += chargesNeeded * state.cooldown; + } + + // Calculate painting time more accurately + // Each batch uses thresholdCharges, so calculate how many cycles we need + const pixelsPerCycle = Math.min(batchSize, thresholdCharges); + const cyclesNeeded = Math.ceil(remainingPixels / pixelsPerCycle); + + // Time between cycles (waiting for charges to regenerate) + if (cyclesNeeded > 1) { + const regenerationTime = (cyclesNeeded - 1) * thresholdCharges * state.cooldown; + estimatedMs += regenerationTime; + } + + // Add painting speed delay if enabled + if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0) { + const paintingDelay = remainingPixels * (1000 / state.paintingSpeed); + estimatedMs += paintingDelay; + } + } + + // Safety check to prevent infinity or invalid values + if (!Number.isFinite(estimatedMs) || estimatedMs < 0) { + estimatedMs = 0; + } + + state.estimatedTime = estimatedMs; progressBar.style.width = `${progress}%`; imageStatsHTML = ` @@ -3798,12 +3902,6 @@ function getText(key, params) { )}
${state.paintedPixels}/${state.totalPixels}
-
-
${Utils.t( - 'estimatedTime' - )}
-
${Utils.formatTime(state.estimatedTime)}
-
`; } @@ -3842,8 +3940,26 @@ function getText(key, params) { .join(''); } + // Calculate multi-account statistics + let totalAllCharges = 0; + let totalMaxCharges = 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); + } + statsArea.innerHTML = ` ${imageStatsHTML} + ${state.allAccountsInfo.length > 0 ? ` +
+
+ Total All Accounts Charges +
+
+ ${totalAllCharges}/${totalMaxCharges} +
+
+ ` : ''}
${Utils.t('charges')} @@ -5186,6 +5302,16 @@ function getText(key, params) { await ensureToken(); if (!getTurnstileToken()) return; + // Only reset painted pixels on first start of session (when pre-filtering hasn't been done) + if (!state.preFilteringDone) { + const savedPaintedPixels = state.paintedPixels; // Store original value + state.paintedPixels = 0; + console.log(`🔄 First start this session - reset progress counter for accurate tracking (was: ${savedPaintedPixels})`); + updateStats(); // Update UI to show 0 progress + } else { + console.log('🔄 Continuing session - pre-filtering already done, keeping current progress'); + } + state.running = true; state.stopFlag = false; startBtn.disabled = true; @@ -5331,7 +5457,7 @@ function getText(key, params) { ); } - function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight) { + function generateCoordinates(width, height, mode, direction, snake, blockWidth, blockHeight, startFromX = 0, startFromY = 0) { const coords = []; console.log( 'Generating coordinates with \n mode:', @@ -5343,7 +5469,11 @@ function getText(key, params) { '\n blockWidth:', blockWidth, '\n blockHeight:', - blockHeight + blockHeight, + '\n startFromX:', + startFromX, + '\n startFromY:', + startFromY ); // --------- Standard 4 corners traversal ---------- let xStart, xEnd, xStep; @@ -5470,6 +5600,30 @@ function getText(key, params) { throw new Error(`Unknown mode: ${mode}`); } + // Filter coordinates to start from the specified position + if (startFromX > 0 || startFromY > 0) { + console.log(`🔄 Filtering coordinates to resume from position (${startFromX}, ${startFromY})`); + let startIndex = -1; + + // Find the starting position in the coordinate list + for (let i = 0; i < coords.length; i++) { + const [x, y] = coords[i]; + if (x === startFromX && y === startFromY) { + startIndex = i; + break; + } + } + + if (startIndex >= 0) { + // Resume from the found position (skip all previous coordinates) + const filteredCoords = coords.slice(startIndex); + console.log(`✂️ Resuming: skipped ${startIndex} coordinates, continuing with ${filteredCoords.length} remaining`); + return filteredCoords; + } else { + console.warn(`⚠️ Resume position (${startFromX}, ${startFromY}) not found in coordinate list, starting from beginning`); + } + } + return coords; } @@ -5490,15 +5644,19 @@ function getText(key, params) { state.paintedPixels++; Utils.markPixelPainted(p.x, p.y, pixelBatch.regionX, pixelBatch.regionY); }); + + // IMPORTANT: Decrement charges locally to match Acc-Switch.js behavior + state.displayCharges = Math.max(0, state.displayCharges - batchSize); + state.preciseCurrentCharges = Math.max(0, state.preciseCurrentCharges - batchSize); + state.fullChargeData = { ...state.fullChargeData, spentSinceShot: state.fullChargeData.spentSinceShot + batchSize, }; updateStats(); - updateUI('paintingProgress', 'default', { - painted: state.paintedPixels, - total: state.totalPixels, - }); + // Update account list with new charges + updateCurrentAccountInList(); + // Progress tracking removed from UI to reduce visual clutter Utils.performSmartSave(); if (CONFIG.PAINTING_SPEED_ENABLED && state.paintingSpeed > 0 && batchSize > 0) { @@ -5517,16 +5675,127 @@ function getText(key, params) { } async function processImage() { + console.log('🚀 Starting auto-swap enabled painting workflow'); + + try { + // Main painting cycle - repeats until image complete or stopped + while (!state.stopFlag) { + console.log('📋 Phase 1: Starting painting session'); + const paintingResult = await executePaintingSession(); + + if (paintingResult === 'completed') { + console.log('🎉 Image painting completed!'); + break; + } + + if (paintingResult === 'stopped') { + console.log('⏹️ Painting stopped by user'); + break; + } + + if (paintingResult === 'charges_depleted') { + if (!CONFIG.autoSwap) { + // Original workflow: cooldown period + console.log('⏱️ Phase 2: Entering cooldown period (auto-swap disabled)'); + const cooldownResult = await executeCooldownPeriod(); + + if (cooldownResult === 'stopped') { + console.log('⏹️ Cooldown stopped by user'); + break; + } + + // Phase 3: Regenerate token for next painting session + console.log('🔑 Phase 3: Regenerating token for next session'); + const tokenResult = await regenerateTokenForNewSession(); + + if (!tokenResult) { + console.log('❌ Failed to regenerate token, stopping'); + state.stopFlag = true; + break; + } + } else { + // Auto-swap workflow: switch to next account or use cooldown + console.log('🔄 Auto-swap enabled: checking account switching options'); + + const accounts = JSON.parse(localStorage.getItem("accounts")) || []; + if (accounts.length <= 1) { + console.log('📋 Only one account available, using standard cooldown'); + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + + const tokenResult = await regenerateTokenForNewSession(); + if (!tokenResult) { + state.stopFlag = true; + break; + } + } else { + // Check if we're at the last account in the cycle + const isLastAccount = state.accountIndex >= accounts.length - 1; + + if (!isLastAccount) { + // Switch to next account + console.log(`🔄 Switching to next account (${state.accountIndex + 1}/${accounts.length})`); + const switchResult = await switchToNextAccount(accounts); + if (!switchResult) { + console.log('❌ Account switch failed, stopping'); + state.stopFlag = true; + break; + } + // Continue painting with new account (no cooldown, no token regeneration) + continue; + } else { + // Last account - check cooldown flag + if (!state.cooldownUsedThisCycle) { + // Use cooldown once per cycle + console.log('⏱️ Last account reached, using cooldown period'); + state.cooldownUsedThisCycle = true; + + const cooldownResult = await executeCooldownPeriod(); + if (cooldownResult === 'stopped') break; + + const tokenResult = await regenerateTokenForNewSession(); + if (!tokenResult) { + state.stopFlag = true; + break; + } + } else { + // Reset cycle - go back to first account + console.log('🔁 Cooldown already used, resetting to first account'); + state.cooldownUsedThisCycle = false; + state.accountIndex = 0; + + const switchResult = await switchToSpecificAccount(accounts[0], 0); + if (!switchResult) { + console.log('❌ Reset to first account failed, stopping'); + state.stopFlag = true; + break; + } + // Continue painting with first account + continue; + } + } + } + } + } + + console.log('🔄 Cycle complete, starting next painting session'); + } + } finally { + await finalizePaintingProcess(); + } + } + + // Phase 1: Execute a complete painting session using all available charges + async function executePaintingSession() { + console.log('🎨 Starting painting session - using all charges until 0'); const { width, height, pixels } = state.imageData; const { x: startX, y: startY } = state.startPosition; const { x: regionX, y: regionY } = state.region; // Check if we're working with restored data by looking for existing availableColors - // If availableColors exists and colorsChecked is true, we don't need to wait for tiles const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked; if (!isRestoredData) { - // Only wait for tiles if this is not restored data // Wait for original tiles to load if needed const tilesReady = await overlayManager.waitForTiles( regionX, @@ -5541,10 +5810,8 @@ function getText(key, params) { if (!tilesReady) { updateUI('overlayTilesNotLoaded', 'error'); state.stopFlag = true; - return; + return 'stopped'; } - } else { - // Using restored data - skipping tile wait } let pixelBatch = null; @@ -5555,82 +5822,21 @@ function getText(key, params) { colorUnavailable: 0, }; - const transparencyThreshold = - state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + // IMPORTANT: Check charges once at start, then paint until depleted + console.log('🔋 Checking initial charges for painting session'); + const initialChargeCheck = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(initialChargeCheck.charges); + state.preciseCurrentCharges = initialChargeCheck.charges; + state.cooldown = initialChargeCheck.cooldown; - function checkPixelEligibility(x, y) { - // CRITICAL FIX: Check module availability before processing - if (!Utils || typeof Utils.isWhitePixel !== 'function') { - console.error('❌ Utils module not available for pixel eligibility check'); - return { - eligible: false, - reason: 'moduleUnavailable', - }; - } - - const idx = (y * width + x) * 4; - const r = pixels[idx], - g = pixels[idx + 1], - b = pixels[idx + 2], - a = pixels[idx + 3]; - - if (!state.paintTransparentPixels && a < transparencyThreshold) - return { - eligible: false, - reason: 'transparent', - }; - if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) - return { - eligible: false, - reason: 'white', - }; - - let targetRgb = Utils.isWhitePixel(r, g, b) - ? [255, 255, 255] - : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); - - // Template color ID, normalized/mapped to the nearest available color in our palette. - // Example: template requires "Slate", but we only have "Dark Gray" available - // → mappedTargetColorId = ID of Dark Gray. - // - // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier - // because "Slate" was not found (null returned). - // - // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"), - // and we proceed with painting using that mapped color. - // - // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray), - // we skip painting, since template and canvas both resolve to the same available color (Dark Gray). - const mappedTargetColorId = Utils.resolveColor( - targetRgb, - state.availableColors, - !state.paintUnavailablePixels - ); - - // Technically, checking only `!mappedTargetColorId.id` would be enough, - // but combined with `state.paintUnavailablePixels` it makes the logic explicit: - // we only skip when the template color cannot be mapped AND strict mode is on. - if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { - return { - eligible: false, - reason: 'colorUnavailable', - r, - g, - b, - a, - mappedColorId: mappedTargetColorId.id, - }; - } - return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + if (state.displayCharges <= 0) { + console.log('⚡ No charges available, skipping painting session'); + return 'charges_depleted'; } - function skipPixel(reason, id, rgb, x, y) { - if (reason !== 'transparent') { - console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); - } - skippedPixels[reason]++; - } + console.log(`🔋 Starting with ${state.displayCharges} charges - painting until depleted`); + // Paint pixels until we run out of charges or complete the image try { const coords = generateCoordinates( width, @@ -5639,24 +5845,127 @@ function getText(key, params) { state.coordinateDirection, state.coordinateSnake, state.blockWidth, - state.blockHeight + state.blockHeight, + state.lastPosition.x, + state.lastPosition.y ); - outerLoop: for (const [x, y] of coords) { + // OPTIMIZATION: Pre-filter already painted pixels (happens only once per session) + let eligibleCoords = []; + let alreadyPaintedCount = 0; + + // Log resume information if applicable + if (state.lastPosition.x > 0 || state.lastPosition.y > 0) { + console.log(`🔄 Resuming painting from position (${state.lastPosition.x}, ${state.lastPosition.y})`); + console.log(`📊 Current progress: ${state.paintedPixels} pixels painted`); + } + + if (!state.preFilteringDone) { + console.log('🔍 Pre-filtering already painted pixels (one-time detection for this session)...'); + + for (const [x, y] of coords) { + const targetPixelInfo = checkPixelEligibility(x, y); + + if (!targetPixelInfo.eligible) { + if (targetPixelInfo.reason !== 'alreadyPainted') { + skippedPixels[targetPixelInfo.reason]++; + } + continue; + } + + // Check if already painted (only once per session) + 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; + + try { + const tilePixelRGBA = await overlayManager.getTilePixelColor( + regionX + adderX, + regionY + adderY, + pixelX, + pixelY + ); + + if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { + const mappedCanvasColor = Utils.resolveColor( + tilePixelRGBA.slice(0, 3), + state.availableColors, + !state.paintUnavailablePixels // Use same parameter as target pixel + ); + const isMatch = mappedCanvasColor.id === targetPixelInfo.mappedColorId; + if (isMatch) { + alreadyPaintedCount++; + // Add detected already-painted pixels to progress (after reset to 0) + state.paintedPixels++; + Utils.markPixelPainted(x, y, regionX + adderX, regionY + adderY); + continue; // Skip already painted pixels + } + } + } catch (e) { + // If we can't check, include the pixel (better to attempt than skip) + } + + // Add eligible unpainted pixel to list + eligibleCoords.push([x, y, targetPixelInfo]); + } + + // Mark pre-filtering as done for this session + state.preFilteringDone = true; + + // Log pre-filtering results + if (alreadyPaintedCount > 0) { + console.log(`✓ Pre-filter complete: ${alreadyPaintedCount} already painted pixels detected and added to progress`); + console.log('ℹ️ This detection will not happen again until a new image/save is loaded'); + // Update UI to reflect the new progress immediately + updateStats(); + } + skippedPixels.alreadyPainted = alreadyPaintedCount; + } else { + // Pre-filtering already done this session, just filter for basic eligibility + console.log('🔍 Using existing pre-filter results (already done this session)'); + for (const [x, y] of coords) { + const targetPixelInfo = checkPixelEligibility(x, y); + + if (!targetPixelInfo.eligible) { + if (targetPixelInfo.reason !== 'alreadyPainted') { + skippedPixels[targetPixelInfo.reason]++; + } + continue; + } + + // Only include pixels that haven't been marked as painted yet + if (!Utils.isPixelPainted(x, y)) { + eligibleCoords.push([x, y, targetPixelInfo]); + } + } + } + + // Paint eligible pixels (already pre-filtered, no duplicate checks) + outerLoop: for (const [x, y, targetPixelInfo] of eligibleCoords) { if (state.stopFlag) { if (pixelBatch && pixelBatch.pixels.length > 0) { - console.log( - `🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels` - ); + console.log(`🎯 Sending last batch before stop with ${pixelBatch.pixels.length} pixels`); await flushPixelBatch(pixelBatch); } state.lastPosition = { x, y }; - updateUI('paintingPaused', 'warning', { x, y }); - // noinspection UnnecessaryLabelOnBreakStatementJS - break outerLoop; + // Removed 'Paused at' message from main panel to reduce UI clutter + return 'stopped'; + } + + // Check if we have charges left (local count, no API call) + if (state.displayCharges <= 0) { + console.log('⚡ No charges left (local count), ending painting session'); + if (pixelBatch && pixelBatch.pixels.length > 0) { + console.log(`🎯 Sending final batch with ${pixelBatch.pixels.length} pixels`); + await flushPixelBatch(pixelBatch); + } + state.lastPosition = { x, y }; + return 'charges_depleted'; } - const targetPixelInfo = checkPixelEligibility(x, y); let absX = startX + x; let absY = startY + y; @@ -5665,61 +5974,23 @@ function getText(key, params) { let pixelX = absX % 1000; let pixelY = absY % 1000; - // Template color ID, normalized/mapped to the nearest available color in our palette. - // Example: template requires "Slate", but we only have "Dark Gray" available - // → mappedTargetColorId = ID of Dark Gray. - // - // If `state.paintUnavailablePixels` is enabled, the painting would stop earlier - // because "Slate" was not found (null returned). - // - // Else, the template "Slate" is mapped to the closest available color (e.g., "Dark Gray"), - // and we proceed with painting using that mapped color. - // - // In this case, if the canvas pixel is already Slate (mapped to available Dark Gray), - // we skip painting, since template and canvas both resolve to the same available color (Dark Gray). const targetMappedColorId = targetPixelInfo.mappedColorId; - if (!targetPixelInfo.eligible) { - skipPixel( - targetPixelInfo.reason, - targetMappedColorId, - [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b], - pixelX, - pixelY - ); - continue; - } - + // Set up pixel batch for new region if needed 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})` - ); + console.log(`🌍 Sending region-change batch with ${pixelBatch.pixels.length} pixels`); const success = await flushPixelBatch(pixelBatch); - - if (success) { - if ( - CONFIG.PAINTING_SPEED_ENABLED && - state.paintingSpeed > 0 && - pixelBatch.pixels.length > 0 - ) { - const batchDelayFactor = Math.max(1, 100 / state.paintingSpeed); - const totalDelay = Math.max(100, batchDelayFactor * pixelBatch.pixels.length); - await Utils.sleep(totalDelay); - } - updateStats(); - } else { + if (!success) { console.error(`❌ Batch failed permanently after retries. Stopping painting.`); state.stopFlag = true; - updateUI('paintingBatchFailed', 'error'); - // noinspection UnnecessaryLabelOnBreakStatementJS - break outerLoop; + return 'stopped'; } + updateStats(); } pixelBatch = { @@ -5729,51 +6000,7 @@ function getText(key, params) { }; } - try { - // CRITICAL FIX: Check overlay manager availability - if (!overlayManager || typeof overlayManager.getTilePixelColor !== 'function') { - console.error('❌ Overlay manager not available for pixel color check'); - state.log(`❌ Overlay manager unavailable - skipping pixel (${pixelX}, ${pixelY})`); - continue; - } - - const tileKeyParts = [pixelBatch.regionX, pixelBatch.regionY]; - - const tilePixelRGBA = await overlayManager.getTilePixelColor( - tileKeyParts[0], - tileKeyParts[1], - pixelX, - pixelY - ); - - if (tilePixelRGBA && Array.isArray(tilePixelRGBA)) { - // Resolve the actual canvas pixel color to the closest available color. - // (The raw canvas RGB [er, eg, eb] is mapped into state.availableColors) - // so that comparison is consistent with targetMappedColorId. - const mappedCanvasColor = Utils.resolveColor( - tilePixelRGBA.slice(0, 3), - state.availableColors - ); - const isMatch = mappedCanvasColor.id === targetMappedColorId; - if (isMatch) { - skipPixel( - 'alreadyPainted', - targetMappedColorId, - [targetPixelInfo.r, targetPixelInfo.g, targetPixelInfo.b], - pixelX, - pixelY - ); - continue; - } - } - } catch (e) { - console.error(`Error checking existing pixel at (${pixelX}, ${pixelY}):`, e); - updateUI('paintingPixelCheckFailed', 'error', { x: pixelX, y: pixelY }); - state.stopFlag = true; - // noinspection UnnecessaryLabelOnBreakStatementJS - break outerLoop; - } - + // Add pixel to batch (no need to check again - already pre-filtered) pixelBatch.pixels.push({ x: pixelX, y: pixelY, @@ -5782,118 +6009,136 @@ function getText(key, params) { localY: y, }); + // Send batch if it's full 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})` - ); + console.log(`📦 Sending batch with ${pixelBatch.pixels.length} pixels`); const success = await flushPixelBatch(pixelBatch); if (!success) { console.error(`❌ Batch failed permanently after retries. Stopping painting.`); state.stopFlag = true; - updateUI('paintingBatchFailed', 'error'); - // noinspection UnnecessaryLabelOnBreakStatementJS - break outerLoop; + return 'stopped'; } - pixelBatch.pixels = []; - } - if (!CONFIG.autoSwap) { - if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { - await Utils.dynamicSleep(() => { - if (state.displayCharges >= state.cooldownChargeThreshold) { - NotificationManager.maybeNotifyChargesReached(true); - return 0; - } - if (state.stopFlag) return 0; - return getMsToTargetCharges( - state.preciseCurrentCharges, - state.cooldownChargeThreshold, - state.cooldown - ); - }); - } - } - else { - if (state.displayCharges < state.cooldownChargeThreshold && !state.stopFlag) { - console.log("⚠️ Charges too low, swapping to next account..."); - - const accounts = JSON.parse(localStorage.getItem("accounts")) || []; - if (accounts.length === 0) { - console.warn("❌ No accounts available, stopping painting."); - state.stopFlag = true; - return; - } - - state.accountIndex = (state.accountIndex + 1) % accounts.length; - console.log("🔄 Switching to account index:", state.accountIndex); - - const nextToken = accounts[state.accountIndex]; - console.log("🔑 Next token:", nextToken); - - if (!nextToken) { - console.warn("⚠️ Invalid token, skipping..."); - return; - } - - swapAccountTrigger(nextToken); - - let maxRetries = 20; - let retryCount = 0; - let swapSuccess = false; - - while (!swapSuccess && retryCount < maxRetries) { - console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); - - // Wait for a short period before checking. - await new Promise(resolve => setTimeout(resolve, 1000)); - - try { - await fetchAccount(); - - console.log("✅ Account swap confirmed."); - swapSuccess = true; - } catch (error) { - console.warn("❌ Account swap not yet successful. Retrying...", error); - retryCount++; - } - } - - if (swapSuccess) { - - const { charges, cooldown } = await WPlaceService.getCharges(); - state.displayCharges = Math.floor(charges); - state.cooldown = cooldown; - Utils.performSmartSave(); - updateStats(); - } else { - console.error("❌ Failed to swap account after multiple retries. Stopping loop."); - state.stopFlag = true; - } - } - } - - if (state.stopFlag) { - // noinspection UnnecessaryLabelOnBreakStatementJS - break outerLoop; + updateStats(); } } + // Send final batch if any pixels remain if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) { console.log(`🏁 Sending final batch with ${pixelBatch.pixels.length} pixels`); const success = await flushPixelBatch(pixelBatch); if (!success) { - console.warn( - `⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.` - ); + console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels`); } } + + // If we completed the entire coordinate loop, image is complete + return state.stopFlag ? 'stopped' : 'completed'; + } finally { - if (window._chargesInterval) clearInterval(window._chargesInterval); + // Log skip statistics for this session + console.log(`📊 Session Statistics:`); + console.log(` New pixels painted: ${state.paintedPixels - (skippedPixels.alreadyPainted || 0)}`); + console.log(` Already painted detected: ${skippedPixels.alreadyPainted}`); + console.log(` Total progress: ${state.paintedPixels}`); + console.log(` Pre-filtered - Transparent: ${skippedPixels.transparent}`); + console.log(` Pre-filtered - White: ${skippedPixels.white}`); + console.log(` Pre-filtered - Color Unavailable: ${skippedPixels.colorUnavailable}`); + } + } + + // Phase 2: Execute cooldown period - wait for target charges (NO token regeneration) + async function executeCooldownPeriod() { + console.log('⏱️ Entering cooldown period - waiting for target charges'); + console.log('🚫 NO token regeneration during cooldown (even if expired/invalid)'); + + // Check initial charges to calculate wait time + let chargeCheckCount = 0; + const maxChargeChecks = 10; // Limit API calls during cooldown + + while (!state.stopFlag) { + chargeCheckCount++; + + const { charges, cooldown } = await WPlaceService.getCharges(); + state.displayCharges = Math.floor(charges); + state.preciseCurrentCharges = charges; + state.cooldown = cooldown; + + if (state.displayCharges >= state.cooldownChargeThreshold) { + console.log(`✅ Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`); + NotificationManager.maybeNotifyChargesReached(true); + updateStats(); + return 'target_reached'; + } + + updateUI('noChargesThreshold', 'warning', { + time: Utils.msToTimeText(state.cooldown), + threshold: state.cooldownChargeThreshold, + current: state.displayCharges, + }); + await updateStats(); + + // Smart delay calculation to reduce API calls + const chargesNeeded = state.cooldownChargeThreshold - state.displayCharges; + const estimatedWaitTime = chargesNeeded * state.cooldown; + + // Use longer delays during cooldown to prevent rate limiting + let delayTime; + if (chargeCheckCount < 3) { + // First few checks - shorter delay + delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum + } else if (estimatedWaitTime > 60000) { + // Long wait expected - check every 15 seconds + delayTime = 10000; + } else if (estimatedWaitTime > 30000) { + // Medium wait - check every 15 seconds + delayTime = 15000; + } else { + // Close to target - check every 5 seconds + delayTime = 5000; + } + + console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`); + await Utils.sleep(10000); + + // Fail-safe: Don't exceed max checks + if (chargeCheckCount >= maxChargeChecks) { + console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway'); + break; + } + } + + return 'stopped'; + } + + // Phase 3: Regenerate token for new painting session + async function regenerateTokenForNewSession() { + console.log('🔑 Regenerating token for new painting session'); + + try { + // Force regenerate token for new session + await ensureToken(true); // forceRefresh = true + + if (!getTurnstileToken()) { + console.error('❌ Failed to generate token for new session'); + return false; + } + + console.log('✅ Token regenerated successfully for new session'); + return true; + } catch (error) { + console.error('❌ Token regeneration failed:', error); + return false; + } + } + + // Finalize painting process cleanup + async function finalizePaintingProcess() { + console.log('🧹 Finalizing painting process'); + + if (window._chargesInterval) { + clearInterval(window._chargesInterval); window._chargesInterval = null; } @@ -5903,8 +6148,6 @@ function getText(key, params) { } 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'); @@ -5913,27 +6156,71 @@ function getText(key, params) { toggleOverlayBtn.disabled = true; } } - - // Log skip statistics - console.log(`📊 Pixel Statistics:`); - console.log(` Painted: ${state.paintedPixels}`); - console.log(` Skipped - Transparent: ${skippedPixels.transparent}`); - console.log(` Skipped - White (disabled): ${skippedPixels.white}`); - console.log(` Skipped - Already painted: ${skippedPixels.alreadyPainted}`); - console.log(` Skipped - Color Unavailable: ${skippedPixels.colorUnavailable}`); - console.log( - ` Total processed: ${state.paintedPixels + - skippedPixels.transparent + - skippedPixels.white + - skippedPixels.alreadyPainted + - skippedPixels.colorUnavailable - }` - ); - - updateStats(); } - // Helper function to calculate batch size based on mode + // Helper function to check pixel eligibility (shared by painting functions) + function checkPixelEligibility(x, y) { + const { width, height, pixels } = state.imageData; + const transparencyThreshold = state.customTransparencyThreshold || CONFIG.TRANSPARENCY_THRESHOLD; + + // CRITICAL FIX: Check module availability before processing + if (!Utils || typeof Utils.isWhitePixel !== 'function') { + console.error('❌ Utils module not available for pixel eligibility check'); + return { + eligible: false, + reason: 'moduleUnavailable', + }; + } + + const idx = (y * width + x) * 4; + const r = pixels[idx], + g = pixels[idx + 1], + b = pixels[idx + 2], + a = pixels[idx + 3]; + + if (!state.paintTransparentPixels && a < transparencyThreshold) + return { + eligible: false, + reason: 'transparent', + }; + if (!state.paintWhitePixels && Utils.isWhitePixel(r, g, b)) + return { + eligible: false, + reason: 'white', + }; + + let targetRgb = Utils.isWhitePixel(r, g, b) + ? [255, 255, 255] + : Utils.findClosestPaletteColor(r, g, b, state.activeColorPalette); + + const mappedTargetColorId = Utils.resolveColor( + targetRgb, + state.availableColors, + !state.paintUnavailablePixels + ); + + if (!state.paintUnavailablePixels && !mappedTargetColorId.id) { + return { + eligible: false, + reason: 'colorUnavailable', + r, + g, + b, + a, + mappedColorId: mappedTargetColorId.id, + }; + } + return { eligible: true, r, g, b, a, mappedColorId: mappedTargetColorId.id }; + } + + // Helper function to skip pixel and log the reason (minimized logging) + function skipPixel(reason, id, rgb, x, y, skippedPixels) { + // Minimize logging to prevent console flooding - only log non-routine skips + if (reason !== 'transparent' && reason !== 'alreadyPainted') { + console.log(`Skipped pixel for ${reason} (id: ${id}, (${rgb.join(', ')})) at (${x}, ${y})`); + } + skippedPixels[reason]++; + } function calculateBatchSize() { let targetBatchSize; @@ -5970,15 +6257,31 @@ function getText(key, params) { console.log(`✅ Batch succeeded on attempt ${attempt}`); return true; } else if (result === 'token_error') { - console.log(`🔑 Token error on attempt ${attempt}, regenerating...`); + console.log(`🔑 Token error on attempt ${attempt} - no token available during processing`); + console.log(`❌ Stopping batch processing - tokens must be generated at startup/start button only`); + updateUI('captchaFailed', 'error'); + return false; // Stop processing entirely - don't regenerate during processing + } else if (result === 'token_regenerated') { + console.log(`🔄 Token regenerated on attempt ${attempt} after 403 error - retrying batch`); + updateUI('paintingPaused', 'warning', { message: 'Token refreshed, resuming...' }); + // Don't count token regeneration as a failed attempt, retry immediately + attempt--; + await Utils.sleep(500); // Brief pause before retry + continue; + } else if (result === 'token_regeneration_failed') { + console.log(`❌ Token regeneration failed on attempt ${attempt} after 403 error`); + updateUI('captchaFailed', 'error'); + return false; // Stop processing if we can't get a valid token + } else if (result === 'invalid_token_error') { + console.log(`🔑 Invalid token detected on attempt ${attempt}, regenerating...`); updateUI('captchaSolving', 'warning'); try { - await handleCaptcha(); + await handleCaptcha(true); // Allow generation for invalid token cases // Don't count token regeneration as a failed attempt attempt--; continue; } catch (e) { - console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e); + console.error(`❌ Token regeneration failed after invalid token on attempt ${attempt}:`, e); updateUI('captchaFailed', 'error'); // Wait longer before retrying after token failure await Utils.sleep(5000); @@ -6006,17 +6309,10 @@ function getText(key, params) { async function sendPixelBatch(pixelBatch, regionX, regionY) { let token = getTurnstileToken(); - // Generate new token if we don't have one + // Don't auto-generate tokens during processing - return error if no token available if (!token) { - try { - console.log('🔑 Generating Turnstile token for pixel batch...'); - token = await handleCaptcha(); - setTurnstileToken(token); // Store for potential reuse - } catch (error) { - console.error('❌ Failed to generate Turnstile token:', error); - createTokenPromise(); - return 'token_error'; - } + console.warn('⚠️ No token available and auto-generation disabled during processing'); + return 'token_error'; } const coords = new Array(pixelBatch.length * 2); @@ -6043,40 +6339,21 @@ function getText(key, params) { 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(); - setTurnstileToken(token); - - // Retry the request with new token - const retryPayload = { coords, colors, t: token, fp: fpStr32 }; - var wasmtoken = await createWasmToken(regionX, regionY, retryPayload); - const retryRes = await fetch( - `https://backend.wplace.live/s0/pixel/${regionX}/${regionY}`, - { - method: 'POST', - headers: { 'Content-Type': 'text/plain;charset=UTF-8', "x-pawtect-token": wasmtoken }, - credentials: 'include', - body: JSON.stringify(retryPayload), - } - ); - - if (retryRes.status === 403) { - setTurnstileToken(null); - createTokenPromise(); - return 'token_error'; - } - - const retryData = await retryRes.json(); - return retryData?.painted === pixelBatch.length; - } catch (retryError) { - console.error('❌ Token regeneration failed:', retryError); - setTurnstileToken(null); - createTokenPromise(); - return 'token_error'; + console.error('❌ 403 Forbidden. Token invalid during painting - regeneration allowed.'); + + // 403 errors during painting allow token regeneration per workflow requirements + console.log('� Token invalid (403) during painting - regenerating token as allowed by workflow'); + setTurnstileToken(null); + createTokenPromise(); + + // Attempt to regenerate token immediately + const newToken = await ensureToken(true); + if (newToken) { + console.log('✅ Token regenerated after 403 error, returning regenerate signal'); + return 'token_regenerated'; + } else { + console.error('❌ Failed to regenerate token after 403 error'); + return 'token_regeneration_failed'; } } @@ -6663,6 +6940,211 @@ function getText(key, params) { console.log("User's Droplets :", Droplets); } + 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(); + + 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(); + const actualName = fetchedInfo.Username || `User${fetchedInfo.ID}` || `Account ${i + 1}`; + if (fetchedInfo.ID === originalId) originalToken = token; + state.allAccountsInfo.push({ ...fetchedInfo, token, displayName: actualName, isCurrent: fetchedInfo.ID === originalId }); + } else { + const fallbackName = `Account ${i + 1}`; + state.allAccountsInfo.push({ token, ID: `...${token.slice(-4)}`, displayName: fallbackName, 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(); + renderAccountsList(); + + state.isFetchingAllAccounts = false; + if (refreshBtn) { + refreshBtn.innerHTML = ''; + refreshBtn.disabled = false; + } + } + } + + // Function to update current account charges in the account list + function updateCurrentAccountInList() { + if (state.allAccountsInfo.length === 0) return; + + // Find current account in the list and update its charges + const currentAccountInList = state.allAccountsInfo.find(acc => acc.isCurrent); + if (currentAccountInList) { + currentAccountInList.Charges = Math.floor(state.displayCharges || state.preciseCurrentCharges || 0); + currentAccountInList.Max = state.maxCharges; + // Re-render the account list to show updated charges + renderAccountsList(); + } + } + + 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; + } + + state.allAccountsInfo.forEach((info, index) => { + const item = document.createElement('div'); + item.className = `wplace-account-item ${info.isCurrent ? 'is-current' : ''}`; + + // Create ordering number element + const orderNumber = document.createElement('div'); + orderNumber.className = 'wplace-account-number'; + orderNumber.textContent = index + 1; + + const displayName = info.displayName || `Account ${index + 1}`; + + const details = document.createElement('div'); + details.className = 'wplace-account-details'; + const nameDiv = document.createElement('div'); + nameDiv.className = 'wplace-account-name'; + nameDiv.textContent = displayName; + nameDiv.title = displayName; + details.appendChild(nameDiv); + + let stats; + if (info.error) { + stats = document.createElement('div'); + stats.className = 'wplace-account-stats'; + stats.style.color = 'red'; + stats.textContent = 'Error'; + } else { + stats = document.createElement('div'); + stats.className = 'wplace-account-stats'; + stats.innerHTML = ` + ${Math.floor(info.Charges || 0)}/${Math.floor(info.Max || 0)} + ${Math.floor(info.Droplets || 0)} + `; + } + + item.appendChild(orderNumber); + item.appendChild(details); + item.appendChild(stats); + accountsListArea.appendChild(item); + }); + } + + // Account switching helper functions + async function switchToNextAccount(accounts) { + 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 false; + } + + return await switchToSpecificAccount(nextToken, state.accountIndex); + } + + async function switchToSpecificAccount(token, accountIndex) { + swapAccountTrigger(token); + + let maxRetries = 20; + let retryCount = 0; + let swapSuccess = false; + + while (!swapSuccess && retryCount < maxRetries) { + console.log(`⏳ Waiting for account swap... (Attempt ${retryCount + 1}/${maxRetries})`); + + 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.preciseCurrentCharges = charges; + state.cooldown = cooldown; + state.accountIndex = accountIndex; + Utils.performSmartSave(); + updateStats(); + return true; + } else { + console.error('❌ Failed to swap account after multiple retries.'); + return false; + } + } + // Wait for dependencies before initializing UI async function waitForDependenciesAndInitialize() { // Wait for all global managers to be available diff --git a/Extension/scripts/Auto-Repair.js b/Extension/scripts/Auto-Repair.js index cde5232..a4ef4ee 100644 --- a/Extension/scripts/Auto-Repair.js +++ b/Extension/scripts/Auto-Repair.js @@ -1773,16 +1773,21 @@ 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 - }); + if (targetColor.id === currentColor.id) { + // Pixel is already correctly painted - skip it + continue; + } + + // Pixel has wrong color - mark as damaged + 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) { diff --git a/Extension/scripts/image-processor.js b/Extension/scripts/image-processor.js index f25e22f..cc8bff6 100644 --- a/Extension/scripts/image-processor.js +++ b/Extension/scripts/image-processor.js @@ -10,7 +10,7 @@ /** * ImageProcessor - Handles image loading, processing, color conversion, and dithering for WPlace AutoBot - * Extracted from Auto-Image.js for better modularity and reusability + * Extracted from Auto-Image.js for better modularity and reusabilit */ class ImageProcessor { constructor(imageSrc = null) { diff --git a/Extension/scripts/overlay-manager.js b/Extension/scripts/overlay-manager.js index c7f3fa5..b3290ef 100644 --- a/Extension/scripts/overlay-manager.js +++ b/Extension/scripts/overlay-manager.js @@ -10,7 +10,7 @@ /** * OverlayManager - Handles overlay processing, tile chunking, and canvas operations for WPlace AutoBot - * Extracted from Auto-Image.js for better modularity and reusability + * Extracted from Auto-Image.js for better modularity and reusabilityc */ class OverlayManager { constructor() { diff --git a/Extension/scripts/token-manager.js b/Extension/scripts/token-manager.js index 6e07a7f..874bfe4 100644 --- a/Extension/scripts/token-manager.js +++ b/Extension/scripts/token-manager.js @@ -5,12 +5,12 @@ // @description Turnstile token management for WPlace AutoBot // @author Wbot // @match https://wplace.live/* -// @grant none +// @grant nones // ==/UserScript== /** * TokenManager - Handles Turnstile token generation, caching, and validation for WPlace AutoBot - * Extracted from Auto-Image.js for better modularity and reusability + * Extracted from Auto-Image.js for better modularity and reusabilitys */ class TokenManager { constructor() { diff --git a/Extension/scripts/utils-manager.js b/Extension/scripts/utils-manager.js index c1bf6e4..e0f458a 100644 --- a/Extension/scripts/utils-manager.js +++ b/Extension/scripts/utils-manager.js @@ -1,6 +1,6 @@ /** * WPlace AutoBOT - Utils Manager - * Centralized utility functions for the WPlace automation system + * Centralized utility functions for the WPlace automation systemds */ class WPlaceUtilsManager { @@ -408,6 +408,16 @@ class WPlaceUtilsManager { } formatTime(ms) { + // Handle invalid or infinite values + if (!Number.isFinite(ms) || ms < 0) { + return '--:--:--'; + } + + // Handle very large values (more than 999 days) + if (ms > 999 * 24 * 60 * 60 * 1000) { + return '999d+'; + } + const seconds = Math.floor((ms / 1000) % 60); const minutes = Math.floor((ms / (1000 * 60)) % 60); const hours = Math.floor((ms / (1000 * 60 * 60)) % 24); @@ -423,15 +433,34 @@ class WPlaceUtilsManager { } calculateEstimatedTime(remainingPixels, charges, cooldown) { - if (remainingPixels <= 0) return 0; + // Safety checks for input parameters + if (!Number.isFinite(remainingPixels) || remainingPixels <= 0) return 0; + if (!Number.isFinite(charges) || charges <= 0) charges = 1; + if (!Number.isFinite(cooldown) || cooldown <= 0) cooldown = 30000; // Default 30s + + const paintingSpeed = window.state?.paintingSpeed || 5; + if (!Number.isFinite(paintingSpeed) || paintingSpeed <= 0) { + // Fallback calculation without painting speed + const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); + const timeFromCharges = cyclesNeeded * cooldown; + return Math.min(timeFromCharges, 999 * 24 * 60 * 60 * 1000); // Cap at 999 days + } - const paintingSpeedDelay = window.state.paintingSpeed > 0 ? 1000 / window.state.paintingSpeed : 1000; + const paintingSpeedDelay = 1000 / paintingSpeed; const timeFromSpeed = remainingPixels * paintingSpeedDelay; const cyclesNeeded = Math.ceil(remainingPixels / Math.max(charges, 1)); const timeFromCharges = cyclesNeeded * cooldown; - return timeFromSpeed + timeFromCharges; + const totalTime = timeFromSpeed + timeFromCharges; + + // Safety check to prevent infinity and cap at reasonable maximum + if (!Number.isFinite(totalTime) || totalTime < 0) { + return 0; + } + + // Cap at 999 days to prevent display issues + return Math.min(totalTime, 999 * 24 * 60 * 60 * 1000); } // Painted pixel tracking helpers @@ -472,6 +501,11 @@ class WPlaceUtilsManager { return false; } + // Alias for isPixelPainted - used in pre-filtering logic + isPixelMarkedPainted(x, y, regionX = 0, regionY = 0) { + return this.isPixelPainted(x, y, regionX, regionY); + } + // Smart save shouldAutoSave() { const now = Date.now(); diff --git a/Extension/themes/acrylic.css b/Extension/themes/acrylic.css index 10995a5..c096e97 100644 --- a/Extension/themes/acrylic.css +++ b/Extension/themes/acrylic.css @@ -627,3 +627,156 @@ border-radius: var(--wplace-radius); color: var(--wplace-text); } + +/* Account switching UI styles */ +.wplace-theme-acrylic .wplace-section { + margin: 15px 0; + border-radius: var(--wplace-radius); + background: rgba(255, 255, 255, 0.08); + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.15); + backdrop-filter: var(--wplace-backdrop); +} + +.wplace-theme-acrylic .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +.wplace-theme-acrylic .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-acrylic .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-acrylic .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.2); + transition: 0.4s; + border-radius: 24px; + backdrop-filter: blur(10px); +} + +.wplace-theme-acrylic .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.wplace-theme-acrylic .wplace-switch input:checked + .wplace-slider-round { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} + +.wplace-theme-acrylic .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +.wplace-theme-acrylic .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-acrylic .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.15); + transition: all 0.3s ease; + backdrop-filter: blur(10px); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-acrylic .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +}.wplace-theme-acrylic .wplace-account-item:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.3); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.wplace-theme-acrylic .wplace-account-item.is-current { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%); + border-color: #667eea; + box-shadow: 0 0 20px rgba(102, 126, 234, 0.4); +} + +/* Account details */ +.wplace-theme-acrylic .wplace-account-details { + flex: 1; + margin-left: 25px; +}.wplace-theme-acrylic .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 13px; + margin-bottom: 2px; +} + +/* Account statistics */ +.wplace-theme-acrylic .wplace-account-stats { + display: flex; + gap: 12px; + align-items: center; + font-size: 12px; + color: rgba(255, 255, 255, 0.75); +} + +.wplace-theme-acrylic .wplace-account-stats i { + margin-right: 4px; +} + +.wplace-theme-acrylic .wplace-account-stats .fas.fa-bolt { + color: #ffd700; +} + +.wplace-theme-acrylic .wplace-account-stats .fas.fa-tint { + color: #00bfff; +} diff --git a/Extension/themes/classic-light.css b/Extension/themes/classic-light.css index 09fd3ae..742b9d9 100644 --- a/Extension/themes/classic-light.css +++ b/Extension/themes/classic-light.css @@ -556,7 +556,7 @@ color: var(--wplace-text); padding: 4px 8px; font-size: 12px; - width: 50px; + width: 65px; text-align: center; transition: all 0.2s ease; height: 22px; @@ -617,6 +617,164 @@ border: 1px solid rgb(0 0 0 / 10%) !important; } +/* Account switching UI styles */ +.wplace-theme-classic-light .wplace-section { + margin: 15px 0; + border-radius: var(--wplace-radius); + background: rgba(0, 0, 0, 0.03); + padding: 15px; + border: 1px solid rgba(0, 0, 0, 0.1); +} + +.wplace-theme-classic-light .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +.wplace-theme-classic-light .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-classic-light .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-classic-light .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.4s; + border-radius: 24px; +} + +.wplace-theme-classic-light .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.wplace-theme-classic-light .wplace-switch input:checked + .wplace-slider-round { + background-color: #4facfe; +} + +.wplace-theme-classic-light .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +.wplace-theme-classic-light .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-classic-light .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 0, 0, 0.02); + border-radius: 8px; + border: 1px solid rgba(0, 0, 0, 0.08); + transition: all 0.3s ease; + position: relative; +} + +/* Account ordering number */ +.wplace-theme-classic-light .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: #4facfe; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.wplace-theme-classic-light .wplace-account-item:hover { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.wplace-theme-classic-light .wplace-account-item.is-current { + background: rgba(79, 172, 254, 0.1); + border-color: #4facfe; + box-shadow: 0 0 10px rgba(79, 172, 254, 0.2); +} + +/* Account details */ +.wplace-theme-classic-light .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-classic-light .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 12px; + margin-bottom: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-classic-light .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 11px; + color: rgba(0, 0, 0, 0.6); + flex-shrink: 0; +} + +.wplace-theme-classic-light .wplace-account-stats i { + margin-right: 4px; +} + +.wplace-theme-classic-light .wplace-account-stats .fas.fa-bolt { + color: #ff9800; +} + +.wplace-theme-classic-light .wplace-account-stats .fas.fa-tint { + color: #2196f3; +} + diff --git a/Extension/themes/classic.css b/Extension/themes/classic.css index 6db93a4..b20fd21 100644 --- a/Extension/themes/classic.css +++ b/Extension/themes/classic.css @@ -689,7 +689,7 @@ color: white; padding: 4px 8px; font-size: 12px; - width: 50px; + width: 65px; text-align: center; transition: all 0.2s ease; height: 22px; @@ -747,3 +747,177 @@ box-shadow: 0 0 20px rgb(0 0 0 / 50%); } +/* Account switching UI styles */ +:root .wplace-section, +.wplace-theme-classic .wplace-section { + margin: 15px 0; + border-radius: var(--wplace-radius); + background: rgba(255, 255, 255, 0.05); + padding: 15px; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +:root .wplace-section-title, +.wplace-theme-classic .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 14px; +} + +/* Auto-swap toggle switch */ +:root .wplace-switch, +.wplace-theme-classic .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +:root .wplace-switch input, +.wplace-theme-classic .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +:root .wplace-slider-round, +.wplace-theme-classic .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #444; + transition: 0.4s; + border-radius: 24px; +} + +:root .wplace-slider-round:before, +.wplace-theme-classic .wplace-slider-round:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} + +:root .wplace-switch input:checked + .wplace-slider-round, +.wplace-theme-classic .wplace-switch input:checked + .wplace-slider-round { + background: var(--wplace-slider-track-bg); +} + +:root .wplace-switch input:checked + .wplace-slider-round:before, +.wplace-theme-classic .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); +} + +/* Account list container */ +:root .accounts-list-container, +.wplace-theme-classic .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +:root .wplace-account-item, +.wplace-theme-classic .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + transition: all 0.3s ease; + position: relative; +} + +/* Account ordering number */ +:root .wplace-account-number, +.wplace-theme-classic .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 16px; + height: 16px; + background: var(--wplace-highlight); + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 9px; + font-weight: bold; + z-index: 1; +} + +:root .wplace-account-item:hover, +.wplace-theme-classic .wplace-account-item:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--wplace-highlight); +} + +:root .wplace-account-item.is-current, +.wplace-theme-classic .wplace-account-item.is-current { + background: rgba(79, 172, 254, 0.2); + border-color: #4facfe; + box-shadow: 0 0 10px rgba(79, 172, 254, 0.3); +} + +/* Account details */ +:root .wplace-account-details, +.wplace-theme-classic .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +:root .wplace-account-name, +.wplace-theme-classic .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 12px; + margin-bottom: 1px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +:root .wplace-account-stats, +.wplace-theme-classic .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 11px; + color: var(--wplace-muted-text); + flex-shrink: 0; +} + +:root .wplace-account-stats i, +.wplace-theme-classic .wplace-account-stats i { + margin-right: 4px; +} + +:root .wplace-account-stats .fas.fa-bolt, +.wplace-theme-classic .wplace-account-stats .fas.fa-bolt { + color: #ffd700; +} + +:root .wplace-account-stats .fas.fa-tint, +.wplace-theme-classic .wplace-account-stats .fas.fa-tint { + color: #00bfff; +} + diff --git a/Extension/themes/neon-cyan.css b/Extension/themes/neon-cyan.css index f41140d..33f4858 100644 --- a/Extension/themes/neon-cyan.css +++ b/Extension/themes/neon-cyan.css @@ -455,7 +455,7 @@ color: var(--wplace-text) !important; padding: 2px 4px; font-size: 10px; - width: 40px; + width: 65px; text-align: center; transition: all 0.2s ease; height: 18px; @@ -516,3 +516,188 @@ box-shadow: none !important; /* Optional: remove glows */ font-family: 'Press Start 2P', monospace !important; } + +/* Account switching UI styles */ +.wplace-theme-neon-cyan .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(0, 255, 255, 0.05); + padding: 15px; + border: 2px solid #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-cyan .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 8px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon-cyan .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon-cyan .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon-cyan .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #1a1a2e; + transition: 0.4s; + border-radius: 0; + border: 2px solid #333; + box-shadow: 0 0 10px rgba(0, 255, 255, 0.2); +} + +.wplace-theme-neon-cyan .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #fff; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(255, 255, 255, 0.5); +} + +.wplace-theme-neon-cyan .wplace-switch input:checked + .wplace-slider-round { + background-color: #00ffff; + box-shadow: 0 0 20px rgba(0, 255, 255, 0.6); +} + +.wplace-theme-neon-cyan .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + box-shadow: 0 0 12px rgba(0, 255, 255, 0.8); +} + +/* Account list container */ +.wplace-theme-neon-cyan .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon-cyan .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 255, 255, 0.03); + border-radius: 0; + border: 1px solid #00ffff; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(0, 255, 255, 0.2); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon-cyan .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8x; + height: 8px; + background: #00ffff; + color: #1a1a2e; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #00ffff; + box-shadow: 0 0 8px rgba(0, 255, 255, 0.6); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-cyan .wplace-account-item:hover { + background: rgba(0, 255, 255, 0.08); + border-color: #00ffff; + box-shadow: 0 0 15px rgba(0, 255, 255, 0.4); + transform: translateX(2px); +} + +.wplace-theme-neon-cyan .wplace-account-item.is-current { + background: rgba(0, 255, 255, 0.15); + border-color: #00ffff; + box-shadow: 0 0 25px rgba(0, 255, 255, 0.6); + animation: neon-cyan-pulse 2s infinite; +} + +@keyframes neon-cyan-pulse { + 0%, 100% { box-shadow: 0 0 25px rgba(0, 255, 255, 0.6); } + 50% { box-shadow: 0 0 35px rgba(0, 255, 255, 0.8); } +} + +/* Account details */ +.wplace-theme-neon-cyan .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon-cyan .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon-cyan .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(0, 255, 255, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon-cyan .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 5px currentColor; +} + +.wplace-theme-neon-cyan .wplace-account-stats .fas.fa-bolt { + color: #ffff00; + text-shadow: 0 0 8px #ffff00; +} + +.wplace-theme-neon-cyan .wplace-account-stats .fas.fa-tint { + color: #00ffff; + text-shadow: 0 0 8px #00ffff; +} diff --git a/Extension/themes/neon-light.css b/Extension/themes/neon-light.css index e7c662c..f5debd0 100644 --- a/Extension/themes/neon-light.css +++ b/Extension/themes/neon-light.css @@ -455,7 +455,7 @@ color: var(--wplace-text) !important; padding: 2px 4px; font-size: 10px; - width: 40px; + width: 65px; text-align: center; transition: all 0.2s ease; height: 18px; @@ -517,3 +517,189 @@ box-shadow: none !important; /* Optional: remove glows */ font-family: 'Press Start 2P', monospace !important; } + +/* Account switching UI styles */ +.wplace-theme-neon-light .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(32, 60, 93, 0.05); + padding: 15px; + border: 2px solid #203C5D; + box-shadow: 0 0 20px rgba(32, 60, 93, 0.2); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon-light .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon-light .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon-light .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #f0f0f0; + transition: 0.4s; + border-radius: 0; + border: 2px solid #ccc; + box-shadow: 0 0 8px rgba(32, 60, 93, 0.15); +} + +.wplace-theme-neon-light .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #203C5D; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(32, 60, 93, 0.3); +} + +.wplace-theme-neon-light .wplace-switch input:checked + .wplace-slider-round { + background-color: #203C5D; + box-shadow: 0 0 15px rgba(32, 60, 93, 0.4); +} + +.wplace-theme-neon-light .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + background-color: #fff; + box-shadow: 0 0 10px rgba(32, 60, 93, 0.5); +} + +/* Account list container */ +.wplace-theme-neon-light .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon-light .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(32, 60, 93, 0.03); + border-radius: 0; + border: 1px solid #203C5D; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(32, 60, 93, 0.1); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon-light .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8px; + height: 8px; + background: #203C5D; + color: white; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #203C5D; + box-shadow: 0 0 6px rgba(32, 60, 93, 0.4); + font-family: var(--wplace-font); +} + +.wplace-theme-neon-light .wplace-account-item:hover { + background: rgba(32, 60, 93, 0.08); + border-color: #203C5D; + box-shadow: 0 0 12px rgba(32, 60, 93, 0.2); + transform: translateX(2px); +} + +.wplace-theme-neon-light .wplace-account-item.is-current { + background: rgba(32, 60, 93, 0.15); + border-color: #203C5D; + box-shadow: 0 0 20px rgba(32, 60, 93, 0.3); + animation: neon-light-pulse 2s infinite; +} + +@keyframes neon-light-pulse { + 0%, 100% { box-shadow: 0 0 20px rgba(32, 60, 93, 0.3); } + 50% { box-shadow: 0 0 25px rgba(32, 60, 93, 0.4); } +} + +/* Account details */ +.wplace-theme-neon-light .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon-light .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 3px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon-light .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(32, 60, 93, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon-light .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 3px currentColor; +} + +.wplace-theme-neon-light .wplace-account-stats .fas.fa-bolt { + color: #ff6b35; + text-shadow: 0 0 5px #ff6b35; +} + +.wplace-theme-neon-light .wplace-account-stats .fas.fa-tint { + color: #203C5D; + text-shadow: 0 0 5px #203C5D; +} diff --git a/Extension/themes/neon.css b/Extension/themes/neon.css index 184cb64..a5f0e4c 100644 --- a/Extension/themes/neon.css +++ b/Extension/themes/neon.css @@ -571,7 +571,7 @@ color: var(--wplace-text) !important; padding: 2px 4px; font-size: 10px; - width: 40px; + width: 65px; text-align: center; transition: all 0.2s ease; height: 18px; @@ -633,3 +633,188 @@ font-family: 'Press Start 2P', monospace !important; } +/* Account switching UI styles */ +.wplace-theme-neon .wplace-section { + margin: 15px 0; + border-radius: 0; + background: rgba(0, 255, 65, 0.05); + padding: 15px; + border: 2px solid #00ff41; + box-shadow: 0 0 20px rgba(0, 255, 65, 0.3); + font-family: var(--wplace-font); +} + +.wplace-theme-neon .wplace-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--wplace-text); + margin-bottom: 10px; + font-size: 12px; + text-transform: uppercase; + text-shadow: 0 0 8px var(--wplace-text); + font-family: var(--wplace-font); +} + +/* Auto-swap toggle switch */ +.wplace-theme-neon .wplace-switch { + position: relative; + display: inline-block; + width: 50px; + height: 24px; +} + +.wplace-theme-neon .wplace-switch input { + opacity: 0; + width: 0; + height: 0; +} + +.wplace-theme-neon .wplace-slider-round { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #1a1a2e; + transition: 0.4s; + border-radius: 0; + border: 2px solid #333; + box-shadow: 0 0 10px rgba(0, 255, 65, 0.2); +} + +.wplace-theme-neon .wplace-slider-round:before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #fff; + transition: 0.4s; + border-radius: 0; + box-shadow: 0 0 8px rgba(255, 255, 255, 0.5); +} + +.wplace-theme-neon .wplace-switch input:checked + .wplace-slider-round { + background-color: #00ff41; + box-shadow: 0 0 20px rgba(0, 255, 65, 0.6); +} + +.wplace-theme-neon .wplace-switch input:checked + .wplace-slider-round:before { + transform: translateX(26px); + box-shadow: 0 0 12px rgba(0, 255, 65, 0.8); +} + +/* Account list container */ +.wplace-theme-neon .accounts-list-container { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + padding: 0 6px; +} + +/* Individual account item */ +.wplace-theme-neon .wplace-account-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 8px; + margin: 4px 0; + background: rgba(0, 255, 65, 0.03); + border-radius: 0; + border: 1px solid #00ff41; + transition: all 0.3s ease; + box-shadow: 0 0 5px rgba(0, 255, 65, 0.2); + position: relative; +} + +/* Account ordering number */ +.wplace-theme-neon .wplace-account-number { + position: absolute; + top: 3px; + left: 3px; + width: 8px; + height: 8px; + background: #00ff41; + color: #1a1a2e; + border-radius: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: 8px; + font-weight: bold; + z-index: 1; + border: 1px solid #00ff41; + box-shadow: 0 0 8px rgba(0, 255, 65, 0.6); + font-family: var(--wplace-font); +} + +.wplace-theme-neon .wplace-account-item:hover { + background: rgba(0, 255, 65, 0.08); + border-color: #00ff41; + box-shadow: 0 0 15px rgba(0, 255, 65, 0.4); + transform: translateX(2px); +} + +.wplace-theme-neon .wplace-account-item.is-current { + background: rgba(0, 255, 65, 0.15); + border-color: #00ff41; + box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); + animation: neon-pulse 2s infinite; +} + +@keyframes neon-pulse { + 0%, 100% { box-shadow: 0 0 25px rgba(0, 255, 65, 0.6); } + 50% { box-shadow: 0 0 35px rgba(0, 255, 65, 0.8); } +} + +/* Account details */ +.wplace-theme-neon .wplace-account-details { + flex: 1; + margin-left: 20px; + min-width: 0; +} + +.wplace-theme-neon .wplace-account-name { + font-weight: 600; + color: var(--wplace-text); + font-size: 8px; + margin-bottom: 1px; + text-transform: uppercase; + text-shadow: 0 0 5px var(--wplace-text); + font-family: var(--wplace-font); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Account statistics */ +.wplace-theme-neon .wplace-account-stats { + display: flex; + gap: 8px; + align-items: center; + font-size: 8px; + color: rgba(0, 255, 65, 0.8); + text-transform: uppercase; + font-family: var(--wplace-font); + flex-shrink: 0; +} + +.wplace-theme-neon .wplace-account-stats i { + margin-right: 4px; + text-shadow: 0 0 5px currentColor; +} + +.wplace-theme-neon .wplace-account-stats .fas.fa-bolt { + color: #ffff00; + text-shadow: 0 0 8px #ffff00; +} + +.wplace-theme-neon .wplace-account-stats .fas.fa-tint { + color: #00bfff; + text-shadow: 0 0 8px #00bfff; +} +