From 58db3cb9820ebc02002b8bf0af76e68e314eff02 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:28:25 +0200 Subject: [PATCH 01/15] css --- themes/classic-light.css | 54 ++++++++++++++++++++++++++++++++++++++++ themes/classic.css | 4 +-- themes/neon.css | 26 ++++++++++++++++++- 3 files changed, 81 insertions(+), 3 deletions(-) diff --git a/themes/classic-light.css b/themes/classic-light.css index 3ad47b7..4f36c93 100644 --- a/themes/classic-light.css +++ b/themes/classic-light.css @@ -106,6 +106,18 @@ color: var(--wplace-text) !important; } +/* Data management section buttons */ +.wplace-theme--classic-light .wplace-data-management .wplace-btn { + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; + border: 1px solid rgba(0, 0, 0, 0.2) !important; + color: var(--wplace-text) !important; +} + +.wplace-theme--classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { + background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%) !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; +} + /* Settings dialog */ .wplace-theme--classic-light #wplace-settings-container { background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%) !important; @@ -295,6 +307,17 @@ box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.3) !important; } +/* Dropdown menu options */ +.wplace-theme--classic-light .wplace-settings-select option { + background: #ffffff !important; + color: var(--wplace-text) !important; +} + +.wplace-theme--classic-light .wplace-settings-option { + background: #ffffff !important; + color: var(--wplace-text) !important; +} + /* Sliders with classic gradient */ .wplace-theme--classic-light .wplace-speed-slider { background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; @@ -306,6 +329,11 @@ border-radius: 4px !important; } +.wplace-theme--classic-light .wplace-slider { + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%) !important; + border-radius: 4px !important; +} + .wplace-theme--classic-light .wplace-speed-value { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important; border-radius: 8px !important; @@ -342,6 +370,32 @@ } } +/* Turnstile/CF checkbox overlay - CRITICAL FIX */ +.wplace-theme--classic-light .wplace-turnstile-overlay { + background: rgba(255, 255, 255, 0.98) !important; + border-radius: 12px !important; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3) !important; + backdrop-filter: blur(10px) !important; + border: 1px solid rgba(0, 0, 0, 0.2) !important; + color: var(--wplace-text) !important; + font-family: 'Segoe UI', Roboto, sans-serif !important; +} + +.wplace-theme--classic-light .wplace-turnstile-title { + color: var(--wplace-text) !important; +} + +.wplace-theme--classic-light .wplace-turnstile-hide-btn { + color: var(--wplace-text) !important; + border: 1px solid rgba(0, 0, 0, 0.2) !important; + border-radius: 6px !important; + background: rgba(0, 0, 0, 0.05) !important; +} + +.wplace-theme--classic-light .wplace-turnstile-hide-btn:hover { + background: rgba(0, 0, 0, 0.1) !important; +} + /* Color swatches with light styling */ .wplace-theme--classic-light .wplace-color-swatch { border: 1px solid rgba(0, 0, 0, 0.2) !important; diff --git a/themes/classic.css b/themes/classic.css index cd1a45b..9d19a9e 100644 --- a/themes/classic.css +++ b/themes/classic.css @@ -492,8 +492,8 @@ /* Form controls and sliders for classic theme */ :root .wplace-slider, .wplace-theme--classic .wplace-slider { - background: #444; - border-radius: 2px; + background: linear-gradient(to right, #4facfe 0%, #00f2fe 100%); + border-radius: 4px; } :root .wplace-slider::-webkit-slider-thumb, diff --git a/themes/neon.css b/themes/neon.css index 2676c14..ee67493 100644 --- a/themes/neon.css +++ b/themes/neon.css @@ -285,6 +285,16 @@ text-shadow: 0 0 5px var(--wplace-text) !important; } +/* Neon slider styling */ +.wplace-theme--neon .wplace-slider, +.wplace-theme--neon .wplace-speed-slider, +.wplace-theme--neon .wplace-overlay-opacity-slider { + background: var(--wplace-secondary) !important; + border: 1px solid var(--wplace-text) !important; + border-radius: 0 !important; + box-shadow: 0 0 10px rgba(0, 255, 65, 0.3) !important; +} + /* Scanline animation for neon theme */ .wplace-theme--neon #wplace-image-bot-container::before { content: ''; @@ -300,6 +310,11 @@ opacity: 0.7; } +.wplace-theme--neon #wplace-stats-container { + position: relative; + overflow: hidden !important; +} + .wplace-theme--neon #wplace-stats-container::before { content: ''; position: absolute; @@ -310,7 +325,7 @@ background: linear-gradient(90deg, transparent, var(--wplace-text), transparent); z-index: 1; pointer-events: none; - animation: scanline 4s linear infinite; + animation: scanlineStats 4s linear infinite; opacity: 0.7; } @@ -347,6 +362,15 @@ } } +@keyframes scanlineStats { + 0% { + transform: translateY(-100%); + } + 100% { + transform: translateY(300px); + } +} + /* Resize dialog styling for neon theme */ .wplace-theme--neon .resize-container { background: #1a1a2e !important; From d230e515022d7e3ad480008c8104110d7c127e14 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:58:40 +0200 Subject: [PATCH 02/15] css --- themes/classic-light.css | 15 ++++++++++++++- themes/classic.css | 7 +++++++ themes/neon.css | 24 +++++++++++++++++++++++- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/themes/classic-light.css b/themes/classic-light.css index 4f36c93..2e58279 100644 --- a/themes/classic-light.css +++ b/themes/classic-light.css @@ -110,7 +110,8 @@ .wplace-theme--classic-light .wplace-data-management .wplace-btn { background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important; border: 1px solid rgba(0, 0, 0, 0.2) !important; - color: var(--wplace-text) !important; + color: #212529 !important; + font-weight: 500 !important; } .wplace-theme--classic-light .wplace-data-management .wplace-btn:hover:not(:disabled) { @@ -334,6 +335,18 @@ border-radius: 4px !important; } +.wplace-theme--classic-light .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + border: 1px solid rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + .wplace-theme--classic-light .wplace-speed-value { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%) !important; border-radius: 8px !important; diff --git a/themes/classic.css b/themes/classic.css index 9d19a9e..564d2cd 100644 --- a/themes/classic.css +++ b/themes/classic.css @@ -498,7 +498,14 @@ :root .wplace-slider::-webkit-slider-thumb, .wplace-theme--classic .wplace-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; border-radius: 50%; + background: var(--wplace-slider-thumb-bg); + border: none; + cursor: pointer; + transition: all 0.2s ease; } :root .wplace-speed-slider, diff --git a/themes/neon.css b/themes/neon.css index ee67493..af3a0d8 100644 --- a/themes/neon.css +++ b/themes/neon.css @@ -295,6 +295,27 @@ box-shadow: 0 0 10px rgba(0, 255, 65, 0.3) !important; } +.wplace-theme--neon .wplace-slider::-webkit-slider-thumb, +.wplace-theme--neon .wplace-speed-slider::-webkit-slider-thumb, +.wplace-theme--neon .wplace-overlay-opacity-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 0 !important; + background: var(--wplace-slider-thumb-bg) !important; + border: 2px solid var(--wplace-text) !important; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 0 8px rgba(0, 255, 65, 0.5) !important; +} + +.wplace-theme--neon .wplace-slider::-webkit-slider-thumb:hover, +.wplace-theme--neon .wplace-speed-slider::-webkit-slider-thumb:hover, +.wplace-theme--neon .wplace-overlay-opacity-slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 0 15px var(--wplace-text) !important; +} + /* Scanline animation for neon theme */ .wplace-theme--neon #wplace-image-bot-container::before { content: ''; @@ -311,8 +332,9 @@ } .wplace-theme--neon #wplace-stats-container { - position: relative; + position: fixed !important; overflow: hidden !important; + z-index: 9998 !important; } .wplace-theme--neon #wplace-stats-container::before { From a657bd6b2b37f67d157e5ca4125f725a178767ef Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:15:50 +0200 Subject: [PATCH 03/15] css --- themes/classic-light.css | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/themes/classic-light.css b/themes/classic-light.css index 2e58279..01db6ad 100644 --- a/themes/classic-light.css +++ b/themes/classic-light.css @@ -92,10 +92,7 @@ color: var(--wplace-text) !important; } -/* Comprehensive text and element styling for light theme */ -.wplace-theme--classic-light * { - color: var(--wplace-text) !important; -} +/* Comprehensive text and element styling for light theme - scoped to bot containers only */ .wplace-theme--classic-light .wplace-status, .wplace-theme--classic-light .wplace-stats, @@ -268,8 +265,13 @@ text-shadow: none !important; } +.wplace-theme--classic-light .wplace-stat-label { + color: white !important; + text-shadow: none !important; +} + .wplace-theme--classic-light .wplace-stat-value { - color: var(--wplace-highlight) !important; + color: white !important; text-shadow: none !important; font-weight: 600 !important; } From 5e13c567275475f410f6d306c2a90d368f72b287 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:12:03 +0200 Subject: [PATCH 04/15] test commit to check if ci gets triggered when i push another branch --- themes/neon.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/neon.css b/themes/neon.css index af3a0d8..3b69283 100644 --- a/themes/neon.css +++ b/themes/neon.css @@ -1,5 +1,5 @@ /* WPlace Auto-Image Bot - Neon Theme */ -/* Retro cyberpunk aesthetic with green neon glow effects */ +/* Retro cyberpunk aesthetic with green neon glow effects :) */ .wplace-theme--neon { /* Neon theme colors - matching upstream main neon theme */ From be340b799caed0c4c046f826765e8e6ab1d08d03 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:27:25 +0200 Subject: [PATCH 05/15] cd should run --- themes/neon.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/themes/neon.css b/themes/neon.css index 3b69283..af3a0d8 100644 --- a/themes/neon.css +++ b/themes/neon.css @@ -1,5 +1,5 @@ /* WPlace Auto-Image Bot - Neon Theme */ -/* Retro cyberpunk aesthetic with green neon glow effects :) */ +/* Retro cyberpunk aesthetic with green neon glow effects */ .wplace-theme--neon { /* Neon theme colors - matching upstream main neon theme */ From 9165c5b7509f89c527d5362ec81acecb29c69452 Mon Sep 17 00:00:00 2001 From: Staninna <26458805+Staninna@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:24:59 +0200 Subject: [PATCH 06/15] Are we revived? Restored all the mysteriously disappeared code --- Auto-Image.js | 2801 +++++++++++------------------------------ auto-image-styles.css | 1966 +++++++++++++++++++++++++++++ 2 files changed, 2715 insertions(+), 2052 deletions(-) create mode 100644 auto-image-styles.css diff --git a/Auto-Image.js b/Auto-Image.js index c967534..546cb66 100644 --- a/Auto-Image.js +++ b/Auto-Image.js @@ -21,10 +21,10 @@ COOLDOWN_CHARGE_THRESHOLD: 1, // Default wait threshold // Desktop Notifications (defaults) NOTIFICATIONS: { - ENABLED: true, - ON_CHARGES_REACHED: true, - ONLY_WHEN_UNFOCUSED: true, - REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds + ENABLED: true, + ON_CHARGES_REACHED: true, + ONLY_WHEN_UNFOCUSED: true, + REPEAT_MINUTES: 5, // repeat reminder while threshold condition holds }, OVERLAY: { OPACITY_DEFAULT: 0.6, @@ -146,6 +146,27 @@ pixelBlink: false, }, }, + "Classic Light": { + primary: "#ffffff", + secondary: "#f8f9fa", + accent: "#e9ecef", + text: "#212529", + highlight: "#6f42c1", + success: "#28a745", + error: "#dc3545", + warning: "#ffc107", + fontFamily: "'Segoe UI', Roboto, sans-serif", + borderRadius: "12px", + borderStyle: "solid", + borderWidth: "1px", + boxShadow: "0 8px 32px rgba(0,0,0,0.15), 0 0 0 1px rgba(0,0,0,0.08)", + backdropFilter: "blur(10px)", + animations: { + glow: false, + scanline: false, + pixelBlink: false, + }, + }, "Neon Retro": { primary: "#1a1a2e", secondary: "#16213e", @@ -177,20 +198,56 @@ const getCurrentTheme = () => CONFIG.THEMES[CONFIG.currentTheme] const switchTheme = (themeName) => { - if (CONFIG.THEMES[themeName]) { - CONFIG.currentTheme = themeName - saveThemePreference() + if (CONFIG.THEMES[themeName]) { + CONFIG.currentTheme = themeName + saveThemePreference() - // Remove existing theme styles - const existingStyle = document.querySelector('style[data-wplace-theme="true"]') - if (existingStyle) { - existingStyle.remove() - } + // APPLY THEME VARS/CLASS (new) + applyTheme() - // Recreate UI with new theme (cleanup is handled in createUI) - createUI() - } + // Recreate UI (kept for now) + createUI() } +} + +// Add this helper (place it after getCurrentTheme/switchTheme definitions) +function applyTheme() { + const theme = getCurrentTheme(); + // Toggle theme class on documentElement so CSS vars cascade to our UI + document.documentElement.classList.remove('wplace-theme--classic', 'wplace-theme--classic-light', 'wplace-theme--neon'); + + let themeClass = 'wplace-theme--classic'; // default + if (CONFIG.currentTheme === 'Neon Retro') { + themeClass = 'wplace-theme--neon'; + } else if (CONFIG.currentTheme === 'Classic Light') { + themeClass = 'wplace-theme--classic-light'; + } + + document.documentElement.classList.add(themeClass); + + // Also set CSS variables explicitly in case you want runtime overrides + const root = document.documentElement; + const setVar = (k, v) => { try { root.style.setProperty(k, v); } catch {} }; + + setVar('--wplace-primary', theme.primary); + setVar('--wplace-secondary', theme.secondary); + setVar('--wplace-accent', theme.accent); + setVar('--wplace-text', theme.text); + setVar('--wplace-highlight', theme.highlight); + setVar('--wplace-success', theme.success); + setVar('--wplace-error', theme.error); + setVar('--wplace-warning', theme.warning); + + // Typography + look + setVar('--wplace-font', theme.fontFamily || "'Segoe UI', Roboto, sans-serif"); + setVar('--wplace-radius', ('' + (theme.borderRadius || '12px'))); + setVar('--wplace-border-style', (('' + (theme.borderStyle || 'solid')))); + setVar('--wplace-border-width', (('' + (theme.borderWidth || '1px')))); + setVar('--wplace-backdrop', (('' + (theme.backdropFilter || 'blur(10px)')))); + setVar('--wplace-border-color', 'rgba(255,255,255,0.1)'); +} + + const saveThemePreference = () => { try { @@ -212,19 +269,13 @@ } const loadLanguagePreference = () => { - const savedLanguage = localStorage.getItem("wplace_language") - - const browserLocale = navigator.language - const browserLanguage = browserLocale.split("-")[0] - - if (TEXT[savedLanguage]) { // trying to load saved language - state.language = savedLanguage - } else if (TEXT[browserLocale]) { // trying to load full locale match (e.g. "zh-CN", "zh-TW" etc) - state.language = browserLocale - localStorage.setItem("wplace_language", browserLocale) - } else if (TEXT[browserLanguage]) { // trying to load base language match (e.g. "en" for "en-US" or "en-GB" etc) - state.language = browserLanguage - localStorage.setItem("wplace_language", browserLanguage) + try { + const saved = localStorage.getItem("wplace_language") + if (saved && TEXT[saved]) { + state.language = saved + } + } catch (e) { + console.warn("Could not load language preference:", e) } } @@ -740,7 +791,7 @@ automation: "Otomasyon", noChargesThreshold: "⌛ Hakların {threshold} seviyesine ulaşması bekleniyor. Şu anda {current}. Sonraki {time} içinde...", }, - "zh-CN": { + zh: { title: "WPlace 自动图像", toggleOverlay: "切换覆盖层", scanColors: "扫描颜色", @@ -816,7 +867,7 @@ automation: "自动化", noChargesThreshold: "⌛ 等待次数达到 {threshold}。当前 {current}。下次在 {time}...", }, - "zh-TW": { + "zh-tw": { title: "WPlace 自動圖像", toggleOverlay: "切換覆蓋層", scanColors: "掃描顏色", @@ -1152,28 +1203,28 @@ initialSetupComplete: false, // Track if initial startup setup is complete (only happens once) overlayOpacity: CONFIG.OVERLAY.OPACITY_DEFAULT, blueMarbleEnabled: CONFIG.OVERLAY.BLUE_MARBLE_DEFAULT, - ditheringEnabled: true, - // Advanced color matching settings - colorMatchingAlgorithm: 'lab', - enableChromaPenalty: true, - chromaPenaltyWeight: 0.15, - customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, - customWhiteThreshold: CONFIG.WHITE_THRESHOLD, - resizeSettings: null, - originalImage: null, - resizeIgnoreMask: null, - // Notification prefs and runtime bookkeeping - notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, - notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, - notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, - notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, - _lastChargesNotifyAt: 0, - _lastChargesBelow: true, - // Smart save tracking - _lastSavePixelCount: 0, - _lastSaveTime: 0, - _saveInProgress: false, - paintedMap: null, + ditheringEnabled: true, + // Advanced color matching settings + colorMatchingAlgorithm: 'lab', + enableChromaPenalty: true, + chromaPenaltyWeight: 0.15, + customTransparencyThreshold: CONFIG.TRANSPARENCY_THRESHOLD, + customWhiteThreshold: CONFIG.WHITE_THRESHOLD, + resizeSettings: null, + originalImage: null, + resizeIgnoreMask: null, + // Notification prefs and runtime bookkeeping + notificationsEnabled: CONFIG.NOTIFICATIONS.ENABLED, + notifyOnChargesReached: CONFIG.NOTIFICATIONS.ON_CHARGES_REACHED, + notifyOnlyWhenUnfocused: CONFIG.NOTIFICATIONS.ONLY_WHEN_UNFOCUSED, + notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES, + _lastChargesNotifyAt: 0, + _lastChargesBelow: true, + // Smart save tracking + _lastSavePixelCount: 0, + _lastSaveTime: 0, + _saveInProgress: false, + paintedMap: null, } let _updateResizePreview = () => { }; @@ -1206,8 +1257,8 @@ this.disable(); this.imageBitmap = null; this.chunkedTiles.clear(); - this.originalTiles.clear(); - this.originalTilesData.clear(); + this.originalTiles.clear(); + this.originalTilesData.clear(); this.lastProcessedHash = null; if (this.processPromise) { this.processPromise = null; @@ -1274,7 +1325,7 @@ async _doProcessImageIntoChunks() { const startTime = performance.now(); this.chunkedTiles.clear(); - + const { width: imageWidth, height: imageHeight } = this.imageBitmap; const { x: startPixelX, y: startPixelY } = this.startCoords.pixel; const { x: startRegionX, y: startRegionY } = this.startCoords.region; @@ -1303,7 +1354,7 @@ // Process tiles in batches with yielding for (let i = 0; i < tilesToProcess.length; i += batchSize) { const batch = tilesToProcess.slice(i, i + batchSize); - + await Promise.all(batch.map(async ({ tx, ty }) => { const tileKey = `${tx},${ty}`; const chunkBitmap = await this._processTile(tx, ty, imageWidth, imageHeight, startPixelX, startPixelY, startRegionX, startRegionY); @@ -1351,18 +1402,18 @@ if (state.blueMarbleEnabled) { const imageData = chunkCtx.getImageData(dX, dY, sW, sH); const data = imageData.data; - + // Faster pixel manipulation using typed arrays for (let i = 0; i < data.length; i += 4) { const pixelIndex = i / 4; const pixelY = Math.floor(pixelIndex / sW); const pixelX = pixelIndex % sW; - + if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) { data[i + 3] = 0; // Set alpha to 0 } } - + chunkCtx.putImageData(imageData, dX, dY); } @@ -1491,7 +1542,7 @@ const originalBitmap = await createImageBitmap(originalBlob); const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height); const ctx = canvas.getContext('2d'); - + // Disable antialiasing for pixel-perfect rendering ctx.imageSmoothingEnabled = false; @@ -1504,7 +1555,7 @@ ctx.drawImage(overlayBitmap, 0, 0); // Use faster blob conversion with compression settings - return await canvas.convertToBlob({ + return await canvas.convertToBlob({ type: 'image/png', quality: 0.95 // Slight compression for faster processing }); @@ -1561,7 +1612,7 @@ } tokenGenerationInProgress = true; - + try { console.log("🔄 Token expired or missing, generating new one..."); const token = await handleCaptchaWithRetry(); @@ -1700,6 +1751,18 @@ } }); + async function detectLanguage() { + try { + const response = await fetch("https://backend.wplace.live/me", { + credentials: "include", + }) + const data = await response.json() + state.language = data.language === "pt" ? "pt" : "en" + } catch { + state.language = navigator.language.startsWith("pt") ? "pt" : "en" + } + } + // UTILITY FUNCTIONS const Utils = { sleep: (ms) => new Promise((r) => setTimeout(r, ms)), @@ -1727,7 +1790,7 @@ this.turnstileLoaded = true; return Promise.resolve(); } - + return new Promise((resolve, reject) => { // Avoid adding the script twice if (document.querySelector('script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]')) { @@ -1741,7 +1804,7 @@ }; return checkReady(); } - + const script = document.createElement('script'); script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; script.async = true; @@ -1766,20 +1829,9 @@ if (this._turnstileContainer) { this._turnstileContainer.remove(); } - + this._turnstileContainer = document.createElement('div'); - this._turnstileContainer.style.cssText = ` - position: fixed !important; - left: -99999px !important; - top: -99999px !important; - width: 1px !important; - height: 1px !important; - pointer-events: none !important; - opacity: 0 !important; - visibility: hidden !important; - z-index: -99999 !important; - overflow: hidden !important; - `; + this._turnstileContainer.className = 'wplace-turnstile-hidden'; this._turnstileContainer.setAttribute('aria-hidden', 'true'); this._turnstileContainer.id = 'turnstile-widget-container'; document.body.appendChild(this._turnstileContainer); @@ -1795,35 +1847,19 @@ const overlay = document.createElement('div'); overlay.id = 'turnstile-overlay-container'; - overlay.style.cssText = ` - position: fixed !important; - bottom: 20px !important; - right: 20px !important; - z-index: 99999 !important; - background: rgba(0,0,0,0.9) !important; - border-radius: 12px !important; - padding: 20px !important; - box-shadow: 0 8px 32px rgba(0,0,0,0.4) !important; - backdrop-filter: blur(10px) !important; - border: 1px solid rgba(255,255,255,0.2) !important; - color: white !important; - font-family: 'Segoe UI', sans-serif !important; - display: none !important; - max-width: 350px !important; - min-width: 300px !important; - `; + overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden'; const title = document.createElement('div'); title.textContent = 'Cloudflare Turnstile — please complete the check if shown'; - title.style.cssText = 'font: 600 12px/1.3 "Segoe UI",sans-serif; margin-bottom: 8px; opacity: 0.9;'; + title.className = 'wplace-turnstile-title'; const host = document.createElement('div'); host.id = 'turnstile-overlay-host'; - host.style.cssText = 'width: 100%; min-height: 70px;'; + host.className = 'wplace-turnstile-host'; const hideBtn = document.createElement('button'); hideBtn.textContent = 'Hide'; - hideBtn.style.cssText = 'position:absolute; top:6px; right:6px; font-size:11px; background:transparent; color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; padding:2px 6px; cursor:pointer;'; + hideBtn.className = 'wplace-turnstile-hide-btn'; hideBtn.addEventListener('click', () => overlay.remove()); overlay.appendChild(title); @@ -1870,24 +1906,24 @@ try { // Force cleanup of any existing widget if (this._turnstileWidgetId && window.turnstile?.remove) { - try { - window.turnstile.remove(this._turnstileWidgetId); + try { + window.turnstile.remove(this._turnstileWidgetId); console.log('🧹 Cleaned up existing Turnstile widget'); } catch (e) { console.warn('⚠️ Widget cleanup warning:', e.message); } } - + const container = this.ensureTurnstileContainer(); container.innerHTML = ''; - + // Verify Turnstile is available if (!window.turnstile?.render) { console.error('❌ Turnstile not available for rendering'); resolve(null); return; } - + console.log('🔧 Creating invisible Turnstile widget...'); const widgetId = window.turnstile.render(container, { sitekey, @@ -1902,10 +1938,10 @@ 'error-callback': () => resolve(null), 'timeout-callback': () => resolve(null) }); - + this._turnstileWidgetId = widgetId; this._lastSitekey = sitekey; - + if (!widgetId) { return resolve(null); } @@ -1925,31 +1961,33 @@ async createTurnstileWidgetInteractive(sitekey, action) { // Create a visible widget that users can interact with if needed console.log('🔄 Creating interactive Turnstile widget (visible)'); - + return new Promise((resolve) => { try { // Force cleanup of any existing widget if (this._turnstileWidgetId && window.turnstile?.remove) { - try { - window.turnstile.remove(this._turnstileWidgetId); + try { + window.turnstile.remove(this._turnstileWidgetId); } catch (e) { console.warn('⚠️ Widget cleanup warning:', e.message); } } - + const overlay = this.ensureTurnstileOverlayContainer(); + overlay.classList.remove('wplace-overlay-hidden'); overlay.style.display = 'block'; - + const host = overlay.querySelector('#turnstile-overlay-host'); host.innerHTML = ''; - + // Set a timeout for interactive mode const timeout = setTimeout(() => { console.warn('⏰ Interactive Turnstile widget timeout'); + overlay.classList.add('wplace-overlay-hidden'); overlay.style.display = 'none'; resolve(null); }, 60000); // 60 seconds for user interaction - + const widgetId = window.turnstile.render(host, { sitekey, action, @@ -1957,9 +1995,10 @@ theme: 'light', callback: (token) => { clearTimeout(timeout); - overlay.style.display = 'none'; + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; console.log('✅ Interactive Turnstile completed successfully'); - + if (typeof token === 'string' && token.length > 20) { resolve(token); } else { @@ -1969,17 +2008,19 @@ }, 'error-callback': (error) => { clearTimeout(timeout); - overlay.style.display = 'none'; + overlay.classList.add('wplace-overlay-hidden'); + overlay.style.display = 'none'; console.warn('❌ Interactive Turnstile error:', error); resolve(null); }, }); - + this._turnstileWidgetId = widgetId; this._lastSitekey = sitekey; - + if (!widgetId) { clearTimeout(timeout); + overlay.classList.add('wplace-overlay-hidden'); overlay.style.display = 'none'; console.warn('❌ Failed to create interactive Turnstile widget'); resolve(null); @@ -2006,15 +2047,15 @@ console.warn('Failed to cleanup Turnstile widget:', e); } } - + if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) { this._turnstileContainer.remove(); } - + if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) { this._turnstileOverlay.remove(); } - + this._turnstileWidgetId = null; this._turnstileContainer = null; this._turnstileOverlay = null; @@ -2092,11 +2133,11 @@ this._cachedSitekey = testSitekey; return testSitekey; } - + } catch (error) { console.warn('Error detecting sitekey:', error); } - + console.log("🔍 Using fallback sitekey:", fallback); this._cachedSitekey = fallback; return fallback; @@ -2152,40 +2193,7 @@ showAlert: (message, type = "info") => { const alertDiv = document.createElement("div") - alertDiv.style.cssText = ` - position: fixed; - top: 20px; - left: 50%; - transform: translateX(-50%); - padding: 12px 20px; - border-radius: 8px; - color: white; - font-weight: 600; - z-index: 10001; - max-width: 400px; - text-align: center; - box-shadow: 0 4px 12px rgba(0,0,0,0.3); - animation: slideDown 0.3s ease-out; - font-family: 'Segoe UI', sans-serif; - ` - - const colors = { - info: "background: linear-gradient(135deg, #3498db, #2980b9);", - success: "background: linear-gradient(135deg, #27ae60, #229954);", - warning: "background: linear-gradient(135deg, #f39c12, #e67e22);", - error: "background: linear-gradient(135deg, #e74c3c, #c0392b);", - } - - alertDiv.style.cssText += colors[type] || colors.info - - const style = document.createElement("style") - style.textContent = ` - @keyframes slideDown { - from { transform: translateX(-50%) translateY(-20px); opacity: 0; } - to { transform: translateX(-50%) translateY(0); opacity: 1; } - } - ` - document.head.appendChild(style) + alertDiv.className = `wplace-alert-base wplace-alert-${type}` alertDiv.textContent = message document.body.appendChild(alertDiv) @@ -2194,7 +2202,6 @@ alertDiv.style.animation = "slideDown 0.3s ease-out reverse" setTimeout(() => { document.body.removeChild(alertDiv) - document.head.removeChild(style) }, 300) }, 4000) }, @@ -2236,7 +2243,7 @@ // Use provided palette or derive from COLOR_MAP if (!palette || palette.length === 0) { palette = Object.values(CONFIG.COLOR_MAP) - .filter(c => c.rgb) + .filter(c => c.rgb) .map(c => [c.rgb.r, c.rgb.g, c.rgb.b]); } if (state.colorMatchingAlgorithm === 'legacy') { @@ -2290,17 +2297,17 @@ }, createImageUploader: () => - new Promise((resolve) => { - const input = document.createElement("input") - input.type = "file" - input.accept = "image/png,image/jpeg" - input.onchange = () => { - const fr = new FileReader() - fr.onload = () => resolve(fr.result) - fr.readAsDataURL(input.files[0]) - } - input.click() - }), + new Promise((resolve) => { + const input = document.createElement("input") + input.type = "file" + input.accept = "image/png,image/jpeg" + input.onchange = () => { + const fr = new FileReader() + fr.onload = () => resolve(fr.result) + fr.readAsDataURL(input.files[0]) + } + input.click() + }), createFileDownloader: (data, filename) => { const blob = new Blob([data], { type: "application/json" }) @@ -2315,30 +2322,30 @@ }, createFileUploader: () => - new Promise((resolve, reject) => { - const input = document.createElement("input") - input.type = "file" - input.accept = ".json" - input.onchange = (e) => { - const file = e.target.files[0] - if (file) { - const reader = new FileReader() - reader.onload = () => { - try { - const data = JSON.parse(reader.result) - resolve(data) - } catch (error) { - reject(new Error("Invalid JSON file")) - } + new Promise((resolve, reject) => { + const input = document.createElement("input") + input.type = "file" + input.accept = ".json" + input.onchange = (e) => { + const file = e.target.files[0] + if (file) { + const reader = new FileReader() + reader.onload = () => { + try { + const data = JSON.parse(reader.result) + resolve(data) + } catch (error) { + reject(new Error("Invalid JSON file")) } - reader.onerror = () => reject(new Error("File reading error")) - reader.readAsText(file) - } else { - reject(new Error("No file selected")) } + reader.onerror = () => reject(new Error("File reading error")) + reader.readAsText(file) + } else { + reject(new Error("No file selected")) } - input.click() - }), + } + input.click() + }), extractAvailableColors: () => { const colorElements = document.querySelectorAll('[id^="color-"]') @@ -2431,8 +2438,8 @@ markPixelPainted: (x, y, regionX = 0, regionY = 0) => { const actualX = x + regionX; const actualY = y + regionY; - - if (state.paintedMap && state.paintedMap[actualY] && + + if (state.paintedMap && state.paintedMap[actualY] && actualX >= 0 && actualX < state.paintedMap[actualY].length) { state.paintedMap[actualY][actualX] = true; } @@ -2441,8 +2448,8 @@ isPixelPainted: (x, y, regionX = 0, regionY = 0) => { const actualX = x + regionX; const actualY = y + regionY; - - if (state.paintedMap && state.paintedMap[actualY] && + + if (state.paintedMap && state.paintedMap[actualY] && actualX >= 0 && actualX < state.paintedMap[actualY].length) { return state.paintedMap[actualY][actualX]; } @@ -2454,28 +2461,28 @@ const now = Date.now(); const pixelsSinceLastSave = state.paintedPixels - state._lastSavePixelCount; const timeSinceLastSave = now - state._lastSaveTime; - + // Save conditions: // 1. Every 25 pixels (reduced from 50 for more frequent saves) // 2. At least 30 seconds since last save (prevent spam) // 3. Not already saving - return !state._saveInProgress && - pixelsSinceLastSave >= 25 && - timeSinceLastSave >= 30000; + return !state._saveInProgress && + pixelsSinceLastSave >= 25 && + timeSinceLastSave >= 30000; }, performSmartSave: () => { if (!Utils.shouldAutoSave()) return false; - + state._saveInProgress = true; const success = Utils.saveProgress(); - + if (success) { state._lastSavePixelCount = state.paintedPixels; state._lastSaveTime = Date.now(); console.log(`💾 Auto-saved at ${state.paintedPixels} pixels`); } - + state._saveInProgress = false; return success; }, @@ -2529,7 +2536,7 @@ migrateProgressToV2: (saved) => { if (!saved) return saved; const isV1 = !saved.version || saved.version === '1' || saved.version === '1.0' || saved.version === '1.1'; - if (!isV1) return saved; + if (!isV1) return saved; try { const migrated = { ...saved }; @@ -2574,7 +2581,7 @@ } }, - saveProgress: () => { + saveProgress: () => { try { // Pack painted map if available let paintedMapPacked = null; @@ -2603,13 +2610,13 @@ availableColors: state.availableColors, }, imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, + ? { + width: state.imageData.width, + height: state.imageData.height, + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } + : null, paintedMapPacked: paintedMapPacked, } @@ -2679,7 +2686,7 @@ const imageData = new ImageData(state.imageData.pixels, state.imageData.width, state.imageData.height); ctx.putImageData(imageData, 0, 0); const proc = new ImageProcessor(''); - proc.img = canvas; + proc.img = canvas; proc.canvas = canvas; proc.ctx = ctx; state.imageData.processor = proc; @@ -2703,7 +2710,7 @@ } }, - saveProgressToFile: () => { + saveProgressToFile: () => { try { // Pack painted map if available let paintedMapPacked = null; @@ -2732,13 +2739,13 @@ availableColors: state.availableColors, }, imageData: state.imageData - ? { - width: state.imageData.width, - height: state.imageData.height, - pixels: Array.from(state.imageData.pixels), - totalPixels: state.imageData.totalPixels, - } - : null, + ? { + width: state.imageData.width, + height: state.imageData.height, + pixels: Array.from(state.imageData.pixels), + totalPixels: state.imageData.totalPixels, + } + : null, paintedMapPacked: paintedMapPacked, } @@ -2751,7 +2758,7 @@ } }, - loadProgressFromFile: async () => { + loadProgressFromFile: async () => { try { const data = await Utils.createFileUploader() if (!data || !data.state) { @@ -2765,7 +2772,7 @@ } else { migrated = Utils.migrateProgressToV21(data) || data; } - const success = Utils.restoreProgress(migrated) + const success = Utils.restoreProgress(migrated) return success } catch (error) { console.error("Error loading from file:", error) @@ -2782,9 +2789,9 @@ try { // Recreate ImageBitmap from loaded pixel data const imageData = new ImageData( - state.imageData.pixels, - state.imageData.width, - state.imageData.height + state.imageData.pixels, + state.imageData.width, + state.imageData.height ); const canvas = new OffscreenCanvas(state.imageData.width, state.imageData.height); @@ -2933,96 +2940,96 @@ // Desktop Notification Manager const NotificationManager = { - pollTimer: null, - pollIntervalMs: 60_000, - icon() { - const link = document.querySelector("link[rel~='icon']"); - return link?.href || (location.origin + "/favicon.ico"); - }, - async requestPermission() { - if (!("Notification" in window)) { - Utils.showAlert("Notifications are not supported in this browser.", "warning"); - return "denied"; - } - if (Notification.permission === "granted") return "granted"; - try { - const perm = await Notification.requestPermission(); - return perm; - } catch { - return Notification.permission; - } - }, - canNotify() { - return state.notificationsEnabled && - typeof Notification !== "undefined" && - Notification.permission === "granted"; - }, - notify(title, body, tag = "wplace-charges", force = false) { - if (!this.canNotify()) return false; - if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; - try { - new Notification(title, { - body, - tag, - renotify: true, - icon: this.icon(), - badge: this.icon(), - silent: false, - }); - return true; - } catch { - // Graceful fallback - Utils.showAlert(body, "info"); - return false; - } - }, - resetEdgeTracking() { - state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold; - state._lastChargesNotifyAt = 0; - }, - maybeNotifyChargesReached(force = false) { - if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; - const reached = state.currentCharges >= state.cooldownChargeThreshold; - const now = Date.now(); - const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; - if (reached) { - const shouldEdge = state._lastChargesBelow || force; - const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; - if (shouldEdge || shouldRepeat) { - const msg = `Charges ready: ${Math.floor(state.currentCharges)} / ${state.maxCharges}. Threshold: ${state.cooldownChargeThreshold}.`; - this.notify("WPlace — Charges Ready", msg, "wplace-notify-charges"); - state._lastChargesNotifyAt = now; - } - state._lastChargesBelow = false; - } else { - state._lastChargesBelow = true; - } - }, - startPolling() { - this.stopPolling(); - if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; - // lightweight background polling - this.pollTimer = setInterval(async () => { - try { - const { charges, cooldown, max } = await WPlaceService.getCharges(); - state.currentCharges = Math.floor(charges); - state.cooldown = cooldown; - state.maxCharges = Math.max(1, Math.floor(max)); - this.maybeNotifyChargesReached(); - } catch { /* ignore */ } - }, this.pollIntervalMs); - }, - stopPolling() { - if (this.pollTimer) { - clearInterval(this.pollTimer); - this.pollTimer = null; - } - }, - syncFromState() { - this.resetEdgeTracking(); - if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); - else this.stopPolling(); - }, + pollTimer: null, + pollIntervalMs: 60_000, + icon() { + const link = document.querySelector("link[rel~='icon']"); + return link?.href || (location.origin + "/favicon.ico"); + }, + async requestPermission() { + if (!("Notification" in window)) { + Utils.showAlert("Notifications are not supported in this browser.", "warning"); + return "denied"; + } + if (Notification.permission === "granted") return "granted"; + try { + const perm = await Notification.requestPermission(); + return perm; + } catch { + return Notification.permission; + } + }, + canNotify() { + return state.notificationsEnabled && + typeof Notification !== "undefined" && + Notification.permission === "granted"; + }, + notify(title, body, tag = "wplace-charges", force = false) { + if (!this.canNotify()) return false; + if (!force && state.notifyOnlyWhenUnfocused && document.hasFocus()) return false; + try { + new Notification(title, { + body, + tag, + renotify: true, + icon: this.icon(), + badge: this.icon(), + silent: false, + }); + return true; + } catch { + // Graceful fallback + Utils.showAlert(body, "info"); + return false; + } + }, + resetEdgeTracking() { + state._lastChargesBelow = state.currentCharges < state.cooldownChargeThreshold; + state._lastChargesNotifyAt = 0; + }, + maybeNotifyChargesReached(force = false) { + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + const reached = state.currentCharges >= state.cooldownChargeThreshold; + const now = Date.now(); + const repeatMs = Math.max(1, Number(state.notificationIntervalMinutes || 5)) * 60_000; + if (reached) { + const shouldEdge = state._lastChargesBelow || force; + const shouldRepeat = now - (state._lastChargesNotifyAt || 0) >= repeatMs; + if (shouldEdge || shouldRepeat) { + const msg = `Charges ready: ${Math.floor(state.currentCharges)} / ${state.maxCharges}. Threshold: ${state.cooldownChargeThreshold}.`; + this.notify("WPlace — Charges Ready", msg, "wplace-notify-charges"); + state._lastChargesNotifyAt = now; + } + state._lastChargesBelow = false; + } else { + state._lastChargesBelow = true; + } + }, + startPolling() { + this.stopPolling(); + if (!state.notificationsEnabled || !state.notifyOnChargesReached) return; + // lightweight background polling + this.pollTimer = setInterval(async () => { + try { + const { charges, cooldown, max } = await WPlaceService.getCharges(); + state.currentCharges = Math.floor(charges); + state.cooldown = cooldown; + state.maxCharges = Math.max(1, Math.floor(max)); + this.maybeNotifyChargesReached(); + } catch { /* ignore */ } + }, this.pollIntervalMs); + }, + stopPolling() { + if (this.pollTimer) { + clearInterval(this.pollTimer); + this.pollTimer = null; + } + }, + syncFromState() { + this.resetEdgeTracking(); + if (state.notificationsEnabled && state.notifyOnChargesReached) this.startPolling(); + else this.stopPolling(); + }, }; // COLOR MATCHING FUNCTION - Optimized with caching @@ -3127,7 +3134,7 @@ // Don't re-fetch colors here, use what was captured when user clicked upload if (!state.availableColors || state.availableColors.length === 0) { // If no colors have been captured yet, show message - colorsContainer.innerHTML = '